140 lines
5.1 KiB
GDScript
140 lines
5.1 KiB
GDScript
@tool
|
|
extends RefCounted
|
|
|
|
# physics.autofit_collision_shape_2d — compute a CollisionShape2D sized to a
|
|
# sibling/child Sprite2D or AnimatedSprite2D's visual bounds. Common pattern
|
|
# that's tedious to set up by hand: you have a sprite, you want a rectangle or
|
|
# circle collider that roughly matches it.
|
|
#
|
|
# Params: {
|
|
# node_path: CollisionShape2D to fit (created if missing when create:true)
|
|
# source: NodePath to a Sprite2D / AnimatedSprite2D (auto-detected
|
|
# among siblings if omitted)
|
|
# shape: "rectangle" | "circle" | "capsule" (default "rectangle")
|
|
# margin: float pixels to shrink by (default 0)
|
|
# create: bool — if true and node_path doesn't exist, create it as
|
|
# a child of the source's parent. Default false.
|
|
# }
|
|
static func autofit_collision_shape_2d(params: Dictionary) -> Dictionary:
|
|
var node_path: String = params.get("node_path", "")
|
|
var source_path: String = params.get("source", "")
|
|
var shape_type: String = params.get("shape", "rectangle")
|
|
var margin: float = float(params.get("margin", 0.0))
|
|
var create: bool = params.get("create", false)
|
|
|
|
if node_path == "":
|
|
return _err(-32602, "missing 'node_path'")
|
|
|
|
var root := EditorInterface.get_edited_scene_root()
|
|
if root == null:
|
|
return _err(-32001, "no scene open")
|
|
|
|
var target: Node = root if node_path == "." else root.get_node_or_null(node_path)
|
|
if target == null and not create:
|
|
return _err(-32001, "node not found: %s (pass create:true to auto-create)" % node_path)
|
|
|
|
# Resolve the visual source.
|
|
var source: Node = null
|
|
if source_path != "":
|
|
source = root.get_node_or_null(source_path)
|
|
if source == null:
|
|
return _err(-32001, "source not found: %s" % source_path)
|
|
elif target != null:
|
|
# Look among siblings for a Sprite2D / AnimatedSprite2D.
|
|
var parent := target.get_parent()
|
|
if parent:
|
|
for sib in parent.get_children():
|
|
if sib is Sprite2D or sib is AnimatedSprite2D:
|
|
source = sib
|
|
break
|
|
if source == null:
|
|
return _err(-32001, "no Sprite2D/AnimatedSprite2D source found — pass 'source'")
|
|
|
|
# Compute visual bounds (untransformed; CollisionShape2D inherits parent's transform).
|
|
var bounds := _sprite_bounds(source)
|
|
if bounds.size.x <= 0 or bounds.size.y <= 0:
|
|
return _err(-32001, "source has no visible extents (%s)" % source.get_class())
|
|
|
|
# Create the CollisionShape2D if needed.
|
|
if target == null and create:
|
|
var inst := CollisionShape2D.new()
|
|
inst.name = node_path.get_file() if "/" in node_path else node_path
|
|
var parent := source.get_parent()
|
|
parent.add_child(inst)
|
|
inst.owner = root
|
|
target = inst
|
|
|
|
if not target is CollisionShape2D:
|
|
return _err(-32602, "target is not a CollisionShape2D: %s (%s)" % [node_path, target.get_class()])
|
|
var cs: CollisionShape2D = target
|
|
|
|
# Build the requested shape sized to bounds.
|
|
var shape_res: Shape2D
|
|
match shape_type:
|
|
"rectangle":
|
|
var r := RectangleShape2D.new()
|
|
r.size = Vector2(
|
|
max(1.0, bounds.size.x - margin * 2.0),
|
|
max(1.0, bounds.size.y - margin * 2.0)
|
|
)
|
|
shape_res = r
|
|
"circle":
|
|
var c := CircleShape2D.new()
|
|
c.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin)
|
|
shape_res = c
|
|
"capsule":
|
|
var cap := CapsuleShape2D.new()
|
|
cap.radius = max(1.0, min(bounds.size.x, bounds.size.y) / 2.0 - margin)
|
|
cap.height = max(cap.radius * 2.0 + 1.0, bounds.size.y - margin * 2.0)
|
|
shape_res = cap
|
|
_:
|
|
return _err(-32602, "unknown shape '%s' (supported: rectangle, circle, capsule)" % shape_type)
|
|
|
|
cs.shape = shape_res
|
|
# Center the CollisionShape2D on the sprite's center.
|
|
cs.position = bounds.position + bounds.size / 2.0
|
|
EditorInterface.mark_scene_as_unsaved()
|
|
|
|
return _ok({
|
|
"node_path": String(root.get_path_to(cs)),
|
|
"shape": shape_type,
|
|
"size": [bounds.size.x - margin * 2.0, bounds.size.y - margin * 2.0],
|
|
"center": [cs.position.x, cs.position.y],
|
|
"source": String(root.get_path_to(source)),
|
|
})
|
|
|
|
|
|
static func _sprite_bounds(source: Node) -> Rect2:
|
|
if source is Sprite2D:
|
|
var s: Sprite2D = source
|
|
if s.texture == null:
|
|
return Rect2(Vector2.ZERO, Vector2.ZERO)
|
|
var tex_size: Vector2 = s.texture.get_size()
|
|
if s.region_enabled:
|
|
tex_size = s.region_rect.size
|
|
var size: Vector2 = tex_size * Vector2(abs(s.scale.x), abs(s.scale.y))
|
|
# For centered sprites, bounds are at (-size/2, -size/2).
|
|
var offset: Vector2 = -size / 2.0 if s.centered else Vector2.ZERO
|
|
offset += s.offset
|
|
return Rect2(offset, size)
|
|
if source is AnimatedSprite2D:
|
|
var a: AnimatedSprite2D = source
|
|
if a.sprite_frames == null or a.animation == "":
|
|
return Rect2(Vector2.ZERO, Vector2.ZERO)
|
|
var tex := a.sprite_frames.get_frame_texture(a.animation, a.frame)
|
|
if tex == null:
|
|
return Rect2(Vector2.ZERO, Vector2.ZERO)
|
|
var size: Vector2 = tex.get_size() * Vector2(abs(a.scale.x), abs(a.scale.y))
|
|
var offset: Vector2 = -size / 2.0 if a.centered else Vector2.ZERO
|
|
offset += a.offset
|
|
return Rect2(offset, size)
|
|
return Rect2(Vector2.ZERO, Vector2.ZERO)
|
|
|
|
|
|
static func _ok(data) -> Dictionary:
|
|
return {"data": data}
|
|
|
|
|
|
static func _err(code: int, msg: String) -> Dictionary:
|
|
return {"error": {"code": code, "message": msg}}
|