30 KiB
Phase 10a — Prop Scaling
Status: implemented and user verified (amended and verified 2026-09-22).
Source: docs/archive/plans/PHASE_10a_PROP_SCALING.md. Phase 10a adds two distinct operations:
permanent size changes to a .prop document in the Prop Editor, and reversible
uniform instance scaling of one PropBlock in Sandbox EDIT mode. The final UI uses
origin-based Normalize Size and direct whole-prop Arrange gestures; it does not expose
a numeric document-scale control. Sandbox keeps single-prop corner handles that scale
around the prop's local origin.
1. Existing contracts and scope
PropCanvasstores every shape'sPackedVector2Arraypoints and computes the union bounds.PropEditorowns the matching anchor dictionary, dirty/autosave flow, inspector, and the mode-specific top-bar controls.PropAnchorDataalways synthesizes a fixedoriginat(0, 0).- Circles carry both
pointsandradius; both must remain consistent after a document-wide uniform scale. Keepshape_type,closed,vertex_flags,reshaped, fill, outline color, and material preset unchanged. StageGizmoscurrently has translation and rotation gestures and emitstransform_started,transform_updated, andtransform_committed.SandboxStagealready uses these signals to sync frozen bodies, save authored transforms, rebuild spatial cells, and mark the scene dirty.SandboxStage._collect_scene_data()already writes propscale; scene loading already reads it._restore_authored_state()restores authored position and rotation and zeroes rigid-body velocities without changing scale. No.propor.sceneversion change is required.
In scope: document scale/normalize controls, a passive 1:1 guide in the Prop Editor, prop-only Sandbox scale handles, and focused verification. Keep the existing built-in/custom prop rendering and one-hull custom collision policy. Do not add Sandbox numeric scale fields, presets, new authoring modes, or document-wide undo/redo. Debt item #37 continues to track undo.
The 2026-09-22 amendment replaces the visible, Arrange-only Resize Whole Prop toggle with Transform Whole Prop. One union gizmo moves, rotates, or uniformly resizes the complete document. It does not change Sandbox scaling.
2. Class and API contracts
| Class / file | Required contract |
|---|---|
class_name PropCanvas extends Control (scripts/editors/prop/prop_canvas.gd) |
Keep can_scale_all_shapes() / scale_all_shapes(), and add equivalent validated document helpers can_translate_all_shapes(delta: Vector2) -> bool, translate_all_shapes(delta: Vector2) -> bool, can_rotate_all_shapes(angle_rad: float) -> bool, and rotate_all_shapes(angle_rad: float) -> bool. Successful commit helpers mutate every shape and emit shape_changed(shapes: Array[Dictionary]) exactly once; preflight/no-op/failure never mutates or emits. Add the transient whole-prop tool API in §3.4. get_shapes_bounds() -> Rect2 remains authoritative for its union gizmo. |
class_name PropAnchorData extends RefCounted (scripts/props/prop_anchor_data.gd) |
Keep pure try_scale_non_origin(), and add pure try_translate_non_origin(anchors: Dictionary, delta: Vector2) -> Dictionary and try_rotate_non_origin(anchors: Dictionary, angle_rad: float) -> Dictionary, each returning { "ok": bool, "anchors": Dictionary }. Translation changes editable positions only. Rotation rotates editable positions about (0,0) and adds the angle to their wrapped rotation_deg. Every helper regenerates canonical origin unchanged and rejects invalid results without partially modifying input. Existing translated_non_origin() may delegate to the validated helper. |
class_name PropEditor extends Control (scripts/editors/prop/prop_editor.gd) |
Own three atomic document transactions: _translate_document(delta: Vector2) -> bool, _rotate_document(angle_rad: float) -> bool, and existing _scale_document(factor: float) -> bool. Each prevalidates shapes and anchors, applies the anchor copy, commits canvas geometry once, rolls anchors back on an unexpected canvas failure, and refreshes inspector/preview/buttons through the existing shape-change path. Connect the typed request signals in §3.4. Persist only guide visibility in editor settings. |
class_name StageGizmos extends Node2D (scripts/stage/stage_gizmos.gd) |
Extend enum Handle with SCALE. Draw and hit-test four corner crosses only when _targets.size() == 1 and _targets[0] is PropBlock; keep hit radii and strokes in screen pixels via camera zoom. Capture start scale: Vector2, world origin, selected corner, and cursor at gesture start. Use one positive uniform multiplier through the drag; emit the existing transform signals. |
class_name SandboxStage extends Node2D (scripts/editors/stage/sandbox_stage.gd) |
Keep the current EDIT pointer routing and existing transform-signal handlers. Route scale gestures through the same physics-sync and authored-state commit path as move/rotate. No save/load or PLAY→EDIT reset rewrite is needed unless verification exposes a defect. |
class_name StageSelection extends RefCounted (scripts/stage/stage_selection.gd) |
Its existing get_world_aabb(node: Node2D) -> Rect2 transforms visual points to world space. Verify scaled/rotated built-in and custom props; change the method only if a specific failing case appears. |
No new persistent shape fields are required. The whole-prop tool adds three typed commit-request signals. Use static type hints for new fields, parameters, local values where inference is ambiguous, and return values. The signal interfaces are:
signal shape_changed(shapes: Array[Dictionary])
signal transform_started(nodes: Array[Node2D])
signal transform_updated(nodes: Array[Node2D])
signal transform_committed(nodes: Array[Node2D])
signal whole_prop_move_requested(delta: Vector2)
signal whole_prop_rotate_requested(angle_rad: float)
signal whole_prop_resize_requested(factor: float)
3. Prop Editor document transaction
3.1 Explicit scale and 96 × 96 normalization
Do not expose a numeric scale textbox or Apply Scale button. In Arrange, the
mode top bar presents Transform Whole Prop, 📏 Normalize Size, 🎯 Center
Prop, and Show Stickman Guide, in that order. Normalize Size is a
document operation, not saved UI state: it uniformly fits valid shape-point
bounds to a 96 px maximum dimension around (0, 0), preserves proportional
anchor offsets, and leaves outline width unchanged. Disable it when there is no
valid shape geometry. Transform Whole Prop supplies direct move, rotate, and
uniform resize gestures; its detailed snapshot and commit contract is in §3.4.
Transform Whole Prop and Normalize Size are hidden in Reshape and Attach, while
Center Prop and Show Stickman Guide remain available.
Normalize uses the shape-point union already returned by get_shapes_bounds():
var extent: float = maxf(bounds.size.x, bounds.size.y)
var factor: float = 96.0 / extent
For a nonempty finite bounds with extent > 0, apply this factor uniformly
around (0, 0). The resulting maximum geometry extent is 96 world pixels;
the smaller dimension retains its aspect ratio. A line with one nonzero
dimension is valid. Empty or point-only geometry, zero extent, nonfinite
values, or an output coordinate/radius that cannot be represented safely make
the action a no-op. Do not silently clamp the factor and miss the 96 px target.
Use a reasonable coordinate safety cap documented in code rather than relying
on floating-point overflow. Normalization does not call Center Prop:
off-origin content stays off-origin at its new proportional offset.
3.2 Geometry and anchor invariants
The transaction computes its result from a deep copy before updating either
owner. Transform every point of every polygon, line, and circle as point *= factor. For every circle, also set radius *= factor. Do not regenerate
circle points: that would erase an intentionally reshaped circle or move its
center. Preserve reshaped exactly as stored. Keep vertex_flags in their
original index order. Outline width is a style value and stays unchanged;
the 96 px target measures shape points, not stroke thickness. Preserve the
canonical origin while multiplying all editable anchor positions by the same
factor. Anchor rotations and metadata stay unchanged.
Call _canvas.can_scale_all_shapes(factor) and
PropAnchorData.try_scale_non_origin(_anchors, factor) before changing either
owner. A failed preflight leaves both unchanged. Apply the prevalidated anchor
copy without emitting a separate document edit, then commit the canvas shapes
with the same factor; this synchronous path must use the same canvas validation
as preflight. Let its one shape_changed emission use the existing
PropEditor._on_canvas_shape_changed() dirty/autosave/preview refresh path.
Refresh selected anchor controls and any disabled button state afterward.
The scale operation must not reset pan/zoom, selection, edit mode, clipboard,
or guide visibility. Both successful actions mark the document dirty once;
changing the guide toggle does not.
Example with factor 2:
Before: circle points centered at (20, 10), radius 12;
primary_grip.position = (8, -3), origin = (0, 0)
After: circle points centered at (40, 20), radius 24;
primary_grip.position = (16, -6), origin = (0, 0)
Save, autosave, recovery, and load keep the existing .prop v1.0 shape and
anchor representation. PropFile._serialize_shape() must continue to coerce
export-format point dictionaries; never assign JSON arrays directly to a
PackedVector2Array.
3.3 Stickman scale guide
Add a Show Stickman Guide CheckBox in the Prop Editor canvas controls.
Default is OFF for a fresh settings file. Persist the Boolean as
show_stickman_guide in user://prop_editor_settings.json alongside
recent_props; a missing key means false, and old settings files remain
valid. Apply the loaded value during _ready() after the controls exist.
Toggling calls PropCanvas.set_show_stickman_guide(), saves settings, and
redraws, without changing _dirty, autosave, .prop data, or scene data.
Draw the same 1:1 head circle and bone-line reference already described by
WholeStickmanPreview.GUIDE_JOINTS, GUIDE_BONES, and
GUIDE_HEAD_RADIUS. Prefer preloading that source's constants or extracting
a shared constant-only guide source if compilation requires it; do not copy
the numerical joint table into a second implementation. Draw raw master
coordinates with Hips = Vector2.ZERO after _draw_grid() and before user
shapes, under the canvas's existing pan/zoom transform. This keeps a 96 px
prop directly comparable with the approximately 940 px standing figure in
every view. Use muted translucent strokes with width divided by _zoom for
legibility; keep the guide noninteractive and out of hit-testing, shape bounds,
thumbnails, and .prop serialization. Existing origin marker and selection
gizmos remain above it. New/load/reset do not forcibly change its saved
visibility preference.
3.4 Arrange-only Transform Whole Prop tool (2026-09-22 decision)
Interaction and scope. Replace ↗ Resize Whole Prop with one prominent
toggle labeled ✥ Transform Whole Prop in the Arrange mode top bar. It is available only in Arrange
(EditMode.TRANSFORM) when valid shape geometry exists. It starts OFF, remains
armed after each successful gesture, is session-only, and is excluded from
settings and .prop data. This extends the already learned per-shape Arrange
mapping to the full document:
| Target | Gesture | Whole-document result |
|---|---|---|
| Any visible shape body | Drag | Move all shapes and editable anchors. Transparent gaps inside the union box do nothing. |
| Dot below union bounds | Drag | Rotate all shapes and editable anchors about canonical (0,0). Ctrl snaps to 15°. |
| Any of four union corners | Drag | Uniformly resize all shapes and editable anchors about canonical (0,0). |
Hit-test in the strict order corner → rotation dot → visible shape body.
This keeps small handles usable when they overlap geometry. Suppress per-shape
selection, translation/rotation/scale gizmos, and right-click menus while the
tool is armed. Preserve _selected_shape_idx internally and restore its
selection/inspector when the toggle turns OFF. Existing wheel/pinch zoom and
middle-button/touchpad pan remain available during the tool.
Draw one two-tone union-bounds outline, four large constant-screen-size corner crosses (at least 12 px hit radius), and the familiar constant-screen-size rotation dot below the union AABB. Keep the canonical origin marker visible; if Show Other Markers hid it, restore that preference when the tool exits. Also draw small passive markers for every non-origin anchor, with grip markers clearly visible. During a gesture, the union outline and markers follow the temporary preview. These markers are noninteractive and excluded from bounds, hit-testing, thumbnails, and serialization. The tooltip must say that body, dot, and corners move, rotate, and resize the whole prop including grip markers.
Typed Canvas API and signals. Replace resize-specific tool methods with:
enum WholePropDrag { NONE, MOVE, ROTATE, SCALE }
func set_whole_prop_transform_enabled(enabled: bool) -> void
func is_whole_prop_transform_enabled() -> bool
func is_whole_prop_transform_dragging() -> bool
func cancel_whole_prop_transform() -> void
signal whole_prop_move_requested(delta: Vector2)
signal whole_prop_rotate_requested(angle_rad: float)
signal whole_prop_resize_requested(factor: float)
set_whole_prop_transform_enabled(false) cancels a live preview before
disarming. cancel_whole_prop_transform() clears the active drag, snapshots,
preview arrays, accumulated rotation, and pending payload without changing
authoritative data or the armed state. PropEditor connects the three signals
to _translate_document, _rotate_document, and _scale_document. Do not
emit anchors_changed for these document gestures.
The implementation also retains resize-named compatibility aliases internally
(set_whole_prop_resize_enabled, is_whole_prop_resize_enabled,
is_whole_prop_resize_dragging, and cancel_whole_prop_resize) so existing
controller paths continue to target the same Transform Whole Prop state. They
are not separate user-facing tools.
Snapshot and preview contract. On pointer-down, capture deep starting
copies of shapes: Array[Dictionary] and normalized anchors, starting union
bounds, pointer, selected handle, and any gesture-specific state. Render all
motion from those starting copies into temporary preview shape/anchor arrays.
Do not replace authoritative shapes or editor _anchors before release;
therefore get_shape_data(), save, autosave, and recovery always see the
pre-drag document if their timer runs during a drag. Preview motion can redraw
each input event but never emits edit signals or arms autosave.
All gestures preserve shape type, layer order, closed, vertex_flags,
reshaped, fill/outline, and outline width, plus anchor role, z offset,
allowed hands, and IDs. Canonical origin remains at (0,0) with rotation
0°. Circle behavior is explicit: move and rotate transform its stored points
but leave radius unchanged; resize transforms its points and multiplies
radius. Never regenerate circle points.
Move math. Capture start_pointer; compute from the baseline every frame:
var delta: Vector2 = current_pointer - start_pointer
if snap_enabled:
delta = Vector2(snappedf(delta.x, grid_size), snappedf(delta.y, grid_size))
preview_point = start_point + delta
preview_anchor.position = start_anchor.position + delta
Snap the delta, so all layers and anchors retain their relative placement
even when the starting document is off-grid. Anchor rotation_deg and circle
radius remain unchanged. Moving the whole prop means moving authored geometry
and editable anchor markers relative to the fixed document pivot; the canonical
origin itself does not translate. A zero delta is a no-op.
Rotate math. The pivot is always canonical Vector2.ZERO, independent of
the union-bounds center. Capture the pointer angle around origin and accumulate
wrapped frame deltas so crossing -PI/PI cannot jump:
var frame_delta: float = wrapf(current_angle - previous_angle, -PI, PI)
total_angle += frame_delta
previous_angle = current_angle
var preview_angle: float = snappedf(total_angle, deg_to_rad(15.0)) if ctrl_pressed else total_angle
preview_point = start_point.rotated(preview_angle)
preview_anchor.position = start_anchor.position.rotated(preview_angle)
preview_anchor.rotation_deg = wrapf(start_anchor.rotation_deg + rad_to_deg(preview_angle), -180.0, 180.0)
Reject pointer-down or motion whose angle around origin is nonfinite or too
close to origin for stable measurement. Always render the captured baseline
rotated by preview_angle; never rotate the prior preview. The rotation dot's
union-bounds location is only an input affordance and does not change the pivot.
Circle radius and reshaped remain unchanged. An approximately zero angle is
a no-op.
Resize math. Preserve the implemented uniform resize rule. Compute the gesture direction from the starting union center and selected starting corner, but measure pointer travel from the exact captured press position:
var direction: Vector2 = start_corner - start_bounds.get_center()
var pointer_delta: Vector2 = current_pointer - start_pointer
var factor: float = 1.0 + pointer_delta.dot(direction) / direction.length_squared()
The factor is exactly 1.0 on pointer-down even when the press is off the
handle center. Apply it to points, circle radii, and editable anchor positions
about (0,0); anchor rotations stay unchanged. Clamp to positive finite
0.01..10.0, satisfy canvas coordinate/radius and anchor preflight, and never
cross zero or flip geometry. Reject a near-zero direction denominator. An
approximately 1.0 factor is a no-op.
Commit and cancellation. On left release, capture the final payload, clear
the temporary preview, and emit exactly one matching typed request signal.
The controller transaction prevalidates geometry and anchors, applies both
atomically, and lets the canvas's single shape_changed emission mark dirty,
arm autosave once, refresh inspector/preview/buttons, and retain the armed
Transform Whole Prop tool. An invalid/no-op release emits nothing. On an
unexpected canvas commit failure, restore the original anchors and leave the
document unchanged.
Esc during a drag cancels only that preview and keeps the tool armed. Esc while
armed but idle turns the tool OFF. Toggling OFF, changing to Reshape/Attach,
New, Load, or Clear cancels a live preview and disarms before changing mode or
document. Route Esc through PropEditor._unhandled_key_input() while respecting
dialogs and text fields, and consume it. A committed gesture is not reverted by
Esc because document-wide UndoRedo remains deferred under debt #37. Each move,
rotate, or resize gesture must become one future shapes-plus-anchors undo unit.
Arrange, shape scope Transform Whole Prop ON
selected-shape bbox all-shape union bbox
body = move one shape body = move entire document
dot = rotate one shape dot = rotate entire document
corners = resize one shape corners = resize entire document
pointer down → deep snapshots → temporary preview from baseline
→ release → one typed controller transaction
→ shape_changed once → dirty/autosave once → tool remains armed
↘ Esc/mode/data change → discard preview, no dirty change
4. Sandbox Stage instance gesture
4.1 Visual and input contract
When exactly one PropBlock is selected in EDIT, draw four crosses on the
corners of StageSelection.get_world_aabb(prop). Use a contrasting backing
and bright core so they remain visible on light/dark props, and a 12 screen px
hit radius. Handle hit-testing precedes the existing rotate ring, then normal
body selection; an active placement mode keeps the current gizmo precedence.
No scale handles appear for multi-selection, terrain, trigger areas,
stickmen, missing-prop placeholders, DIRECT, or PLAY. The existing rotation
ring and body translation remain available.
On pointer-down, record the selected corner's world point, the prop's
global_position as pivot, and its starting scale: Vector2. For each drag
update, project the cursor vector onto the start-corner vector around the
fixed pivot:
var start: Vector2 = start_corner_world - pivot_world
var current: Vector2 = cursor_world - pivot_world
var requested: float = current.dot(start) / start.length_squared()
var new_scale: Vector2 = start_scale * positive_clamped_multiplier
Guard near-zero denominators/nonfinite values. Clamp the multiplier so the
absolute values of both final scale components stay within 0.05..20.0 and
never cross zero; retain the signs of any previously saved scale. The gesture
multiplies both axes by the same positive factor, preserving the instance's
starting aspect ratio even if an older .scene contains nonuniform scale.
It changes only prop.scale, never global_position, global_rotation,
template vertices, radius, anchors, material_preset, or mass. Compute from
the captured starting scale on every motion event rather than multiplying the
previous frame, avoiding drift. Draw from the updated world AABB while moving.
One release emits one transform_committed and enters the existing dirty/save
pipeline. No motion or invalid gesture must not produce a scale edit; keep
the existing move/rotate behavior unless a focused regression needs repair.
4.2 Physics and lifecycle
The PropBlock root is a RigidBody2D; its child visuals and collision
geometry inherit its node transform. material_preset remains the sole mass
source (PropBlock.mass_for()), so scale must not recalculate mass. During
EDIT drag, use the existing _on_gizmo_transform_updated()/
_sync_editable_physics_transform() path to keep the frozen physics body in
sync. On commit, _on_transform_committed() saves authored state and rebuilds
grid cells. PLAY may move/rotate the prop physically; returning to EDIT must
restore authored position/rotation and zero velocities while retaining the
authored scale. Saving during PLAY must serialize authored position/rotation
and the selected scale as the current code already does.
Godot warns that scaling collision shapes can produce unexpected physics
behavior. Verify polygon and circle contacts at several positive scales in
the project engine, including a rotated prop and PLAY→EDIT→PLAY. If the
RigidBody2D root transform does not give matching collisions, adapt the
per-instance collision construction while keeping the scene's scale field,
visual size, preset mass, and source .prop unchanged. A visually scaled prop
with stale collision is not an acceptable implementation.
5. Data and signal flow
flowchart LR
UI[Whole-prop body, dot, or corner drag] --> V[Snapshot-only live preview]
V -->|one typed request on release| E[PropEditor validates transaction]
N[Normalize Size] --> E
E --> A[PropAnchorData transforms non-origin anchors]
E --> C[PropCanvas transforms every shape]
C -->|shape_changed once| D[Dirty + autosave + preview refresh]
D --> P[Existing PropFile v1.0 save]
P --> L[Sandbox PropBlock instance]
G[StageGizmos prop corner drag] -->|uniform prop.scale| S[Stage transform signals]
S --> F[Frozen-body sync + authored state + dirty]
F --> I[Existing .scene scale field]
Sandbox EDIT selection
├─ one PropBlock → AABB + rotate ring + four scale crosses
└─ any other selection → existing AABB/rotate behavior, no scale crosses
PLAY → EDIT → PLAY
authored {position, rotation, scale} → simulate → restore position/rotation
and zero velocities → retain authored scale → simulate at retained size
Example persisted values remain additive to existing formats:
{"version":"1.0","shapes":[{"shape_type":"circle","points":[{"x":24,"y":0},{"x":0,"y":24},{"x":-24,"y":0},{"x":0,"y":-24}],"radius":24,"reshaped":false}],"anchors":{"origin":{"role":"pivot","x":0,"y":0},"primary_grip":{"role":"grip","x":12,"y":-4}}}
{"type":"prop","file_id":"prop_1","asset_id":"res://content/props/ball.prop","position":{"x":100,"y":200},"rotation":0,"scale":{"x":1.5,"y":1.5},"material_preset":2}
The JSON fragments show only the fields relevant to scaling; ordinary serializer fields remain present in real files.
6. Implementation order and acceptance
- Keep the existing scale transaction, guide, and whole-prop resize behavior; add validated geometry/anchor move and rotate helpers plus atomic editor transactions.
- Rename and extend the Arrange-only toggle/gizmo with body move and rotation dot gestures, retaining snapshot preview and resize behavior.
- Implement prop-only Stage scale crosses and pointer gesture through the existing transform signal flow. Leave persistence/lifecycle code alone unless an assertion fails.
- Run Godot headless syntax checks for every changed script and focused
headless tests. Per project instructions, use
Godot_v4.7.1-stable_win64_console.exe --headless --check-only --script res://scripts/<changed>.gd --path C:\Godot4\stickman; if class caches need refresh, run a headless editor scan first, then repeat checks. Fix failures before handoff.
Acceptance checks:
- A
200%document-scale transaction doubles all point coordinates, circle radii, and editable anchor offsets, keeps origin(0,0), and produces one dirty edit. A reshaped circle remains reshaped; style widths, flags, and order remain. - Normalize a 192 × 48 bounds to 96 × 24; a nonzero off-origin center scales proportionally and is not silently centered. Invalid/empty geometry is a no-op. Save/load and autosave/recovery retain exact point and anchor values.
- The guide starts hidden with no settings file, can be enabled in all modes,
stays aligned with world origin through pan/zoom, survives editor restart
through settings, and never enters a
.propfile or a thumbnail. - In Arrange, Transform Whole Prop shows one union outline, four large corners, and a rotation dot. Body drag moves all points and non-origin anchors; with Snap ON, its baseline-derived delta lands on the current grid. Circle radius and anchor angles do not change. Canonical origin stays fixed.
- Rotation-dot drag rotates all points and anchor positions about
(0,0)and adds the same wrapped angle to every editable anchor'srotation_deg. Circle radius/primitive state stay fixed, crossing ±180° is continuous, and Ctrl produces 15° increments. - Corner drag retains the verified uniform origin-based resize behavior, including circle radius and anchor positions, without changing anchor angle.
- During every gesture, authoritative shape/anchor export stays at the baseline
while the union outline, shapes, and visible markers show preview copies.
Release performs one matching editor transaction, one
shape_changed, and one dirty/autosave arm. No-op release and cancellation emit nothing. - Esc cancels an active drag but leaves the tool armed; idle Esc, toggle OFF, mode switch, New, Load, or Clear cancel and disarm without a dirty edit. The tool stays armed after a successful commit. Turning it OFF restores the former selected shape and per-shape Arrange behavior.
- Focused headless tests assert numeric point/radius/anchor values, anchor-angle wrapping, snap deltas, signal counts, baseline immutability during preview, resize regression, no-op/cancel behavior, and selection/mode restoration.
- Exactly one selected prop exposes four usable crosses. Dragging retains its aspect ratio and pivot, never flips it, changes collision bounds along with visuals, and leaves material mass unchanged. Other object types and multiple selection do not expose scale handles.
- Save/reload retains prop scale. PLAY→EDIT restores position/rotation and zeroes velocities while retaining scale; a second PLAY collides at the retained size. Existing rotation, translation, selection, and custom-prop rendering still work.
7. Trade-offs and deferred work
- Origin-based sizing is predictable for anchor offsets and preserves the
fixed
origin, but an off-origin prop moves relative to its bbox center when resized. Center Prop remains the explicit remedy; no auto-centering. - Geometry bounds exclude outline stroke and physics center of mass, matching current Center Prop semantics and debt item #39.
- Stage scaling uses one rigid-body root transform and the existing
.scenescale field. Physics accuracy must be proved in the project engine; a per-instance collider adjustment is a contingency, not a separate user UI. - No document-wide undo is added; debt item #37 remains open.
- One visible Transform Whole Prop toggle avoids three competing subtools, a hidden modifier key, or a fourth primary mode. Its body/dot/corner mapping mirrors per-shape Arrange. Each completed move, rotate, or resize is one future undo unit; Esc provides cancellation before commit, while existing save/recovery protects committed work until document-wide undo is designed.
- Move translates authored geometry and non-origin anchors relative to the fixed canonical pivot. Rotate uses that same pivot and changes anchor orientation as well as position. This consistency benefits runtime mounting, though an off-origin prop may orbit widely; Center Prop remains explicit.
The focused Phase 10a suite and Prop Editor startup smoke test passed, and the user verified the final Prop Editor layout and controls in the Godot Editor.