Reupload to new git vps.
This commit is contained in:
commit
a368e4b1c9
4 changed files with 423 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.godot/
|
||||
*.uid
|
401
collision_creator.gd
Normal file
401
collision_creator.gd
Normal file
|
@ -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
|
6
plugin.cfg
Normal file
6
plugin.cfg
Normal file
|
@ -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"
|
14
plugin.gd
Normal file
14
plugin.gd
Normal file
|
@ -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()
|
Loading…
Reference in a new issue