233 lines
8.1 KiB
GDScript
233 lines
8.1 KiB
GDScript
@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()
|