open-fpsz/addons/terrain_3d/editor/editor.gd
2024-04-07 14:25:59 -04:00

282 lines
8.9 KiB
GDScript

@tool
extends EditorPlugin
#class_name Terrain3DEditorPlugin Cannot be named until Godot #75388
# Includes
const UI: Script = preload("res://addons/terrain_3d/editor/components/ui.gd")
const RegionGizmo: Script = preload("res://addons/terrain_3d/editor/components/region_gizmo.gd")
const TextureDock: Script = preload("res://addons/terrain_3d/editor/components/texture_dock.gd")
var terrain: Terrain3D
var nav_region: NavigationRegion3D
var editor: Terrain3DEditor
var ui: Node # Terrain3DUI see Godot #75388
var texture_dock: TextureDock
var texture_dock_container: CustomControlContainer = CONTAINER_INSPECTOR_BOTTOM
var visible: bool
var region_gizmo: RegionGizmo
var current_region_position: Vector2
var mouse_global_position: Vector3 = Vector3.ZERO
func _enter_tree() -> void:
editor = Terrain3DEditor.new()
ui = UI.new()
ui.plugin = self
add_child(ui)
texture_dock = TextureDock.new()
texture_dock.hide()
texture_dock.resource_changed.connect(_on_texture_dock_resource_changed)
texture_dock.resource_inspected.connect(_on_texture_dock_resource_selected)
texture_dock.resource_selected.connect(ui._on_setting_changed)
region_gizmo = RegionGizmo.new()
add_control_to_container(texture_dock_container, texture_dock)
texture_dock.get_parent().visibility_changed.connect(_on_texture_dock_visibility_changed)
func _exit_tree() -> void:
remove_control_from_container(texture_dock_container, texture_dock)
texture_dock.queue_free()
ui.queue_free()
editor.free()
func _handles(p_object: Object) -> bool:
return p_object is Terrain3D or p_object is NavigationRegion3D
func _edit(p_object: Object) -> void:
if !p_object:
_clear()
if p_object is Terrain3D:
if p_object == terrain:
return
terrain = p_object
editor.set_terrain(terrain)
region_gizmo.set_node_3d(terrain)
terrain.add_gizmo(region_gizmo)
terrain.set_plugin(self)
if not terrain.texture_list_changed.is_connected(_load_textures):
terrain.texture_list_changed.connect(_load_textures)
_load_textures()
if not terrain.storage_changed.is_connected(_load_storage):
terrain.storage_changed.connect(_load_storage)
_load_storage()
else:
terrain = null
if p_object is NavigationRegion3D:
nav_region = p_object
else:
nav_region = null
_update_visibility()
func _make_visible(p_visible: bool) -> void:
visible = p_visible
_update_visibility()
func _update_visibility() -> void:
ui.set_visible(visible)
texture_dock.set_visible(visible and terrain)
if terrain:
update_region_grid()
region_gizmo.set_hidden(not visible or not terrain)
func _clear() -> void:
if is_terrain_valid():
terrain.storage_changed.disconnect(_load_storage)
terrain.clear_gizmos()
terrain = null
editor.set_terrain(null)
ui.clear_picking()
region_gizmo.clear()
func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> int:
if not is_terrain_valid():
return AFTER_GUI_INPUT_PASS
## Handle mouse movement
if p_event is InputEventMouseMotion:
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_PASS
## Get mouse location on terrain
# Snap terrain to current camera
terrain.set_camera(p_viewport_camera)
# Detect if viewport is set to half_resolution
# Structure is: Node3DEditorViewportContainer/Node3DEditorViewport/SubViewportContainer/SubViewport/Camera3D
var editor_vpc: SubViewportContainer = p_viewport_camera.get_parent().get_parent()
var full_resolution: bool = false if editor_vpc.stretch_shrink == 2 else true
# Project 2D mouse position to 3D position and direction
var mouse_pos: Vector2 = p_event.position if full_resolution else p_event.position/2
var camera_pos: Vector3 = p_viewport_camera.project_ray_origin(mouse_pos)
var camera_dir: Vector3 = p_viewport_camera.project_ray_normal(mouse_pos)
# If region tool, grab mouse position without considering height
if editor.get_tool() == Terrain3DEditor.REGION:
var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir)
mouse_global_position = (camera_pos + t * camera_dir)
else:
# Else look for intersection with terrain
var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir)
if intersection_point.z > 3.4e38: # double max
return AFTER_GUI_INPUT_STOP
mouse_global_position = intersection_point
## Update decal
ui.decal.global_position = mouse_global_position
ui.decal.albedo_mix = 1.0
if ui.decal_timer.is_stopped():
ui.update_decal()
else:
ui.decal_timer.start()
## Update region highlight
var region_size = terrain.get_storage().get_region_size()
var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \
/ (region_size * terrain.get_mesh_vertex_spacing()) ).floor()
if current_region_position != region_position:
current_region_position = region_position
update_region_grid()
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and editor.is_operating():
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif p_event is InputEventMouseButton:
ui.update_decal()
if p_event.get_button_index() == MOUSE_BUTTON_LEFT:
if p_event.is_pressed():
if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
return AFTER_GUI_INPUT_STOP
# If picking
if ui.is_picking():
ui.pick(mouse_global_position)
if not ui.operation_builder or not ui.operation_builder.is_ready():
return AFTER_GUI_INPUT_STOP
# If adjusting regions
if editor.get_tool() == Terrain3DEditor.REGION:
# Skip regions that already exist or don't
var has_region: bool = terrain.get_storage().has_region(mouse_global_position)
var op: int = editor.get_operation()
if ( has_region and op == Terrain3DEditor.ADD) or \
( not has_region and op == Terrain3DEditor.SUBTRACT ):
return AFTER_GUI_INPUT_STOP
# If an automatic operation is ready to go (e.g. gradient)
if ui.operation_builder and ui.operation_builder.is_ready():
ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
# Mouse clicked, start editing
editor.start_operation(mouse_global_position)
editor.operate(mouse_global_position, p_viewport_camera.rotation.y)
return AFTER_GUI_INPUT_STOP
elif editor.is_operating():
# Mouse released, save undo data
editor.stop_operation()
return AFTER_GUI_INPUT_STOP
return AFTER_GUI_INPUT_PASS
func is_terrain_valid() -> bool:
var valid: bool = false
if is_instance_valid(terrain):
valid = terrain.get_storage() != null
return valid
func update_texture_dock(p_args: Array) -> void:
texture_dock.clear()
if is_terrain_valid() and terrain.texture_list:
var texture_count: int = terrain.texture_list.get_texture_count()
for i in texture_count:
var texture: Terrain3DTexture = terrain.texture_list.get_texture(i)
texture_dock.add_item(texture)
if texture_count < Terrain3DTextureList.MAX_TEXTURES:
texture_dock.add_item()
func update_region_grid() -> void:
if !region_gizmo.get_node_3d():
return
if is_terrain_valid():
region_gizmo.show_rect = editor.get_tool() == Terrain3DEditor.REGION
region_gizmo.use_secondary_color = editor.get_operation() == Terrain3DEditor.SUBTRACT
region_gizmo.region_position = current_region_position
region_gizmo.region_size = terrain.get_storage().get_region_size() * terrain.get_mesh_vertex_spacing()
region_gizmo.grid = terrain.get_storage().get_region_offsets()
terrain.update_gizmos()
return
region_gizmo.show_rect = false
region_gizmo.region_size = 1024
region_gizmo.grid = [Vector2i.ZERO]
# Signal handlers
func _load_textures() -> void:
if terrain and terrain.texture_list:
if not terrain.texture_list.textures_changed.is_connected(update_texture_dock):
terrain.texture_list.textures_changed.connect(update_texture_dock)
update_texture_dock(Array())
func _load_storage() -> void:
if terrain:
update_region_grid()
func _on_texture_dock_resource_changed(texture: Resource, index: int) -> void:
if is_terrain_valid():
# If removing last entry and its selected, clear inspector
if not texture and index == texture_dock.get_selected_index() and \
texture_dock.get_selected_index() == texture_dock.entries.size() - 2:
get_editor_interface().inspect_object(null)
terrain.get_texture_list().set_texture(index, texture)
call_deferred("_load_storage")
func _on_texture_dock_resource_selected(texture) -> void:
get_editor_interface().inspect_object(texture, "", true)
func _on_texture_dock_visibility_changed() -> void:
if texture_dock.get_parent() != null:
remove_control_from_container(texture_dock_container, texture_dock)
if texture_dock.get_parent() == null:
texture_dock_container = CONTAINER_INSPECTOR_BOTTOM
if get_editor_interface().is_distraction_free_mode_enabled():
texture_dock_container = CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT
add_control_to_container(texture_dock_container, texture_dock)