Phase 14 - Organized directories.
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
class_name PropAttachPreview
|
||||
extends PanelContainer
|
||||
## Private editor visualization. It never creates a PropBlock or touches Sandbox state.
|
||||
|
||||
enum PreviewHand { LEFT, RIGHT }
|
||||
enum PreviewMotion { FRONT_POSE, WALK_LEFT, WALK_RIGHT }
|
||||
const PropAnchorDataScript := preload("res://scripts/props/prop_anchor_data.gd")
|
||||
const StickmanFactoryScript := preload("res://scripts/characters/stickman/stickman_factory.gd")
|
||||
const StkRigAdapterScript := preload("res://scripts/characters/stickman/stk_rig_adapter.gd")
|
||||
const RigScene := preload("res://scenes/characters/stickman/stickman_rig.tscn")
|
||||
const RIG_CENTER := Vector2(340.0, 575.0)
|
||||
const TARGET_RADIUS := 14.0
|
||||
|
||||
var _viewport: SubViewport
|
||||
var _viewport_container: SubViewportContainer
|
||||
var _world: Node2D
|
||||
var _rig_root: Node2D
|
||||
var _rig: StickmanRig
|
||||
var _prop: Node2D
|
||||
var _target_marker: Polygon2D
|
||||
var _camera: Camera2D
|
||||
var _hint: Label
|
||||
var _walk_playback_button: Button
|
||||
var _reset_arm_button: Button
|
||||
var _reset_view_button: Button
|
||||
var _hand_option: OptionButton
|
||||
var _motion_option: OptionButton
|
||||
var _shapes: Array[Dictionary] = []
|
||||
var _anchors: Dictionary = {}
|
||||
var _selected: String = ""
|
||||
var _hand: int = PreviewHand.RIGHT
|
||||
var _motion: PreviewMotion = PreviewMotion.FRONT_POSE
|
||||
var _walk_paused: bool = true
|
||||
var _manual_arm_override: bool = false
|
||||
# Retained only for old test harnesses; Phase 9c UI uses _walk_paused.
|
||||
var _animate: bool = false
|
||||
var _dragging_target: bool = false
|
||||
var _camera_dragging: bool = false
|
||||
var _last_pointer := Vector2.ZERO
|
||||
var _hand_target_rests_local: Dictionary = {}
|
||||
var _camera_zoom := 1.0
|
||||
var _fitted_zoom := 1.0
|
||||
|
||||
func _ready() -> void:
|
||||
custom_minimum_size.x = 360.0
|
||||
var surface := StyleBoxFlat.new()
|
||||
surface.bg_color = Color(0.075, 0.09, 0.12, 1.0)
|
||||
surface.set_border_width_all(0)
|
||||
surface.set_corner_radius_all(5)
|
||||
add_theme_stylebox_override("panel", surface)
|
||||
_build_ui()
|
||||
_build_private_world()
|
||||
_reframe_camera()
|
||||
|
||||
func _build_ui() -> void:
|
||||
var layout := VBoxContainer.new()
|
||||
layout.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(layout)
|
||||
var header := FlowContainer.new()
|
||||
layout.add_child(header)
|
||||
var title := Label.new()
|
||||
title.text = "Holding Preview"
|
||||
title.custom_minimum_size.x = 132.0
|
||||
header.add_child(title)
|
||||
_hand_option = OptionButton.new()
|
||||
_hand_option.name = "HandOption"
|
||||
_hand_option.add_item("Left Hand", PreviewHand.LEFT)
|
||||
_hand_option.add_item("Right Hand", PreviewHand.RIGHT)
|
||||
_hand_option.select(PreviewHand.RIGHT)
|
||||
_hand_option.item_selected.connect(set_preview_hand)
|
||||
header.add_child(_hand_option)
|
||||
_motion_option = OptionButton.new()
|
||||
_motion_option.name = "MotionOption"
|
||||
_motion_option.add_item("Front Pose", PreviewMotion.FRONT_POSE)
|
||||
_motion_option.add_item("Walk Left", PreviewMotion.WALK_LEFT)
|
||||
_motion_option.add_item("Walk Right", PreviewMotion.WALK_RIGHT)
|
||||
_motion_option.item_selected.connect(set_preview_motion)
|
||||
header.add_child(_motion_option)
|
||||
_hint = Label.new()
|
||||
_hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
layout.add_child(_hint)
|
||||
_viewport_container = SubViewportContainer.new()
|
||||
# The pane keeps its 360 px width, but its height must yield to the editor's
|
||||
# fixed name/status rows when Attach mode makes the preview visible.
|
||||
_viewport_container.custom_minimum_size = Vector2(360.0, 0.0)
|
||||
_viewport_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_viewport_container.stretch = true
|
||||
_viewport_container.gui_input.connect(_on_viewport_gui_input)
|
||||
layout.add_child(_viewport_container)
|
||||
var footer := FlowContainer.new()
|
||||
layout.add_child(footer)
|
||||
_walk_playback_button = Button.new()
|
||||
_walk_playback_button.name = "WalkPlaybackButton"
|
||||
_walk_playback_button.pressed.connect(toggle_walk_playback)
|
||||
footer.add_child(_walk_playback_button)
|
||||
_reset_arm_button = Button.new()
|
||||
_reset_arm_button.text = "Reset Arm"
|
||||
_reset_arm_button.pressed.connect(reset_arm_pose)
|
||||
footer.add_child(_reset_arm_button)
|
||||
_reset_view_button = Button.new()
|
||||
_reset_view_button.text = "Reset View"
|
||||
_reset_view_button.pressed.connect(_reframe_camera)
|
||||
footer.add_child(_reset_view_button)
|
||||
_apply_hit_target_metrics(36.0)
|
||||
|
||||
func set_ui_hit_target_height(height: float) -> void:
|
||||
_apply_hit_target_metrics(maxf(height, 36.0))
|
||||
|
||||
func _apply_hit_target_metrics(height: float) -> void:
|
||||
for option: OptionButton in [_hand_option, _motion_option]:
|
||||
if option != null:
|
||||
option.custom_minimum_size = Vector2(88.0, height)
|
||||
option.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
option.focus_mode = Control.FOCUS_NONE
|
||||
for button: Button in [_walk_playback_button, _reset_arm_button, _reset_view_button]:
|
||||
if button != null:
|
||||
button.custom_minimum_size = Vector2(112.0, height)
|
||||
button.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
button.focus_mode = Control.FOCUS_NONE
|
||||
|
||||
func _build_private_world() -> void:
|
||||
_viewport = SubViewport.new()
|
||||
_viewport.name = "PreviewViewport"
|
||||
_viewport.transparent_bg = false
|
||||
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||
_viewport_container.add_child(_viewport)
|
||||
_viewport_container.resized.connect(_resize_viewport)
|
||||
_world = Node2D.new()
|
||||
_world.name = "PreviewWorld"
|
||||
_viewport.add_child(_world)
|
||||
_rig_root = Node2D.new()
|
||||
_rig_root.name = "PreviewRigRoot"
|
||||
_rig_root.position = RIG_CENTER
|
||||
_world.add_child(_rig_root)
|
||||
_rig = RigScene.instantiate() as StickmanRig
|
||||
_rig.set_physics_process(false)
|
||||
_rig_root.add_child(_rig)
|
||||
var data: Dictionary = StickmanFactoryScript.load_stk("res://content/stickmen/test.stk")
|
||||
if not data.is_empty():
|
||||
StkRigAdapterScript.apply(data, _rig)
|
||||
_prop = Node2D.new()
|
||||
_prop.name = "PreviewProp"
|
||||
_world.add_child(_prop)
|
||||
_target_marker = Polygon2D.new()
|
||||
_target_marker.polygon = PackedVector2Array([Vector2(0, -10), Vector2(10, 0), Vector2(0, 10), Vector2(-10, 0)])
|
||||
_target_marker.color = Color(0.25, 0.9, 1.0, 0.85)
|
||||
_world.add_child(_target_marker)
|
||||
_camera = Camera2D.new()
|
||||
_camera.name = "PreviewCamera"
|
||||
_world.add_child(_camera)
|
||||
_camera.make_current()
|
||||
_disable_collisions(_rig)
|
||||
call_deferred("_finish_rig_setup")
|
||||
|
||||
func _finish_rig_setup() -> void:
|
||||
var player := _rig.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if player != null:
|
||||
player.stop()
|
||||
_rig.stop_queue()
|
||||
_rig.clear_queue()
|
||||
_rig.set_facing_profile(StickmanRig.FacingProfile.FORWARD)
|
||||
for hand: int in [PreviewHand.LEFT, PreviewHand.RIGHT]:
|
||||
var target := _target_for(hand)
|
||||
if target != null:
|
||||
_hand_target_rests_local[hand] = target.position
|
||||
_apply_motion_pose()
|
||||
_update_target_marker()
|
||||
_rebuild_prop()
|
||||
_reframe_camera()
|
||||
|
||||
func set_document(shapes: Array[Dictionary], anchors: Dictionary, selected_anchor_id: String) -> void:
|
||||
_shapes = shapes.duplicate(true)
|
||||
_anchors = anchors.duplicate(true)
|
||||
_selected = selected_anchor_id
|
||||
_rebuild_prop()
|
||||
_reframe_camera()
|
||||
|
||||
func set_selected_anchor(anchor_id: String) -> void:
|
||||
_selected = anchor_id
|
||||
_rebuild_prop()
|
||||
|
||||
func set_preview_hand(hand: int) -> void:
|
||||
_hand = clampi(hand, PreviewHand.LEFT, PreviewHand.RIGHT)
|
||||
_manual_arm_override = false
|
||||
_apply_motion_pose()
|
||||
_rebuild_prop()
|
||||
|
||||
func set_preview_motion(motion: int) -> void:
|
||||
_motion = clampi(motion, PreviewMotion.FRONT_POSE, PreviewMotion.WALK_RIGHT)
|
||||
_manual_arm_override = false
|
||||
_apply_motion_pose(true)
|
||||
_rebuild_prop()
|
||||
_reframe_camera()
|
||||
|
||||
# Compatibility facade for Phase 9a callers. The visible Facing control was
|
||||
# removed in 9c; callers now map to the equivalent preview motion.
|
||||
func set_facing_profile(profile: int) -> void:
|
||||
if profile == StickmanRig.FacingProfile.LEFT:
|
||||
set_preview_motion(PreviewMotion.WALK_LEFT)
|
||||
elif profile == StickmanRig.FacingProfile.RIGHT:
|
||||
set_preview_motion(PreviewMotion.WALK_RIGHT)
|
||||
else:
|
||||
set_preview_motion(PreviewMotion.FRONT_POSE)
|
||||
|
||||
func set_walk_paused(paused: bool) -> void:
|
||||
if _motion == PreviewMotion.FRONT_POSE:
|
||||
_walk_paused = true
|
||||
_update_walk_button()
|
||||
return
|
||||
var player := _animation_player()
|
||||
_walk_paused = paused
|
||||
if player != null:
|
||||
if paused:
|
||||
player.pause()
|
||||
else:
|
||||
player.play()
|
||||
_manual_arm_override = false if not paused else _manual_arm_override
|
||||
_update_walk_button()
|
||||
|
||||
func toggle_walk_playback() -> void:
|
||||
set_walk_paused(not _walk_paused)
|
||||
|
||||
func set_arm_animation_enabled(enabled: bool) -> void:
|
||||
_animate = enabled
|
||||
if enabled and _motion == PreviewMotion.FRONT_POSE:
|
||||
set_preview_motion(PreviewMotion.WALK_RIGHT)
|
||||
elif not enabled:
|
||||
set_walk_paused(true)
|
||||
|
||||
func reset_arm_pose() -> void:
|
||||
if _motion != PreviewMotion.FRONT_POSE:
|
||||
set_walk_paused(true)
|
||||
var player := _animation_player()
|
||||
if player != null:
|
||||
player.seek(player.current_animation_position, true)
|
||||
else:
|
||||
_restore_standing_targets()
|
||||
_manual_arm_override = false
|
||||
_animate = false
|
||||
_update_target_marker()
|
||||
_update_prop_transform()
|
||||
|
||||
func is_preview_available() -> bool:
|
||||
return _rig != null and is_instance_valid(_rig) and _viewport != null
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not is_preview_available():
|
||||
return
|
||||
_update_target_marker()
|
||||
_update_prop_transform()
|
||||
|
||||
func _animation_player() -> AnimationPlayer:
|
||||
return _rig.get_node_or_null("AnimationPlayer") as AnimationPlayer if _rig != null else null
|
||||
|
||||
func _restore_standing_targets() -> void:
|
||||
for hand: int in [PreviewHand.LEFT, PreviewHand.RIGHT]:
|
||||
var target := _target_for(hand)
|
||||
if target != null and _hand_target_rests_local.has(hand):
|
||||
target.position = _hand_target_rests_local[hand]
|
||||
|
||||
func _apply_motion_pose(restart: bool = false) -> void:
|
||||
if _rig == null:
|
||||
return
|
||||
var player := _animation_player()
|
||||
if _motion == PreviewMotion.FRONT_POSE:
|
||||
_rig.set_facing_profile(StickmanRig.FacingProfile.FORWARD)
|
||||
_rig_root.scale = Vector2.ONE
|
||||
if player != null: player.stop(true)
|
||||
_restore_standing_targets()
|
||||
_walk_paused = true
|
||||
else:
|
||||
var profile: int = StickmanRig.FacingProfile.LEFT if _motion == PreviewMotion.WALK_LEFT else StickmanRig.FacingProfile.RIGHT
|
||||
_rig.set_facing_profile(profile)
|
||||
_rig.scale = Vector2.ONE
|
||||
_rig_root.scale = Vector2(-1.0, 1.0) if _motion == PreviewMotion.WALK_LEFT else Vector2.ONE
|
||||
if player != null:
|
||||
if restart or player.current_animation != "walk_right":
|
||||
player.play("walk_right")
|
||||
player.seek(0.0, true)
|
||||
else:
|
||||
player.play()
|
||||
_walk_paused = false
|
||||
_enable_arm_ik()
|
||||
_update_walk_button()
|
||||
|
||||
func _update_walk_button() -> void:
|
||||
if _walk_playback_button == null:
|
||||
return
|
||||
var front: bool = _motion == PreviewMotion.FRONT_POSE
|
||||
_walk_playback_button.disabled = front
|
||||
_walk_playback_button.text = "Resume Walk" if _walk_paused else "Pause Walk"
|
||||
_walk_playback_button.tooltip_text = "Choose Walk Left or Walk Right to preview walking." if front else ""
|
||||
|
||||
func _hand_target() -> Marker2D:
|
||||
return _target_for(_hand)
|
||||
|
||||
func _target_for(hand: int) -> Marker2D:
|
||||
if _rig == null:
|
||||
return null
|
||||
return _rig.get_node_or_null("IK_Targets/Left_Hand" if hand == PreviewHand.LEFT else "IK_Targets/Right_Hand") as Marker2D
|
||||
|
||||
func _lower_arm() -> Bone2D:
|
||||
if _rig == null:
|
||||
return null
|
||||
return _rig.get_node_or_null("Skeleton2D/Torso/LeftUpperArm/LeftLowerArm" if _hand == PreviewHand.LEFT else "Skeleton2D/Torso/RightUpperArm/RightLowerArm") as Bone2D
|
||||
|
||||
func _solved_wrist_transform() -> Transform2D:
|
||||
var lower_visual := _rig.get_node_or_null("Body/LeftLowerArm" if _hand == PreviewHand.LEFT else "Body/RightLowerArm") as Node2D
|
||||
var lower := _lower_arm()
|
||||
if lower == null or lower_visual == null:
|
||||
return Transform2D.IDENTITY
|
||||
var elbow := lower_visual.to_global(Vector2.ZERO)
|
||||
var wrist := lower_visual.to_global(Vector2(0.0, lower.length))
|
||||
var direction := (wrist - elbow).normalized()
|
||||
if direction.length_squared() < 0.0001:
|
||||
return Transform2D.IDENTITY
|
||||
return Transform2D(direction.angle(), wrist)
|
||||
|
||||
func _rebuild_prop() -> void:
|
||||
if _prop == null:
|
||||
return
|
||||
for child: Node in _prop.get_children():
|
||||
child.queue_free()
|
||||
var anchor: Dictionary = _anchors.get(_selected, {})
|
||||
var grip_ok := str(anchor.get("role", "")) == "grip" and _grip_allows_hand(anchor)
|
||||
_update_hand_choices(anchor)
|
||||
_prop.visible = grip_ok
|
||||
if _hint != null:
|
||||
_hint.text = "Drag the cyan target to pose the arm." if grip_ok else "Select a grip anchor compatible with this hand to preview holding."
|
||||
if not grip_ok:
|
||||
return
|
||||
for shape: Dictionary in _shapes:
|
||||
_add_shape_visual(shape)
|
||||
_update_prop_transform()
|
||||
|
||||
func _add_shape_visual(shape: Dictionary) -> void:
|
||||
# PropCanvas exports JSON-safe {x,y} dictionaries. Assigning that Array
|
||||
# directly to PackedVector2Array silently turns every point into zero.
|
||||
var points := _coerce_points(shape.get("points", PackedVector2Array()))
|
||||
if points.is_empty() and str(shape.get("shape_type", "polygon")) == "circle":
|
||||
var radius := maxf(float(shape.get("radius", 24.0)), 0.001)
|
||||
var center := Vector2.ZERO
|
||||
for index: int in 24:
|
||||
points.append(center + Vector2.from_angle(TAU * float(index) / 24.0) * radius)
|
||||
if points.is_empty():
|
||||
return
|
||||
var color := Color.from_string(str(shape.get("fill_color", "#999999")), Color.GRAY)
|
||||
if int(shape.get("shape_type", 0)) == 2 or not bool(shape.get("closed", true)):
|
||||
var line := Line2D.new()
|
||||
line.points = points
|
||||
line.width = maxf(float(shape.get("outline_width", 2.0)), 1.0)
|
||||
line.default_color = Color.from_string(str(shape.get("outline_color", "#eeeeee")), Color.WHITE)
|
||||
_prop.add_child(line)
|
||||
else:
|
||||
var polygon := Polygon2D.new()
|
||||
polygon.polygon = points
|
||||
polygon.color = color
|
||||
_prop.add_child(polygon)
|
||||
|
||||
func _coerce_points(raw: Variant) -> PackedVector2Array:
|
||||
if raw is PackedVector2Array:
|
||||
return raw as PackedVector2Array
|
||||
var result := PackedVector2Array()
|
||||
if raw is Array:
|
||||
for item: Variant in raw:
|
||||
if item is Vector2:
|
||||
result.append(item as Vector2)
|
||||
elif item is Dictionary:
|
||||
var point: Dictionary = item as Dictionary
|
||||
result.append(Vector2(float(point.get("x", 0.0)), float(point.get("y", 0.0))))
|
||||
return result
|
||||
|
||||
func _update_prop_transform() -> void:
|
||||
if _prop == null or not _prop.visible:
|
||||
return
|
||||
var anchor: Dictionary = _anchors.get(_selected, {})
|
||||
var mirror := _motion == PreviewMotion.WALK_LEFT
|
||||
_prop.global_transform = PropAnchorDataScript.solve_attachment_transform(_solved_wrist_transform(), anchor, Vector2.ONE, mirror)
|
||||
# PreviewProp follows PreviewRigRoot in tree order, so zero remains visible
|
||||
# above the stickman; negative puts it behind and positive remains in front.
|
||||
_prop.z_index = int(anchor.get("z_index_offset", 0))
|
||||
|
||||
func _grip_allows_hand(anchor: Dictionary) -> bool:
|
||||
var allowed := str(anchor.get("allowed_hands", "both"))
|
||||
return allowed == "both" or (allowed == "left_only" and _hand == PreviewHand.LEFT) or (allowed == "right_only" and _hand == PreviewHand.RIGHT)
|
||||
|
||||
func _update_target_marker() -> void:
|
||||
var target := _hand_target()
|
||||
if target != null and _target_marker != null:
|
||||
_target_marker.global_position = target.global_position
|
||||
_target_marker.visible = _prop == null or _prop.visible
|
||||
|
||||
func _resize_viewport() -> void:
|
||||
if _viewport != null:
|
||||
# SubViewportContainer.stretch owns the viewport size. Resizing it here
|
||||
# fights that ownership and produces an engine warning.
|
||||
_reframe_camera()
|
||||
|
||||
func _on_viewport_gui_input(event: InputEvent) -> void:
|
||||
if _camera == null:
|
||||
return
|
||||
if event is InputEventMouseButton:
|
||||
var mouse := event as InputEventMouseButton
|
||||
if mouse.button_index == MOUSE_BUTTON_WHEEL_UP or mouse.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
var world_before := _screen_to_world(mouse.position)
|
||||
_camera_zoom = clampf(_camera_zoom * (1.1 if mouse.button_index == MOUSE_BUTTON_WHEEL_UP else 1.0 / 1.1), _fitted_zoom * 0.3, _fitted_zoom * 3.0)
|
||||
_camera.zoom = Vector2(_camera_zoom, _camera_zoom)
|
||||
_camera.position += world_before - _screen_to_world(mouse.position)
|
||||
return
|
||||
var world_pos := _screen_to_world(mouse.position)
|
||||
if mouse.button_index == MOUSE_BUTTON_LEFT:
|
||||
if mouse.pressed and world_pos.distance_to(_target_marker.global_position) <= TARGET_RADIUS / _camera_zoom:
|
||||
_dragging_target = true
|
||||
if _motion != PreviewMotion.FRONT_POSE:
|
||||
set_walk_paused(true)
|
||||
_manual_arm_override = true
|
||||
elif not mouse.pressed:
|
||||
_dragging_target = false
|
||||
if mouse.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
_camera_dragging = mouse.pressed
|
||||
_last_pointer = mouse.position
|
||||
elif event is InputEventMouseMotion:
|
||||
var motion := event as InputEventMouseMotion
|
||||
if _dragging_target:
|
||||
_set_target_clamped(_screen_to_world(motion.position))
|
||||
elif _camera_dragging:
|
||||
_camera.position -= (motion.position - _last_pointer) / _camera_zoom
|
||||
_last_pointer = motion.position
|
||||
|
||||
func _screen_to_world(screen: Vector2) -> Vector2:
|
||||
return _viewport.get_canvas_transform().affine_inverse() * screen
|
||||
|
||||
func _set_target_clamped(requested: Vector2) -> void:
|
||||
var target := _hand_target()
|
||||
var lower := _lower_arm()
|
||||
if target == null or lower == null:
|
||||
return
|
||||
target.global_position = _clamp_target(requested)
|
||||
|
||||
func _clamp_target(requested: Vector2) -> Vector2:
|
||||
var target := _hand_target()
|
||||
var lower := _lower_arm()
|
||||
var upper := lower.get_parent() as Bone2D if lower != null else null
|
||||
if target == null or lower == null or upper == null:
|
||||
return requested
|
||||
var origin := upper.global_position
|
||||
var offset := requested - origin
|
||||
var rest: Vector2 = target.to_global(_hand_target_rests_local.get(_hand, Vector2.RIGHT)) - origin
|
||||
if offset.length_squared() < 0.0001:
|
||||
offset = rest.normalized() if rest.length_squared() > 0.0001 else Vector2.RIGHT
|
||||
var min_reach := absf(upper.length - lower.length) + 8.0
|
||||
var max_reach := maxf(min_reach, upper.length + lower.length - 8.0)
|
||||
return origin + offset.normalized() * clampf(offset.length(), min_reach, max_reach)
|
||||
|
||||
func _update_hand_choices(anchor: Dictionary) -> void:
|
||||
var allowed := str(anchor.get("allowed_hands", "both"))
|
||||
if allowed == "left_only" and _hand != PreviewHand.LEFT:
|
||||
_hand = PreviewHand.LEFT
|
||||
reset_arm_pose()
|
||||
if allowed == "right_only" and _hand != PreviewHand.RIGHT:
|
||||
_hand = PreviewHand.RIGHT
|
||||
reset_arm_pose()
|
||||
if _hand_option == null:
|
||||
return
|
||||
_hand_option.set_item_disabled(PreviewHand.LEFT, allowed == "right_only")
|
||||
_hand_option.set_item_disabled(PreviewHand.RIGHT, allowed == "left_only")
|
||||
_hand_option.select(_hand)
|
||||
|
||||
func _enable_arm_ik() -> void:
|
||||
var skeleton := _rig.get_node_or_null("Skeleton2D") as Skeleton2D
|
||||
if skeleton == null:
|
||||
return
|
||||
var stack := skeleton.get_modification_stack()
|
||||
if stack == null:
|
||||
return
|
||||
for index: int in 2:
|
||||
var modification := stack.get_modification(index)
|
||||
if modification is SkeletonModification2DTwoBoneIK:
|
||||
(modification as SkeletonModification2DTwoBoneIK).enabled = true
|
||||
|
||||
func _reframe_camera() -> void:
|
||||
if _camera == null or _viewport == null or _viewport.size.x < 1:
|
||||
return
|
||||
var bounds := _visual_bounds(true)
|
||||
if bounds.size.length_squared() < 0.001:
|
||||
return
|
||||
bounds = bounds.grow(32.0)
|
||||
_camera.position = bounds.get_center()
|
||||
_fitted_zoom = minf(float(_viewport.size.x) / bounds.size.x, float(_viewport.size.y) / bounds.size.y)
|
||||
_fitted_zoom = maxf(_fitted_zoom, 0.001)
|
||||
_camera_zoom = _fitted_zoom
|
||||
_camera.zoom = Vector2(_camera_zoom, _camera_zoom)
|
||||
|
||||
func _visual_bounds(full_figure: bool) -> Rect2:
|
||||
var points := PackedVector2Array()
|
||||
_collect_visual_points(_rig.get_node_or_null("Body"), points, full_figure)
|
||||
_collect_visual_points(_prop, points, true)
|
||||
if points.is_empty():
|
||||
return Rect2(RIG_CENTER - Vector2(200.0, 200.0), Vector2(400.0, 400.0))
|
||||
var rect := Rect2(points[0], Vector2.ZERO)
|
||||
for point: Vector2 in points:
|
||||
rect = rect.expand(point)
|
||||
return rect
|
||||
|
||||
func get_prop_global_bounds() -> Rect2:
|
||||
return _visual_bounds_for_node(_prop)
|
||||
|
||||
func get_preview_world_rect() -> Rect2:
|
||||
if _viewport == null:
|
||||
return Rect2()
|
||||
var transform := _viewport.get_canvas_transform().affine_inverse()
|
||||
var top_left := transform * Vector2.ZERO
|
||||
var bottom_right := transform * Vector2(_viewport.size)
|
||||
return Rect2(top_left, bottom_right - top_left).abs()
|
||||
|
||||
func get_solved_wrist_transform() -> Transform2D:
|
||||
return _solved_wrist_transform()
|
||||
|
||||
func _visual_bounds_for_node(node: Node) -> Rect2:
|
||||
var points := PackedVector2Array()
|
||||
_collect_visual_points(node, points, true)
|
||||
if points.is_empty():
|
||||
return Rect2()
|
||||
var rect := Rect2(points[0], Vector2.ZERO)
|
||||
for point: Vector2 in points:
|
||||
rect = rect.expand(point)
|
||||
return rect
|
||||
|
||||
func _collect_visual_points(node: Node, out: PackedVector2Array, full_figure: bool) -> void:
|
||||
if node == null or not (node as CanvasItem).visible:
|
||||
return
|
||||
var include := full_figure or not (node.name.contains("Leg"))
|
||||
if include and node is Polygon2D:
|
||||
for point: Vector2 in (node as Polygon2D).polygon:
|
||||
out.append((node as Polygon2D).to_global(point))
|
||||
if include and node is Line2D:
|
||||
for point: Vector2 in (node as Line2D).points:
|
||||
out.append((node as Line2D).to_global(point))
|
||||
for child: Node in node.get_children():
|
||||
_collect_visual_points(child, out, full_figure)
|
||||
|
||||
func _disable_collisions(node: Node) -> void:
|
||||
if node is CollisionObject2D:
|
||||
var body := node as CollisionObject2D
|
||||
body.collision_layer = 0
|
||||
body.collision_mask = 0
|
||||
for child: Node in node.get_children():
|
||||
_disable_collisions(child)
|
||||
Reference in New Issue
Block a user