@tool extends VBoxContainer enum HeightOffsetMode { WORLD_Y, SURFACE_NORMAL, } signal selected_scene_changed(scene: PackedScene) signal scene_palette_changed(scenes: Array) signal brush_enabled_changed(enabled: bool) signal brush_radius_changed(radius: float) signal instances_per_click_changed(count: int) signal minimum_spacing_changed(spacing: float) signal random_y_rotation_changed(enabled: bool) signal align_to_surface_changed(enabled: bool) signal maximum_slope_changed(value: float) signal random_scale_enabled_changed(enabled: bool) signal minimum_scale_changed(value: float) signal maximum_scale_changed(value: float) signal height_offset_changed(value: float) signal height_offset_mode_changed(mode: int) signal lod_settings_changed( enabled: bool, lod1_scene: PackedScene, lod2_scene: PackedScene, lod1_distance: float, lod2_distance: float, cull_distance: float ) signal auto_lod_settings_changed( enabled: bool, lod_bias: float, cull_distance: float ) signal auto_lod_import_requested( scene_path: String, enabled: bool ) const PALETTE_CONFIG_PATH := "user://world_brush_palette.cfg" const PALETTE_SECTION := "scene_palette" const CONFIG_SCENE_PATHS := "scene_paths" const CONFIG_ACTIVE_SCENE_PATH := "active_scene_path" const SETTINGS_SECTION := "brush_settings" const SCENE_SETTINGS_SECTION := "scene_brush_settings" const CONFIG_SCENE_SETTINGS := "profiles" const CONFIG_BRUSH_RADIUS := "brush_radius" const CONFIG_INSTANCES_PER_STAMP := "instances_per_stamp" const CONFIG_MINIMUM_SPACING := "minimum_spacing" const CONFIG_RANDOM_Y_ROTATION := "random_y_rotation" const CONFIG_ALIGN_TO_SURFACE := "align_to_surface" const CONFIG_MAXIMUM_SLOPE := "maximum_slope" const CONFIG_RANDOM_SCALE_ENABLED := "random_scale_enabled" const CONFIG_MINIMUM_SCALE := "minimum_scale" const CONFIG_MAXIMUM_SCALE := "maximum_scale" const CONFIG_HEIGHT_OFFSET := "height_offset" const CONFIG_HEIGHT_OFFSET_MODE := "height_offset_mode" const CONFIG_LOD_ENABLED := "lod_enabled" const CONFIG_LOD1_SCENE_PATH := "lod1_scene_path" const CONFIG_LOD2_SCENE_PATH := "lod2_scene_path" const CONFIG_LOD1_DISTANCE := "lod1_distance" const CONFIG_LOD2_DISTANCE := "lod2_distance" const CONFIG_LOD_CULL_DISTANCE := "lod_cull_distance" const CONFIG_AUTO_LOD_ENABLED := "auto_lod_enabled" const CONFIG_AUTO_LOD_BIAS := "auto_lod_bias" const CONFIG_AUTO_LOD_CULL_DISTANCE := "auto_lod_cull_distance" const INTERFACE_SECTION := "interface" const CONFIG_INTERFACE_LANGUAGE := "language" const LANGUAGE_ENGLISH := "en" const LANGUAGE_ARABIC := "ar" @onready var title_label: Label = $TitleLabel @onready var status_label: Label = $StatusLabel var settings_scroll: ScrollContainer var controls_container: VBoxContainer var language_button: Button var scenes_list: VBoxContainer var palette_title_label: Label var active_scene_label: Label var previous_scene_button: Button var next_scene_button: Button var add_scene_button: Button var brush_toggle: CheckButton var brush_radius_label: Label var radius_spin_box: SpinBox var instances_label: Label var instances_spin_box: SpinBox var spacing_label: Label var spacing_spin_box: SpinBox var random_rotation_toggle: CheckButton var align_surface_toggle: CheckButton var maximum_slope_label: Label var maximum_slope_spin_box: SpinBox var height_offset_label: Label var height_offset_spin_box: SpinBox var height_offset_mode_label: Label var height_offset_mode_option: OptionButton var height_offset_help_label: Label var random_scale_toggle: CheckButton var minimum_scale_label: Label var minimum_scale_spin_box: SpinBox var maximum_scale_label: Label var maximum_scale_spin_box: SpinBox var lod_toggle: CheckButton var lod_controls_container: VBoxContainer var lod1_scene_label: Label var lod1_picker: EditorResourcePicker var lod2_scene_label: Label var lod2_picker: EditorResourcePicker var lod1_distance_label: Label var lod1_distance_spin_box: SpinBox var lod2_distance_label: Label var lod2_distance_spin_box: SpinBox var lod_cull_distance_label: Label var lod_cull_distance_spin_box: SpinBox var lod_help_label: Label var auto_lod_toggle: CheckButton var auto_lod_controls_container: VBoxContainer var auto_lod_bias_label: Label var auto_lod_bias_spin_box: SpinBox var auto_lod_cull_label: Label var auto_lod_cull_spin_box: SpinBox var auto_lod_reimport_button: Button var auto_lod_help_label: Label var scene_rows: Array[Dictionary] = [] var active_picker: EditorResourcePicker 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 auto_lod_enabled: bool = false var auto_lod_bias: float = 1.0 var auto_lod_cull_distance: float = 0.0 var current_language: String = LANGUAGE_ENGLISH var is_loading_configuration: bool = false var is_applying_scene_settings: bool = false var scene_brush_settings: Dictionary = {} var last_status_source: String = "" var last_status_display: String = "" var is_applying_status_translation: bool = false func _ready() -> void: set_process(true) _create_interface() _load_saved_configuration() _apply_language() _update_scene_interface() _update_controls() call_deferred( "_emit_loaded_state" ) func _process( _delta: float ) -> void: _watch_external_status_message() # ========================================================= # إنشاء الواجهة # ========================================================= func _create_interface() -> void: if is_instance_valid( settings_scroll ): return settings_scroll = ScrollContainer.new() settings_scroll.name = "BrushSettingsScroll" settings_scroll.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) settings_scroll.size_flags_vertical = ( Control.SIZE_EXPAND_FILL ) settings_scroll.horizontal_scroll_mode = ( ScrollContainer.SCROLL_MODE_DISABLED ) settings_scroll.vertical_scroll_mode = ( ScrollContainer.SCROLL_MODE_AUTO ) settings_scroll.follow_focus = true settings_scroll.clip_contents = true _insert_before_status( settings_scroll ) controls_container = VBoxContainer.new() controls_container.name = "BrushControls" controls_container.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) controls_container.size_flags_vertical = ( Control.SIZE_SHRINK_BEGIN ) settings_scroll.add_child( controls_container ) _create_language_selector() _create_scene_palette() _create_auto_lod_controls() _create_lod_controls() _create_brush_toggle() _create_radius_control() _create_instances_control() _create_spacing_control() _create_random_rotation_control() _create_align_surface_control() _create_maximum_slope_control() _create_height_offset_controls() _create_random_scale_controls() func _insert_before_status( control: Control ) -> void: var status_index := status_label.get_index() add_child( control ) move_child( control, status_index ) # ========================================================= # اللغة # ========================================================= func _create_language_selector() -> void: language_button = Button.new() language_button.name = "LanguageButton" language_button.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) language_button.pressed.connect( _on_language_button_pressed ) controls_container.add_child( language_button ) var separator := HSeparator.new() separator.name = "LanguageSeparator" controls_container.add_child( separator ) func _on_language_button_pressed() -> void: if current_language == LANGUAGE_ENGLISH: current_language = LANGUAGE_ARABIC else: current_language = LANGUAGE_ENGLISH _apply_language() _save_configuration() if current_language == LANGUAGE_ARABIC: _set_status( "Interface language changed to Arabic." ) else: _set_status( "Interface language changed to English." ) func _apply_language() -> void: var using_arabic := ( current_language == LANGUAGE_ARABIC ) if using_arabic: layout_direction = ( Control.LAYOUT_DIRECTION_RTL ) status_label.horizontal_alignment = ( HORIZONTAL_ALIGNMENT_RIGHT ) else: layout_direction = ( Control.LAYOUT_DIRECTION_LTR ) status_label.horizontal_alignment = ( HORIZONTAL_ALIGNMENT_LEFT ) if is_instance_valid(title_label): title_label.text = _tr( "title" ) if is_instance_valid(language_button): if using_arabic: language_button.text = "English" language_button.tooltip_text = ( "Switch the World Brush interface to English." ) else: language_button.text = "العربية" language_button.tooltip_text = ( "تغيير واجهة فرشاة العالم إلى العربية." ) if is_instance_valid(palette_title_label): palette_title_label.text = _tr( "scene_palette" ) if is_instance_valid(previous_scene_button): previous_scene_button.text = _tr( "previous" ) previous_scene_button.tooltip_text = _tr( "previous_tooltip" ) if is_instance_valid(next_scene_button): next_scene_button.text = _tr( "next" ) next_scene_button.tooltip_text = _tr( "next_tooltip" ) if is_instance_valid(add_scene_button): add_scene_button.text = _tr( "add_scene" ) if is_instance_valid(lod_toggle): lod_toggle.text = _tr("add_distant_scene") if is_instance_valid(lod1_scene_label): lod1_scene_label.text = _tr("medium_scene") if is_instance_valid(lod2_scene_label): lod2_scene_label.text = _tr("distant_scene") if is_instance_valid(lod1_distance_label): lod1_distance_label.text = _tr("medium_starts") if is_instance_valid(lod2_distance_label): lod2_distance_label.text = _tr("distant_starts") if is_instance_valid(lod_cull_distance_label): lod_cull_distance_label.text = _tr("hide_after") if is_instance_valid(lod_help_label): lod_help_label.text = _tr("lod_help") if is_instance_valid(auto_lod_toggle): auto_lod_toggle.text = _tr("auto_lod") if is_instance_valid(auto_lod_bias_label): auto_lod_bias_label.text = _tr("auto_lod_bias") if is_instance_valid(auto_lod_cull_label): auto_lod_cull_label.text = _tr("auto_lod_hide_after") if is_instance_valid(auto_lod_reimport_button): auto_lod_reimport_button.text = _tr("auto_lod_reimport") if is_instance_valid(auto_lod_help_label): auto_lod_help_label.text = _tr("auto_lod_help") if is_instance_valid(brush_toggle): brush_toggle.text = _tr( "enable_brush" ) if is_instance_valid(brush_radius_label): brush_radius_label.text = _tr( "brush_radius" ) if is_instance_valid(instances_label): instances_label.text = _tr( "instances_per_stamp" ) if is_instance_valid(spacing_label): spacing_label.text = _tr( "minimum_spacing" ) if is_instance_valid(random_rotation_toggle): random_rotation_toggle.text = _tr( "random_y_rotation" ) if is_instance_valid(align_surface_toggle): align_surface_toggle.text = _tr( "align_to_surface" ) if is_instance_valid(maximum_slope_label): maximum_slope_label.text = _tr( "maximum_slope" ) if is_instance_valid(height_offset_label): height_offset_label.text = _tr( "height_offset" ) if is_instance_valid(height_offset_mode_label): height_offset_mode_label.text = _tr( "offset_direction" ) if is_instance_valid(height_offset_mode_option): height_offset_mode_option.set_item_text( HeightOffsetMode.WORLD_Y, _tr("world_y") ) height_offset_mode_option.set_item_text( HeightOffsetMode.SURFACE_NORMAL, _tr("surface_normal") ) height_offset_mode_option.tooltip_text = _tr( "offset_mode_tooltip" ) height_offset_mode_option.set_item_tooltip( HeightOffsetMode.WORLD_Y, _tr("world_y_tooltip") ) height_offset_mode_option.set_item_tooltip( HeightOffsetMode.SURFACE_NORMAL, _tr("surface_normal_tooltip") ) if is_instance_valid(height_offset_help_label): height_offset_help_label.text = _tr( "offset_help" ) if is_instance_valid(align_surface_toggle): align_surface_toggle.tooltip_text = _tr( "align_surface_tooltip" ) if is_instance_valid(random_scale_toggle): random_scale_toggle.text = _tr( "random_scale" ) if is_instance_valid(minimum_scale_label): minimum_scale_label.text = _tr( "minimum_scale" ) if is_instance_valid(maximum_scale_label): maximum_scale_label.text = _tr( "maximum_scale" ) _update_scene_interface() _refresh_status_language() func _tr( key: String ) -> String: if current_language == LANGUAGE_ARABIC: match key: "title": return "فرشاة العالم" "scene_palette": return "مكتبة المشاهد" "active_scene_none": return "المشهد النشط: لا يوجد" "previous": return "◀ السابق" "next": return "التالي ▶" "previous_tooltip": return "اختيار المشهد السابق." "next_tooltip": return "اختيار المشهد التالي." "add_scene": return "+ إضافة مشهد" "add_distant_scene": return "إضافة مشهد بعيد" "medium_scene": return "المشهد المتوسط" "distant_scene": return "المشهد البعيد" "medium_starts": return "بداية المتوسط" "distant_starts": return "بداية البعيد" "hide_after": return "الإخفاء بعد" "lod_help": return "يُستخدم المشهد الأصلي عند القرب، ثم يبدّل Godot تلقائيًا إلى المتوسط والبعيد حسب مسافة الكاميرا. القيمة 0 لمسافة الإخفاء تعني عدم الإخفاء." "auto_lod": return "Godot Auto LOD — نموذج واحد" "auto_lod_bias": return "دقة الانتقال" "auto_lod_hide_after": return "الإخفاء بعد" "auto_lod_reimport": return "تطبيق وإعادة استيراد هذا المجسم" "auto_lod_help": return "خاص بالمجسم النشط فقط. يولّد Godot مستويات أخف من النموذج الواحد عند الاستيراد. دقة 0 تفرض أخف مستوى للاختبار، والقيمة الأكبر تحفظ التفاصيل لمسافة أطول. الإخفاء 0 يعني عدم الإخفاء." "use": return "استخدام" "active": return "نشط" "select_scene_tooltip": return "جعل هذا المشهد هو المستخدم في الرسم." "remove_scene_tooltip": return "حذف المشهد من المكتبة." "enable_brush": return "تفعيل الفرشاة" "brush_radius": return "حجم الفرشاة" "instances_per_stamp": return "عدد العناصر في الضربة" "minimum_spacing": return "أقل مسافة" "random_y_rotation": return "دوران عشوائي حول Y" "align_to_surface": return "تدوير المجسم مع السطح" "align_surface_tooltip": return "يدير المجسم ليتبع ميل السطح. هذا الخيار مستقل عن اتجاه إزاحة الموضع." "maximum_slope": return "أقصى ميل مسموح" "height_offset": return "إزاحة الارتفاع" "offset_direction": return "اتجاه تحريك الموضع" "world_y": return "على محور Y" "surface_normal": return "بعيدًا عن السطح" "offset_help": return "اتجاه الإزاحة يحرك موضع المجسم فقط. لتغيير ميلانه فعّل خيار تدوير المجسم مع السطح." "offset_mode_tooltip": return "يحدد اتجاه تحريك موضع المجسم، ولا يغير دورانه." "world_y_tooltip": return "يحرك المجسم للأعلى أو الأسفل على محور Y العالمي." "surface_normal_tooltip": return "يحرك المجسم بعيدًا عن السطح أو داخله باتجاه Normal السطح." "random_scale": return "حجم عشوائي" "minimum_scale": return "أقل حجم" "maximum_scale": return "أكبر حجم" "unsaved_scene": return "مشهد غير محفوظ" _: return key match key: "title": return "World Brush" "scene_palette": return "Scene Palette" "active_scene_none": return "Active Scene: None" "previous": return "◀ Previous" "next": return "Next ▶" "previous_tooltip": return "Select the previous scene." "next_tooltip": return "Select the next scene." "add_scene": return "+ Add Scene" "add_distant_scene": return "Add Distant Scene" "medium_scene": return "Medium Scene" "distant_scene": return "Distant Scene" "medium_starts": return "Medium Starts" "distant_starts": return "Distant Starts" "hide_after": return "Hide After" "lod_help": return "The original scene is used nearby. Godot switches to the medium and distant scenes using camera distance. A hide distance of 0 disables distance culling." "auto_lod": return "Godot Auto LOD — Single Model" "auto_lod_bias": return "Transition Detail" "auto_lod_hide_after": return "Hide After" "auto_lod_reimport": return "Apply & Reimport This Model" "auto_lod_help": return "Applies only to the active model. Detail 0 forces the lightest generated LOD for testing; higher values preserve detail farther away. Hide distance 0 disables culling." "use": return "Use" "active": return "Active" "select_scene_tooltip": return "Make this the active painting scene." "remove_scene_tooltip": return "Remove Scene" "enable_brush": return "Enable Brush" "brush_radius": return "Brush Radius" "instances_per_stamp": return "Instances Per Stamp" "minimum_spacing": return "Minimum Spacing" "random_y_rotation": return "Random Y Rotation" "align_to_surface": return "Rotate With Surface" "align_surface_tooltip": return "Rotates the instance to follow the surface slope. This is independent from the position offset direction." "maximum_slope": return "Maximum Slope" "height_offset": return "Height Offset" "offset_direction": return "Position Offset Direction" "world_y": return "Along World Y" "surface_normal": return "Away From Surface" "offset_help": return "The offset direction moves the instance position only. Enable Rotate With Surface to change its tilt." "offset_mode_tooltip": return "Controls the direction used to move the instance position. It does not change rotation." "world_y_tooltip": return "Moves the instance up or down along the global Y axis." "surface_normal_tooltip": return "Moves the instance away from or into the surface along its normal." "random_scale": return "Random Scale" "minimum_scale": return "Minimum Scale" "maximum_scale": return "Maximum Scale" "unsaved_scene": return "Unsaved PackedScene" _: return key # ========================================================= # رسائل الحالة # ========================================================= func _set_status( source_message: String ) -> void: last_status_source = source_message var translated_message := ( _translate_status_message( source_message ) ) is_applying_status_translation = true status_label.text = translated_message is_applying_status_translation = false last_status_display = translated_message func _refresh_status_language() -> void: if last_status_source.is_empty(): last_status_source = status_label.text _set_status( last_status_source ) func _watch_external_status_message() -> void: if not is_instance_valid( status_label ): return if is_applying_status_translation: return var current_text := status_label.text if current_text == last_status_display: return last_status_source = current_text var translated_text := ( _translate_status_message( last_status_source ) ) is_applying_status_translation = true status_label.text = translated_text is_applying_status_translation = false last_status_display = translated_text func _translate_status_message( message: String ) -> String: if current_language != LANGUAGE_ARABIC: return message var exact_translations := { "No Scenes Selected": "لم يتم اختيار أي مشهد", "New Scene Slot Added": "تمت إضافة خانة مشهد جديدة", "Choose A Scene In This Slot": "اختر مشهدًا في هذه الخانة", "At Least One Scene Slot Must Remain": "يجب أن تبقى خانة مشهد واحدة على الأقل", "Select An Active Scene First": "اختر مشهدًا نشطًا أولًا", "Select A Saved Scene First": "اختر مشهدًا محفوظًا أولًا", "Auto LOD: Active scene is missing or unsaved": "LOD التلقائي: المشهد النشط مفقود أو غير محفوظ", "Auto LOD: No supported imported model found in active scene": "LOD التلقائي: لم يُعثر على نموذج مستورد مدعوم داخل المشهد النشط", "Auto LOD: Import settings could not be updated": "LOD التلقائي: تعذر تحديث إعدادات الاستيراد", "Auto LOD: Reimporting active model only...": "LOD التلقائي: جارٍ إعادة استيراد المجسم النشط فقط...", "Auto LOD enabled for active model": "تم تفعيل LOD التلقائي للمجسم النشط", "Auto LOD disabled for active model": "تم تعطيل LOD التلقائي للمجسم النشط", "Brush Disabled": "الفرشاة متوقفة", "No Valid Surface": "لا يوجد سطح صالح", "Selected Scene Cannot Be Instantiated": "لا يمكن إنشاء نسخة من المشهد المختار", "Scene Root Must Be Node3D": "يجب أن يكون جذر المشهد من نوع Node3D", "3D World Not Available": "العالم ثلاثي الأبعاد غير متاح", "Edited Scene Changed During Stroke": "تم تغيير المشهد أثناء استخدام الفرشاة", "Failed To Create Container": "تعذر إنشاء حاوية عناصر الفرشاة", "No Valid Space Inside Brush": "لا توجد مساحة صالحة داخل الفرشاة", "Selected Scene Root Must Inherit Node3D": "يجب أن يرث جذر المشهد المختار من Node3D", "No World Brush Instances": "لا توجد عناصر منشورة بواسطة الفرشاة", "Open A 3D Scene First": "افتح مشهدًا ثلاثي الأبعاد أولًا", "No Surface Under Cursor": "لا يوجد سطح أسفل المؤشر", "Invalid Surface Position": "موضع السطح غير صالح", "Erase Brush — No Instances Here": "فرشاة الحذف — لا توجد عناصر هنا", "Interface language changed to Arabic.": "تم تغيير لغة واجهة الفرشاة إلى العربية.", "Interface language changed to English.": "تم تغيير لغة واجهة الفرشاة إلى الإنجليزية.", } if exact_translations.has( message ): return str( exact_translations[message] ) var prefix_translations := { "Active: ": "المشهد النشط: ", "Brush Enabled — ": "الفرشاة مفعّلة — ", "Palette Restored: ": "تمت استعادة المكتبة: ", "Painted In Stroke: ": "العناصر المرسومة في السحبة: ", "Erased In Stroke: ": "العناصر المحذوفة في السحبة: ", "Paint Stroke Saved: ": "تم حفظ سحبة الرسم: ", "Erase Stroke Saved: ": "تم حفظ سحبة الحذف: ", } for english_prefix in prefix_translations: if not message.begins_with( english_prefix ): continue return ( str( prefix_translations[ english_prefix ] ) + message.substr( english_prefix.length() ) ) if message.begins_with( "Physics | " ): return ( "سطح فيزيائي | " + message.substr( "Physics | ".length() ) ) if message.begins_with( "Surface | " ): return ( "سطح | " + message.substr( "Surface | ".length() ) ) return message # ========================================================= # مكتبة المشاهد # ========================================================= func _create_scene_palette() -> void: palette_title_label = Label.new() palette_title_label.name = "ScenePaletteTitle" palette_title_label.text = "Scene Palette" controls_container.add_child( palette_title_label ) active_scene_label = Label.new() active_scene_label.name = "ActiveSceneLabel" active_scene_label.text = "Active Scene: None" controls_container.add_child( active_scene_label ) _create_scene_navigation() var separator := HSeparator.new() separator.name = "PaletteSeparator" controls_container.add_child( separator ) scenes_list = VBoxContainer.new() scenes_list.name = "ScenesList" scenes_list.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) controls_container.add_child( scenes_list ) add_scene_button = Button.new() add_scene_button.name = "AddSceneButton" add_scene_button.text = "+ Add Scene" add_scene_button.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) add_scene_button.pressed.connect( _on_add_scene_pressed ) controls_container.add_child( add_scene_button ) func _create_scene_navigation() -> void: var navigation_row := HBoxContainer.new() navigation_row.name = "SceneNavigation" navigation_row.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) previous_scene_button = Button.new() previous_scene_button.name = ( "PreviousSceneButton" ) previous_scene_button.text = "◀ Previous" previous_scene_button.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) previous_scene_button.pressed.connect( _on_previous_scene_pressed ) next_scene_button = Button.new() next_scene_button.name = "NextSceneButton" next_scene_button.text = "Next ▶" next_scene_button.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) next_scene_button.pressed.connect( _on_next_scene_pressed ) navigation_row.add_child( previous_scene_button ) navigation_row.add_child( next_scene_button ) controls_container.add_child( navigation_row ) func _create_auto_lod_controls() -> void: var separator := HSeparator.new() separator.name = "AutoLODSeparator" controls_container.add_child(separator) auto_lod_toggle = CheckButton.new() auto_lod_toggle.name = "AutoLODToggle" auto_lod_toggle.text = "Godot Auto LOD — Single Model" auto_lod_toggle.button_pressed = auto_lod_enabled auto_lod_toggle.tooltip_text = "Independent setting for the active palette model only." auto_lod_toggle.toggled.connect(_on_auto_lod_toggled) controls_container.add_child(auto_lod_toggle) auto_lod_controls_container = VBoxContainer.new() auto_lod_controls_container.name = "AutoLODControls" auto_lod_controls_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL controls_container.add_child(auto_lod_controls_container) var bias_row := HBoxContainer.new() auto_lod_bias_label = Label.new() auto_lod_bias_label.text = "Transition Detail" auto_lod_bias_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL auto_lod_bias_spin_box = SpinBox.new() auto_lod_bias_spin_box.name = "AutoLODBiasSpinBox" auto_lod_bias_spin_box.min_value = 0.0 auto_lod_bias_spin_box.max_value = 4.0 auto_lod_bias_spin_box.step = 0.05 auto_lod_bias_spin_box.value = auto_lod_bias auto_lod_bias_spin_box.suffix = " ×" auto_lod_bias_spin_box.custom_minimum_size.x = 105.0 auto_lod_bias_spin_box.value_changed.connect(_on_auto_lod_bias_changed) bias_row.add_child(auto_lod_bias_label) bias_row.add_child(auto_lod_bias_spin_box) auto_lod_controls_container.add_child(bias_row) var cull_row := HBoxContainer.new() auto_lod_cull_label = Label.new() auto_lod_cull_label.text = "Hide After" auto_lod_cull_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL auto_lod_cull_spin_box = _create_lod_distance_spin_box( "AutoLODCullDistanceSpinBox", auto_lod_cull_distance ) auto_lod_cull_spin_box.min_value = 0.0 auto_lod_cull_spin_box.value_changed.connect(_on_auto_lod_cull_changed) cull_row.add_child(auto_lod_cull_label) cull_row.add_child(auto_lod_cull_spin_box) auto_lod_controls_container.add_child(cull_row) auto_lod_reimport_button = Button.new() auto_lod_reimport_button.name = "AutoLODReimportButton" auto_lod_reimport_button.text = "Apply & Reimport This Model" auto_lod_reimport_button.tooltip_text = "Changes Generate LODs only for the active model source, then reimports it." auto_lod_reimport_button.pressed.connect(_on_auto_lod_reimport_pressed) auto_lod_controls_container.add_child(auto_lod_reimport_button) auto_lod_help_label = Label.new() auto_lod_help_label.name = "AutoLODHelpLabel" auto_lod_help_label.text = "Godot generates lighter mesh levels from the active single model during import." auto_lod_help_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART auto_lod_help_label.modulate = Color(0.82, 0.90, 1.0, 0.78) auto_lod_controls_container.add_child(auto_lod_help_label) _update_auto_lod_controls() func _create_lod_controls() -> void: var separator := HSeparator.new() separator.name = "DistanceLODSeparator" controls_container.add_child(separator) lod_toggle = CheckButton.new() lod_toggle.name = "DistanceLODToggle" lod_toggle.text = "Add Distant Scene" lod_toggle.button_pressed = lod_enabled lod_toggle.toggled.connect(_on_lod_toggled) controls_container.add_child(lod_toggle) lod_controls_container = VBoxContainer.new() lod_controls_container.name = "DistanceLODControls" lod_controls_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL controls_container.add_child(lod_controls_container) var lod1_row := HBoxContainer.new() lod1_row.name = "LOD1SceneContainer" lod1_scene_label = Label.new() lod1_scene_label.text = "Medium Scene" lod1_scene_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod1_picker = EditorResourcePicker.new() lod1_picker.name = "LOD1ScenePicker" lod1_picker.base_type = "PackedScene" lod1_picker.editable = true lod1_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod1_picker.resource_changed.connect(_on_lod1_scene_changed) lod1_row.add_child(lod1_scene_label) lod1_row.add_child(lod1_picker) lod_controls_container.add_child(lod1_row) var lod2_row := HBoxContainer.new() lod2_row.name = "LOD2SceneContainer" lod2_scene_label = Label.new() lod2_scene_label.text = "Distant Scene" lod2_scene_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod2_picker = EditorResourcePicker.new() lod2_picker.name = "LOD2ScenePicker" lod2_picker.base_type = "PackedScene" lod2_picker.editable = true lod2_picker.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod2_picker.resource_changed.connect(_on_lod2_scene_changed) lod2_row.add_child(lod2_scene_label) lod2_row.add_child(lod2_picker) lod_controls_container.add_child(lod2_row) var lod1_distance_row := HBoxContainer.new() lod1_distance_row.name = "LOD1DistanceContainer" lod1_distance_label = Label.new() lod1_distance_label.text = "Medium Starts" lod1_distance_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod1_distance_spin_box = _create_lod_distance_spin_box( "LOD1DistanceSpinBox", lod1_distance ) lod1_distance_spin_box.value_changed.connect(_on_lod1_distance_changed) lod1_distance_row.add_child(lod1_distance_label) lod1_distance_row.add_child(lod1_distance_spin_box) lod_controls_container.add_child(lod1_distance_row) var lod2_distance_row := HBoxContainer.new() lod2_distance_row.name = "LOD2DistanceContainer" lod2_distance_label = Label.new() lod2_distance_label.text = "Distant Starts" lod2_distance_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod2_distance_spin_box = _create_lod_distance_spin_box( "LOD2DistanceSpinBox", lod2_distance ) lod2_distance_spin_box.value_changed.connect(_on_lod2_distance_changed) lod2_distance_row.add_child(lod2_distance_label) lod2_distance_row.add_child(lod2_distance_spin_box) lod_controls_container.add_child(lod2_distance_row) var cull_row := HBoxContainer.new() cull_row.name = "LODCullDistanceContainer" lod_cull_distance_label = Label.new() lod_cull_distance_label.text = "Hide After" lod_cull_distance_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL lod_cull_distance_spin_box = _create_lod_distance_spin_box( "LODCullDistanceSpinBox", lod_cull_distance ) lod_cull_distance_spin_box.min_value = 0.0 lod_cull_distance_spin_box.tooltip_text = "0 keeps the distant scene visible without a maximum distance." lod_cull_distance_spin_box.value_changed.connect(_on_lod_cull_distance_changed) cull_row.add_child(lod_cull_distance_label) cull_row.add_child(lod_cull_distance_spin_box) lod_controls_container.add_child(cull_row) lod_help_label = Label.new() lod_help_label.name = "DistanceLODHelpLabel" lod_help_label.text = "The original scene is used nearby. Godot switches to the medium and distant scenes using camera distance." lod_help_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lod_help_label.modulate = Color(1.0, 1.0, 1.0, 0.72) lod_controls_container.add_child(lod_help_label) _update_lod_controls() func _create_lod_distance_spin_box( control_name: String, value: float ) -> SpinBox: var spin_box := SpinBox.new() spin_box.name = control_name spin_box.min_value = 1.0 spin_box.max_value = 100000.0 spin_box.step = 1.0 spin_box.value = value spin_box.allow_greater = true spin_box.suffix = " m" spin_box.custom_minimum_size.x = 105.0 return spin_box func _on_lod_toggled(enabled: bool) -> void: lod_enabled = enabled _update_lod_controls() _emit_lod_settings_changed() _save_configuration() func _on_auto_lod_toggled(enabled: bool) -> void: auto_lod_enabled = enabled _update_auto_lod_controls() _emit_auto_lod_settings_changed() _save_configuration() _request_auto_lod_reimport() func _on_auto_lod_bias_changed(value: float) -> void: auto_lod_bias = clampf(value, 0.0, 4.0) auto_lod_bias_spin_box.set_value_no_signal(auto_lod_bias) _emit_auto_lod_settings_changed() _save_configuration() func _on_auto_lod_cull_changed(value: float) -> void: auto_lod_cull_distance = maxf(value, 0.0) auto_lod_cull_spin_box.set_value_no_signal(auto_lod_cull_distance) _emit_auto_lod_settings_changed() _save_configuration() func _on_auto_lod_reimport_pressed() -> void: _request_auto_lod_reimport() func _request_auto_lod_reimport() -> void: if selected_scene == null or selected_scene.resource_path.is_empty(): _set_status("Select A Saved Scene First") return auto_lod_import_requested.emit( selected_scene.resource_path, auto_lod_enabled ) func _emit_auto_lod_settings_changed() -> void: auto_lod_settings_changed.emit( auto_lod_enabled, auto_lod_bias, auto_lod_cull_distance ) func _update_auto_lod_controls() -> void: var has_active_scene := selected_scene != null if is_instance_valid(auto_lod_toggle): auto_lod_toggle.visible = has_active_scene auto_lod_toggle.disabled = not has_active_scene if is_instance_valid(auto_lod_controls_container): auto_lod_controls_container.visible = has_active_scene and auto_lod_enabled var details_enabled := has_active_scene and auto_lod_enabled if is_instance_valid(auto_lod_bias_spin_box): auto_lod_bias_spin_box.editable = details_enabled if is_instance_valid(auto_lod_cull_spin_box): auto_lod_cull_spin_box.editable = details_enabled if is_instance_valid(auto_lod_reimport_button): auto_lod_reimport_button.disabled = not details_enabled func _on_lod1_scene_changed(resource: Resource) -> void: lod1_scene = resource as PackedScene _emit_lod_settings_changed() _save_configuration() func _on_lod2_scene_changed(resource: Resource) -> void: lod2_scene = resource as PackedScene _emit_lod_settings_changed() _save_configuration() func _on_lod1_distance_changed(value: float) -> void: lod1_distance = maxf(value, 1.0) if lod2_distance <= lod1_distance: lod2_distance = lod1_distance + 1.0 lod2_distance_spin_box.set_value_no_signal(lod2_distance) _validate_lod_cull_distance() _emit_lod_settings_changed() _save_configuration() func _on_lod2_distance_changed(value: float) -> void: lod2_distance = maxf(value, lod1_distance + 1.0) lod2_distance_spin_box.set_value_no_signal(lod2_distance) _validate_lod_cull_distance() _emit_lod_settings_changed() _save_configuration() func _on_lod_cull_distance_changed(value: float) -> void: lod_cull_distance = maxf(value, 0.0) _validate_lod_cull_distance() _emit_lod_settings_changed() _save_configuration() func _validate_lod_cull_distance() -> void: lod_cull_distance = maxf(lod_cull_distance, 0.0) if is_instance_valid(lod_cull_distance_spin_box): lod_cull_distance_spin_box.set_value_no_signal(lod_cull_distance) func _emit_lod_settings_changed() -> void: lod_settings_changed.emit( lod_enabled, lod1_scene, lod2_scene, lod1_distance, lod2_distance, lod_cull_distance ) func _update_lod_controls() -> void: var has_active_scene := selected_scene != null if is_instance_valid(lod_toggle): lod_toggle.visible = has_active_scene lod_toggle.disabled = not has_active_scene if is_instance_valid(lod_controls_container): lod_controls_container.visible = has_active_scene and lod_enabled var details_enabled := has_active_scene and lod_enabled if is_instance_valid(lod1_picker): lod1_picker.editable = details_enabled if is_instance_valid(lod2_picker): lod2_picker.editable = details_enabled if is_instance_valid(lod1_distance_spin_box): lod1_distance_spin_box.editable = details_enabled if is_instance_valid(lod2_distance_spin_box): lod2_distance_spin_box.editable = details_enabled if is_instance_valid(lod_cull_distance_spin_box): lod_cull_distance_spin_box.editable = details_enabled func _add_scene_row( initial_scene: PackedScene = null, update_after_creation: bool = true ) -> EditorResourcePicker: var row := HBoxContainer.new() row.name = "SceneRow_%d" % ( scene_rows.size() + 1 ) row.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) var picker := EditorResourcePicker.new() picker.name = "ScenePicker" picker.base_type = "PackedScene" picker.editable = true picker.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) if initial_scene != null: picker.edited_resource = initial_scene var select_button := Button.new() select_button.name = "SelectSceneButton" select_button.text = "Use" select_button.custom_minimum_size = Vector2( 62.0, 0.0 ) var remove_button := Button.new() remove_button.name = "RemoveSceneButton" remove_button.text = "×" remove_button.custom_minimum_size = Vector2( 38.0, 0.0 ) row.add_child( picker ) row.add_child( select_button ) row.add_child( remove_button ) scenes_list.add_child( row ) scene_rows.append( { "row": row, "picker": picker, "select_button": select_button, "remove_button": remove_button, } ) picker.resource_changed.connect( _on_scene_resource_changed.bind( picker ) ) select_button.pressed.connect( _on_select_scene_pressed.bind( picker ) ) remove_button.pressed.connect( _on_remove_scene_pressed.bind( picker ) ) if update_after_creation: _update_scene_interface() return picker func _on_add_scene_pressed() -> void: _add_scene_row() _set_status( "New Scene Slot Added" ) func _on_scene_resource_changed( resource: Resource, picker: EditorResourcePicker ) -> void: var packed_scene := ( resource as PackedScene ) var picker_was_active := ( picker == active_picker ) if picker_was_active: _store_current_scene_settings() if ( packed_scene != null and not is_instance_valid( active_picker ) ): active_picker = picker selected_scene = packed_scene elif picker_was_active: if packed_scene == null: active_picker = null selected_scene = null _select_first_available_scene() else: selected_scene = packed_scene if ( picker == active_picker and selected_scene != null ): _load_active_scene_settings( false ) _update_scene_interface() _emit_palette_changed() _emit_active_scene_changed() _emit_all_brush_settings_changed() _save_configuration() func _on_select_scene_pressed( picker: EditorResourcePicker ) -> void: if not is_instance_valid( picker ): return var packed_scene := ( picker.edited_resource as PackedScene ) if packed_scene == null: _set_status( "Choose A Scene In This Slot" ) return _set_active_picker( picker ) func _on_remove_scene_pressed( picker: EditorResourcePicker ) -> void: if scene_rows.size() <= 1: _set_status( "At Least One Scene Slot Must Remain" ) return var row_to_remove: Control = null var index_to_remove: int = -1 var removed_active_scene := ( picker == active_picker ) if removed_active_scene: _store_current_scene_settings() for index in range( scene_rows.size() ): var entry_picker := ( scene_rows[index].get( "picker" ) as EditorResourcePicker ) if entry_picker != picker: continue row_to_remove = ( scene_rows[index].get( "row" ) as Control ) index_to_remove = index break if index_to_remove < 0: return scene_rows.remove_at( index_to_remove ) if removed_active_scene: active_picker = null selected_scene = null if is_instance_valid( row_to_remove ): row_to_remove.queue_free() if removed_active_scene: _select_available_scene_near_index( index_to_remove ) _load_active_scene_settings( false ) _update_scene_interface() _emit_palette_changed() _emit_active_scene_changed() if removed_active_scene: _emit_all_brush_settings_changed() _save_configuration() func _on_previous_scene_pressed() -> void: _change_active_scene( -1 ) func _on_next_scene_pressed() -> void: _change_active_scene( 1 ) func _change_active_scene( direction: int ) -> void: var valid_pickers := ( _get_valid_scene_pickers() ) if valid_pickers.is_empty(): active_picker = null selected_scene = null _update_scene_interface() _emit_active_scene_changed() _save_configuration() return var current_index := valid_pickers.find( active_picker ) if current_index < 0: current_index = 0 else: current_index = posmod( current_index + direction, valid_pickers.size() ) _set_active_picker( valid_pickers[current_index] ) func _set_active_picker( picker: EditorResourcePicker ) -> void: if not is_instance_valid( picker ): return var packed_scene := ( picker.edited_resource as PackedScene ) if packed_scene == null: return _store_current_scene_settings() active_picker = picker selected_scene = packed_scene _load_active_scene_settings( false ) _update_scene_interface() _emit_active_scene_changed() _emit_all_brush_settings_changed() _save_configuration() _set_status( "Active: %s" % _get_scene_display_name( selected_scene ) ) func _select_first_available_scene() -> void: var valid_pickers := ( _get_valid_scene_pickers() ) if valid_pickers.is_empty(): active_picker = null selected_scene = null return active_picker = valid_pickers[0] selected_scene = ( active_picker.edited_resource as PackedScene ) func _select_available_scene_near_index( preferred_index: int ) -> void: if scene_rows.is_empty(): active_picker = null selected_scene = null return var clamped_index := clampi( preferred_index, 0, scene_rows.size() - 1 ) for index in range( clamped_index, scene_rows.size() ): var picker := _get_picker_from_row( scene_rows[index] ) if not _picker_has_scene( picker ): continue active_picker = picker selected_scene = ( picker.edited_resource as PackedScene ) return for index in range( clamped_index - 1, -1, -1 ): var picker := _get_picker_from_row( scene_rows[index] ) if not _picker_has_scene( picker ): continue active_picker = picker selected_scene = ( picker.edited_resource as PackedScene ) return active_picker = null selected_scene = null # ========================================================= # حفظ وتحميل الإعدادات # ========================================================= func _save_configuration() -> void: if is_loading_configuration: return _store_current_scene_settings() var scene_paths := PackedStringArray() for picker in _get_valid_scene_pickers(): var packed_scene := ( picker.edited_resource as PackedScene ) if packed_scene == null: continue if packed_scene.resource_path.is_empty(): continue scene_paths.append( packed_scene.resource_path ) var active_scene_path := "" if is_instance_valid( active_picker ): var active_scene := ( active_picker.edited_resource as PackedScene ) if active_scene != null: active_scene_path = ( active_scene.resource_path ) var config := ConfigFile.new() config.set_value( PALETTE_SECTION, CONFIG_SCENE_PATHS, scene_paths ) config.set_value( PALETTE_SECTION, CONFIG_ACTIVE_SCENE_PATH, active_scene_path ) config.set_value( SETTINGS_SECTION, CONFIG_BRUSH_RADIUS, brush_radius ) config.set_value( SETTINGS_SECTION, CONFIG_INSTANCES_PER_STAMP, instances_per_click ) config.set_value( SETTINGS_SECTION, CONFIG_MINIMUM_SPACING, minimum_spacing ) config.set_value( SETTINGS_SECTION, CONFIG_RANDOM_Y_ROTATION, random_y_rotation ) config.set_value( SETTINGS_SECTION, CONFIG_ALIGN_TO_SURFACE, align_to_surface ) config.set_value( SETTINGS_SECTION, CONFIG_MAXIMUM_SLOPE, maximum_slope_degrees ) config.set_value( SETTINGS_SECTION, CONFIG_HEIGHT_OFFSET, height_offset ) config.set_value( SETTINGS_SECTION, CONFIG_HEIGHT_OFFSET_MODE, height_offset_mode ) config.set_value( SETTINGS_SECTION, CONFIG_RANDOM_SCALE_ENABLED, random_scale_enabled ) config.set_value( SETTINGS_SECTION, CONFIG_MINIMUM_SCALE, minimum_scale ) config.set_value( SETTINGS_SECTION, CONFIG_MAXIMUM_SCALE, maximum_scale ) config.set_value( SETTINGS_SECTION, CONFIG_LOD_ENABLED, lod_enabled ) config.set_value( SETTINGS_SECTION, CONFIG_LOD1_SCENE_PATH, _get_packed_scene_path(lod1_scene) ) config.set_value( SETTINGS_SECTION, CONFIG_LOD2_SCENE_PATH, _get_packed_scene_path(lod2_scene) ) config.set_value( SETTINGS_SECTION, CONFIG_LOD1_DISTANCE, lod1_distance ) config.set_value( SETTINGS_SECTION, CONFIG_LOD2_DISTANCE, lod2_distance ) config.set_value( SETTINGS_SECTION, CONFIG_LOD_CULL_DISTANCE, lod_cull_distance ) config.set_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_ENABLED, auto_lod_enabled ) config.set_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_BIAS, auto_lod_bias ) config.set_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_CULL_DISTANCE, auto_lod_cull_distance ) config.set_value( INTERFACE_SECTION, CONFIG_INTERFACE_LANGUAGE, current_language ) config.set_value( SCENE_SETTINGS_SECTION, CONFIG_SCENE_SETTINGS, scene_brush_settings ) var save_error := config.save( PALETTE_CONFIG_PATH ) if save_error != OK: push_warning( "World Brush: Failed to save configuration. Error: %s" % error_string(save_error) ) func _load_saved_configuration() -> void: is_loading_configuration = true var config := ConfigFile.new() var load_error := config.load( PALETTE_CONFIG_PATH ) if load_error != OK: _add_scene_row( null, false ) is_loading_configuration = false return _load_interface_settings( config ) _load_brush_settings( config ) _load_scene_brush_settings( config ) var stored_paths: Variant = config.get_value( PALETTE_SECTION, CONFIG_SCENE_PATHS, PackedStringArray() ) var active_scene_path := str( config.get_value( PALETTE_SECTION, CONFIG_ACTIVE_SCENE_PATH, "" ) ) var scene_paths: Array[String] = [] if typeof( stored_paths ) == TYPE_PACKED_STRING_ARRAY: for path_value in stored_paths: scene_paths.append( str(path_value) ) elif typeof( stored_paths ) == TYPE_ARRAY: for path_value in stored_paths: scene_paths.append( str(path_value) ) for scene_path in scene_paths: if scene_path.is_empty(): continue if not ResourceLoader.exists( scene_path, "PackedScene" ): push_warning( "World Brush: Saved scene no longer exists -> %s" % scene_path ) continue var loaded_scene := ( ResourceLoader.load( scene_path, "PackedScene" ) as PackedScene ) if loaded_scene == null: continue var picker := _add_scene_row( loaded_scene, false ) if scene_path == active_scene_path: active_picker = picker selected_scene = loaded_scene if scene_rows.is_empty(): _add_scene_row( null, false ) if not is_instance_valid( active_picker ): _select_first_available_scene() _load_active_scene_settings( false ) is_loading_configuration = false func _load_interface_settings( config: ConfigFile ) -> void: var stored_language := str( config.get_value( INTERFACE_SECTION, CONFIG_INTERFACE_LANGUAGE, LANGUAGE_ENGLISH ) ) if ( stored_language != LANGUAGE_ENGLISH and stored_language != LANGUAGE_ARABIC ): stored_language = LANGUAGE_ENGLISH current_language = stored_language func _get_packed_scene_path(scene: PackedScene) -> String: if scene == null: return "" return scene.resource_path func _load_packed_scene(scene_path: String) -> PackedScene: if scene_path.is_empty(): return null if not ResourceLoader.exists(scene_path, "PackedScene"): return null return ResourceLoader.load(scene_path, "PackedScene") as PackedScene func _load_brush_settings( config: ConfigFile ) -> void: brush_radius = maxf( float( config.get_value( SETTINGS_SECTION, CONFIG_BRUSH_RADIUS, brush_radius ) ), 0.5 ) instances_per_click = maxi( int( config.get_value( SETTINGS_SECTION, CONFIG_INSTANCES_PER_STAMP, instances_per_click ) ), 1 ) minimum_spacing = maxf( float( config.get_value( SETTINGS_SECTION, CONFIG_MINIMUM_SPACING, minimum_spacing ) ), 0.0 ) random_y_rotation = bool( config.get_value( SETTINGS_SECTION, CONFIG_RANDOM_Y_ROTATION, random_y_rotation ) ) align_to_surface = bool( config.get_value( SETTINGS_SECTION, CONFIG_ALIGN_TO_SURFACE, align_to_surface ) ) maximum_slope_degrees = clampf( float( config.get_value( SETTINGS_SECTION, CONFIG_MAXIMUM_SLOPE, maximum_slope_degrees ) ), 0.0, 90.0 ) height_offset = float( config.get_value( SETTINGS_SECTION, CONFIG_HEIGHT_OFFSET, height_offset ) ) height_offset_mode = int( config.get_value( SETTINGS_SECTION, CONFIG_HEIGHT_OFFSET_MODE, HeightOffsetMode.SURFACE_NORMAL ) ) height_offset_mode = clampi( height_offset_mode, HeightOffsetMode.WORLD_Y, HeightOffsetMode.SURFACE_NORMAL ) random_scale_enabled = bool( config.get_value( SETTINGS_SECTION, CONFIG_RANDOM_SCALE_ENABLED, random_scale_enabled ) ) minimum_scale = maxf( float( config.get_value( SETTINGS_SECTION, CONFIG_MINIMUM_SCALE, minimum_scale ) ), 0.01 ) maximum_scale = maxf( float( config.get_value( SETTINGS_SECTION, CONFIG_MAXIMUM_SCALE, maximum_scale ) ), 0.01 ) if minimum_scale > maximum_scale: maximum_scale = minimum_scale lod_enabled = bool( config.get_value( SETTINGS_SECTION, CONFIG_LOD_ENABLED, lod_enabled ) ) lod1_scene = _load_packed_scene( str(config.get_value( SETTINGS_SECTION, CONFIG_LOD1_SCENE_PATH, "" )) ) lod2_scene = _load_packed_scene( str(config.get_value( SETTINGS_SECTION, CONFIG_LOD2_SCENE_PATH, "" )) ) lod1_distance = maxf( float(config.get_value( SETTINGS_SECTION, CONFIG_LOD1_DISTANCE, lod1_distance )), 1.0 ) lod2_distance = maxf( float(config.get_value( SETTINGS_SECTION, CONFIG_LOD2_DISTANCE, lod2_distance )), lod1_distance + 1.0 ) lod_cull_distance = maxf( float(config.get_value( SETTINGS_SECTION, CONFIG_LOD_CULL_DISTANCE, lod_cull_distance )), 0.0 ) _validate_lod_cull_distance() auto_lod_enabled = bool( config.get_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_ENABLED, auto_lod_enabled ) ) auto_lod_bias = clampf( float(config.get_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_BIAS, auto_lod_bias )), 0.0, 4.0 ) auto_lod_cull_distance = maxf( float(config.get_value( SETTINGS_SECTION, CONFIG_AUTO_LOD_CULL_DISTANCE, auto_lod_cull_distance )), 0.0 ) _apply_settings_to_controls() func _load_scene_brush_settings( config: ConfigFile ) -> void: scene_brush_settings.clear() var stored_profiles: Variant = config.get_value( SCENE_SETTINGS_SECTION, CONFIG_SCENE_SETTINGS, {} ) if typeof( stored_profiles ) != TYPE_DICTIONARY: return var profiles_dictionary: Dictionary = ( stored_profiles ) for stored_scene_path in profiles_dictionary: var scene_path := str( stored_scene_path ) var stored_profile: Variant = ( profiles_dictionary[ stored_scene_path ] ) if typeof( stored_profile ) != TYPE_DICTIONARY: continue var profile_dictionary: Dictionary = ( stored_profile ) scene_brush_settings[ scene_path ] = profile_dictionary.duplicate( true ) func _get_active_scene_settings_key() -> String: if selected_scene == null: return "" return selected_scene.resource_path func _capture_current_brush_settings() -> Dictionary: return { CONFIG_BRUSH_RADIUS: brush_radius, CONFIG_INSTANCES_PER_STAMP: instances_per_click, CONFIG_MINIMUM_SPACING: minimum_spacing, CONFIG_RANDOM_Y_ROTATION: random_y_rotation, CONFIG_ALIGN_TO_SURFACE: align_to_surface, CONFIG_MAXIMUM_SLOPE: maximum_slope_degrees, CONFIG_HEIGHT_OFFSET: height_offset, CONFIG_HEIGHT_OFFSET_MODE: height_offset_mode, CONFIG_RANDOM_SCALE_ENABLED: random_scale_enabled, CONFIG_MINIMUM_SCALE: minimum_scale, CONFIG_MAXIMUM_SCALE: maximum_scale, CONFIG_LOD_ENABLED: lod_enabled, CONFIG_LOD1_SCENE_PATH: _get_packed_scene_path(lod1_scene), CONFIG_LOD2_SCENE_PATH: _get_packed_scene_path(lod2_scene), CONFIG_LOD1_DISTANCE: lod1_distance, CONFIG_LOD2_DISTANCE: lod2_distance, CONFIG_LOD_CULL_DISTANCE: lod_cull_distance, CONFIG_AUTO_LOD_ENABLED: auto_lod_enabled, CONFIG_AUTO_LOD_BIAS: auto_lod_bias, CONFIG_AUTO_LOD_CULL_DISTANCE: auto_lod_cull_distance, } func _create_default_scene_settings() -> Dictionary: return { CONFIG_BRUSH_RADIUS: 5.0, CONFIG_INSTANCES_PER_STAMP: 5, CONFIG_MINIMUM_SPACING: 1.0, CONFIG_RANDOM_Y_ROTATION: true, CONFIG_ALIGN_TO_SURFACE: false, CONFIG_MAXIMUM_SLOPE: 85.0, CONFIG_HEIGHT_OFFSET: 0.0, CONFIG_HEIGHT_OFFSET_MODE: HeightOffsetMode.SURFACE_NORMAL, CONFIG_RANDOM_SCALE_ENABLED: true, CONFIG_MINIMUM_SCALE: 0.8, CONFIG_MAXIMUM_SCALE: 1.3, CONFIG_LOD_ENABLED: false, CONFIG_LOD1_SCENE_PATH: "", CONFIG_LOD2_SCENE_PATH: "", CONFIG_LOD1_DISTANCE: 25.0, CONFIG_LOD2_DISTANCE: 60.0, CONFIG_LOD_CULL_DISTANCE: 0.0, CONFIG_AUTO_LOD_ENABLED: false, CONFIG_AUTO_LOD_BIAS: 1.0, CONFIG_AUTO_LOD_CULL_DISTANCE: 0.0, } func _store_current_scene_settings() -> void: if ( is_loading_configuration or is_applying_scene_settings ): return var scene_key := ( _get_active_scene_settings_key() ) if scene_key.is_empty(): return scene_brush_settings[ scene_key ] = _capture_current_brush_settings() func _load_active_scene_settings( emit_changes: bool = true ) -> void: var scene_key := ( _get_active_scene_settings_key() ) if scene_key.is_empty(): return if not scene_brush_settings.has( scene_key ): scene_brush_settings[ scene_key ] = _create_default_scene_settings() _apply_brush_settings_profile( scene_brush_settings[scene_key] ) if emit_changes: _emit_all_brush_settings_changed() return var stored_profile: Variant = ( scene_brush_settings[ scene_key ] ) if typeof( stored_profile ) != TYPE_DICTIONARY: scene_brush_settings[ scene_key ] = _create_default_scene_settings() _apply_brush_settings_profile( scene_brush_settings[scene_key] ) if emit_changes: _emit_all_brush_settings_changed() return var profile: Dictionary = stored_profile _apply_brush_settings_profile( profile ) if emit_changes: _emit_all_brush_settings_changed() func _apply_brush_settings_profile( profile: Dictionary ) -> void: is_applying_scene_settings = true brush_radius = maxf( float( profile.get( CONFIG_BRUSH_RADIUS, brush_radius ) ), 0.5 ) instances_per_click = maxi( int( profile.get( CONFIG_INSTANCES_PER_STAMP, instances_per_click ) ), 1 ) minimum_spacing = maxf( float( profile.get( CONFIG_MINIMUM_SPACING, minimum_spacing ) ), 0.0 ) random_y_rotation = bool( profile.get( CONFIG_RANDOM_Y_ROTATION, random_y_rotation ) ) align_to_surface = bool( profile.get( CONFIG_ALIGN_TO_SURFACE, align_to_surface ) ) maximum_slope_degrees = clampf( float( profile.get( CONFIG_MAXIMUM_SLOPE, maximum_slope_degrees ) ), 0.0, 90.0 ) height_offset = float( profile.get( CONFIG_HEIGHT_OFFSET, height_offset ) ) height_offset_mode = clampi( int( profile.get( CONFIG_HEIGHT_OFFSET_MODE, height_offset_mode ) ), HeightOffsetMode.WORLD_Y, HeightOffsetMode.SURFACE_NORMAL ) random_scale_enabled = bool( profile.get( CONFIG_RANDOM_SCALE_ENABLED, random_scale_enabled ) ) minimum_scale = maxf( float( profile.get( CONFIG_MINIMUM_SCALE, minimum_scale ) ), 0.01 ) maximum_scale = maxf( float( profile.get( CONFIG_MAXIMUM_SCALE, maximum_scale ) ), 0.01 ) lod_enabled = bool( profile.get( CONFIG_LOD_ENABLED, lod_enabled ) ) lod1_scene = _load_packed_scene( str(profile.get( CONFIG_LOD1_SCENE_PATH, _get_packed_scene_path(lod1_scene) )) ) lod2_scene = _load_packed_scene( str(profile.get( CONFIG_LOD2_SCENE_PATH, _get_packed_scene_path(lod2_scene) )) ) lod1_distance = maxf( float(profile.get( CONFIG_LOD1_DISTANCE, lod1_distance )), 1.0 ) lod2_distance = maxf( float(profile.get( CONFIG_LOD2_DISTANCE, lod2_distance )), lod1_distance + 1.0 ) lod_cull_distance = maxf( float(profile.get( CONFIG_LOD_CULL_DISTANCE, lod_cull_distance )), 0.0 ) _validate_lod_cull_distance() auto_lod_enabled = bool( profile.get( CONFIG_AUTO_LOD_ENABLED, false ) ) auto_lod_bias = clampf( float(profile.get( CONFIG_AUTO_LOD_BIAS, 1.0 )), 0.0, 4.0 ) auto_lod_cull_distance = maxf( float(profile.get( CONFIG_AUTO_LOD_CULL_DISTANCE, 0.0 )), 0.0 ) if minimum_scale > maximum_scale: maximum_scale = minimum_scale _apply_settings_to_controls() _update_controls() is_applying_scene_settings = false func _emit_all_brush_settings_changed() -> void: brush_radius_changed.emit( brush_radius ) instances_per_click_changed.emit( instances_per_click ) minimum_spacing_changed.emit( minimum_spacing ) random_y_rotation_changed.emit( random_y_rotation ) align_to_surface_changed.emit( align_to_surface ) maximum_slope_changed.emit( maximum_slope_degrees ) height_offset_changed.emit( height_offset ) height_offset_mode_changed.emit( height_offset_mode ) random_scale_enabled_changed.emit( random_scale_enabled ) minimum_scale_changed.emit( minimum_scale ) maximum_scale_changed.emit( maximum_scale ) _emit_lod_settings_changed() _emit_auto_lod_settings_changed() func _apply_settings_to_controls() -> void: if is_instance_valid(radius_spin_box): radius_spin_box.set_value_no_signal( brush_radius ) if is_instance_valid(instances_spin_box): instances_spin_box.set_value_no_signal( float(instances_per_click) ) if is_instance_valid(spacing_spin_box): spacing_spin_box.set_value_no_signal( minimum_spacing ) if is_instance_valid(random_rotation_toggle): random_rotation_toggle.set_pressed_no_signal( random_y_rotation ) if is_instance_valid(align_surface_toggle): align_surface_toggle.set_pressed_no_signal( align_to_surface ) if is_instance_valid(maximum_slope_spin_box): maximum_slope_spin_box.set_value_no_signal( maximum_slope_degrees ) if is_instance_valid(height_offset_spin_box): height_offset_spin_box.set_value_no_signal( height_offset ) if is_instance_valid(height_offset_mode_option): height_offset_mode_option.select( height_offset_mode ) if is_instance_valid(random_scale_toggle): random_scale_toggle.set_pressed_no_signal( random_scale_enabled ) if is_instance_valid(minimum_scale_spin_box): minimum_scale_spin_box.set_value_no_signal( minimum_scale ) if is_instance_valid(maximum_scale_spin_box): maximum_scale_spin_box.set_value_no_signal( maximum_scale ) if is_instance_valid(lod_toggle): lod_toggle.set_pressed_no_signal(lod_enabled) if is_instance_valid(lod1_picker): lod1_picker.edited_resource = lod1_scene if is_instance_valid(lod2_picker): lod2_picker.edited_resource = lod2_scene if is_instance_valid(lod1_distance_spin_box): lod1_distance_spin_box.set_value_no_signal(lod1_distance) if is_instance_valid(lod2_distance_spin_box): lod2_distance_spin_box.set_value_no_signal(lod2_distance) if is_instance_valid(lod_cull_distance_spin_box): lod_cull_distance_spin_box.set_value_no_signal(lod_cull_distance) if is_instance_valid(auto_lod_toggle): auto_lod_toggle.set_pressed_no_signal(auto_lod_enabled) if is_instance_valid(auto_lod_bias_spin_box): auto_lod_bias_spin_box.set_value_no_signal(auto_lod_bias) if is_instance_valid(auto_lod_cull_spin_box): auto_lod_cull_spin_box.set_value_no_signal(auto_lod_cull_distance) _update_lod_controls() _update_auto_lod_controls() func _emit_loaded_state() -> void: _apply_language() _update_scene_interface() _emit_palette_changed() _emit_active_scene_changed() _emit_all_brush_settings_changed() var valid_scene_count := ( _get_valid_scene_pickers().size() ) if valid_scene_count <= 0: _set_status( "No Scenes Selected" ) return _set_status( "Palette Restored: %d Scene(s)" % valid_scene_count ) # ========================================================= # تحديث قائمة المشاهد # ========================================================= func _update_scene_interface() -> void: _validate_active_picker() var valid_pickers := ( _get_valid_scene_pickers() ) var valid_scene_count := ( valid_pickers.size() ) var active_valid_index := valid_pickers.find( active_picker ) for entry in scene_rows: var picker := _get_picker_from_row( entry ) var select_button := ( entry.get( "select_button" ) as Button ) var remove_button := ( entry.get( "remove_button" ) as Button ) var has_scene := _picker_has_scene( picker ) var is_active := ( has_scene and picker == active_picker ) if is_instance_valid( select_button ): select_button.disabled = ( not has_scene or is_active ) select_button.tooltip_text = _tr( "select_scene_tooltip" ) if is_active: select_button.text = _tr( "active" ) else: select_button.text = _tr( "use" ) if is_instance_valid( remove_button ): remove_button.disabled = ( scene_rows.size() <= 1 ) remove_button.tooltip_text = _tr( "remove_scene_tooltip" ) if is_instance_valid( previous_scene_button ): previous_scene_button.disabled = ( valid_scene_count <= 1 ) if is_instance_valid( next_scene_button ): next_scene_button.disabled = ( valid_scene_count <= 1 ) if ( is_instance_valid(active_picker) and active_valid_index >= 0 ): selected_scene = ( active_picker.edited_resource as PackedScene ) if current_language == LANGUAGE_ARABIC: active_scene_label.text = ( "المشهد النشط: %d/%d — %s" % [ active_valid_index + 1, valid_scene_count, _get_scene_display_name( selected_scene ), ] ) else: active_scene_label.text = ( "Active Scene: %d/%d — %s" % [ active_valid_index + 1, valid_scene_count, _get_scene_display_name( selected_scene ), ] ) active_scene_label.tooltip_text = ( selected_scene.resource_path ) else: selected_scene = null active_scene_label.text = _tr( "active_scene_none" ) active_scene_label.tooltip_text = "" _update_controls() func _validate_active_picker() -> void: if ( is_instance_valid(active_picker) and _picker_belongs_to_palette( active_picker ) and _picker_has_scene( active_picker ) ): return active_picker = null selected_scene = null _select_first_available_scene() func _picker_belongs_to_palette( picker: EditorResourcePicker ) -> bool: for entry in scene_rows: if _get_picker_from_row( entry ) == picker: return true return false func _get_valid_scene_pickers() -> Array[EditorResourcePicker]: var result: Array[EditorResourcePicker] = [] for entry in scene_rows: var picker := _get_picker_from_row( entry ) if _picker_has_scene( picker ): result.append( picker ) return result func _get_picker_from_row( entry: Dictionary ) -> EditorResourcePicker: return ( entry.get("picker") as EditorResourcePicker ) func _picker_has_scene( picker: EditorResourcePicker ) -> bool: return ( is_instance_valid(picker) and picker.edited_resource is PackedScene ) func _get_scene_display_name( scene: PackedScene ) -> String: if scene == null: return "None" if scene.resource_path.is_empty(): return _tr( "unsaved_scene" ) return scene.resource_path.get_file() func _emit_active_scene_changed() -> void: _validate_active_picker() if not is_instance_valid( active_picker ): selected_scene = null selected_scene_changed.emit( null ) _disable_brush_if_no_scene() return selected_scene = ( active_picker.edited_resource as PackedScene ) selected_scene_changed.emit( selected_scene ) _disable_brush_if_no_scene() func _emit_palette_changed() -> void: scene_palette_changed.emit( get_scene_palette() ) func get_scene_palette() -> Array: var scenes: Array = [] for picker in _get_valid_scene_pickers(): var packed_scene := ( picker.edited_resource as PackedScene ) if packed_scene != null: scenes.append( packed_scene ) return scenes # ========================================================= # إعدادات الفرشاة # ========================================================= func _create_brush_toggle() -> void: var separator := HSeparator.new() separator.name = "BrushSeparator" controls_container.add_child( separator ) brush_toggle = CheckButton.new() brush_toggle.name = "BrushToggle" brush_toggle.text = "Enable Brush" brush_toggle.button_pressed = false brush_toggle.toggled.connect( _on_brush_toggled ) controls_container.add_child( brush_toggle ) func _on_brush_toggled( enabled: bool ) -> void: if enabled and selected_scene == null: _disable_brush() _set_status( "Select An Active Scene First" ) return brush_enabled = enabled if brush_enabled: _set_status( "Brush Enabled — %s" % _get_scene_display_name( selected_scene ) ) else: _set_status( "Brush Disabled" ) brush_enabled_changed.emit( brush_enabled ) func _disable_brush_if_no_scene() -> void: if selected_scene == null: _disable_brush() func _disable_brush() -> void: brush_enabled = false if is_instance_valid( brush_toggle ): brush_toggle.set_pressed_no_signal( false ) brush_enabled_changed.emit( false ) func _create_radius_control() -> void: var row := _create_setting_row( "RadiusContainer", "Brush Radius" ) brush_radius_label = ( row.get_child(0) as Label ) radius_spin_box = SpinBox.new() radius_spin_box.name = "RadiusSpinBox" radius_spin_box.min_value = 0.5 radius_spin_box.max_value = 100.0 radius_spin_box.step = 0.5 radius_spin_box.value = brush_radius radius_spin_box.allow_greater = true radius_spin_box.suffix = " m" radius_spin_box.custom_minimum_size.x = ( 105.0 ) radius_spin_box.value_changed.connect( _on_radius_changed ) row.add_child( radius_spin_box ) func _on_radius_changed( value: float ) -> void: brush_radius = maxf( value, 0.5 ) brush_radius_changed.emit( brush_radius ) _save_configuration() func _create_instances_control() -> void: var row := _create_setting_row( "InstancesContainer", "Instances Per Stamp" ) instances_label = ( row.get_child(0) as Label ) instances_spin_box = SpinBox.new() instances_spin_box.name = "InstancesSpinBox" instances_spin_box.min_value = 1.0 instances_spin_box.max_value = 100.0 instances_spin_box.step = 1.0 instances_spin_box.value = instances_per_click instances_spin_box.rounded = true instances_spin_box.allow_greater = true instances_spin_box.custom_minimum_size.x = ( 105.0 ) instances_spin_box.value_changed.connect( _on_instances_changed ) row.add_child( instances_spin_box ) func _on_instances_changed( value: float ) -> void: instances_per_click = maxi( roundi(value), 1 ) instances_per_click_changed.emit( instances_per_click ) _save_configuration() func _create_spacing_control() -> void: var row := _create_setting_row( "SpacingContainer", "Minimum Spacing" ) spacing_label = ( row.get_child(0) as Label ) spacing_spin_box = SpinBox.new() spacing_spin_box.name = "SpacingSpinBox" spacing_spin_box.min_value = 0.0 spacing_spin_box.max_value = 100.0 spacing_spin_box.step = 0.25 spacing_spin_box.value = minimum_spacing spacing_spin_box.allow_greater = true spacing_spin_box.suffix = " m" spacing_spin_box.custom_minimum_size.x = ( 105.0 ) spacing_spin_box.value_changed.connect( _on_spacing_changed ) row.add_child( spacing_spin_box ) func _on_spacing_changed( value: float ) -> void: minimum_spacing = maxf( value, 0.0 ) minimum_spacing_changed.emit( minimum_spacing ) _save_configuration() func _create_random_rotation_control() -> void: random_rotation_toggle = CheckButton.new() random_rotation_toggle.name = "RandomRotationToggle" random_rotation_toggle.text = "Random Y Rotation" random_rotation_toggle.button_pressed = ( random_y_rotation ) random_rotation_toggle.toggled.connect( _on_random_rotation_toggled ) controls_container.add_child( random_rotation_toggle ) func _on_random_rotation_toggled( enabled: bool ) -> void: random_y_rotation = enabled random_y_rotation_changed.emit( random_y_rotation ) _save_configuration() func _create_align_surface_control() -> void: align_surface_toggle = CheckButton.new() align_surface_toggle.name = "AlignSurfaceToggle" align_surface_toggle.text = "Rotate With Surface" align_surface_toggle.tooltip_text = ( "Rotates the instance to follow the surface slope." ) align_surface_toggle.button_pressed = ( align_to_surface ) align_surface_toggle.toggled.connect( _on_align_surface_toggled ) controls_container.add_child( align_surface_toggle ) func _on_align_surface_toggled( enabled: bool ) -> void: align_to_surface = enabled align_to_surface_changed.emit( align_to_surface ) _save_configuration() # ========================================================= # أقصى ميل مسموح # ========================================================= func _create_maximum_slope_control() -> void: var separator := HSeparator.new() separator.name = "MaximumSlopeSeparator" controls_container.add_child( separator ) var row := _create_setting_row( "MaximumSlopeContainer", "Maximum Slope" ) maximum_slope_label = ( row.get_child(0) as Label ) maximum_slope_spin_box = SpinBox.new() maximum_slope_spin_box.name = "MaximumSlopeSpinBox" maximum_slope_spin_box.min_value = 0.0 maximum_slope_spin_box.max_value = 90.0 maximum_slope_spin_box.step = 1.0 maximum_slope_spin_box.value = ( maximum_slope_degrees ) maximum_slope_spin_box.suffix = "°" maximum_slope_spin_box.custom_minimum_size.x = ( 105.0 ) maximum_slope_spin_box.tooltip_text = ( "0° allows flat surfaces only. " + "90° allows nearly all surface slopes." ) maximum_slope_spin_box.value_changed.connect( _on_maximum_slope_changed ) row.add_child( maximum_slope_spin_box ) func _on_maximum_slope_changed( value: float ) -> void: maximum_slope_degrees = clampf( value, 0.0, 90.0 ) maximum_slope_changed.emit( maximum_slope_degrees ) _save_configuration() # ========================================================= # إزاحة الارتفاع # ========================================================= func _create_height_offset_controls() -> void: var separator := HSeparator.new() separator.name = "HeightOffsetSeparator" controls_container.add_child( separator ) var offset_row := _create_setting_row( "HeightOffsetContainer", "Height Offset" ) height_offset_label = ( offset_row.get_child(0) as Label ) height_offset_spin_box = SpinBox.new() height_offset_spin_box.name = "HeightOffsetSpinBox" height_offset_spin_box.min_value = -100.0 height_offset_spin_box.max_value = 100.0 height_offset_spin_box.step = 0.05 height_offset_spin_box.value = height_offset height_offset_spin_box.allow_lesser = true height_offset_spin_box.allow_greater = true height_offset_spin_box.suffix = " m" height_offset_spin_box.custom_minimum_size.x = ( 105.0 ) height_offset_spin_box.value_changed.connect( _on_height_offset_changed ) offset_row.add_child( height_offset_spin_box ) var direction_row := _create_setting_row( "HeightOffsetModeContainer", "Offset Direction" ) height_offset_mode_label = ( direction_row.get_child(0) as Label ) height_offset_mode_option = OptionButton.new() height_offset_mode_option.name = ( "HeightOffsetModeOption" ) height_offset_mode_option.custom_minimum_size.x = ( 150.0 ) height_offset_mode_option.add_item( "World Y", HeightOffsetMode.WORLD_Y ) height_offset_mode_option.add_item( "Surface Normal", HeightOffsetMode.SURFACE_NORMAL ) height_offset_mode_option.select( height_offset_mode ) height_offset_mode_option.item_selected.connect( _on_height_offset_mode_selected ) direction_row.add_child( height_offset_mode_option ) height_offset_help_label = Label.new() height_offset_help_label.name = "HeightOffsetHelpLabel" height_offset_help_label.text = ( "The offset direction moves the instance position only. " + "Enable Rotate With Surface to change its tilt." ) height_offset_help_label.autowrap_mode = ( TextServer.AUTOWRAP_WORD_SMART ) height_offset_help_label.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) height_offset_help_label.modulate = Color( 1.0, 1.0, 1.0, 0.72 ) controls_container.add_child( height_offset_help_label ) func _on_height_offset_changed( value: float ) -> void: height_offset = value height_offset_changed.emit( height_offset ) _save_configuration() func _on_height_offset_mode_selected( index: int ) -> void: height_offset_mode = clampi( index, HeightOffsetMode.WORLD_Y, HeightOffsetMode.SURFACE_NORMAL ) height_offset_mode_changed.emit( height_offset_mode ) _save_configuration() # ========================================================= # الحجم العشوائي # ========================================================= func _create_random_scale_controls() -> void: var separator := HSeparator.new() separator.name = "RandomScaleSeparator" controls_container.add_child( separator ) random_scale_toggle = CheckButton.new() random_scale_toggle.name = "RandomScaleToggle" random_scale_toggle.text = "Random Scale" random_scale_toggle.button_pressed = ( random_scale_enabled ) random_scale_toggle.toggled.connect( _on_random_scale_toggled ) controls_container.add_child( random_scale_toggle ) var minimum_row := _create_setting_row( "MinimumScaleContainer", "Minimum Scale" ) minimum_scale_label = ( minimum_row.get_child(0) as Label ) minimum_scale_spin_box = SpinBox.new() minimum_scale_spin_box.name = ( "MinimumScaleSpinBox" ) minimum_scale_spin_box.min_value = 0.01 minimum_scale_spin_box.max_value = 100.0 minimum_scale_spin_box.step = 0.05 minimum_scale_spin_box.value = minimum_scale minimum_scale_spin_box.allow_greater = true minimum_scale_spin_box.custom_minimum_size.x = ( 105.0 ) minimum_scale_spin_box.value_changed.connect( _on_minimum_scale_changed ) minimum_row.add_child( minimum_scale_spin_box ) var maximum_row := _create_setting_row( "MaximumScaleContainer", "Maximum Scale" ) maximum_scale_label = ( maximum_row.get_child(0) as Label ) maximum_scale_spin_box = SpinBox.new() maximum_scale_spin_box.name = ( "MaximumScaleSpinBox" ) maximum_scale_spin_box.min_value = 0.01 maximum_scale_spin_box.max_value = 100.0 maximum_scale_spin_box.step = 0.05 maximum_scale_spin_box.value = maximum_scale maximum_scale_spin_box.allow_greater = true maximum_scale_spin_box.custom_minimum_size.x = ( 105.0 ) maximum_scale_spin_box.value_changed.connect( _on_maximum_scale_changed ) maximum_row.add_child( maximum_scale_spin_box ) func _on_random_scale_toggled( enabled: bool ) -> void: random_scale_enabled = enabled random_scale_enabled_changed.emit( random_scale_enabled ) _update_controls() _save_configuration() func _on_minimum_scale_changed( value: float ) -> void: minimum_scale = maxf( value, 0.01 ) if minimum_scale > maximum_scale: maximum_scale = minimum_scale if is_instance_valid( maximum_scale_spin_box ): maximum_scale_spin_box.set_value_no_signal( maximum_scale ) maximum_scale_changed.emit( maximum_scale ) minimum_scale_changed.emit( minimum_scale ) _save_configuration() func _on_maximum_scale_changed( value: float ) -> void: maximum_scale = maxf( value, 0.01 ) if maximum_scale < minimum_scale: minimum_scale = maximum_scale if is_instance_valid( minimum_scale_spin_box ): minimum_scale_spin_box.set_value_no_signal( minimum_scale ) minimum_scale_changed.emit( minimum_scale ) maximum_scale_changed.emit( maximum_scale ) _save_configuration() func _create_setting_row( row_name: String, label_text: String ) -> HBoxContainer: var row := HBoxContainer.new() row.name = row_name row.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) var label := Label.new() label.name = "%sLabel" % row_name label.text = label_text label.size_flags_horizontal = ( Control.SIZE_EXPAND_FILL ) row.add_child( label ) controls_container.add_child( row ) return row func _update_controls() -> void: var has_active_scene := ( selected_scene != null ) _update_lod_controls() _update_auto_lod_controls() if is_instance_valid( brush_toggle ): brush_toggle.disabled = ( not has_active_scene ) if is_instance_valid( radius_spin_box ): radius_spin_box.editable = ( has_active_scene ) if is_instance_valid( instances_spin_box ): instances_spin_box.editable = ( has_active_scene ) if is_instance_valid( spacing_spin_box ): spacing_spin_box.editable = ( has_active_scene ) if is_instance_valid( random_rotation_toggle ): random_rotation_toggle.disabled = ( not has_active_scene ) if is_instance_valid( align_surface_toggle ): align_surface_toggle.disabled = ( not has_active_scene ) if is_instance_valid( maximum_slope_spin_box ): maximum_slope_spin_box.editable = ( has_active_scene ) if is_instance_valid( height_offset_spin_box ): height_offset_spin_box.editable = ( has_active_scene ) if is_instance_valid( height_offset_mode_option ): height_offset_mode_option.disabled = ( not has_active_scene ) if is_instance_valid( random_scale_toggle ): random_scale_toggle.disabled = ( not has_active_scene ) var scale_controls_enabled := ( has_active_scene and random_scale_enabled ) if is_instance_valid( minimum_scale_spin_box ): minimum_scale_spin_box.editable = ( scale_controls_enabled ) if is_instance_valid( maximum_scale_spin_box ): maximum_scale_spin_box.editable = ( scale_controls_enabled ) # ========================================================= # دوال القراءة # ========================================================= func get_selected_scene() -> PackedScene: return selected_scene func get_active_scene_index() -> int: return ( _get_valid_scene_pickers().find( active_picker ) ) func is_brush_enabled() -> bool: return brush_enabled func get_brush_radius() -> float: return brush_radius func get_instances_per_click() -> int: return instances_per_click func get_minimum_spacing() -> float: return minimum_spacing func is_random_y_rotation_enabled() -> bool: return random_y_rotation func is_align_to_surface_enabled() -> bool: return align_to_surface func get_maximum_slope() -> float: return maximum_slope_degrees func get_height_offset() -> float: return height_offset func get_height_offset_mode() -> int: return height_offset_mode func is_random_scale_enabled() -> bool: return random_scale_enabled func get_minimum_scale() -> float: return minimum_scale func get_maximum_scale() -> float: return maximum_scale func get_interface_language() -> String: return current_language