DCCollisionShapeCreator/collision_creator.gd

401 lines
12 KiB
GDScript

@tool
extends Control
enum ShapeType { AUTO, BOX, SPHERE, CAPSULE, CYLINDER, CONVEX, TRIMESH }
enum BodyType { STATIC, RIGID, CHARACTER, AREA, NONE }
enum Axis { X, Y, Z }
# Constants for auto-detection thresholds
const SPHERE_RATIO_THRESHOLD = 1.3
const CAPSULE_RATIO_THRESHOLD = 2.0
var target_mesh: MeshInstance3D
var preview_node: MeshInstance3D
@onready var shape_option: OptionButton
@onready var body_option: OptionButton
@onready var as_child_check: CheckBox
@onready var auto_center_check: CheckBox
@onready var margin_spin: SpinBox
@onready var capsule_axis_option: OptionButton
@onready var cylinder_axis_option: OptionButton
@onready var axis_container: Control
@onready var create_button: Button
@onready var preview_check: CheckBox
@onready var target_label: Label
class ShapeFactory:
static func create_box(size: Vector3) -> BoxShape3D:
var shape = BoxShape3D.new()
shape.size = size
return shape
static func create_sphere(size: Vector3) -> SphereShape3D:
var shape = SphereShape3D.new()
shape.radius = size.length() / 3.0
return shape
static func create_capsule(size: Vector3, axis: Axis) -> CapsuleShape3D:
var shape = CapsuleShape3D.new()
var dims = _get_capsule_dimensions(size, axis)
shape.radius = dims.x
shape.height = dims.y
return shape
static func create_cylinder(size: Vector3, axis: Axis) -> CylinderShape3D:
var shape = CylinderShape3D.new()
var dims = _get_capsule_dimensions(size, axis)
shape.radius = dims.x
shape.height = dims.y
return shape
static func _get_capsule_dimensions(size: Vector3, axis: Axis) -> Vector2:
match axis:
Axis.X: return Vector2((size.y + size.z) / 4.0, size.x)
Axis.Y: return Vector2((size.x + size.z) / 4.0, size.y)
Axis.Z: return Vector2((size.x + size.y) / 4.0, size.z)
_: return Vector2.ZERO
func _ready():
_setup_ui()
_connect_signals()
func _setup_ui():
var vbox = _create_main_container()
_create_header(vbox)
_create_controls(vbox)
_create_actions(vbox)
vbox.add_spacer(false)
func _create_main_container() -> VBoxContainer:
var vbox = VBoxContainer.new()
vbox.name = "VBox"
add_child(vbox)
return vbox
func _create_header(parent: VBoxContainer):
var title = Label.new()
title.text = "DC Collision Shape Creator"
title.add_theme_font_size_override("font_size", 16)
parent.add_child(title)
parent.add_child(HSeparator.new())
target_label = Label.new()
target_label.name = "TargetLabel"
target_label.text = "No MeshInstance3D selected"
target_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
parent.add_child(target_label)
parent.add_child(HSeparator.new())
func _create_controls(parent: VBoxContainer):
shape_option = _create_option_row(parent, "Shape Type:", ShapeType.keys())
body_option = _create_option_row(parent, "Body Type:", BodyType.keys())
_create_checkbox_options(parent)
_create_margin_control(parent)
_create_axis_controls(parent)
func _create_option_row(parent: Control, label_text: String, options: Array) -> OptionButton:
var container = HBoxContainer.new()
parent.add_child(container)
var label = Label.new()
label.text = label_text
label.custom_minimum_size.x = 100
container.add_child(label)
var option_button = OptionButton.new()
for option in options:
option_button.add_item(option.capitalize())
container.add_child(option_button)
return option_button
func _create_checkbox_options(parent: VBoxContainer):
var options_container = VBoxContainer.new()
parent.add_child(options_container)
as_child_check = _create_checkbox(options_container, "Create as child of mesh", true)
auto_center_check = _create_checkbox(options_container, "Auto-center collision shape", true)
func _create_checkbox(parent: Control, text: String, pressed: bool) -> CheckBox:
var checkbox = CheckBox.new()
checkbox.text = text
checkbox.button_pressed = pressed
parent.add_child(checkbox)
return checkbox
func _create_margin_control(parent: VBoxContainer):
var container = HBoxContainer.new()
parent.add_child(container)
var label = Label.new()
label.text = "Margin:"
label.custom_minimum_size.x = 100
container.add_child(label)
margin_spin = SpinBox.new()
margin_spin.min_value = 0.0
margin_spin.max_value = 1.0
margin_spin.step = 0.01
margin_spin.value = 0.0
container.add_child(margin_spin)
func _create_axis_controls(parent: VBoxContainer):
axis_container = VBoxContainer.new()
axis_container.visible = false
parent.add_child(axis_container)
capsule_axis_option = _create_axis_option(axis_container, "Capsule Axis:", Axis.Y)
cylinder_axis_option = _create_axis_option(axis_container, "Cylinder Axis:", Axis.Y)
func _create_axis_option(parent: Control, label_text: String, default: Axis) -> OptionButton:
var container = HBoxContainer.new()
parent.add_child(container)
var label = Label.new()
label.text = label_text
label.custom_minimum_size.x = 100
container.add_child(label)
var option = OptionButton.new()
for axis in Axis.keys():
option.add_item(axis)
option.selected = default
container.add_child(option)
return option
func _create_actions(parent: VBoxContainer):
parent.add_child(HSeparator.new())
preview_check = CheckBox.new()
preview_check.text = "Preview Shape"
parent.add_child(preview_check)
create_button = Button.new()
create_button.text = "Create Collision Shape"
create_button.disabled = true
parent.add_child(create_button)
func _connect_signals():
EditorInterface.get_selection().selection_changed.connect(_on_selection_changed)
shape_option.item_selected.connect(_on_shape_changed)
preview_check.toggled.connect(_on_preview_toggled)
create_button.pressed.connect(_on_create_pressed)
func _on_selection_changed():
target_mesh = _get_selected_mesh()
_update_ui()
func _get_selected_mesh() -> MeshInstance3D:
var selection = EditorInterface.get_selection().get_selected_nodes()
for node in selection:
if node is MeshInstance3D:
return node
return null
func _update_ui():
var has_valid_target = target_mesh and target_mesh.mesh
if has_valid_target:
target_label.text = "Target: " + target_mesh.name
create_button.disabled = false
preview_check.disabled = false
else:
target_label.text = "No MeshInstance3D selected"
create_button.disabled = true
preview_check.disabled = true
preview_check.button_pressed = false
_clear_preview()
func _on_shape_changed(index: int):
var needs_axis = index in [ShapeType.CAPSULE, ShapeType.CYLINDER]
axis_container.visible = needs_axis
if axis_container.visible:
capsule_axis_option.get_parent().visible = index == ShapeType.CAPSULE
cylinder_axis_option.get_parent().visible = index == ShapeType.CYLINDER
if preview_check.button_pressed:
_update_preview()
func _on_preview_toggled(pressed: bool):
if pressed and target_mesh:
_update_preview()
else:
_clear_preview()
func _on_create_pressed():
if not _validate_target():
return
_clear_preview()
var collision = _create_collision_shape()
if not collision:
push_error("Failed to create collision shape")
return
_add_to_scene(collision)
func _validate_target() -> bool:
return target_mesh != null and target_mesh.mesh != null
func _create_collision_shape() -> CollisionShape3D:
var shape = _create_shape()
if not shape:
return null
var collision = CollisionShape3D.new()
collision.shape = shape
collision.name = ShapeType.keys()[shape_option.selected] + "Collision"
if auto_center_check.button_pressed:
collision.position = target_mesh.get_aabb().get_center()
_apply_shape_rotation(collision)
return collision
func _create_shape() -> Shape3D:
var aabb = target_mesh.get_aabb()
var size = aabb.size + Vector3.ONE * margin_spin.value * 2
var shape_type = _get_effective_shape_type()
match shape_type:
ShapeType.BOX: return ShapeFactory.create_box(size)
ShapeType.SPHERE: return ShapeFactory.create_sphere(size)
ShapeType.CAPSULE: return ShapeFactory.create_capsule(size, capsule_axis_option.selected)
ShapeType.CYLINDER: return ShapeFactory.create_cylinder(size, cylinder_axis_option.selected)
ShapeType.CONVEX: return target_mesh.mesh.create_convex_shape()
ShapeType.TRIMESH: return target_mesh.mesh.create_trimesh_shape()
_: return null
func _get_effective_shape_type() -> ShapeType:
if shape_option.selected != ShapeType.AUTO:
return shape_option.selected
return _auto_detect_shape_type()
func _auto_detect_shape_type() -> ShapeType:
var size = target_mesh.get_aabb().size
var sorted = [size.x, size.y, size.z]
sorted.sort()
var uniformity_ratio = sorted[2] / sorted[0] if sorted[0] > 0 else INF
var elongation_ratio = sorted[2] / sorted[1] if sorted[1] > 0 else INF
if uniformity_ratio < SPHERE_RATIO_THRESHOLD:
return ShapeType.SPHERE
elif elongation_ratio > CAPSULE_RATIO_THRESHOLD:
return ShapeType.CAPSULE
else:
return ShapeType.BOX
func _apply_shape_rotation(node: Node3D):
var shape_type = _get_effective_shape_type()
if shape_type not in [ShapeType.CAPSULE, ShapeType.CYLINDER]:
return
var axis = capsule_axis_option.selected if shape_type == ShapeType.CAPSULE else cylinder_axis_option.selected
match axis:
Axis.X: node.rotation_degrees.z = 90
Axis.Z: node.rotation_degrees.x = 90
func _add_to_scene(collision: CollisionShape3D):
var parent = _determine_parent()
var body = _create_body_if_needed()
if body:
body.add_child(collision, true)
parent.add_child(body, true)
body.owner = EditorInterface.get_edited_scene_root()
if not as_child_check.button_pressed:
body.transform = target_mesh.transform
else:
parent.add_child(collision, true)
collision.owner = EditorInterface.get_edited_scene_root()
func _determine_parent() -> Node:
return target_mesh if as_child_check.button_pressed else target_mesh.get_parent()
func _create_body_if_needed() -> Node3D:
if body_option.selected == BodyType.NONE:
return null
match body_option.selected:
BodyType.STATIC: return StaticBody3D.new()
BodyType.RIGID: return RigidBody3D.new()
BodyType.CHARACTER: return CharacterBody3D.new()
BodyType.AREA: return Area3D.new()
_: return null
func _update_preview():
_clear_preview()
if not target_mesh:
return
var mesh = _create_preview_mesh()
if not mesh:
return
preview_node = MeshInstance3D.new()
preview_node.mesh = mesh
preview_node.material_override = _create_preview_material()
_apply_shape_rotation(preview_node)
target_mesh.add_child(preview_node)
func _create_preview_mesh() -> Mesh:
var size = target_mesh.get_aabb().size + Vector3.ONE * margin_spin.value * 2
var shape_type = _get_effective_shape_type()
match shape_type:
ShapeType.BOX: return _mesh_from_shape(ShapeFactory.create_box(size))
ShapeType.SPHERE: return _mesh_from_shape(ShapeFactory.create_sphere(size))
ShapeType.CAPSULE: return _mesh_from_shape(ShapeFactory.create_capsule(size, capsule_axis_option.selected))
ShapeType.CYLINDER: return _mesh_from_shape(ShapeFactory.create_cylinder(size, cylinder_axis_option.selected))
_: return null
func _mesh_from_shape(shape: Shape3D) -> Mesh:
if shape is BoxShape3D:
var mesh = BoxMesh.new()
mesh.size = shape.size
return mesh
elif shape is SphereShape3D:
var mesh = SphereMesh.new()
mesh.radius = shape.radius
mesh.height = shape.radius * 2.0
return mesh
elif shape is CapsuleShape3D:
var mesh = CapsuleMesh.new()
mesh.radius = shape.radius
mesh.height = shape.height
return mesh
elif shape is CylinderShape3D:
var mesh = CylinderMesh.new()
mesh.top_radius = shape.radius
mesh.bottom_radius = shape.radius
mesh.height = shape.height
return mesh
return null
func _create_preview_material() -> StandardMaterial3D:
var mat = StandardMaterial3D.new()
mat.albedo_color = Color.GREEN
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
mat.albedo_color.a = 0.3
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
mat.no_depth_test = true
return mat
func _clear_preview():
if is_instance_valid(preview_node):
preview_node.queue_free()
preview_node = null