diff options
Diffstat (limited to 'game/addons/zylann.hterrain/tools/importer')
-rw-r--r-- | game/addons/zylann.hterrain/tools/importer/importer_dialog.gd | 312 | ||||
-rw-r--r-- | game/addons/zylann.hterrain/tools/importer/importer_dialog.tscn | 87 |
2 files changed, 399 insertions, 0 deletions
diff --git a/game/addons/zylann.hterrain/tools/importer/importer_dialog.gd b/game/addons/zylann.hterrain/tools/importer/importer_dialog.gd new file mode 100644 index 0000000..5f57bb1 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/importer/importer_dialog.gd @@ -0,0 +1,312 @@ +@tool +extends AcceptDialog + +const HT_Util = preload("../../util/util.gd") +const HTerrain = preload("../../hterrain.gd") +const HTerrainData = preload("../../hterrain_data.gd") +const HT_Errors = preload("../../util/errors.gd") +const HT_Logger = preload("../../util/logger.gd") +const HT_XYZFormat = preload("../../util/xyz_format.gd") +const HT_Inspector = preload("../inspector/inspector.gd") + +signal permanent_change_performed(message) + +@onready var _inspector : HT_Inspector = $VBoxContainer/Inspector +@onready var _errors_label : Label = $VBoxContainer/ColorRect/ScrollContainer/VBoxContainer/Errors +@onready var _warnings_label : Label = \ + $VBoxContainer/ColorRect/ScrollContainer/VBoxContainer/Warnings + +const RAW_LITTLE_ENDIAN = 0 +const RAW_BIG_ENDIAN = 1 + +var _terrain : HTerrain = null +var _logger = HT_Logger.get_for(self) + + +func _init(): + get_ok_button().hide() + + +func _ready(): + _inspector.set_prototype({ + "heightmap": { + "type": TYPE_STRING, + "usage": "file", + "exts": ["raw", "png", "exr", "xyz"] + }, + "raw_endianess": { + "type": TYPE_INT, + "usage": "enum", + "enum_items": ["Little Endian", "Big Endian"], + "enabled": false + }, + "min_height": { + "type": TYPE_FLOAT, + "range": {"min": -2000.0, "max": 2000.0, "step": 0.01}, + "default_value": 0.0 + }, + "max_height": { + "type": TYPE_FLOAT, + "range": {"min": -2000.0, "max": 2000.0, "step": 0.01}, + "default_value": 400.0 + }, + "splatmap": { + "type": TYPE_STRING, + "usage": "file", + "exts": ["png"] + }, + "colormap": { + "type": TYPE_STRING, + "usage": "file", + "exts": ["png"] + } + }) + + # Testing +# _errors_label.text = "- Hello World!" +# _warnings_label.text = "- Yolo Jesus!" + + +func set_terrain(terrain: HTerrain): + _terrain = terrain + + +func _notification(what: int): + if what == NOTIFICATION_VISIBILITY_CHANGED: + # Checking a node set in _ready, + # because visibility can also change between _enter_tree and _ready... + if visible and _inspector != null: + _clear_feedback() + + +static func _format_feedbacks(feed): + var a := [] + for s in feed: + a.append("- " + s) + return "\n".join(PackedStringArray(a)) + + +func _clear_feedback(): + _errors_label.text = "" + _warnings_label.text = "" + + +class HT_ErrorCheckReport: + var errors := [] + var warnings := [] + + +func _show_feedback(res: HT_ErrorCheckReport): + for e in res.errors: + _logger.error(e) + + for w in res.warnings: + _logger.warn(w) + + _clear_feedback() + + if len(res.errors) > 0: + _errors_label.text = _format_feedbacks(res.errors) + + if len(res.warnings) > 0: + _warnings_label.text = _format_feedbacks(res.warnings) + + +func _on_CheckButton_pressed(): + var res := _validate_form() + _show_feedback(res) + + +func _on_ImportButton_pressed(): + assert(_terrain != null and _terrain.get_data() != null) + + # Verify input to inform the user of potential issues + var res := _validate_form() + _show_feedback(res) + + if len(res.errors) != 0: + _logger.debug("Cannot import due to errors, aborting") + return + + var params := {} + + var heightmap_path = _inspector.get_value("heightmap") + if heightmap_path != "": + var endianess = _inspector.get_value("raw_endianess") + params[HTerrainData.CHANNEL_HEIGHT] = { + "path": heightmap_path, + "min_height": _inspector.get_value("min_height"), + "max_height": _inspector.get_value("max_height"), + "big_endian": endianess == RAW_BIG_ENDIAN + } + + var colormap_path = _inspector.get_value("colormap") + if colormap_path != "": + params[HTerrainData.CHANNEL_COLOR] = { + "path": colormap_path + } + + var splatmap_path = _inspector.get_value("splatmap") + if splatmap_path != "": + params[HTerrainData.CHANNEL_SPLAT] = { + "path": splatmap_path + } + + var data = _terrain.get_data() + data._edit_import_maps(params) + emit_signal("permanent_change_performed", "Import maps") + + _logger.debug("Terrain import finished") + hide() + + +func _on_CancelButton_pressed(): + hide() + + +func _on_Inspector_property_changed(key: String, value): + if key == "heightmap": + var is_raw = value.get_extension().to_lower() == "raw" + _inspector.set_property_enabled("raw_endianess", is_raw) + + +func _validate_form() -> HT_ErrorCheckReport: + var res := HT_ErrorCheckReport.new() + + var heightmap_path : String = _inspector.get_value("heightmap") + var splatmap_path : String = _inspector.get_value("splatmap") + var colormap_path : String = _inspector.get_value("colormap") + + if colormap_path == "" and heightmap_path == "" and splatmap_path == "": + res.errors.append("No maps specified.") + return res + + # If a heightmap is specified, it will override the size of the existing terrain. + # If not specified, maps will have to match the resolution of the existing terrain. + var heightmap_size := _terrain.get_data().get_resolution() + + if heightmap_path != "": + var min_height = _inspector.get_value("min_height") + var max_height = _inspector.get_value("max_height") + + if min_height >= max_height: + res.errors.append("Minimum height must be lower than maximum height") + # Returning early because min and max can be slided, + # so we avoid loading other maps every time to do further checks. + return res + + var image_size_result = _load_image_size(heightmap_path, _logger) + if image_size_result.error_code != OK: + res.errors.append(str("Cannot open heightmap file: ", image_size_result.to_string())) + return res + + var adjusted_size = HTerrainData.get_adjusted_map_size( + image_size_result.width, image_size_result.height) + + if adjusted_size != image_size_result.width: + res.warnings.append( + "The square resolution deduced from heightmap file size is not power of two + 1.\n" + \ + "The heightmap will be cropped.") + + heightmap_size = adjusted_size + + if splatmap_path != "": + _check_map_size(splatmap_path, "splatmap", heightmap_size, res, _logger) + + if colormap_path != "": + _check_map_size(colormap_path, "colormap", heightmap_size, res, _logger) + + return res + + +static func _check_map_size(path: String, map_name: String, heightmap_size: int, + res: HT_ErrorCheckReport, logger): + + var size_result := _load_image_size(path, logger) + if size_result.error_code != OK: + res.errors.append(str("Cannot open splatmap file: ", size_result.to_string())) + return + var adjusted_size := HTerrainData.get_adjusted_map_size(size_result.width, size_result.height) + if adjusted_size != heightmap_size: + res.errors.append(str( + "The ", map_name, + " must have the same resolution as the heightmap (", heightmap_size, ")")) + else: + if adjusted_size != size_result.width: + res.warnings.append(str( + "The square resolution deduced from ", map_name, + " file size is not power of two + 1.\nThe ", + map_name, " will be cropped.")) + + +class HT_ImageSizeResult: + var width := 0 + var height := 0 + var error_code := OK + var error_message := "" + + func to_string() -> String: + if error_message != "": + return error_message + return HT_Errors.get_message(error_code) + + +static func _load_image_size(path: String, logger) -> HT_ImageSizeResult: + var ext := path.get_extension().to_lower() + var result := HT_ImageSizeResult.new() + + if ext == "png" or ext == "exr": + # Godot can load these formats natively + var im := Image.new() + var err := im.load(path) + if err != OK: + logger.error("An error occurred loading image '{0}', code {1}".format([path, err])) + result.error_code = err + return result + + result.width = im.get_width() + result.height = im.get_height() + return result + + elif ext == "raw": + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + var err := FileAccess.get_open_error() + logger.error("Error opening file {0}".format([path])) + result.error_code = err + return result + + # Assume the raw data is square in 16-bit format, + # so its size is function of file length + var flen := f.get_length() + f = null + var size_px = HT_Util.integer_square_root(flen / 2) + if size_px == -1: + result.error_code = ERR_INVALID_DATA + result.error_message = "RAW image is not square" + return result + + logger.debug("Deduced RAW heightmap resolution: {0}*{1}, for a length of {2}" \ + .format([size_px, size_px, flen])) + + result.width = size_px + result.height = size_px + return result + + elif ext == "xyz": + var f := FileAccess.open(path, FileAccess.READ) + if f == null: + var err := FileAccess.get_open_error() + logger.error("Error opening file {0}".format([path])) + result.error_code = err + return result + + var bounds := HT_XYZFormat.load_bounds(f) + + result.width = bounds.image_width + result.height = bounds.image_height + return result + + else: + result.error_code = ERR_FILE_UNRECOGNIZED + return result diff --git a/game/addons/zylann.hterrain/tools/importer/importer_dialog.tscn b/game/addons/zylann.hterrain/tools/importer/importer_dialog.tscn new file mode 100644 index 0000000..0c2d267 --- /dev/null +++ b/game/addons/zylann.hterrain/tools/importer/importer_dialog.tscn @@ -0,0 +1,87 @@ +[gd_scene load_steps=4 format=3 uid="uid://on7x7xkovsc8"] + +[ext_resource type="Script" path="res://addons/zylann.hterrain/tools/importer/importer_dialog.gd" id="1"] +[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/inspector/inspector.tscn" id="2"] +[ext_resource type="PackedScene" path="res://addons/zylann.hterrain/tools/util/dialog_fitter.tscn" id="3"] + +[node name="WindowDialog" type="AcceptDialog"] +title = "Import maps" +size = Vector2i(500, 433) +min_size = Vector2i(500, 380) +script = ExtResource("1") + +[node name="VBoxContainer" 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 = -18.0 + +[node name="Label" type="Label" parent="VBoxContainer"] +layout_mode = 2 +text = "Select maps to import. Leave empty if you don't need some." + +[node name="Spacer" type="Control" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 16) +layout_mode = 2 + +[node name="Inspector" parent="VBoxContainer" instance=ExtResource("2")] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="ColorRect" type="ColorRect" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 100) +layout_mode = 2 +color = Color(0, 0, 0, 0.417529) + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer/ColorRect"] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/ColorRect/ScrollContainer"] +layout_mode = 2 + +[node name="Errors" type="Label" parent="VBoxContainer/ColorRect/ScrollContainer/VBoxContainer"] +self_modulate = Color(1, 0.203125, 0.203125, 1) +layout_mode = 2 + +[node name="Warnings" type="Label" parent="VBoxContainer/ColorRect/ScrollContainer/VBoxContainer"] +self_modulate = Color(1, 0.901428, 0.257813, 1) +layout_mode = 2 + +[node name="Spacer2" type="Control" parent="VBoxContainer"] +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 + +[node name="ButtonsArea" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 +mouse_filter = 0 +alignment = 1 + +[node name="CheckButton" type="Button" parent="VBoxContainer/ButtonsArea"] +layout_mode = 2 +text = "Check" + +[node name="ImportButton" type="Button" parent="VBoxContainer/ButtonsArea"] +layout_mode = 2 +text = "Import" + +[node name="CancelButton" type="Button" parent="VBoxContainer/ButtonsArea"] +layout_mode = 2 +text = "Cancel" + +[node name="DialogFitter" parent="." instance=ExtResource("3")] +layout_mode = 3 +anchors_preset = 0 +offset_left = 8.0 +offset_top = 8.0 +offset_right = 492.0 +offset_bottom = 415.0 + +[connection signal="property_changed" from="VBoxContainer/Inspector" to="." method="_on_Inspector_property_changed"] +[connection signal="pressed" from="VBoxContainer/ButtonsArea/CheckButton" to="." method="_on_CheckButton_pressed"] +[connection signal="pressed" from="VBoxContainer/ButtonsArea/ImportButton" to="." method="_on_ImportButton_pressed"] +[connection signal="pressed" from="VBoxContainer/ButtonsArea/CancelButton" to="." method="_on_CancelButton_pressed"] |