# Copyright © 2022 Kasper Arnklit Frandsen - MIT License # See `LICENSE.md` included in the source distribution for details. @tool extends EditorPlugin const WaterHelperMethods = preload("./water_helper_methods.gd") const WaterSystem = preload("./water_system_manager.gd") const RiverManager = preload("./river_manager.gd") const RiverGizmo = preload("./river_gizmo.gd") const InspectorPlugin = preload("./inspector_plugin.gd") const ProgressWindow = preload("./gui/progress_window.tscn") const RiverControls = preload("./gui/river_controls.gd") var river_gizmo: RiverGizmo = RiverGizmo.new() var gradient_inspector: InspectorPlugin = InspectorPlugin.new() var _river_controls = preload("./gui/river_controls.tscn").instantiate() var _water_system_controls = preload("./gui/water_system_controls.tscn").instantiate() var _edited_node = null var _progress_window = null var _editor_selection : EditorSelection = null var _heightmap_renderer = null var _mode := "select" var constraint: int = RiverControls.CONSTRAINTS.NONE var local_editing := false func _enter_tree() -> void: add_custom_type("River", "Node3D", preload("./river_manager.gd"), preload("./icons/river.svg")) add_custom_type("WaterSystem", "Node3D", preload("./water_system_manager.gd"), preload("./icons/system.svg")) add_custom_type("Buoyant", "Node3D", preload("./buoyant_manager.gd"), preload("./icons/buoyant.svg")) add_node_3d_gizmo_plugin(river_gizmo) add_inspector_plugin(gradient_inspector) river_gizmo.editor_plugin = self _river_controls.connect("mode", Callable(self, "_on_mode_change")) _river_controls.connect("options", Callable(self, "_on_option_change")) _progress_window = ProgressWindow.instantiate() _river_controls.add_child(_progress_window) _editor_selection = get_editor_interface().get_selection() _editor_selection.connect("selection_changed", Callable(self, "_on_selection_change")) scene_changed.connect(_on_scene_changed) scene_closed.connect(_on_scene_closed) func _on_generate_flowmap_pressed() -> void: _edited_node.bake_texture() func _on_generate_mesh_pressed() -> void: _edited_node.spawn_mesh() func _on_debug_view_changed(index : int) -> void: _edited_node.set_debug_view(index) func _on_generate_system_maps_pressed() -> void: _edited_node.generate_system_maps() func _exit_tree() -> void: remove_custom_type("River") remove_custom_type("Water System") remove_custom_type("Buoyant") remove_node_3d_gizmo_plugin(river_gizmo) remove_inspector_plugin(gradient_inspector) _river_controls.disconnect("mode", Callable(self, "_on_mode_change")) _river_controls.disconnect("options", Callable(self, "_on_option_change")) _editor_selection.disconnect("selection_changed", Callable(self, "_on_selection_change")) disconnect("scene_changed", Callable(self, "_on_scene_changed")); disconnect("scene_closed", Callable(self, "_on_scene_closed")); _hide_river_control_panel() _hide_water_system_control_panel() func _handles(node): return node is RiverManager or node is WaterSystem #func _edit(node): # print("edit(), node is: ", node) # if node is RiverManager: # _show_river_control_panel() # _edited_node = node as RiverManager # if node is WaterSystem: # _show_water_system_control_panel() # _edited_node = node as WaterSystem func _on_selection_change() -> void: _editor_selection = get_editor_interface().get_selection() #print("_on_selection_change(), Selection: ", _editor_selection) var selected = _editor_selection.get_selected_nodes() if len(selected) == 0: return if selected[0] is RiverManager: _show_river_control_panel() _edited_node = selected[0] as RiverManager _river_controls.menu.debug_view_menu_selected = _edited_node.debug_view if not _edited_node.is_connected("progress_notified", Callable(self, "_river_progress_notified")): _edited_node.connect("progress_notified", Callable(self, "_river_progress_notified")) _hide_water_system_control_panel() elif selected[0] is WaterSystem: # TODO - is there anything we need to add here? _show_water_system_control_panel() _edited_node = selected[0] as WaterSystem _hide_river_control_panel() else: print("_edited_node set to null") _edited_node = null _hide_river_control_panel() _hide_water_system_control_panel() func _on_scene_changed(scene_root) -> void: # TODO - Hmmm # print(scene_root) _hide_river_control_panel() _hide_water_system_control_panel() func _on_scene_closed(_value) -> void: _hide_river_control_panel() _hide_water_system_control_panel() func _on_mode_change(mode) -> void: _mode = mode func _on_option_change(option, value) -> void: if option == "constraint": constraint = value if constraint == RiverControls.CONSTRAINTS.COLLIDERS: WaterHelperMethods.reset_all_colliders(_edited_node.get_tree().root) elif option == "local_mode": local_editing = value func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int: if not _edited_node: # TODO - This should be updated to the enum when it's fixed https://github.com/godotengine/godot/pull/64465 return 0 var global_transform: Transform3D = _edited_node.transform if _edited_node.is_inside_tree(): global_transform = _edited_node.get_global_transform() var global_inverse: Transform3D = global_transform.affine_inverse() if (event is InputEventMouseButton) and (event.button_index == MOUSE_BUTTON_LEFT): var ray_from = camera.project_ray_origin(event.position) var ray_dir = camera.project_ray_normal(event.position) var g1 = global_inverse * (ray_from) var g2 = global_inverse * (ray_from + ray_dir * 4096) # Iterate through points to find closest segment var curve_points = _edited_node.get_curve_points() var closest_distance = 4096.0 var closest_segment = -1 for point in curve_points.size() -1: var p1 = curve_points[point] var p2 = curve_points[point + 1] var result = Geometry3D.get_closest_points_between_segments(p1, p2, g1, g2) var dist = result[0].distance_to(result[1]) if dist < closest_distance: closest_distance = dist closest_segment = point # Iterate through baked points to find the closest position on the # curved path var baked_curve_points = _edited_node.curve.get_baked_points() var baked_closest_distance = 4096.0 var baked_closest_point = Vector3() var baked_point_found = false for baked_point in baked_curve_points.size() - 1: var p1 = baked_curve_points[baked_point] var p2 = baked_curve_points[baked_point + 1] var result = Geometry3D.get_closest_points_between_segments(p1, p2, g1, g2) var dist = result[0].distance_to(result[1]) if dist < 0.1 and dist < baked_closest_distance: baked_closest_distance = dist baked_closest_point = result[0] baked_point_found = true # In case we were close enough to a line segment to find a segment, # but not close enough to the curved line if not baked_point_found: closest_segment = -1 # We'll use this closest point to add a point in between if on the line # and to remove if close to a point if _mode == "select": if not event.pressed: river_gizmo.reset() return 0 if _mode == "add" and not event.pressed: # if we don't have a point on the line, we'll calculate a point # based of a plane of the last point of the curve if closest_segment == -1: var end_pos = _edited_node.curve.get_point_position(_edited_node.curve.get_point_count() - 1) var end_pos_global : Vector3 = _edited_node.to_global(end_pos) var z : Vector3 = _edited_node.curve.get_point_out(_edited_node.curve.get_point_count() - 1).normalized() var x := z.cross(Vector3.DOWN).normalized() var y := z.cross(x).normalized() var _handle_base_transform = Transform3D( Basis(x, y, z) * global_transform.basis, end_pos_global ) var plane := Plane(end_pos_global, end_pos_global + camera.transform.basis.x, end_pos_global + camera.transform.basis.y) var new_pos if constraint == RiverControls.CONSTRAINTS.COLLIDERS: var space_state = _edited_node.get_world_3d().direct_space_state var result = space_state.intersect_ray(ray_from, ray_from + ray_dir * 4096) if result: new_pos = result.position else: return 0 elif constraint == RiverControls.CONSTRAINTS.NONE: new_pos = plane.intersects_ray(ray_from, ray_from + ray_dir * 4096) elif constraint in RiverGizmo.AXIS_MAPPING: var axis: Vector3 = RiverGizmo.AXIS_MAPPING[constraint] if local_editing: axis = _handle_base_transform.basis * (axis) var axis_from = end_pos_global + (axis * RiverGizmo.AXIS_CONSTRAINT_LENGTH) var axis_to = end_pos_global - (axis * RiverGizmo.AXIS_CONSTRAINT_LENGTH) var ray_to = ray_from + (ray_dir * RiverGizmo.AXIS_CONSTRAINT_LENGTH) var result = Geometry3D.get_closest_points_between_segments(axis_from, axis_to, ray_from, ray_to) new_pos = result[0] elif constraint in RiverGizmo.PLANE_MAPPING: var normal: Vector3 = RiverGizmo.PLANE_MAPPING[constraint] if local_editing: normal = _handle_base_transform.basis * (normal) var projected : Vector3 = end_pos_global.project(normal) var direction : Vector3 = sign(projected.dot(normal)) var distance : Vector3 = direction * projected.length() plane = Plane(normal, distance) new_pos = plane.intersects_ray(ray_from, ray_dir) baked_closest_point = _edited_node.to_local(new_pos) var ur := get_undo_redo() ur.create_action("Add River point") ur.add_do_method(_edited_node, "add_point", baked_closest_point, closest_segment) ur.add_do_method(_edited_node, "properties_changed") ur.add_do_method(_edited_node, "set_materials", "i_valid_flowmap", false) ur.add_do_property(_edited_node, "valid_flowmap", false) ur.add_do_method(_edited_node, "update_configuration_warnings") if closest_segment == -1: ur.add_undo_method(_edited_node, "remove_point", _edited_node.curve.get_point_count()) # remove last else: ur.add_undo_method(_edited_node, "remove_point", closest_segment + 1) ur.add_undo_method(_edited_node, "properties_changed") ur.add_undo_method(_edited_node, "set_materials", "i_valid_flowmap", _edited_node.valid_flowmap) ur.add_undo_property(_edited_node, "valid_flowmap", _edited_node.valid_flowmap) ur.add_undo_method(_edited_node, "update_configuration_warnings") ur.commit_action() if _mode == "remove" and not event.pressed: # A closest_segment of -1 means we didn't press close enough to a # point for it to be removed if not closest_segment == -1: var closest_index = _edited_node.get_closest_point_to(baked_closest_point) #_edited_node.remove_point(closest_index) var ur = get_undo_redo() ur.create_action("Remove River point") ur.add_do_method(_edited_node, "remove_point", closest_index) ur.add_do_method(_edited_node, "properties_changed") ur.add_do_method(_edited_node, "set_materials", "i_valid_flowmap", false) ur.add_do_property(_edited_node, "valid_flowmap", false) ur.add_do_method(_edited_node, "update_configuration_warnings") if closest_index == _edited_node.curve.get_point_count() - 1: ur.add_undo_method(_edited_node, "add_point", _edited_node.curve.get_point_position(closest_index), -1) else: ur.add_undo_method(_edited_node, "add_point", _edited_node.curve.get_point_position(closest_index), closest_index - 1, _edited_node.curve.get_point_out(closest_index), _edited_node.widths[closest_index]) ur.add_undo_method(_edited_node, "properties_changed") ur.add_undo_method(_edited_node, "set_materials", "i_valid_flowmap", _edited_node.valid_flowmap) ur.add_undo_property(_edited_node, "valid_flowmap", _edited_node.valid_flowmap) ur.add_undo_method(_edited_node, "update_configuration_warnings") ur.commit_action() # TODO - This should be updated to the enum when it's fixed https://github.com/godotengine/godot/pull/64465 return 1 elif _edited_node is RiverManager: # Forward input to river controls. This is cleaner than handling # the keybindings here as the keybindings need to interact with # the buttons. Handling it here would expose more private details # of the controls than needed, instead only the spatial_gui_input() # method needs to be exposed. # TODO - so this was returning a bool before? Check this return _river_controls.spatial_gui_input(event) # TODO - This should be updated to the enum when it's fixed https://github.com/godotengine/godot/pull/64465 return 0 func _river_progress_notified(progress : float, message : String) -> void: if message == "finished": _progress_window.hide() else: if not _progress_window.visible: _progress_window.popup_centered() _progress_window.show_progress(message, progress) func _show_river_control_panel() -> void: if not _river_controls.get_parent(): add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, _river_controls) _river_controls.menu.connect("generate_flowmap", Callable(self, "_on_generate_flowmap_pressed")) _river_controls.menu.connect("generate_mesh", Callable(self, "_on_generate_mesh_pressed")) _river_controls.menu.connect("debug_view_changed", Callable(self, "_on_debug_view_changed")) func _hide_river_control_panel() -> void: if _river_controls.get_parent(): remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, _river_controls) _river_controls.menu.disconnect("generate_flowmap", Callable(self, "_on_generate_flowmap_pressed")) _river_controls.menu.disconnect("generate_mesh", Callable(self, "_on_generate_mesh_pressed")) _river_controls.menu.disconnect("debug_view_changed", Callable(self, "_on_debug_view_changed")) func _show_water_system_control_panel() -> void: if not _water_system_controls.get_parent(): add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, _water_system_controls) _water_system_controls.menu.connect("generate_system_maps", Callable(self, "_on_generate_system_maps_pressed")) func _hide_water_system_control_panel() -> void: if _water_system_controls.get_parent(): remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, _water_system_controls) _water_system_controls.menu.disconnect("generate_system_maps", Callable(self, "_on_generate_system_maps_pressed"))