diff --git a/terrain/SideAndCornerExtensions.cs b/GodotExtensions.cs similarity index 86% rename from terrain/SideAndCornerExtensions.cs rename to GodotExtensions.cs index 40e2836..f00501b 100644 --- a/terrain/SideAndCornerExtensions.cs +++ b/GodotExtensions.cs @@ -1,5 +1,8 @@ -public static class SideAndCornerExtensions +public static class GodotExtensions { + public static Vector2I RoundToVector2I(this Vector2 vector) + => new(RoundToInt(vector.X), RoundToInt(vector.Y)); + public static (Corner, Corner) GetCorners(this Side side) => side switch { Side.Left => (Corner.TopLeft, Corner.BottomLeft), diff --git a/PhysicsLayer.cs b/PhysicsLayer.cs index 972eb50..ce1ac75 100644 --- a/PhysicsLayer.cs +++ b/PhysicsLayer.cs @@ -1,3 +1,4 @@ +[Flags] public enum PhysicsLayer { Terrain = 0b0000_0001, diff --git a/assets/textures/terrain/editing/circle.png b/addons/terrain-editing/icons/circle.png similarity index 100% rename from assets/textures/terrain/editing/circle.png rename to addons/terrain-editing/icons/circle.png diff --git a/assets/textures/terrain/editing/circle.png.import b/addons/terrain-editing/icons/circle.png.import similarity index 71% rename from assets/textures/terrain/editing/circle.png.import rename to addons/terrain-editing/icons/circle.png.import index 2b64172..f234752 100644 --- a/assets/textures/terrain/editing/circle.png.import +++ b/addons/terrain-editing/icons/circle.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://2u1ldmh0osbx" -path="res://.godot/imported/circle.png-480be724d6b688a76820398ea30097a4.ctex" +path="res://.godot/imported/circle.png-7ef635c7ab3f0bfc0f9202fc8a8f1dd4.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/circle.png" -dest_files=["res://.godot/imported/circle.png-480be724d6b688a76820398ea30097a4.ctex"] +source_file="res://addons/terrain-editing/icons/circle.png" +dest_files=["res://.godot/imported/circle.png-7ef635c7ab3f0bfc0f9202fc8a8f1dd4.ctex"] [params] diff --git a/assets/textures/terrain/editing/connected_off.png b/addons/terrain-editing/icons/connected_off.png similarity index 100% rename from assets/textures/terrain/editing/connected_off.png rename to addons/terrain-editing/icons/connected_off.png diff --git a/assets/textures/terrain/editing/connected_off.png.import b/addons/terrain-editing/icons/connected_off.png.import similarity index 69% rename from assets/textures/terrain/editing/connected_off.png.import rename to addons/terrain-editing/icons/connected_off.png.import index 25cf970..58e1007 100644 --- a/assets/textures/terrain/editing/connected_off.png.import +++ b/addons/terrain-editing/icons/connected_off.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://c5j5v62f7p6qt" -path="res://.godot/imported/connected_off.png-9d4314286cb3dda8aeb51987f4722c68.ctex" +path="res://.godot/imported/connected_off.png-b005e48b1e78c2e4f166c1bb681cb9cd.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/connected_off.png" -dest_files=["res://.godot/imported/connected_off.png-9d4314286cb3dda8aeb51987f4722c68.ctex"] +source_file="res://addons/terrain-editing/icons/connected_off.png" +dest_files=["res://.godot/imported/connected_off.png-b005e48b1e78c2e4f166c1bb681cb9cd.ctex"] [params] diff --git a/assets/textures/terrain/editing/connected_on.png b/addons/terrain-editing/icons/connected_on.png similarity index 100% rename from assets/textures/terrain/editing/connected_on.png rename to addons/terrain-editing/icons/connected_on.png diff --git a/assets/textures/terrain/editing/connected_on.png.import b/addons/terrain-editing/icons/connected_on.png.import similarity index 69% rename from assets/textures/terrain/editing/connected_on.png.import rename to addons/terrain-editing/icons/connected_on.png.import index ae791fd..8282afe 100644 --- a/assets/textures/terrain/editing/connected_on.png.import +++ b/addons/terrain-editing/icons/connected_on.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dsbovbbrtuv8f" -path="res://.godot/imported/connected_on.png-d142e6ffef231eda75d9207321617fb4.ctex" +path="res://.godot/imported/connected_on.png-553b19507f8a6c35fd8746a903469e11.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/connected_on.png" -dest_files=["res://.godot/imported/connected_on.png-d142e6ffef231eda75d9207321617fb4.ctex"] +source_file="res://addons/terrain-editing/icons/connected_on.png" +dest_files=["res://.godot/imported/connected_on.png-553b19507f8a6c35fd8746a903469e11.ctex"] [params] diff --git a/assets/textures/terrain/editing/corner.png b/addons/terrain-editing/icons/corner.png similarity index 100% rename from assets/textures/terrain/editing/corner.png rename to addons/terrain-editing/icons/corner.png diff --git a/assets/textures/terrain/editing/corner.png.import b/addons/terrain-editing/icons/corner.png.import similarity index 71% rename from assets/textures/terrain/editing/corner.png.import rename to addons/terrain-editing/icons/corner.png.import index a290bc6..31f83c1 100644 --- a/assets/textures/terrain/editing/corner.png.import +++ b/addons/terrain-editing/icons/corner.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://btl3jsqeldix2" -path="res://.godot/imported/corner.png-b3e3cf9873b34b5d334d6e85884e6746.ctex" +path="res://.godot/imported/corner.png-c9f5b33637b5a9c1ee2a2ae62a163672.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/corner.png" -dest_files=["res://.godot/imported/corner.png-b3e3cf9873b34b5d334d6e85884e6746.ctex"] +source_file="res://addons/terrain-editing/icons/corner.png" +dest_files=["res://.godot/imported/corner.png-c9f5b33637b5a9c1ee2a2ae62a163672.ctex"] [params] diff --git a/assets/textures/terrain/editing/corner_paint.png b/addons/terrain-editing/icons/corner_paint.png similarity index 100% rename from assets/textures/terrain/editing/corner_paint.png rename to addons/terrain-editing/icons/corner_paint.png diff --git a/assets/textures/terrain/editing/corner_paint.png.import b/addons/terrain-editing/icons/corner_paint.png.import similarity index 69% rename from assets/textures/terrain/editing/corner_paint.png.import rename to addons/terrain-editing/icons/corner_paint.png.import index 94ca0dd..45b5ce4 100644 --- a/assets/textures/terrain/editing/corner_paint.png.import +++ b/addons/terrain-editing/icons/corner_paint.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dc0q2xn2cgcjw" -path="res://.godot/imported/corner_paint.png-d3f97054fffda3dc2896d8416d2182b9.ctex" +path="res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/corner_paint.png" -dest_files=["res://.godot/imported/corner_paint.png-d3f97054fffda3dc2896d8416d2182b9.ctex"] +source_file="res://addons/terrain-editing/icons/corner_paint.png" +dest_files=["res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex"] [params] diff --git a/assets/textures/terrain/editing/flatten.png b/addons/terrain-editing/icons/flatten.png similarity index 100% rename from assets/textures/terrain/editing/flatten.png rename to addons/terrain-editing/icons/flatten.png diff --git a/assets/textures/terrain/editing/flatten.png.import b/addons/terrain-editing/icons/flatten.png.import similarity index 70% rename from assets/textures/terrain/editing/flatten.png.import rename to addons/terrain-editing/icons/flatten.png.import index dc9a3f7..ae3a469 100644 --- a/assets/textures/terrain/editing/flatten.png.import +++ b/addons/terrain-editing/icons/flatten.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://bcb8w33ns56go" -path="res://.godot/imported/flatten.png-f2f312eef574983bc85e28cb99e262b2.ctex" +path="res://.godot/imported/flatten.png-763c4dd2a0a6c7164d57bc1cefac0f84.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/flatten.png" -dest_files=["res://.godot/imported/flatten.png-f2f312eef574983bc85e28cb99e262b2.ctex"] +source_file="res://addons/terrain-editing/icons/flatten.png" +dest_files=["res://.godot/imported/flatten.png-763c4dd2a0a6c7164d57bc1cefac0f84.ctex"] [params] diff --git a/assets/textures/terrain/editing/height.png b/addons/terrain-editing/icons/height.png similarity index 100% rename from assets/textures/terrain/editing/height.png rename to addons/terrain-editing/icons/height.png diff --git a/assets/textures/terrain/editing/height.png.import b/addons/terrain-editing/icons/height.png.import similarity index 71% rename from assets/textures/terrain/editing/height.png.import rename to addons/terrain-editing/icons/height.png.import index 23759ec..77450f4 100644 --- a/assets/textures/terrain/editing/height.png.import +++ b/addons/terrain-editing/icons/height.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dqbtbf8pe05qv" -path="res://.godot/imported/height.png-dfdcad891e41d3086ffca9947f565f65.ctex" +path="res://.godot/imported/height.png-a11588cb8bcd4e93c189a14ad0809338.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/height.png" -dest_files=["res://.godot/imported/height.png-dfdcad891e41d3086ffca9947f565f65.ctex"] +source_file="res://addons/terrain-editing/icons/height.png" +dest_files=["res://.godot/imported/height.png-a11588cb8bcd4e93c189a14ad0809338.ctex"] [params] diff --git a/assets/textures/terrain/editing/height_lower.png b/addons/terrain-editing/icons/height_lower.png similarity index 100% rename from assets/textures/terrain/editing/height_lower.png rename to addons/terrain-editing/icons/height_lower.png diff --git a/assets/textures/terrain/editing/height_lower.png.import b/addons/terrain-editing/icons/height_lower.png.import similarity index 69% rename from assets/textures/terrain/editing/height_lower.png.import rename to addons/terrain-editing/icons/height_lower.png.import index a5dde0a..72f37b6 100644 --- a/assets/textures/terrain/editing/height_lower.png.import +++ b/addons/terrain-editing/icons/height_lower.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://covyafauwthij" -path="res://.godot/imported/height_lower.png-d00d3c7062c0a355f5c8381ee7e664da.ctex" +path="res://.godot/imported/height_lower.png-25839775bfb1679719ee439c00f0fe1f.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/height_lower.png" -dest_files=["res://.godot/imported/height_lower.png-d00d3c7062c0a355f5c8381ee7e664da.ctex"] +source_file="res://addons/terrain-editing/icons/height_lower.png" +dest_files=["res://.godot/imported/height_lower.png-25839775bfb1679719ee439c00f0fe1f.ctex"] [params] diff --git a/assets/textures/terrain/editing/height_raise.png b/addons/terrain-editing/icons/height_raise.png similarity index 100% rename from assets/textures/terrain/editing/height_raise.png rename to addons/terrain-editing/icons/height_raise.png diff --git a/assets/textures/terrain/editing/height_raise.png.import b/addons/terrain-editing/icons/height_raise.png.import similarity index 69% rename from assets/textures/terrain/editing/height_raise.png.import rename to addons/terrain-editing/icons/height_raise.png.import index 0c86a68..c7c05e8 100644 --- a/assets/textures/terrain/editing/height_raise.png.import +++ b/addons/terrain-editing/icons/height_raise.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dxbfohim13ti1" -path="res://.godot/imported/height_raise.png-a5718ceb2e90ce17597f6300a0a9a715.ctex" +path="res://.godot/imported/height_raise.png-c4da2d9b5b635aa915c13f021df2ac3a.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/height_raise.png" -dest_files=["res://.godot/imported/height_raise.png-a5718ceb2e90ce17597f6300a0a9a715.ctex"] +source_file="res://addons/terrain-editing/icons/height_raise.png" +dest_files=["res://.godot/imported/height_raise.png-c4da2d9b5b635aa915c13f021df2ac3a.ctex"] [params] diff --git a/assets/textures/terrain/editing/paint.png b/addons/terrain-editing/icons/paint.png similarity index 100% rename from assets/textures/terrain/editing/paint.png rename to addons/terrain-editing/icons/paint.png diff --git a/assets/textures/terrain/editing/paint.png.import b/addons/terrain-editing/icons/paint.png.import similarity index 71% rename from assets/textures/terrain/editing/paint.png.import rename to addons/terrain-editing/icons/paint.png.import index 7b91225..4995e6e 100644 --- a/assets/textures/terrain/editing/paint.png.import +++ b/addons/terrain-editing/icons/paint.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://btdpyu4n3pgkx" -path="res://.godot/imported/paint.png-fd14d265b51ee1ac5604bd82d9114189.ctex" +path="res://.godot/imported/paint.png-7f2ce8187160ce862842aed3c583971f.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/paint.png" -dest_files=["res://.godot/imported/paint.png-fd14d265b51ee1ac5604bd82d9114189.ctex"] +source_file="res://addons/terrain-editing/icons/paint.png" +dest_files=["res://.godot/imported/paint.png-7f2ce8187160ce862842aed3c583971f.ctex"] [params] diff --git a/assets/textures/terrain/editing/square.png b/addons/terrain-editing/icons/square.png similarity index 100% rename from assets/textures/terrain/editing/square.png rename to addons/terrain-editing/icons/square.png diff --git a/assets/textures/terrain/editing/square.png.import b/addons/terrain-editing/icons/square.png.import similarity index 71% rename from assets/textures/terrain/editing/square.png.import rename to addons/terrain-editing/icons/square.png.import index 901dc0c..3119f2f 100644 --- a/assets/textures/terrain/editing/square.png.import +++ b/addons/terrain-editing/icons/square.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://btjd1704xtdjv" -path="res://.godot/imported/square.png-1bc8a7ae7e9504f37db6cb7269dd99e4.ctex" +path="res://.godot/imported/square.png-ee9f4ef6bc1f78130e7829ecfc4538ca.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/textures/terrain/editing/square.png" -dest_files=["res://.godot/imported/square.png-1bc8a7ae7e9504f37db6cb7269dd99e4.ctex"] +source_file="res://addons/terrain-editing/icons/square.png" +dest_files=["res://.godot/imported/square.png-ee9f4ef6bc1f78130e7829ecfc4538ca.ctex"] [params] diff --git a/addons/terrain-editing/modifier_toggle_button.gd b/addons/terrain-editing/modifier_toggle_button.gd new file mode 100644 index 0000000..2272325 --- /dev/null +++ b/addons/terrain-editing/modifier_toggle_button.gd @@ -0,0 +1,13 @@ +@tool +extends Button + +@export var on_texture : Texture2D +@export var off_texture : Texture2D +@export var modifier_key : Key + +func _toggled(on: bool) -> void: + icon = on_texture if on else off_texture + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.keycode == modifier_key: + button_pressed = !button_pressed diff --git a/addons/terrain-editing/terrain_editing_controls.gd b/addons/terrain-editing/terrain_editing_controls.gd new file mode 100644 index 0000000..6f093ee --- /dev/null +++ b/addons/terrain-editing/terrain_editing_controls.gd @@ -0,0 +1,89 @@ +@tool +class_name TerrainEditingControls +extends VBoxContainer + +enum ToolMode { HEIGHT, FLATTEN, PAINT } +enum ToolShape { CORNER, CIRCLE, SQUARE } + +@onready var tool_mode_buttons : Array[Button] = [ $Height, $Flatten, $Paint ] +@onready var tool_shape_buttons : Array[Button] = [ $Corner, $Circle, $Square ] +@onready var paint_texture_buttons : Array[Button] = [ $Grass, $Dirt, $Rock, $Sand ] + +@onready var draw_size_label : Label = $SizeLabel +@onready var draw_size_slider : Slider = $SizeSlider +@onready var raise_lower_toggle : Button = $RaiseLower +@onready var connected_toggle : Button = $Connected + +@onready var corner_texture_default : Texture2D = preload("icons/corner.png") +@onready var corner_texture_paint : Texture2D = preload("icons/corner_paint.png") + + +## Gets or sets the currently active tool mode (HEIGHT, FLATTEN, PAINT). +var tool_mode : ToolMode: + get: return index_of_pressed(tool_mode_buttons) + set(value): set_pressed(tool_mode_buttons, value); tool_mode_changed() +## Gets or sets the currently selected tool shape (CORNER, CIRCLE, SQUARE). +var tool_shape : ToolShape: + get: return index_of_pressed(tool_shape_buttons) + set(value): set_pressed(tool_shape_buttons, value); tool_shape_changed() +## Gets or sets the currently selected texture to paint with. +var texture : int: + get: return index_of_pressed(paint_texture_buttons) + set(value): set_pressed(paint_texture_buttons, value) +## Gets or sets the current draw size for CIRCLE or SQUARE shapes. +var draw_size : int: + get: return roundi(-draw_size_slider.value) + set(value): draw_size_slider.value = -value + +## Gets whether the raise/lower button is currently active. +var is_raise : bool: + get: return raise_lower_toggle.button_pressed +## Gets whether the raise/lower button is currently active. +var is_connected : bool: + get: return connected_toggle.button_pressed + + +func _ready() -> void: + # Update 'tool_mode', 'tool_shape' or 'texture' when any of the buttons are pressed. + for i in len(tool_mode_buttons ): tool_mode_buttons [i].pressed.connect(func(): tool_mode = i) + for i in len(tool_shape_buttons ): tool_shape_buttons [i].pressed.connect(func(): tool_shape = i) + for i in len(paint_texture_buttons): paint_texture_buttons[i].pressed.connect(func(): texture = i) + + # Update 'draw_size_label' whenever the slider changes. + draw_size_slider.value_changed.connect(func(_value): + draw_size_label.text = str(draw_size)) + + +func tool_mode_changed() -> void: + var is_height := (tool_mode == ToolMode.HEIGHT) + var is_paint := (tool_mode == ToolMode.PAINT) + + # In 'PAINT' mode, 'CORNER' affects a single tile regardless of 'draw_size'. + # This changes the button's icon to a small square to communicate that. + tool_shape_buttons[0].icon = corner_texture_paint if is_paint else corner_texture_default + + for button in paint_texture_buttons: button.disabled = !is_paint + raise_lower_toggle.disabled = !is_height + connected_toggle.disabled = is_paint + +func tool_shape_changed() -> void: + var is_corner := (tool_shape == ToolShape.CORNER) + draw_size_slider.editable = !is_corner; + + +## Returns the index of the first pressed (toggled on) button. +func index_of_pressed(buttons: Array[Button]) -> int: + for i in len(buttons): + var button := buttons[i] + if button.button_pressed: + return i + return 0 + +## Sets the pressed state (toggled on) of the button +## with the specified index, unsetting all others. +func set_pressed(buttons: Array[Button], value: int) -> void: + for i in len(buttons): + var button := buttons[i] + var is_pressed := (value == i) + button.button_pressed = is_pressed + button.flat = !is_pressed diff --git a/terrain/editing/TerrainEditingControls.tscn b/addons/terrain-editing/terrain_editing_controls.tscn similarity index 76% rename from terrain/editing/TerrainEditingControls.tscn rename to addons/terrain-editing/terrain_editing_controls.tscn index f4b938f..12a6eec 100644 --- a/terrain/editing/TerrainEditingControls.tscn +++ b/addons/terrain-editing/terrain_editing_controls.tscn @@ -1,18 +1,17 @@ -[gd_scene load_steps=18 format=3 uid="uid://bmljchm3fj42"] - -[ext_resource type="Script" path="res://terrain/editing/TerrainEditingControls.cs" id="1_fklx3"] -[ext_resource type="Script" path="res://terrain/editing/ModifierToggleButton.cs" id="9_7fwmx"] -[ext_resource type="Texture2D" uid="uid://dqbtbf8pe05qv" path="res://assets/textures/terrain/editing/height.png" id="2_hrmm4"] -[ext_resource type="Texture2D" uid="uid://bcb8w33ns56go" path="res://assets/textures/terrain/editing/flatten.png" id="3_gvr2t"] -[ext_resource type="Texture2D" uid="uid://btdpyu4n3pgkx" path="res://assets/textures/terrain/editing/paint.png" id="3_5x55r"] -[ext_resource type="Texture2D" uid="uid://btl3jsqeldix2" path="res://assets/textures/terrain/editing/corner.png" id="1_w5qr7"] -[ext_resource type="Texture2D" uid="uid://dc0q2xn2cgcjw" path="res://assets/textures/terrain/editing/corner_paint.png" id="3_e00xo"] -[ext_resource type="Texture2D" uid="uid://2u1ldmh0osbx" path="res://assets/textures/terrain/editing/circle.png" id="2_yvc34"] -[ext_resource type="Texture2D" uid="uid://btjd1704xtdjv" path="res://assets/textures/terrain/editing/square.png" id="3_aaaoe"] -[ext_resource type="Texture2D" uid="uid://dxbfohim13ti1" path="res://assets/textures/terrain/editing/height_raise.png" id="9_u4loi"] -[ext_resource type="Texture2D" uid="uid://covyafauwthij" path="res://assets/textures/terrain/editing/height_lower.png" id="10_owj33"] -[ext_resource type="Texture2D" uid="uid://dsbovbbrtuv8f" path="res://assets/textures/terrain/editing/connected_on.png" id="8_4qifu"] -[ext_resource type="Texture2D" uid="uid://c5j5v62f7p6qt" path="res://assets/textures/terrain/editing/connected_off.png" id="12_5hs2d"] +[gd_scene load_steps=17 format=3 uid="uid://bp0wulaxcrutd"] + +[ext_resource type="Script" path="res://addons/terrain-editing/terrain_editing_controls.gd" id="1_4e1sk"] +[ext_resource type="Script" path="res://addons/terrain-editing/modifier_toggle_button.gd" id="2_61bkl"] +[ext_resource type="Texture2D" uid="uid://dqbtbf8pe05qv" path="res://addons/terrain-editing/icons/height.png" id="3_mn5pg"] +[ext_resource type="Texture2D" uid="uid://bcb8w33ns56go" path="res://addons/terrain-editing/icons/flatten.png" id="4_c1bhf"] +[ext_resource type="Texture2D" uid="uid://btdpyu4n3pgkx" path="res://addons/terrain-editing/icons/paint.png" id="5_547tx"] +[ext_resource type="Texture2D" uid="uid://btl3jsqeldix2" path="res://addons/terrain-editing/icons/corner.png" id="6_fcc6v"] +[ext_resource type="Texture2D" uid="uid://2u1ldmh0osbx" path="res://addons/terrain-editing/icons/circle.png" id="7_b1ydi"] +[ext_resource type="Texture2D" uid="uid://btjd1704xtdjv" path="res://addons/terrain-editing/icons/square.png" id="8_w3t42"] +[ext_resource type="Texture2D" uid="uid://dxbfohim13ti1" path="res://addons/terrain-editing/icons/height_raise.png" id="9_ih748"] +[ext_resource type="Texture2D" uid="uid://dsbovbbrtuv8f" path="res://addons/terrain-editing/icons/connected_on.png" id="10_eb7qi"] +[ext_resource type="Texture2D" uid="uid://covyafauwthij" path="res://addons/terrain-editing/icons/height_lower.png" id="11_trhv6"] +[ext_resource type="Texture2D" uid="uid://c5j5v62f7p6qt" path="res://addons/terrain-editing/icons/connected_off.png" id="12_2esvb"] [ext_resource type="Image" uid="uid://b0jp1dyxugbr7" path="res://assets/textures/terrain/grass.png" id="Image_d41co"] [ext_resource type="Image" uid="uid://bpo7mkr6sctqr" path="res://assets/textures/terrain/dirt.png" id="Image_y3rra"] [ext_resource type="Image" uid="uid://dqyqg6yt7yk3k" path="res://assets/textures/terrain/rock.png" id="Image_x8cdn"] @@ -31,26 +30,24 @@ image = ExtResource("Image_x8cdn") image = ExtResource("Image_sb66e") [node name="TerrainEditingControls" type="VBoxContainer"] -script = ExtResource("1_fklx3") -CornerTextureNormal = ExtResource("1_w5qr7") -CornerTexturePaint = ExtResource("3_e00xo") +script = ExtResource("1_4e1sk") [node name="Height" type="Button" parent="."] layout_mode = 2 toggle_mode = true button_pressed = true -icon = ExtResource("2_hrmm4") +icon = ExtResource("3_mn5pg") [node name="Flatten" type="Button" parent="."] layout_mode = 2 toggle_mode = true -icon = ExtResource("3_gvr2t") +icon = ExtResource("4_c1bhf") flat = true [node name="Paint" type="Button" parent="."] layout_mode = 2 toggle_mode = true -icon = ExtResource("3_5x55r") +icon = ExtResource("5_547tx") flat = true [node name="HSeparator" type="HSeparator" parent="."] @@ -59,19 +56,19 @@ layout_mode = 2 [node name="Corner" type="Button" parent="."] layout_mode = 2 toggle_mode = true -icon = ExtResource("1_w5qr7") +icon = ExtResource("6_fcc6v") flat = true [node name="Circle" type="Button" parent="."] layout_mode = 2 toggle_mode = true button_pressed = true -icon = ExtResource("2_yvc34") +icon = ExtResource("7_b1ydi") [node name="Square" type="Button" parent="."] layout_mode = 2 toggle_mode = true -icon = ExtResource("3_aaaoe") +icon = ExtResource("8_w3t42") flat = true [node name="SizeLabel" type="Label" parent="."] @@ -95,21 +92,21 @@ layout_mode = 2 layout_mode = 2 toggle_mode = true button_pressed = true -icon = ExtResource("9_u4loi") -script = ExtResource("9_7fwmx") -OnTexture = ExtResource("9_u4loi") -OffTexture = ExtResource("10_owj33") -ModifierKey = 4194325 +icon = ExtResource("9_ih748") +script = ExtResource("2_61bkl") +on_texture = ExtResource("9_ih748") +off_texture = ExtResource("11_trhv6") +modifier_key = 4194325 [node name="Connected" type="Button" parent="."] layout_mode = 2 toggle_mode = true button_pressed = true -icon = ExtResource("8_4qifu") -script = ExtResource("9_7fwmx") -OnTexture = ExtResource("8_4qifu") -OffTexture = ExtResource("12_5hs2d") -ModifierKey = 4194326 +icon = ExtResource("10_eb7qi") +script = ExtResource("2_61bkl") +on_texture = ExtResource("10_eb7qi") +off_texture = ExtResource("12_2esvb") +modifier_key = 4194326 [node name="HSeparator3" type="HSeparator" parent="."] layout_mode = 2 diff --git a/addons/terrain-editing/terrain_editing_plugin.gd b/addons/terrain-editing/terrain_editing_plugin.gd index 979fe63..c09e5aa 100644 --- a/addons/terrain-editing/terrain_editing_plugin.gd +++ b/addons/terrain-editing/terrain_editing_plugin.gd @@ -2,25 +2,51 @@ extends EditorPlugin const CONTAINER := CustomControlContainer.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT +@onready var controls_scene := preload("terrain_editing_controls.tscn") +@onready var controls : TerrainEditingControls = controls_scene.instantiate() -var controls: Control = null; +var current_terrain : StaticBody3D func _handles(object: Object) -> bool: - var script := object.get_script() as CSharpScript - return script and script.resource_path.ends_with("/Terrain.cs") - -func _exit_tree() -> void: - if not controls: return - if controls.get_parent(): - remove_control_from_container(CONTAINER, controls) - controls.free() + return is_terrain(object) func _make_visible(visible: bool) -> void: if visible: - if not controls: - var controls_scene = load("res://terrain/editing/TerrainEditingControls.tscn") - controls = controls_scene.instantiate() - controls.EditorUndoRedo = get_undo_redo() add_control_to_container(CONTAINER, controls) elif controls and controls.get_parent(): remove_control_from_container(CONTAINER, controls) + +func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int: + if event is InputEventMouse: + var previous_terrain := current_terrain + current_terrain = null + + var viewport := EditorInterface.get_editor_viewport_3d() + if viewport.get_visible_rect().has_point(event.position): + var from := camera.project_ray_origin(event.position) + var to := from + camera.project_ray_normal(event.position) * camera.far + + var root := EditorInterface.get_edited_scene_root() as Node3D + var space := root.get_world_3d().direct_space_state + var query := PhysicsRayQueryParameters3D.create(from, to) + + var result := space.intersect_ray(query) + var terrain := result.get("collider") as StaticBody3D + if is_terrain(terrain): + # Allow terrain access to editor undo redo functionality. + terrain.EditorUndoRedo = get_undo_redo() + if terrain.EditorInput(event, result["position"], controls): + return AFTER_GUI_INPUT_STOP + + current_terrain = terrain + return AFTER_GUI_INPUT_PASS + + if previous_terrain and previous_terrain != current_terrain: + previous_terrain.EditorUnfocus() + + return AFTER_GUI_INPUT_PASS + +func is_terrain(object: Object) -> bool: + if not object: return false + var script := object.get_script() as CSharpScript + return script and script.resource_path.ends_with("/Terrain.cs") diff --git a/terrain/Terrain+Editing.cs b/terrain/Terrain+Editing.cs new file mode 100644 index 0000000..2dc6c47 --- /dev/null +++ b/terrain/Terrain+Editing.cs @@ -0,0 +1,245 @@ +using System.IO; + +public partial class Terrain +{ + enum ToolMode { Height, Flatten, Paint } + enum ToolShape { Corner, Circle, Square } + + // Set by the terrain editing plugin. + public EditorUndoRedoManager EditorUndoRedo { get; set; } + + Material _editToolMaterial; + public override void _EnterTree() + { + _editToolMaterial = new StandardMaterial3D { + AlbedoColor = Colors.Blue, + ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, + DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, + NoDepthTest = true, + }; + } + + public bool EditorInput(InputEventMouse ev, Vector3 position, Control controls) + { + var prevent_default = false; + + var toolMode = (ToolMode)(int)controls.Get("tool_mode"); + var toolShape = (ToolShape)(int)controls.Get("tool_shape"); + var texture = (int)controls.Get("texture"); + var drawSize = (int)controls.Get("draw_size"); + + var isRaise = (bool)controls.Get("is_raise"); + var isConnected = (bool)controls.Get("is_connected"); + var isFlatten = toolMode == ToolMode.Flatten; + var isCorner = toolShape == ToolShape.Corner; + + var hover = ToTilePos(position); + if (isCorner) drawSize = 1; + var isEven = (drawSize % 2) == 0; + var radius = FloorToInt(drawSize / 2.0f); + + // Offset hover tile position by corner. + if (isEven) hover.Position = hover.Corner switch { + Corner.TopLeft => hover.Position.Offset(0, 0), + Corner.TopRight => hover.Position.Offset(1, 0), + Corner.BottomRight => hover.Position.Offset(1, 1), + Corner.BottomLeft => hover.Position.Offset(0, 1), + _ => throw new InvalidOperationException(), + }; + + IEnumerable GetTilesInSquare() { + var min = hover.Position.Offset(-radius, -radius); + var max = hover.Position.Offset(+radius, +radius); + if (isEven) max = max.Offset(-1, -1); + for (var x = min.X; x <= max.X; x++) + for (var y = min.Y; y <= max.Y; y++) + yield return new(x, y); + } + + IEnumerable GetTilesInRadius() { + var center = isEven ? hover.Position.ToVector2I() + : hover.Position.ToCenter(); + var distanceSqr = Pow(radius + 0.25f * (isEven ? -1 : 1), 2); + return GetTilesInSquare().Where(tile => + center.DistanceSquaredTo(tile.ToCenter()) < distanceSqr); + } + + var tiles = (toolShape switch { + ToolShape.Corner => [ hover.Position ], + ToolShape.Circle => GetTilesInRadius(), + ToolShape.Square => GetTilesInSquare(), + _ => throw new InvalidOperationException(), + }).ToHashSet(); + + // TODO: Handle different tool modes, such as painting. + // TODO: Finally allow editing single corners. + // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. + // TODO: Make mesh generation generate vertical walls between disconnected corners. + // TODO: Use ArrayMesh instead of ImmediateMesh. + // TODO: Dynamically expand terrain instead of having it be a set size. + + // Raise / lower the terrain when left mouse button is pressed. + if (ev is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { + prevent_default = true; + + var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>(); + + // Raise selected tiles themselves. + foreach (var pos in tiles) + foreach (var corner2 in Enum.GetValues()) + cornersToChange.Add((pos, corner2)); + + if (isConnected) { + // If the 'connected_toggle' button is active, move "connected" corners. + // Connected corners are the ones that are at the same height as ones already being moved. + foreach (var pos in tiles) { + var tile = GetTile(pos); + foreach (var corner in Enum.GetValues()) { + var height = tile.Height[corner]; + foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner)) { + if (tiles.Contains(neighborPos)) continue; + var neighborHeight = GetTile(neighborPos).Height[neighborCorner]; + if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner)); + } + } + } + } + + const float AdjustHeight = 0.5f; + var amount = isFlatten ? GetTile(hover.Position).Height[hover.Corner] + : isRaise ? AdjustHeight : -AdjustHeight; + + var tilesPrevious = new List<(TilePos, Corners)>(); + var tilesChanged = new List<(TilePos, Corners)>(); + foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) { + var pos = group.Key; + var tile = GetTile(pos); + tilesPrevious.Add((pos, tile.Height)); + + var newHeight = tile.Height; + foreach (var corner in group) { + if (isFlatten) newHeight[corner] = amount; + else newHeight[corner] += amount; + } + tilesChanged.Add((pos, newHeight)); + } + + if (EditorUndoRedo is EditorUndoRedoManager undo) { + var name = "Modify terrain height"; // TODO: Change name depending on tool mode. + undo.CreateAction(name, backwardUndoOps: false); + + undo.AddDoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesChanged)); + undo.AddDoMethod(this, nameof(UpdateMeshAndShape)); + undo.AddDoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); + + undo.AddUndoMethod(this, nameof(DoModifyTerrainHeight), Pack(tilesPrevious)); + undo.AddUndoMethod(this, nameof(UpdateMeshAndShape)); + undo.AddUndoMethod(this, GodotObject.MethodName.NotifyPropertyListChanged); + + undo.CommitAction(true); + } + } + + UpdateEditToolMesh(tiles); + return prevent_default; + } + + public void EditorUnfocus() + => ClearEditToolMesh(); + + public void DoModifyTerrainHeight(byte[] data) + { + foreach (var (pos, corners) in Unpack(data)) { + var tile = GetTile(pos); + tile.Height = corners; + SetTile(pos, tile); + } + } + + + void UpdateEditToolMesh(IEnumerable tiles) + { + var mesh = GetOrCreateMesh("EditToolMesh"); + mesh.ClearSurfaces(); + mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); + + void AddLine(Vector3 start, Vector3 end) { + mesh.SurfaceAddVertex(start); + mesh.SurfaceAddVertex(end); + } + + void AddQuad(Vector3 topLeft , Vector3 topRight , + Vector3 bottomRight, Vector3 bottomLeft) { + AddLine(topLeft , topRight ); + AddLine(topRight , bottomRight); + AddLine(bottomRight, bottomLeft ); + AddLine(bottomLeft , topLeft ); + } + + foreach (var tile in tiles) { + var (topLeft, topRight, bottomRight, bottomLeft) + = GetTileCornerPositions(tile); + AddQuad(topLeft, topRight, bottomRight, bottomLeft); + } + + mesh.SurfaceEnd(); + mesh.SurfaceSetMaterial(0, _editToolMaterial); + } + + void ClearEditToolMesh() + => GetNodeOrNull("EditToolMesh")?.QueueFree(); + + + (TilePos Position, Corner Corner) ToTilePos(Vector3 position) + { + var local = ToLocal(position); + var coord = new Vector2(local.X, local.Z) / TileSize + (Size + Vector2.One) / 2; + var pos = TilePos.From(coord); + var corner = coord.PosMod(1).RoundToVector2I() switch { + (0, 0) => Corner.TopLeft, + (1, 0) => Corner.TopRight, + (1, 1) => Corner.BottomRight, + (0, 1) => Corner.BottomLeft, + _ => throw new InvalidOperationException(), + }; + return (pos, corner); + } + + static readonly Dictionary _offsetLookup = new(){ + [Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )], + [Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)], + [Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )], + [Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )], + }; + static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner) + => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); + + static byte[] Pack(IEnumerable<(TilePos Position, Corners Corners)> data) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + foreach (var (pos, corners) in data) { + writer.Write(pos.X); + writer.Write(pos.Y); + writer.Write(corners.TopLeft); + writer.Write(corners.TopRight); + writer.Write(corners.BottomRight); + writer.Write(corners.BottomLeft); + } + return stream.ToArray(); + } + + static IEnumerable<(TilePos Position, Corners Corners)> Unpack(byte[] data) + { + using var stream = new MemoryStream(data); + using var reader = new BinaryReader(stream); + while (stream.Position < stream.Length) { + var x = reader.ReadInt32(); + var y = reader.ReadInt32(); + var corners = new Corners( + reader.ReadSingle(), reader.ReadSingle(), + reader.ReadSingle(), reader.ReadSingle()); + yield return (new(x, y), corners); + } + } +} diff --git a/terrain/Tile.cs b/terrain/Tile.cs index 4523288..f2802a9 100644 --- a/terrain/Tile.cs +++ b/terrain/Tile.cs @@ -20,8 +20,13 @@ public readonly record struct TilePos(int X, int Y) _ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), }; + public TilePos Offset(Vector2I value) => Offset(value.X, value.Y); + public TilePos Offset(int x, int y) => new(X + x, Y + y); + public static TilePos From(Vector2I value) => new(value.X, value.Y); + public static TilePos From(Vector2 value) => new(FloorToInt(value.X), FloorToInt(value.Y)); public Vector2I ToVector2I() => new(X, Y); + public Vector2 ToCenter() => new(X + 0.5f, Y + 0.5f); } public struct Tile diff --git a/terrain/editing/ModifierToggleButton.cs b/terrain/editing/ModifierToggleButton.cs deleted file mode 100644 index 02b9380..0000000 --- a/terrain/editing/ModifierToggleButton.cs +++ /dev/null @@ -1,17 +0,0 @@ -[Tool] -public partial class ModifierToggleButton : Button -{ - [Export] public Texture2D OnTexture { get; set; } - [Export] public Texture2D OffTexture { get; set; } - [Export] public Key ModifierKey { get; set; } - - - public override void _Ready() - => Toggled += (on) => Icon = on ? OnTexture : OffTexture; - - public override void _Input(InputEvent ev) - { - if ((ev is InputEventKey { Keycode: var key }) && (key == ModifierKey)) - ButtonPressed = !ButtonPressed; - } -} diff --git a/terrain/editing/TerrainEditingControls+Editing.cs b/terrain/editing/TerrainEditingControls+Editing.cs deleted file mode 100644 index 204c34e..0000000 --- a/terrain/editing/TerrainEditingControls+Editing.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System.IO; - -public partial class TerrainEditingControls -{ - // Set by the terrain editing plugin. - public EditorUndoRedoManager EditorUndoRedo { get; set; } - - Terrain _currentTerrain = null; - - Material _editToolMaterial; - public override void _EnterTree() - { - _editToolMaterial = new StandardMaterial3D { - AlbedoColor = Colors.Blue, - ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded, - DepthDrawMode = BaseMaterial3D.DepthDrawModeEnum.Disabled, - NoDepthTest = true, - }; - } - - public override void _ExitTree() - => ClearEditToolMesh(); - - public override void _Input(InputEvent ev) - { - var viewport = !Engine.IsEditorHint() ? GetViewport() - : EditorInterface.Singleton.GetEditorViewport3D(); - - if (Engine.IsEditorHint()) { - // Make sure to transform the input event to the 3D scene's viewport. - var container = viewport.GetParent(); - ev = ev.XformedBy(container.GetGlobalTransform().AffineInverse()); - if (ev is InputEventMouse m) m.GlobalPosition = m.Position; - } - - if (ev is InputEventMouse mouse) { - if (viewport.GetVisibleRect().HasPoint(mouse.Position) - && (RayCastTerrain(mouse) is var (terrain, position))) { - var (tile, corner) = FindClosestTile(terrain, position); - - var drawSize = (ToolShape == ToolShape.Corner) ? 1 : DrawSize; - var isEven = (drawSize % 2) == 0; - var radius = FloorToInt(drawSize / 2.0f); - - // Offset tile position by corner. - if (isEven) tile = corner switch { - Corner.TopLeft => new(tile.X + 0, tile.Y + 0), - Corner.TopRight => new(tile.X + 1, tile.Y + 0), - Corner.BottomRight => new(tile.X + 1, tile.Y + 1), - Corner.BottomLeft => new(tile.X + 0, tile.Y + 1), - _ => throw new InvalidOperationException(), - }; - - IEnumerable GetTilesInSquare() { - var minX = tile.X - radius; - var minY = tile.Y - radius; - var maxX = tile.X + radius - (isEven ? 1 : 0); - var maxY = tile.Y + radius - (isEven ? 1 : 0); - for (var x = minX; x <= maxX; x++) - for (var y = minY; y <= maxY; y++) - yield return new(x, y); - } - - IEnumerable GetTilesInRadius() { - var center = isEven - ? new Vector2(tile.X , tile.Y ) - : new Vector2(tile.X + 0.5f, tile.Y + 0.5f); - var distanceSqr = Pow(isEven ? radius - 0.25f : radius + 0.25f, 2); - return GetTilesInSquare() - .Where(tile => center.DistanceSquaredTo( - new Vector2(tile.X + 0.5f, tile.Y + 0.5f)) < distanceSqr); - } - - var tiles = (ToolShape switch { - // TODO: Edit corner, not full tile. - ToolShape.Corner => [tile], - ToolShape.Circle => GetTilesInRadius(), - ToolShape.Square => GetTilesInSquare(), - _ => throw new InvalidOperationException(), - }).ToHashSet(); - - // TODO: Handle different tool modes, such as flatten and painting. - // TODO: Allow click-dragging which doesn't affect already changed tiles / corners. - // TODO: Make mesh generation generate vertical walls between disconnected corners. - // TODO: Use ArrayMesh instead of ImmediateMesh. - - // Raise / lower the terrain if left / right mouse button is pressed. - if (mouse is InputEventMouseButton { ButtonIndex: MouseButton.Left, Pressed: true }) { - GetViewport().SetInputAsHandled(); - - var cornersToChange = new HashSet<(TilePos Position, Corner Corner)>(); - - // Raise selected tiles themselves. - foreach (var pos in tiles) - foreach (var corner2 in Enum.GetValues()) - cornersToChange.Add((pos, corner2)); - - // If the Connected toggle button is active, move "connected" corners. - // Connected corners are the ones that are at the same height as ones already being moved. - if (ConnectedToggle.ButtonPressed) { - foreach (var pos in tiles) { - var tile2 = terrain.GetTile(pos); - foreach (var corner2 in Enum.GetValues()) { - var height = tile2.Height[corner2]; - foreach (var (neighborPos, neighborCorner) in GetNeighbors(pos, corner2)) { - if (tiles.Contains(neighborPos)) continue; - var neighborHeight = terrain.GetTile(neighborPos).Height[neighborCorner]; - if (neighborHeight == height) cornersToChange.Add((neighborPos, neighborCorner)); - } - } - } - } - - var isFlatten = ToolMode == ToolMode.Flatten; - var isRaise = RaiseLowerToggle.ButtonPressed; - - const float AdjustHeight = 0.5f; - var amount = isFlatten ? terrain.GetTile(tile).Height[corner] - : isRaise ? AdjustHeight : -AdjustHeight; - - var tilesPrevious = new List<(TilePos, Corners)>(); - var tilesChanged = new List<(TilePos, Corners)>(); - foreach (var group in cornersToChange.GroupBy(c => c.Position, c => c.Corner)) { - var pos = group.Key; - var tile2 = terrain.GetTile(pos); - tilesPrevious.Add((pos, tile2.Height)); - - var newHeight = tile2.Height; - foreach (var corner2 in group) { - if (isFlatten) newHeight[corner2] = amount; - else newHeight[corner2] += amount; - } - tilesChanged.Add((pos, newHeight)); - } - - if (EditorUndoRedo is EditorUndoRedoManager undo) { - var name = "Modify terrain"; // TODO: Change name depending on tool mode. - undo.CreateAction(name, customContext: terrain, backwardUndoOps: false); - - undo.AddDoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesChanged)); - undo.AddDoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged); - undo.AddDoMethod(terrain, nameof(Terrain.UpdateMeshAndShape)); - - undo.AddUndoMethod(this, nameof(TerrainModifyHeight), terrain, Pack(tilesPrevious)); - undo.AddUndoMethod(terrain, GodotObject.MethodName.NotifyPropertyListChanged); - undo.AddUndoMethod(terrain, nameof(Terrain.UpdateMeshAndShape)); - - undo.CommitAction(true); - } - } - - UpdateEditToolMesh(terrain, tiles); - } else { - ClearEditToolMesh(); - } - } - } - - public void TerrainModifyHeight(Terrain terrain, byte[] data) - { - foreach (var (pos, corners) in Unpack(data)) { - var tile = terrain.GetTile(pos); - tile.Height = corners; - terrain.SetTile(pos, tile); - } - } - - static byte[] Pack(IEnumerable<(TilePos Position, Corners Corners)> data) { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream); - foreach (var (pos, corners) in data) { - writer.Write(pos.X); - writer.Write(pos.Y); - writer.Write(corners.TopLeft); - writer.Write(corners.TopRight); - writer.Write(corners.BottomRight); - writer.Write(corners.BottomLeft); - } - return stream.ToArray(); - } - - static IEnumerable<(TilePos Position, Corners Corners)> Unpack(byte[] data) - { - using var stream = new MemoryStream(data); - using var reader = new BinaryReader(stream); - while (stream.Position < stream.Length) { - var x = reader.ReadInt32(); - var y = reader.ReadInt32(); - var corners = new Corners( - reader.ReadSingle(), reader.ReadSingle(), - reader.ReadSingle(), reader.ReadSingle()); - yield return (new(x, y), corners); - } - } - - void UpdateEditToolMesh(Terrain terrain, IEnumerable tiles) - { - if (terrain != _currentTerrain) ClearEditToolMesh(); - _currentTerrain = terrain; - - var mesh = terrain.GetOrCreateMesh("EditToolMesh"); - mesh.ClearSurfaces(); - mesh.SurfaceBegin(Mesh.PrimitiveType.Lines); - - void AddLine(Vector3 start, Vector3 end) { - mesh.SurfaceAddVertex(start); - mesh.SurfaceAddVertex(end); - } - - void AddQuad(Vector3 topLeft , Vector3 topRight, - Vector3 bottomRight, Vector3 bottomLeft) { - AddLine(topLeft , topRight ); - AddLine(topRight , bottomRight); - AddLine(bottomRight, bottomLeft ); - AddLine(bottomLeft , topLeft ); - } - - foreach (var tile in tiles) { - var (topLeft, topRight, bottomRight, bottomLeft) - = terrain.GetTileCornerPositions(tile); - AddQuad(topLeft, topRight, bottomRight, bottomLeft); - } - - mesh.SurfaceEnd(); - mesh.SurfaceSetMaterial(0, _editToolMaterial); - } - - void ClearEditToolMesh() - => _currentTerrain?.GetNodeOrNull("EditToolMesh")?.QueueFree(); - - (Terrain Terrain, Vector3 Position)? RayCastTerrain(InputEventMouse ev) - { - // Ray is cast from the editor camera's view. - var camera = EditorInterface.Singleton.GetEditorViewport3D().GetCamera3D(); - var from = camera.ProjectRayOrigin(ev.Position); - var to = from + camera.ProjectRayNormal(ev.Position) * camera.Far; - - // Actual collision is done in the edited scene though. - var root = (Node3D)EditorInterface.Singleton.GetEditedSceneRoot(); - var space = root.GetWorld3D().DirectSpaceState; - var query = PhysicsRayQueryParameters3D.Create(from, to); - - var result = space.IntersectRay(query); - var collider = (GodotObject)result.GetValueOrDefault("collider"); - return (collider is Terrain terrain) - ? (terrain, (Vector3)result["position"]) - : null; - } - - static (TilePos, Corner) FindClosestTile(Terrain terrain, Vector3 position) - { - var local = terrain.ToLocal(position); - - var tileX = local.X / terrain.TileSize + 0.5 + terrain.Size.X / 2; - var tileY = local.Z / terrain.TileSize + 0.5 + terrain.Size.Y / 2; - var tile = new TilePos(FloorToInt(tileX), FloorToInt(tileY)); - - var cornerX = RoundToInt(PosMod(tileX, 1)); - var cornerY = RoundToInt(PosMod(tileY, 1)); - var corner = (cornerX, cornerY) switch { - (0, 0) => Corner.TopLeft, - (1, 0) => Corner.TopRight, - (1, 1) => Corner.BottomRight, - (0, 1) => Corner.BottomLeft, - _ => throw new InvalidOperationException(), - }; - - return (tile, corner); - } - - static readonly Dictionary _offsetLookup = new(){ - [Corner.TopLeft ] = [(-1, -1, Corner.BottomRight), (-1, 0, Corner.TopRight ), (0, -1, Corner.BottomLeft )], - [Corner.TopRight ] = [(+1, -1, Corner.BottomLeft ), (+1, 0, Corner.TopLeft ), (0, -1, Corner.BottomRight)], - [Corner.BottomRight] = [(+1, +1, Corner.TopLeft ), (+1, 0, Corner.BottomLeft ), (0, +1, Corner.TopRight )], - [Corner.BottomLeft ] = [(-1, +1, Corner.TopRight ), (-1, 0, Corner.BottomRight), (0, +1, Corner.TopLeft )], - }; - static IEnumerable<(TilePos, Corner)> GetNeighbors(TilePos pos, Corner corner) - => _offsetLookup[corner].Select(e => (new TilePos(pos.X + e.X, pos.Y + e.Y), e.Opposite)); -} diff --git a/terrain/editing/TerrainEditingControls.cs b/terrain/editing/TerrainEditingControls.cs deleted file mode 100644 index d0f7b86..0000000 --- a/terrain/editing/TerrainEditingControls.cs +++ /dev/null @@ -1,118 +0,0 @@ -[Tool] -public partial class TerrainEditingControls - : VBoxContainer -{ - public (ToolMode , Button)[] ToolModeButtons { get; private set; } - public (ToolShape, Button)[] ToolShapeButtons { get; private set; } - public Slider DrawSizeSlider { get; private set; } - public Button[] PaintTextureButtons { get; private set; } - - public Button RaiseLowerToggle { get; private set; } - public Button ConnectedToggle { get; private set; } - - public ToolMode ToolMode { get => GetToolMode (); set => SetToolMode (value); } - public ToolShape ToolShape { get => GetToolShape(); set => SetToolShape(value); } - public int DrawSize { get => GetDrawSize (); set => SetDrawSize (value); } - public int Texture { get => GetTexture (); set => SetTexture (value); } - - [Export] public Texture2D CornerTextureNormal { get; set; } - [Export] public Texture2D CornerTexturePaint { get; set; } - - public override void _Ready() - { - ToolModeButtons = [ - (ToolMode.Height , GetNode