Added footsteps, new tree, various other tweaks

This commit is contained in:
derek
2024-12-05 11:47:34 -06:00
parent 816ae85938
commit 023879ea9f
389 changed files with 20484 additions and 234 deletions

View File

@@ -0,0 +1,36 @@
@tool
class_name ProtonScatterBaseShape
extends Resource
func is_point_inside_global(_point_global: Vector3, _global_transform: Transform3D) -> bool:
return false
func is_point_inside_local(_point_local: Vector3) -> bool:
return false
# Returns an array of Vector3. This should contain enough points to compute
# a bounding box for the given shape.
func get_corners_global(_shape_global_transform: Transform3D) -> Array[Vector3]:
return []
# Returns the closed contour of the shape (closed, inner and outer if
# applicable) as a 2D polygon, in local space relative to the scatter node.
func get_closed_edges(_shape_t: Transform3D) -> Array[PackedVector2Array]:
return []
# Returns the open edges (in the case of a regular path, not closed)
# in local space relative to the scatter node.
func get_open_edges(_shape_t: Transform3D) -> Array[Curve3D]:
return []
# Returns a copy of this shape.
# TODO: check later when Godot4 enters beta if we can get rid of this and use
# the built-in duplicate() method properly.
func get_copy() -> Resource:
return null

View File

@@ -0,0 +1,95 @@
@tool
class_name ProtonScatterBoxShape
extends ProtonScatterBaseShape
@export var size := Vector3.ONE:
set(val):
size = val
_half_size = size * 0.5
emit_changed()
var _half_size := Vector3.ONE
func get_copy():
var copy = get_script().new()
copy.size = size
return copy
func is_point_inside(point: Vector3, global_transform: Transform3D) -> bool:
var local_point = global_transform.affine_inverse() * point
return AABB(-_half_size, size).has_point(local_point)
func get_corners_global(gt: Transform3D) -> Array:
var res := []
var corners := [
Vector3(-1, -1, -1),
Vector3(-1, -1, 1),
Vector3(1, -1, 1),
Vector3(1, -1, -1),
Vector3(-1, 1, -1),
Vector3(-1, 1, 1),
Vector3(1, 1, 1),
Vector3(1, 1, -1),
]
for c in corners:
c *= size * 0.5
res.push_back(gt * c)
return res
# Intersection between and box and a plane results in a polygon between 3 and 6
# vertices.
# Compute the intersection of each of the 12 edges to the plane, then recompute
# the polygon from the positions found.
func get_closed_edges(shape_t: Transform3D) -> Array[PackedVector2Array]:
var polygon := PackedVector2Array()
var plane := Plane(Vector3.UP, 0.0)
var box_edges := [
# Bottom square
[Vector3(-1, -1, -1), Vector3(-1, -1, 1)],
[Vector3(-1, -1, 1), Vector3(1, -1, 1)],
[Vector3(1, -1, 1), Vector3(1, -1, -1)],
[Vector3(1, -1, -1), Vector3(-1, -1, -1)],
# Top square
[Vector3(-1, 1, -1), Vector3(-1, 1, 1)],
[Vector3(-1, 1, 1), Vector3(1, 1, 1)],
[Vector3(1, 1, 1), Vector3(1, 1, -1)],
[Vector3(1, 1, -1), Vector3(-1, 1, -1)],
# Vertical lines
[Vector3(-1, -1, -1), Vector3(-1, 1, -1)],
[Vector3(-1, -1, 1), Vector3(-1, 1, 1)],
[Vector3(1, -1, 1), Vector3(1, 1, 1)],
[Vector3(1, -1, -1), Vector3(1, 1, -1)],
]
var intersection_points := PackedVector3Array()
var point
var shape_t_inverse := shape_t.affine_inverse()
for edge in box_edges:
var p1 = (edge[0] * _half_size) * shape_t_inverse
var p2 = (edge[1] * _half_size) * shape_t_inverse
point = plane.intersects_segment(p1, p2)
if point:
intersection_points.push_back(point)
if intersection_points.size() < 3:
return []
var points_unordered := PackedVector2Array()
for p in intersection_points:
points_unordered.push_back(Vector2(p.x, p.z))
polygon = Geometry2D.convex_hull(points_unordered)
return [polygon]

View File

@@ -0,0 +1,135 @@
@tool
extends "gizmo_handler.gd"
# 3D Gizmo for the Box shape.
func get_handle_name(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> String:
return "Box Size"
func get_handle_value(gizmo: EditorNode3DGizmo, handle_id: int, _secondary: bool) -> Variant:
return gizmo.get_node_3d().shape.size
func set_handle(gizmo: EditorNode3DGizmo, handle_id: int, _secondary: bool, camera: Camera3D, screen_pos: Vector2) -> void:
if handle_id < 0 or handle_id > 2:
return
var axis := Vector3.ZERO
axis[handle_id] = 1.0 # handle 0:x, 1:y, 2:z
var shape_node = gizmo.get_node_3d()
var gt := shape_node.get_global_transform()
var gt_inverse := gt.affine_inverse()
var origin := gt.origin
var drag_axis := (axis * 4096) * gt_inverse
var ray_from = camera.project_ray_origin(screen_pos)
var ray_to = ray_from + camera.project_ray_normal(screen_pos) * 4096
var points = Geometry3D.get_closest_points_between_segments(origin, drag_axis, ray_from, ray_to)
var size = shape_node.shape.size
size -= axis * size
var dist = origin.distance_to(points[0]) * 2.0
size += axis * dist
shape_node.shape.size = size
func commit_handle(gizmo: EditorNode3DGizmo, handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void:
var shape: ProtonScatterBoxShape = gizmo.get_node_3d().shape
if cancel:
shape.size = restore
return
_undo_redo.create_action("Set ScatterShape size")
_undo_redo.add_undo_method(self, "_set_size", shape, restore)
_undo_redo.add_do_method(self, "_set_size", shape, shape.size)
_undo_redo.commit_action()
func redraw(plugin: EditorNode3DGizmoPlugin, gizmo: EditorNode3DGizmo):
gizmo.clear()
var scatter_shape = gizmo.get_node_3d()
var shape: ProtonScatterBoxShape = scatter_shape.shape
### Draw the Box lines
var lines = PackedVector3Array()
var lines_material := plugin.get_material("primary_top", gizmo)
var half_size = shape.size * 0.5
var corners := [
[ # Bottom square
Vector3(-1, -1, -1),
Vector3(-1, -1, 1),
Vector3(1, -1, 1),
Vector3(1, -1, -1),
Vector3(-1, -1, -1),
],
[ # Top square
Vector3(-1, 1, -1),
Vector3(-1, 1, 1),
Vector3(1, 1, 1),
Vector3(1, 1, -1),
Vector3(-1, 1, -1),
],
[ # Vertical lines
Vector3(-1, -1, -1),
Vector3(-1, 1, -1),
],
[
Vector3(-1, -1, 1),
Vector3(-1, 1, 1),
],
[
Vector3(1, -1, 1),
Vector3(1, 1, 1),
],
[
Vector3(1, -1, -1),
Vector3(1, 1, -1),
]
]
var block_count = corners.size()
if not is_selected(gizmo):
block_count = 1
for i in block_count:
var block = corners[i]
for j in block.size() - 1:
lines.push_back(block[j] * half_size)
lines.push_back(block[j + 1] * half_size)
gizmo.add_lines(lines, lines_material)
gizmo.add_collision_segments(lines)
### Fills the box inside
var mesh = BoxMesh.new()
mesh.size = shape.size
var mesh_material: StandardMaterial3D
if scatter_shape.negative:
mesh_material = plugin.get_material("exclusive", gizmo)
else:
mesh_material = plugin.get_material("inclusive", gizmo)
gizmo.add_mesh(mesh, mesh_material)
### Draw the handles, one for each axis
var handles := PackedVector3Array()
var handles_ids := PackedInt32Array()
var handles_material := plugin.get_material("default_handle", gizmo)
handles.push_back(Vector3.RIGHT * shape.size.x * 0.5)
handles.push_back(Vector3.UP * shape.size.y * 0.5)
handles.push_back(Vector3.BACK * shape.size.z * 0.5)
gizmo.add_handles(handles, handles_material, handles_ids)
func _set_size(box: ProtonScatterBoxShape, size: Vector3) -> void:
if box:
box.size = size

View File

@@ -0,0 +1,3 @@
[gd_resource type="ButtonGroup" format=3 uid="uid://1xy55037k3k5"]
[resource]

View File

@@ -0,0 +1,55 @@
[gd_scene format=3 uid="uid://qb8j7oasuqbc"]
[node name="AdvancedOptionsPanel" type="MarginContainer"]
offset_right = 221.0
offset_bottom = 136.0
grow_horizontal = 2
size_flags_horizontal = 4
size_flags_vertical = 4
metadata/_edit_use_custom_anchors = true
[node name="HBoxContainer" type="HBoxContainer" parent="."]
offset_right = 221.0
offset_bottom = 136.0
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
offset_right = 217.0
offset_bottom = 136.0
[node name="MirrorLength" type="CheckButton" parent="HBoxContainer/VBoxContainer"]
offset_right = 217.0
offset_bottom = 31.0
focus_mode = 0
text = "Mirror handles length"
[node name="MirrorAngle" type="CheckButton" parent="HBoxContainer/VBoxContainer"]
offset_top = 35.0
offset_right = 217.0
offset_bottom = 66.0
focus_mode = 0
text = "Mirror handles angle"
[node name="LockToPlane" type="CheckButton" parent="HBoxContainer/VBoxContainer"]
offset_top = 70.0
offset_right = 217.0
offset_bottom = 101.0
focus_mode = 0
text = "Lock to plane"
[node name="MirrorAngle3" type="CheckButton" parent="HBoxContainer/VBoxContainer"]
offset_top = 105.0
offset_right = 217.0
offset_bottom = 136.0
focus_mode = 0
text = "Snap to colliders"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer"]
visible = false
offset_left = 221.0
offset_right = 225.0
offset_bottom = 136.0
[node name="VBoxContainer2" type="VBoxContainer" parent="HBoxContainer"]
offset_left = 221.0
offset_right = 221.0
offset_bottom = 136.0

View File

@@ -0,0 +1,96 @@
@tool
extends Control
const ScatterShape = preload("../../../scatter_shape.gd")
const PathShape = preload("../../path_shape.gd")
var shape_node: ScatterShape
@onready var _options_button: Button = $%Options
@onready var _options_panel: Popup = $%OptionsPanel
func _ready() -> void:
_options_button.toggled.connect(_on_options_button_toggled)
_options_panel.popup_hide.connect(_on_options_panel_hide)
$%SnapToColliders.toggled.connect(_on_snap_to_colliders_toggled)
$%ClosedPath.toggled.connect(_on_closed_path_toggled)
$%MirrorAngle.toggled.connect(_on_mirror_angle_toggled)
for button in [$%LockToPlane, $%SnapToColliders, $%ClosedPath]:
button.pressed.connect(_on_button_pressed)
# Called by the editor plugin when the node selection changes.
# Hides the panel when the selected node is not a path shape.
func selection_changed(selected: Array) -> void:
if selected.is_empty():
visible = false
shape_node = null
return
var node = selected[0]
visible = node is ScatterShape and node.shape is PathShape
if visible:
shape_node = node
$%ClosedPath.button_pressed = node.shape.closed
func is_select_mode_enabled() -> bool:
return $%Select.button_pressed
func is_create_mode_enabled() -> bool:
return $%Create.button_pressed
func is_delete_mode_enabled() -> bool:
return $%Delete.button_pressed
func is_lock_to_plane_enabled() -> bool:
return $%LockToPlane.button_pressed and not is_snap_to_colliders_enabled()
func is_snap_to_colliders_enabled() -> bool:
return $%SnapToColliders.button_pressed
func is_mirror_length_enabled() -> bool:
return $%MirrorLength.button_pressed
func is_mirror_angle_enabled() -> bool:
return $%MirrorAngle.button_pressed
func _on_options_button_toggled(enabled: bool) -> void:
if enabled:
var popup_position := Vector2i(get_global_transform().origin)
popup_position.y += size.y + 12
_options_panel.popup(Rect2i(popup_position, Vector2i.ZERO))
else:
_options_panel.hide()
func _on_options_panel_hide() -> void:
_options_button.button_pressed = false
func _on_mirror_angle_toggled(enabled: bool) -> void:
$%MirrorLength.disabled = not enabled
func _on_snap_to_colliders_toggled(enabled: bool) -> void:
$%LockToPlane.disabled = enabled
func _on_closed_path_toggled(enabled: bool) -> void:
if shape_node and shape_node.shape is PathShape:
shape_node.shape.closed = enabled
func _on_button_pressed() -> void:
if shape_node:
shape_node.update_gizmos()

View File

@@ -0,0 +1,124 @@
[gd_scene load_steps=7 format=3 uid="uid://vijpujrvtyin"]
[ext_resource type="Script" path="res://addons/proton_scatter/src/shapes/gizmos_plugin/components/path_panel.gd" id="1_o7kkg"]
[ext_resource type="Texture2D" uid="uid://c1t5x34pc4vs5" path="res://addons/proton_scatter/icons/curve_select.svg" id="2_d7o1n"]
[ext_resource type="ButtonGroup" uid="uid://1xy55037k3k5" path="res://addons/proton_scatter/src/shapes/gizmos_plugin/components/curve_mode_button_group.tres" id="2_sl6yo"]
[ext_resource type="Texture2D" uid="uid://cmykha5ja17vj" path="res://addons/proton_scatter/icons/curve_create.svg" id="3_l70sn"]
[ext_resource type="Texture2D" uid="uid://cligdljx1ad5e" path="res://addons/proton_scatter/icons/curve_delete.svg" id="4_b5yum"]
[ext_resource type="Texture2D" uid="uid://n66mufjib4ds" path="res://addons/proton_scatter/icons/menu.svg" id="6_xiaj2"]
[node name="PathPanel" type="MarginContainer"]
offset_right = 108.0
offset_bottom = 24.0
size_flags_horizontal = 0
size_flags_vertical = 4
script = ExtResource("1_o7kkg")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
[node name="Select" type="Button" parent="HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
toggle_mode = true
button_pressed = true
button_group = ExtResource("2_sl6yo")
icon = ExtResource("2_d7o1n")
flat = true
icon_alignment = 1
[node name="Create" type="Button" parent="HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
toggle_mode = true
button_group = ExtResource("2_sl6yo")
icon = ExtResource("3_l70sn")
flat = true
icon_alignment = 1
[node name="Delete" type="Button" parent="HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
toggle_mode = true
button_group = ExtResource("2_sl6yo")
icon = ExtResource("4_b5yum")
flat = true
icon_alignment = 1
[node name="Options" type="Button" parent="HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
toggle_mode = true
action_mode = 0
icon = ExtResource("6_xiaj2")
flat = true
icon_alignment = 1
[node name="OptionsPanel" type="PopupPanel" parent="."]
unique_name_in_owner = true
size = Vector2i(229, 179)
[node name="AdvancedOptionsPanel" type="MarginContainer" parent="OptionsPanel"]
offset_left = 4.0
offset_top = 4.0
offset_right = 225.0
offset_bottom = 175.0
grow_horizontal = 2
size_flags_horizontal = 4
size_flags_vertical = 4
metadata/_edit_use_custom_anchors = true
[node name="HBoxContainer" type="HBoxContainer" parent="OptionsPanel/AdvancedOptionsPanel"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer"]
layout_mode = 2
[node name="MirrorAngle" type="CheckButton" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
button_pressed = true
text = "Mirror handles angle"
[node name="MirrorLength" type="CheckButton" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
button_pressed = true
text = "Mirror handles length"
[node name="ClosedPath" type="CheckButton" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
text = "Closed path"
[node name="LockToPlane" type="CheckButton" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
button_pressed = true
text = "Lock to plane"
[node name="SnapToColliders" type="CheckButton" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
focus_mode = 0
text = "Snap to colliders"
[node name="VSeparator" type="VSeparator" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer"]
visible = false
layout_mode = 2
[node name="VBoxContainer2" type="VBoxContainer" parent="OptionsPanel/AdvancedOptionsPanel/HBoxContainer"]
layout_mode = 2

View File

@@ -0,0 +1,50 @@
@tool
extends RefCounted
# Abstract class.
var _undo_redo: EditorUndoRedoManager
var _plugin: EditorPlugin
func set_undo_redo(ur: EditorUndoRedoManager) -> void:
_undo_redo = ur
func set_editor_plugin(plugin: EditorPlugin) -> void:
_plugin = plugin
func get_handle_name(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> String:
return ""
func get_handle_value(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> Variant:
return null
func set_handle(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, _camera: Camera3D, _screen_pos: Vector2) -> void:
pass
func commit_handle(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, _restore: Variant, _cancel: bool) -> void:
pass
func redraw(_gizmo_plugin: EditorNode3DGizmoPlugin, _gizmo: EditorNode3DGizmo):
pass
func forward_3d_gui_input(_viewport_camera: Camera3D, _event: InputEvent) -> bool:
return false
func is_selected(gizmo: EditorNode3DGizmo) -> bool:
if not _plugin:
return true
var current_node = gizmo.get_node_3d()
var selected_nodes := _plugin.get_editor_interface().get_selection().get_selected_nodes()
return current_node in selected_nodes

View File

@@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m13.6 2.4v11.2h-11.2v-11.2z" fill="#ffffff" stroke="#000000" stroke-linecap="square" stroke-width="1.6"/></svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dmjp2vpqp4qjy"
path.s3tc="res://.godot/imported/main_handle.svg-e76638c615070e68035d2b711214a1fc.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://addons/proton_scatter/src/shapes/gizmos_plugin/icons/main_handle.svg"
dest_files=["res://.godot/imported/main_handle.svg-e76638c615070e68035d2b711214a1fc.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m14.868629 8.0000002-6.8686288 6.8686288-6.8686293-6.8686288 6.8686293-6.8686293z" fill="#ffffff" stroke="#000000" stroke-linecap="square" stroke-width="1.6"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://kygbxbbnqkdh"
path.s3tc="res://.godot/imported/secondary_handle.svg-d46e6e295afbc9a7509025fe11144dfd.s3tc.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"vram_texture": true
}
[deps]
source_file="res://addons/proton_scatter/src/shapes/gizmos_plugin/icons/secondary_handle.svg"
dest_files=["res://.godot/imported/secondary_handle.svg-d46e6e295afbc9a7509025fe11144dfd.s3tc.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,356 @@
@tool
extends "gizmo_handler.gd"
const ProtonScatter := preload("res://addons/proton_scatter/src/scatter.gd")
const ProtonScatterShape := preload("res://addons/proton_scatter/src/scatter_shape.gd")
const ProtonScatterEventHelper := preload("res://addons/proton_scatter/src/common/event_helper.gd")
const PathPanel := preload("./components/path_panel.gd")
var _gizmo_panel: PathPanel
var _event_helper: ProtonScatterEventHelper
func get_handle_name(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> String:
return "Path point"
func get_handle_value(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> Variant:
var shape: ProtonScatterPathShape = gizmo.get_node_3d().shape
return shape.get_copy()
func set_handle(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool, camera: Camera3D, screen_pos: Vector2) -> void:
if not _gizmo_panel.is_select_mode_enabled():
return
var shape_node: ProtonScatterShape = gizmo.get_node_3d()
var curve: Curve3D = shape_node.shape.curve
var point_count: int = curve.get_point_count()
var curve_index := handle_id
var previous_handle_position: Vector3
if not secondary:
previous_handle_position = curve.get_point_position(curve_index)
else:
curve_index = int(handle_id / 2)
previous_handle_position = curve.get_point_position(curve_index)
if handle_id % 2 == 0:
previous_handle_position += curve.get_point_in(curve_index)
else:
previous_handle_position += curve.get_point_out(curve_index)
var click_world_position := _intersect_with(shape_node, camera, screen_pos, previous_handle_position)
var point_local_position: Vector3 = shape_node.get_global_transform().affine_inverse() * click_world_position
if not secondary:
# Main curve point moved
curve.set_point_position(handle_id, point_local_position)
else:
# In out handle moved
var mirror_angle := _gizmo_panel.is_mirror_angle_enabled()
var mirror_length := _gizmo_panel.is_mirror_length_enabled()
var point_origin = curve.get_point_position(curve_index)
var in_out_position = point_local_position - point_origin
var mirror_position = -in_out_position
if handle_id % 2 == 0:
curve.set_point_in(curve_index, in_out_position)
if mirror_angle:
if not mirror_length:
mirror_position = curve.get_point_out(curve_index).length() * -in_out_position.normalized()
curve.set_point_out(curve_index, mirror_position)
else:
curve.set_point_out(curve_index, in_out_position)
if mirror_angle:
if not mirror_length:
mirror_position = curve.get_point_in(curve_index).length() * -in_out_position.normalized()
curve.set_point_in(curve_index, mirror_position)
shape_node.update_gizmos()
func commit_handle(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void:
var shape_node: ProtonScatterShape = gizmo.get_node_3d()
if cancel:
_edit_path(shape_node, restore)
else:
_undo_redo.create_action("Edit ScatterShape Path")
_undo_redo.add_undo_method(self, "_edit_path", shape_node, restore)
_undo_redo.add_do_method(self, "_edit_path", shape_node, shape_node.shape.get_copy())
_undo_redo.commit_action()
shape_node.update_gizmos()
func redraw(plugin: EditorNode3DGizmoPlugin, gizmo: EditorNode3DGizmo):
gizmo.clear()
# Force the path panel to appear when the scatter shape type is changed
# from the inspector.
if is_selected(gizmo):
_gizmo_panel.selection_changed([gizmo.get_node_3d()])
var shape_node: ProtonScatterShape = gizmo.get_node_3d()
var shape: ProtonScatterPathShape = shape_node.shape
if not shape:
return
var curve: Curve3D = shape.curve
if not curve or curve.get_point_count() == 0:
return
# ------ Common stuff ------
var points := curve.tessellate(4, 8)
var points_2d := PackedVector2Array()
for p in points:
points_2d.push_back(Vector2(p.x, p.z))
var line_material: StandardMaterial3D = plugin.get_material("primary_top", gizmo)
var mesh_material: StandardMaterial3D = plugin.get_material("inclusive", gizmo)
if shape_node.negative:
mesh_material = plugin.get_material("exclusive", gizmo)
# ------ Main line along the path curve ------
var lines := PackedVector3Array()
var lines_count := points.size() - 1
for i in lines_count:
lines.append(points[i])
lines.append(points[i + 1])
gizmo.add_lines(lines, line_material)
gizmo.add_collision_segments(lines)
# ------ Draw handles ------
var main_handles := PackedVector3Array()
var in_out_handles := PackedVector3Array()
var handle_lines := PackedVector3Array()
var ids := PackedInt32Array() # Stays empty on purpose
for i in curve.get_point_count():
var point_pos = curve.get_point_position(i)
var point_in = curve.get_point_in(i) + point_pos
var point_out = curve.get_point_out(i) + point_pos
handle_lines.push_back(point_pos)
handle_lines.push_back(point_in)
handle_lines.push_back(point_pos)
handle_lines.push_back(point_out)
in_out_handles.push_back(point_in)
in_out_handles.push_back(point_out)
main_handles.push_back(point_pos)
gizmo.add_handles(main_handles, plugin.get_material("primary_handle", gizmo), ids)
gizmo.add_handles(in_out_handles, plugin.get_material("secondary_handle", gizmo), ids, false, true)
if is_selected(gizmo):
gizmo.add_lines(handle_lines, plugin.get_material("secondary_top", gizmo))
# -------- Visual when lock to plane is enabled --------
if _gizmo_panel.is_lock_to_plane_enabled() and is_selected(gizmo):
var bounds = shape.get_bounds()
var aabb = AABB(bounds.min, bounds.size).grow(shape.thickness / 2.0)
var width: float = aabb.size.x
var length: float = aabb.size.z
var plane_center: Vector3 = bounds.center
plane_center.y = 0.0
var plane_mesh := PlaneMesh.new()
plane_mesh.set_size(Vector2(width, length))
plane_mesh.set_center_offset(plane_center)
gizmo.add_mesh(plane_mesh, plugin.get_material("tertiary", gizmo))
var plane_lines := PackedVector3Array()
var corners = [
Vector3(-width, 0, -length),
Vector3(-width, 0, length),
Vector3(width, 0, length),
Vector3(width, 0, -length),
Vector3(-width, 0, -length),
]
for i in corners.size() - 1:
plane_lines.push_back(corners[i] * 0.5 + plane_center)
plane_lines.push_back(corners[i + 1] * 0.5 + plane_center)
gizmo.add_lines(plane_lines, plugin.get_material("secondary_top", gizmo))
# ----- Mesh representing the inside part of the path -----
if shape.closed:
var indices = Geometry2D.triangulate_polygon(points_2d)
if indices.is_empty():
indices = Geometry2D.triangulate_delaunay(points_2d)
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
for index in indices:
var p = points_2d[index]
st.add_vertex(Vector3(p.x, 0.0, p.y))
var mesh = st.commit()
gizmo.add_mesh(mesh, mesh_material)
# ------ Mesh representing path thickness ------
if shape.thickness > 0 and points.size() > 1:
# ____ TODO ____ : check if this whole section could be replaced by
# Geometry2D.expand_polyline, or an extruded capsule along the path
## Main path mesh
var st = SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLE_STRIP)
for i in points.size() - 1:
var p1: Vector3 = points[i]
var p2: Vector3 = points[i + 1]
var normal = (p2 - p1).cross(Vector3.UP).normalized()
var offset = normal * shape.thickness * 0.5
st.add_vertex(p1 - offset)
st.add_vertex(p1 + offset)
## Add the last missing two triangles from the loop above
var p1: Vector3 = points[-1]
var p2: Vector3 = points[-2]
var normal = (p1 - p2).cross(Vector3.UP).normalized()
var offset = normal * shape.thickness * 0.5
st.add_vertex(p1 - offset)
st.add_vertex(p1 + offset)
var mesh := st.commit()
gizmo.add_mesh(mesh, mesh_material)
## Rounded cap (start)
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var center = points[0]
var next = points[1]
normal = (center - next).cross(Vector3.UP).normalized()
for i in 12:
st.add_vertex(center)
st.add_vertex(center + normal * shape.thickness * 0.5)
normal = normal.rotated(Vector3.UP, PI / 12)
st.add_vertex(center + normal * shape.thickness * 0.5)
mesh = st.commit()
gizmo.add_mesh(mesh, mesh_material)
## Rounded cap (end)
st.begin(Mesh.PRIMITIVE_TRIANGLES)
center = points[-1]
next = points[-2]
normal = (next - center).cross(Vector3.UP).normalized()
for i in 12:
st.add_vertex(center)
st.add_vertex(center + normal * shape.thickness * 0.5)
normal = normal.rotated(Vector3.UP, -PI / 12)
st.add_vertex(center + normal * shape.thickness * 0.5)
mesh = st.commit()
gizmo.add_mesh(mesh, mesh_material)
func forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> bool:
if not _event_helper:
_event_helper = ProtonScatterEventHelper.new()
_event_helper.feed(event)
if not event is InputEventMouseButton:
return false
if not _event_helper.is_key_just_pressed(MOUSE_BUTTON_LEFT): # Can't use just_released here
return false
var shape_node: ProtonScatterShape = _gizmo_panel.shape_node
if not shape_node:
return false
if not shape_node.shape or not shape_node.shape is ProtonScatterPathShape:
return false
var shape: ProtonScatterPathShape = shape_node.shape
# In select mode, the set_handle and commit_handle functions take over.
if _gizmo_panel.is_select_mode_enabled():
return false
var click_world_position := _intersect_with(shape_node, viewport_camera, event.position)
var point_local_position: Vector3 = shape_node.get_global_transform().affine_inverse() * click_world_position
if _gizmo_panel.is_create_mode_enabled():
shape.create_point(point_local_position) # TODO: add undo redo
shape_node.update_gizmos()
return true
elif _gizmo_panel.is_delete_mode_enabled():
var index = shape.get_closest_to(point_local_position)
if index != -1:
shape.remove_point(index) # TODO: add undo redo
shape_node.update_gizmos()
return true
return false
func set_gizmo_panel(panel: PathPanel) -> void:
_gizmo_panel = panel
func _edit_path(shape_node: ProtonScatterShape, restore: ProtonScatterPathShape) -> void:
shape_node.shape.curve = restore.curve.duplicate()
shape_node.shape.thickness = restore.thickness
shape_node.update_gizmos()
func _intersect_with(path: ProtonScatterShape, camera: Camera3D, screen_point: Vector2, handle_position_local = null) -> Vector3:
# Get the ray data
var from = camera.project_ray_origin(screen_point)
var dir = camera.project_ray_normal(screen_point)
var gt = path.get_global_transform()
# Snap to collider enabled
if _gizmo_panel.is_snap_to_colliders_enabled():
var space_state: PhysicsDirectSpaceState3D = path.get_world_3d().get_direct_space_state()
var parameters := PhysicsRayQueryParameters3D.new()
parameters.from = from
parameters.to = from + (dir * 2048)
var hit := space_state.intersect_ray(parameters)
if not hit.is_empty():
return hit.position
# Lock to plane enabled
if _gizmo_panel.is_lock_to_plane_enabled():
var t = Transform3D(gt)
var a = t.basis.x
var b = t.basis.z
var c = a + b
var o = t.origin
var plane = Plane(a + o, b + o, c + o)
var result = plane.intersects_ray(from, dir)
if result != null:
return result
# Default case (similar to the built in Path3D node)
var origin: Vector3
if handle_position_local:
origin = gt * handle_position_local
else:
origin = path.get_global_transform().origin
var plane = Plane(dir, origin)
var res = plane.intersects_ray(from, dir)
if res != null:
return res
return origin

View File

@@ -0,0 +1,136 @@
@tool
extends EditorNode3DGizmoPlugin
# Actual logic split in the handler class to avoid cluttering this script as
# we add extra shapes.
#
# Although we could make an actual gizmo per shape type and add the extra type
# check in the 'has_gizmo' function, it causes more issues to the editor
# than it's worth (2 fewer files), so it's done like this instead.
const ScatterShape = preload("../../scatter_shape.gd")
const GizmoHandler = preload("./gizmo_handler.gd")
var _editor_plugin: EditorPlugin
var _handlers: Dictionary
func _init():
var handle_icon = preload("./icons/main_handle.svg")
var secondary_handle_icon = preload("./icons/secondary_handle.svg")
# TODO: Replace hardcoded colors by a setting fetch
create_material("primary", Color(1, 0.4, 0))
create_material("secondary", Color(0.4, 0.7, 1.0))
create_material("tertiary", Color(Color.STEEL_BLUE, 0.2))
create_custom_material("primary_top", Color(1, 0.4, 0))
create_custom_material("secondary_top", Color(0.4, 0.7, 1.0))
create_custom_material("tertiary_top", Color(Color.STEEL_BLUE, 0.1))
create_material("inclusive", Color(0.9, 0.7, 0.2, 0.15))
create_material("exclusive", Color(0.9, 0.1, 0.2, 0.15))
create_handle_material("default_handle")
create_handle_material("primary_handle", false, handle_icon)
create_handle_material("secondary_handle", false, secondary_handle_icon)
_handlers[ProtonScatterSphereShape] = preload("./sphere_gizmo.gd").new()
_handlers[ProtonScatterPathShape] = preload("./path_gizmo.gd").new()
_handlers[ProtonScatterBoxShape] = preload("./box_gizmo.gd").new()
func _get_gizmo_name() -> String:
return "ScatterShape"
func _has_gizmo(node) -> bool:
return node is ScatterShape
func _get_handle_name(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool) -> String:
return _get_handler(gizmo).get_handle_name(gizmo, handle_id, secondary)
func _get_handle_value(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool) -> Variant:
return _get_handler(gizmo).get_handle_value(gizmo, handle_id, secondary)
func _set_handle(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool, camera: Camera3D, screen_pos: Vector2) -> void:
_get_handler(gizmo).set_handle(gizmo, handle_id, secondary, camera, screen_pos)
func _commit_handle(gizmo: EditorNode3DGizmo, handle_id: int, secondary: bool, restore: Variant, cancel: bool) -> void:
_get_handler(gizmo).commit_handle(gizmo, handle_id, secondary, restore, cancel)
func _redraw(gizmo: EditorNode3DGizmo):
if _is_node_selected(gizmo):
_get_handler(gizmo).redraw(self, gizmo)
else:
gizmo.clear()
func forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> int:
for handler in _handlers.values():
if handler.forward_3d_gui_input(viewport_camera, event):
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_PASS
func set_undo_redo(ur: EditorUndoRedoManager) -> void:
for handler_type in _handlers:
_handlers[handler_type].set_undo_redo(ur)
func set_path_gizmo_panel(panel: Control) -> void:
if ProtonScatterPathShape in _handlers:
_handlers[ProtonScatterPathShape].set_gizmo_panel(panel)
func set_editor_plugin(plugin: EditorPlugin) -> void:
_editor_plugin = plugin
for handler_type in _handlers:
_handlers[handler_type].set_editor_plugin(plugin)
# Creates a standard material displayed on top of everything.
# Only exists because 'create_material() on_top' parameter doesn't seem to work.
func create_custom_material(name: String, color := Color.WHITE):
var material := StandardMaterial3D.new()
material.set_blend_mode(StandardMaterial3D.BLEND_MODE_ADD)
material.set_shading_mode(StandardMaterial3D.SHADING_MODE_UNSHADED)
material.set_flag(StandardMaterial3D.FLAG_DISABLE_DEPTH_TEST, true)
material.set_albedo(color)
material.render_priority = 100
add_material(name, material)
func _get_handler(gizmo: EditorNode3DGizmo) -> GizmoHandler:
var null_handler = GizmoHandler.new() # Only so we don't have to check existence later
var shape_node = gizmo.get_node_3d()
if not shape_node or not shape_node is ScatterShape:
return null_handler
var shape_resource = shape_node.shape
if not shape_resource:
return null_handler
var shape_type = shape_resource.get_script()
if not shape_type in _handlers:
return null_handler
return _handlers[shape_type]
func _is_node_selected(gizmo: EditorNode3DGizmo) -> bool:
if ProjectSettings.get_setting(_editor_plugin.GIZMO_SETTING):
return true
var selected_nodes: Array[Node] = _editor_plugin.get_custom_selection()
return gizmo.get_node_3d() in selected_nodes

View File

@@ -0,0 +1,97 @@
@tool
extends "gizmo_handler.gd"
# 3D Gizmo for the Sphere shape. Draws three circle on each axis to represent
# a sphere, displays one handle on the size to control the radius.
#
# (handle_id is ignored in every function since there's a single handle)
const SphereShape = preload("../sphere_shape.gd")
func get_handle_name(_gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> String:
return "Radius"
func get_handle_value(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool) -> Variant:
return gizmo.get_node_3d().shape.radius
func set_handle(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, camera: Camera3D, screen_pos: Vector2) -> void:
var shape_node = gizmo.get_node_3d()
var gt := shape_node.get_global_transform()
var gt_inverse := gt.affine_inverse()
var origin := gt.origin
var ray_from = camera.project_ray_origin(screen_pos)
var ray_to = ray_from + camera.project_ray_normal(screen_pos) * 4096
var points = Geometry3D.get_closest_points_between_segments(origin, (Vector3.LEFT * 4096) * gt_inverse, ray_from, ray_to)
shape_node.shape.radius = origin.distance_to(points[0])
func commit_handle(gizmo: EditorNode3DGizmo, _handle_id: int, _secondary: bool, restore: Variant, cancel: bool) -> void:
var shape: SphereShape = gizmo.get_node_3d().shape
if cancel:
shape.radius = restore
return
_undo_redo.create_action("Set ScatterShape Radius")
_undo_redo.add_undo_method(self, "_set_radius", shape, restore)
_undo_redo.add_do_method(self, "_set_radius", shape, shape.radius)
_undo_redo.commit_action()
func redraw(plugin: EditorNode3DGizmoPlugin, gizmo: EditorNode3DGizmo):
gizmo.clear()
var scatter_shape = gizmo.get_node_3d()
var shape: SphereShape = scatter_shape.shape
### Draw the 3 circles on each axis to represent the sphere
var lines = PackedVector3Array()
var lines_material := plugin.get_material("primary_top", gizmo)
var steps = 32 # TODO: Update based on sphere radius maybe ?
var step_angle = 2 * PI / steps
var radius = shape.radius
for i in steps:
lines.append(Vector3(cos(i * step_angle), 0.0, sin(i * step_angle)) * radius)
lines.append(Vector3(cos((i + 1) * step_angle), 0.0, sin((i + 1) * step_angle)) * radius)
if is_selected(gizmo):
for i in steps:
lines.append(Vector3(cos(i * step_angle), sin(i * step_angle), 0.0) * radius)
lines.append(Vector3(cos((i + 1) * step_angle), sin((i + 1) * step_angle), 0.0) * radius)
for i in steps:
lines.append(Vector3(0.0, cos(i * step_angle), sin(i * step_angle)) * radius)
lines.append(Vector3(0.0, cos((i + 1) * step_angle), sin((i + 1) * step_angle)) * radius)
gizmo.add_lines(lines, lines_material)
gizmo.add_collision_segments(lines)
### Draw the handle
var handles := PackedVector3Array()
var handles_ids := PackedInt32Array()
var handles_material := plugin.get_material("default_handle", gizmo)
var handle_position: Vector3 = Vector3.LEFT * radius
handles.push_back(handle_position)
gizmo.add_handles(handles, handles_material, handles_ids)
### Fills the sphere inside
var mesh = SphereMesh.new()
mesh.height = shape.radius * 2.0
mesh.radius = shape.radius
var mesh_material: StandardMaterial3D
if scatter_shape.negative:
mesh_material = plugin.get_material("exclusive", gizmo)
else:
mesh_material = plugin.get_material("inclusive", gizmo)
gizmo.add_mesh(mesh, mesh_material)
func _set_radius(sphere: SphereShape, radius: float) -> void:
if sphere:
sphere.radius = radius

View File

@@ -0,0 +1,249 @@
@tool
class_name ProtonScatterPathShape
extends ProtonScatterBaseShape
const Bounds := preload("../common/bounds.gd")
@export var closed := true:
set(val):
closed = val
emit_changed()
@export var thickness := 0.0:
set(val):
thickness = max(0, val) # Width cannot be negative
_half_thickness_squared = pow(thickness * 0.5, 2)
emit_changed()
@export var curve: Curve3D:
set(val):
# Disconnect previous signal
if curve and curve.changed.is_connected(_on_curve_changed):
curve.changed.disconnect(_on_curve_changed)
curve = val
curve.changed.connect(_on_curve_changed)
emit_changed()
var _polygon: PolygonPathFinder
var _half_thickness_squared: float
var _bounds: Bounds
func is_point_inside(point: Vector3, global_transform: Transform3D) -> bool:
if not _polygon:
_update_polygon_from_curve()
if not _polygon:
return false
point = global_transform.affine_inverse() * point
if thickness > 0:
var closest_point_on_curve: Vector3 = curve.get_closest_point(point)
var dist2 = closest_point_on_curve.distance_squared_to(point)
if dist2 < _half_thickness_squared:
return true
if closed:
return _polygon.is_point_inside(Vector2(point.x, point.z))
return false
func get_corners_global(gt: Transform3D) -> Array:
var res := []
if not curve:
return res
var half_thickness = thickness * 0.5
var corners = [
Vector3(-1, -1, -1),
Vector3(1, -1, -1),
Vector3(1, -1, 1),
Vector3(-1, -1, 1),
Vector3(-1, 1, -1),
Vector3(1, 1, -1),
Vector3(1, 1, 1),
Vector3(-1, 1, 1),
]
var points = curve.tessellate(3, 10)
for p in points:
res.push_back(gt * p)
if thickness > 0:
for offset in corners:
res.push_back(gt * (p + offset * half_thickness))
return res
func get_bounds() -> Bounds:
if not _bounds:
_update_polygon_from_curve()
return _bounds
func get_copy():
var copy = get_script().new()
copy.thickness = thickness
copy.closed = closed
if curve:
copy.curve = curve.duplicate()
return copy
func copy_from(source) -> void:
thickness = source.thickness
if source.curve:
curve = source.curve.duplicate() # TODO, update signals
# TODO: create points in the middle of the path
func create_point(position: Vector3) -> void:
if not curve:
curve = Curve3D.new()
curve.add_point(position)
func remove_point(index):
if index > curve.get_point_count() - 1:
return
curve.remove_point(index)
func get_closest_to(position):
if curve.get_point_count() == 0:
return -1
var closest = -1
var dist_squared = -1
for i in curve.get_point_count():
var point_pos: Vector3 = curve.get_point_position(i)
var point_dist: float = point_pos.distance_squared_to(position)
if (closest == -1) or (dist_squared > point_dist):
closest = i
dist_squared = point_dist
var threshold = 16 # Ignore if the closest point is farther than this
if dist_squared >= threshold:
return -1
return closest
func get_closed_edges(shape_t: Transform3D) -> Array[PackedVector2Array]:
if not closed and thickness <= 0:
return []
if not curve:
return []
var edges: Array[PackedVector2Array] = []
var polyline := PackedVector2Array()
var shape_t_inverse := shape_t.affine_inverse()
var points := curve.tessellate(5, 5) # TODO: find optimal values
for p in points:
p *= shape_t_inverse # Apply the shape node transform
polyline.push_back(Vector2(p.x, p.z))
if closed:
# Ensure the polygon is closed
var first_point: Vector3 = points[0]
var last_point: Vector3 = points[-1]
if first_point != last_point:
first_point *= shape_t_inverse
polyline.push_back(Vector2(first_point.x, first_point.z))
# Prevents the polyline to be considered as a hole later.
if Geometry2D.is_polygon_clockwise(polyline):
polyline.reverse()
# Expand the polyline to get the outer edge of the path.
if thickness > 0:
# WORKAROUND. We cant specify the round end caps resolution, but it's tied to the polyline
# size. So we scale everything up before calling offset_polyline(), then scale the result
# down so we get rounder caps.
var scale = 5.0 * thickness
var delta = (thickness / 2.0) * scale
var t2 = Transform2D().scaled(Vector2.ONE * scale)
var result := Geometry2D.offset_polyline(polyline * t2, delta, Geometry2D.JOIN_ROUND, Geometry2D.END_ROUND)
t2 = Transform2D().scaled(Vector2.ONE * (1.0 / scale))
for polygon in result:
edges.push_back(polygon * t2)
if closed and thickness == 0.0:
edges.push_back(polyline)
return edges
func get_open_edges(shape_t: Transform3D) -> Array[Curve3D]:
if not curve or closed or thickness > 0:
return []
var res := Curve3D.new()
var shape_t_inverse := shape_t.affine_inverse()
for i in curve.get_point_count():
var pos = curve.get_point_position(i)
var pos_t = pos * shape_t_inverse
var p_in = (curve.get_point_in(i) + pos) * shape_t_inverse - pos_t
var p_out = (curve.get_point_out(i) + pos) * shape_t_inverse - pos_t
res.add_point(pos_t, p_in, p_out)
return [res]
func _update_polygon_from_curve() -> void:
var connections = PackedInt32Array()
var polygon_points = PackedVector2Array()
if not _bounds:
_bounds = Bounds.new()
_bounds.clear()
_polygon = PolygonPathFinder.new()
if not curve:
curve = Curve3D.new()
if curve.get_point_count() == 0:
return
var baked_points = curve.tessellate(4, 6)
var steps := baked_points.size()
for i in baked_points.size():
var point = baked_points[i]
var projected_point = Vector2(point.x, point.z)
_bounds.feed(point)
polygon_points.push_back(projected_point)
connections.append(i)
if i == steps - 1:
connections.append(0)
else:
connections.append(i + 1)
_bounds.compute_bounds()
_polygon.setup(polygon_points, connections)
func _on_curve_changed() -> void:
_update_polygon_from_curve()
emit_changed()

View File

@@ -0,0 +1,71 @@
@tool
class_name ProtonScatterSphereShape
extends ProtonScatterBaseShape
@export var radius := 1.0:
set(val):
radius = val
_radius_squared = val * val
emit_changed()
var _radius_squared := 0.0
func get_copy():
var copy = ProtonScatterSphereShape.new()
copy.radius = radius
return copy
func is_point_inside(point: Vector3, global_transform: Transform3D) -> bool:
var shape_center = global_transform * Vector3.ZERO
return shape_center.distance_squared_to(point) < _radius_squared
func get_corners_global(gt: Transform3D) -> Array:
var res := []
var corners := [
Vector3(-1, -1, -1),
Vector3(-1, -1, 1),
Vector3(1, -1, 1),
Vector3(1, -1, -1),
Vector3(-1, 1, -1),
Vector3(-1, 1, 1),
Vector3(1, 1, 1),
Vector3(1, 1, -1),
]
for c in corners:
c *= radius
res.push_back(gt * c)
return res
# Returns the circle matching the intersection between the scatter node XZ plane
# and the sphere. Returns an empty array if there's no intersection.
func get_closed_edges(shape_t: Transform3D) -> Array[PackedVector2Array]:
var edge := PackedVector2Array()
var plane := Plane(Vector3.UP, 0.0)
var sphere_center := shape_t.origin
var dist2plane = plane.distance_to(sphere_center)
var radius_at_ground_level := sqrt(pow(radius, 2) - pow(dist2plane, 2))
# No intersection with plane
if radius_at_ground_level <= 0.0 or radius_at_ground_level > radius:
return []
var origin := Vector2(sphere_center.x, sphere_center.z)
var steps: int = max(16, int(radius_at_ground_level * 12))
var angle: float = TAU / steps
for i in steps + 1:
var theta = angle * i
var point := origin + Vector2(cos(theta), sin(theta)) * radius_at_ground_level
edge.push_back(point)
return [edge]