Files
MovementTests/addons/csg_toolkit/scripts/csg_side_toolkit_bar.gd
Minimata 2b74c9e70c
All checks were successful
Create tag and build when new code gets to main / BumpTag (push) Successful in 20s
Create tag and build when new code gets to main / Test (push) Successful in 7m54s
Create tag and build when new code gets to main / Export (push) Successful in 9m52s
added CSG toolkit
2026-02-06 18:35:38 +01:00

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()