@tool class_name CsgBlockout extends EditorPlugin var config: CsgBlockoutConfig: get: return CsgBlockoutConfig.get_config() var sidebar: CSGSideBlockoutBar var topbar: CSGTopBlockoutBar static var csg_plugin_path: String static var undo_manager: EditorUndoRedoManager var pie_menu: CsgPieMenu var pie_menu_tab_pressed_time: int = 0 func _get_shape_menu() -> Array[Dictionary]: return [ {"label": CsgBlockoutI18n.t("BOX"), "type": "create_csg", "csg_type": "CSGBox3D"}, {"label": CsgBlockoutI18n.t("CYLINDER"), "type": "create_csg", "csg_type": "CSGCylinder3D"}, {"label": CsgBlockoutI18n.t("MESH"), "type": "create_csg", "csg_type": "CSGMesh3D"}, {"label": CsgBlockoutI18n.t("POLYGON"), "type": "create_csg", "csg_type": "CSGPolygon3D"}, {"label": CsgBlockoutI18n.t("SPHERE"), "type": "create_csg", "csg_type": "CSGSphere3D"}, {"label": CsgBlockoutI18n.t("TORUS"), "type": "create_csg", "csg_type": "CSGTorus3D"} ] func _get_pie_menu_items() -> Array[Dictionary]: return [ { "label": CsgBlockoutI18n.t("UNION"), "type": "submenu", "operation": 0, "children": _get_shape_menu() }, { "label": CsgBlockoutI18n.t("INTERSECTION"), "type": "submenu", "operation": 1, "children": _get_shape_menu() }, { "label": CsgBlockoutI18n.t("SUBTRACTION"), "type": "submenu", "operation": 2, "children": _get_shape_menu() } ] func _enter_tree() -> void: csg_plugin_path = get_script().get_path().get_base_dir() undo_manager = get_undo_redo() # Nodes add_custom_type("CSGRepeater3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_repeater_3d.gd"), null) add_custom_type("CSGSpreader3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_spreader_3d.gd"), null) # Sidebar var sidebarScene = preload("res://addons/csg_blockout/scenes/csg_side_blockout_bar.tscn") sidebar = sidebarScene.instantiate() add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar) # Topbar var topbarScene = preload("res://addons/csg_blockout/scenes/csg_top_blockout_bar.tscn") topbar = topbarScene.instantiate() add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar) func _handles(object: Object) -> bool: return object is Node3D func _forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> int: if event is InputEventKey and event.keycode == KEY_A and not event.echo and _is_action_key_held(event): if event.pressed: if not is_instance_valid(pie_menu): _open_pie_menu() return EditorPlugin.AFTER_GUI_INPUT_STOP else: if is_instance_valid(pie_menu): if Time.get_ticks_msec() - pie_menu_tab_pressed_time > 200: # Hold mode release pie_menu.execute_active_item(true) return EditorPlugin.AFTER_GUI_INPUT_STOP if is_instance_valid(pie_menu): if event is InputEventMouseButton: if event.button_index == MOUSE_BUTTON_LEFT and event.pressed: pie_menu.execute_active_item(true) return EditorPlugin.AFTER_GUI_INPUT_STOP elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed: pie_menu.execute_back() return EditorPlugin.AFTER_GUI_INPUT_STOP return EditorPlugin.AFTER_GUI_INPUT_STOP if event is InputEventMouseMotion: return EditorPlugin.AFTER_GUI_INPUT_STOP return EditorPlugin.AFTER_GUI_INPUT_PASS func _is_action_key_held(event: InputEventWithModifiers) -> bool: var key: Key = config.action_key if config else KEY_SHIFT return (key == KEY_SHIFT and event.shift_pressed) \ or (key == KEY_CTRL and event.ctrl_pressed) \ or (key == KEY_ALT and event.alt_pressed) \ or (key == KEY_META and event.meta_pressed) func _open_pie_menu() -> void: pie_menu = CsgPieMenu.new() pie_menu.action_triggered.connect(_on_pie_menu_action_triggered) pie_menu.close_requested.connect(_close_pie_menu) var ed_scale: float = EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0 pie_menu.apply_editor_scale(ed_scale) var base_control: Control = EditorInterface.get_base_control() base_control.add_child(pie_menu) var mouse_pos: Vector2 = base_control.get_local_mouse_position() # Clamp position var margin: float = pie_menu.outer_radius + 10.0 * ed_scale var rect_size: Vector2 = base_control.get_rect().size mouse_pos.x = clampf(mouse_pos.x, margin, rect_size.x - margin) mouse_pos.y = clampf(mouse_pos.y, margin, rect_size.y - margin) pie_menu.position = mouse_pos pie_menu.setup(_get_pie_menu_items(), ed_scale) pie_menu_tab_pressed_time = Time.get_ticks_msec() func _close_pie_menu() -> void: if is_instance_valid(pie_menu): pie_menu.queue_free() pie_menu = null func _on_pie_menu_action_triggered(item: Dictionary) -> void: var action_type = item.get("type", "") if action_type == "submenu" and item.has("operation"): var op = item.get("operation") if config: config.default_operation = op var selection = EditorInterface.get_selection().get_selected_nodes() if selection.size() > 0 and selection[0] is CSGShape3D: var node = selection[0] as CSGShape3D undo_manager.create_action(CsgBlockoutI18n.t("CHANGE_CSG_OP")) undo_manager.add_do_property(node, "operation", op) undo_manager.add_undo_property(node, "operation", node.operation) undo_manager.commit_action() elif action_type == "create_csg": var csg_type = item.get("csg_type", "") if csg_type != "" and ClassDB.can_instantiate(csg_type): _create_csg_node(csg_type) func _create_csg_node(csg_type: String) -> void: var new_node: Node3D = ClassDB.instantiate(csg_type) as Node3D if not new_node: return if new_node is CSGShape3D: new_node.operation = config.default_operation if config else CSGShape3D.OPERATION_UNION if config: new_node.material = config.get_active_material() var selection = EditorInterface.get_selection() var selected_nodes = selection.get_selected_nodes() var parent: Node = null var insert_index: int = -1 var target_pos: Vector3 = Vector3.ZERO if selected_nodes.size() > 0 and selected_nodes[0] is Node3D: var selected_node: Node3D = selected_nodes[0] target_pos = selected_node.global_position if selected_node is CSGCombiner3D: parent = selected_node insert_index = parent.get_child_count() elif selected_node is CSGShape3D: parent = selected_node.get_parent() if parent == null: parent = selected_node insert_index = parent.get_child_count() else: insert_index = selected_node.get_index() + 1 else: parent = selected_node insert_index = parent.get_child_count() else: parent = EditorInterface.get_edited_scene_root() if parent: insert_index = parent.get_child_count() if not parent: new_node.free() return var owner_ref = EditorInterface.get_edited_scene_root() if owner_ref == null: owner_ref = parent undo_manager.create_action(CsgBlockoutI18n.tf("CREATE_NODE", [csg_type])) undo_manager.add_undo_reference(new_node) undo_manager.add_do_method(self, "_undoable_create_csg", parent, new_node, owner_ref, target_pos, insert_index) undo_manager.add_do_method(self, "_select_node", new_node) undo_manager.add_undo_method(self, "_undoable_remove_csg", parent, new_node) undo_manager.commit_action() func _undoable_create_csg(parent: Node, node: Node3D, owner_ref: Node, global_pos: Vector3, insert_index: int) -> void: if node.get_parent() != parent: parent.add_child(node, true) if insert_index >= 0 and insert_index < parent.get_child_count(): parent.move_child(node, insert_index) node.owner = owner_ref node.global_position = global_pos func _undoable_remove_csg(parent: Node, node: Node3D) -> void: if is_instance_valid(node) and node.get_parent() == parent: parent.remove_child(node) func _select_node(node: Node) -> void: if not is_instance_valid(node) or not node.is_inside_tree(): return var selection = EditorInterface.get_selection() if selection: selection.clear() selection.add_node(node) func _exit_tree() -> void: remove_custom_type("CSGRepeater3D") remove_custom_type("CSGSpreader3D") undo_manager = null if sidebar and is_instance_valid(sidebar): remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar) sidebar.free() if topbar and is_instance_valid(topbar): remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar) topbar.free()