Files
millimeters-of-aluminum/addons/module_builder_plugin/module_builder_editor_plugin.gd
2025-10-02 17:22:15 +02:00

433 lines
15 KiB
GDScript

@tool
class_name BuilderEditor
extends EditorPlugin
# --- Constants and Scene References ---
const MAIN_EDITOR_SCENE = preload("res://addons/module_builder_plugin/module_editor.tscn")
const BUILDER_DOCK_SCENE = preload("res://addons/module_builder_plugin/builder_dock.tscn")
const CONSTRUCTION_TREE_SCENE = preload("res://addons/module_builder_plugin/construction_tree.tscn")
const CONSTRUCTION_INSPECTOR_SCENE = preload("res://addons/module_builder_plugin/construction_inspector.tscn")
const MODULE_SCENE = preload("res://scenes/ship/builder/module.tscn")
# --- Dock references ---
var builder_dock: Control
var construction_tree_dock: Control
var construction_inspector_dock: Control
var tree_control: Tree
# --- Node References from the main screen scene ---
var builder_world: World2D
var main_screen: Control
var main_viewport: SubViewport
var zoom_label: Label
var rotate_button: Button
var center_button: Button
var pressurize_button: Button
var save_button: Button
var builder_scene_root: Node2D
var builder_camera: Camera2D
# --- State Variables ---
var preview_piece: StructuralPiece = null
var active_piece_scene: PackedScene = null
var rotation_angle: float = 0.0
var grid_size: float = 50.0
var undo_redo: EditorUndoRedoManager
func _enter_tree():
main_screen = MAIN_EDITOR_SCENE.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_screen)
main_viewport = main_screen.find_child("SubViewport")
builder_camera = main_screen.find_child("Camera2D")
# Get button and label references
zoom_label = main_screen.find_child("ZoomLabel")
rotate_button = main_screen.find_child("RotateButton")
center_button = main_screen.find_child("CenterButton")
pressurize_button = main_screen.find_child("PressuriseButton")
save_button = main_screen.find_child("SaveButton")
_setup_builder_world()
_setup_button_connections()
_update_ui_labels()
main_screen.hide()
undo_redo = EditorInterface.get_editor_undo_redo()
undo_redo.action_is_committing.connect(_on_undo_redo_action_committed)
func _setup_builder_world():
builder_world = World2D.new()
if is_instance_valid(main_viewport):
main_viewport.world_2d = builder_world
builder_scene_root = Node2D.new()
builder_scene_root.name = "BuilderRoot"
main_viewport.add_child(builder_scene_root)
func _setup_docks():
if BUILDER_DOCK_SCENE:
builder_dock = BUILDER_DOCK_SCENE.instantiate()
builder_dock.active_piece_set.connect(on_active_piece_set)
add_control_to_bottom_panel(builder_dock, "Ship Builder")
if CONSTRUCTION_TREE_SCENE:
construction_tree_dock = CONSTRUCTION_TREE_SCENE.instantiate()
tree_control = construction_tree_dock.find_child("Tree")
add_control_to_dock(DOCK_SLOT_LEFT_UR, construction_tree_dock)
_refresh_tree_display()
builder_world.changed.connect(_refresh_tree_display)
if CONSTRUCTION_INSPECTOR_SCENE:
construction_inspector_dock = CONSTRUCTION_INSPECTOR_SCENE.instantiate()
add_control_to_dock(DOCK_SLOT_RIGHT_UL, construction_inspector_dock)
func switch_to_dock_tab(dock_control: Control, tab_name: String):
# Find the TabContainer within the dock's control node.
var tab_container = dock_control.find_child("TabContainer")
if not is_instance_valid(tab_container):
print("Error: TabContainer not found in dock control.")
return
# Iterate through the tabs to find the one with the correct name.
for i in range(tab_container.get_tab_count()):
if tab_container.get_tab_title(i) == tab_name:
tab_container.current_tab = i
return
print("Warning: Tab '%s' not found." % tab_name)
func _teardown_docks():
if builder_dock:
remove_control_from_bottom_panel(builder_dock)
builder_dock.queue_free()
if construction_tree_dock:
remove_control_from_docks(construction_tree_dock)
construction_tree_dock.queue_free()
builder_world.changed.disconnect(_refresh_tree_display)
if construction_inspector_dock:
remove_control_from_docks(construction_inspector_dock)
construction_inspector_dock.queue_free()
func _exit_tree():
if main_screen:
main_screen.queue_free()
func _has_main_screen() -> bool:
return true
func _make_visible(visible):
if main_screen:
main_screen.visible = visible
_setup_gui_input_listener(visible)
if visible:
_setup_docks()
else:
_teardown_docks()
func _get_plugin_name():
return "Ship Builder"
func _get_plugin_icon():
return EditorInterface.get_editor_theme().get_icon("Node", "EditorIcons")
func _setup_gui_input_listener(connect: bool):
if main_screen:
if connect:
main_screen.gui_input.connect(_on_viewport_input)
else:
main_screen.gui_input.disconnect(_on_viewport_input)
func _setup_button_connections():
if rotate_button: rotate_button.pressed.connect(_on_rotate_button_pressed)
if center_button: center_button.pressed.connect(_on_center_button_pressed)
if pressurize_button: pressurize_button.pressed.connect(_on_pressurise_button_pressed)
if save_button: save_button.pressed.connect(_on_save_button_pressed)
func _update_ui_labels():
if is_instance_valid(zoom_label):
var zoom_percent = int(builder_camera.zoom.x * 100)
zoom_label.text = "Zoom: %d%%" % zoom_percent
func _process(_delta):
_update_ui_labels()
_refresh_tree_display()
func _on_viewport_input(event: InputEvent) -> void:
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_RIGHT:
builder_camera.position -= event.relative / builder_camera.zoom
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_WHEEL_UP:
builder_camera.zoom *= 1.1
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
builder_camera.zoom /= 1.1
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
if is_instance_valid(preview_piece):
on_clear_preview_piece()
else:
_remove_piece_under_mouse()
if event is InputEventMouseMotion:
_update_preview_position()
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
_place_piece_from_preview()
func _unhandled_key_input(event: InputEvent):
if not event.is_pressed(): return
if event is InputEventKey and event.as_text() == "R":
_on_rotate_button_pressed()
func on_active_piece_set(scene: PackedScene):
if is_instance_valid(preview_piece):
preview_piece.queue_free()
active_piece_scene = scene
preview_piece = scene.instantiate() as StructuralPiece
preview_piece.is_preview = true
builder_scene_root.add_child(preview_piece)
_update_preview_position()
func on_clear_preview_piece():
if is_instance_valid(preview_piece):
preview_piece.queue_free()
preview_piece = null
active_piece_scene = null
func _update_preview_position():
if not is_instance_valid(preview_piece):
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
preview_piece.global_position = snapped_pos
preview_piece.rotation = rotation_angle
func _place_piece_from_preview():
if not is_instance_valid(preview_piece):
return
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
var target_module = _find_nearby_modules(snapped_pos)
if not target_module:
target_module = MODULE_SCENE.instantiate() as Module
builder_scene_root.add_child(target_module)
target_module.global_position = snapped_pos
target_module.owner = builder_scene_root
var piece_to_place = active_piece_scene.instantiate()
target_module.structural_container.add_child(piece_to_place)
piece_to_place.owner = target_module
piece_to_place.rotation = rotation_angle
piece_to_place.global_position = snapped_pos
# --- The Undo/Redo Logic ---
undo_redo.create_action("Place Structural Piece")
# DO method: adds the piece to the scene.
undo_redo.add_do_method(target_module.structural_container, "add_child", piece_to_place)
undo_redo.add_do_method(piece_to_place, "set_owner", target_module)
# DO method: recalculates physics.
undo_redo.add_do_method(target_module, "_recalculate_collision_shape")
# UNDO method: removes the piece from the scene parent.
undo_redo.add_undo_method(target_module.structural_container, "remove_child", piece_to_place)
# UNDO method: recalculates physics.
undo_redo.add_undo_method(target_module, "_recalculate_collision_shape")
undo_redo.commit_action()
func _find_nearby_modules(position: Vector2) -> Module:
# Define a margin for the overlap check.
const OVERLAP_MARGIN = 20.0
# Get the shape from the active piece scene.
var piece_shape = active_piece_scene.instantiate().find_child("CollisionShape2D").shape
# Create a temporary, slightly larger shape for the overlap check.
var enlarged_shape
if piece_shape is RectangleShape2D:
enlarged_shape = RectangleShape2D.new()
enlarged_shape.size = piece_shape.size + Vector2(OVERLAP_MARGIN, OVERLAP_MARGIN) * 2
elif piece_shape is CapsuleShape2D:
enlarged_shape = CapsuleShape2D.new()
enlarged_shape.radius = piece_shape.radius + OVERLAP_MARGIN
enlarged_shape.height = piece_shape.height + OVERLAP_MARGIN
else:
# Fallback for other shapes
return null
# Use a PhysicsShapeQuery to find overlapping pieces.
var space_state = builder_world.direct_space_state
var query = PhysicsShapeQueryParameters2D.new()
query.set_shape(enlarged_shape)
query.transform = Transform2D(0, position)
var result = space_state.intersect_shape(query, 1) # Limit to a single result
if not result.is_empty():
var collider = result[0].get("collider")
if collider is StructuralPiece:
if collider.get_parent() and collider.get_parent().get_parent() is Module:
return collider.get_parent().get_parent()
return null
func _remove_piece_under_mouse():
var viewport: SubViewport = main_screen.find_child("SubViewport")
if not viewport: return
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
for node in builder_scene_root.get_children():
if node is Module:
for piece in node.structural_container.get_children():
if piece is StructuralPiece and piece.global_position == snapped_pos:
_remove_piece_with_undo_redo(piece)
return
func _remove_piece_with_undo_redo(piece: StructuralPiece):
var module = piece.owner as Module
var parent = piece.get_parent()
if not is_instance_valid(module) or not module is Module:
return
undo_redo.create_action("Remove Structural Piece")
print(module.structural_container.get_child_count(false))
if module.structural_container.get_child_count(false) >= 1:
undo_redo.add_do_method(builder_scene_root, "remove_child", module)
undo_redo.add_undo_method(builder_scene_root, "add_child", module)
else:
undo_redo.add_do_method(parent, "remove_child", piece)
undo_redo.add_do_method(module, "_recalculate_collision_shape")
undo_redo.add_undo_method(parent, "add_child", piece)
undo_redo.add_undo_method(module, "_recalculate_collision_shape")
undo_redo.commit_action()
# --- Toolbar Button Functions ---
func _on_rotate_button_pressed():
rotation_angle = wrapf(rotation_angle + PI / 2, 0, TAU)
if preview_piece:
preview_piece.rotation = rotation_angle
_update_preview_position()
func _on_center_button_pressed():
builder_camera.position = Vector2.ZERO
builder_camera.zoom = Vector2(1.0, 1.0)
func _on_pressurise_button_pressed():
pass
func _on_save_button_pressed():
# Find a module to save. We'll prioritize the selected one.
var module_to_save: Module
var selected_nodes = EditorInterface.get_selection().get_selected_nodes()
if not selected_nodes.is_empty() and selected_nodes[0] is Module:
module_to_save = selected_nodes[0]
elif builder_scene_root.get_child_count() > 0:
for node in builder_scene_root.get_children():
if node is Module:
module_to_save = node
break
if not is_instance_valid(module_to_save):
push_error("Error: No Module node found or selected to save.")
return
# Create and configure the save dialog
var save_dialog = EditorFileDialog.new()
save_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
save_dialog.add_filter("*.tscn; Godot Scene")
save_dialog.current_path = "res://modules/" + module_to_save.name + ".tscn"
# FIX: Add the dialog to the main editor screen, not the plugin's control node
EditorInterface.get_editor_main_screen().add_child(save_dialog)
save_dialog.popup_centered_ratio()
# Connect the signal to our new save function
save_dialog.file_selected.connect(Callable(self, "_perform_save").bind(module_to_save))
func _perform_save(file_path: String, module_to_save: Module):
# Make sure the directory exists before attempting to save.
var save_dir = file_path.get_base_dir()
var dir = DirAccess.open("res://")
if not dir.dir_exists(save_dir):
dir.make_dir_recursive(save_dir)
#
## FIX: Manually get the structural container reference from the duplicated module.
#var duplicate_structural_container = duplicate_module.find_child("StructuralContainer")
#
#if is_instance_valid(duplicate_structural_container):
## FIX: Correctly set the owner of all child nodes to the new root.
## This is the crucial step to ensure the children are packed correctly.
#for piece in duplicate_structural_container.get_children():
#print(piece)
#piece.owner = duplicate_module
# Pack the node into a PackedScene.
var packed_scene = PackedScene.new()
var error = packed_scene.pack(module_to_save)
if error != OK:
push_error("Error packing scene: ", error_string(error))
return
# FIX: Reset the duplicated module's position so it's centered in its own scene.
#duplicate_module.global_position = Vector2.ZERO
# Save the PackedScene to a file.
var save_result = ResourceSaver.save(packed_scene, file_path)
if save_result == OK:
print("Module saved successfully to ", file_path)
else:
push_error("Error saving scene: ", error_string(save_result))
EditorInterface.get_resource_filesystem().scan()
func _on_undo_redo_action_committed():
_refresh_tree_display()
func _refresh_tree_display():
if not is_instance_valid(tree_control):
return
tree_control.clear()
var root_item = tree_control.create_item()
root_item.set_text(0, builder_scene_root.name)
# Iterate through all modules and populate the tree.
for module in builder_scene_root.get_children():
if module is Module:
var module_item = tree_control.create_item(root_item)
module_item.set_text(0, module.name)
module_item.set_meta("node", module)
for piece in module.structural_container.get_children():
if piece is StructuralPiece:
var piece_item = tree_control.create_item(module_item)
piece_item.set_text(0, piece.name)
piece_item.set_meta("node", piece)