diff options
Diffstat (limited to 'game/addons/zylann.hterrain/tools/brush/settings_dialog')
5 files changed, 624 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd new file mode 100644 index 0000000..71f81d1 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd @@ -0,0 +1,280 @@ +@tool +extends AcceptDialog + +const HT_Util = preload("../../../util/util.gd") +const HT_Brush = preload("../brush.gd") +const HT_Logger = preload("../../../util/logger.gd") +const HT_EditorUtil = preload("../../util/editor_util.gd") +const HT_SpinSlider = preload("../../util/spin_slider.gd") +const HT_Scratchpad = preload("./preview_scratchpad.gd") + +@onready var _scratchpad : HT_Scratchpad = $VB/HB/VB3/PreviewScratchpad + +@onready var _shape_list : ItemList = $VB/HB/VB/ShapeList +@onready var _remove_shape_button : Button = $VB/HB/VB/HBoxContainer/RemoveShape +@onready var _change_shape_button : Button = $VB/HB/VB/ChangeShape + +@onready var _size_slider : HT_SpinSlider = $VB/HB/VB2/Settings/Size +@onready var _opacity_slider : HT_SpinSlider = $VB/HB/VB2/Settings/Opacity +@onready var _pressure_enabled_checkbox : CheckBox = $VB/HB/VB2/Settings/PressureEnabled +@onready var _pressure_over_size_slider : HT_SpinSlider = $VB/HB/VB2/Settings/PressureOverSize +@onready var _pressure_over_opacity_slider : HT_SpinSlider = $VB/HB/VB2/Settings/PressureOverOpacity +@onready var _frequency_distance_slider : HT_SpinSlider = $VB/HB/VB2/Settings/FrequencyDistance +@onready var _frequency_time_slider : HT_SpinSlider = $VB/HB/VB2/Settings/FrequencyTime +@onready var _random_rotation_checkbox : CheckBox = $VB/HB/VB2/Settings/RandomRotation +@onready var _shape_cycling_checkbox : CheckBox = $VB/HB/VB2/Settings/ShapeCycling + +var _brush : HT_Brush +# This is a `EditorFileDialog`, +# but cannot type it because I want to be able to test it by running the scene. +# And when I run it, Godot does not allow to use `EditorFileDialog`. +var _load_image_dialog +# -1 means add, otherwise replace +var _load_image_index := -1 +var _logger = HT_Logger.get_for(self) + + +func _ready(): + if HT_Util.is_in_edited_scene(self): + return + + _size_slider.set_max_value(HT_Brush.MAX_SIZE_FOR_SLIDERS) + _size_slider.set_greater_max_value(HT_Brush.MAX_SIZE) + + # TESTING + if not Engine.is_editor_hint(): + setup_dialogs(self) + call_deferred("popup") + + +func set_brush(brush : HT_Brush): + assert(brush != null) + _brush = brush + _update_controls_from_brush() + + +# `base_control` can no longer be hinted as a `Control` because in Godot 4 it could be a +# window or dialog, which are no longer controls... +func setup_dialogs(base_control: Node): + assert(_load_image_dialog == null) + _load_image_dialog = HT_EditorUtil.create_open_file_dialog() + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE + _load_image_dialog.add_filter("*.exr ; EXR files") + _load_image_dialog.unresizable = false + _load_image_dialog.access = EditorFileDialog.ACCESS_FILESYSTEM + _load_image_dialog.current_dir = HT_Brush.SHAPES_DIR + _load_image_dialog.file_selected.connect(_on_LoadImageDialog_file_selected) + _load_image_dialog.files_selected.connect(_on_LoadImageDialog_files_selected) + #base_control.add_child(_load_image_dialog) + # When a dialog opens another dialog, we get this error: + # "Transient parent has another exclusive child." + # Which is worked around by making the other dialog a child of the first one (I don't know why) + add_child(_load_image_dialog) + + +func _exit_tree(): + if _load_image_dialog != null: + _load_image_dialog.queue_free() + _load_image_dialog = null + + +func _get_shapes_from_gui() -> Array[Texture2D]: + var shapes : Array[Texture2D] = [] + for i in _shape_list.get_item_count(): + var icon : Texture2D = _shape_list.get_item_icon(i) + assert(icon != null) + shapes.append(icon) + return shapes + + +func _update_shapes_gui(shapes: Array[Texture2D]): + _shape_list.clear() + for shape in shapes: + assert(shape != null) + assert(shape is Texture2D) + _shape_list.add_icon_item(shape) + _update_shape_list_buttons() + + +func _on_AddShape_pressed(): + _load_image_index = -1 + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILES + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_RemoveShape_pressed(): + var selected_indices := _shape_list.get_selected_items() + if len(selected_indices) == 0: + return + + var index : int = selected_indices[0] + _shape_list.remove_item(index) + + var shapes := _get_shapes_from_gui() + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shape_list_buttons() + + +func _on_ShapeList_item_activated(index: int): + _request_modify_shape(index) + + +func _on_ChangeShape_pressed(): + var selected = _shape_list.get_selected_items() + if len(selected) == 0: + return + _request_modify_shape(selected[0]) + + +func _request_modify_shape(index: int): + _load_image_index = index + _load_image_dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE + _load_image_dialog.popup_centered_ratio(0.7) + + +func _on_LoadImageDialog_files_selected(fpaths: PackedStringArray): + var shapes := _get_shapes_from_gui() + + for fpath in fpaths: + var tex := HT_Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + continue + shapes.append(tex) + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _on_LoadImageDialog_file_selected(fpath: String): + var tex := HT_Brush.load_shape_from_image_file(fpath, _logger) + if tex == null: + # Failed + return + + var shapes := _get_shapes_from_gui() + if _load_image_index == -1 or _load_image_index >= len(shapes): + # Add + shapes.append(tex) + else: + # Replace + assert(_load_image_index >= 0) + shapes[_load_image_index] = tex + + for brush in _get_brushes(): + brush.set_shapes(shapes) + + _update_shapes_gui(shapes) + + +func _notification(what: int): + if what == NOTIFICATION_VISIBILITY_CHANGED: + # Testing the scratchpad because visibility can not only change before entering the tree + # since Godot 4 port, it can also change between entering the tree and being _ready... + if visible and _scratchpad != null: + _update_controls_from_brush() + + +func _update_controls_from_brush(): + var brush := _brush + + if brush == null: + # To allow testing + brush = _scratchpad.get_painter().get_brush() + + _update_shapes_gui(brush.get_shapes()) + + _size_slider.set_value(brush.get_size(), false) + _opacity_slider.set_value(brush.get_opacity() * 100.0, false) + _pressure_enabled_checkbox.button_pressed = brush.is_pressure_enabled() + _pressure_over_size_slider.set_value(brush.get_pressure_over_scale() * 100.0, false) + _pressure_over_opacity_slider.set_value(brush.get_pressure_over_opacity() * 100.0, false) + _frequency_distance_slider.set_value(brush.get_frequency_distance(), false) + _frequency_time_slider.set_value( + 1000.0 / maxf(0.1, float(brush.get_frequency_time_ms())), false) + _random_rotation_checkbox.button_pressed = brush.is_random_rotation_enabled() + _shape_cycling_checkbox.button_pressed = brush.is_shape_cycling_enabled() + + +func _on_ClearScratchpad_pressed(): + _scratchpad.reset_image() + + +func _on_Size_value_changed(value: float): + for brush in _get_brushes(): + brush.set_size(value) + + +func _on_Opacity_value_changed(value): + for brush in _get_brushes(): + brush.set_opacity(value / 100.0) + + +func _on_PressureEnabled_toggled(button_pressed): + for brush in _get_brushes(): + brush.set_pressure_enabled(button_pressed) + + +func _on_PressureOverSize_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_scale(value / 100.0) + + +func _on_PressureOverOpacity_value_changed(value): + for brush in _get_brushes(): + brush.set_pressure_over_opacity(value / 100.0) + + +func _on_FrequencyDistance_value_changed(value): + for brush in _get_brushes(): + brush.set_frequency_distance(value) + + +func _on_FrequencyTime_value_changed(fps): + fps = max(1.0, fps) + var ms = 1000.0 / fps + if is_equal_approx(fps, 60.0): + ms = 0 + for brush in _get_brushes(): + brush.set_frequency_time_ms(ms) + + +func _on_RandomRotation_toggled(button_pressed: bool): + for brush in _get_brushes(): + brush.set_random_rotation_enabled(button_pressed) + + +func _on_shape_cycling_toggled(button_pressed: bool): + for brush in _get_brushes(): + brush.set_shape_cycling_enabled(button_pressed) + + +func _get_brushes() -> Array[HT_Brush]: + if _brush != null: + # We edit both the preview brush and the terrain brush + # TODO Could we simply share the brush? + return [_brush, _scratchpad.get_painter().get_brush()] + # When testing the dialog in isolation, the edited brush might be null + return [_scratchpad.get_painter().get_brush()] + + +func _on_ShapeList_item_selected(index): + _update_shape_list_buttons() + for brush in _get_brushes(): + brush.set_shape_index(index) + + +func _update_shape_list_buttons(): + var selected_count := len(_shape_list.get_selected_items()) + # There must be at least one shape + _remove_shape_button.disabled = _shape_list.get_item_count() == 1 or selected_count == 0 + _change_shape_button.disabled = selected_count == 0 + + +func _on_shape_list_empty_clicked(at_position, mouse_button_index): + _update_shape_list_buttons() + diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn new file mode 100644 index 0000000..7d7e07d --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.tscn @@ -0,0 +1,211 @@ +[gd_scene load_steps=4 format=3 uid="uid://d2rt3wj8xkhp2"] + +[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/spin_slider.tscn" id="2"] +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/brush_settings_dialog.gd" id="3"] +[ext_resource type="PackedScene" uid="uid://ng00jipfeucy" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn" id="4"] + +[node name="BrushSettingsDialog" type="AcceptDialog"] +title = "Brush settings" +size = Vector2i(700, 422) +min_size = Vector2i(700, 400) +script = ExtResource("3") + +[node name="VB" type="VBoxContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = -49.0 + +[node name="HB" type="HBoxContainer" parent="VB"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="VB" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="VB/HB/VB"] +layout_mode = 2 +text = "Shapes" + +[node name="ShapeList" type="ItemList" parent="VB/HB/VB"] +layout_mode = 2 +size_flags_vertical = 3 +fixed_icon_size = Vector2i(100, 100) + +[node name="ChangeShape" type="Button" parent="VB/HB/VB"] +layout_mode = 2 +disabled = true +text = "Change..." + +[node name="HBoxContainer" type="HBoxContainer" parent="VB/HB/VB"] +layout_mode = 2 + +[node name="AddShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +layout_mode = 2 +text = "Add..." + +[node name="RemoveShape" type="Button" parent="VB/HB/VB/HBoxContainer"] +layout_mode = 2 +disabled = true +text = "Remove" + +[node name="VB2" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="VB/HB/VB2"] +layout_mode = 2 + +[node name="Settings" type="VBoxContainer" parent="VB/HB/VB2"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Size" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +size_flags_horizontal = 3 +value = 32.0 +min_value = 2.0 +max_value = 500.0 +prefix = "Size:" +suffix = "px" +rounded = true +centered = true +allow_greater = true +greater_max_value = 4000.0 + +[node name="Opacity" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +size_flags_horizontal = 3 +value = 100.0 +max_value = 100.0 +prefix = "Opacity" +suffix = "%" +rounded = true +centered = true +greater_max_value = 10000.0 + +[node name="PressureEnabled" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Enable pressure (pen tablets)" + +[node name="PressureOverSize" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 50.0 +max_value = 100.0 +prefix = "Pressure affects size:" +suffix = "%" +centered = true +greater_max_value = 10000.0 + +[node name="PressureOverOpacity" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 50.0 +max_value = 100.0 +prefix = "Pressure affects opacity:" +suffix = "%" +centered = true +greater_max_value = 10000.0 + +[node name="FrequencyTime" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +value = 60.0 +min_value = 1.0 +max_value = 60.0 +prefix = "Frequency time:" +suffix = "fps" +centered = true +greater_max_value = 10000.0 + +[node name="FrequencyDistance" parent="VB/HB/VB2/Settings" instance=ExtResource("2")] +custom_minimum_size = Vector2(32, 28) +layout_mode = 2 +max_value = 100.0 +prefix = "Frequency distance:" +suffix = "px" +centered = true +greater_max_value = 4000.0 + +[node name="RandomRotation" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Random rotation" + +[node name="ShapeCycling" type="CheckBox" parent="VB/HB/VB2/Settings"] +layout_mode = 2 +text = "Shape cycling" + +[node name="HSeparator" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="SizeLimitHB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="Label" type="Label" parent="VB/HB/VB2/Settings/SizeLimitHB"] +layout_mode = 2 +mouse_filter = 0 +text = "Size limit:" + +[node name="SizeLimit" type="SpinBox" parent="VB/HB/VB2/Settings/SizeLimitHB"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 1.0 +max_value = 1000.0 +value = 200.0 + +[node name="HSeparator2" type="HSeparator" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="HB" type="HBoxContainer" parent="VB/HB/VB2/Settings"] +visible = false +layout_mode = 2 + +[node name="Button" type="Button" parent="VB/HB/VB2/Settings/HB"] +layout_mode = 2 +text = "Load preset..." + +[node name="Button2" type="Button" parent="VB/HB/VB2/Settings/HB"] +layout_mode = 2 +text = "Save preset..." + +[node name="VB3" type="VBoxContainer" parent="VB/HB"] +layout_mode = 2 + +[node name="Label" type="Label" parent="VB/HB/VB3"] +layout_mode = 2 +text = "Scratchpad" + +[node name="PreviewScratchpad" parent="VB/HB/VB3" instance=ExtResource("4")] +custom_minimum_size = Vector2(200, 300) +layout_mode = 2 + +[node name="ClearScratchpad" type="Button" parent="VB/HB/VB3"] +layout_mode = 2 +text = "Clear" + +[connection signal="empty_clicked" from="VB/HB/VB/ShapeList" to="." method="_on_shape_list_empty_clicked"] +[connection signal="item_activated" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_activated"] +[connection signal="item_selected" from="VB/HB/VB/ShapeList" to="." method="_on_ShapeList_item_selected"] +[connection signal="pressed" from="VB/HB/VB/ChangeShape" to="." method="_on_ChangeShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/AddShape" to="." method="_on_AddShape_pressed"] +[connection signal="pressed" from="VB/HB/VB/HBoxContainer/RemoveShape" to="." method="_on_RemoveShape_pressed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Size" to="." method="_on_Size_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/Opacity" to="." method="_on_Opacity_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/PressureEnabled" to="." method="_on_PressureEnabled_toggled"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverSize" to="." method="_on_PressureOverSize_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/PressureOverOpacity" to="." method="_on_PressureOverOpacity_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyTime" to="." method="_on_FrequencyTime_value_changed"] +[connection signal="value_changed" from="VB/HB/VB2/Settings/FrequencyDistance" to="." method="_on_FrequencyDistance_value_changed"] +[connection signal="toggled" from="VB/HB/VB2/Settings/RandomRotation" to="." method="_on_RandomRotation_toggled"] +[connection signal="toggled" from="VB/HB/VB2/Settings/ShapeCycling" to="." method="_on_shape_cycling_toggled"] +[connection signal="pressed" from="VB/HB/VB3/ClearScratchpad" to="." method="_on_ClearScratchpad_pressed"] diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd new file mode 100644 index 0000000..be52072 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd @@ -0,0 +1,41 @@ +@tool +extends Node + +const HT_Painter = preload("./../painter.gd") +const HT_Brush = preload("./../brush.gd") + +const HT_ColorShader = preload("../shaders/color.gdshader") + +var _painter : HT_Painter +var _brush : HT_Brush + + +func _init(): + var p = HT_Painter.new() + # The name is just for debugging + p.set_name("Painter") + add_child(p) + _painter = p + + _brush = HT_Brush.new() + + +func set_image_texture(image: Image, texture: ImageTexture): + _painter.set_image(image, texture) + + +func get_brush() -> HT_Brush: + return _brush + + +# This may be called from an `_input` callback +func paint_input(position: Vector2, pressure: float): + var p : HT_Painter = _painter + + if not _brush.configure_paint_input([p], position, pressure): + return + + p.set_brush_shader(HT_ColorShader) + p.set_brush_shader_param("u_color", Color(0,0,0,1)) + #p.set_image(_image, _texture) + p.paint_input(position) diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd new file mode 100644 index 0000000..cec0728 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd @@ -0,0 +1,70 @@ +@tool +extends Control + +const HT_PreviewPainter = preload("./preview_painter.gd") +# TODO Can't preload because it causes the plugin to fail loading if assets aren't imported +#const HT_DefaultBrushTexture = preload("../shapes/round2.exr") +const HT_Brush = preload("../brush.gd") +const HT_Logger = preload("../../../util/logger.gd") +const HT_EditorUtil = preload("../../util/editor_util.gd") +const HT_Util = preload("../../../util/util.gd") + +@onready var _texture_rect : TextureRect = $TextureRect +@onready var _painter : HT_PreviewPainter = $Painter + +var _logger := HT_Logger.get_for(self) + + +func _ready(): + if HT_Util.is_in_edited_scene(self): + # If it runs in the edited scene, + # saving the scene would also save the ImageTexture in it... + return + reset_image() + # Default so it doesn't crash when painting and can be tested + var default_brush_texture = \ + HT_EditorUtil.load_texture(HT_Brush.DEFAULT_BRUSH_TEXTURE_PATH, _logger) + _painter.get_brush().set_shapes([default_brush_texture]) + + +func reset_image(): + var image = Image.create(_texture_rect.size.x, _texture_rect.size.y, false, Image.FORMAT_RGB8) + image.fill(Color(1,1,1)) + + # TEST +# var fnl = FastNoiseLite.new() +# for y in image.get_height(): +# for x in image.get_width(): +# var g = 0.5 + 0.5 * fnl.get_noise_2d(x, y) +# image.set_pixel(x, y, Color(g, g, g, 1.0)) + + var texture = ImageTexture.create_from_image(image) + _texture_rect.texture = texture + _painter.set_image_texture(image, texture) + + +func get_painter() -> HT_PreviewPainter: + return _painter + + +func _gui_input(event): + if event is InputEventMouseMotion: + if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): + _painter.paint_input(event.position, event.pressure) + queue_redraw() + + elif event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + if event.pressed: + # TODO `pressure` is not available on button events + # So I have to assume zero... which means clicks do not paint anything? + _painter.paint_input(event.position, 0.0) + else: + _painter.get_brush().on_paint_end() + + +func _draw(): + var mpos = get_local_mouse_position() + var brush = _painter.get_brush() + draw_arc(mpos, 0.5 * brush.get_size(), -PI, PI, 32, Color(1, 0.2, 0.2), 2.0, true) + diff --git a/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn new file mode 100644 index 0000000..0b50c91 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=3 format=3 uid="uid://ng00jipfeucy"] + +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_scratchpad.gd" id="1"] +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/brush/settings_dialog/preview_painter.gd" id="2"] + +[node name="PreviewScratchpad" type="Control"] +clip_contents = true +layout_mode = 3 +anchors_preset = 0 +offset_right = 200.0 +offset_bottom = 274.0 +script = ExtResource("1") + +[node name="Painter" type="Node" parent="."] +script = ExtResource("2") + +[node name="TextureRect" type="TextureRect" parent="."] +show_behind_parent = true +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +stretch_mode = 5 |