3629 lines
64 KiB
GDScript
3629 lines
64 KiB
GDScript
@tool
|
|
extends EditorPlugin
|
|
|
|
|
|
enum StrokeMode {
|
|
NONE,
|
|
PAINT,
|
|
ERASE,
|
|
}
|
|
|
|
|
|
enum HeightOffsetMode {
|
|
WORLD_Y,
|
|
SURFACE_NORMAL,
|
|
}
|
|
|
|
|
|
const DOCK_SCENE: PackedScene = preload(
|
|
"res://addons/world_brush/ui/world_brush_dock.tscn"
|
|
)
|
|
|
|
const LOD_INSTANCE_SCRIPT: Script = preload(
|
|
"res://addons/world_brush/world_brush_lod_instance.gd"
|
|
)
|
|
|
|
const AUTO_LOD_CONTROLLER_SCRIPT: Script = preload(
|
|
"res://addons/world_brush/world_brush_auto_lod_controller.gd"
|
|
)
|
|
|
|
const RAY_LENGTH: float = 100000.0
|
|
const MAX_RAY_SKIPS: int = 64
|
|
const SAMPLE_VERTICAL_RANGE: float = 100000.0
|
|
|
|
const BRUSH_SEGMENTS: int = 64
|
|
const BRUSH_HEIGHT_OFFSET: float = 0.08
|
|
|
|
const PAINT_PREVIEW_COLOR := Color(
|
|
0.10,
|
|
0.80,
|
|
1.00,
|
|
1.00
|
|
)
|
|
|
|
const ERASE_PREVIEW_COLOR := Color(
|
|
1.00,
|
|
0.12,
|
|
0.08,
|
|
1.00
|
|
)
|
|
|
|
const STAMP_STEP_RADIUS_FACTOR: float = 0.35
|
|
const MINIMUM_STAMP_STEP: float = 0.5
|
|
|
|
const PLACEMENT_ATTEMPTS_PER_INSTANCE: int = 30
|
|
const MINIMUM_PLACEMENT_ATTEMPTS: int = 80
|
|
const MAX_TOTAL_PLACEMENT_ATTEMPTS: int = 2000
|
|
|
|
const TERRAIN_SCAN_INTERVAL_MS: int = 1000
|
|
const INVALID_TERRAIN_LIMIT: float = 3.0e38
|
|
|
|
const CONTAINER_NAME: StringName = &"WorldBrushInstances"
|
|
const CONTAINER_META: StringName = &"world_brush_container"
|
|
const INSTANCE_META: StringName = &"world_brush_instance"
|
|
const SOURCE_SCENE_META: StringName = &"world_brush_source_scene_path"
|
|
const LOD_GROUP_META: StringName = &"world_brush_lod_group"
|
|
const LOD_NODE_GROUP: StringName = &"world_brush_distance_lod"
|
|
const AUTO_LOD_CONTROLLER_NAME: StringName = &"WorldBrushAutoLOD"
|
|
const AUTO_LOD_IMPORT_EXTENSIONS := [
|
|
"glb", "gltf", "fbx", "obj", "blend"
|
|
]
|
|
|
|
|
|
var world_brush_dock: EditorDock
|
|
var dock_content: Control
|
|
|
|
var selected_scene: PackedScene
|
|
|
|
var brush_enabled: bool = false
|
|
var brush_radius: float = 5.0
|
|
var instances_per_click: int = 5
|
|
var minimum_spacing: float = 1.0
|
|
|
|
var random_y_rotation: bool = true
|
|
var align_to_surface: bool = false
|
|
|
|
var maximum_slope_degrees: float = 85.0
|
|
|
|
var height_offset: float = 0.0
|
|
var height_offset_mode: int = HeightOffsetMode.SURFACE_NORMAL
|
|
|
|
var random_scale_enabled: bool = true
|
|
var minimum_scale: float = 0.8
|
|
var maximum_scale: float = 1.3
|
|
|
|
var lod_enabled: bool = false
|
|
var lod1_scene: PackedScene
|
|
var lod2_scene: PackedScene
|
|
var lod1_distance: float = 25.0
|
|
var lod2_distance: float = 60.0
|
|
var lod_cull_distance: float = 0.0
|
|
var editor_lod_preview_elapsed: float = 0.0
|
|
var auto_lod_enabled: bool = false
|
|
var auto_lod_bias: float = 1.0
|
|
var auto_lod_cull_distance: float = 0.0
|
|
|
|
|
|
# =========================================================
|
|
# السطح الحالي
|
|
# =========================================================
|
|
|
|
var has_surface_hit: bool = false
|
|
var surface_position: Vector3 = Vector3.ZERO
|
|
var surface_normal: Vector3 = Vector3.UP
|
|
var surface_type: String = ""
|
|
|
|
|
|
# =========================================================
|
|
# حالة السحبة
|
|
# =========================================================
|
|
|
|
var stroke_mode: int = StrokeMode.NONE
|
|
|
|
var has_last_stamp_position: bool = false
|
|
var last_stamp_position: Vector3 = Vector3.ZERO
|
|
|
|
var stroke_scene_root: Node3D
|
|
var stroke_container: Node3D
|
|
var stroke_created_container: bool = false
|
|
|
|
|
|
# =========================================================
|
|
# عناصر سحبة الرسم والحذف
|
|
# =========================================================
|
|
|
|
var painted_instances: Array[Node3D] = []
|
|
var painted_transforms: Array[Transform3D] = []
|
|
|
|
var erased_instances: Array[Node3D] = []
|
|
var erased_transforms: Array[Transform3D] = []
|
|
|
|
|
|
# =========================================================
|
|
# معاينة الفرشاة
|
|
# =========================================================
|
|
|
|
var brush_preview: MeshInstance3D
|
|
var brush_preview_mesh: ImmediateMesh
|
|
var brush_preview_material: StandardMaterial3D
|
|
|
|
var erase_preview_active: bool = false
|
|
|
|
|
|
# =========================================================
|
|
# Terrain3D Cache
|
|
# =========================================================
|
|
|
|
var cached_scene_root: Node
|
|
var cached_terrain_nodes: Array[Node] = []
|
|
var last_terrain_scan_time: int = 0
|
|
|
|
|
|
var random_generator: RandomNumberGenerator
|
|
|
|
|
|
# =========================================================
|
|
# تشغيل وإيقاف الإضافة
|
|
# =========================================================
|
|
|
|
func _enter_tree() -> void:
|
|
random_generator = RandomNumberGenerator.new()
|
|
random_generator.randomize()
|
|
|
|
set_process(true)
|
|
|
|
dock_content = DOCK_SCENE.instantiate() as Control
|
|
|
|
if dock_content == null:
|
|
push_error(
|
|
"World Brush: Failed to instantiate dock content."
|
|
)
|
|
return
|
|
|
|
_create_editor_dock()
|
|
_connect_dock_signals()
|
|
if not scene_changed.is_connected(
|
|
_on_editor_scene_changed
|
|
):
|
|
scene_changed.connect(
|
|
_on_editor_scene_changed
|
|
)
|
|
|
|
set_input_event_forwarding_always_enabled()
|
|
|
|
call_deferred(
|
|
"_show_world_brush_dock"
|
|
)
|
|
call_deferred(
|
|
"_upgrade_existing_lod_groups"
|
|
)
|
|
call_deferred(
|
|
"_apply_lod_settings_to_existing_groups"
|
|
)
|
|
call_deferred(
|
|
"_apply_auto_lod_settings_to_existing_instances"
|
|
)
|
|
|
|
print(
|
|
"World Brush: Plugin enabled successfully."
|
|
)
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
_end_active_stroke()
|
|
if scene_changed.is_connected(
|
|
_on_editor_scene_changed
|
|
):
|
|
scene_changed.disconnect(
|
|
_on_editor_scene_changed
|
|
)
|
|
|
|
set_process(false)
|
|
|
|
brush_enabled = false
|
|
selected_scene = null
|
|
|
|
has_surface_hit = false
|
|
surface_type = ""
|
|
|
|
_clear_terrain_cache()
|
|
_remove_brush_preview()
|
|
|
|
if is_instance_valid(world_brush_dock):
|
|
remove_dock(
|
|
world_brush_dock
|
|
)
|
|
|
|
world_brush_dock.queue_free()
|
|
|
|
world_brush_dock = null
|
|
dock_content = null
|
|
random_generator = null
|
|
|
|
print(
|
|
"World Brush: Plugin disabled successfully."
|
|
)
|
|
|
|
|
|
func _process(
|
|
delta: float
|
|
) -> void:
|
|
_update_editor_lod_preview(delta)
|
|
|
|
if stroke_mode == StrokeMode.NONE:
|
|
return
|
|
|
|
if stroke_mode == StrokeMode.PAINT:
|
|
if not Input.is_mouse_button_pressed(
|
|
MOUSE_BUTTON_LEFT
|
|
):
|
|
_end_active_stroke()
|
|
|
|
elif stroke_mode == StrokeMode.ERASE:
|
|
if not Input.is_mouse_button_pressed(
|
|
MOUSE_BUTTON_RIGHT
|
|
):
|
|
_end_active_stroke()
|
|
|
|
|
|
func _handles(
|
|
object: Object
|
|
) -> bool:
|
|
if object == null:
|
|
var edited_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
|
|
return edited_root is Node3D
|
|
|
|
return object is Node3D
|
|
|
|
|
|
# =========================================================
|
|
# نافذة الإضافة
|
|
# =========================================================
|
|
|
|
func _create_editor_dock() -> void:
|
|
world_brush_dock = EditorDock.new()
|
|
|
|
world_brush_dock.name = (
|
|
"WorldBrushEditorDock"
|
|
)
|
|
|
|
world_brush_dock.title = "World Brush"
|
|
|
|
world_brush_dock.layout_key = (
|
|
"world_brush_editor_dock"
|
|
)
|
|
|
|
world_brush_dock.default_slot = (
|
|
EditorDock.DOCK_SLOT_RIGHT_UL
|
|
)
|
|
|
|
world_brush_dock.global = true
|
|
|
|
world_brush_dock.add_child(
|
|
dock_content
|
|
)
|
|
|
|
add_dock(
|
|
world_brush_dock
|
|
)
|
|
|
|
|
|
func _connect_dock_signals() -> void:
|
|
if not _connect_required_signal(
|
|
&"selected_scene_changed",
|
|
&"_on_selected_scene_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"brush_enabled_changed",
|
|
&"_on_brush_enabled_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"brush_radius_changed",
|
|
&"_on_brush_radius_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"instances_per_click_changed",
|
|
&"_on_instances_per_click_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"minimum_spacing_changed",
|
|
&"_on_minimum_spacing_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"random_y_rotation_changed",
|
|
&"_on_random_y_rotation_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"align_to_surface_changed",
|
|
&"_on_align_to_surface_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"maximum_slope_changed",
|
|
&"_on_maximum_slope_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"height_offset_changed",
|
|
&"_on_height_offset_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"height_offset_mode_changed",
|
|
&"_on_height_offset_mode_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"random_scale_enabled_changed",
|
|
&"_on_random_scale_enabled_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"minimum_scale_changed",
|
|
&"_on_minimum_scale_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"maximum_scale_changed",
|
|
&"_on_maximum_scale_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"lod_settings_changed",
|
|
&"_on_lod_settings_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"auto_lod_settings_changed",
|
|
&"_on_auto_lod_settings_changed"
|
|
):
|
|
return
|
|
|
|
if not _connect_required_signal(
|
|
&"auto_lod_import_requested",
|
|
&"_on_auto_lod_import_requested"
|
|
):
|
|
return
|
|
|
|
|
|
func _connect_required_signal(
|
|
signal_name: StringName,
|
|
method_name: StringName
|
|
) -> bool:
|
|
if not dock_content.has_signal(
|
|
signal_name
|
|
):
|
|
push_error(
|
|
"World Brush: Missing signal -> %s"
|
|
% signal_name
|
|
)
|
|
|
|
return false
|
|
|
|
var signal_callable := Callable(
|
|
self,
|
|
method_name
|
|
)
|
|
|
|
if not dock_content.is_connected(
|
|
signal_name,
|
|
signal_callable
|
|
):
|
|
dock_content.connect(
|
|
signal_name,
|
|
signal_callable
|
|
)
|
|
|
|
return true
|
|
|
|
|
|
func _show_world_brush_dock() -> void:
|
|
if not is_instance_valid(
|
|
world_brush_dock
|
|
):
|
|
return
|
|
|
|
world_brush_dock.open()
|
|
world_brush_dock.make_visible()
|
|
|
|
|
|
# =========================================================
|
|
# إدخال نافذة 3D
|
|
# =========================================================
|
|
|
|
func _forward_3d_gui_input(
|
|
camera: Camera3D,
|
|
event: InputEvent
|
|
) -> int:
|
|
if event is InputEventKey:
|
|
_handle_keyboard_event(
|
|
event as InputEventKey
|
|
)
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
if not brush_enabled:
|
|
_end_active_stroke()
|
|
_hide_brush_preview()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
if event is InputEventMouseMotion:
|
|
return _handle_mouse_motion(
|
|
camera,
|
|
event as InputEventMouseMotion
|
|
)
|
|
|
|
if event is InputEventMouseButton:
|
|
return _handle_mouse_button(
|
|
camera,
|
|
event as InputEventMouseButton
|
|
)
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
|
|
func _handle_keyboard_event(
|
|
key_event: InputEventKey
|
|
) -> void:
|
|
if not key_event.pressed:
|
|
return
|
|
|
|
if key_event.echo:
|
|
return
|
|
|
|
var undo_shortcut := (
|
|
(
|
|
key_event.ctrl_pressed
|
|
or key_event.meta_pressed
|
|
)
|
|
and key_event.keycode == KEY_Z
|
|
)
|
|
|
|
if undo_shortcut:
|
|
_end_active_stroke()
|
|
|
|
|
|
func _handle_mouse_motion(
|
|
camera: Camera3D,
|
|
mouse_motion: InputEventMouseMotion
|
|
) -> int:
|
|
_update_surface_hit(
|
|
camera,
|
|
mouse_motion.position
|
|
)
|
|
|
|
if _event_uses_modifier(
|
|
mouse_motion
|
|
):
|
|
_end_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
var left_button_down := (
|
|
mouse_motion.button_mask
|
|
& MOUSE_BUTTON_MASK_LEFT
|
|
) != 0
|
|
|
|
var right_button_down := (
|
|
mouse_motion.button_mask
|
|
& MOUSE_BUTTON_MASK_RIGHT
|
|
) != 0
|
|
|
|
if right_button_down:
|
|
if stroke_mode != StrokeMode.ERASE:
|
|
_end_active_stroke()
|
|
_begin_erase_stroke()
|
|
else:
|
|
_continue_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
|
|
|
if left_button_down:
|
|
if stroke_mode != StrokeMode.PAINT:
|
|
_end_active_stroke()
|
|
_begin_paint_stroke()
|
|
else:
|
|
_continue_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
|
|
|
if stroke_mode != StrokeMode.NONE:
|
|
_end_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
|
|
func _handle_mouse_button(
|
|
camera: Camera3D,
|
|
mouse_button: InputEventMouseButton
|
|
) -> int:
|
|
_update_surface_hit(
|
|
camera,
|
|
mouse_button.position
|
|
)
|
|
|
|
var is_left_button := (
|
|
mouse_button.button_index
|
|
== MOUSE_BUTTON_LEFT
|
|
)
|
|
|
|
var is_right_button := (
|
|
mouse_button.button_index
|
|
== MOUSE_BUTTON_RIGHT
|
|
)
|
|
|
|
if not is_left_button and not is_right_button:
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
if _event_uses_modifier(
|
|
mouse_button
|
|
):
|
|
_end_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
|
|
|
if mouse_button.pressed:
|
|
if is_left_button:
|
|
_end_active_stroke()
|
|
_begin_paint_stroke()
|
|
|
|
elif is_right_button:
|
|
_end_active_stroke()
|
|
_begin_erase_stroke()
|
|
|
|
else:
|
|
if (
|
|
is_left_button
|
|
and stroke_mode == StrokeMode.PAINT
|
|
):
|
|
_end_active_stroke()
|
|
|
|
elif (
|
|
is_right_button
|
|
and stroke_mode == StrokeMode.ERASE
|
|
):
|
|
_end_active_stroke()
|
|
|
|
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
|
|
|
|
|
func _event_uses_modifier(
|
|
event: InputEventWithModifiers
|
|
) -> bool:
|
|
return (
|
|
event.alt_pressed
|
|
or event.ctrl_pressed
|
|
or event.shift_pressed
|
|
or event.meta_pressed
|
|
)
|
|
|
|
|
|
# =========================================================
|
|
# إدارة السحبة
|
|
# =========================================================
|
|
|
|
func _begin_paint_stroke() -> void:
|
|
if selected_scene == null:
|
|
_set_dock_status(
|
|
"Select An Active Scene First"
|
|
)
|
|
|
|
return
|
|
|
|
if not has_surface_hit:
|
|
_set_dock_status(
|
|
"No Valid Surface"
|
|
)
|
|
|
|
return
|
|
|
|
_reset_stroke_data()
|
|
|
|
stroke_mode = StrokeMode.PAINT
|
|
|
|
has_last_stamp_position = false
|
|
last_stamp_position = Vector3.ZERO
|
|
|
|
_set_erase_preview_active(
|
|
false
|
|
)
|
|
|
|
_stamp_brush(
|
|
true
|
|
)
|
|
|
|
|
|
func _begin_erase_stroke() -> void:
|
|
if not has_surface_hit:
|
|
_set_dock_status(
|
|
"No Valid Surface"
|
|
)
|
|
|
|
return
|
|
|
|
_reset_stroke_data()
|
|
|
|
stroke_mode = StrokeMode.ERASE
|
|
|
|
has_last_stamp_position = false
|
|
last_stamp_position = Vector3.ZERO
|
|
|
|
_set_erase_preview_active(
|
|
true
|
|
)
|
|
|
|
_stamp_brush(
|
|
true
|
|
)
|
|
|
|
|
|
func _continue_active_stroke() -> void:
|
|
if stroke_mode == StrokeMode.NONE:
|
|
return
|
|
|
|
_stamp_brush(
|
|
false
|
|
)
|
|
|
|
|
|
func _end_active_stroke() -> void:
|
|
var finished_mode := stroke_mode
|
|
|
|
if finished_mode == StrokeMode.NONE:
|
|
return
|
|
|
|
stroke_mode = StrokeMode.NONE
|
|
|
|
has_last_stamp_position = false
|
|
last_stamp_position = Vector3.ZERO
|
|
|
|
_set_erase_preview_active(
|
|
false
|
|
)
|
|
|
|
if finished_mode == StrokeMode.PAINT:
|
|
_commit_paint_stroke()
|
|
|
|
elif finished_mode == StrokeMode.ERASE:
|
|
_commit_erase_stroke()
|
|
|
|
_reset_stroke_data()
|
|
|
|
|
|
func _reset_stroke_data() -> void:
|
|
stroke_scene_root = null
|
|
stroke_container = null
|
|
stroke_created_container = false
|
|
|
|
painted_instances.clear()
|
|
painted_transforms.clear()
|
|
|
|
erased_instances.clear()
|
|
erased_transforms.clear()
|
|
|
|
|
|
func _stamp_brush(
|
|
force_stamp: bool
|
|
) -> void:
|
|
if not has_surface_hit:
|
|
return
|
|
|
|
if (
|
|
not force_stamp
|
|
and has_last_stamp_position
|
|
):
|
|
var required_step := (
|
|
_get_stamp_step()
|
|
)
|
|
|
|
var distance_squared := (
|
|
_horizontal_distance_squared(
|
|
surface_position,
|
|
last_stamp_position
|
|
)
|
|
)
|
|
|
|
if (
|
|
distance_squared
|
|
< required_step * required_step
|
|
):
|
|
return
|
|
|
|
var affected_count: int = 0
|
|
|
|
if stroke_mode == StrokeMode.PAINT:
|
|
affected_count = (
|
|
_paint_scene_instances_live()
|
|
)
|
|
|
|
elif stroke_mode == StrokeMode.ERASE:
|
|
affected_count = (
|
|
_erase_scene_instances_live()
|
|
)
|
|
|
|
last_stamp_position = surface_position
|
|
has_last_stamp_position = true
|
|
|
|
if stroke_mode == StrokeMode.PAINT:
|
|
if affected_count > 0:
|
|
_set_dock_status(
|
|
"Painted In Stroke: %d"
|
|
% painted_instances.size()
|
|
)
|
|
|
|
elif stroke_mode == StrokeMode.ERASE:
|
|
if affected_count > 0:
|
|
_set_dock_status(
|
|
"Erased In Stroke: %d"
|
|
% erased_instances.size()
|
|
)
|
|
else:
|
|
_set_dock_status(
|
|
"Erase Brush — No Instances Here"
|
|
)
|
|
|
|
|
|
func _get_stamp_step() -> float:
|
|
var radius_step := (
|
|
brush_radius
|
|
* STAMP_STEP_RADIUS_FACTOR
|
|
)
|
|
|
|
return maxf(
|
|
MINIMUM_STAMP_STEP,
|
|
maxf(
|
|
minimum_spacing,
|
|
radius_step
|
|
)
|
|
)
|
|
|
|
|
|
# =========================================================
|
|
# الرسم المباشر
|
|
# =========================================================
|
|
|
|
func _paint_scene_instances_live() -> int:
|
|
if selected_scene == null:
|
|
return 0
|
|
|
|
if not selected_scene.can_instantiate():
|
|
_set_dock_status(
|
|
"Selected Scene Cannot Be Instantiated"
|
|
)
|
|
|
|
return 0
|
|
|
|
var edited_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
|
|
if not edited_root is Node3D:
|
|
_set_dock_status(
|
|
"Scene Root Must Be Node3D"
|
|
)
|
|
|
|
return 0
|
|
|
|
var scene_root := edited_root as Node3D
|
|
var world_3d := scene_root.get_world_3d()
|
|
|
|
if world_3d == null:
|
|
_set_dock_status(
|
|
"3D World Not Available"
|
|
)
|
|
|
|
return 0
|
|
|
|
if stroke_scene_root == null:
|
|
stroke_scene_root = scene_root
|
|
|
|
if stroke_scene_root != scene_root:
|
|
_set_dock_status(
|
|
"Edited Scene Changed During Stroke"
|
|
)
|
|
|
|
return 0
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
stroke_container = (
|
|
_find_world_brush_container(
|
|
scene_root
|
|
)
|
|
)
|
|
|
|
if stroke_container == null:
|
|
stroke_container = Node3D.new()
|
|
stroke_container.name = CONTAINER_NAME
|
|
|
|
stroke_container.set_meta(
|
|
CONTAINER_META,
|
|
true
|
|
)
|
|
|
|
scene_root.add_child(
|
|
stroke_container,
|
|
true
|
|
)
|
|
|
|
stroke_container.owner = scene_root
|
|
stroke_created_container = true
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
_set_dock_status(
|
|
"Failed To Create Container"
|
|
)
|
|
|
|
return 0
|
|
|
|
var placement_samples := (
|
|
_generate_brush_samples(
|
|
scene_root,
|
|
world_3d,
|
|
stroke_container
|
|
)
|
|
)
|
|
|
|
if placement_samples.is_empty():
|
|
_set_dock_status(
|
|
"No Valid Space Inside Brush"
|
|
)
|
|
|
|
return 0
|
|
|
|
var placed_count: int = 0
|
|
|
|
for sample in placement_samples:
|
|
var created_node := (
|
|
_instantiate_selected_scene()
|
|
)
|
|
|
|
if created_node == null:
|
|
continue
|
|
|
|
if not created_node is Node3D:
|
|
created_node.free()
|
|
|
|
_set_dock_status(
|
|
"Selected Scene Root Must Inherit Node3D"
|
|
)
|
|
|
|
break
|
|
|
|
var instance := created_node as Node3D
|
|
|
|
instance.set_meta(
|
|
INSTANCE_META,
|
|
true
|
|
)
|
|
instance.set_meta(
|
|
SOURCE_SCENE_META,
|
|
selected_scene.resource_path
|
|
)
|
|
var authored_basis := (
|
|
instance.transform.basis
|
|
)
|
|
|
|
var placement_position: Vector3 = (
|
|
sample.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var placement_normal: Vector3 = (
|
|
sample.get(
|
|
"normal",
|
|
Vector3.UP
|
|
)
|
|
)
|
|
|
|
var placement_basis: Basis = (
|
|
sample.get(
|
|
"placement_basis",
|
|
Basis.IDENTITY
|
|
)
|
|
)
|
|
|
|
placement_position = _apply_height_offset(
|
|
placement_position,
|
|
placement_normal
|
|
)
|
|
|
|
var scale_factor := (
|
|
_get_random_scale_factor()
|
|
)
|
|
|
|
var final_basis := (
|
|
placement_basis
|
|
* authored_basis
|
|
)
|
|
|
|
final_basis = final_basis.scaled_local(
|
|
Vector3.ONE * scale_factor
|
|
)
|
|
|
|
var final_transform := Transform3D(
|
|
final_basis,
|
|
placement_position
|
|
)
|
|
|
|
stroke_container.add_child(
|
|
instance,
|
|
true
|
|
)
|
|
|
|
instance.owner = scene_root
|
|
_apply_auto_lod_to_instance(instance, scene_root)
|
|
_assign_lod_child_owners(
|
|
instance,
|
|
scene_root
|
|
)
|
|
instance.global_transform = final_transform
|
|
|
|
painted_instances.append(
|
|
instance
|
|
)
|
|
|
|
painted_transforms.append(
|
|
final_transform
|
|
)
|
|
|
|
placed_count += 1
|
|
|
|
return placed_count
|
|
|
|
|
|
func _instantiate_selected_scene() -> Node3D:
|
|
if not lod_enabled:
|
|
return _instantiate_scene_root(
|
|
selected_scene
|
|
)
|
|
|
|
var levels: Array[Dictionary] = [
|
|
{
|
|
"scene": selected_scene,
|
|
"begin": 0.0,
|
|
"name": "Near",
|
|
},
|
|
]
|
|
|
|
if lod1_scene != null:
|
|
levels.append({
|
|
"scene": lod1_scene,
|
|
"begin": lod1_distance,
|
|
"name": "Medium",
|
|
})
|
|
|
|
if lod2_scene != null:
|
|
levels.append({
|
|
"scene": lod2_scene,
|
|
"begin": lod2_distance,
|
|
"name": "Distant",
|
|
})
|
|
|
|
if levels.size() == 1 and lod_cull_distance <= 0.0:
|
|
return _instantiate_scene_root(
|
|
selected_scene
|
|
)
|
|
|
|
var group := Node3D.new()
|
|
group.name = "%s_LOD" % selected_scene.resource_path.get_file().get_basename()
|
|
group.set_script(LOD_INSTANCE_SCRIPT)
|
|
group.add_to_group(LOD_NODE_GROUP, true)
|
|
group.set_meta(LOD_GROUP_META, true)
|
|
group.set_meta(
|
|
&"lod_source_scene_path",
|
|
selected_scene.resource_path
|
|
)
|
|
group.set_meta(&"lod_enabled", lod_enabled)
|
|
group.set_meta(
|
|
&"lod1_scene_path",
|
|
lod1_scene.resource_path if lod1_scene != null else ""
|
|
)
|
|
group.set_meta(
|
|
&"lod2_scene_path",
|
|
lod2_scene.resource_path if lod2_scene != null else ""
|
|
)
|
|
group.set_meta(&"lod1_distance", lod1_distance)
|
|
group.set_meta(&"lod2_distance", lod2_distance)
|
|
group.set_meta(&"lod_cull_distance", lod_cull_distance)
|
|
|
|
var authored_basis := Basis.IDENTITY
|
|
|
|
for index in range(levels.size()):
|
|
var level: Dictionary = levels[index]
|
|
var level_scene := level.get("scene") as PackedScene
|
|
var level_root := _instantiate_scene_root(level_scene)
|
|
|
|
if level_root == null:
|
|
group.free()
|
|
return null
|
|
|
|
if index == 0:
|
|
authored_basis = level_root.transform.basis
|
|
|
|
level_root.name = str(level.get("name", "LOD"))
|
|
level_root.transform = Transform3D.IDENTITY
|
|
group.add_child(level_root, true)
|
|
level_root.visible = index == 0
|
|
|
|
_disable_native_lod_ranges(
|
|
level_root
|
|
)
|
|
|
|
group.transform = Transform3D(
|
|
authored_basis,
|
|
Vector3.ZERO
|
|
)
|
|
group.call(
|
|
&"configure",
|
|
lod1_distance,
|
|
lod2_distance,
|
|
lod_cull_distance
|
|
)
|
|
return group
|
|
|
|
|
|
func _instantiate_scene_root(
|
|
scene: PackedScene
|
|
) -> Node3D:
|
|
if scene == null or not scene.can_instantiate():
|
|
_set_dock_status(
|
|
"LOD Scene Cannot Be Instantiated"
|
|
)
|
|
return null
|
|
|
|
var created_node := scene.instantiate(
|
|
PackedScene.GEN_EDIT_STATE_INSTANCE
|
|
)
|
|
|
|
if created_node is Node3D:
|
|
return created_node as Node3D
|
|
|
|
if created_node != null:
|
|
created_node.free()
|
|
|
|
_set_dock_status(
|
|
"LOD Scene Root Must Inherit Node3D"
|
|
)
|
|
return null
|
|
|
|
|
|
func _disable_native_lod_ranges(
|
|
root: Node
|
|
) -> bool:
|
|
var geometry_instances: Array[GeometryInstance3D] = []
|
|
_collect_geometry_instances(
|
|
root,
|
|
geometry_instances
|
|
)
|
|
var changed := false
|
|
|
|
for geometry in geometry_instances:
|
|
if (
|
|
not is_zero_approx(geometry.visibility_range_begin)
|
|
or not is_zero_approx(geometry.visibility_range_end)
|
|
or not is_zero_approx(geometry.visibility_range_begin_margin)
|
|
or not is_zero_approx(geometry.visibility_range_end_margin)
|
|
):
|
|
changed = true
|
|
|
|
geometry.visibility_range_begin = 0.0
|
|
geometry.visibility_range_end = 0.0
|
|
geometry.visibility_range_begin_margin = 0.0
|
|
geometry.visibility_range_end_margin = 0.0
|
|
geometry.visibility_range_fade_mode = (
|
|
GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
|
|
)
|
|
|
|
return changed
|
|
|
|
|
|
func _collect_geometry_instances(
|
|
node: Node,
|
|
result: Array[GeometryInstance3D]
|
|
) -> void:
|
|
if node is GeometryInstance3D:
|
|
result.append(node as GeometryInstance3D)
|
|
|
|
for child in node.get_children():
|
|
_collect_geometry_instances(
|
|
child,
|
|
result
|
|
)
|
|
|
|
|
|
func _assign_lod_child_owners(
|
|
instance: Node3D,
|
|
scene_root: Node3D
|
|
) -> void:
|
|
if not bool(instance.get_meta(LOD_GROUP_META, false)):
|
|
return
|
|
|
|
for child in instance.get_children():
|
|
if child is Node3D:
|
|
(child as Node3D).owner = scene_root
|
|
|
|
|
|
func _on_editor_scene_changed(
|
|
_scene_root: Node
|
|
) -> void:
|
|
call_deferred(
|
|
"_upgrade_existing_lod_groups"
|
|
)
|
|
call_deferred(
|
|
"_apply_lod_settings_to_existing_groups"
|
|
)
|
|
call_deferred(
|
|
"_apply_auto_lod_settings_to_existing_instances"
|
|
)
|
|
|
|
|
|
func _upgrade_existing_lod_groups() -> void:
|
|
var scene_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
if scene_root == null:
|
|
return
|
|
|
|
var upgraded := _upgrade_lod_groups_recursive(
|
|
scene_root
|
|
)
|
|
if upgraded <= 0:
|
|
return
|
|
|
|
var editor_interface := get_editor_interface()
|
|
if editor_interface.has_method(
|
|
&"mark_scene_as_unsaved"
|
|
):
|
|
editor_interface.call(
|
|
&"mark_scene_as_unsaved"
|
|
)
|
|
|
|
print(
|
|
"World Brush: Upgraded existing LOD groups -> ",
|
|
upgraded
|
|
)
|
|
|
|
|
|
func _upgrade_lod_groups_recursive(
|
|
node: Node
|
|
) -> int:
|
|
var upgraded := 0
|
|
if (
|
|
node is Node3D
|
|
and bool(node.get_meta(LOD_GROUP_META, false))
|
|
):
|
|
var lod_group := node as Node3D
|
|
var group_changed := false
|
|
if not lod_group.is_in_group(LOD_NODE_GROUP):
|
|
lod_group.add_to_group(LOD_NODE_GROUP, true)
|
|
group_changed = true
|
|
if lod_group.get_script() != LOD_INSTANCE_SCRIPT:
|
|
lod_group.set_script(
|
|
LOD_INSTANCE_SCRIPT
|
|
)
|
|
group_changed = true
|
|
|
|
lod_group.call(
|
|
&"configure",
|
|
float(lod_group.get_meta(&"lod1_distance", 25.0)),
|
|
float(lod_group.get_meta(&"lod2_distance", 60.0)),
|
|
float(lod_group.get_meta(&"lod_cull_distance", 0.0))
|
|
)
|
|
|
|
for level in lod_group.get_children():
|
|
if _disable_native_lod_ranges(level):
|
|
group_changed = true
|
|
|
|
if group_changed:
|
|
upgraded += 1
|
|
|
|
for child in node.get_children():
|
|
upgraded += _upgrade_lod_groups_recursive(
|
|
child
|
|
)
|
|
|
|
return upgraded
|
|
|
|
|
|
func _update_editor_lod_preview(
|
|
delta: float
|
|
) -> void:
|
|
editor_lod_preview_elapsed += delta
|
|
if editor_lod_preview_elapsed < 0.15:
|
|
return
|
|
|
|
editor_lod_preview_elapsed = 0.0
|
|
var scene_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
if scene_root == null:
|
|
return
|
|
|
|
var editor_viewport := (
|
|
get_editor_interface()
|
|
.get_editor_viewport_3d(0)
|
|
)
|
|
if editor_viewport == null:
|
|
return
|
|
|
|
var camera := editor_viewport.get_camera_3d()
|
|
if camera == null:
|
|
return
|
|
|
|
for candidate in get_tree().get_nodes_in_group(
|
|
LOD_NODE_GROUP
|
|
):
|
|
if not candidate is Node3D:
|
|
continue
|
|
|
|
var lod_group := candidate as Node3D
|
|
if (
|
|
lod_group != scene_root
|
|
and not scene_root.is_ancestor_of(lod_group)
|
|
):
|
|
continue
|
|
|
|
if lod_group.has_method(&"update_for_distance"):
|
|
lod_group.call(
|
|
&"update_for_distance",
|
|
camera.global_position.distance_to(
|
|
lod_group.global_position
|
|
)
|
|
)
|
|
|
|
|
|
func _apply_height_offset(
|
|
position: Vector3,
|
|
normal: Vector3
|
|
) -> Vector3:
|
|
if is_zero_approx(
|
|
height_offset
|
|
):
|
|
return position
|
|
|
|
if (
|
|
height_offset_mode
|
|
== HeightOffsetMode.WORLD_Y
|
|
):
|
|
return (
|
|
position
|
|
+ Vector3.UP * height_offset
|
|
)
|
|
|
|
var safe_normal := normal
|
|
|
|
if not _is_valid_normal(
|
|
safe_normal
|
|
):
|
|
safe_normal = Vector3.UP
|
|
else:
|
|
safe_normal = safe_normal.normalized()
|
|
|
|
return (
|
|
position
|
|
+ safe_normal * height_offset
|
|
)
|
|
|
|
|
|
func _get_random_scale_factor() -> float:
|
|
if not random_scale_enabled:
|
|
return 1.0
|
|
|
|
var safe_minimum := maxf(
|
|
minimum_scale,
|
|
0.01
|
|
)
|
|
|
|
var safe_maximum := maxf(
|
|
maximum_scale,
|
|
safe_minimum
|
|
)
|
|
|
|
if is_equal_approx(
|
|
safe_minimum,
|
|
safe_maximum
|
|
):
|
|
return safe_minimum
|
|
|
|
return random_generator.randf_range(
|
|
safe_minimum,
|
|
safe_maximum
|
|
)
|
|
|
|
|
|
# =========================================================
|
|
# الحذف المباشر
|
|
# =========================================================
|
|
|
|
func _erase_scene_instances_live() -> int:
|
|
var edited_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
|
|
if not edited_root is Node3D:
|
|
_set_dock_status(
|
|
"Scene Root Must Be Node3D"
|
|
)
|
|
|
|
return 0
|
|
|
|
var scene_root := edited_root as Node3D
|
|
|
|
if stroke_scene_root == null:
|
|
stroke_scene_root = scene_root
|
|
|
|
if stroke_scene_root != scene_root:
|
|
_set_dock_status(
|
|
"Edited Scene Changed During Stroke"
|
|
)
|
|
|
|
return 0
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
stroke_container = (
|
|
_find_world_brush_container(
|
|
scene_root
|
|
)
|
|
)
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
_set_dock_status(
|
|
"No World Brush Instances"
|
|
)
|
|
|
|
return 0
|
|
|
|
var radius_squared := (
|
|
brush_radius * brush_radius
|
|
)
|
|
|
|
var erased_count: int = 0
|
|
|
|
for child in stroke_container.get_children():
|
|
if not child is Node3D:
|
|
continue
|
|
|
|
var instance := child as Node3D
|
|
|
|
if not bool(
|
|
instance.get_meta(
|
|
INSTANCE_META,
|
|
false
|
|
)
|
|
):
|
|
continue
|
|
|
|
var distance_squared := (
|
|
_horizontal_distance_squared(
|
|
instance.global_position,
|
|
surface_position
|
|
)
|
|
)
|
|
|
|
if distance_squared > radius_squared:
|
|
continue
|
|
|
|
erased_instances.append(
|
|
instance
|
|
)
|
|
|
|
erased_transforms.append(
|
|
instance.global_transform
|
|
)
|
|
|
|
instance.owner = null
|
|
|
|
stroke_container.remove_child(
|
|
instance
|
|
)
|
|
|
|
erased_count += 1
|
|
|
|
return erased_count
|
|
|
|
|
|
# =========================================================
|
|
# Undo لسحبة الرسم
|
|
# =========================================================
|
|
|
|
func _commit_paint_stroke() -> void:
|
|
if painted_instances.is_empty():
|
|
_remove_empty_created_container()
|
|
return
|
|
|
|
if not is_instance_valid(
|
|
stroke_scene_root
|
|
):
|
|
return
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
return
|
|
|
|
var instances_snapshot: Array = (
|
|
painted_instances.duplicate()
|
|
)
|
|
|
|
var transforms_snapshot: Array = (
|
|
painted_transforms.duplicate()
|
|
)
|
|
|
|
var saved_scene_root := stroke_scene_root
|
|
var saved_container := stroke_container
|
|
|
|
var saved_created_container := (
|
|
stroke_created_container
|
|
)
|
|
|
|
var undo_redo := get_undo_redo()
|
|
|
|
undo_redo.create_action(
|
|
"World Brush: Paint Stroke (%d)"
|
|
% instances_snapshot.size(),
|
|
UndoRedo.MERGE_DISABLE,
|
|
saved_scene_root
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
&"_redo_paint_batch",
|
|
saved_scene_root,
|
|
saved_container,
|
|
instances_snapshot,
|
|
transforms_snapshot,
|
|
saved_created_container
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
&"_undo_paint_batch",
|
|
saved_scene_root,
|
|
saved_container,
|
|
instances_snapshot,
|
|
saved_created_container
|
|
)
|
|
|
|
if saved_created_container:
|
|
undo_redo.add_do_reference(
|
|
saved_container
|
|
)
|
|
|
|
for instance_value in instances_snapshot:
|
|
if instance_value is Node3D:
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if is_instance_valid(instance):
|
|
undo_redo.add_do_reference(
|
|
instance
|
|
)
|
|
|
|
undo_redo.commit_action(
|
|
false
|
|
)
|
|
|
|
_set_dock_status(
|
|
"Paint Stroke Saved: %d"
|
|
% instances_snapshot.size()
|
|
)
|
|
|
|
|
|
func _redo_paint_batch(
|
|
scene_root: Node3D,
|
|
container: Node3D,
|
|
instances: Array,
|
|
transforms: Array,
|
|
created_container: bool
|
|
) -> void:
|
|
if not is_instance_valid(scene_root):
|
|
return
|
|
|
|
if not is_instance_valid(container):
|
|
return
|
|
|
|
if (
|
|
created_container
|
|
and container.get_parent() == null
|
|
):
|
|
scene_root.add_child(
|
|
container,
|
|
true
|
|
)
|
|
|
|
container.owner = scene_root
|
|
|
|
for index in range(
|
|
instances.size()
|
|
):
|
|
var instance_value: Variant = (
|
|
instances[index]
|
|
)
|
|
|
|
if not instance_value is Node3D:
|
|
continue
|
|
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if not is_instance_valid(instance):
|
|
continue
|
|
|
|
if instance.get_parent() == null:
|
|
container.add_child(
|
|
instance,
|
|
true
|
|
)
|
|
|
|
instance.owner = scene_root
|
|
_assign_lod_child_owners(
|
|
instance,
|
|
scene_root
|
|
)
|
|
|
|
if index < transforms.size():
|
|
var saved_transform: Variant = (
|
|
transforms[index]
|
|
)
|
|
|
|
if saved_transform is Transform3D:
|
|
instance.global_transform = (
|
|
saved_transform
|
|
)
|
|
|
|
|
|
func _undo_paint_batch(
|
|
scene_root: Node3D,
|
|
container: Node3D,
|
|
instances: Array,
|
|
created_container: bool
|
|
) -> void:
|
|
if not is_instance_valid(container):
|
|
return
|
|
|
|
for instance_value in instances:
|
|
if not instance_value is Node3D:
|
|
continue
|
|
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if not is_instance_valid(instance):
|
|
continue
|
|
|
|
if instance.get_parent() == container:
|
|
instance.owner = null
|
|
|
|
container.remove_child(
|
|
instance
|
|
)
|
|
|
|
if (
|
|
created_container
|
|
and is_instance_valid(scene_root)
|
|
and container.get_parent() == scene_root
|
|
and container.get_child_count() == 0
|
|
):
|
|
container.owner = null
|
|
|
|
scene_root.remove_child(
|
|
container
|
|
)
|
|
|
|
|
|
# =========================================================
|
|
# Undo لسحبة الحذف
|
|
# =========================================================
|
|
|
|
func _commit_erase_stroke() -> void:
|
|
if erased_instances.is_empty():
|
|
return
|
|
|
|
if not is_instance_valid(
|
|
stroke_scene_root
|
|
):
|
|
return
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
return
|
|
|
|
var instances_snapshot: Array = (
|
|
erased_instances.duplicate()
|
|
)
|
|
|
|
var transforms_snapshot: Array = (
|
|
erased_transforms.duplicate()
|
|
)
|
|
|
|
var saved_scene_root := stroke_scene_root
|
|
var saved_container := stroke_container
|
|
|
|
var undo_redo := get_undo_redo()
|
|
|
|
undo_redo.create_action(
|
|
"World Brush: Erase Stroke (%d)"
|
|
% instances_snapshot.size(),
|
|
UndoRedo.MERGE_DISABLE,
|
|
saved_scene_root
|
|
)
|
|
|
|
undo_redo.add_do_method(
|
|
self,
|
|
&"_redo_erase_batch",
|
|
saved_container,
|
|
instances_snapshot
|
|
)
|
|
|
|
undo_redo.add_undo_method(
|
|
self,
|
|
&"_undo_erase_batch",
|
|
saved_scene_root,
|
|
saved_container,
|
|
instances_snapshot,
|
|
transforms_snapshot
|
|
)
|
|
|
|
for instance_value in instances_snapshot:
|
|
if instance_value is Node3D:
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if is_instance_valid(instance):
|
|
undo_redo.add_undo_reference(
|
|
instance
|
|
)
|
|
|
|
undo_redo.commit_action(
|
|
false
|
|
)
|
|
|
|
_set_dock_status(
|
|
"Erase Stroke Saved: %d"
|
|
% instances_snapshot.size()
|
|
)
|
|
|
|
|
|
func _redo_erase_batch(
|
|
container: Node3D,
|
|
instances: Array
|
|
) -> void:
|
|
if not is_instance_valid(container):
|
|
return
|
|
|
|
for instance_value in instances:
|
|
if not instance_value is Node3D:
|
|
continue
|
|
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if not is_instance_valid(instance):
|
|
continue
|
|
|
|
if instance.get_parent() == container:
|
|
instance.owner = null
|
|
|
|
container.remove_child(
|
|
instance
|
|
)
|
|
|
|
|
|
func _undo_erase_batch(
|
|
scene_root: Node3D,
|
|
container: Node3D,
|
|
instances: Array,
|
|
transforms: Array
|
|
) -> void:
|
|
if not is_instance_valid(scene_root):
|
|
return
|
|
|
|
if not is_instance_valid(container):
|
|
return
|
|
|
|
if container.get_parent() == null:
|
|
scene_root.add_child(
|
|
container,
|
|
true
|
|
)
|
|
|
|
container.owner = scene_root
|
|
|
|
for index in range(
|
|
instances.size()
|
|
):
|
|
var instance_value: Variant = (
|
|
instances[index]
|
|
)
|
|
|
|
if not instance_value is Node3D:
|
|
continue
|
|
|
|
var instance := (
|
|
instance_value as Node3D
|
|
)
|
|
|
|
if not is_instance_valid(instance):
|
|
continue
|
|
|
|
if instance.get_parent() == null:
|
|
container.add_child(
|
|
instance,
|
|
true
|
|
)
|
|
|
|
instance.owner = scene_root
|
|
_assign_lod_child_owners(
|
|
instance,
|
|
scene_root
|
|
)
|
|
|
|
if index < transforms.size():
|
|
var saved_transform: Variant = (
|
|
transforms[index]
|
|
)
|
|
|
|
if saved_transform is Transform3D:
|
|
instance.global_transform = (
|
|
saved_transform
|
|
)
|
|
|
|
|
|
func _remove_empty_created_container() -> void:
|
|
if not stroke_created_container:
|
|
return
|
|
|
|
if not is_instance_valid(
|
|
stroke_container
|
|
):
|
|
return
|
|
|
|
if stroke_container.get_child_count() > 0:
|
|
return
|
|
|
|
var parent := stroke_container.get_parent()
|
|
|
|
if parent != null:
|
|
stroke_container.owner = null
|
|
|
|
parent.remove_child(
|
|
stroke_container
|
|
)
|
|
|
|
stroke_container.free()
|
|
|
|
|
|
# =========================================================
|
|
# العثور على حاوية العناصر
|
|
# =========================================================
|
|
|
|
func _find_world_brush_container(
|
|
scene_root: Node3D
|
|
) -> Node3D:
|
|
for child in scene_root.get_children():
|
|
if not child is Node3D:
|
|
continue
|
|
|
|
if bool(
|
|
child.get_meta(
|
|
CONTAINER_META,
|
|
false
|
|
)
|
|
):
|
|
return child as Node3D
|
|
|
|
return null
|
|
|
|
|
|
# =========================================================
|
|
# اكتشاف السطح
|
|
# =========================================================
|
|
|
|
func _update_surface_hit(
|
|
camera: Camera3D,
|
|
mouse_position: Vector2
|
|
) -> void:
|
|
var edited_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
|
|
if not edited_root is Node3D:
|
|
_clear_surface_hit(
|
|
"Open A 3D Scene First"
|
|
)
|
|
|
|
return
|
|
|
|
var scene_root := edited_root as Node3D
|
|
var world_3d := scene_root.get_world_3d()
|
|
|
|
if world_3d == null:
|
|
_clear_surface_hit(
|
|
"3D World Not Available"
|
|
)
|
|
|
|
return
|
|
|
|
var ray_origin := (
|
|
camera.project_ray_origin(
|
|
mouse_position
|
|
)
|
|
)
|
|
|
|
var ray_direction := (
|
|
camera.project_ray_normal(
|
|
mouse_position
|
|
).normalized()
|
|
)
|
|
|
|
var ray_end := (
|
|
ray_origin
|
|
+ ray_direction * RAY_LENGTH
|
|
)
|
|
|
|
var result := _find_cursor_surface(
|
|
scene_root,
|
|
world_3d,
|
|
ray_origin,
|
|
ray_direction,
|
|
ray_end
|
|
)
|
|
|
|
if result.is_empty():
|
|
_clear_surface_hit(
|
|
"No Surface Under Cursor"
|
|
)
|
|
|
|
return
|
|
|
|
var hit_position: Vector3 = (
|
|
result.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var hit_normal: Vector3 = (
|
|
result.get(
|
|
"normal",
|
|
Vector3.UP
|
|
)
|
|
)
|
|
|
|
if not hit_position.is_finite():
|
|
_clear_surface_hit(
|
|
"Invalid Surface Position"
|
|
)
|
|
|
|
return
|
|
|
|
if not _is_valid_normal(
|
|
hit_normal
|
|
):
|
|
hit_normal = Vector3.UP
|
|
|
|
has_surface_hit = true
|
|
|
|
surface_position = hit_position
|
|
surface_normal = hit_normal.normalized()
|
|
|
|
surface_type = str(
|
|
result.get(
|
|
"surface_type",
|
|
"Surface"
|
|
)
|
|
)
|
|
|
|
_show_brush_preview(
|
|
scene_root
|
|
)
|
|
|
|
if stroke_mode == StrokeMode.NONE:
|
|
_set_dock_status(
|
|
"%s | X %.2f | Y %.2f | Z %.2f" % [
|
|
surface_type,
|
|
surface_position.x,
|
|
surface_position.y,
|
|
surface_position.z,
|
|
]
|
|
)
|
|
|
|
|
|
func _find_cursor_surface(
|
|
scene_root: Node3D,
|
|
world_3d: World3D,
|
|
ray_origin: Vector3,
|
|
ray_direction: Vector3,
|
|
ray_end: Vector3
|
|
) -> Dictionary:
|
|
var physics_hit := (
|
|
_intersect_physics_surface(
|
|
world_3d,
|
|
ray_origin,
|
|
ray_end
|
|
)
|
|
)
|
|
|
|
var terrain_hit := (
|
|
_intersect_terrain_cursor(
|
|
scene_root,
|
|
ray_origin,
|
|
ray_direction
|
|
)
|
|
)
|
|
|
|
if physics_hit.is_empty():
|
|
return terrain_hit
|
|
|
|
if terrain_hit.is_empty():
|
|
return physics_hit
|
|
|
|
var physics_position: Vector3 = (
|
|
physics_hit.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var terrain_position: Vector3 = (
|
|
terrain_hit.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var physics_distance := (
|
|
ray_origin.distance_squared_to(
|
|
physics_position
|
|
)
|
|
)
|
|
|
|
var terrain_distance := (
|
|
ray_origin.distance_squared_to(
|
|
terrain_position
|
|
)
|
|
)
|
|
|
|
if physics_distance <= terrain_distance:
|
|
return physics_hit
|
|
|
|
return terrain_hit
|
|
|
|
|
|
func _intersect_terrain_cursor(
|
|
scene_root: Node3D,
|
|
ray_origin: Vector3,
|
|
ray_direction: Vector3
|
|
) -> Dictionary:
|
|
var terrain_nodes := (
|
|
_get_terrain3d_nodes(
|
|
scene_root
|
|
)
|
|
)
|
|
|
|
var closest_result: Dictionary = {}
|
|
var closest_distance: float = INF
|
|
|
|
for terrain_node in terrain_nodes:
|
|
if not is_instance_valid(
|
|
terrain_node
|
|
):
|
|
continue
|
|
|
|
if not terrain_node.has_method(
|
|
&"get_intersection"
|
|
):
|
|
continue
|
|
|
|
var point_value: Variant = (
|
|
terrain_node.call(
|
|
&"get_intersection",
|
|
ray_origin,
|
|
ray_direction,
|
|
false
|
|
)
|
|
)
|
|
|
|
if typeof(point_value) != TYPE_VECTOR3:
|
|
continue
|
|
|
|
var terrain_point: Vector3 = point_value
|
|
|
|
if not _is_valid_terrain_point(
|
|
terrain_point
|
|
):
|
|
continue
|
|
|
|
var direction_to_point := (
|
|
terrain_point - ray_origin
|
|
)
|
|
|
|
if (
|
|
direction_to_point.dot(
|
|
ray_direction
|
|
)
|
|
<= 0.0
|
|
):
|
|
continue
|
|
|
|
var distance_squared := (
|
|
ray_origin.distance_squared_to(
|
|
terrain_point
|
|
)
|
|
)
|
|
|
|
if distance_squared >= closest_distance:
|
|
continue
|
|
|
|
closest_distance = distance_squared
|
|
|
|
closest_result = {
|
|
"position": terrain_point,
|
|
"normal": _get_terrain_normal(
|
|
terrain_node,
|
|
terrain_point
|
|
),
|
|
"collider": terrain_node,
|
|
"surface_type": "Terrain3D",
|
|
}
|
|
|
|
return closest_result
|
|
|
|
|
|
# =========================================================
|
|
# عينات السطح
|
|
# =========================================================
|
|
|
|
func _sample_surface_at_xz(
|
|
scene_root: Node3D,
|
|
world_3d: World3D,
|
|
x_position: float,
|
|
z_position: float,
|
|
reference_height: float
|
|
) -> Dictionary:
|
|
var ray_origin := Vector3(
|
|
x_position,
|
|
reference_height + SAMPLE_VERTICAL_RANGE,
|
|
z_position
|
|
)
|
|
|
|
var ray_end := Vector3(
|
|
x_position,
|
|
reference_height - SAMPLE_VERTICAL_RANGE,
|
|
z_position
|
|
)
|
|
|
|
var physics_hit := (
|
|
_intersect_physics_surface(
|
|
world_3d,
|
|
ray_origin,
|
|
ray_end
|
|
)
|
|
)
|
|
|
|
var terrain_hit := (
|
|
_sample_terrain_height(
|
|
scene_root,
|
|
x_position,
|
|
z_position,
|
|
reference_height
|
|
)
|
|
)
|
|
|
|
if physics_hit.is_empty():
|
|
return terrain_hit
|
|
|
|
if terrain_hit.is_empty():
|
|
return physics_hit
|
|
|
|
var physics_position: Vector3 = (
|
|
physics_hit.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var terrain_position: Vector3 = (
|
|
terrain_hit.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
if physics_position.y >= terrain_position.y:
|
|
return physics_hit
|
|
|
|
return terrain_hit
|
|
|
|
|
|
func _sample_terrain_height(
|
|
scene_root: Node3D,
|
|
x_position: float,
|
|
z_position: float,
|
|
reference_height: float
|
|
) -> Dictionary:
|
|
var terrain_nodes := (
|
|
_get_terrain3d_nodes(
|
|
scene_root
|
|
)
|
|
)
|
|
|
|
var best_result: Dictionary = {}
|
|
var highest_height: float = -INF
|
|
|
|
var query_position := Vector3(
|
|
x_position,
|
|
reference_height,
|
|
z_position
|
|
)
|
|
|
|
for terrain_node in terrain_nodes:
|
|
if not is_instance_valid(
|
|
terrain_node
|
|
):
|
|
continue
|
|
|
|
var terrain_data := (
|
|
_get_terrain_data(
|
|
terrain_node
|
|
)
|
|
)
|
|
|
|
if terrain_data == null:
|
|
continue
|
|
|
|
if not terrain_data.has_method(
|
|
&"get_height"
|
|
):
|
|
continue
|
|
|
|
var height_value: Variant = (
|
|
terrain_data.call(
|
|
&"get_height",
|
|
query_position
|
|
)
|
|
)
|
|
|
|
if (
|
|
typeof(height_value) != TYPE_FLOAT
|
|
and typeof(height_value) != TYPE_INT
|
|
):
|
|
continue
|
|
|
|
var terrain_height := float(
|
|
height_value
|
|
)
|
|
|
|
if (
|
|
is_nan(terrain_height)
|
|
or is_inf(terrain_height)
|
|
):
|
|
continue
|
|
|
|
if terrain_height <= highest_height:
|
|
continue
|
|
|
|
var terrain_position := Vector3(
|
|
x_position,
|
|
terrain_height,
|
|
z_position
|
|
)
|
|
|
|
highest_height = terrain_height
|
|
|
|
best_result = {
|
|
"position": terrain_position,
|
|
"normal": _get_terrain_normal(
|
|
terrain_node,
|
|
terrain_position
|
|
),
|
|
"collider": terrain_node,
|
|
"surface_type": "Terrain3D",
|
|
}
|
|
|
|
return best_result
|
|
|
|
|
|
func _get_terrain_data(
|
|
terrain_node: Node
|
|
) -> Object:
|
|
if terrain_node == null:
|
|
return null
|
|
|
|
if terrain_node.has_method(
|
|
&"get_data"
|
|
):
|
|
var data_value: Variant = (
|
|
terrain_node.call(
|
|
&"get_data"
|
|
)
|
|
)
|
|
|
|
if data_value is Object:
|
|
return data_value as Object
|
|
|
|
var property_value: Variant = (
|
|
terrain_node.get(
|
|
"data"
|
|
)
|
|
)
|
|
|
|
if property_value is Object:
|
|
return property_value as Object
|
|
|
|
return null
|
|
|
|
|
|
func _get_terrain_normal(
|
|
terrain_node: Node,
|
|
global_position: Vector3
|
|
) -> Vector3:
|
|
var terrain_data := (
|
|
_get_terrain_data(
|
|
terrain_node
|
|
)
|
|
)
|
|
|
|
if terrain_data == null:
|
|
return Vector3.UP
|
|
|
|
if not terrain_data.has_method(
|
|
&"get_normal"
|
|
):
|
|
return Vector3.UP
|
|
|
|
var normal_value: Variant = (
|
|
terrain_data.call(
|
|
&"get_normal",
|
|
global_position
|
|
)
|
|
)
|
|
|
|
if typeof(normal_value) != TYPE_VECTOR3:
|
|
return Vector3.UP
|
|
|
|
var terrain_normal: Vector3 = normal_value
|
|
|
|
if not _is_valid_normal(
|
|
terrain_normal
|
|
):
|
|
return Vector3.UP
|
|
|
|
return terrain_normal.normalized()
|
|
|
|
|
|
# =========================================================
|
|
# RayCast للأسطح العادية
|
|
# =========================================================
|
|
|
|
func _intersect_physics_surface(
|
|
world_3d: World3D,
|
|
ray_origin: Vector3,
|
|
ray_end: Vector3
|
|
) -> Dictionary:
|
|
var excluded_objects: Array[RID] = []
|
|
|
|
for _attempt in range(
|
|
MAX_RAY_SKIPS
|
|
):
|
|
var query := (
|
|
PhysicsRayQueryParameters3D.create(
|
|
ray_origin,
|
|
ray_end
|
|
)
|
|
)
|
|
|
|
query.collide_with_bodies = true
|
|
query.collide_with_areas = true
|
|
query.exclude = excluded_objects
|
|
|
|
var result := (
|
|
world_3d
|
|
.direct_space_state
|
|
.intersect_ray(
|
|
query
|
|
)
|
|
)
|
|
|
|
if result.is_empty():
|
|
return {}
|
|
|
|
var collider: Object = (
|
|
result.get(
|
|
"collider"
|
|
)
|
|
)
|
|
|
|
if not _belongs_to_world_brush_instance(
|
|
collider
|
|
):
|
|
result["surface_type"] = "Physics"
|
|
return result
|
|
|
|
if collider is CollisionObject3D:
|
|
var collision_object := (
|
|
collider as CollisionObject3D
|
|
)
|
|
|
|
excluded_objects.append(
|
|
collision_object.get_rid()
|
|
)
|
|
|
|
continue
|
|
|
|
return {}
|
|
|
|
return {}
|
|
|
|
|
|
func _belongs_to_world_brush_instance(
|
|
collider: Object
|
|
) -> bool:
|
|
if not collider is Node:
|
|
return false
|
|
|
|
var current_node := collider as Node
|
|
|
|
while current_node != null:
|
|
if bool(
|
|
current_node.get_meta(
|
|
INSTANCE_META,
|
|
false
|
|
)
|
|
):
|
|
return true
|
|
|
|
current_node = (
|
|
current_node.get_parent()
|
|
)
|
|
|
|
return false
|
|
|
|
|
|
# =========================================================
|
|
# توليد نقاط الرسم
|
|
# =========================================================
|
|
|
|
func _generate_brush_samples(
|
|
scene_root: Node3D,
|
|
world_3d: World3D,
|
|
container: Node3D
|
|
) -> Array[Dictionary]:
|
|
var generated_samples: Array[Dictionary] = []
|
|
|
|
var occupied_positions := (
|
|
_get_existing_positions(
|
|
container
|
|
)
|
|
)
|
|
|
|
var spacing_squared := (
|
|
minimum_spacing
|
|
* minimum_spacing
|
|
)
|
|
|
|
var maximum_attempts := maxi(
|
|
MINIMUM_PLACEMENT_ATTEMPTS,
|
|
instances_per_click
|
|
* PLACEMENT_ATTEMPTS_PER_INSTANCE
|
|
)
|
|
|
|
maximum_attempts = mini(
|
|
maximum_attempts,
|
|
MAX_TOTAL_PLACEMENT_ATTEMPTS
|
|
)
|
|
|
|
var attempts: int = 0
|
|
|
|
while (
|
|
generated_samples.size()
|
|
< instances_per_click
|
|
and attempts < maximum_attempts
|
|
):
|
|
attempts += 1
|
|
|
|
var candidate_x := surface_position.x
|
|
var candidate_z := surface_position.z
|
|
|
|
if instances_per_click > 1:
|
|
var angle := (
|
|
random_generator.randf_range(
|
|
0.0,
|
|
TAU
|
|
)
|
|
)
|
|
|
|
var distance_from_center := (
|
|
sqrt(
|
|
random_generator.randf()
|
|
)
|
|
* brush_radius
|
|
)
|
|
|
|
candidate_x += (
|
|
cos(angle)
|
|
* distance_from_center
|
|
)
|
|
|
|
candidate_z += (
|
|
sin(angle)
|
|
* distance_from_center
|
|
)
|
|
|
|
var surface_sample := (
|
|
_sample_surface_at_xz(
|
|
scene_root,
|
|
world_3d,
|
|
candidate_x,
|
|
candidate_z,
|
|
surface_position.y
|
|
)
|
|
)
|
|
|
|
if surface_sample.is_empty():
|
|
continue
|
|
|
|
var candidate_position: Vector3 = (
|
|
surface_sample.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
var candidate_normal: Vector3 = (
|
|
surface_sample.get(
|
|
"normal",
|
|
Vector3.UP
|
|
)
|
|
)
|
|
|
|
if not candidate_position.is_finite():
|
|
continue
|
|
|
|
if not _is_valid_normal(
|
|
candidate_normal
|
|
):
|
|
continue
|
|
|
|
candidate_normal = (
|
|
candidate_normal.normalized()
|
|
)
|
|
|
|
if not _is_slope_allowed(
|
|
candidate_normal
|
|
):
|
|
continue
|
|
|
|
if not _has_required_spacing(
|
|
candidate_position,
|
|
occupied_positions,
|
|
generated_samples,
|
|
spacing_squared
|
|
):
|
|
continue
|
|
|
|
var rotation_angle: float = 0.0
|
|
|
|
if random_y_rotation:
|
|
rotation_angle = (
|
|
random_generator.randf_range(
|
|
0.0,
|
|
TAU
|
|
)
|
|
)
|
|
|
|
var placement_basis := (
|
|
_create_placement_basis(
|
|
candidate_normal,
|
|
rotation_angle,
|
|
align_to_surface
|
|
)
|
|
)
|
|
|
|
generated_samples.append(
|
|
{
|
|
"position": candidate_position,
|
|
"normal": candidate_normal,
|
|
"placement_basis": placement_basis,
|
|
"surface_type": surface_sample.get(
|
|
"surface_type",
|
|
"Surface"
|
|
),
|
|
}
|
|
)
|
|
|
|
return generated_samples
|
|
|
|
|
|
func _create_placement_basis(
|
|
normal: Vector3,
|
|
rotation_angle: float,
|
|
should_align_to_surface: bool
|
|
) -> Basis:
|
|
if not should_align_to_surface:
|
|
if is_zero_approx(
|
|
rotation_angle
|
|
):
|
|
return Basis.IDENTITY
|
|
|
|
return Basis(
|
|
Vector3.UP,
|
|
rotation_angle
|
|
).orthonormalized()
|
|
|
|
var up_vector := normal.normalized()
|
|
|
|
if not _is_valid_normal(
|
|
up_vector
|
|
):
|
|
up_vector = Vector3.UP
|
|
|
|
var surface_basis := (
|
|
_create_surface_basis(
|
|
up_vector
|
|
)
|
|
)
|
|
|
|
if is_zero_approx(
|
|
rotation_angle
|
|
):
|
|
return surface_basis
|
|
|
|
var rotation_basis := Basis(
|
|
up_vector,
|
|
rotation_angle
|
|
)
|
|
|
|
return (
|
|
rotation_basis
|
|
* surface_basis
|
|
).orthonormalized()
|
|
|
|
|
|
func _create_surface_basis(
|
|
up_vector: Vector3
|
|
) -> Basis:
|
|
var up := up_vector.normalized()
|
|
|
|
if not _is_valid_normal(up):
|
|
up = Vector3.UP
|
|
|
|
var forward := (
|
|
Vector3.FORWARD
|
|
- up * Vector3.FORWARD.dot(up)
|
|
)
|
|
|
|
if forward.length_squared() < 0.000001:
|
|
forward = (
|
|
Vector3.RIGHT
|
|
- up * Vector3.RIGHT.dot(up)
|
|
)
|
|
|
|
if forward.length_squared() < 0.000001:
|
|
return Basis.IDENTITY
|
|
|
|
forward = forward.normalized()
|
|
|
|
var right := (
|
|
forward.cross(up).normalized()
|
|
)
|
|
|
|
if not _is_valid_normal(right):
|
|
return Basis.IDENTITY
|
|
|
|
var backward := (
|
|
right.cross(up).normalized()
|
|
)
|
|
|
|
if not _is_valid_normal(backward):
|
|
return Basis.IDENTITY
|
|
|
|
return Basis(
|
|
right,
|
|
up,
|
|
backward
|
|
).orthonormalized()
|
|
|
|
|
|
func _is_slope_allowed(
|
|
normal: Vector3
|
|
) -> bool:
|
|
if not _is_valid_normal(
|
|
normal
|
|
):
|
|
return false
|
|
|
|
var safe_normal := normal.normalized()
|
|
|
|
var up_alignment := clampf(
|
|
safe_normal.dot(
|
|
Vector3.UP
|
|
),
|
|
-1.0,
|
|
1.0
|
|
)
|
|
|
|
var slope_radians := acos(
|
|
up_alignment
|
|
)
|
|
|
|
var slope_degrees := rad_to_deg(
|
|
slope_radians
|
|
)
|
|
|
|
return (
|
|
slope_degrees
|
|
<= maximum_slope_degrees
|
|
)
|
|
|
|
|
|
func _get_existing_positions(
|
|
container: Node3D
|
|
) -> Array[Vector3]:
|
|
var positions: Array[Vector3] = []
|
|
|
|
if not is_instance_valid(container):
|
|
return positions
|
|
|
|
if not container.is_inside_tree():
|
|
return positions
|
|
|
|
for child in container.get_children():
|
|
if child is Node3D:
|
|
positions.append(
|
|
(child as Node3D).global_position
|
|
)
|
|
|
|
return positions
|
|
|
|
|
|
func _has_required_spacing(
|
|
candidate: Vector3,
|
|
existing_positions: Array[Vector3],
|
|
new_samples: Array[Dictionary],
|
|
spacing_squared: float
|
|
) -> bool:
|
|
if spacing_squared <= 0.0:
|
|
return true
|
|
|
|
for existing_position in existing_positions:
|
|
if (
|
|
_horizontal_distance_squared(
|
|
candidate,
|
|
existing_position
|
|
)
|
|
< spacing_squared
|
|
):
|
|
return false
|
|
|
|
for sample in new_samples:
|
|
var new_position: Vector3 = (
|
|
sample.get(
|
|
"position",
|
|
Vector3.ZERO
|
|
)
|
|
)
|
|
|
|
if (
|
|
_horizontal_distance_squared(
|
|
candidate,
|
|
new_position
|
|
)
|
|
< spacing_squared
|
|
):
|
|
return false
|
|
|
|
return true
|
|
|
|
|
|
func _horizontal_distance_squared(
|
|
first: Vector3,
|
|
second: Vector3
|
|
) -> float:
|
|
var difference := Vector2(
|
|
first.x - second.x,
|
|
first.z - second.z
|
|
)
|
|
|
|
return difference.length_squared()
|
|
|
|
|
|
# =========================================================
|
|
# Terrain3D Cache
|
|
# =========================================================
|
|
|
|
func _get_terrain3d_nodes(
|
|
scene_root: Node
|
|
) -> Array[Node]:
|
|
var current_time := (
|
|
Time.get_ticks_msec()
|
|
)
|
|
|
|
var root_changed := (
|
|
not is_instance_valid(
|
|
cached_scene_root
|
|
)
|
|
or cached_scene_root != scene_root
|
|
)
|
|
|
|
var scan_expired := (
|
|
current_time
|
|
- last_terrain_scan_time
|
|
>= TERRAIN_SCAN_INTERVAL_MS
|
|
)
|
|
|
|
var cache_invalid := false
|
|
|
|
for terrain_node in cached_terrain_nodes:
|
|
if not is_instance_valid(
|
|
terrain_node
|
|
):
|
|
cache_invalid = true
|
|
break
|
|
|
|
if (
|
|
root_changed
|
|
or scan_expired
|
|
or cache_invalid
|
|
):
|
|
cached_scene_root = scene_root
|
|
cached_terrain_nodes.clear()
|
|
|
|
_collect_terrain3d_nodes(
|
|
scene_root,
|
|
cached_terrain_nodes
|
|
)
|
|
|
|
last_terrain_scan_time = current_time
|
|
|
|
return cached_terrain_nodes
|
|
|
|
|
|
func _collect_terrain3d_nodes(
|
|
current_node: Node,
|
|
result: Array[Node]
|
|
) -> void:
|
|
if _is_terrain3d_node(
|
|
current_node
|
|
):
|
|
result.append(
|
|
current_node
|
|
)
|
|
|
|
for child in current_node.get_children(
|
|
true
|
|
):
|
|
if child is Node:
|
|
_collect_terrain3d_nodes(
|
|
child as Node,
|
|
result
|
|
)
|
|
|
|
|
|
func _is_terrain3d_node(
|
|
node: Node
|
|
) -> bool:
|
|
if node == null:
|
|
return false
|
|
|
|
if node.get_class() == "Terrain3D":
|
|
return true
|
|
|
|
return node.is_class(
|
|
"Terrain3D"
|
|
)
|
|
|
|
|
|
func _clear_terrain_cache() -> void:
|
|
cached_scene_root = null
|
|
cached_terrain_nodes.clear()
|
|
last_terrain_scan_time = 0
|
|
|
|
|
|
# =========================================================
|
|
# دائرة الفرشاة
|
|
# =========================================================
|
|
|
|
func _show_brush_preview(
|
|
scene_root: Node3D
|
|
) -> void:
|
|
_ensure_brush_preview(
|
|
scene_root
|
|
)
|
|
|
|
if not is_instance_valid(
|
|
brush_preview
|
|
):
|
|
return
|
|
|
|
brush_preview.global_transform = Transform3D(
|
|
Basis.IDENTITY,
|
|
surface_position
|
|
+ Vector3.UP * BRUSH_HEIGHT_OFFSET
|
|
)
|
|
|
|
brush_preview.visible = true
|
|
|
|
|
|
func _ensure_brush_preview(
|
|
scene_root: Node3D
|
|
) -> void:
|
|
if is_instance_valid(
|
|
brush_preview
|
|
):
|
|
if brush_preview.get_parent() == scene_root:
|
|
return
|
|
|
|
_remove_brush_preview()
|
|
|
|
brush_preview = MeshInstance3D.new()
|
|
brush_preview.name = "_WorldBrushPreview"
|
|
brush_preview.top_level = true
|
|
brush_preview.visible = false
|
|
|
|
brush_preview.cast_shadow = (
|
|
GeometryInstance3D
|
|
.SHADOW_CASTING_SETTING_OFF
|
|
)
|
|
|
|
brush_preview_mesh = ImmediateMesh.new()
|
|
brush_preview.mesh = brush_preview_mesh
|
|
|
|
brush_preview_material = StandardMaterial3D.new()
|
|
|
|
brush_preview_material.shading_mode = (
|
|
BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
)
|
|
|
|
brush_preview_material.no_depth_test = true
|
|
|
|
_apply_preview_color()
|
|
|
|
scene_root.add_child(
|
|
brush_preview,
|
|
false,
|
|
Node.INTERNAL_MODE_BACK
|
|
)
|
|
|
|
_rebuild_brush_preview_mesh()
|
|
|
|
|
|
func _rebuild_brush_preview_mesh() -> void:
|
|
if not is_instance_valid(
|
|
brush_preview_mesh
|
|
):
|
|
return
|
|
|
|
brush_preview_mesh.clear_surfaces()
|
|
|
|
brush_preview_mesh.surface_begin(
|
|
Mesh.PRIMITIVE_LINE_STRIP,
|
|
brush_preview_material
|
|
)
|
|
|
|
for index in range(
|
|
BRUSH_SEGMENTS + 1
|
|
):
|
|
var percentage := (
|
|
float(index)
|
|
/ float(BRUSH_SEGMENTS)
|
|
)
|
|
|
|
var angle := percentage * TAU
|
|
|
|
var point := Vector3(
|
|
cos(angle) * brush_radius,
|
|
0.0,
|
|
sin(angle) * brush_radius
|
|
)
|
|
|
|
brush_preview_mesh.surface_add_vertex(
|
|
point
|
|
)
|
|
|
|
brush_preview_mesh.surface_end()
|
|
|
|
|
|
func _set_erase_preview_active(
|
|
enabled: bool
|
|
) -> void:
|
|
erase_preview_active = enabled
|
|
_apply_preview_color()
|
|
|
|
|
|
func _apply_preview_color() -> void:
|
|
if not is_instance_valid(
|
|
brush_preview_material
|
|
):
|
|
return
|
|
|
|
if erase_preview_active:
|
|
brush_preview_material.albedo_color = (
|
|
ERASE_PREVIEW_COLOR
|
|
)
|
|
else:
|
|
brush_preview_material.albedo_color = (
|
|
PAINT_PREVIEW_COLOR
|
|
)
|
|
|
|
|
|
func _hide_brush_preview() -> void:
|
|
if is_instance_valid(
|
|
brush_preview
|
|
):
|
|
brush_preview.visible = false
|
|
|
|
|
|
func _remove_brush_preview() -> void:
|
|
if is_instance_valid(
|
|
brush_preview
|
|
):
|
|
brush_preview.queue_free()
|
|
|
|
brush_preview = null
|
|
brush_preview_mesh = null
|
|
brush_preview_material = null
|
|
erase_preview_active = false
|
|
|
|
|
|
# =========================================================
|
|
# التحقق والحالة
|
|
# =========================================================
|
|
|
|
func _is_valid_terrain_point(
|
|
point: Vector3
|
|
) -> bool:
|
|
if not point.is_finite():
|
|
return false
|
|
|
|
if absf(point.x) >= INVALID_TERRAIN_LIMIT:
|
|
return false
|
|
|
|
if absf(point.y) >= INVALID_TERRAIN_LIMIT:
|
|
return false
|
|
|
|
if absf(point.z) >= INVALID_TERRAIN_LIMIT:
|
|
return false
|
|
|
|
return true
|
|
|
|
|
|
func _is_valid_normal(
|
|
normal: Vector3
|
|
) -> bool:
|
|
if not normal.is_finite():
|
|
return false
|
|
|
|
return (
|
|
normal.length_squared()
|
|
>= 0.000001
|
|
)
|
|
|
|
|
|
func _clear_surface_hit(
|
|
message: String
|
|
) -> void:
|
|
has_surface_hit = false
|
|
|
|
surface_position = Vector3.ZERO
|
|
surface_normal = Vector3.UP
|
|
surface_type = ""
|
|
|
|
_hide_brush_preview()
|
|
|
|
_set_dock_status(
|
|
message
|
|
)
|
|
|
|
|
|
func _set_dock_status(
|
|
message: String
|
|
) -> void:
|
|
if not is_instance_valid(
|
|
dock_content
|
|
):
|
|
return
|
|
|
|
var status_label := (
|
|
dock_content.get_node_or_null(
|
|
"StatusLabel"
|
|
) as Label
|
|
)
|
|
|
|
if status_label != null:
|
|
status_label.text = message
|
|
|
|
|
|
# =========================================================
|
|
# إشارات الواجهة
|
|
# =========================================================
|
|
|
|
func _on_selected_scene_changed(
|
|
scene: PackedScene
|
|
) -> void:
|
|
_end_active_stroke()
|
|
|
|
selected_scene = scene
|
|
|
|
if selected_scene == null:
|
|
has_surface_hit = false
|
|
surface_type = ""
|
|
|
|
_hide_brush_preview()
|
|
|
|
print(
|
|
"World Brush: Scene selection cleared."
|
|
)
|
|
|
|
return
|
|
|
|
print(
|
|
"World Brush: Selected scene -> ",
|
|
selected_scene.resource_path
|
|
)
|
|
|
|
|
|
func _on_brush_enabled_changed(
|
|
enabled: bool
|
|
) -> void:
|
|
if not enabled:
|
|
_end_active_stroke()
|
|
|
|
brush_enabled = enabled
|
|
|
|
if not brush_enabled:
|
|
has_surface_hit = false
|
|
surface_type = ""
|
|
|
|
_hide_brush_preview()
|
|
|
|
print(
|
|
"World Brush: Brush enabled -> ",
|
|
brush_enabled
|
|
)
|
|
|
|
|
|
func _on_brush_radius_changed(
|
|
radius: float
|
|
) -> void:
|
|
brush_radius = maxf(
|
|
radius,
|
|
0.5
|
|
)
|
|
|
|
_rebuild_brush_preview_mesh()
|
|
|
|
|
|
func _on_instances_per_click_changed(
|
|
count: int
|
|
) -> void:
|
|
instances_per_click = maxi(
|
|
count,
|
|
1
|
|
)
|
|
|
|
|
|
func _on_minimum_spacing_changed(
|
|
spacing: float
|
|
) -> void:
|
|
minimum_spacing = maxf(
|
|
spacing,
|
|
0.0
|
|
)
|
|
|
|
|
|
func _on_random_y_rotation_changed(
|
|
enabled: bool
|
|
) -> void:
|
|
random_y_rotation = enabled
|
|
|
|
|
|
func _on_align_to_surface_changed(
|
|
enabled: bool
|
|
) -> void:
|
|
align_to_surface = enabled
|
|
|
|
|
|
func _on_maximum_slope_changed(
|
|
value: float
|
|
) -> void:
|
|
maximum_slope_degrees = clampf(
|
|
value,
|
|
0.0,
|
|
90.0
|
|
)
|
|
|
|
print(
|
|
"World Brush: Maximum slope -> ",
|
|
maximum_slope_degrees
|
|
)
|
|
|
|
|
|
func _on_height_offset_changed(
|
|
value: float
|
|
) -> void:
|
|
height_offset = value
|
|
|
|
print(
|
|
"World Brush: Height offset -> ",
|
|
height_offset
|
|
)
|
|
|
|
|
|
func _on_height_offset_mode_changed(
|
|
mode: int
|
|
) -> void:
|
|
height_offset_mode = clampi(
|
|
mode,
|
|
HeightOffsetMode.WORLD_Y,
|
|
HeightOffsetMode.SURFACE_NORMAL
|
|
)
|
|
|
|
print(
|
|
"World Brush: Height offset mode -> ",
|
|
height_offset_mode
|
|
)
|
|
|
|
|
|
func _on_random_scale_enabled_changed(
|
|
enabled: bool
|
|
) -> void:
|
|
random_scale_enabled = enabled
|
|
|
|
print(
|
|
"World Brush: Random scale -> ",
|
|
random_scale_enabled
|
|
)
|
|
|
|
|
|
func _on_minimum_scale_changed(
|
|
value: float
|
|
) -> void:
|
|
minimum_scale = maxf(
|
|
value,
|
|
0.01
|
|
)
|
|
|
|
if minimum_scale > maximum_scale:
|
|
maximum_scale = minimum_scale
|
|
|
|
print(
|
|
"World Brush: Minimum scale -> ",
|
|
minimum_scale
|
|
)
|
|
|
|
|
|
func _on_maximum_scale_changed(
|
|
value: float
|
|
) -> void:
|
|
maximum_scale = maxf(
|
|
value,
|
|
0.01
|
|
)
|
|
|
|
if maximum_scale < minimum_scale:
|
|
minimum_scale = maximum_scale
|
|
|
|
print(
|
|
"World Brush: Maximum scale -> ",
|
|
maximum_scale
|
|
)
|
|
|
|
|
|
func _on_auto_lod_settings_changed(
|
|
enabled: bool,
|
|
detail_bias: float,
|
|
cull_distance: float
|
|
) -> void:
|
|
auto_lod_enabled = enabled
|
|
auto_lod_bias = clampf(detail_bias, 0.0, 4.0)
|
|
auto_lod_cull_distance = maxf(cull_distance, 0.0)
|
|
_apply_auto_lod_settings_to_existing_instances()
|
|
print(
|
|
"World Brush: Auto LOD -> ",
|
|
auto_lod_enabled,
|
|
" | bias ",
|
|
auto_lod_bias,
|
|
" | cull ",
|
|
auto_lod_cull_distance
|
|
)
|
|
|
|
|
|
func _on_auto_lod_import_requested(
|
|
scene_path: String,
|
|
enabled: bool
|
|
) -> void:
|
|
call_deferred(
|
|
"_apply_auto_lod_import_setting",
|
|
scene_path,
|
|
enabled
|
|
)
|
|
|
|
|
|
func _apply_auto_lod_to_instance(
|
|
instance: Node3D,
|
|
scene_owner: Node = null
|
|
) -> bool:
|
|
var controller := instance.get_node_or_null(
|
|
NodePath(AUTO_LOD_CONTROLLER_NAME)
|
|
)
|
|
var effective_cull := auto_lod_cull_distance
|
|
if bool(instance.get_meta(LOD_GROUP_META, false)):
|
|
effective_cull = 0.0
|
|
if auto_lod_enabled:
|
|
var created := false
|
|
if controller == null:
|
|
controller = Node.new()
|
|
controller.name = AUTO_LOD_CONTROLLER_NAME
|
|
controller.set_script(AUTO_LOD_CONTROLLER_SCRIPT)
|
|
instance.add_child(controller, true)
|
|
created = true
|
|
if scene_owner != null:
|
|
controller.owner = scene_owner
|
|
controller.call(
|
|
"configure",
|
|
true,
|
|
auto_lod_bias,
|
|
effective_cull
|
|
)
|
|
return created or true
|
|
if controller == null:
|
|
return false
|
|
if controller.has_method("restore_original_settings"):
|
|
controller.call("restore_original_settings")
|
|
instance.remove_child(controller)
|
|
controller.free()
|
|
return true
|
|
|
|
|
|
func _apply_auto_lod_settings_to_existing_instances() -> void:
|
|
if selected_scene == null or selected_scene.resource_path.is_empty():
|
|
return
|
|
var scene_root := get_editor_interface().get_edited_scene_root()
|
|
if scene_root == null:
|
|
return
|
|
var roots: Array[Node3D] = []
|
|
_collect_painted_instance_roots(scene_root, roots)
|
|
var updated := 0
|
|
for instance in roots:
|
|
if _get_painted_source_path(instance) != selected_scene.resource_path:
|
|
continue
|
|
if _apply_auto_lod_to_instance(instance, scene_root):
|
|
updated += 1
|
|
if updated <= 0:
|
|
return
|
|
var editor_interface := get_editor_interface()
|
|
if editor_interface.has_method(&"mark_scene_as_unsaved"):
|
|
editor_interface.call(&"mark_scene_as_unsaved")
|
|
print("World Brush: Updated Auto LOD instances -> ", updated)
|
|
|
|
|
|
func _collect_painted_instance_roots(
|
|
node: Node,
|
|
result: Array[Node3D]
|
|
) -> void:
|
|
if node is Node3D and (
|
|
bool(node.get_meta(INSTANCE_META, false))
|
|
or bool(node.get_meta(LOD_GROUP_META, false))
|
|
):
|
|
result.append(node as Node3D)
|
|
return
|
|
for child in node.get_children():
|
|
_collect_painted_instance_roots(child, result)
|
|
|
|
|
|
func _get_painted_source_path(instance: Node3D) -> String:
|
|
var source_path := str(instance.get_meta(SOURCE_SCENE_META, ""))
|
|
if source_path.is_empty():
|
|
source_path = str(instance.get_meta(&"lod_source_scene_path", ""))
|
|
if source_path.is_empty():
|
|
source_path = instance.scene_file_path
|
|
if source_path.is_empty() and bool(instance.get_meta(LOD_GROUP_META, false)):
|
|
var near_level := instance.get_node_or_null(^"Near") as Node3D
|
|
if near_level != null:
|
|
source_path = near_level.scene_file_path
|
|
return source_path
|
|
|
|
|
|
func _apply_auto_lod_import_setting(
|
|
scene_path: String,
|
|
enabled: bool
|
|
) -> void:
|
|
if scene_path.is_empty() or not ResourceLoader.exists(scene_path, "PackedScene"):
|
|
_set_dock_status("Auto LOD: Active scene is missing or unsaved")
|
|
return
|
|
var import_paths := PackedStringArray()
|
|
_collect_import_sources_from_path(scene_path, import_paths)
|
|
if import_paths.is_empty():
|
|
_set_dock_status("Auto LOD: No supported imported model found in active scene")
|
|
return
|
|
var changed_paths := PackedStringArray()
|
|
for source_path in import_paths:
|
|
var sidecar_path := source_path + ".import"
|
|
var config := ConfigFile.new()
|
|
if config.load(sidecar_path) != OK:
|
|
continue
|
|
var current_value := bool(config.get_value("params", "meshes/generate_lods", true))
|
|
if current_value == enabled:
|
|
changed_paths.append(source_path)
|
|
continue
|
|
config.set_value("params", "meshes/generate_lods", enabled)
|
|
if config.save(sidecar_path) == OK:
|
|
changed_paths.append(source_path)
|
|
if changed_paths.is_empty():
|
|
_set_dock_status("Auto LOD: Import settings could not be updated")
|
|
return
|
|
_set_dock_status("Auto LOD: Reimporting active model only...")
|
|
get_editor_interface().get_resource_filesystem().reimport_files(changed_paths)
|
|
_set_dock_status(
|
|
"Auto LOD enabled for active model" if enabled
|
|
else "Auto LOD disabled for active model"
|
|
)
|
|
print("World Brush: Reimported Auto LOD sources -> ", changed_paths)
|
|
|
|
|
|
func _collect_import_sources_from_path(
|
|
scene_path: String,
|
|
result: PackedStringArray
|
|
) -> void:
|
|
_add_import_source_if_supported(scene_path, result)
|
|
var packed_scene := load(scene_path) as PackedScene
|
|
if packed_scene == null or not packed_scene.can_instantiate():
|
|
return
|
|
var instance := packed_scene.instantiate()
|
|
_collect_import_sources_from_node(instance, result)
|
|
instance.free()
|
|
|
|
|
|
func _collect_import_sources_from_node(
|
|
node: Node,
|
|
result: PackedStringArray
|
|
) -> void:
|
|
if node is Node3D:
|
|
_add_import_source_if_supported((node as Node3D).scene_file_path, result)
|
|
if node is MeshInstance3D:
|
|
var mesh := (node as MeshInstance3D).mesh
|
|
if mesh != null:
|
|
_add_import_source_if_supported(mesh.resource_path, result)
|
|
for child in node.get_children():
|
|
_collect_import_sources_from_node(child, result)
|
|
|
|
|
|
func _add_import_source_if_supported(
|
|
resource_path: String,
|
|
result: PackedStringArray
|
|
) -> void:
|
|
if resource_path.is_empty():
|
|
return
|
|
var source_path := resource_path.get_slice("::", 0)
|
|
if not AUTO_LOD_IMPORT_EXTENSIONS.has(source_path.get_extension().to_lower()):
|
|
return
|
|
if not FileAccess.file_exists(source_path + ".import"):
|
|
return
|
|
if not result.has(source_path):
|
|
result.append(source_path)
|
|
|
|
|
|
func _on_lod_settings_changed(
|
|
enabled: bool,
|
|
medium_scene: PackedScene,
|
|
distant_scene: PackedScene,
|
|
medium_distance: float,
|
|
distant_distance: float,
|
|
cull_distance: float
|
|
) -> void:
|
|
lod_enabled = enabled
|
|
lod1_scene = medium_scene
|
|
lod2_scene = distant_scene
|
|
lod1_distance = maxf(medium_distance, 1.0)
|
|
lod2_distance = maxf(
|
|
distant_distance,
|
|
lod1_distance + 1.0
|
|
)
|
|
lod_cull_distance = maxf(cull_distance, 0.0)
|
|
_apply_lod_settings_to_existing_groups()
|
|
_apply_auto_lod_settings_to_existing_instances()
|
|
|
|
print(
|
|
"World Brush: Distance LOD -> ",
|
|
lod_enabled,
|
|
" | ",
|
|
lod1_distance,
|
|
" | ",
|
|
lod2_distance,
|
|
" | ",
|
|
lod_cull_distance
|
|
)
|
|
|
|
|
|
func _apply_lod_settings_to_existing_groups() -> void:
|
|
if selected_scene == null:
|
|
return
|
|
|
|
var source_path := selected_scene.resource_path
|
|
if source_path.is_empty():
|
|
return
|
|
|
|
var scene_root := (
|
|
get_editor_interface()
|
|
.get_edited_scene_root()
|
|
)
|
|
if scene_root == null:
|
|
return
|
|
|
|
var lod1_path := (
|
|
lod1_scene.resource_path if lod1_scene != null else ""
|
|
)
|
|
var lod2_path := (
|
|
lod2_scene.resource_path if lod2_scene != null else ""
|
|
)
|
|
var updated := 0
|
|
for candidate in get_tree().get_nodes_in_group(
|
|
LOD_NODE_GROUP
|
|
):
|
|
if not candidate is Node3D:
|
|
continue
|
|
|
|
var lod_group := candidate as Node3D
|
|
var group_source_path := str(
|
|
lod_group.get_meta(
|
|
&"lod_source_scene_path",
|
|
""
|
|
)
|
|
)
|
|
if group_source_path.is_empty():
|
|
var near_level := lod_group.get_node_or_null(
|
|
^"Near"
|
|
) as Node3D
|
|
if near_level != null:
|
|
group_source_path = near_level.scene_file_path
|
|
|
|
if group_source_path != source_path:
|
|
continue
|
|
|
|
var settings_changed := (
|
|
bool(lod_group.get_meta(&"lod_enabled", true))
|
|
!= lod_enabled
|
|
or str(lod_group.get_meta(&"lod1_scene_path", ""))
|
|
!= lod1_path
|
|
or str(lod_group.get_meta(&"lod2_scene_path", ""))
|
|
!= lod2_path
|
|
or not is_equal_approx(
|
|
float(lod_group.get_meta(&"lod1_distance", -1.0)),
|
|
lod1_distance
|
|
)
|
|
or not is_equal_approx(
|
|
float(lod_group.get_meta(&"lod2_distance", -1.0)),
|
|
lod2_distance
|
|
)
|
|
or not is_equal_approx(
|
|
float(lod_group.get_meta(&"lod_cull_distance", -1.0)),
|
|
lod_cull_distance
|
|
)
|
|
)
|
|
if not settings_changed:
|
|
continue
|
|
|
|
lod_group.set_meta(&"lod_source_scene_path", source_path)
|
|
lod_group.set_meta(&"lod_enabled", lod_enabled)
|
|
lod_group.set_meta(&"lod1_scene_path", lod1_path)
|
|
lod_group.set_meta(&"lod2_scene_path", lod2_path)
|
|
lod_group.set_meta(&"lod1_distance", lod1_distance)
|
|
lod_group.set_meta(&"lod2_distance", lod2_distance)
|
|
lod_group.set_meta(&"lod_cull_distance", lod_cull_distance)
|
|
_sync_existing_lod_level(
|
|
lod_group,
|
|
&"Medium",
|
|
lod1_scene if lod_enabled else null,
|
|
scene_root
|
|
)
|
|
_sync_existing_lod_level(
|
|
lod_group,
|
|
&"Distant",
|
|
lod2_scene if lod_enabled else null,
|
|
scene_root
|
|
)
|
|
if lod_group.has_method(&"configure"):
|
|
lod_group.call(
|
|
&"configure",
|
|
lod1_distance,
|
|
lod2_distance,
|
|
lod_cull_distance
|
|
)
|
|
updated += 1
|
|
|
|
if updated <= 0:
|
|
return
|
|
|
|
var editor_interface := get_editor_interface()
|
|
if editor_interface.has_method(&"mark_scene_as_unsaved"):
|
|
editor_interface.call(&"mark_scene_as_unsaved")
|
|
|
|
print(
|
|
"World Brush: Updated painted LOD distances -> ",
|
|
updated
|
|
)
|
|
|
|
|
|
func _sync_existing_lod_level(
|
|
lod_group: Node3D,
|
|
level_name: StringName,
|
|
desired_scene: PackedScene,
|
|
scene_root: Node3D
|
|
) -> void:
|
|
var existing := lod_group.get_node_or_null(
|
|
NodePath(level_name)
|
|
) as Node3D
|
|
if desired_scene == null:
|
|
if existing != null:
|
|
lod_group.remove_child(existing)
|
|
existing.free()
|
|
return
|
|
|
|
if (
|
|
existing != null
|
|
and existing.scene_file_path == desired_scene.resource_path
|
|
):
|
|
return
|
|
|
|
var replacement := _instantiate_scene_root(desired_scene)
|
|
if replacement == null:
|
|
return
|
|
|
|
if existing != null:
|
|
lod_group.remove_child(existing)
|
|
existing.free()
|
|
|
|
replacement.name = str(level_name)
|
|
replacement.transform = Transform3D.IDENTITY
|
|
replacement.visible = false
|
|
lod_group.add_child(replacement, true)
|
|
replacement.owner = scene_root
|
|
_disable_native_lod_ranges(replacement)
|