Implement Phase 10a - Prop Scaling

- Added specifications for Phase 10a, introducing permanent size changes to `.prop` documents and reversible uniform instance scaling in Sandbox EDIT mode.
- Updated `PropCanvas`, `PropAnchorData`, `PropEditor`, `StageGizmos`, `SandboxStage`, and `StageSelection` classes to support new scaling features.
- Implemented document transactions for scaling, including normalization to a 96 px maximum dimension.
- Introduced a new **Transform Whole Prop** tool in the Prop Editor for moving, rotating, and resizing props.
- Created unit tests for the new scaling functionality, validating shape scaling, anchor transformations, and whole-prop interactions.
- Ensured compatibility with existing prop formats and maintained the integrity of the document structure during scaling operations.
This commit is contained in:
2026-09-22 22:23:31 -04:00
parent 97f3730413
commit 0a7f97cec7
18 changed files with 1782 additions and 78 deletions
+25 -2
View File
@@ -242,6 +242,13 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
`prop_block.gd`, `prop_canvas.gd`, `thumbnails/prop_thumbnail.gd`, and
`thumbnails/thumbnail_cache.gd` via `preload` consts. `_apply_json_data(json, is_recovery =
false) -> String` returns `""` on success or an error string (dialog on failure).
- **Phase 10a final controls:** the Arrange top bar orders **Transform Whole Prop**,
**Normalize Size**, **Center Prop**, and **Show Stickman Guide**. The numeric scale textbox
and Apply Scale button are not exposed. Transform Whole Prop is Arrange-only and previews
whole-document move, origin-based rotation, and uniform resize; Snap rounds move deltas,
Ctrl snaps rotation to 15°, editable anchors follow, and Esc cancels the preview. Reshape
and Attach hide Transform Whole Prop and Normalize Size; Center Prop and Show Stickman Guide
remain available.
- `scripts/prop_canvas.gd` — `class_name PropCanvas`, `extends Control`; the Prop Editor's
**multi-shape vector canvas** (Phase 9, **not used by the editor**), mirroring
`BodyPartPanel`'s proven input model (cursor-centered zoom, pinch/pan gestures, middle-mouse
@@ -317,6 +324,11 @@ preview_area.size * 0.5`, so it also re-centers on window resize; it remains a
~2 px wider) + a **bright core pass** (`SELECTION_CORE_COLOR`, white, original width). The
pair is visible on **any** background (the Round 6 fill-adaptive `contrast_color_for` was
removed because it could not know what sits *behind* a selected shape).
- **Phase 10a whole-prop transform:** the canvas exposes typed move/rotate/resize request
signals and renders a union gizmo while Transform Whole Prop is armed. Body, rotation-dot,
and corner gestures use snapshot-only previews and commit one atomic document edit on
release; `set_whole_prop_transform_enabled()` and related transform names are the preferred
API. Resize-named compatibility aliases remain internally for existing controller paths.
- `scripts/prop_file.gd` — `class_name PropFile`, `extends RefCounted`; the static `.prop`
schema + converters (Phase 9, **not used by the editor**). Consts `FILE_VERSION := "1.0"`,
`FILE_EXT := ".prop"`, `DEFAULT_DIR := "res://props"`, `SUPPORTED_VERSIONS: ["1.0"]`,
@@ -897,6 +909,11 @@ PackedVector2Array`, `radius: float`, `fill_color`, `outline_color`, `outline_wi
matching direct-state contact row as world-space normal/position, with a center-direction
fallback when timing leaves no row. The physics signal covers prop-vs-prop and Phase 4.5
stickman-prop contacts; the old geometric feet-point stickman test is removed.
- **Phase 10a instance scaling:** Sandbox EDIT exposes four uniform corner crosses only for
a single `PropBlock`. During PLAY, the authored root scale is moved into child visuals and
collision geometry, including the custom one-hull path and nonuniform circle fallback;
returning to EDIT restores the authored root scale. Preset mass and material physics stay
unchanged.
- `scripts/trigger_area.gd` — `class_name TriggerArea`, `extends Node2D`; a **placeable sensor**
(Phase 4, **not used by the editor**). `@export size: Vector2` (default 96×96), `get_area_rect()
-> Rect2` (centered local-space rectangle), and `_draw()` (translucent green fill +
@@ -973,7 +990,10 @@ KNOCK_UP_VELOCITY = (0, -450))` (mass-scaled `apply_central_impulse` on every ra
`global_position`/`global_rotation` + zeroed velocity. Because the freeze+teleport can take a
physics frame or two to settle, `_restore_authored_state()` is also re-asserted over the next
few `_physics_process` frames (`_restore_frames_left`) — so every Play session starts from and
returns to the same authored state. **Placement ghost:** while a palette
returns to the same authored state. **Phase 10a scale lifecycle:** prop corner-scale gestures
use the existing transform signal path, synchronize the frozen body during EDIT, save the
authored scale on commit, and retain that scale across PLAY → EDIT → PLAY. **Placement ghost:**
while a palette
item is active, `set_placement_mode(id)` spawns a translucent (`modulate.a = 0.5`),
non-colliding (`collision_layer/mask = 0`, frozen) copy of the object reparented out of
`World` into `_ghost_holder`; `_process()` tracks it to the cursor (+ snap) via
@@ -1245,7 +1265,10 @@ page_count}` helper. `_build_cell(entry)` returns a `Button` with a `TextureRect
`_rotate_start_rotation + (current − start angle)`, with holding **Ctrl** snapping to
15° increments via `snappedf(new_rotation, deg_to_rad(15.0))`.
`_draw()` draws a selection outline for **every** selected node. Pure `_draw()` +
distance-based hit-testing (no `Area2D`); `enum Handle { NONE, TRANSLATE, ROTATE }`.
distance-based hit-testing (no `Area2D`); `enum Handle { NONE, TRANSLATE, ROTATE, SCALE }`.
**Phase 10a** adds four constant-screen-size corner crosses for a single `PropBlock`; the
`Handle.SCALE` drag applies one positive uniform factor around the root origin through the
existing transform signals. Other selections keep the existing gizmos.
`transform_committed(nodes: Array[Node2D])` emitted on drag end. `set_enabled(false)` hides +
disables in PLAY. Also draws the box-select marquee via `set_box_rect(rect)`.
- `scripts/stage_grid.gd` — `class_name StageGrid`, `extends Node2D`; the optional world-space