From d9c2595d2141ce2f20db06b9ceef80294e4d1ab2 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Sun, 10 Dec 2023 16:35:13 -0500 Subject: [PATCH] Added gizmos to help align interactable joints This PR resolves request #584 by adding gizmos to align interactable hinges, sliders, and joysticks. This does cause minor breakage in that: - New origin nodes have been introduced where the ranges are visualized - Testing showed Y-Axis inversion on the slider and joystick --- VERSIONS.md | 3 + .../editor/gizmos/hinge_origin.gd | 172 ++++++++++++++ .../editor/gizmos/joystick_origin.gd | 212 ++++++++++++++++++ .../editor/gizmos/slider_origin.gd | 170 ++++++++++++++ .../interactables/interactable_hinge.gd | 110 +++++---- .../interactable_hinge_origin.gd | 51 +++++ .../interactables/interactable_joystick.gd | 201 ++++++++--------- .../interactable_joystick_origin.gd | 75 +++++++ .../interactables/interactable_slider.gd | 80 ++++--- .../interactable_slider_origin.gd | 51 +++++ addons/godot-xr-tools/plugin.gd | 37 ++- .../meshes/interactables/joystick_smooth.tscn | 4 +- .../meshes/interactables/joystick_snap.tscn | 4 +- .../meshes/interactables/joystick_zero.tscn | 4 +- assets/meshes/interactables/lever_smooth.tscn | 4 +- assets/meshes/interactables/lever_snap.tscn | 4 +- assets/meshes/interactables/lever_zero.tscn | 4 +- .../meshes/interactables/slider_smooth.tscn | 6 +- assets/meshes/interactables/slider_snap.tscn | 6 +- assets/meshes/interactables/slider_zero.tscn | 6 +- assets/meshes/interactables/wheel_smooth.tscn | 8 +- 21 files changed, 996 insertions(+), 216 deletions(-) create mode 100644 addons/godot-xr-tools/editor/gizmos/hinge_origin.gd create mode 100644 addons/godot-xr-tools/editor/gizmos/joystick_origin.gd create mode 100644 addons/godot-xr-tools/editor/gizmos/slider_origin.gd create mode 100644 addons/godot-xr-tools/interactables/interactable_hinge_origin.gd create mode 100644 addons/godot-xr-tools/interactables/interactable_joystick_origin.gd create mode 100644 addons/godot-xr-tools/interactables/interactable_slider_origin.gd diff --git a/VERSIONS.md b/VERSIONS.md index 6cf6d6fd..1d27bf7b 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -1,3 +1,6 @@ +# 4.4.0 +- **minor-breakage** Add gizmos to help align interactable hinges, sliders, and joysticks + # 4.3.0 - Upgraded project to Godot 4.1 as the new minimum version. - Added reporting of stage load errors. diff --git a/addons/godot-xr-tools/editor/gizmos/hinge_origin.gd b/addons/godot-xr-tools/editor/gizmos/hinge_origin.gd new file mode 100644 index 00000000..d9cead66 --- /dev/null +++ b/addons/godot-xr-tools/editor/gizmos/hinge_origin.gd @@ -0,0 +1,172 @@ +extends EditorNode3DGizmoPlugin + + +## Editor Gizmo for [XRToolsInteractableHingeOrigin] +## +## This editor gizmo helps align interactable hinge joints. + + +var undo_redo : EditorUndoRedoManager + + +func _init() -> void: + create_material("axis", Color(1, 1, 0)) + create_material("extent", Color(1, 1, 0, 0.3), false, true) + create_handle_material("handles") + + +func _get_gizmo_name() -> String: + return "InteractableHingeOrigin" + + +func _get_handle_name( + _gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> String: + # Return minimum or maximum handle name + return "Minimum" if p_handle_id == 0 else "Maximum" + + +func _get_handle_value( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> Variant: + # Return limit + return _get_limit(p_gizmo.get_node_3d(), p_handle_id) + + +func _set_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_camera : Camera3D, + p_screen_pos : Vector2) -> void: + # Get the hinge origin node + var origin : XRToolsInteractableHingeOrigin = p_gizmo.get_node_3d() + var origin_pos := origin.global_position + var origin_dir := origin.global_transform.basis.x + + # Construct the plane + var plane := Plane(origin_dir, origin_pos) + + # Find the intersection between the ray and the plane + var v_intersect = plane.intersects_ray( + p_camera.global_position, + p_camera.project_ray_normal(p_screen_pos)) + if not v_intersect: + return + + # Find the local position and the delta in angle + var local := origin.to_local(v_intersect) + var old_local := _get_limit_pos(origin, p_handle_id) + var delta := rad_to_deg(old_local.signed_angle_to(local, Vector3.LEFT)) + + # Adjust the current limit + var limit := _get_limit(origin, p_handle_id) + limit = snappedf(limit + delta, 5) + _set_limit(origin, p_handle_id, limit) + + +func _commit_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_restore : Variant, + p_cancel : bool) -> void: + # Get the slider origin node + var origin : XRToolsInteractableHingeOrigin = p_gizmo.get_node_3d() + + # If canceling then restore limit + if p_cancel: + _set_limit(origin, p_handle_id, p_restore) + return + + # Commit the handle change + match p_handle_id: + 0: + undo_redo.create_action("Set interactable hinge limit_minimum") + undo_redo.add_do_method(origin, "set_limit_minimum", origin.limit_minimum) + undo_redo.add_undo_method(origin, "set_limit_minimum", p_restore) + undo_redo.commit_action() + 1: + undo_redo.create_action("Set interactable hinge limit_maximum") + undo_redo.add_do_method(origin, "set_limit_maximum", origin.limit_maximum) + undo_redo.add_undo_method(origin, "set_limit_maximum", p_restore) + undo_redo.commit_action() + + +func _has_gizmo(p_node : Node3D) -> bool: + return p_node is XRToolsInteractableHingeOrigin + + +func _redraw(p_gizmo : EditorNode3DGizmo) -> void: + # Clear the current gizmo contents + p_gizmo.clear() + + # Get the hinge origin and its extents + var origin : XRToolsInteractableHingeOrigin = p_gizmo.get_node_3d() + var min_angle := deg_to_rad(origin.limit_minimum) + var max_angle := deg_to_rad(origin.limit_maximum) + + # Draw the lines (for the axis) + var lines := PackedVector3Array() + lines.push_back(Vector3(-0.2, 0, 0)) + lines.push_back(Vector3(0.2, 0, 0)) + p_gizmo.add_lines(lines, get_material("axis", p_gizmo)) + + # Construct an immediate mesh for the extent + var mesh := ImmediateMesh.new() + mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP) + var steps := int(abs(max_angle - min_angle) / 0.1) + for i in steps + 1: + if i != 0: + mesh.surface_add_vertex(Vector3.ZERO) + var angle := lerpf(min_angle, max_angle, i / float(steps)) + mesh.surface_add_vertex( + Vector3(0, sin(angle) * 0.2, cos(angle) * 0.2)) + mesh.surface_end() + + # Draw the extent mesh + p_gizmo.add_mesh(mesh, get_material("extent", p_gizmo)) + + # Add the handles + var handles := PackedVector3Array() + handles.push_back(_get_limit_pos(origin, 0)) + handles.push_back(_get_limit_pos(origin, 1)) + p_gizmo.add_handles(handles, get_material("handles", p_gizmo), []) + + +# Get the limit of a hinge by handle +func _get_limit( + p_origin : XRToolsInteractableHingeOrigin, + p_handle_id : int) -> float: + # Read the limit + match p_handle_id: + 0: + return p_origin.limit_minimum + 1: + return p_origin.limit_maximum + _: + return 0.0 + + +# Get the limit position of a slider by handle +func _get_limit_pos( + p_origin : XRToolsInteractableHingeOrigin, + p_handle_id : int) -> Vector3: + # Return the limit position + var angle := deg_to_rad(_get_limit(p_origin, p_handle_id)) + return Vector3(0, sin(angle) * 0.2, cos(angle) * 0.2) + + +# Set the limit of a hinge by handle +func _set_limit( + p_origin : XRToolsInteractableHingeOrigin, + p_handle_id : int, + p_limit : float) -> void: + # Apply the limit + match p_handle_id: + 0: + p_origin.limit_minimum = p_limit + 1: + p_origin.limit_maximum = p_limit diff --git a/addons/godot-xr-tools/editor/gizmos/joystick_origin.gd b/addons/godot-xr-tools/editor/gizmos/joystick_origin.gd new file mode 100644 index 00000000..03952014 --- /dev/null +++ b/addons/godot-xr-tools/editor/gizmos/joystick_origin.gd @@ -0,0 +1,212 @@ +extends EditorNode3DGizmoPlugin + + +## Editor Gizmo for [XRToolsInteractableJoystickOrigin] +## +## This editor gizmo helps align interactable joystick joints. + + +var undo_redo : EditorUndoRedoManager + + +func _init() -> void: + create_material("extent", Color(1, 1, 0, 0.3), false, true) + create_handle_material("handles") + + +func _get_gizmo_name() -> String: + return "InteractableJoystickOrigin" + + +func _get_handle_name( + _gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> String: + # Return handle name + if p_handle_id == 0: return "Minimum X" + if p_handle_id == 1: return "Maximum X" + if p_handle_id == 2: return "Minimum Y" + return "Maximum Y" + + +func _get_handle_value( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> Variant: + # Return limit + return _get_limit(p_gizmo.get_node_3d(), p_handle_id) + + +func _set_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_camera : Camera3D, + p_screen_pos : Vector2) -> void: + # Get the hinge origin node + var origin : XRToolsInteractableJoystickOrigin = p_gizmo.get_node_3d() + var origin_pos := origin.global_position + var origin_dir_x := origin.global_transform.basis.y + var origin_dir_y := -origin.global_transform.basis.x + + # Construct the plane + var plane : Plane + if p_handle_id < 2: + plane = Plane(origin_dir_x, origin_pos) + else: + plane = Plane(origin_dir_y, origin_pos) + + # Find the intersection between the ray and the plane + var v_intersect = plane.intersects_ray( + p_camera.global_position, + p_camera.project_ray_normal(p_screen_pos)) + if not v_intersect: + return + + # Find the local position + var local := origin.to_local(v_intersect) + var old_local := _get_limit_pos(origin, p_handle_id) + var delta : float + if p_handle_id < 2: + delta = rad_to_deg(old_local.signed_angle_to(local, Vector3.UP)) + else: + delta = rad_to_deg(old_local.signed_angle_to(local, Vector3.LEFT)) + + # Adjust the current limit + var limit := _get_limit(origin, p_handle_id) + limit = snappedf(limit + delta, 5) + _set_limit(origin, p_handle_id, limit) + + +func _commit_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_restore : Variant, + p_cancel : bool) -> void: + # Get the slider origin node + var origin : XRToolsInteractableJoystickOrigin = p_gizmo.get_node_3d() + + # If canceling then restore limit + if p_cancel: + _set_limit(origin, p_handle_id, p_restore) + return + + # Commit the handle change + match p_handle_id: + 0: + undo_redo.create_action("Set interactable joystick limit_x_minimum") + undo_redo.add_do_method(origin, "set_limit_x_minimum", origin.limit_x_minimum) + undo_redo.add_undo_method(origin, "set_limit_x_minimum", p_restore) + undo_redo.commit_action() + 1: + undo_redo.create_action("Set interactable joystick limit_x_maximum") + undo_redo.add_do_method(origin, "set_limit_x_maximum", origin.limit_x_maximum) + undo_redo.add_undo_method(origin, "set_limit_x_maximum", p_restore) + undo_redo.commit_action() + 2: + undo_redo.create_action("Set interactable joystick limit_y_minimum") + undo_redo.add_do_method(origin, "set_limit_y_minimum", origin.limit_y_minimum) + undo_redo.add_undo_method(origin, "set_limit_y_minimum", p_restore) + undo_redo.commit_action() + 3: + undo_redo.create_action("Set interactable joystick limit_y_maximum") + undo_redo.add_do_method(origin, "set_limit_y_maximum", origin.limit_y_maximum) + undo_redo.add_undo_method(origin, "set_limit_y_maximum", p_restore) + undo_redo.commit_action() + + +func _has_gizmo(p_node : Node3D) -> bool: + return p_node is XRToolsInteractableJoystickOrigin + + +func _redraw(p_gizmo : EditorNode3DGizmo) -> void: + # Clear the current gizmo contents + p_gizmo.clear() + + # Get the joystick origin and its extents + var origin : XRToolsInteractableJoystickOrigin = p_gizmo.get_node_3d() + var min_x_angle := deg_to_rad(origin.limit_x_minimum) + var max_x_angle := deg_to_rad(origin.limit_x_maximum) + var min_y_angle := deg_to_rad(origin.limit_y_minimum) + var max_y_angle := deg_to_rad(origin.limit_y_maximum) + + # Construct an immediate mesh for the extent + var mesh := ImmediateMesh.new() + mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP) + for i in 33: + if i != 0: + mesh.surface_add_vertex(Vector3.ZERO) + var angle := lerpf(min_x_angle, max_x_angle, i / 32.0) + mesh.surface_add_vertex( + Vector3(sin(angle) * 0.2, 0, cos(angle) * 0.2)) + mesh.surface_end() + mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLE_STRIP) + for i in 33: + if i != 0: + mesh.surface_add_vertex(Vector3.ZERO) + var angle := lerpf(min_y_angle, max_y_angle, i / 32.0) + mesh.surface_add_vertex( + Vector3(0, sin(angle) * 0.2, cos(angle) * 0.2)) + mesh.surface_end() + + # Draw the extent mesh + p_gizmo.add_mesh(mesh, get_material("extent", p_gizmo)) + + # Add the handles + var handles := PackedVector3Array() + handles.push_back(_get_limit_pos(origin, 0)) + handles.push_back(_get_limit_pos(origin, 1)) + handles.push_back(_get_limit_pos(origin, 2)) + handles.push_back(_get_limit_pos(origin, 3)) + p_gizmo.add_handles(handles, get_material("handles", p_gizmo), []) + + +# Get the limit of a joystick by handle +func _get_limit( + p_origin : XRToolsInteractableJoystickOrigin, + p_handle_id : int) -> float: + # Read the limit + match p_handle_id: + 0: + return p_origin.limit_x_minimum + 1: + return p_origin.limit_x_maximum + 2: + return p_origin.limit_y_minimum + 3: + return p_origin.limit_y_maximum + _: + return 0.0 + + +# Get the limit position of a slider by handle +func _get_limit_pos( + p_origin : XRToolsInteractableJoystickOrigin, + p_handle_id : int) -> Vector3: + # Return the limit position + var angle := deg_to_rad(_get_limit(p_origin, p_handle_id)) + match p_handle_id: + 0, 1: + return Vector3(sin(angle) * 0.2, 0, cos(angle) * 0.2) + 2, 3: + return Vector3(0, sin(angle) * 0.2, cos(angle) * 0.2) + _: + return Vector3.ZERO + + +# Set the limit of a joystick by handle +func _set_limit( + p_origin : XRToolsInteractableJoystickOrigin, + p_handle_id : int, + p_limit : float) -> void: + # Apply the limit + match p_handle_id: + 0: + p_origin.limit_x_minimum = p_limit + 1: + p_origin.limit_x_maximum = p_limit + 2: + p_origin.limit_y_minimum = p_limit + 3: + p_origin.limit_y_maximum = p_limit diff --git a/addons/godot-xr-tools/editor/gizmos/slider_origin.gd b/addons/godot-xr-tools/editor/gizmos/slider_origin.gd new file mode 100644 index 00000000..4fc7ddf9 --- /dev/null +++ b/addons/godot-xr-tools/editor/gizmos/slider_origin.gd @@ -0,0 +1,170 @@ +extends EditorNode3DGizmoPlugin + + +## Editor Gizmo for [XRToolsInteractableSliderOrigin] +## +## This editor gizmo helps align interactable slider joints. + + +var undo_redo : EditorUndoRedoManager + + +func _init() -> void: + create_material("extent", Color(1, 1, 0, 0.3), false, true) + create_handle_material("handles") + + +func _get_gizmo_name() -> String: + return "InteractableSliderOrigin" + + +func _get_handle_name( + _gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> String: + # Return minimum or maximum handle name + return "Minimum" if p_handle_id == 0 else "Maximum" + + +func _get_handle_value( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool) -> Variant: + # Return limit + return _get_limit(p_gizmo.get_node_3d(), p_handle_id) + + +func _set_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_camera : Camera3D, + p_screen_pos : Vector2) -> void: + # Get the slider origin node + var origin : XRToolsInteractableSliderOrigin = p_gizmo.get_node_3d() + var origin_pos := origin.global_position + var origin_dir := origin.global_transform.basis.x + + # Build the normal vector pointing to the camera but orthoginal to the slide + var to_camera := p_camera.global_position - origin_pos + to_camera = to_camera.slide(origin_dir).normalized() + + # Construct the plane + var plane := Plane(to_camera, origin_pos) + + # Find the intersection between the ray and the plane + var v_intersect = plane.intersects_ray( + p_camera.global_position, + p_camera.project_ray_normal(p_screen_pos)) + if not v_intersect: + return + + # Set the limit + var local := origin.to_local(v_intersect) + var limit := snappedf(local.x, 0.01) + _set_limit(origin, p_handle_id, limit) + + +func _commit_handle( + p_gizmo : EditorNode3DGizmo, + p_handle_id : int, + _secondary : bool, + p_restore : Variant, + p_cancel : bool) -> void: + # Get the slider origin node + var origin : XRToolsInteractableSliderOrigin = p_gizmo.get_node_3d() + + # If canceling then restore limit + if p_cancel: + _set_limit(origin, p_handle_id, p_restore) + return + + # Commit the handle change + match p_handle_id: + 0: + undo_redo.create_action("Set interactable slider limit_minimum") + undo_redo.add_do_method(origin, "set_limit_minimum", origin.limit_minimum) + undo_redo.add_undo_method(origin, "set_limit_minimum", p_restore) + undo_redo.commit_action() + 1: + undo_redo.create_action("Set interactable slider limit_maximum") + undo_redo.add_do_method(origin, "set_limit_maximum", origin.limit_maximum) + undo_redo.add_undo_method(origin, "set_limit_maximum", p_restore) + undo_redo.commit_action() + + +func _has_gizmo(p_node : Node3D) -> bool: + return p_node is XRToolsInteractableSliderOrigin + + +func _redraw(p_gizmo : EditorNode3DGizmo) -> void: + # Clear the current gizmo contents + p_gizmo.clear() + + # Get the slider origin and its extents + var origin : XRToolsInteractableSliderOrigin = p_gizmo.get_node_3d() + var min_x := origin.limit_minimum + var max_x := origin.limit_maximum + var mid_x := (min_x + max_x) * 0.5 + var width := (max_x - min_x) * 0.1 + + # Construct an immediate mesh for the extent + var mesh := ImmediateMesh.new() + mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES) + mesh.surface_add_vertex(Vector3(mid_x, -width, 0.0)) + mesh.surface_add_vertex(Vector3(min_x, 0.0, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, width, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, -width, 0.0)) + mesh.surface_add_vertex(Vector3(max_x, 0.0, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, width, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, 0.0, -width)) + mesh.surface_add_vertex(Vector3(min_x, 0.0, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, 0.0, width)) + mesh.surface_add_vertex(Vector3(mid_x, 0.0, -width)) + mesh.surface_add_vertex(Vector3(max_x, 0.0, 0.0)) + mesh.surface_add_vertex(Vector3(mid_x, 0.0, width)) + mesh.surface_end() + + # Draw the extent mesh + p_gizmo.add_mesh(mesh, get_material("extent", p_gizmo)) + + # Add the handles + var handles := PackedVector3Array() + handles.push_back(_get_limit_pos(origin, 0)) + handles.push_back(_get_limit_pos(origin, 1)) + p_gizmo.add_handles(handles, get_material("handles", p_gizmo), []) + + +# Get the limit of a slider by handle +func _get_limit( + p_origin : XRToolsInteractableSliderOrigin, + p_handle_id : int) -> float: + # Read the limit + match p_handle_id: + 0: + return p_origin.limit_minimum + 1: + return p_origin.limit_maximum + _: + return 0.0 + + +# Get the limit position of a slider by handle +func _get_limit_pos( + p_origin : XRToolsInteractableSliderOrigin, + p_handle_id : int) -> Vector3: + # Return the limit position + return Vector3(_get_limit(p_origin, p_handle_id), 0, 0) + + +# Set the limit of a slider by handle +func _set_limit( + p_origin : XRToolsInteractableSliderOrigin, + p_handle_id : int, + p_limit : float) -> void: + # Apply the limit + match p_handle_id: + 0: + p_origin.limit_minimum = p_limit + 1: + p_origin.limit_maximum = p_limit diff --git a/addons/godot-xr-tools/interactables/interactable_hinge.gd b/addons/godot-xr-tools/interactables/interactable_hinge.gd index ce774a53..21441943 100644 --- a/addons/godot-xr-tools/interactables/interactable_hinge.gd +++ b/addons/godot-xr-tools/interactables/interactable_hinge.gd @@ -19,12 +19,6 @@ extends XRToolsInteractableHandleDriven signal hinge_moved(angle) -## Hinge minimum limit -@export var hinge_limit_min : float = -45.0: set = _set_hinge_limit_min - -## Hinge maximum limit -@export var hinge_limit_max : float = 45.0: set = _set_hinge_limit_max - ## Hinge step size (zero for no steps) @export var hinge_steps : float = 0.0: set = _set_hinge_steps @@ -38,12 +32,8 @@ signal hinge_moved(angle) @export var default_on_release : bool = false -# Hinge values in radians -@onready var _hinge_limit_min_rad : float = deg_to_rad(hinge_limit_min) -@onready var _hinge_limit_max_rad : float = deg_to_rad(hinge_limit_max) -@onready var _hinge_steps_rad : float = deg_to_rad(hinge_steps) -@onready var _hinge_position_rad : float = deg_to_rad(hinge_position) -@onready var _default_position_rad : float = deg_to_rad(default_position) +## Hinge origin +var _origin : XRToolsInteractableHingeOrigin # Add support for is_xr_class on XRTools classes @@ -56,9 +46,12 @@ func _ready(): # In Godot 4 we must now manually call our super class ready function super() + # Get the parent origin + _origin = get_parent() + # Set the initial position to match the initial hinge position value transform = Transform3D( - Basis.from_euler(Vector3(_hinge_position_rad, 0, 0)), + Basis.from_euler(Vector3(-deg_to_rad(hinge_position), 0, 0)), Vector3.ZERO ) @@ -67,6 +60,18 @@ func _ready(): push_error("Cannot connect hinge released signal") +# Check for configuration warnings +func _get_configuration_warnings() -> PackedStringArray: + var ret := PackedStringArray() + + # Check for origin + var origin := get_parent() as XRToolsInteractableHingeOrigin + if not origin: + ret.append("Must be a child of an XRToolsInteractableHingeOrigin") + + return ret + + # Called every frame when one or more handles are held by the player func _process(_delta: float) -> void: # Get the total handle angular offsets @@ -75,82 +80,75 @@ func _process(_delta: float) -> void: var handle := item as XRToolsInteractableHandle var to_handle: Vector3 = handle.global_transform.origin * global_transform var to_handle_origin: Vector3 = handle.handle_origin.global_transform.origin * global_transform - to_handle.x = 0.0 - to_handle_origin.x = 0.0 - offset_sum += to_handle_origin.signed_angle_to(to_handle, Vector3.RIGHT) + to_handle = to_handle.slide(Vector3.LEFT) + to_handle_origin = to_handle_origin.slide(Vector3.LEFT) + offset_sum += rad_to_deg(to_handle_origin.signed_angle_to(to_handle, Vector3.LEFT)) # Average the angular offsets var offset := offset_sum / grabbed_handles.size() # Move the hinge by the requested offset - move_hinge(_hinge_position_rad + offset) + move_hinge(hinge_position + offset) # Move the hinge to the specified position -func move_hinge(position: float) -> void: +func move_hinge(p_position: float) -> void: # Do the hinge move - position = _do_move_hinge(position) - if position == _hinge_position_rad: + p_position = _do_move_hinge(p_position) + if p_position == hinge_position: return # Update the current positon - _hinge_position_rad = position - hinge_position = rad_to_deg(position) + hinge_position = p_position # Emit the moved signal - emit_signal("hinge_moved", hinge_position) + emit_signal("hinge_moved", p_position) # Handle release of hinge func _on_hinge_released(_interactable: XRToolsInteractableHinge): if default_on_release: - move_hinge(_default_position_rad) - - -# Called when hinge_limit_min is set externally -func _set_hinge_limit_min(value: float) -> void: - hinge_limit_min = value - _hinge_limit_min_rad = deg_to_rad(value) - - -# Called when hinge_limit_max is set externally -func _set_hinge_limit_max(value: float) -> void: - hinge_limit_max = value - _hinge_limit_max_rad = deg_to_rad(value) + move_hinge(default_position) # Called when hinge_steps is set externally -func _set_hinge_steps(value: float) -> void: - hinge_steps = value - _hinge_steps_rad = deg_to_rad(value) +func _set_hinge_steps(p_hinge_steps: float) -> void: + hinge_steps = maxf(0.0, p_hinge_steps) # Called when hinge_position is set externally -func _set_hinge_position(value: float) -> void: - var position := deg_to_rad(value) - position = _do_move_hinge(position) - hinge_position = rad_to_deg(position) - _hinge_position_rad = position +func _set_hinge_position(p_hinge_position: float) -> void: + hinge_position = _do_move_hinge(p_hinge_position) # Called when default_position is set externally -func _set_default_position(value: float) -> void: - default_position = value - _default_position_rad = deg_to_rad(value) +func _set_default_position(p_default_position: float) -> void: + default_position = _clamp_position(p_default_position) # Do the hinge move -func _do_move_hinge(position: float) -> float: +func _do_move_hinge(p_position: float) -> float: + # Clamp the position + p_position = _clamp_position(p_position) + + # Move if necessary + if p_position != hinge_position: + transform.basis = Basis.from_euler( + Vector3(-deg_to_rad(p_position), 0.0, 0.0)) + + # Return the updated position + return p_position + + +# Clamp the position based on the hinge rules +func _clamp_position(p_position : float) -> float: # Apply hinge step-quantization - if _hinge_steps_rad: - position = round(position / _hinge_steps_rad) * _hinge_steps_rad + if hinge_steps: + p_position = snappedf(p_position, hinge_steps) # Apply hinge limits - position = clamp(position, _hinge_limit_min_rad, _hinge_limit_max_rad) - - # Move if necessary - if position != _hinge_position_rad: - transform.basis = Basis.from_euler(Vector3(position, 0.0, 0.0)) + if _origin: + p_position = clamp(p_position, _origin.limit_minimum, _origin.limit_maximum) # Return the updated position - return position + return p_position diff --git a/addons/godot-xr-tools/interactables/interactable_hinge_origin.gd b/addons/godot-xr-tools/interactables/interactable_hinge_origin.gd new file mode 100644 index 00000000..4ce0e86b --- /dev/null +++ b/addons/godot-xr-tools/interactables/interactable_hinge_origin.gd @@ -0,0 +1,51 @@ +@tool +class_name XRToolsInteractableHingeOrigin +extends Node3D + + +## XR Tools Interactable Hinge Origin script +## +## The interactable hinge origin is parent of an [XRToolsInteractableHinge] node +## and defines the extent of travel the hinge can move throught. + + +## Hinge minimum limit (degrees) +@export var limit_minimum : float = -45.0: set = set_limit_minimum + +## Hinge maximum limit (degrees) +@export var limit_maximum : float = 45.0: set = set_limit_maximum + + +# Add support for is_xr_class on XRTools classes +func is_xr_class(name : String) -> bool: + return name == "XRToolsInteractableHingeOrigin" + + +# Check for configuration warnings +func _get_configuration_warnings() -> PackedStringArray: + var ret := PackedStringArray() + + # Check for invalid limits + if limit_maximum <= limit_minimum: + ret.append("Invalid hinge range") + + # Check for a hinge child + if get_children().all( + func(n : Node) : return not n is XRToolsInteractableHinge): + ret.append("Missing XRToolsInteractableHinge child") + + return ret + + +# Handle setting the minimum limit +func set_limit_minimum(p_limit_minimum : float) -> void: + limit_minimum = p_limit_minimum + update_configuration_warnings() + update_gizmos() + + +# Handle setting the maximum limit +func set_limit_maximum(p_limit_maximum : float) -> void: + limit_maximum = p_limit_maximum + update_configuration_warnings() + update_gizmos() diff --git a/addons/godot-xr-tools/interactables/interactable_joystick.gd b/addons/godot-xr-tools/interactables/interactable_joystick.gd index c33047fa..c515cff0 100644 --- a/addons/godot-xr-tools/interactables/interactable_joystick.gd +++ b/addons/godot-xr-tools/interactables/interactable_joystick.gd @@ -19,58 +19,30 @@ extends XRToolsInteractableHandleDriven signal joystick_moved(x_angle, y_angle) -## Constant for flattening a vector horizontally (X/Z only) -const VECTOR_XZ := Vector3(1.0, 0.0, 1.0) - -## Constant for flattening a vector vertically (Y/Z only) -const VECTOR_YZ := Vector3(0.0, 1.0, 1.0) - - -## Joystick X minimum limit -@export var joystick_x_limit_min : float = -45.0: set = _set_joystick_x_limit_min - -## Joystick X maximum limit -@export var joystick_x_limit_max : float = 45.0: set = _set_joystick_x_limit_max - -## Joystick Y minimum limit -@export var joystick_y_limit_min : float = -45.0: set = _set_joystick_y_limit_min - -## Joystick Y maximum limit -@export var joystick_y_limit_max : float = 45.0: set = _set_joystick_y_limit_max - ## Joystick X step size (zero for no steps) -@export var joystick_x_steps : float = 0.0: set = _set_joystick_x_steps +@export var joystick_x_steps : float = 0.0 : set = _set_joystick_x_steps ## Joystick Y step size (zero for no steps) -@export var joystick_y_steps : float = 0.0: set = _set_joystick_y_steps +@export var joystick_y_steps : float = 0.0 : set = _set_joystick_y_steps ## Joystick X position -@export var joystick_x_position : float = 0.0: set = _set_joystick_x_position +@export var joystick_x_position : float = 0.0 : set = _set_joystick_x_position ## Joystick Y position -@export var joystick_y_position : float = 0.0: set = _set_joystick_y_position +@export var joystick_y_position : float = 0.0 : set = _set_joystick_y_position ## Default X position -@export var default_x_position : float = 0.0: set = _set_default_x_position +@export var default_x_position : float = 0.0 : set = _set_default_x_position ## Default Y position -@export var default_y_position : float = 0.0: set = _set_default_y_position +@export var default_y_position : float = 0.0 : set = _set_default_y_position ## If true, the joystick moves to the default position when released @export var default_on_release : bool = false -# Joystick values in radians -@onready var _joystick_x_limit_min_rad : float = deg_to_rad(joystick_x_limit_min) -@onready var _joystick_x_limit_max_rad : float = deg_to_rad(joystick_x_limit_max) -@onready var _joystick_y_limit_min_rad : float = deg_to_rad(joystick_y_limit_min) -@onready var _joystick_y_limit_max_rad : float = deg_to_rad(joystick_y_limit_max) -@onready var _joystick_x_steps_rad : float = deg_to_rad(joystick_x_steps) -@onready var _joystick_y_steps_rad : float = deg_to_rad(joystick_y_steps) -@onready var _joystick_x_position_rad : float = deg_to_rad(joystick_x_position) -@onready var _joystick_y_position_rad : float = deg_to_rad(joystick_y_position) -@onready var _default_x_position_rad : float = deg_to_rad(default_x_position) -@onready var _default_y_position_rad : float = deg_to_rad(default_y_position) +## Joystick origin +var _origin : XRToolsInteractableJoystickOrigin # Add support for is_xr_class on XRTools classes @@ -83,9 +55,16 @@ func _ready(): # In Godot 4 we must now manually call our super class ready function super() + # Get the parent origin + _origin = get_parent() + # Set the initial position to match the initial joystick position value transform = Transform3D( - Basis.from_euler(Vector3(_joystick_y_position_rad, _joystick_x_position_rad, 0)), + Basis.from_euler( + Vector3( + -deg_to_rad(joystick_y_position), + deg_to_rad(joystick_x_position), + 0.0)), Vector3.ZERO) # Connect signals @@ -94,7 +73,7 @@ func _ready(): # Called every frame when one or more handles are held by the player -func _process(_delta: float) -> void: +func _process(_delta : float) -> void: # Do not process in the editor if Engine.is_editor_hint(): return @@ -111,13 +90,15 @@ func _process(_delta: float) -> void: var to_handle: Vector3 = handle.global_transform.origin * global_transform var to_handle_origin: Vector3 = handle.handle_origin.global_transform.origin * global_transform - var to_handle_x := to_handle * VECTOR_XZ - var to_handle_origin_x := to_handle_origin * VECTOR_XZ - offset_x_sum += to_handle_origin_x.signed_angle_to(to_handle_x, Vector3.UP) + var to_handle_x := to_handle.slide(Vector3.UP) + var to_handle_origin_x := to_handle_origin.slide(Vector3.UP) + offset_x_sum += rad_to_deg( + to_handle_origin_x.signed_angle_to(to_handle_x, Vector3.UP)) - var to_handle_y := to_handle * VECTOR_YZ - var to_handle_origin_y := to_handle_origin * VECTOR_YZ - offset_y_sum += to_handle_origin_y.signed_angle_to(to_handle_y, Vector3.RIGHT) + var to_handle_y := to_handle.slide(Vector3.LEFT) + var to_handle_origin_y := to_handle_origin.slide(Vector3.LEFT) + offset_y_sum += rad_to_deg( + to_handle_origin_y.signed_angle_to(to_handle_y, Vector3.LEFT)) # Average the angular offsets var offset_x := offset_x_sum / grabbed_handles.size() @@ -125,112 +106,106 @@ func _process(_delta: float) -> void: # Move the joystick by the requested offset move_joystick( - _joystick_x_position_rad + offset_x, - _joystick_y_position_rad + offset_y) + joystick_x_position + offset_x, + joystick_y_position + offset_y) # Move the joystick to the specified position -func move_joystick(position_x: float, position_y: float) -> void: +func move_joystick(pos_x: float, pos_y: float) -> void: # Do the move - var position := _do_move_joystick(Vector2(position_x, position_y)) - if position.x == _joystick_x_position_rad and position.y == _joystick_y_position_rad: + var pos := _do_move_joystick(Vector2(pos_x, pos_y)) + if pos.x == joystick_x_position and pos.y == joystick_y_position: return # Update the current positon - _joystick_x_position_rad = position.x - _joystick_y_position_rad = position.y - joystick_x_position = rad_to_deg(position.x) - joystick_y_position = rad_to_deg(position.y) + joystick_x_position = pos.x + joystick_y_position = pos.y # Emit the joystick signal - emit_signal("joystick_moved", joystick_x_position, joystick_y_position) + emit_signal("joystick_moved", pos.x, pos.y) # Handle release of joystick func _on_joystick_released(_interactable: XRToolsInteractableJoystick): if default_on_release: - move_joystick(_default_x_position_rad, _default_y_position_rad) + move_joystick(default_x_position, default_y_position) -# Called when joystick_x_limit_min is set externally -func _set_joystick_x_limit_min(value: float) -> void: - joystick_x_limit_min = value - _joystick_x_limit_min_rad = deg_to_rad(value) +# Called when joystick_x_steps is set +func _set_joystick_x_steps(p_joystick_x_steps : float) -> void: + joystick_x_steps = maxf(0.0, p_joystick_x_steps) -# Called when joystick_y_limit_min is set externally -func _set_joystick_y_limit_min(value: float) -> void: - joystick_y_limit_min = value - _joystick_y_limit_min_rad = deg_to_rad(value) +# Called when joystick_y_steps is set +func _set_joystick_y_steps(p_joystick_y_steps : float) -> void: + joystick_y_steps = maxf(0.0, p_joystick_y_steps) -# Called when joystick_x_limit_max is set externally -func _set_joystick_x_limit_max(value: float) -> void: - joystick_x_limit_max = value - _joystick_x_limit_max_rad = deg_to_rad(value) +# Called when joystick_x_position is set +func _set_joystick_x_position(p_joystick_x_position : float) -> void: + var pos := Vector2(p_joystick_x_position, joystick_y_position) + pos = _do_move_joystick(pos) + joystick_x_position = pos.x -# Called when joystick_y_limit_max is set externally -func _set_joystick_y_limit_max(value: float) -> void: - joystick_y_limit_max = value - _joystick_y_limit_max_rad = deg_to_rad(value) +# Called when joystick_y_position is set +func _set_joystick_y_position(p_joystick_y_position : float) -> void: + var pos := Vector2(joystick_x_position, p_joystick_y_position) + pos = _do_move_joystick(pos) + joystick_y_position = pos.y -# Called when joystick_x_steps is set externally -func _set_joystick_x_steps(value: float) -> void: - joystick_x_steps = value - _joystick_x_steps_rad = deg_to_rad(value) +# Called when default_x_position is set +func _set_default_x_position(p_default_x_position : float) -> void: + default_x_position = _clamp_x_position(p_default_x_position) -# Called when joystick_y_steps is set externally -func _set_joystick_y_steps(value: float) -> void: - joystick_y_steps = value - _joystick_y_steps_rad = deg_to_rad(value) +# Called when default_y_position is set +func _set_default_y_position(p_default_y_position : float) -> void: + default_y_position = _clamp_y_position(p_default_y_position) -# Called when joystick_x_position is set externally -func _set_joystick_x_position(value: float) -> void: - var position := Vector2(deg_to_rad(value), _joystick_y_position_rad) - position = _do_move_joystick(position) - joystick_x_position = rad_to_deg(position.x) - _joystick_x_position_rad = position.x +# Do the joystick move +func _do_move_joystick(p_pos: Vector2) -> Vector2: + # Clamp position + p_pos.x = _clamp_x_position(p_pos.x) + p_pos.y = _clamp_y_position(p_pos.y) + # Move if necessary + if p_pos.x != joystick_x_position or p_pos.y != joystick_y_position: + transform.basis = Basis.from_euler( + Vector3( + -deg_to_rad(p_pos.y), + deg_to_rad(p_pos.x), + 0.0)) -# Called when joystick_y_position is set externally -func _set_joystick_y_position(value: float) -> void: - var position := Vector2(_joystick_x_position_rad, deg_to_rad(value)) - position = _do_move_joystick(position) - joystick_y_position = rad_to_deg(position.y) - _joystick_y_position_rad = position.y + # Return the updated position + return p_pos -# Called when default_x_position is set externally -func _set_default_x_position(value: float) -> void: - default_x_position = value - _default_x_position_rad = deg_to_rad(value) +# Clamp the X position based on the hinge rules +func _clamp_x_position(p_x_position : float) -> float: + # Apply joystick step-quantization + if joystick_x_steps: + p_x_position = snappedf(p_x_position, joystick_x_steps) + # Apply joystick limits + if _origin: + p_x_position = clamp(p_x_position, _origin.limit_x_minimum, _origin.limit_x_maximum) -# Called when default_y_position is set externally -func _set_default_y_position(value: float) -> void: - default_y_position = value - _default_y_position_rad = deg_to_rad(value) + # Return the updated x position + return p_x_position -# Do the joystick move -func _do_move_joystick(position: Vector2) -> Vector2: +# Clamp the Y position based on the hinge rules +func _clamp_y_position(p_y_position : float) -> float: # Apply joystick step-quantization - if _joystick_x_steps_rad: - position.x = round(position.x / _joystick_x_steps_rad) * _joystick_x_steps_rad - if _joystick_y_steps_rad: - position.y = round(position.y / _joystick_y_steps_rad) * _joystick_y_steps_rad + if joystick_y_steps: + p_y_position = snappedf(p_y_position, joystick_y_steps) # Apply joystick limits - position.x = clamp(position.x, _joystick_x_limit_min_rad, _joystick_x_limit_max_rad) - position.y = clamp(position.y, _joystick_y_limit_min_rad, _joystick_y_limit_max_rad) - - # Move if necessary - if position.x != _joystick_x_position_rad or position.y != _joystick_y_position_rad: - transform.basis = Basis.from_euler(Vector3(position.y, position.x, 0.0)) + if _origin: + p_y_position = clamp(p_y_position, _origin.limit_y_minimum, _origin.limit_y_maximum) - # Return the updated position - return position + # Return the updated y position + return p_y_position diff --git a/addons/godot-xr-tools/interactables/interactable_joystick_origin.gd b/addons/godot-xr-tools/interactables/interactable_joystick_origin.gd new file mode 100644 index 00000000..7b378846 --- /dev/null +++ b/addons/godot-xr-tools/interactables/interactable_joystick_origin.gd @@ -0,0 +1,75 @@ +@tool +class_name XRToolsInteractableJoystickOrigin +extends Node3D + + +## XR Tools Interactable Joystick Origin script +## +## The interactable slider origin is parent of an [XRToolsInteractableJoystick] +## node and defines the extent of travel the joystick can move throught. + + +## Joystick X minimum limit +@export var limit_x_minimum : float = -45.0: set = set_limit_x_minimum + +## Joystick X maximum limit +@export var limit_x_maximum : float = 45.0: set = set_limit_x_maximum + +## Joystick Y minimum limit +@export var limit_y_minimum : float = -45.0: set = set_limit_y_minimum + +## Joystick Y maximum limit +@export var limit_y_maximum : float = 45.0: set = set_limit_y_maximum + + +# Add support for is_xr_class on XRTools classes +func is_xr_class(name : String) -> bool: + return name == "XRToolsInteractableJoystickOrigin" + + +# Check for configuration warnings +func _get_configuration_warnings() -> PackedStringArray: + var ret := PackedStringArray() + + # Check for invalid X limits + if limit_x_maximum <= limit_x_minimum: + ret.append("Invalid joystick X range") + + # Check for invalid Y limits + if limit_y_maximum <= limit_y_minimum: + ret.append("Invalid joystick Y range") + + # Check for a hinge child + if get_children().all( + func(n : Node) : return not n is XRToolsInteractableJoystick): + ret.append("Missing XRToolsInteractableJoystick child") + + return ret + + +# Handle setting the minimum X limit +func set_limit_x_minimum(p_limit_x_minimum : float) -> void: + limit_x_minimum = p_limit_x_minimum + update_configuration_warnings() + update_gizmos() + + +# Handle setting the maximum X limit +func set_limit_x_maximum(p_limit_x_maximum : float) -> void: + limit_x_maximum = p_limit_x_maximum + update_configuration_warnings() + update_gizmos() + + +# Handle setting the minimum Y limit +func set_limit_y_minimum(p_limit_y_minimum : float) -> void: + limit_y_minimum = p_limit_y_minimum + update_configuration_warnings() + update_gizmos() + + +# Handle setting the maximum Y limit +func set_limit_y_maximum(p_limit_y_maximum : float) -> void: + limit_y_maximum = p_limit_y_maximum + update_configuration_warnings() + update_gizmos() diff --git a/addons/godot-xr-tools/interactables/interactable_slider.gd b/addons/godot-xr-tools/interactables/interactable_slider.gd index 5faf517e..6e8af4e2 100644 --- a/addons/godot-xr-tools/interactables/interactable_slider.gd +++ b/addons/godot-xr-tools/interactables/interactable_slider.gd @@ -19,25 +19,23 @@ extends XRToolsInteractableHandleDriven signal slider_moved(position) -## Slider minimum limit -@export var slider_limit_min : float = 0.0 - -## Slider maximum limit -@export var slider_limit_max : float = 1.0 - ## Slider step size (zero for no steps) -@export var slider_steps : float = 0.0 +@export var slider_steps : float = 0.0 : set = _set_slider_steps ## Slider position -@export var slider_position : float = 0.0: set = _set_slider_position +@export var slider_position : float = 0.0 : set = _set_slider_position ## Default position -@export var default_position : float = 0.0 +@export var default_position : float = 0.0 : set = _set_default_position ## If true, the slider moves to the default position when released @export var default_on_release : bool = false +## Slider origin +var _origin : XRToolsInteractableSliderOrigin + + # Add support for is_xr_class on XRTools classes func is_xr_class(name : String) -> bool: return name == "XRToolsInteractableSlider" or super(name) @@ -48,6 +46,9 @@ func _ready() -> void: # In Godot 4 we must now manually call our super class ready function super() + # Get the parent origin + _origin = get_parent() + # Set the initial position to match the initial slider position value transform = Transform3D( Basis.IDENTITY, @@ -60,7 +61,7 @@ func _ready() -> void: # Called every frame when one or more handles are held by the player -func _process(_delta: float) -> void: +func _process(_delta : float) -> void: # Get the total handle offsets var offset_sum := Vector3.ZERO for item in grabbed_handles: @@ -78,43 +79,62 @@ func _process(_delta: float) -> void: # Move the slider to the specified position -func move_slider(position: float) -> void: +func move_slider(p_position : float) -> void: # Do the slider move - position = _do_move_slider(position) - if position == slider_position: + p_position = _do_move_slider(p_position) + if p_position == slider_position: return # Update the current position - slider_position = position + slider_position = p_position # Emit the moved signal - emit_signal("slider_moved", position) + emit_signal("slider_moved", p_position) # Handle release of slider -func _on_slider_released(_interactable: XRToolsInteractableSlider): +func _on_slider_released(_interactable : XRToolsInteractableSlider): if default_on_release: move_slider(default_position) -# Called when the slider position is set externally -func _set_slider_position(position: float) -> void: - position = _do_move_slider(position) - slider_position = position +# Called when the slider steps are set +func _set_slider_steps(p_slider_steps : float) -> void: + slider_steps = maxf(0.0, p_slider_steps) -# Do the slider move -func _do_move_slider(position: float) -> float: - # Apply slider step-quantization - if slider_steps: - position = round(position / slider_steps) * slider_steps +# Called when the slider position is set +func _set_slider_position(p_slider_position : float) -> void: + slider_position = _do_move_slider(p_slider_position) + + +# Called when the default position is set +func _set_default_position(p_default_position : float) -> void: + default_position = _clamp_position(p_default_position) - # Apply slider limits - position = clamp(position, slider_limit_min, slider_limit_max) + +# Do the slider move +func _do_move_slider(p_position : float) -> float: + # Clamp the position + p_position = _clamp_position(p_position) # Move if necessary - if position != slider_position: - transform.origin.x = position + if p_position != slider_position: + transform.origin.x = p_position + + # Return the updated position + return p_position + + +# Clamp the position based on the hinge rules +func _clamp_position(p_position : float) -> float: + # Apply hinge step-quantization + if slider_steps: + p_position = snappedf(p_position, slider_steps) + + # Apply hinge limits + if _origin: + p_position = clamp(p_position, _origin.limit_minimum, _origin.limit_maximum) # Return the updated position - return position + return p_position diff --git a/addons/godot-xr-tools/interactables/interactable_slider_origin.gd b/addons/godot-xr-tools/interactables/interactable_slider_origin.gd new file mode 100644 index 00000000..c559c63a --- /dev/null +++ b/addons/godot-xr-tools/interactables/interactable_slider_origin.gd @@ -0,0 +1,51 @@ +@tool +class_name XRToolsInteractableSliderOrigin +extends Node3D + + +## XR Tools Interactable Slider Origin script +## +## The interactable slider origin is parent of an [XRToolsInteractableSlider] +## node and defines the extent of travel the slider can move throught. + + +## Slider minimum limit +@export var limit_minimum : float = 0.0 : set = set_limit_minimum + +## Slider maximum limit +@export var limit_maximum : float = 1.0 : set = set_limit_maximum + + +# Add support for is_xr_class on XRTools classes +func is_xr_class(name : String) -> bool: + return name == "XRToolsInteractableSliderOrigin" + + +# Check for configuration warnings +func _get_configuration_warnings() -> PackedStringArray: + var ret := PackedStringArray() + + # Check for invalid limits + if limit_maximum <= limit_minimum: + ret.append("Invalid slider range") + + # Check for a hinge child + if get_children().all( + func(n : Node) : return not n is XRToolsInteractableSlider): + ret.append("Missing XRToolsInteractableSlider child") + + return ret + + +# Handle setting the minimum limit +func set_limit_minimum(p_limit_minimum : float) -> void: + limit_minimum = p_limit_minimum + update_configuration_warnings() + update_gizmos() + + +# Handle setting the maximum limit +func set_limit_maximum(p_limit_maximum : float) -> void: + limit_maximum = p_limit_maximum + update_configuration_warnings() + update_gizmos() diff --git a/addons/godot-xr-tools/plugin.gd b/addons/godot-xr-tools/plugin.gd index c310c7bd..cdba6899 100644 --- a/addons/godot-xr-tools/plugin.gd +++ b/addons/godot-xr-tools/plugin.gd @@ -8,10 +8,31 @@ const MENU_ID_ENABLE_OPENXR := 1001 ## Menu ID for setting the physics layers const MENU_ID_SET_PHYSICS_LAYERS := 1002 +## Hinge joint editor gizmo +const GIZMO_HINGE_JOINT = preload( + "res://addons/godot-xr-tools/editor/gizmos/hinge_origin.gd") + +## Slider joint editor gizmo +const GIZMO_SLIDER_JOINT = preload( + "res://addons/godot-xr-tools/editor/gizmos/slider_origin.gd") + +## Joystick joint editor gizmo +const GIZMO_JOYSTICK_JOINT = preload( + "res://addons/godot-xr-tools/editor/gizmos/joystick_origin.gd") + # XR Tools popup menu var _xr_tools_menu : PopupMenu +# Gizmo for editing hinge joints +var _gizmo_hinge_joint : EditorNode3DGizmoPlugin = GIZMO_HINGE_JOINT.new() + +# Gizmo for editing slider joints +var _gizmo_slider_joint : EditorNode3DGizmoPlugin = GIZMO_SLIDER_JOINT.new() + +# Gizmo for editing joystick joints +var _gizmo_joystick_joint : EditorNode3DGizmoPlugin = GIZMO_JOYSTICK_JOINT.new() + func _define_project_setting( p_name : String, @@ -134,7 +155,19 @@ func _enter_tree(): "XRToolsUserSettings", "res://addons/godot-xr-tools/user_settings/user_settings.gd") + # TODO: Fix saving undo/redo manager in gizmos + _gizmo_hinge_joint.undo_redo = get_undo_redo() + _gizmo_slider_joint.undo_redo = get_undo_redo() + _gizmo_joystick_joint.undo_redo = get_undo_redo() + + # Add gizmos + add_node_3d_gizmo_plugin(_gizmo_hinge_joint) + add_node_3d_gizmo_plugin(_gizmo_slider_joint) + add_node_3d_gizmo_plugin(_gizmo_joystick_joint) + func _exit_tree(): - # our plugin is turned off - pass + # Remove gizmos + remove_node_3d_gizmo_plugin(_gizmo_hinge_joint) + remove_node_3d_gizmo_plugin(_gizmo_slider_joint) + remove_node_3d_gizmo_plugin(_gizmo_joystick_joint) diff --git a/assets/meshes/interactables/joystick_smooth.tscn b/assets/meshes/interactables/joystick_smooth.tscn index 9ef50865..d86726b2 100644 --- a/assets/meshes/interactables/joystick_smooth.tscn +++ b/assets/meshes/interactables/joystick_smooth.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://bvgjfffe0w2uu"] +[gd_scene load_steps=21 format=3 uid="uid://bvgjfffe0w2uu"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick_origin.gd" id="2_66lop"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_dxrni"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="JoystickOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_66lop") [node name="InteractableJoystick" type="Node3D" parent="JoystickOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/joystick_snap.tscn b/assets/meshes/interactables/joystick_snap.tscn index c6bd31ce..5bc3b6c9 100644 --- a/assets/meshes/interactables/joystick_snap.tscn +++ b/assets/meshes/interactables/joystick_snap.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://bnckmqfewiysy"] +[gd_scene load_steps=21 format=3 uid="uid://bnckmqfewiysy"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick_origin.gd" id="2_jse5a"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_0rs04"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="JoystickOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_jse5a") [node name="InteractableJoystick" type="Node3D" parent="JoystickOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/joystick_zero.tscn b/assets/meshes/interactables/joystick_zero.tscn index 070a1e45..6af44f6c 100644 --- a/assets/meshes/interactables/joystick_zero.tscn +++ b/assets/meshes/interactables/joystick_zero.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://dtrftty6i383g"] +[gd_scene load_steps=21 format=3 uid="uid://dtrftty6i383g"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_joystick_origin.gd" id="2_eqvfo"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_i88vc"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="JoystickOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_eqvfo") [node name="InteractableJoystick" type="Node3D" parent="JoystickOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/lever_smooth.tscn b/assets/meshes/interactables/lever_smooth.tscn index 2a209d72..7cfa7e5f 100644 --- a/assets/meshes/interactables/lever_smooth.tscn +++ b/assets/meshes/interactables/lever_smooth.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://cs6dxfu5owcgo"] +[gd_scene load_steps=21 format=3 uid="uid://cs6dxfu5owcgo"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge_origin.gd" id="2_i6lyg"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_psbg5"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="LeverOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_i6lyg") [node name="InteractableLever" type="Node3D" parent="LeverOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/lever_snap.tscn b/assets/meshes/interactables/lever_snap.tscn index c84d225d..58d6a6b6 100644 --- a/assets/meshes/interactables/lever_snap.tscn +++ b/assets/meshes/interactables/lever_snap.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://dxgv8axd355m7"] +[gd_scene load_steps=21 format=3 uid="uid://dxgv8axd355m7"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge_origin.gd" id="2_ebhnr"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_ia5nn"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="LeverOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_ebhnr") [node name="InteractableLever" type="Node3D" parent="LeverOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/lever_zero.tscn b/assets/meshes/interactables/lever_zero.tscn index a6e477b0..3a8a2545 100644 --- a/assets/meshes/interactables/lever_zero.tscn +++ b/assets/meshes/interactables/lever_zero.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://cll4pk3kuuq1"] +[gd_scene load_steps=21 format=3 uid="uid://cll4pk3kuuq1"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge_origin.gd" id="2_2fa64"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_lu2nr"] @@ -59,6 +60,7 @@ surface_material_override/0 = ExtResource("1") [node name="LeverOrigin" type="Node3D" parent="."] transform = Transform3D(1, -1.06581e-14, 0, 1.06581e-14, 1, 0, 0, 0, 1, 0, 0, 0) +script = ExtResource("2_2fa64") [node name="InteractableLever" type="Node3D" parent="LeverOrigin"] script = ExtResource("2") diff --git a/assets/meshes/interactables/slider_smooth.tscn b/assets/meshes/interactables/slider_smooth.tscn index 03b8cec2..bd781149 100644 --- a/assets/meshes/interactables/slider_smooth.tscn +++ b/assets/meshes/interactables/slider_smooth.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://cpwdvy41fqdua"] +[gd_scene load_steps=21 format=3 uid="uid://cpwdvy41fqdua"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider_origin.gd" id="2_nj3mq"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_klojw"] @@ -59,11 +60,12 @@ surface_material_override/0 = ExtResource("1") [node name="SliderOrigin" type="Node3D" parent="."] transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, -0.2, 0) +script = ExtResource("2_nj3mq") +limit_maximum = 0.4 [node name="InteractableSlider" type="Node3D" parent="SliderOrigin"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2, 0, 0) script = ExtResource("2") -slider_limit_max = 0.4 slider_position = 0.2 default_position = 0.2 diff --git a/assets/meshes/interactables/slider_snap.tscn b/assets/meshes/interactables/slider_snap.tscn index b1daf743..22ae1146 100644 --- a/assets/meshes/interactables/slider_snap.tscn +++ b/assets/meshes/interactables/slider_snap.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://dukyhft262nrv"] +[gd_scene load_steps=21 format=3 uid="uid://dukyhft262nrv"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider_origin.gd" id="2_s7vpi"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_15h3x"] @@ -59,11 +60,12 @@ surface_material_override/0 = ExtResource("1") [node name="SliderOrigin" type="Node3D" parent="."] transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, -0.2, 0) +script = ExtResource("2_s7vpi") +limit_maximum = 0.4 [node name="InteractableSlider" type="Node3D" parent="SliderOrigin"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2, 0, 0) script = ExtResource("2") -slider_limit_max = 0.4 slider_steps = 0.1 slider_position = 0.2 default_position = 0.2 diff --git a/assets/meshes/interactables/slider_zero.tscn b/assets/meshes/interactables/slider_zero.tscn index 3d91bf9d..218997f8 100644 --- a/assets/meshes/interactables/slider_zero.tscn +++ b/assets/meshes/interactables/slider_zero.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=20 format=3 uid="uid://hv8nnbkuccxi"] +[gd_scene load_steps=21 format=3 uid="uid://hv8nnbkuccxi"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/blue_grid.tres" id="1"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider.gd" id="2"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_slider_origin.gd" id="2_7uj2d"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="3"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="4"] [ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="4_0po6f"] @@ -59,11 +60,12 @@ surface_material_override/0 = ExtResource("1") [node name="SliderOrigin" type="Node3D" parent="."] transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, -0.2, 0) +script = ExtResource("2_7uj2d") +limit_maximum = 0.4 [node name="InteractableSlider" type="Node3D" parent="SliderOrigin"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.2, 0, 0) script = ExtResource("2") -slider_limit_max = 0.4 slider_position = 0.2 default_position = 0.2 default_on_release = true diff --git a/assets/meshes/interactables/wheel_smooth.tscn b/assets/meshes/interactables/wheel_smooth.tscn index 729a1e24..e0b4a383 100644 --- a/assets/meshes/interactables/wheel_smooth.tscn +++ b/assets/meshes/interactables/wheel_smooth.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=11 format=3 uid="uid://d2td5hwh2oluy"] +[gd_scene load_steps=12 format=3 uid="uid://d2td5hwh2oluy"] [ext_resource type="Material" path="res://assets/wahooney.itch.io/brown_grid_triplanar.tres" id="1"] +[ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge_origin.gd" id="1_amejw"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_hinge.gd" id="2"] [ext_resource type="Script" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="3"] [ext_resource type="PackedScene" uid="uid://dc5t2qgmhb2nf" path="res://addons/godot-xr-tools/objects/hand_pose_area.tscn" id="4_2vgo6"] @@ -29,11 +30,12 @@ radius = 0.22 [node name="HingeOrigin" type="Node3D" parent="."] transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, 0, 0, 0) +script = ExtResource("1_amejw") +limit_minimum = -180.0 +limit_maximum = 180.0 [node name="InteractableHinge" type="Node3D" parent="HingeOrigin"] script = ExtResource("2") -hinge_limit_min = -360.0 -hinge_limit_max = 360.0 [node name="WheelBody" type="StaticBody3D" parent="HingeOrigin/InteractableHinge"]