From a368e4b1c9e90f43d7f861013ef2058e9ed641db Mon Sep 17 00:00:00 2001 From: Daniel Cumbor Date: Sun, 13 Jul 2025 19:39:40 +0100 Subject: [PATCH] Reupload to new git vps. --- .gitignore | 2 + collision_creator.gd | 401 +++++++++++++++++++++++++++++++++++++++++++ plugin.cfg | 6 + plugin.gd | 14 ++ 4 files changed, 423 insertions(+) create mode 100644 .gitignore create mode 100644 collision_creator.gd create mode 100644 plugin.cfg create mode 100644 plugin.gd diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c33025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.godot/ +*.uid \ No newline at end of file diff --git a/collision_creator.gd b/collision_creator.gd new file mode 100644 index 0000000..01d1f39 --- /dev/null +++ b/collision_creator.gd @@ -0,0 +1,401 @@ +@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 diff --git a/plugin.cfg b/plugin.cfg new file mode 100644 index 0000000..f717ba9 --- /dev/null +++ b/plugin.cfg @@ -0,0 +1,6 @@ +[plugin] +name="DC Collision Shape Creator" +description="Automatically defines collision shape dimensions to match mesh bounds" +author="Daniel Cumbor" +version="1.0" +script="plugin.gd" diff --git a/plugin.gd b/plugin.gd new file mode 100644 index 0000000..fecaece --- /dev/null +++ b/plugin.gd @@ -0,0 +1,14 @@ +@tool +extends EditorPlugin + +var dock + +func _enter_tree(): + dock = preload("res://addons/DCCollisionShapeCreator/collision_creator.gd").new() + dock.name = "Collision Creator" + add_control_to_dock(DOCK_SLOT_RIGHT_BL, dock) + +func _exit_tree(): + remove_control_from_docks(dock) + if dock: + dock.queue_free()