Added footsteps, new tree, various other tweaks
This commit is contained in:
36
addons/proton_scatter/src/shapes/base_shape.gd
Normal file
36
addons/proton_scatter/src/shapes/base_shape.gd
Normal 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
|
||||
95
addons/proton_scatter/src/shapes/box_shape.gd
Normal file
95
addons/proton_scatter/src/shapes/box_shape.gd
Normal 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]
|
||||
135
addons/proton_scatter/src/shapes/gizmos_plugin/box_gizmo.gd
Normal file
135
addons/proton_scatter/src/shapes/gizmos_plugin/box_gizmo.gd
Normal 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
|
||||
@@ -0,0 +1,3 @@
|
||||
[gd_resource type="ButtonGroup" format=3 uid="uid://1xy55037k3k5"]
|
||||
|
||||
[resource]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
356
addons/proton_scatter/src/shapes/gizmos_plugin/path_gizmo.gd
Normal file
356
addons/proton_scatter/src/shapes/gizmos_plugin/path_gizmo.gd
Normal 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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
249
addons/proton_scatter/src/shapes/path_shape.gd
Normal file
249
addons/proton_scatter/src/shapes/path_shape.gd
Normal 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()
|
||||
71
addons/proton_scatter/src/shapes/sphere_shape.gd
Normal file
71
addons/proton_scatter/src/shapes/sphere_shape.gd
Normal 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]
|
||||
Reference in New Issue
Block a user