237 lines
7.6 KiB
GDScript
237 lines
7.6 KiB
GDScript
@tool
|
|
class_name CSGSideToolkitBar extends Control
|
|
|
|
@onready var config: CsgTkConfig:
|
|
get:
|
|
return get_tree().root.get_node_or_null(CsgToolkit.AUTOLOAD_NAME) as CsgTkConfig
|
|
|
|
var operation: CSGShape3D.Operation = CSGShape3D.OPERATION_UNION
|
|
var selected_material: BaseMaterial3D
|
|
var selected_shader: ShaderMaterial
|
|
|
|
@onready var picker_button: Button = $ScrollContainer/HBoxContainer/Material/MaterialPicker
|
|
|
|
func _enter_tree():
|
|
EditorInterface.get_selection().selection_changed.connect(_on_selection_changed)
|
|
|
|
func _exit_tree():
|
|
EditorInterface.get_selection().selection_changed.disconnect(_on_selection_changed)
|
|
|
|
func _on_selection_changed():
|
|
if not config.auto_hide:
|
|
return
|
|
var selection = EditorInterface.get_selection().get_selected_nodes()
|
|
if selection.any(func (node): return node is CSGShape3D):
|
|
show()
|
|
else:
|
|
hide()
|
|
|
|
func _ready():
|
|
picker_button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
# Connect material picker button if not connected via scene
|
|
if not picker_button.pressed.is_connected(_on_material_picker_pressed):
|
|
picker_button.pressed.connect(_on_material_picker_pressed)
|
|
set_process_unhandled_key_input(true)
|
|
|
|
func _unhandled_key_input(event: InputEvent):
|
|
# Shortcut: action_key + 1/2/3 to set operation (Union / Intersection / Subtraction)
|
|
if not (event is InputEventKey):
|
|
return
|
|
var ev := event as InputEventKey
|
|
if ev.pressed and not ev.echo:
|
|
# Ensure action key is held (config.action_key)
|
|
if Input.is_key_pressed(config.action_key):
|
|
match ev.physical_keycode:
|
|
KEY_1, KEY_KP_1:
|
|
set_operation(0)
|
|
_accept_shortcut_feedback("Union")
|
|
KEY_2, KEY_KP_2:
|
|
set_operation(1)
|
|
_accept_shortcut_feedback("Intersection")
|
|
KEY_3, KEY_KP_3:
|
|
set_operation(2)
|
|
_accept_shortcut_feedback("Subtraction")
|
|
|
|
func _accept_shortcut_feedback(label: String):
|
|
# Provide lightweight visual/editor feedback. Avoid static call to non-existent get_status_bar in Godot 4.
|
|
# Fallback: print to output.
|
|
var ei = EditorInterface
|
|
if ei:
|
|
# Some editor builds expose status bar via base control's children - skip deep search for now.
|
|
print("CSG Operation: %s" % label)
|
|
else:
|
|
print("CSG Operation: %s" % label)
|
|
|
|
func _on_box_pressed():
|
|
create_csg(CSGBox3D)
|
|
|
|
func _on_cylinder_pressed():
|
|
create_csg(CSGCylinder3D)
|
|
|
|
func _on_mesh_pressed():
|
|
create_csg(CSGMesh3D)
|
|
|
|
func _on_polygon_pressed():
|
|
create_csg(CSGPolygon3D)
|
|
|
|
func _on_sphere_pressed():
|
|
create_csg(CSGSphere3D)
|
|
|
|
func _on_torus_pressed():
|
|
create_csg(CSGTorus3D)
|
|
|
|
# Operation Toggle (accept optional arg for signal variations)
|
|
func _on_operation_pressed(val := 0):
|
|
set_operation(val)
|
|
|
|
func _on_config_pressed():
|
|
var config_view_scene = preload("res://addons/csg_toolkit/scenes/config_window.tscn")
|
|
var config_view = config_view_scene.instantiate()
|
|
config_view.close_requested.connect(func ():
|
|
get_tree().root.remove_child(config_view)
|
|
config_view.queue_free()
|
|
)
|
|
get_tree().root.add_child(config_view)
|
|
|
|
func _request_material():
|
|
var dialog = EditorFileDialog.new()
|
|
dialog.title = "Select Material"
|
|
dialog.display_mode = EditorFileDialog.DISPLAY_LIST
|
|
dialog.filters = ["*.tres, *.material, *.res"]
|
|
dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
|
|
dialog.position = ((EditorInterface.get_base_control().size / 2) as Vector2i) - dialog.size
|
|
dialog.close_requested.connect(func ():
|
|
get_tree().root.remove_child(dialog)
|
|
dialog.queue_free()
|
|
)
|
|
get_tree().root.add_child(dialog)
|
|
dialog.show()
|
|
var res_path = await dialog.file_selected
|
|
var res = ResourceLoader.load(res_path)
|
|
if res == null:
|
|
return
|
|
if res is BaseMaterial3D:
|
|
update_material(res)
|
|
elif res is ShaderMaterial:
|
|
update_shader(res)
|
|
else:
|
|
return
|
|
var previewer = EditorInterface.get_resource_previewer()
|
|
previewer.queue_edited_resource_preview(res, self, "_update_picker_icon", null)
|
|
|
|
func _update_picker_icon(path, preview, thumbnail, userdata):
|
|
if preview:
|
|
picker_button.icon = preview
|
|
|
|
func set_operation(val: int):
|
|
match val:
|
|
0: operation = CSGShape3D.OPERATION_UNION
|
|
1: operation = CSGShape3D.OPERATION_INTERSECTION
|
|
2: operation = CSGShape3D.OPERATION_SUBTRACTION
|
|
_: operation = CSGShape3D.OPERATION_UNION
|
|
|
|
func update_material(material: BaseMaterial3D):
|
|
selected_material = material
|
|
selected_shader = null
|
|
|
|
func update_shader(shader: ShaderMaterial):
|
|
selected_material = null
|
|
selected_shader = shader
|
|
|
|
func create_csg(type: Variant):
|
|
var selection = EditorInterface.get_selection()
|
|
var selected_nodes = selection.get_selected_nodes()
|
|
if selected_nodes.is_empty() or !(selected_nodes[0] is CSGShape3D):
|
|
push_warning("Select a CSGShape3D to add a new CSG node")
|
|
return
|
|
var selected_node: CSGShape3D = selected_nodes[0]
|
|
var csg: CSGShape3D
|
|
match type:
|
|
CSGBox3D: csg = CSGBox3D.new()
|
|
CSGCylinder3D: csg = CSGCylinder3D.new()
|
|
CSGSphere3D: csg = CSGSphere3D.new()
|
|
CSGMesh3D: csg = CSGMesh3D.new()
|
|
CSGPolygon3D: csg = CSGPolygon3D.new()
|
|
CSGTorus3D: csg = CSGTorus3D.new()
|
|
|
|
csg.operation = operation
|
|
if selected_material:
|
|
csg.material = selected_material
|
|
elif selected_shader:
|
|
csg.material = selected_shader
|
|
|
|
if (selected_node.get_owner() == null):
|
|
return
|
|
|
|
var parent: Node
|
|
var add_as_child := false
|
|
|
|
# Behavior inversion now uses secondary_action_key (e.g. Alt) instead of primary action key
|
|
var invert := Input.is_key_pressed(config.secondary_action_key)
|
|
if config.default_behavior == CsgTkConfig.CSGBehavior.SIBLING:
|
|
add_as_child = invert
|
|
else:
|
|
add_as_child = !invert
|
|
|
|
parent = selected_node if add_as_child else selected_node.get_parent()
|
|
if parent == null:
|
|
return
|
|
|
|
# Try undo manager path if plugin provided one
|
|
if CsgToolkit.undo_manager:
|
|
var insert_index := parent.get_child_count()
|
|
CsgToolkit.undo_manager.create_action("Add %s" % csg.get_class())
|
|
# DO methods
|
|
CsgToolkit.undo_manager.add_do_method(self, "_undoable_add_csg", parent, csg, selected_node.get_owner(), selected_node.global_position, insert_index)
|
|
CsgToolkit.undo_manager.add_do_method(self, "_select_created_csg", csg)
|
|
# UNDO methods
|
|
CsgToolkit.undo_manager.add_undo_method(self, "_undoable_remove_csg", parent, csg)
|
|
CsgToolkit.undo_manager.add_undo_method(self, "_clear_selection_if", csg)
|
|
CsgToolkit.undo_manager.commit_action()
|
|
else:
|
|
parent.add_child(csg, true)
|
|
csg.owner = selected_node.get_owner()
|
|
csg.global_position = selected_node.global_position
|
|
call_deferred("_select_created_csg", csg)
|
|
|
|
func _deferred_select(csg: Node):
|
|
call_deferred("_select_created_csg", csg)
|
|
|
|
func _undoable_add_csg(parent: Node, csg: CSGShape3D, owner_ref: Node, global_pos: Vector3, insert_index: int):
|
|
if csg.get_parent() != parent:
|
|
parent.add_child(csg, true)
|
|
if insert_index >= 0 and insert_index < parent.get_child_count():
|
|
parent.move_child(csg, insert_index)
|
|
csg.owner = owner_ref
|
|
csg.global_position = global_pos
|
|
|
|
func _undoable_remove_csg(parent: Node, csg: CSGShape3D):
|
|
if csg.get_parent() == parent:
|
|
parent.remove_child(csg)
|
|
# Intentionally do NOT free node so redo can re-add it. If you need memory, implement a recreate pattern instead.
|
|
|
|
func _clear_selection_if(csg: Node):
|
|
var selection = EditorInterface.get_selection()
|
|
if selection:
|
|
var nodes: Array = selection.get_selected_nodes()
|
|
if csg in nodes:
|
|
selection.remove_node(csg)
|
|
|
|
func _select_created_csg(csg: Node):
|
|
var selection = EditorInterface.get_selection()
|
|
selection.clear()
|
|
selection.add_node(csg)
|
|
|
|
func _add_as_child(selected_node: CSGShape3D, csg: CSGShape3D):
|
|
selected_node.add_child(csg, true)
|
|
csg.owner = selected_node.get_owner()
|
|
csg.global_position = selected_node.global_position
|
|
|
|
func _add_as_sibling(selected_node: CSGShape3D, csg: CSGShape3D):
|
|
selected_node.get_parent().add_child(csg, true)
|
|
csg.owner = selected_node.get_owner()
|
|
csg.global_position = selected_node.global_position
|
|
|
|
func _on_material_picker_pressed() -> void:
|
|
_request_material()
|