Phase 14 - Organized directories.
This commit is contained in:
@@ -0,0 +1,877 @@
|
||||
# Phase 9a — Role-Based Prop Anchors and IK Authoring Preview
|
||||
|
||||
Status: **proposed — awaiting user sign-off** (2026-09-20).
|
||||
|
||||
Source: `docs/archive/plans/PHASE_9a_PROP_ATTACH.md`, refined through the spec-pipeline architecture
|
||||
clarification round.
|
||||
|
||||
## 1. Accepted decisions
|
||||
|
||||
The user approved these three architecture decisions:
|
||||
|
||||
1. **Prop Editor-only phase.** This phase implements anchor authoring, `.prop` round-trip,
|
||||
role queries, and an isolated rig-backed IK holding preview. All Sandbox Stage integration,
|
||||
runtime loading, attachment, gameplay, physics, and scene persistence are future work.
|
||||
2. **Fixed origin pivot.** Every prop has exactly one reserved anchor named `origin`, with
|
||||
role `pivot`, at `(0, 0)`. It cannot be deleted, renamed, moved, or assigned another role.
|
||||
Legacy `.prop` files synthesize it on load.
|
||||
3. **One-hand authoring preview.** The selected grip follows the solved wrist endpoint of an
|
||||
isolated preview rig. Secondary grips remain serialized/queryable and may be previewed one at
|
||||
a time, but never drive the opposite hand.
|
||||
|
||||
These are requirements, not implementation suggestions.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
### 2.1 In scope
|
||||
|
||||
- Extend the existing `[ Arrange | Reshape ]` toolbar and mode enum with one new `Attach`
|
||||
mode. Arrange and Reshape retain their current labels, behavior, and tests.
|
||||
- Add a normalized role-based anchor dictionary to `.prop` v1.0.
|
||||
- Preserve legacy `.prop` compatibility by synthesizing the fixed `origin` pivot.
|
||||
- Add anchor list/property editing, add/delete/rename, role selection, quick presets, and
|
||||
role-specific canvas gizmos.
|
||||
- Add anchor translation, grid snapping, rotation handles, and an isolated rig-backed preview
|
||||
where a stickman visibly holds the prop while its selected arm is posed through IK.
|
||||
- Include anchors in ordinary save/load, Save As, autosave, recovery, and New.
|
||||
- Add pure role/hand query and transform helpers that the editor preview consumes and future
|
||||
gameplay systems may reuse.
|
||||
- Add headless regression coverage plus manual Prop Editor checks.
|
||||
|
||||
### 2.2 Explicitly deferred
|
||||
|
||||
- **All Sandbox Stage/runtime changes:** custom `.prop` loading/spawning, selector integration,
|
||||
stage provenance/persistence, hand sockets, `StickmanPropAttachment`, equip/release, held-prop
|
||||
z ordering, collision/freeze handling, ragdoll/drop behavior, and runtime mirroring.
|
||||
- Secondary-hand IK and simultaneous two-hand constraint solving in gameplay. The editor preview
|
||||
remains one-hand-at-a-time.
|
||||
- Automatic grip choice, reach-to-pickup animation, pickup menus, director actions, or action
|
||||
queue schema changes.
|
||||
- Gameplay behavior for `grip`, `emitter`, `impact`, `mount`, or `custom` anchors. This phase
|
||||
authors and previews metadata only.
|
||||
- Projectile, particle, light, melee, accessory, throw, or center-of-mass systems.
|
||||
- Full multi-shape runtime rendering/collision. The existing runtime flattening contract is not
|
||||
modified; the editor preview may show the full authoring stack.
|
||||
- Document-wide undo/redo. The Prop Editor currently has no undo system; adding anchor-only
|
||||
undo would make shape and anchor editing inconsistent.
|
||||
- Any `.scene` schema or save/load change.
|
||||
|
||||
## 3. Existing constraints
|
||||
|
||||
- `PropCanvas.EditMode` currently has only `TRANSFORM` and `POINTS`, and
|
||||
`set_edit_mode()` clamps every non-POINTS value to TRANSFORM.
|
||||
- `PropEditor` derives every save/autosave from `_build_json_data()`, so anchor document state
|
||||
must enter through that single path.
|
||||
- `PropFile.FILE_VERSION` is `"1.0"`; additive fields such as `reshaped` already use
|
||||
backward-compatible v1.0 evolution.
|
||||
- `PropBlock` and `PropUtils.spawn_prop()` are single-shape runtime objects, but Phase 9a does not
|
||||
change or consume them. The editor preview uses non-physics visuals built from all shapes.
|
||||
- `StickmanRig` contains runtime locomotion/ragdoll/action components. The editor preview may
|
||||
instantiate it only inside a private SubViewport and must disable gameplay processing and
|
||||
collisions; no rig/runtime class or scene is modified by this phase.
|
||||
- `StickmanRig` mirrors its root on X for LEFT, while Godot TwoBoneIK is not mirror-invariant.
|
||||
The preview must isolate reflection outside the positive-scale solving skeleton.
|
||||
- `IK_Targets/Left_Hand` and `Right_Hand` are solver goals. Preview attachment must sample the
|
||||
solved lower-arm visual endpoint, not pretend the target marker itself is the wrist.
|
||||
|
||||
## 4. `.prop` schema and normalization
|
||||
|
||||
### 4.1 Version policy
|
||||
|
||||
Keep `PropFile.FILE_VERSION == "1.0"` and `SUPPORTED_VERSIONS == ["1.0"]`. `anchors` is an
|
||||
additive field. A missing or malformed `anchors` value is normalized to the mandatory origin
|
||||
rather than rejecting an otherwise valid legacy file.
|
||||
|
||||
`PropFile.to_file_dict()` always emits `anchors`, including the fixed origin. Loading an old
|
||||
file does not mark the editor dirty merely because the in-memory default was synthesized; its
|
||||
next explicit save writes the new field.
|
||||
|
||||
### 4.2 File representation
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"prop_name": "Arcane Staff",
|
||||
"material_preset": "wood",
|
||||
"shapes": [],
|
||||
"anchors": {
|
||||
"origin": {
|
||||
"role": "pivot",
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"rotation_deg": 0.0,
|
||||
"z_index_offset": 0,
|
||||
"allowed_hands": "both"
|
||||
},
|
||||
"primary_grip": {
|
||||
"role": "grip",
|
||||
"x": 0.0,
|
||||
"y": 18.0,
|
||||
"rotation_deg": 90.0,
|
||||
"z_index_offset": 1,
|
||||
"allowed_hands": "both"
|
||||
},
|
||||
"crystal_head": {
|
||||
"role": "emitter",
|
||||
"x": 0.0,
|
||||
"y": -48.0,
|
||||
"rotation_deg": -90.0,
|
||||
"z_index_offset": 0,
|
||||
"allowed_hands": "both"
|
||||
}
|
||||
},
|
||||
"metadata": {}
|
||||
}
|
||||
```
|
||||
|
||||
Every serialized anchor uses the same six fields. Uniform entries make validation, UI binding,
|
||||
and future consumers deterministic even when a field is not currently meaningful for a role.
|
||||
|
||||
### 4.3 Internal representation
|
||||
|
||||
The normalized editor model remains a `Dictionary` keyed by anchor ID, matching the project's
|
||||
JSON-first asset conventions:
|
||||
|
||||
```gdscript
|
||||
{
|
||||
"primary_grip": {
|
||||
"role": "grip",
|
||||
"position": Vector2(0.0, 18.0),
|
||||
"rotation_deg": 90.0,
|
||||
"z_index_offset": 1,
|
||||
"allowed_hands": "both",
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Do not assign JSON `{x, y}` dictionaries directly into typed vector arrays or vector fields.
|
||||
Parse `x` and `y` explicitly, following the existing `_coerce_points()` serialization lesson.
|
||||
|
||||
### 4.4 Registry and validation rules
|
||||
|
||||
- Roles: `pivot`, `grip`, `emitter`, `impact`, `mount`, `custom`.
|
||||
- Allowed hands: `both`, `left_only`, `right_only`.
|
||||
- IDs use lowercase snake case: `^[a-z][a-z0-9_]{0,31}$`.
|
||||
- UI input is normalized by trimming, lowercasing, replacing runs of non-alphanumeric
|
||||
characters with `_`, trimming `_`, and adding `_2`, `_3`, ... until unique.
|
||||
- Position values must be finite; invalid values become `0.0`.
|
||||
- `rotation_deg` must be finite and is wrapped into `[-180.0, 180.0)`.
|
||||
- `z_index_offset` is clamped to `[-5, 5]`.
|
||||
- Unknown roles become `custom`; unknown hand values become `both`.
|
||||
- The `origin` entry is overwritten with the canonical fixed pivot on every normalization.
|
||||
- Any non-origin entry claiming role `pivot` is preserved but downgraded to `custom`, with one
|
||||
warning for that normalization pass. No user data coordinates are discarded.
|
||||
- Malformed non-dictionary entries are skipped with a warning.
|
||||
|
||||
Anchor position is always in canonical prop-local pixels. `rotation_deg` is the direction of
|
||||
the anchor's local **+X axis**. For a grip, +X points from elbow toward wrist; for an emitter,
|
||||
it is the output direction. It is orientation metadata, not an instruction to rotate the prop's
|
||||
stored geometry.
|
||||
|
||||
### 4.5 `PropAnchorData` contract
|
||||
|
||||
Add `scripts/props/prop_anchor_data.gd`:
|
||||
|
||||
```gdscript
|
||||
class_name PropAnchorData
|
||||
extends RefCounted
|
||||
|
||||
const ORIGIN_ID: String = "origin"
|
||||
const ROLE_STRINGS: PackedStringArray = [
|
||||
"pivot", "grip", "emitter", "impact", "mount", "custom"
|
||||
]
|
||||
const ALLOWED_HAND_STRINGS: PackedStringArray = [
|
||||
"both", "left_only", "right_only"
|
||||
]
|
||||
enum Hand { LEFT, RIGHT }
|
||||
|
||||
static func default_anchors() -> Dictionary
|
||||
static func normalize_anchors(raw: Variant, warn: bool = true) -> Dictionary
|
||||
static func serialize_anchors(anchors: Dictionary) -> Dictionary
|
||||
static func normalize_id(candidate: String, anchors: Dictionary) -> String
|
||||
static func ids_for_role(anchors: Dictionary, role: String) -> Array[String]
|
||||
static func compatible_grip_ids(anchors: Dictionary, hand: int) -> Array[String]
|
||||
static func local_transform(anchor: Dictionary, mirror_x: bool = false) -> Transform2D
|
||||
static func solve_attachment_transform(
|
||||
target_transform: Transform2D,
|
||||
anchor: Dictionary,
|
||||
positive_scale: Vector2,
|
||||
mirror_x: bool
|
||||
) -> Transform2D
|
||||
static func preset_layout(preset: String, bounds: Rect2) -> Dictionary
|
||||
```
|
||||
|
||||
All returned dictionaries/arrays that expose document state are deep duplicates. Callers must
|
||||
not be able to mutate the codec's or canvas's state by retaining a reference.
|
||||
|
||||
When `mirror_x` is true, local position becomes `(-x, y)` and the +X direction angle becomes
|
||||
`PI - angle`. The canonical stored anchor is never mutated.
|
||||
|
||||
`solve_attachment_transform()` returns a positive-determinant prop transform satisfying
|
||||
`prop_transform * effective_anchor_transform == target_transform` for translation and +X
|
||||
direction. Normalize `positive_scale` to finite absolute components greater than `0.0001`, compute
|
||||
`prop_angle = target_transform.get_rotation() - effective_anchor_angle`, build the prop basis
|
||||
from `prop_angle` and that positive scale, then set:
|
||||
|
||||
```gdscript
|
||||
var prop_origin := (
|
||||
target_transform.origin
|
||||
- prop_basis.basis_xform(effective_anchor_position)
|
||||
)
|
||||
```
|
||||
|
||||
The helper ignores scale/skew on `target_transform`; callers supply a pure rotation/translation
|
||||
target. Mirroring is represented only by the effective anchor plus separately mirrored preview
|
||||
geometry, never by a negative prop-node scale.
|
||||
|
||||
### 4.6 `PropFile` API changes
|
||||
|
||||
```gdscript
|
||||
static func to_file_dict(
|
||||
prop_name: String,
|
||||
material_preset: int,
|
||||
shapes: Array[Dictionary],
|
||||
anchors: Dictionary = {}
|
||||
) -> Dictionary
|
||||
|
||||
static func from_file_dict(data: Variant) -> Dictionary
|
||||
```
|
||||
|
||||
`from_file_dict()` returns:
|
||||
|
||||
```gdscript
|
||||
{
|
||||
"prop_name": String,
|
||||
"material_preset": int,
|
||||
"shapes": Array[Dictionary],
|
||||
"anchors": Dictionary,
|
||||
}
|
||||
```
|
||||
|
||||
`flatten_to_payload()` remains shape-only and unchanged. Anchors are provenance/interaction
|
||||
metadata, not collision geometry.
|
||||
|
||||
## 5. Prop Editor architecture
|
||||
|
||||
### 5.1 Node layout
|
||||
|
||||
Update `scenes/editors/prop/prop_editor.tscn` without changing unique-name lookup contracts:
|
||||
|
||||
```text
|
||||
PropEditor
|
||||
├── TopRow
|
||||
│ ├── MenuBar
|
||||
│ └── TopRightControls # existing Grid + global Snap source
|
||||
├── TopBar
|
||||
├── MainLayout
|
||||
│ ├── CanvasPanel
|
||||
│ │ └── CanvasArea
|
||||
│ │ ├── ModeBar # existing Arrange/Reshape + new Attach
|
||||
│ │ ├── AttachTools # visible only in Attach
|
||||
│ │ └── CanvasWorkspace (HSplitContainer)
|
||||
│ │ ├── PropCanvas # existing authoring canvas
|
||||
│ │ └── PropAttachPreview # rig-backed holding preview; Attach only
|
||||
│ └── InspectorColumn
|
||||
│ └── InspectorScroll # NEW ScrollContainer
|
||||
│ └── InspectorList # keep %InspectorList
|
||||
├── SaveDialog / LoadDialog / ErrorDialog
|
||||
└── StatusBar
|
||||
```
|
||||
|
||||
`AttachTools` contains:
|
||||
|
||||
- `Show Stickman Preview` (`CheckBox`, default on, session-only tool state; collapses/opens the
|
||||
preview side of `CanvasWorkspace`);
|
||||
- `Snap Anchors` (`CheckBox`, bound bidirectionally to the same `_snap_enabled` value as the
|
||||
existing top-right Snap checkbox);
|
||||
- `Show Inactive Gizmos` (`CheckBox`, default on, session-only tool state).
|
||||
|
||||
The second Snap control is a synchronized view of one setting, not independent state. Changing
|
||||
either checkbox updates both without signal recursion and calls `PropCanvas.set_snap_enabled()`.
|
||||
|
||||
### 5.2 Modes
|
||||
|
||||
Preserve the existing Arrange (`TRANSFORM`) and Reshape (`POINTS`) values and append Attach:
|
||||
|
||||
```gdscript
|
||||
enum EditMode { TRANSFORM, POINTS, ATTACH }
|
||||
```
|
||||
|
||||
`set_edit_mode()` clamps only values outside that enum. Do not renumber or reinterpret the first
|
||||
two values. ATTACH persists across clear/load just like the existing modes. Append `Attach` /
|
||||
`📌` to the controller-owned mode-label arrays; do not rebuild or rename Arrange/Reshape. Keep
|
||||
all three buttons in the existing `ButtonGroup`, with `focus_mode = FOCUS_NONE` to preserve the
|
||||
current selected styling.
|
||||
|
||||
Mode behavior:
|
||||
|
||||
| Behavior | Arrange | Reshape | Attach |
|
||||
| --- | --- | --- | --- |
|
||||
| Shape body translate | yes | select only | no |
|
||||
| Shape transform gizmos | yes | no | no |
|
||||
| Vertex handles/menu | no | yes | no |
|
||||
| Shape right-click menu | yes | reshape matrix | no |
|
||||
| Anchor gizmos/input | no | no | yes |
|
||||
| Wheel/pinch/middle-pan | yes | yes | yes |
|
||||
|
||||
Shape selection may remain remembered while Attach is active, but no selection outline, shape
|
||||
gizmo, or vertex handle is drawn in that mode. Switching back restores the remembered shape
|
||||
selection.
|
||||
|
||||
### 5.3 Composed anchor interaction helper
|
||||
|
||||
Add `scripts/props/prop_anchor_overlay.gd`:
|
||||
|
||||
```gdscript
|
||||
class_name PropAnchorOverlay
|
||||
extends RefCounted
|
||||
|
||||
signal anchors_changed(anchors: Dictionary)
|
||||
signal anchor_selected(anchor_id: String)
|
||||
|
||||
enum DragMode { NONE, TRANSLATE, ROTATE }
|
||||
|
||||
func set_anchor_data(data: Variant) -> void
|
||||
func get_anchor_data() -> Dictionary
|
||||
func select_anchor(anchor_id: String) -> void
|
||||
func get_selected_anchor_id() -> String
|
||||
func set_show_inactive(enabled: bool) -> void
|
||||
func hit_test_anchor(world_pos: Vector2, zoom: float) -> String
|
||||
func begin_pointer(world_pos: Vector2, zoom: float) -> bool
|
||||
func update_pointer(world_pos: Vector2, zoom: float, snap: bool, grid_size: int, ctrl: bool) -> void
|
||||
func end_pointer() -> void
|
||||
func draw_gizmos(canvas: CanvasItem, zoom: float) -> void
|
||||
```
|
||||
|
||||
This helper owns anchor selection/drag state and math, while `PropCanvas` remains the only owner
|
||||
of pan, zoom, coordinate conversion, and `_gui_input`. The canvas routes left-button events to
|
||||
the helper only in ATTACH and re-emits its two signals as public `PropCanvas` signals. The helper
|
||||
draws only authoring gizmos; the isolated rig preview in §5.5 owns all pose visualization.
|
||||
|
||||
Add to `PropCanvas`:
|
||||
|
||||
```gdscript
|
||||
signal anchors_changed(anchors: Dictionary)
|
||||
signal anchor_selected(anchor_id: String)
|
||||
|
||||
func set_anchor_data(data: Variant) -> void
|
||||
func get_anchor_data() -> Dictionary
|
||||
func select_anchor(anchor_id: String) -> void
|
||||
func get_selected_anchor_id() -> String
|
||||
func set_show_inactive_anchors(enabled: bool) -> void
|
||||
func get_shapes_bounds() -> Rect2
|
||||
```
|
||||
|
||||
An empty shape stack must not early-return before anchor drawing. Attach mode still shows the
|
||||
origin and other anchors on an empty canvas.
|
||||
|
||||
### 5.4 Drawing and hit targets
|
||||
|
||||
Use code-drawn vector markers; do not add bitmap icon dependencies.
|
||||
|
||||
| Role | Marker | Core color |
|
||||
| --- | --- | --- |
|
||||
| pivot | crosshair | yellow |
|
||||
| grip | hand-like circle + short palm bars | cyan |
|
||||
| emitter | target reticle + direction arrow | red |
|
||||
| impact | diamond + slash | bright pink |
|
||||
| mount | ring + center dot | purple |
|
||||
| custom | hexagon | light gray |
|
||||
|
||||
- Draw a dark halo behind every marker/line, matching the existing selection visibility rule.
|
||||
- Marker radius and hit radius are constant screen sizes (`value / zoom`).
|
||||
- Draw the selected marker last, with a yellow outer selection ring.
|
||||
- When `Show Inactive Gizmos` is off, draw/hit-test only the selected anchor. If no anchor is
|
||||
selected, continue showing the origin so the canvas is not visually empty.
|
||||
- Pivot is selectable but never draggable and never receives a rotation handle.
|
||||
- A selected `grip` or `emitter` shows a rotation ring and handle.
|
||||
- Translation snaps each coordinate with `snappedf(value, grid_size)` only when the shared Snap
|
||||
setting is enabled.
|
||||
- Rotation is free by default; Ctrl snaps to 15-degree increments, consistent with Arrange.
|
||||
- Emit `anchors_changed` during live drag for inspector feedback. Mouse release sanitizes the
|
||||
final position/angle and emits once more only if sanitization changed it.
|
||||
- Right click has no Attach-mode context menu; list/inspector buttons own add/delete/rename.
|
||||
|
||||
### 5.5 Rig-backed holding and IK preview
|
||||
|
||||
The preview must show a recognizable stickman holding the current prop, not a symbolic hand.
|
||||
It is a separate authoring visualization with its own `SubViewport`, world, rig instance, camera,
|
||||
pose state, and non-physics prop visual. It reuses the actual rig scene, `StkRigAdapter`, arm IK,
|
||||
and the pure anchor-alignment math, while remaining isolated from Sandbox/gameplay state.
|
||||
|
||||
#### 5.5.1 Class and scene contract
|
||||
|
||||
Add `scripts/editors/prop/prop_attach_preview.gd` and `scenes/editors/prop/prop_attach_preview.tscn`:
|
||||
|
||||
```gdscript
|
||||
class_name PropAttachPreview
|
||||
extends PanelContainer
|
||||
|
||||
enum PreviewHand { LEFT, RIGHT }
|
||||
enum FrameMode { UPPER_BODY, FULL_FIGURE }
|
||||
|
||||
func set_document(
|
||||
shapes: Array[Dictionary],
|
||||
anchors: Dictionary,
|
||||
selected_anchor_id: String
|
||||
) -> void
|
||||
func set_selected_anchor(anchor_id: String) -> void
|
||||
func set_preview_hand(hand: int) -> void
|
||||
func set_facing_profile(profile: int) -> void
|
||||
func set_arm_animation_enabled(enabled: bool) -> void
|
||||
func reset_arm_pose() -> void
|
||||
func frame_view(mode: int) -> void
|
||||
func is_preview_available() -> bool
|
||||
```
|
||||
|
||||
```text
|
||||
PropAttachPreview (PanelContainer)
|
||||
└── PreviewLayout (VBoxContainer)
|
||||
├── PreviewHeader (HBoxContainer)
|
||||
│ ├── Title: "Holding Preview"
|
||||
│ ├── HandOption: Left / Right
|
||||
│ ├── FacingOption: Forward / Left / Right
|
||||
│ └── FrameOption: Upper Body / Full Figure
|
||||
├── PreviewHint (Label)
|
||||
├── PreviewViewportContainer (SubViewportContainer)
|
||||
│ └── PreviewViewport (SubViewport)
|
||||
│ └── PreviewWorld (Node2D)
|
||||
│ ├── PreviewRigRoot (Node2D; preview-only mirror wrapper)
|
||||
│ │ └── PreviewRig (dynamically instanced for this editor preview)
|
||||
│ ├── PreviewProp (Node2D; generated visual only)
|
||||
│ ├── PreviewOverlay (Node2D; target handle/instructions)
|
||||
│ └── PreviewCamera (Camera2D)
|
||||
└── PreviewFooter (HBoxContainer)
|
||||
├── AnimateArm (CheckBox)
|
||||
├── ResetArm (Button)
|
||||
└── ResetView (Button)
|
||||
```
|
||||
|
||||
The preview panel has a `360 px` minimum width and is the right pane of the resizable
|
||||
`CanvasWorkspace` split. It is visible only in Attach mode and while `Show Stickman Preview` is
|
||||
enabled. Hiding it must not change document data, selected anchor, or pose state outside this
|
||||
private preview.
|
||||
|
||||
#### 5.5.2 Safe rig reuse and isolation
|
||||
|
||||
Create one private preview rig when the preview enters the tree:
|
||||
|
||||
1. Load `res://content/stickmen/test.stk` through `StickmanFactory.load_stk()`.
|
||||
2. Instantiate `res://scenes/characters/stickman/stickman_rig.tscn`, call `set_physics_process(false)` on the rig
|
||||
before it enters the tree, and apply valid `.stk` data through `StkRigAdapter.apply()`. If the
|
||||
file cannot be loaded, keep the scene's authored master visuals rather than failing the editor.
|
||||
3. Add the rig only to `PreviewViewport`'s private `PreviewWorld`; after `_ready()`, stop its
|
||||
`AnimationPlayer`, call `stop_queue()` then `clear_queue()`, retain the default `ANIMATED`
|
||||
state, and set every descendant `CollisionObject2D` layer and mask to `0`.
|
||||
4. Do not set the whole rig subtree to `PROCESS_MODE_DISABLED`, because that would also stop the
|
||||
skeleton solve. The root physics callback is the gameplay tick boundary; authored component
|
||||
children have no autonomous process callback.
|
||||
5. Keep the `Skeleton2D` modification stack and `RemoteTransform2D` drivers active so moving an
|
||||
IK target solves the real arm and updates the real Body visuals.
|
||||
|
||||
The preview instance is never added to Sandbox `World`, never registered with `StageSpawner`,
|
||||
never serialized, and never connected to stage rules or signals. It must not create a
|
||||
`PropBlock`, hand socket, or runtime attachment component. Preview pose/facing/hand settings are
|
||||
session-only UI state. The preview receives deep copies from `PropEditor`, so no preview mutation
|
||||
can alter the document except through the existing anchor inspector/canvas signals.
|
||||
|
||||
This boundary intentionally reuses visual rig/IK behavior but not gameplay ownership, physics,
|
||||
ragdoll, locomotion, or action execution.
|
||||
|
||||
#### 5.5.3 Prop visual and shared alignment math
|
||||
|
||||
`PreviewProp` is rebuilt from the editor's complete `shapes[]` stack using one `Polygon2D` or
|
||||
`Line2D` per shape, in document order. It has no collision node or physics body and therefore
|
||||
shows all authored shapes even though the staged runtime still flattens collision/render payloads.
|
||||
Coalesce repeated shape/anchor signals into at most one rebuild/alignment per rendered frame.
|
||||
|
||||
Add one pure helper so editor alignment is deterministic, directly testable, and reusable by a
|
||||
future runtime design if that later specification adopts the same convention:
|
||||
|
||||
```gdscript
|
||||
static func solve_attachment_transform(
|
||||
target_transform: Transform2D,
|
||||
anchor: Dictionary,
|
||||
positive_scale: Vector2,
|
||||
mirror_x: bool
|
||||
) -> Transform2D
|
||||
```
|
||||
|
||||
This belongs on `PropAnchorData`. Phase 9a has exactly one production caller
|
||||
(`PropAttachPreview`). Keeping the transform pure and independent of preview nodes avoids
|
||||
coupling the schema to the rig scene. A future runtime phase may reuse it only after separately
|
||||
validating the convention. The preview may mirror its non-physics geometry directly, but
|
||||
canonical document shapes/anchors remain unchanged.
|
||||
|
||||
Do not add HandSocket nodes to the runtime rig. After the skeleton/RemoteTransform update, derive
|
||||
the preview wrist transform from existing nodes:
|
||||
|
||||
1. Resolve `Body/LeftLowerArm` or `Body/RightLowerArm` as the solved hanging-frame visual.
|
||||
2. Resolve the matching lower `Bone2D` and read its fitted `length`.
|
||||
3. Compute elbow = `lower_visual.to_global(Vector2.ZERO)` and wrist =
|
||||
`lower_visual.to_global(Vector2(0, lower_length))`.
|
||||
4. Direction = `(wrist - elbow).normalized()`; build a proper (positive determinant) socket
|
||||
transform whose +X axis follows that direction and whose origin is wrist.
|
||||
5. Pass that transform to `solve_attachment_transform()`.
|
||||
|
||||
`StkRigAdapter` already mounts lower-arm visuals into the +Y hanging frame and fits their extent
|
||||
to the bone length, so this samples the visible endpoint without changing the rig or trusting the
|
||||
IK goal marker as the solved wrist. If the nodes/direction are invalid, hide the preview prop and
|
||||
show a non-fatal preview-unavailable hint.
|
||||
|
||||
When the selected entry is a grip, align it to this derived wrist transform. A one-frame
|
||||
engine-order delay is acceptable while dragging; the following frame must be exact. When no grip
|
||||
is selected, keep the rig visible, hide the prop visual, and show
|
||||
`Select a grip anchor to preview holding.`
|
||||
|
||||
Preview `z_index_offset` relative to the rig root: negative behind, positive in front, and zero
|
||||
placed immediately before the rig so the hand/body stays visible. This is an authoring convention
|
||||
only; exact runtime layering is deliberately left to the future runtime specification.
|
||||
|
||||
#### 5.5.4 Hand and facing behavior
|
||||
|
||||
- Default preview hand is RIGHT and default facing is FORWARD.
|
||||
- `left_only` forces LEFT and disables RIGHT in `HandOption`.
|
||||
- `right_only` forces RIGHT and disables LEFT.
|
||||
- `both` enables both choices and remembers the last session choice.
|
||||
- Selecting any grip—including `secondary_grip`—previews that one grip as the active one-hand
|
||||
attachment. It never moves the opposite hand or consumes another grip.
|
||||
- Changing hand moves only the newly active hand target; first reset both hand targets to their
|
||||
captured rest positions so poses do not leak between hand choices.
|
||||
- Facing uses the preview rig's established `FacingProfile`, bend flags, and Body child ordering.
|
||||
It does not write the document or affect a runtime rig.
|
||||
- LEFT must exercise `PropAnchorData`'s effective-anchor mirroring so authors can evaluate the
|
||||
intended facing behavior before a future runtime implementation exists.
|
||||
|
||||
The runtime rig deliberately disables idle limb IK under its negative-X LEFT root because Godot's
|
||||
TwoBoneIK solver is not mirror-invariant. The preview must not bypass that known constraint by
|
||||
forcing the solver under a reflected skeleton. For LEFT preview:
|
||||
|
||||
1. call `PreviewRig.set_facing_profile(LEFT)` to reuse bend flags and Body ordering;
|
||||
2. restore `PreviewRig.scale = Vector2.ONE`;
|
||||
3. mirror `PreviewRigRoot.scale.x = -1` so reflection occurs *after* the positive-scale skeleton
|
||||
solves;
|
||||
4. explicitly enable the two arm `SkeletonModification2DTwoBoneIK` entries for preview posing;
|
||||
5. keep `PreviewProp` outside `PreviewRigRoot`, mirror its generated geometry, and align it to the
|
||||
reflected socket with `solve_attachment_transform(..., mirror_x = true)`.
|
||||
|
||||
RIGHT/FORWARD use identity `PreviewRigRoot` and `mirror_x = false`. This establishes the editor's
|
||||
facing convention while keeping the isolated preview solver stable. Tests must assert that LEFT
|
||||
leaves `PreviewRig`'s local scale positive and puts the reflection only on the wrapper.
|
||||
|
||||
This preserves the approved one-hand authoring scope while allowing every authored grip to be
|
||||
visually tested with either compatible anatomical hand.
|
||||
|
||||
#### 5.5.5 IK interaction
|
||||
|
||||
Draw a constant-screen-size cyan target handle at the active
|
||||
`IK_Targets/Left_Hand` or `IK_Targets/Right_Hand` marker. The user may drag this handle inside the
|
||||
preview viewport; the real TwoBoneIK modification bends the arm and the preview prop follows the
|
||||
solved wrist socket.
|
||||
|
||||
Clamp manual and animated targets to the reachable annulus around the active upper-arm shoulder:
|
||||
|
||||
```text
|
||||
min reach = abs(upper_length - lower_length) + 8 px
|
||||
max reach = upper_length + lower_length - 8 px
|
||||
```
|
||||
|
||||
If the drag vector is zero, use the captured rest-pose direction before clamping. This avoids
|
||||
solver folding/jitter without inventing a second IK implementation.
|
||||
|
||||
`Animate Arm` provides a hands-off movement check:
|
||||
|
||||
- period: `3.0 s`, sine-eased ping-pong;
|
||||
- endpoint A: the captured rest target;
|
||||
- endpoint B: rest target plus `(outward * 120, -120)`, clamped to the same annulus;
|
||||
- anatomical outward sign: LEFT `+1`, RIGHT `-1` in unmirrored rig-local space;
|
||||
- starting a manual drag turns animation off;
|
||||
- Reset Arm turns animation off and restores both hand targets to their captured rest values.
|
||||
|
||||
The inactive hand remains at rest. Head, torso, and leg targets remain untouched. No animation
|
||||
clip plays while the arm target is manipulated.
|
||||
|
||||
#### 5.5.6 Camera and input
|
||||
|
||||
- `PreviewViewport.size` follows `PreviewViewportContainer.size`.
|
||||
- Default frame is `UPPER_BODY`, fitting the union of head/torso/arms and current prop with a
|
||||
`32 px` screen margin.
|
||||
- `FULL_FIGURE` fits a preview-local union of the transformed `Body/*` visual bounds and current
|
||||
prop AABB with the same margin. `PropAttachPreview` owns this bounds helper; it must not import
|
||||
or call `StageSpawner`.
|
||||
- The local bounds helper walks visible descendants recursively: transform each `Polygon2D.polygon`
|
||||
point and each `Line2D.points` entry into `PreviewWorld` coordinates, expanding line bounds by
|
||||
half the drawn width. `UPPER_BODY` filters to Head/Torso/arm branches; `FULL_FIGURE` includes
|
||||
every `Body/*` branch.
|
||||
- Camera framing is recalculated on preview resize, explicit Reset View, frame-mode change, or a
|
||||
document geometry rebuild—not every arm-animation frame. This prevents zoom pumping.
|
||||
- Mouse wheel performs cursor-centered preview zoom, clamped `[0.3, 3.0]` relative to the fitted
|
||||
zoom. Middle mouse pans. These controls affect only the preview camera.
|
||||
- Left drag moves only the active IK handle; clicks elsewhere do not select/edit anchors.
|
||||
- The parent `PropCanvas` keeps its existing pan/zoom/input unchanged.
|
||||
|
||||
Convert pointer positions through the preview viewport rather than the authoring canvas:
|
||||
|
||||
```gdscript
|
||||
var preview_world_pos := (
|
||||
preview_viewport.get_canvas_transform().affine_inverse()
|
||||
* preview_container_local_position
|
||||
)
|
||||
```
|
||||
|
||||
The viewport size must match the container's integer size (clamped to at least `1 × 1`), so this
|
||||
mapping remains stable after split/window resize.
|
||||
|
||||
The preview overlay shows a short hint: `Drag the cyan hand target to test the grip.` It may show
|
||||
the active anchor ID and hand, but it must not show gameplay/action controls.
|
||||
|
||||
### 5.6 Inspector
|
||||
|
||||
`PropEditor` owns normalized document state:
|
||||
|
||||
```gdscript
|
||||
var _anchors: Dictionary = PropAnchorData.default_anchors()
|
||||
var _selected_anchor_id: String = PropAnchorData.ORIGIN_ID
|
||||
```
|
||||
|
||||
The existing Material section stays visible. In ATTACH, hide Selected Shape and Add Shape, and
|
||||
show a new Anchor section containing:
|
||||
|
||||
1. `ItemList` of anchor IDs (the list itself scrolls);
|
||||
2. `Add Anchor`, `Rename`, `Delete` buttons;
|
||||
3. Role `OptionButton`;
|
||||
4. X/Y `SpinBox` values (`-100000..100000`, step `1`);
|
||||
5. Rotation `SpinBox` (`-180..<180`, step `1`) plus a matching slider if it can share the same
|
||||
no-recursion update guard;
|
||||
6. Z Offset `SpinBox` (`-5..5`, integer);
|
||||
7. Allowed Hands `OptionButton`;
|
||||
8. Quick Presets buttons: One-Handed, Two-Handed, Tool / Nozzle, Melee Edge.
|
||||
|
||||
For `origin`, Rename/Delete and every property control are disabled. Role-specific relevance is
|
||||
communicated by muted labels, but the uniform data remains editable for non-origin anchors.
|
||||
|
||||
Add creates a uniquely named `anchor`, selects it, and defaults to role `custom` at the center of
|
||||
the current shape bounds (or zero when empty). Delete selects the preceding list entry, falling
|
||||
back to `origin`. Rename uses the ID normalizer and immediately updates selection.
|
||||
|
||||
Quick presets replace all non-origin anchors. If any exist, use a confirmation dialog before
|
||||
replacement. Layout is derived from the union shape AABB:
|
||||
|
||||
- major axis = X when width >= height, else Y;
|
||||
- direction angle = `0°` for X or `90°` for Y;
|
||||
- center = bounds center, or zero for empty bounds;
|
||||
- positive endpoint = right-center or bottom-center;
|
||||
- quarter offset = `min(major_length * 0.2, 32)`.
|
||||
|
||||
Preset contents:
|
||||
|
||||
| Preset | Generated anchors |
|
||||
| --- | --- |
|
||||
| One-Handed | `primary_grip` at center |
|
||||
| Two-Handed | `primary_grip` and `secondary_grip` at center ± quarter offset |
|
||||
| Tool / Nozzle | `primary_grip` at center, `emitter` at positive endpoint |
|
||||
| Melee Edge | `primary_grip` at center, `impact` at positive endpoint |
|
||||
|
||||
All generated anchors use the major-axis direction, `both`, and z offset `0`.
|
||||
|
||||
### 5.7 Dirty state, New, save, and recovery
|
||||
|
||||
- `_build_json_data()` passes `_anchors` to `PropFile.to_file_dict()`.
|
||||
- `_apply_json_data()` assigns the normalized model anchors to both `_anchors` and canvas.
|
||||
- `_do_new()` resets anchors to `default_anchors()` and selects `origin`.
|
||||
- Canvas `anchors_changed` updates `_anchors`, calls `_mark_dirty()`, and refreshes inspector.
|
||||
- After shape/anchor/selection changes, pass deep copies to
|
||||
`PropAttachPreview.set_document()`/`set_selected_anchor()`; the preview coalesces visual work.
|
||||
- Preview visibility, preview hand, facing, arm animation, camera, inactive-gizmo visibility,
|
||||
and mode changes are tool state and do not mark the document dirty.
|
||||
- Add/delete/rename/preset/property changes mark dirty through the same centralized path.
|
||||
- Existing debounced autosave automatically includes anchors through `_build_json_data()`.
|
||||
- Recovery restores anchors and keeps the document dirty, matching shape recovery.
|
||||
- Thumbnails remain geometry-only; changing only anchors may update the `.prop` mtime/cache key
|
||||
on save, but anchors are not drawn into the thumbnail.
|
||||
|
||||
## 6. Future Sandbox/runtime scope — not a Phase 9a deliverable
|
||||
|
||||
Phase 9a stops at the `.prop` anchor contract and editor-only preview. It must not modify
|
||||
`PropBlock`, `PropUtils`, `StageSpawner`, `SandboxStage`, `StageSceneIO`, `StickmanRig`,
|
||||
`StkRigAdapter`, `scenes/characters/stickman/stickman_rig.tscn`, collision/physics behavior, director actions, or
|
||||
runtime tests.
|
||||
|
||||
A later, separately approved specification may consume the saved anchor contract to design:
|
||||
|
||||
- custom `.prop` discovery, selector UI, spawning, provenance, missing-asset handling, and stage
|
||||
scene persistence;
|
||||
- a runtime held-prop visual/physics representation that addresses the existing multi-shape
|
||||
flattening limitation;
|
||||
- explicit wrist sockets or another stable solved-wrist API;
|
||||
- one-hand and two-hand equip/release ownership;
|
||||
- facing/mirroring and per-limb draw-order behavior;
|
||||
- collision masks, freeze/reparent strategy, throwing, ragdoll/drop lifecycle, and save semantics;
|
||||
- gameplay consumers for emitter/impact/mount/custom roles.
|
||||
|
||||
The future runtime spec should reuse the pure normalized anchor data and transform convention
|
||||
where appropriate, but must validate them against runtime physics and animation rather than
|
||||
treating the editor preview as authoritative gameplay behavior.
|
||||
|
||||
## 7. Signal and data flow
|
||||
|
||||
### 7.1 Editor
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Input[Canvas/Inspector input] --> Overlay[PropAnchorOverlay]
|
||||
Overlay -->|anchors_changed deep copy| Canvas[PropCanvas]
|
||||
Canvas -->|anchors_changed| Editor[PropEditor document state]
|
||||
Editor --> Dirty[_mark_dirty + autosave debounce]
|
||||
Editor --> Inspector[Anchor inspector refresh]
|
||||
Editor -->|deep-copied shapes/anchors/selection| Preview[PropAttachPreview]
|
||||
Preview --> PreviewRig[Private SubViewport rig + arm IK]
|
||||
Preview --> PreviewProp[Non-physics full-shape visual]
|
||||
Editor --> Save[_build_json_data]
|
||||
Save --> Codec[PropFile.to_file_dict]
|
||||
Codec --> Disk[.prop JSON]
|
||||
```
|
||||
|
||||
The diagram ends at the `.prop` file and private preview. No arrow reaches Sandbox/runtime state.
|
||||
|
||||
## 8. Theme and UX requirements
|
||||
|
||||
- Reuse `PropTheme` font sizes, minimum row heights, text, muted text, border, and accent tokens.
|
||||
- Do not mutate the global project theme.
|
||||
- `InspectorScroll` fills the inspector column vertically and scrolls when the anchor controls do
|
||||
not fit.
|
||||
- `PropAttachPreview` uses the same local `_ui_theme` tokens as the editor; the viewport
|
||||
background uses `canvas_background_color()` and the panel/header use existing border/text/
|
||||
muted/accent tokens.
|
||||
- The preview split is user-resizable, never shrinks below `360 px` while open, and collapses
|
||||
completely when hidden so existing Arrange/Reshape canvas space is unchanged.
|
||||
- Anchor list/buttons meet the existing theme's minimum hit height.
|
||||
- Disabled origin controls remain readable using muted text, with tooltip: “Origin is the fixed
|
||||
prop pivot.”
|
||||
- Add/delete/rename/preset actions must keep keyboard focus from causing the mode-selected style
|
||||
regression; mode buttons remain `FOCUS_NONE`.
|
||||
- Status cursor coordinates continue to report prop world coordinates in Attach mode.
|
||||
- No new image assets are required.
|
||||
|
||||
## 9. Tests and engine validation
|
||||
|
||||
### 9.1 New headless suite
|
||||
|
||||
Add `tests/test_phase9a_prop_attach.gd` (`extends SceneTree`, PASS/FAIL, `quit(0/1)`) covering:
|
||||
|
||||
1. default anchors contain only canonical origin;
|
||||
2. legacy file without anchors synthesizes origin;
|
||||
3. origin tampering is overwritten and extra pivot roles downgrade to custom;
|
||||
4. finite/default/clamp/wrap validation;
|
||||
5. ID normalization and collision suffixes;
|
||||
6. serialize/parse round-trip asserts coordinate and rotation **values**, not only counts;
|
||||
7. role and allowed-hand queries;
|
||||
8. X-mirrored anchor position/direction math;
|
||||
9. all four quick-preset layouts for horizontal, vertical, and empty bounds;
|
||||
10. `PropCanvas.EditMode.ATTACH` persists through clear/load and blocks shape drag routing;
|
||||
11. fixed origin cannot begin translate/rotate;
|
||||
12. snapped and unsnapped anchor translation plus Ctrl rotation snapping;
|
||||
13. preview rig lives under its private `SubViewport` world and adds nothing to a Sandbox world;
|
||||
14. preview disables rig physics ticking/collisions and animation playback while leaving the
|
||||
Skeleton2D modification stack enabled;
|
||||
15. preview visual creates one node per authored shape and never creates `PropBlock` or collision;
|
||||
16. `left_only`/`right_only` force the compatible preview hand while `both` permits either;
|
||||
17. the derived wrist transform (lower-arm visual origin plus fitted lower-bone length) maps the
|
||||
selected anchor to the solved wrist within tolerance;
|
||||
18. manual preview target clamping respects derived arm reach and leaves the inactive target at
|
||||
its captured rest position;
|
||||
19. arm auto-motion changes only the active hand and Reset Arm restores both rest targets;
|
||||
20. FORWARD/RIGHT/LEFT use the same editor transform solver; LEFT reflection lives on
|
||||
`PreviewRigRoot`, the rig's local scale stays positive, and arm IK remains enabled;
|
||||
21. selecting `secondary_grip` previews it with exactly one hand and never moves the other;
|
||||
22. frame-mode/reset/resize recalculate camera framing, while arm-motion ticks do not change
|
||||
fitted zoom.
|
||||
|
||||
### 9.2 Existing regressions
|
||||
|
||||
Run all Prop Editor suites listed in `AGENTS.md`. Because the editor-only preview instantiates
|
||||
the existing rig without changing its runtime contracts, also run the rig modularization suite
|
||||
as a read-only compatibility regression. At minimum:
|
||||
|
||||
- `tests/test_prop_editor_file.gd`
|
||||
- `tests/test_prop_editor_canvas.gd`
|
||||
- `tests/test_prop_editor_theme.gd`
|
||||
- `tests/test_prop_editor_thumb.gd`
|
||||
- `tests/test_prop_editor_round3.gd`
|
||||
- `tests/test_phase8_rig_modularization.gd`
|
||||
|
||||
No Sandbox Stage suite is part of Phase 9a validation because this phase does not modify or
|
||||
integrate with Sandbox/runtime systems.
|
||||
|
||||
### 9.3 Static checks
|
||||
|
||||
Run Godot 4.7.1 `--headless --check-only` for every changed/new script, especially:
|
||||
|
||||
- `prop_anchor_data.gd`
|
||||
- `prop_anchor_overlay.gd`
|
||||
- `prop_attach_preview.gd`
|
||||
- `prop_file.gd`
|
||||
- `prop_canvas.gd`
|
||||
- `prop_editor.gd`
|
||||
- the new test suite
|
||||
|
||||
If new `class_name` entries are not in the global script cache, run one headless editor rescan
|
||||
before per-script checks, per technical-debt item #22.
|
||||
|
||||
## 10. Manual acceptance checklist
|
||||
|
||||
### Prop Editor (F6 `res://scenes/editors/prop/prop_editor.tscn`)
|
||||
|
||||
- Existing Arrange/Reshape behavior and labels are unchanged; the appended Attach button renders
|
||||
and all three selected states remain visible.
|
||||
- Attach mode shows anchors while hiding every shape selection/vertex/transform gizmo.
|
||||
- Pan, wheel zoom, pinch, and Reset View remain correct.
|
||||
- Add/rename/delete work; origin controls are disabled and origin cannot be dragged.
|
||||
- Anchor translation stays under the cursor at multiple zoom levels and snaps from either Snap
|
||||
checkbox.
|
||||
- Grip/emitter rotation ring works; Ctrl snaps 15 degrees.
|
||||
- Inactive toggle and role marker colors/icons are legible over light and dark props.
|
||||
- Opening the Stickman Preview shows the real default rig visibly holding the complete multi-shape
|
||||
prop at the selected grip; selecting a non-grip shows the instructional empty state.
|
||||
- Left/Right hand choice obeys `allowed_hands`; any selected secondary grip is previewed as a
|
||||
one-hand grip and the opposite hand stays at rest.
|
||||
- Dragging the cyan target visibly bends the actual IK arm and keeps the prop locked to the solved
|
||||
wrist; Animate Arm sweeps smoothly, manual drag cancels it, and Reset Arm restores the pose.
|
||||
- Forward/Right/Left preview facing catches mirrored anchor offsets without modifying document or
|
||||
any state outside the private preview.
|
||||
- Upper Body / Full Figure framing, preview wheel zoom, middle pan, Reset View, split resize, and
|
||||
show/hide work without affecting the authoring canvas camera.
|
||||
- Preview z offset visibly swaps the prop behind/in front according to the editor preview
|
||||
convention.
|
||||
- Inspector scrolls at a small window size.
|
||||
- Save/load and crash-recovery preserve anchors exactly; legacy props open with origin.
|
||||
- Quick presets match horizontal and vertical prop bounds.
|
||||
|
||||
## 11. Architectural trade-offs
|
||||
|
||||
| Decision | Benefit | Cost / follow-up |
|
||||
| --- | --- | --- |
|
||||
| Additive `.prop` v1.0 anchors | Old files remain valid; no migration branch | Version alone cannot advertise anchor capability; consumers query presence/defaults |
|
||||
| Fixed origin pivot | Stable coordinates and no geometry/COM migration | User cannot choose a custom physics pivot in this phase |
|
||||
| JSON dictionary + static codec | Matches current project and is easy to inspect | GDScript cannot statically type dictionary fields; normalization boundary is essential |
|
||||
| Composed anchor helper | Keeps anchor state/hit math out of the already large canvas | Canvas remains the input/coordinate owner and must route Attach events explicitly |
|
||||
| Isolated rig-backed preview | Shows real IK bending, facing, custom stickman visuals, and the full authored prop | Adds a private SubViewport/rig instance and requires strict disabling of gameplay processing/collision |
|
||||
| One-hand-at-a-time preview | Keeps anchor evaluation focused and matches the approved authoring scope | It does not solve or demonstrate two-hand constraints |
|
||||
| Derived visual wrist, no sockets | Reuses the fitted rig while avoiding any rig scene/runtime contract change | Depends on the lower-arm visual's established hanging-frame convention and fitted lower-bone length |
|
||||
| Editor-only facing convention | Exposes mirrored offset mistakes during authoring without touching gameplay state | A future runtime design must validate facing and draw order independently |
|
||||
| Full multi-shape preview | Faithfully shows the authored prop inside the editor | It deliberately does not prove how a future runtime representation will render or collide |
|
||||
| No anchor-only undo | Consistent editor expectations | Full shape + anchor document undo remains future work |
|
||||
|
||||
## 12. Implementation order
|
||||
|
||||
1. `PropAnchorData` and schema tests.
|
||||
2. `PropFile` round-trip and legacy normalization.
|
||||
3. Append Attach to the existing mode system; add overlay math/drawing, inspector, presets, and
|
||||
save/autosave integration while preserving Arrange/Reshape.
|
||||
4. Add the isolated rig-backed `PropAttachPreview`, shared editor transform solver, derived wrist
|
||||
transform, IK interaction, and camera/layout behavior without changing rig/runtime files.
|
||||
5. Complete save/autosave/recovery integration, the editor-only headless suite, static checks,
|
||||
and manual hand-off.
|
||||
|
||||
Implementation must stop after engine validation and ask the user to verify in Godot. README or
|
||||
other product documentation updates wait until the user confirms the feature works.
|
||||
Reference in New Issue
Block a user