MagicNStuff/source/addons/boxconstructor/boxconstructor.gd

1157 lines
36 KiB
GDScript

@tool
extends EditorPlugin
# === Constants ===
enum BuildMode {
DISABLE,
SELECT,
ADD
}
const GRID_SCALE_1 = 0.01
const GRID_SCALE_2 = 0.1
const GRID_SCALE_3 = 0.25
const GRID_SCALE_4 = 0.5
const GRID_SCALE_5 = 0.75
const GRID_SCALE_6 = 1
const GRID_SCALE_7 = 2
const GRID_SCALE_8 = 5
const GRID_SCALE_9 = 10
const BASE_PREVIEW_THICKNESS = 0.05
# === Editor properties ===
var current_mode: BuildMode = BuildMode.SELECT
var toolbar: PanelContainer
var editor_viewport = EditorInterface.get_editor_viewport_3d()
var camera = editor_viewport.get_camera_3d()
# === Grid and CSG properties ===
var csg_root: CSGCombiner3D
var selected_grid: CubeGrid3D
var csg_mesh: MeshInstance3D = null
# === Rectangle drawing properties ===
var is_drawing: bool = false
var draw_normal: Vector3 = Vector3.UP
var draw_start: Vector3 = Vector3()
var draw_end: Vector3 = Vector3()
var draw_preview: MeshInstance3D = null
var draw_plane: Plane
var base_rect_points: Array = []
# === Extrusion properties ===
var is_extruding: bool = false
var has_started_extrusion: bool = false
var extrude_distance: float = 0.0
var initial_extrude_point: Vector3
var extrude_line_normal: Vector3
# === Highlight properties ===
var hover_preview: MeshInstance3D = null
var hover_point: Vector3 = Vector3.ZERO
# === Edge Movement properties ===
var edge_preview: MeshInstance3D = null
var current_edge: Array = []
var is_dragging_edge: bool = false
var dragged_mesh: CSGMesh3D = null
var drag_start_position: Vector3
var drag_plane: Plane
var drag_start_offset: Vector3
var is_mouse_in_viewport: bool = false
var undo_redo
var drag_start_vertices: PackedVector3Array
var drag_start_indices: PackedInt32Array
func _update_mesh_arrays(mesh_node: CSGMesh3D, vertices: PackedVector3Array, indices: PackedInt32Array) -> void:
var arr_mesh = mesh_node.mesh as ArrayMesh
if arr_mesh:
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_INDEX] = indices
arr_mesh.clear_surfaces()
arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
# === Lifecycle Methods ===
func _enter_tree() -> void:
EditorInterface.get_selection().selection_changed.connect(_on_selection_changed)
editor_viewport = EditorInterface.get_editor_viewport_3d()
undo_redo = get_undo_redo()
toolbar = preload("res://addons/boxconstructor/scripts/toolbar.gd").new(self) # Create the toolbar
var viewport_base = editor_viewport.get_parent().get_parent()
viewport_base.add_child(toolbar)
toolbar.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP, 0, 10)
toolbar.hide() # Hide toolbar by default
_connect_toolbar_signals() # Connect signals
func _exit_tree() -> void:
if toolbar:
toolbar.queue_free() # Remove the toolbar
if EditorInterface.get_selection().selection_changed.is_connected(_on_selection_changed):
EditorInterface.get_selection().selection_changed.disconnect(_on_selection_changed) # Disconnect signals
# This section handles all of the inputs
func _input(event: InputEvent) -> void:
if not selected_grid or not selected_grid.is_inside_tree():
return
# Pressing the X key will move the grid to mouse position
if event is InputEventKey and event.pressed and event.keycode == KEY_X:
if not camera or not selected_grid:
return
# Cast a ray and get the hit position
var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
var to = from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 5000
var ray_query = PhysicsRayQueryParameters3D.new()
ray_query.from = from
ray_query.to = to
var hit = EditorInterface.get_edited_scene_root().get_world_3d().direct_space_state.intersect_ray(ray_query)
if hit:
var snapped_pos = _snap_to_grid(hit.position)
# Move the Grid to the hit position
_align_grid_to_surface(hit.normal, snapped_pos)
# Pressing Z resets the grid to the 0,0,0 position
if event is InputEventKey and event.pressed and event.keycode == KEY_Z:
_reset_grid_transform()
# Edge movement logic
if current_mode == BuildMode.SELECT:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if not is_dragging_edge:
if edge_preview and edge_preview.visible:
for child in csg_root.get_children():
if child is CSGBox3D or child is CSGMesh3D:
# Get all of the edges of CSGBox3D or CSGMesh3D
var edges = _get_edges(child)
for edge in edges:
# Check if the currently hovered edge is the same as the one we are dragging
if edge[0].is_equal_approx(current_edge[0]) and edge[1].is_equal_approx(current_edge[1]):
var from = camera.project_ray_origin(event.position)
var dir = camera.project_ray_normal(event.position)
# Create a plane for the edge to drag along
var edge_dir = (edge[1] - edge[0]).normalized()
drag_plane = Plane(edge_dir, edge[0].dot(edge_dir))
# Get the intersection point of the ray and the plane
var intersection = drag_plane.intersects_ray(from, dir)
if intersection:
# We turn the CSGBox3D into a custom mesh that allows use to move the vertecies
if child is CSGBox3D:
dragged_mesh = _convert_box_to_CSGMesh(child)
else:
dragged_mesh = child
var arr_mesh = dragged_mesh.mesh as ArrayMesh
if arr_mesh:
var arrays = arr_mesh.surface_get_arrays(0)
drag_start_vertices = arrays[Mesh.ARRAY_VERTEX].duplicate()
drag_start_indices = arrays[Mesh.ARRAY_INDEX].duplicate()
is_dragging_edge = true # Set dragging to true
current_edge = edge # Set the current edge to the one we are dragging
drag_start_offset = _snap_to_grid(intersection) # Starting position of edge drag
break
else:
if is_dragging_edge and dragged_mesh:
var arr_mesh = dragged_mesh.mesh as ArrayMesh
if arr_mesh:
var final_arrays = arr_mesh.surface_get_arrays(0)
undo_redo.create_action("Move Edge")
# Store the current state
var current_mesh = arr_mesh.duplicate(true)
# Create original mesh
var original_mesh = ArrayMesh.new()
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = drag_start_vertices
arrays[Mesh.ARRAY_INDEX] = drag_start_indices
original_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
undo_redo.add_do_property(dragged_mesh, "mesh", current_mesh)
undo_redo.add_undo_property(dragged_mesh, "mesh", original_mesh)
undo_redo.commit_action()
is_dragging_edge = false
dragged_mesh = null
edge_preview.hide()
if event is InputEventMouseMotion:
if is_dragging_edge and dragged_mesh:
edge_preview.hide()
var from = camera.project_ray_origin(event.position)
var dir = camera.project_ray_normal(event.position)
# Project mouse position onto drag plane
var intersection = drag_plane.intersects_ray(from, dir)
if intersection:
# Snapped position on the grid
var snapped_pos = _snap_to_grid(intersection)
# Offset from the start position
var offset = snapped_pos - drag_start_offset
var arr_mesh = dragged_mesh.mesh as ArrayMesh
if arr_mesh:
var arrays = arr_mesh.surface_get_arrays(0)
var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
# Get the edge points in local space
var local_edge = [
dragged_mesh.to_local(current_edge[0]),
dragged_mesh.to_local(current_edge[1])
]
# Calculate the new offset
var local_offset = dragged_mesh.global_transform.basis.inverse() * offset
var new_vertices = PackedVector3Array()
new_vertices.resize(vertices.size())
# Move vertices that match the edge points
for i in range(vertices.size()):
var vertex = vertices[i]
if vertex.is_equal_approx(local_edge[0]) or vertex.is_equal_approx(local_edge[1]):
new_vertices[i] = vertex + local_offset
else:
new_vertices[i] = vertex
# Update the mesh
var new_arrays = []
new_arrays.resize(Mesh.ARRAY_MAX)
new_arrays[Mesh.ARRAY_VERTEX] = new_vertices
new_arrays[Mesh.ARRAY_INDEX] = arrays[Mesh.ARRAY_INDEX]
arr_mesh.clear_surfaces()
arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, new_arrays)
current_edge = [
dragged_mesh.global_transform * (local_edge[0] + local_offset),
dragged_mesh.global_transform * (local_edge[1] + local_offset)
]
drag_start_offset = snapped_pos
# Highlight the closest edge
elif not is_dragging_edge:
if not camera or not csg_root:
if edge_preview:
edge_preview.hide()
return
# Get the closest node
var mouse_pos = editor_viewport.get_mouse_position()
var closest_node = null
var closest_distance = INF
for child in csg_root.get_children():
if not (child is CSGBox3D or child is CSGMesh3D):
continue
var node_center = child.global_position
var screen_pos = camera.unproject_position(node_center) # Takes the position in 3D converts it to 2D
var distance = screen_pos.distance_to(mouse_pos)
if distance < closest_distance:
closest_node = child
closest_distance = distance
if closest_distance:
var closest_edge = _find_closest_edge(closest_node, mouse_pos)
current_edge = closest_edge
_create_edge_preview(closest_edge)
else:
current_edge = []
if edge_preview:
edge_preview.hide()
# Handle Rectangle Drawing and Extrusion Logic
if current_mode == BuildMode.ADD:
if event is InputEventMouseButton:
# If clicked inside toolbar ignore
if toolbar and toolbar.get_global_rect().has_point(event.position):
return
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
if not camera: return
# Extruson end
if is_extruding and has_started_extrusion:
# Create the box when clicking during extrusion
_create_CSGBox3D()
is_drawing = false
is_extruding = false
has_started_extrusion = false
if draw_preview:
draw_preview.queue_free()
draw_preview = null
return
# Start dragging the base rectangle
var ray_query = PhysicsRayQueryParameters3D.new()
ray_query.from = camera.project_ray_origin(editor_viewport.get_mouse_position())
ray_query.to = ray_query.from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 1000
var hit = EditorInterface.get_editor_viewport_3d(0).find_world_3d().direct_space_state.intersect_ray(ray_query)
if hit:
is_drawing = true
draw_normal = hit.normal
draw_start = _snap_to_grid(hit.position)
draw_end = draw_start
draw_plane = Plane(draw_normal, hit.position.dot(draw_normal))
create_rectangle_preview()
else:
# End dragging and start extrusion if we were drawing
if is_drawing and not is_extruding:
is_extruding = true
has_started_extrusion = false
extrude_distance = 0.0
var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
var intersection = draw_plane.intersects_ray(from, dir)
if intersection:
initial_extrude_point = _snap_to_grid(intersection)
extrude_line_normal = draw_normal
_update_rectangle_preview()
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
is_drawing = false
is_extruding = false
has_started_extrusion = false
if draw_preview:
draw_preview.queue_free()
draw_preview = null
get_viewport().set_input_as_handled()
# Update section
elif event is InputEventMouseMotion:
if current_mode == BuildMode.ADD:
if not is_drawing:
if not camera: return
var ray_query = PhysicsRayQueryParameters3D.new()
ray_query.from = camera.project_ray_origin(editor_viewport.get_mouse_position())
ray_query.to = ray_query.from + camera.project_ray_normal(editor_viewport.get_mouse_position()) * 1000
ray_query.collide_with_bodies = true
var hit = EditorInterface.get_editor_viewport_3d(0).find_world_3d().direct_space_state.intersect_ray(ray_query)
if hit:
hover_point = _snap_to_grid(hit.position)
if not hover_preview:
_create_hover_preview()
_update_hover_preview()
elif hover_preview:
hover_preview.queue_free()
if is_drawing and not is_extruding:
if not camera: return
var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
var intersection = draw_plane.intersects_ray(from, dir)
if intersection:
draw_end = _snap_to_grid(intersection)
_calculate_base_rect_points()
_update_rectangle_preview()
elif is_extruding:
if not camera: return
var from = camera.project_ray_origin(editor_viewport.get_mouse_position())
var dir = camera.project_ray_normal(editor_viewport.get_mouse_position())
if not has_started_extrusion:
if event.relative.length() > 0.01:
has_started_extrusion = true
if has_started_extrusion:
var grid_unit = selected_grid.grid_scale
var e_line1 = initial_extrude_point - extrude_line_normal * 5000
var e_line2 = initial_extrude_point + extrude_line_normal * 5000
var m_line1 = from
var m_line2 = from + dir * 5000
var closest_point = Geometry3D.get_closest_points_between_segments(e_line1, e_line2, m_line1, m_line2)
var mouse_on_exturde_line = closest_point[0]
var distance_vec = mouse_on_exturde_line - initial_extrude_point
var distance = distance_vec.length() * distance_vec.normalized().dot(extrude_line_normal)
var new_distance = round(distance / grid_unit) * grid_unit
#print(new_distance)
if not is_equal_approx(extrude_distance, new_distance):
extrude_distance = new_distance
_update_rectangle_preview()
# === Grid Methods ===
# Changes the grid size based on the input from the toolbar
func _on_grid_size_changed(size: float) -> void:
if selected_grid and selected_grid.grid_material:
selected_grid.grid_scale = size
selected_grid.grid_material.set_shader_parameter("grid_scale", size)
# Destroy the hover preview so it gets updated
if hover_preview:
hover_preview.queue_free()
hover_preview = null
# Snaps the position to the grid size
func _snap_to_grid(pos: Vector3) -> Vector3:
if not selected_grid:
return pos
# Get the grid size of the selected grid
var grid_unit = selected_grid.grid_scale
# Divide the coordinates of the given position by the grid scale, to get how far it is from the origin
# and round it to the nearest integer
return Vector3(
round(pos.x / grid_unit) * grid_unit,
round(pos.y / grid_unit) * grid_unit,
round(pos.z / grid_unit) * grid_unit
)
func _align_grid_to_surface(normal: Vector3, hit_position: Vector3) -> void:
if not selected_grid:
return
var mesh = selected_grid.get_node_or_null("CubeGridMesh3D")
var collision = selected_grid.get_node_or_null("CubeGridCollisionShape3D")
var mesh_size = mesh.scale
var collision_size = collision.scale
if not mesh or not collision:
return
# Normalize the normal vector
normal = normal.normalized()
var up = normal
var right = up.cross(Vector3.FORWARD).normalized()
if right.length() < 0.1:
right = up.cross(Vector3.RIGHT).normalized()
var forward = right.cross(up)
# Create the Basis and Transform3D
var basis = Basis(right, up, forward)
var transform = Transform3D(basis, hit_position + up * 0.01)
# Apply the transform
mesh.transform = transform
collision.transform = transform
# Restore correct scale
mesh.scale = mesh_size
collision.scale = collision_size
# Update the material
selected_grid._update_material()
func _reset_grid_transform() -> void:
if not selected_grid:
return
var mesh = selected_grid.get_node_or_null("CubeGridMesh3D")
var collision = selected_grid.get_node_or_null("CubeGridCollisionShape3D")
if not mesh or not collision:
return
# Get the current mesh and collision scale
var mesh_scale = mesh.scale
var collision_scale = collision.scale
# Reset the transform
mesh.transform = Transform3D()
collision.transform = Transform3D()
# Restore the scale
mesh.scale = mesh_scale
collision.scale = collision_scale
# Update the shader
selected_grid._update_material()
# === Drawing Methods ===
# Creates a MeshInstance3D shpere at the current hovered location when in ADD mode
func _create_hover_preview() -> void:
# Clear the existing preview
if hover_preview:
hover_preview.queue_free()
# Create new hover preview
hover_preview = MeshInstance3D.new()
var sphere = SphereMesh.new()
var scale = selected_grid.grid_scale * BASE_PREVIEW_THICKNESS
sphere.radius = scale
sphere.height = scale * 2
hover_preview.mesh = sphere
# Create the material for the hover preview
var material = StandardMaterial3D.new()
material.albedo_color = Color.RED
material.no_depth_test = true # Always visible Renders ontop of other objects
hover_preview.material_override = material
# Add it to the scene
if csg_root:
csg_root.add_child(hover_preview)
hover_preview.position = hover_point
# Do not add owner
# Changes the position of the hover preview
func _update_hover_preview() -> void:
if not hover_preview:
return
hover_preview.global_position = hover_point
func _calculate_base_rect_points() -> void:
if not selected_grid:
return
var grid_unit = selected_grid.grid_scale
# Calculate the base rectangle points
var min_x = floor(min(draw_start.x, draw_end.x) / grid_unit) * grid_unit
var max_x = ceil(max(draw_start.x, draw_end.x) / grid_unit) * grid_unit
var min_y = floor(min(draw_start.y, draw_end.y) / grid_unit) * grid_unit
var max_y = ceil(max(draw_start.y, draw_end.y) / grid_unit) * grid_unit
var min_z = floor(min(draw_start.z, draw_end.z) / grid_unit) * grid_unit
var max_z = ceil(max(draw_start.z, draw_end.z) / grid_unit) * grid_unit
if draw_normal.abs().is_equal_approx(Vector3.UP) or draw_normal.abs().is_equal_approx(Vector3.DOWN):
base_rect_points = [
Vector3(min_x, draw_start.y, min_z),
Vector3(max_x, draw_start.y, min_z),
Vector3(max_x, draw_start.y, max_z),
Vector3(min_x, draw_start.y, max_z)
]
elif draw_normal.abs().is_equal_approx(Vector3.RIGHT) or draw_normal.abs().is_equal_approx(Vector3.LEFT):
base_rect_points = [
Vector3(draw_start.x, min_y, min_z),
Vector3(draw_start.x, max_y, min_z),
Vector3(draw_start.x, max_y, max_z),
Vector3(draw_start.x, min_y, max_z)
]
else:
base_rect_points = [
Vector3(min_x, min_y, draw_start.z),
Vector3(max_x, min_y, draw_start.z),
Vector3(max_x, max_y, draw_start.z),
Vector3(min_x, max_y, draw_start.z)
]
func create_rectangle_preview() -> void:
# Clear the previous preview
if draw_preview:
draw_preview.queue_free()
# Create a new preview
draw_preview = MeshInstance3D.new()
var immediate_mesh = ImmediateMesh.new()
draw_preview.mesh = immediate_mesh
var material = StandardMaterial3D.new()
material.albedo_color = Color.RED
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.no_depth_test = true
draw_preview.material_override = material
if csg_root:
csg_root.add_child(draw_preview)
draw_preview.owner = EditorInterface.get_edited_scene_root()
func _update_rectangle_preview() -> void:
if not draw_preview: return
var immediate_mesh = draw_preview.mesh as ImmediateMesh
immediate_mesh.clear_surfaces()
var base_thickness = BASE_PREVIEW_THICKNESS * selected_grid.grid_scale
var thickness = base_thickness
var grid_unit = selected_grid.grid_scale
var material = draw_preview.material_override as StandardMaterial3D
if is_extruding:
material.albedo_color = Color.GREEN if extrude_distance >= 0 else Color.RED
var preview_offset = draw_normal * (grid_unit * 0.000001)
immediate_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
var preview_points = []
for point in base_rect_points:
preview_points.append(point + preview_offset)
# Rectangle base lines
for i in range(preview_points.size()):
add_thick_line(
immediate_mesh,
preview_points[i],
preview_points[(i + 1) % preview_points.size()],
thickness
)
# Extrusion lines
if is_extruding:
add_thick_line(immediate_mesh,
initial_extrude_point + preview_offset,
initial_extrude_point + draw_normal * (extrude_distance + preview_offset.length()),
thickness * 0.5)
if has_started_extrusion:
var extrude_offset = draw_normal * extrude_distance
for i in range(preview_points.size()):
var extruded_point = preview_points[i] + extrude_offset
add_thick_line(immediate_mesh,
preview_points[i],
extruded_point,
thickness)
add_thick_line(
immediate_mesh,
extruded_point,
preview_points[(i + 1) % preview_points.size()] + extrude_offset,
thickness
)
immediate_mesh.surface_end()
func add_thick_line(immediate_mesh: ImmediateMesh, start: Vector3, end: Vector3, thickness: float) -> void:
var direction = (end - start).normalized()
# Find a perpendicular vector to the line
var perpendicular = Vector3.UP.cross(direction).normalized()
if perpendicular.length() < 0.1:
perpendicular = Vector3.RIGHT.cross(direction).normalized()
# Calculate the four corners of the line
var offset = perpendicular * thickness
var v1 = start + offset
var v2 = start - offset
var v3 = end + offset
var v4 = end - offset
# Add the two faces to the line
create_rectangle(immediate_mesh, v1, v2, v3, v4)
# Creates a rectangle using the given vertecies out of two triangles
func create_rectangle(immediate_mesh: ImmediateMesh, v1: Vector3, v2: Vector3, v3: Vector3, v4: Vector3) -> void:
# Add two triangles to form a rectangle
immediate_mesh.surface_add_vertex(v1)
immediate_mesh.surface_add_vertex(v2)
immediate_mesh.surface_add_vertex(v3)
immediate_mesh.surface_add_vertex(v2)
immediate_mesh.surface_add_vertex(v4)
immediate_mesh.surface_add_vertex(v3)
# === CSG Management Methods ===
func _create_CSGBox3D() -> void:
var new_box = CSGBox3D.new()
new_box.use_collision = true
new_box.set_meta("_edit_lock_", true)
new_box.set_meta("_edit_group_", true)
var min_point = base_rect_points[0]
var max_point = base_rect_points[0]
# Minimum and maximum points of the base rectangle
for point in base_rect_points:
min_point = Vector3(
min(min_point.x, point.x),
min(min_point.y, point.y),
min(min_point.z, point.z)
)
max_point = Vector3(
max(max_point.x, point.x),
max(max_point.y, point.y),
max(max_point.z, point.z)
)
# Initial size and center of the box
var size = (max_point - min_point)
var center = (max_point + min_point) * 0.5
# Adjust size and center based on the extrusion
if draw_normal.abs().is_equal_approx(Vector3.UP) or draw_normal.abs().is_equal_approx(Vector3.DOWN):
size.y = abs(extrude_distance)
center += draw_normal * (extrude_distance * 0.5)
elif draw_normal.abs().is_equal_approx(Vector3.RIGHT) or draw_normal.abs().is_equal_approx(Vector3.LEFT):
size.x = abs(extrude_distance)
center += draw_normal * (extrude_distance * 0.5)
else:
size.z = abs(extrude_distance)
center += draw_normal * (extrude_distance * 0.5)
if size.x < 0.0001 or size.y < 0.0001 or size.z < 0.0001:
return
new_box.size = size
new_box.position = center
# Depending on the extrusion distance set the operation
if extrude_distance < 0:
new_box.operation = CSGShape3D.OPERATION_SUBTRACTION
undo_redo.create_action("Create CSGBox3D")
undo_redo.add_do_method(csg_root, "add_child", new_box)
undo_redo.add_do_method(new_box, "set_owner", EditorInterface.get_edited_scene_root())
undo_redo.add_undo_method(csg_root, "remove_child", new_box)
undo_redo.commit_action()
_update_toolbar_states()
func _on_merge_mesh() -> void:
if not csg_root or csg_root.get_child_count() == 0:
return
# Dont allow to merge an already merged mesh
if csg_root.has_node("CSGMesh"):
push_warning("Already merged!")
return
if edge_preview:
edge_preview.queue_free()
edge_preview = null
current_edge = []
is_dragging_edge = false
var nodes_to_keep = []
# Go over all of the children of the csg root and check if they are CSGBox3D or CSGMesh3D
for node in csg_root.get_children():
if node is MeshInstance3D:
continue
# For subtraction operations, check if it actually cuts something
if node.operation == CSGShape3D.OPERATION_SUBTRACTION:
var cuts_something = false
for other_node in csg_root.get_children():
if other_node.operation == CSGShape3D.OPERATION_UNION:
if node is CSGBox3D and other_node is CSGBox3D:
var node_bounds = AABB(
node.position - (node.size * 0.5),
node.size
)
var other_bounds = AABB(
other_node.position - (other_node.size * 0.5),
other_node.size
)
# Check if the bounding boxes intersect if does keep it
if node_bounds.intersects(other_bounds):
cuts_something = true
break
else:
cuts_something = false
break
# Only keep if it actually cuts something
if cuts_something:
nodes_to_keep.append(node)
else:
nodes_to_keep.append(node)
var nodes_data = []
for node in nodes_to_keep:
nodes_data.append(_store_mesh_data(node))
var meshes = csg_root.get_meshes()
if meshes.size() > 1:
if not csg_mesh:
csg_mesh = MeshInstance3D.new()
csg_mesh.name = "CSGMesh"
csg_root.add_child(csg_mesh)
csg_mesh.owner = EditorInterface.get_edited_scene_root()
csg_mesh.mesh = meshes[1]
csg_mesh.set_meta("csg_data", {
"nodes": nodes_data
})
for child in csg_root.get_children():
if child != csg_mesh:
child.queue_free()
_update_toolbar_states()
_change_mode(BuildMode.DISABLE)
func _on_edit_mesh() -> void:
if not csg_root:
push_warning("No Mesh root found!")
return
if not csg_root.has_node("CSGMesh"):
push_warning("No CSGMesh to edit!")
return
csg_mesh = csg_root.get_node("CSGMesh")
var data = csg_mesh.get_meta("csg_data")
if not data:
push_warning("No CSG data found in mesh!")
return
# Deconstruct the mesh into CSGBox3D or CSGMesh3D
_convert_to_boxes()
# Stores the information about the CSGBox3D or CSGMesh3D
func _store_mesh_data(node: Node) -> Dictionary:
# Create a dictionary to store information
var data = {
"position": node.position,
"operation": node.operation,
"use_collision": node.use_collision,
"type": "box" if node is CSGBox3D else "mesh"
}
# Store the size of the CSGBox3D or the vertices and indices of the CSGMesh3D
if node is CSGBox3D:
data["size"] = node.size
# Store vertices and indices of the CSGMesh3D
elif node is CSGMesh3D:
var mesh = node.mesh as ArrayMesh
if mesh:
data["vertices"] = mesh.surface_get_arrays(0)[Mesh.ARRAY_VERTEX]
data["indices"] = mesh.surface_get_arrays(0)[Mesh.ARRAY_INDEX]
return data
# Recreates the CSGBox3D or CSGMesh3D from the stored metadata
func _convert_to_boxes() -> void:
csg_mesh = csg_root.get_node("CSGMesh")
if not csg_mesh:
push_warning("No CSGMesh node found!")
return
var data = csg_mesh.get_meta("csg_data")
if not data:
push_warning("No CSG data found in mesh!")
return
# Go through all of the nodes and recreate them
for node_info in data["nodes"]:
var new_node
# Based on the type, recreate CSGBox3D or CSGMesh3D
if node_info["type"] == "box":
new_node = CSGBox3D.new() # Create a new CSGBox3D
new_node.size = node_info["size"] # Set the size of the box by getting the size from the metadata
else:
new_node = CSGMesh3D.new() # Create a new CSGMesh3D
var arr_mesh = ArrayMesh.new() # Create a new ArrayMesh
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = node_info["vertices"]
arrays[Mesh.ARRAY_INDEX] = node_info["indices"]
arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) # Add the surface to the ArrayMesh
new_node.mesh = arr_mesh
new_node.position = node_info["position"] # Set the position
new_node.operation = node_info["operation"]
new_node.use_collision = node_info["use_collision"]
new_node.set_meta("_edit_lock_", true)
new_node.set_meta("_edit_group_", true)
csg_root.add_child(new_node)
new_node.owner = EditorInterface.get_edited_scene_root()
# Remove the CSGMesh
csg_mesh.queue_free()
csg_mesh = null
toolbar.update_button_states(false)
_change_mode(BuildMode.DISABLE)
# === UI Management Methods ===
func _connect_toolbar_signals() -> void:
toolbar.select_button_pressed.connect(func(): _change_mode(BuildMode.SELECT))
toolbar.add_button_pressed.connect(func(): _change_mode(BuildMode.ADD))
toolbar.disable_button_pressed.connect(func(): _change_mode(BuildMode.DISABLE))
toolbar.grid_size_changed.connect(_on_grid_size_changed)
toolbar.reset_grid_pressed.connect(_reset_grid_transform)
toolbar.merge_mesh.connect(_on_merge_mesh)
toolbar.edit_mesh.connect(_on_edit_mesh)
func _update_toolbar_states() -> void:
if not csg_root:
return
var has_csg_mesh = csg_root.has_node("CSGMesh")
var has_csg_boxes = false
for child in csg_root.get_children():
if child is CSGBox3D or child is CSGMesh3D:
has_csg_boxes = true
break
if has_csg_mesh:
toolbar.update_button_states(true)
else:
toolbar.update_button_states(false)
toolbar.set_merge_button_enabled(has_csg_boxes)
toolbar.set_select_button_enabled(has_csg_boxes)
toolbar.set_edit_button_enabled(has_csg_mesh)
func _on_selection_changed() -> void:
var selected = EditorInterface.get_selection().get_selected_nodes()
if selected.size() == 1 and selected[0] is CubeGrid3D:
selected_grid = selected[0]
csg_root = selected_grid.get_node("CSGCombiner3D")
toolbar.show()
toolbar.connect_to_grid(selected_grid)
_update_toolbar_states()
var root := EditorInterface.get_base_control()
var toolbar = root.find_children("", "Node3DEditor", true, false)[0].get_child(0).find_children("", "HBoxContainer", true, false)[0]
var btn = toolbar.get_child(2)
btn.pressed.emit()
if hover_preview:
hover_preview.queue_free()
hover_preview = null
else:
_change_mode(BuildMode.DISABLE)
if hover_preview:
hover_preview.queue_free()
hover_preview = null
selected_grid = null
csg_root = null
toolbar.hide()
func _change_mode(new_mode: BuildMode) -> void:
if new_mode == BuildMode.ADD and csg_root and csg_root.has_node("CSGMesh"):
push_warning("Can't switch to ADD mode while CSGMesh exists. Use Edit to modify.")
toolbar.set_active_mode(current_mode)
return
current_mode = new_mode
toolbar.set_active_mode(current_mode)
if edge_preview:
edge_preview.queue_free()
edge_preview = null
current_edge = []
is_dragging_edge = false
if csg_root:
csg_root.set_meta("_edit_lock_", current_mode != BuildMode.SELECT)
# === Edge Movement Methods ===
func _get_edges(node: Node) -> Array:
var edges = []
if node is CSGBox3D:
var aabb = AABB(
node.global_position - (node.size * 0.5),
node.size
)
# Corners of the CSGBox3D
var corners = [
Vector3(aabb.position.x, aabb.position.y, aabb.position.z),
Vector3(aabb.end.x, aabb.position.y, aabb.position.z),
Vector3(aabb.end.x, aabb.end.y, aabb.position.z),
Vector3(aabb.position.x, aabb.end.y, aabb.position.z),
Vector3(aabb.position.x, aabb.position.y, aabb.end.z),
Vector3(aabb.end.x, aabb.position.y, aabb.end.z),
Vector3(aabb.end.x, aabb.end.y, aabb.end.z),
Vector3(aabb.position.x, aabb.end.y, aabb.end.z)
]
# Edges of the CSGBox3D
var edge_indices = [
[0, 1], [1, 2], [2, 3], [3, 0],
[4, 5], [5, 6], [6, 7], [7, 4],
[0, 4], [1, 5], [2, 6], [3, 7]
]
# Create edges by taking pairs of corners
for pair in edge_indices:
edges.append([corners[pair[0]], corners[pair[1]]])
# CSGMesh3D get edges by getting the verticies and indicies
elif node is CSGMesh3D:
var arr_mesh = node.mesh as ArrayMesh
if not arr_mesh:
return edges
# Arraymesh
var arrays = arr_mesh.surface_get_arrays(0)
# Get the vertices and indices
var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var indices = arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
# Create a set to store edges
var edge_set = {}
# Go through the indicies and create an edgemap
for i in range(0, indices.size(), 3):
var tri_indices = [
indices[i],
indices[i + 1],
indices[i + 2]
]
for j in range(3):
var idx1 = tri_indices[j]
var idx2 = tri_indices[(j + 1) % 3] # Get the next 2 indicies
var edge_key = str(min(idx1, idx2)) + "_" + str(max(idx1, idx2))
if not edge_set.has(edge_key):
edge_set[edge_key] = true
var vert1 = node.global_transform * vertices[idx1]
var vert2 = node.global_transform * vertices[idx2]
edges.append([vert1, vert2])
return edges
# Finds the closest edge to the mouse
func _find_closest_edge(node: Node, mouse_pos: Vector2) -> Array:
if not camera:
return []
# Get all of the edges
var edges = _get_edges(node)
# Intialize closest edge
var closest_edge = []
# Set the distance to infinity
var min_distance = INF
# Cast ray from the camera to the mouse position
var from = camera.project_ray_origin(mouse_pos)
var dir = camera.project_ray_normal(mouse_pos)
var m_line1 = from
var m_line2 = from + dir * 5000
# Go over all of the edges
for edge in edges:
# Take the two first endpoints of the edge
var e_line1 = edge[0]
var e_line2 = edge[1]
# Get the closest points between the edge and the ray
var closest_points = Geometry3D.get_closest_points_between_segments(e_line1, e_line2, m_line1, m_line2)
# Return the closest edge
var point_on_edge = closest_points[0]
var point_on_ray = closest_points[1]
# Calculate the distance between the two points
var distance_vec = point_on_ray - point_on_edge
var distance = distance_vec.length()
# If the distance is smaller than the current minimum, update the closest edge
if distance < min_distance:
min_distance = distance
closest_edge = edge
return closest_edge
# Method that draws a line on the currently hovered edge
func _create_edge_preview(edge: Array) -> void:
if edge.is_empty():
if edge_preview:
edge_preview.hide()
return
# Create the edge preview material
if not edge_preview:
edge_preview = MeshInstance3D.new()
var immediate_mesh = ImmediateMesh.new()
edge_preview.mesh = immediate_mesh
var material = StandardMaterial3D.new()
material.albedo_color = Color.RED
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.no_depth_test = true
edge_preview.material_override = material
if csg_root:
csg_root.add_child(edge_preview)
edge_preview.show()
var immediate_mesh = edge_preview.mesh as ImmediateMesh
immediate_mesh.clear_surfaces()
immediate_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
var thickness = selected_grid.grid_scale * BASE_PREVIEW_THICKNESS # Scale the thickness based on grid size
add_thick_line(immediate_mesh, edge[0], edge[1], thickness) # Use the add_thick_line methdod to create the line
immediate_mesh.surface_end()
func _convert_box_to_CSGMesh(box: CSGBox3D) -> CSGMesh3D:
var csg_mesh = CSGMesh3D.new()
var arr_mesh = ArrayMesh.new()
var vertices = PackedVector3Array()
var indices = PackedInt32Array()
var half_size = box.size * 0.5
var local_verts = [
Vector3(-half_size.x, -half_size.y, -half_size.z), # 0
Vector3(half_size.x, -half_size.y, -half_size.z), # 1
Vector3(half_size.x, half_size.y, -half_size.z), # 2
Vector3(-half_size.x, half_size.y, -half_size.z), # 3
Vector3(-half_size.x, -half_size.y, half_size.z), # 4
Vector3(half_size.x, -half_size.y, half_size.z), # 5
Vector3(half_size.x, half_size.y, half_size.z), # 6
Vector3(-half_size.x, half_size.y, half_size.z) # 7
]
vertices.append_array(local_verts)
var faces = [
[0, 1, 2, 2, 3, 0], # Front
[1, 5, 6, 6, 2, 1], # Right
[5, 4, 7, 7, 6, 5], # Back
[4, 0, 3, 3, 7, 4], # Left
[3, 2, 6, 6, 7, 3], # Top
[4, 5, 1, 1, 0, 4] # Bottom
]
for face in faces:
indices.append_array(face)
var arrays = []
arrays.resize(Mesh.ARRAY_MAX)
arrays[Mesh.ARRAY_VERTEX] = vertices
arrays[Mesh.ARRAY_INDEX] = indices
arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
csg_mesh.mesh = arr_mesh
csg_mesh.transform = box.transform
csg_mesh.operation = box.operation
csg_mesh.use_collision = box.use_collision
csg_root.add_child(csg_mesh)
csg_mesh.owner = EditorInterface.get_edited_scene_root()
box.queue_free()
return csg_mesh