Fix "failed to load assemblies" once and for all!

- Moved more code into editor plugin (GDScript)
- Moved remaining code back to Terrain script (C#)
- Editing controls themselves are now fully GDScript
- Plugin (GDScript) just calls `Terrain.EditorInput` (C#)
- Made some more changes and additions where sensible
main
copygirl 2 months ago
parent aff3c22d22
commit c1ec14c41d
  1. 5
      GodotExtensions.cs
  2. 1
      PhysicsLayer.cs
  3. 0
      addons/terrain-editing/icons/circle.png
  4. 6
      addons/terrain-editing/icons/circle.png.import
  5. 0
      addons/terrain-editing/icons/connected_off.png
  6. 6
      addons/terrain-editing/icons/connected_off.png.import
  7. 0
      addons/terrain-editing/icons/connected_on.png
  8. 6
      addons/terrain-editing/icons/connected_on.png.import
  9. 0
      addons/terrain-editing/icons/corner.png
  10. 6
      addons/terrain-editing/icons/corner.png.import
  11. 0
      addons/terrain-editing/icons/corner_paint.png
  12. 6
      addons/terrain-editing/icons/corner_paint.png.import
  13. 0
      addons/terrain-editing/icons/flatten.png
  14. 6
      addons/terrain-editing/icons/flatten.png.import
  15. 0
      addons/terrain-editing/icons/height.png
  16. 6
      addons/terrain-editing/icons/height.png.import
  17. 0
      addons/terrain-editing/icons/height_lower.png
  18. 6
      addons/terrain-editing/icons/height_lower.png.import
  19. 0
      addons/terrain-editing/icons/height_raise.png
  20. 6
      addons/terrain-editing/icons/height_raise.png.import
  21. 0
      addons/terrain-editing/icons/paint.png
  22. 6
      addons/terrain-editing/icons/paint.png.import
  23. 0
      addons/terrain-editing/icons/square.png
  24. 6
      addons/terrain-editing/icons/square.png.import
  25. 13
      addons/terrain-editing/modifier_toggle_button.gd
  26. 89
      addons/terrain-editing/terrain_editing_controls.gd
  27. 65
      addons/terrain-editing/terrain_editing_controls.tscn
  28. 52
      addons/terrain-editing/terrain_editing_plugin.gd
  29. 245
      terrain/Terrain+Editing.cs
  30. 5
      terrain/Tile.cs
  31. 17
      terrain/editing/ModifierToggleButton.cs
  32. 279
      terrain/editing/TerrainEditingControls+Editing.cs
  33. 118
      terrain/editing/TerrainEditingControls.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) public static (Corner, Corner) GetCorners(this Side side)
=> side switch { => side switch {
Side.Left => (Corner.TopLeft, Corner.BottomLeft), Side.Left => (Corner.TopLeft, Corner.BottomLeft),

@ -1,3 +1,4 @@
[Flags]
public enum PhysicsLayer public enum PhysicsLayer
{ {
Terrain = 0b0000_0001, Terrain = 0b0000_0001,

Before

Width:  |  Height:  |  Size: 193 B

After

Width:  |  Height:  |  Size: 193 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://2u1ldmh0osbx" uid="uid://2u1ldmh0osbx"
path="res://.godot/imported/circle.png-480be724d6b688a76820398ea30097a4.ctex" path="res://.godot/imported/circle.png-7ef635c7ab3f0bfc0f9202fc8a8f1dd4.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/circle.png" source_file="res://addons/terrain-editing/icons/circle.png"
dest_files=["res://.godot/imported/circle.png-480be724d6b688a76820398ea30097a4.ctex"] dest_files=["res://.godot/imported/circle.png-7ef635c7ab3f0bfc0f9202fc8a8f1dd4.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 139 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://c5j5v62f7p6qt" uid="uid://c5j5v62f7p6qt"
path="res://.godot/imported/connected_off.png-9d4314286cb3dda8aeb51987f4722c68.ctex" path="res://.godot/imported/connected_off.png-b005e48b1e78c2e4f166c1bb681cb9cd.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/connected_off.png" source_file="res://addons/terrain-editing/icons/connected_off.png"
dest_files=["res://.godot/imported/connected_off.png-9d4314286cb3dda8aeb51987f4722c68.ctex"] dest_files=["res://.godot/imported/connected_off.png-b005e48b1e78c2e4f166c1bb681cb9cd.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 133 B

After

Width:  |  Height:  |  Size: 133 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dsbovbbrtuv8f" uid="uid://dsbovbbrtuv8f"
path="res://.godot/imported/connected_on.png-d142e6ffef231eda75d9207321617fb4.ctex" path="res://.godot/imported/connected_on.png-553b19507f8a6c35fd8746a903469e11.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/connected_on.png" source_file="res://addons/terrain-editing/icons/connected_on.png"
dest_files=["res://.godot/imported/connected_on.png-d142e6ffef231eda75d9207321617fb4.ctex"] dest_files=["res://.godot/imported/connected_on.png-553b19507f8a6c35fd8746a903469e11.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 129 B

After

Width:  |  Height:  |  Size: 129 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://btl3jsqeldix2" uid="uid://btl3jsqeldix2"
path="res://.godot/imported/corner.png-b3e3cf9873b34b5d334d6e85884e6746.ctex" path="res://.godot/imported/corner.png-c9f5b33637b5a9c1ee2a2ae62a163672.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/corner.png" source_file="res://addons/terrain-editing/icons/corner.png"
dest_files=["res://.godot/imported/corner.png-b3e3cf9873b34b5d334d6e85884e6746.ctex"] dest_files=["res://.godot/imported/corner.png-c9f5b33637b5a9c1ee2a2ae62a163672.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 124 B

After

Width:  |  Height:  |  Size: 124 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dc0q2xn2cgcjw" uid="uid://dc0q2xn2cgcjw"
path="res://.godot/imported/corner_paint.png-d3f97054fffda3dc2896d8416d2182b9.ctex" path="res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/corner_paint.png" source_file="res://addons/terrain-editing/icons/corner_paint.png"
dest_files=["res://.godot/imported/corner_paint.png-d3f97054fffda3dc2896d8416d2182b9.ctex"] dest_files=["res://.godot/imported/corner_paint.png-c66b764e062a869b0cd32b525c1718b2.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 113 B

After

Width:  |  Height:  |  Size: 113 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bcb8w33ns56go" uid="uid://bcb8w33ns56go"
path="res://.godot/imported/flatten.png-f2f312eef574983bc85e28cb99e262b2.ctex" path="res://.godot/imported/flatten.png-763c4dd2a0a6c7164d57bc1cefac0f84.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/flatten.png" source_file="res://addons/terrain-editing/icons/flatten.png"
dest_files=["res://.godot/imported/flatten.png-f2f312eef574983bc85e28cb99e262b2.ctex"] dest_files=["res://.godot/imported/flatten.png-763c4dd2a0a6c7164d57bc1cefac0f84.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 168 B

After

Width:  |  Height:  |  Size: 168 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dqbtbf8pe05qv" uid="uid://dqbtbf8pe05qv"
path="res://.godot/imported/height.png-dfdcad891e41d3086ffca9947f565f65.ctex" path="res://.godot/imported/height.png-a11588cb8bcd4e93c189a14ad0809338.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/height.png" source_file="res://addons/terrain-editing/icons/height.png"
dest_files=["res://.godot/imported/height.png-dfdcad891e41d3086ffca9947f565f65.ctex"] dest_files=["res://.godot/imported/height.png-a11588cb8bcd4e93c189a14ad0809338.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 135 B

After

Width:  |  Height:  |  Size: 135 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://covyafauwthij" uid="uid://covyafauwthij"
path="res://.godot/imported/height_lower.png-d00d3c7062c0a355f5c8381ee7e664da.ctex" path="res://.godot/imported/height_lower.png-25839775bfb1679719ee439c00f0fe1f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/height_lower.png" source_file="res://addons/terrain-editing/icons/height_lower.png"
dest_files=["res://.godot/imported/height_lower.png-d00d3c7062c0a355f5c8381ee7e664da.ctex"] dest_files=["res://.godot/imported/height_lower.png-25839775bfb1679719ee439c00f0fe1f.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 149 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dxbfohim13ti1" uid="uid://dxbfohim13ti1"
path="res://.godot/imported/height_raise.png-a5718ceb2e90ce17597f6300a0a9a715.ctex" path="res://.godot/imported/height_raise.png-c4da2d9b5b635aa915c13f021df2ac3a.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/height_raise.png" source_file="res://addons/terrain-editing/icons/height_raise.png"
dest_files=["res://.godot/imported/height_raise.png-a5718ceb2e90ce17597f6300a0a9a715.ctex"] dest_files=["res://.godot/imported/height_raise.png-c4da2d9b5b635aa915c13f021df2ac3a.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 169 B

After

Width:  |  Height:  |  Size: 169 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://btdpyu4n3pgkx" uid="uid://btdpyu4n3pgkx"
path="res://.godot/imported/paint.png-fd14d265b51ee1ac5604bd82d9114189.ctex" path="res://.godot/imported/paint.png-7f2ce8187160ce862842aed3c583971f.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/paint.png" source_file="res://addons/terrain-editing/icons/paint.png"
dest_files=["res://.godot/imported/paint.png-fd14d265b51ee1ac5604bd82d9114189.ctex"] dest_files=["res://.godot/imported/paint.png-7f2ce8187160ce862842aed3c583971f.ctex"]
[params] [params]

Before

Width:  |  Height:  |  Size: 147 B

After

Width:  |  Height:  |  Size: 147 B

@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://btjd1704xtdjv" uid="uid://btjd1704xtdjv"
path="res://.godot/imported/square.png-1bc8a7ae7e9504f37db6cb7269dd99e4.ctex" path="res://.godot/imported/square.png-ee9f4ef6bc1f78130e7829ecfc4538ca.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/textures/terrain/editing/square.png" source_file="res://addons/terrain-editing/icons/square.png"
dest_files=["res://.godot/imported/square.png-1bc8a7ae7e9504f37db6cb7269dd99e4.ctex"] dest_files=["res://.godot/imported/square.png-ee9f4ef6bc1f78130e7829ecfc4538ca.ctex"]
[params] [params]

@ -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

@ -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

@ -1,18 +1,17 @@
[gd_scene load_steps=18 format=3 uid="uid://bmljchm3fj42"] [gd_scene load_steps=17 format=3 uid="uid://bp0wulaxcrutd"]
[ext_resource type="Script" path="res://terrain/editing/TerrainEditingControls.cs" id="1_fklx3"] [ext_resource type="Script" path="res://addons/terrain-editing/terrain_editing_controls.gd" id="1_4e1sk"]
[ext_resource type="Script" path="res://terrain/editing/ModifierToggleButton.cs" id="9_7fwmx"] [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://assets/textures/terrain/editing/height.png" id="2_hrmm4"] [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://assets/textures/terrain/editing/flatten.png" id="3_gvr2t"] [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://assets/textures/terrain/editing/paint.png" id="3_5x55r"] [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://assets/textures/terrain/editing/corner.png" id="1_w5qr7"] [ext_resource type="Texture2D" uid="uid://btl3jsqeldix2" path="res://addons/terrain-editing/icons/corner.png" id="6_fcc6v"]
[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://addons/terrain-editing/icons/circle.png" id="7_b1ydi"]
[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://addons/terrain-editing/icons/square.png" id="8_w3t42"]
[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://addons/terrain-editing/icons/height_raise.png" id="9_ih748"]
[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://dsbovbbrtuv8f" path="res://addons/terrain-editing/icons/connected_on.png" id="10_eb7qi"]
[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://covyafauwthij" path="res://addons/terrain-editing/icons/height_lower.png" id="11_trhv6"]
[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://addons/terrain-editing/icons/connected_off.png" id="12_2esvb"]
[ext_resource type="Texture2D" uid="uid://c5j5v62f7p6qt" path="res://assets/textures/terrain/editing/connected_off.png" id="12_5hs2d"]
[ext_resource type="Image" uid="uid://b0jp1dyxugbr7" path="res://assets/textures/terrain/grass.png" id="Image_d41co"] [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://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"] [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") image = ExtResource("Image_sb66e")
[node name="TerrainEditingControls" type="VBoxContainer"] [node name="TerrainEditingControls" type="VBoxContainer"]
script = ExtResource("1_fklx3") script = ExtResource("1_4e1sk")
CornerTextureNormal = ExtResource("1_w5qr7")
CornerTexturePaint = ExtResource("3_e00xo")
[node name="Height" type="Button" parent="."] [node name="Height" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
button_pressed = true button_pressed = true
icon = ExtResource("2_hrmm4") icon = ExtResource("3_mn5pg")
[node name="Flatten" type="Button" parent="."] [node name="Flatten" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
icon = ExtResource("3_gvr2t") icon = ExtResource("4_c1bhf")
flat = true flat = true
[node name="Paint" type="Button" parent="."] [node name="Paint" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
icon = ExtResource("3_5x55r") icon = ExtResource("5_547tx")
flat = true flat = true
[node name="HSeparator" type="HSeparator" parent="."] [node name="HSeparator" type="HSeparator" parent="."]
@ -59,19 +56,19 @@ layout_mode = 2
[node name="Corner" type="Button" parent="."] [node name="Corner" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
icon = ExtResource("1_w5qr7") icon = ExtResource("6_fcc6v")
flat = true flat = true
[node name="Circle" type="Button" parent="."] [node name="Circle" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
button_pressed = true button_pressed = true
icon = ExtResource("2_yvc34") icon = ExtResource("7_b1ydi")
[node name="Square" type="Button" parent="."] [node name="Square" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
icon = ExtResource("3_aaaoe") icon = ExtResource("8_w3t42")
flat = true flat = true
[node name="SizeLabel" type="Label" parent="."] [node name="SizeLabel" type="Label" parent="."]
@ -95,21 +92,21 @@ layout_mode = 2
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
button_pressed = true button_pressed = true
icon = ExtResource("9_u4loi") icon = ExtResource("9_ih748")
script = ExtResource("9_7fwmx") script = ExtResource("2_61bkl")
OnTexture = ExtResource("9_u4loi") on_texture = ExtResource("9_ih748")
OffTexture = ExtResource("10_owj33") off_texture = ExtResource("11_trhv6")
ModifierKey = 4194325 modifier_key = 4194325
[node name="Connected" type="Button" parent="."] [node name="Connected" type="Button" parent="."]
layout_mode = 2 layout_mode = 2
toggle_mode = true toggle_mode = true
button_pressed = true button_pressed = true
icon = ExtResource("8_4qifu") icon = ExtResource("10_eb7qi")
script = ExtResource("9_7fwmx") script = ExtResource("2_61bkl")
OnTexture = ExtResource("8_4qifu") on_texture = ExtResource("10_eb7qi")
OffTexture = ExtResource("12_5hs2d") off_texture = ExtResource("12_2esvb")
ModifierKey = 4194326 modifier_key = 4194326
[node name="HSeparator3" type="HSeparator" parent="."] [node name="HSeparator3" type="HSeparator" parent="."]
layout_mode = 2 layout_mode = 2

@ -2,25 +2,51 @@
extends EditorPlugin extends EditorPlugin
const CONTAINER := CustomControlContainer.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT 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: func _handles(object: Object) -> bool:
var script := object.get_script() as CSharpScript return is_terrain(object)
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()
func _make_visible(visible: bool) -> void: func _make_visible(visible: bool) -> void:
if visible: 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) add_control_to_container(CONTAINER, controls)
elif controls and controls.get_parent(): elif controls and controls.get_parent():
remove_control_from_container(CONTAINER, controls) 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")

@ -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<TilePos> 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<TilePos> 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<Corner>())
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<Corner>()) {
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<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
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<TilePos> 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<Corner, (int X, int Y, Corner Opposite)[]> _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<float> 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<float> 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<float>(
reader.ReadSingle(), reader.ReadSingle(),
reader.ReadSingle(), reader.ReadSingle());
yield return (new(x, y), corners);
}
}
}

@ -20,8 +20,13 @@ public readonly record struct TilePos(int X, int Y)
_ => throw new ArgumentException($"Invalid Corner value '{corner}'", nameof(corner)), _ => 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(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 Vector2I ToVector2I() => new(X, Y);
public Vector2 ToCenter() => new(X + 0.5f, Y + 0.5f);
} }
public struct Tile public struct Tile

@ -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;
}
}

@ -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<SubViewportContainer>();
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<TilePos> 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<TilePos> 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<Corner>())
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<Corner>()) {
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<float>)>();
var tilesChanged = new List<(TilePos, Corners<float>)>();
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<float> 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<float> 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<float>(
reader.ReadSingle(), reader.ReadSingle(),
reader.ReadSingle(), reader.ReadSingle());
yield return (new(x, y), corners);
}
}
void UpdateEditToolMesh(Terrain terrain, IEnumerable<TilePos> 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<Corner, (int X, int Y, Corner Opposite)[]> _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));
}

@ -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<Button>("Height" )),
(ToolMode.Flatten, GetNode<Button>("Flatten")),
(ToolMode.Paint , GetNode<Button>("Paint" )),
];
ToolShapeButtons = [
(ToolShape.Corner, GetNode<Button>("Corner")),
(ToolShape.Circle, GetNode<Button>("Circle")),
(ToolShape.Square, GetNode<Button>("Square")),
];
PaintTextureButtons = [
GetNode<Button>("Grass"),
GetNode<Button>("Dirt"),
GetNode<Button>("Rock"),
GetNode<Button>("Sand"),
];
foreach (var (mode, button) in ToolModeButtons)
button.Pressed += () => SetToolMode(mode);
foreach (var (shape, button) in ToolShapeButtons)
button.Pressed += () => SetToolShape(shape);
foreach (var (i, button) in PaintTextureButtons.Select((b, i) => (i, b)))
button.Pressed += () => SetTexture(i + 1);
var drawSizeLabel = GetNode<Label> ("SizeLabel");
DrawSizeSlider = GetNode<Slider>("SizeSlider");
DrawSizeSlider.ValueChanged += (_) => drawSizeLabel.Text = $"{DrawSize}";
RaiseLowerToggle = GetNode<Button>("RaiseLower");
ConnectedToggle = GetNode<Button>("Connected");
}
ToolMode GetToolMode()
=> ToolModeButtons?.First(x => x.Item2.ButtonPressed).Item1 ?? ToolMode.Height;
void SetToolMode(ToolMode value)
{
if (ToolModeButtons == null) return;
foreach (var (mode, button) in ToolModeButtons)
button.Flat = !(button.ButtonPressed = value == mode);
foreach (var button in PaintTextureButtons)
button.Disabled = value != ToolMode.Paint;
// In paint mode, `ToolShape.Corner` affects a single tile regardless
// of `DrawSize`. This changes the button's icon to communicate that.
ToolShapeButtons[0].Item2.Icon = (value != ToolMode.Paint)
? CornerTextureNormal : CornerTexturePaint;
RaiseLowerToggle.Disabled = value is ToolMode.Flatten or ToolMode.Paint;
ConnectedToggle.Disabled = value is ToolMode.Paint;
}
ToolShape GetToolShape()
=> ToolShapeButtons?.First(x => x.Item2.ButtonPressed).Item1 ?? ToolShape.Circle;
void SetToolShape(ToolShape value)
{
if (ToolShapeButtons == null) return;
foreach (var (shape, button) in ToolShapeButtons)
button.Flat = !(button.ButtonPressed = value == shape);
DrawSizeSlider.Editable = value != ToolShape.Corner;
}
int GetDrawSize()
=> RoundToInt(-DrawSizeSlider?.Value ?? 1);
void SetDrawSize(int value)
{
if (DrawSizeSlider == null) return;
DrawSizeSlider.Value = -value;
}
int GetTexture()
=> PaintTextureButtons?
.Select((b, i) => (Index: i + 1, Button: b))
.First(x => x.Button.ButtonPressed).Index ?? 1;
void SetTexture(int value)
{
if (PaintTextureButtons == null) return;
if ((value < 1) || (value > PaintTextureButtons.Length))
throw new ArgumentOutOfRangeException(nameof(value));
foreach (var (index, button) in PaintTextureButtons.Select((b, i) => (i, b)))
button.Flat = !(button.ButtonPressed = value == index + 1);
}
}
public enum ToolMode
{
Height,
Flatten,
Paint,
}
public enum ToolShape
{
Corner,
Circle,
Square,
}
Loading…
Cancel
Save