Manipulation: System vs Parent

The Core Split

In 5.0.4, manipulation is divided into two distinct responsibilities:

  1. ManipulationSystem: The “Brain”. Owns business logic, state transitions, and validation.
  2. ManipulationParent: The “Body”. Owns visual transforms, scene hierarchy, and input handling for rotation/flip.

Why This Split Exists

The architecture intentionally separates orchestration from presentation.

  • Testability: The ManipulationSystem can be tested (mostly) without needing a complex scene tree, as it operates on state and signals.
  • Clarity: The node hierarchy remains understandable. You know exactly where to look for rotation logic (Parent) vs validation logic (System).
  • Stability: By isolating transform logic, we prevent “drift” where an object slowly moves off-grid due to floating point errors in repeated calculations.

Responsibilities

ComponentRoleOwnsDoes NOT Own
ManipulationSystemBusiness LogicLifecycle (start/commit), validation (can I move this?), state transitions (is_moving), API (try_move).Direct visual transforms, local rotation math, scene tree parentage.
ManipulationParentVisual LayerRotation/Flip/Scale containers, transform input handling, holding the ghost/preview object.Validation rules, resource consumption, grid occupancy checks.

Scene Hierarchy (Mental Model)

When you pick up an object, the hierarchy temporarily looks like this:

1
2
3
4
GridPositioner2D (The cursor/grid snapper)
  \-- ManipulationParent (The rotator/flipper)
      +-- IndicatorManager (Visual feedback)
      \-- Preview / ghost object (The object being moved)

The GridPositioner2D moves the entire assembly to the target grid cell. The ManipulationParent rotates the assembly around that center point.


Critical 5.0.4 Behaviors

1. Movable Validation

ManipulationSystem.try_move() strictly enforces the is_movable() check on the source object.

  • Behavior: The system will reject the move with a failure message if ManipulatableSettings.movable is false.

2. Transform Preservation

When a move completes, the accumulated transform (rotations applied during the move) must be preserved.

  • Flow: Move Start → Copy original transform → User rotates/flips → Place → Apply final transform to new instance.
  • Bug Watch: If your objects reset rotation after placement, check that you aren’t overwriting the transform in _ready().

3. Stale Move Copy Protection (5.0.4)

\n5.0.4 update: Previous manipulations are now properly cancelled before starting new ones. This prevents stale collision shapes from persisting and interfering with new indicators.

4. Source Deletion Safety (5.0.4)

\n5.0.4 update: Fixed hard crash when the manipulation source is deleted during an active move. Indicators parented in the scene tree are now properly remove_child()’d before freeing, and collision exclusions are cleared on cancel/finish.

5. State Reset Fix (5.0.4)

\n5.0.4 update: Fixed a critical bug where _states.manipulation.data == null was a comparison (discarding the result) instead of an assignment. Stale manipulation data is now properly cleared.

6. Visual Desyncs

Because the parent handles the visual transform, if you manually set the node.rotation of the object inside the parent, you might get double rotations or unexpected offsets.

  • Fix: Always rotate the ManipulationParent using apply_rotation() or apply_grid_rotation_clockwise(), never the child object directly.

Glossary

  • ManipulationSystem: The singleton that orchestrates move/rotate logic.
  • ManipulationParent: The Node2D that holds the preview object.
  • Source Object: The original object in the world being moved.
  • Preview Object: The temporary visual copy attached to the mouse cursor.
  • GridPositioner2D: The component that snaps the preview to the grid cell center.

Common Pitfalls

  • Script Access: Do not try to get_node("ManipulationSystem") from inside a random scene script. Use dependency injection or a global singleton reference if your architecture supports it.
  • Input Consumption: The ManipulationParent often handles input for rotation. If your camera controller consumes all input, the rotation keys might not trigger. Ensure input propagation is handled correctly (e.g., _unhandled_input).
  • Orphaned Previews: If the system is interrupted (e.g., the player dies while building), ensure cancel_interaction() is called to clean up the ManipulationParent and its preview child.

\n5.0.4 update: Orphaned preview leaks are now much less likely due to improved cleanup guards, but explicit cancel_interaction() calls are still recommended on state transitions.


Example: Listening to Manipulation State

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Note: This guide shows the pattern — actual implementation varies
func _ready() -> void:
    var sys = get_node("/root/ManipulationSystem")
    sys.manipulation_started.connect(_on_manipulation_started)

func _on_manipulation_started(context: ManipulationContext) -> void:
    # The System tells us WHAT happened
    print("Started moving: ", context.source_object.name)

    # The Parent handles HOW it looks
    var parent = context.preview_parent
    parent.modulate = Color(1, 1, 1, 0.5)  # Make it semi-transparent