# Phase 8 — Architectural Specification ## Overview Phase 8 has two deliverables: 1. **Save-side `.stk` export** — when saving/exporting, the editor appends a top-level `proportions` object (5 rig bone lengths) and, inside each `body_parts` entry, a `pivot` (`{x, y}`) and `length` (float). This is the **only** editor change; the editor does **not** instantiate the rig. 2. **A standalone runtime adapter `StkRigAdapter.gd`** — a GDScript utility that takes a loaded `.stk` dictionary + an instantiated `master_rig.tscn` node and re-fits the skeleton (bone lengths), recalibrates the IK targets, and mounts the `.stk` vector shapes onto the rig's `Body/` visual nodes. This script is consumed by a **future runtime pipeline**, not by the editor. Scope is intentionally tight: - The editor is **save-side only** — it computes and writes `proportions` / `pivot` / `length`. It never loads `StkRigAdapter.gd` and never imports `master_rig.tscn` (see §5f / §8). - `StkRigAdapter.gd` is a **standalone script** with no hard dependency back into the editor. It is not referenced by any existing scene or autoload. Key facts established from exploration (cited throughout): - The Phase 7 silhouette guide already hardcodes the 13 rest-pose joint coordinates of `master_rig.tscn` as `GUIDE_JOINTS` in `scripts/whole_stickman_preview.gd:59-73`. These are the **same pivots** the adapter's bone-fitting targets. - `master_rig.tscn`'s bone names/paths match the Phase 8 requirement text exactly (`Skeleton2D/Torso/LeftUpperArm`, `LeftLowerArm`, `RightUpperArm`, `RightLowerArm`, `LeftUpperLeg`, `LeftLowerLeg`, `RightUpperLeg`, `RightLowerLeg`, `IK_Targets/Left_Leg`, `IK_Targets/Right_Leg`, `IK_Targets/Left_Hand`, `IK_Targets/Right_Hand`). - The editor already serializes per-part `shapes`/`position`/`rotation`/`scale` in `scripts/stickman_editor.gd:363-394`. --- ## 1. Data Model Changes (`.stk`) ### 1a. Version bump → `"1.4"` `FILE_VERSION` (`stickman_editor.gd:51`) becomes `"1.4"`. `SUPPORTED_VERSIONS` (`:54`) gains `"1.4"` (so loading `["1.0".."1.4"]`). No other load-side migration is required (see 1d). ### 1b. Top-level `proportions` object A new top-level key, sibling to `version` / `stickman_name` / `part_order` / `body_parts` / `metadata`: ```json { "version": "1.4", "stickman_name": "Bob", "part_order": [ "..." ], "proportions": { "upper_arm_length": 168.0, "lower_arm_length": 200.0, "upper_leg_length": 200.0, "lower_leg_length": 200.0, "torso_length": 391.5 }, "body_parts": { "..." }, "metadata": { "..." } } ``` | Key | Type | Description | |---|---|---| | `upper_arm_length` | `float` | Distance shoulder → elbow (== `LeftUpperArm.length`). | | `lower_arm_length` | `float` | Distance elbow → wrist (== `LeftLowerArm.length`). | | `upper_leg_length` | `float` | Distance hip → knee (== `LeftUpperLeg.length`). | | `lower_leg_length` | `float` | Distance knee → ankle (== `LeftLowerLeg.length`). | | `torso_length` | `float` | Height from hip base to neck (`Hips` → `Neck`). | **Source = master-rig rest-pose constants (hardcoded), not the user's shapes.** Full derivation in §2. ### 1c. Per-part `pivot` and `length` Inside **each** of the 10 `body_parts[part_name]` objects (siblings of `shapes`/`position`/`rotation`/`scale`): ```json "torso": { "shapes": [ "..." ], "position": { "x": 150.0, "y": 100.0 }, "rotation": 0.0, "scale": { "x": 1.0, "y": 1.0 }, "pivot": { "x": 300.5, "y": 250.0 }, "length": 99.0 } ``` | Key | Type | Description | |---|---|---| | `pivot` | `object` | `{ "x": float, "y": float }` — the **local origin point of rotation**, i.e. the bounding-box center of all shape points in the part's **local drawing space** (before `position` is applied). | | `length` | `float` | The part's bounding-box extent along its **segment axis** (see §3), in local pixels. | Both are **always written for all 10 parts** (empty part → `pivot {0,0}` / `length 0.0`) so the schema is uniform. ### 1d. Backward compatibility / migration - `pivot` / `length` / `proportions` are **write-only** metadata. The editor never reads them back (they are recomputed from live shapes at every save). Therefore the load path (`_apply_json_data`, `stickman_editor.gd:397-476`) requires **no changes** — old v1.0–v1.3 files simply lack these keys and load identically to today; they gain the keys on their next save. - v1.0/v1.1/v1.2/v1.3 files remain loadable (already handled by `SUPPORTED_VERSIONS` + the existing wrap/migrate logic at `:426-439`). - No `settings.json` change (proportions/pivot/length are figure data, not editor preferences). --- ## 2. Proportions derivation (master-rig rest-pose constants) **Recommendation: hardcode the 5 proportions as `const` values** in `stickman_editor.gd`, derived once from `master_rig.tscn` (the same approach Phase 7 took for `GUIDE_JOINTS`). **Do not** derive them from the user's part bounding boxes. Justification: 1. The proportions describe the **target rig's** joint distances, and the adapter writes them directly into `Bone2D.length` / `position.x/y`. Those values are the authored `master_rig.tscn` bone lengths, not whatever arbitrary size the user happened to draw. 2. The requirement says "after silhouette alignment" — the user aligns their parts **to** the guide (the rig rest pose). The guide *is* the source of truth for joint distances; the user's parts are the thing being aligned, so measuring them would be circular and fragile. 3. Deterministic, editor-safe, no scene instantiation (mirrors the Phase 7 rationale for hardcoding `GUIDE_JOINTS`). ### Derivation table | Proportion | Source node / joint | Value | |---|---|---| | `upper_arm_length` | `Skeleton2D/Torso/LeftUpperArm.length` (`master_rig.tscn:173`); guide `LeftShoulder`→`LeftElbow` | **168.0** | | `lower_arm_length` | `Skeleton2D/Torso/LeftUpperArm/LeftLowerArm.length` (`:181`); guide `LeftElbow`→`LeftWrist` | **200.0** | | `upper_leg_length` | `Skeleton2D/Torso/LeftUpperLeg.length` (`:222`); guide `Hips`→`LeftKnee` | **200.0** | | `lower_leg_length` | `Skeleton2D/Torso/LeftUpperLeg/LeftLowerLeg.length` (`:230`); guide `LeftKnee`→`LeftAnkle` | **200.0** | | `torso_length` | `Hips (0,0)` → `Neck (0,-391.5)` (`Skeleton2D/Torso/Head.position.y`, `:152`) | **391.5** | Notes (reproducible): - **Bone length vs. joint distance.** `upper_arm_length` is the *bone length* `168.0`, not the literal `|shoulder - elbow| = sqrt(168² + 8²) ≈ 168.19`. The 8 px vertical offset comes from the arm bone's `rotation = 0.0477` (`master_rig.tscn:170`), which the adapter does **not** touch. Writing `168.0` reproduces the rest pose exactly when the adapter sets `LeftUpperArm.length = 168.0` and leaves `rotation` alone. (Writing `168.19` would over-extend the bone.) The requirement's "distance between shoulder and elbow" is treated as the bone length. - **`torso_length`** has no corresponding single bone (the spine is the `Head` bone authored at `y = -391.5`); it is informational in `proportions` (no adapter bone op consumes it). - **`RightUpperLeg.length = 90.0` inconsistency.** `master_rig.tscn:245` authors the right upper leg bone at `90.0` while the left is `200.0` (`:222`) and `master_rig2.tscn:262` fixes the right to `200.0`. Use **200.0** (the symmetric/corrected value). The adapter overwrites both legs anyway, so the discrepancy is moot after fitting — but it is flagged in §5f. --- ## 3. Per-part `pivot` and `length` computation Computed in the editor from each panel's local shape points (via `BodyPartPanel.get_shape_data()`, `body_part_panel.gd:195-218`, which returns points as `{x, y}` dicts in **local drawing space**). ### 3a. `pivot` — local bounding-box center ``` min_x, min_y, max_x, max_y = bounds over ALL points of ALL shapes in the part pivot = { x: (min_x + max_x) / 2, y: (min_y + max_y) / 2 } ``` This is the **local** analogue of `WholeStickmanPreview._compute_center()` (`whole_stickman_preview.gd:509-519`), which computes the world-space center from `pt + position`. Local pivot = world center − `position` = bbox center of local points. Matches the editor's actual rotation pivot. ### 3b. `length` — extent along the segment axis ``` width = max_x - min_x height = max_y - min_y length = width (for arm parts) = height (for torso, legs, head) ``` **Axis mapping** (matches the adapter's bone convention — arms use `position.x`, legs/torso use `position.y`, §5c): | Part family | Parts | Axis | |---|---|---| | Arms | `left_upper_arm`, `left_lower_arm`, `right_upper_arm`, `right_lower_arm` | **X** (`width`) | | Legs + torso + head | `torso`, `left_upper_leg`, `left_lower_leg`, `right_upper_leg`, `right_lower_leg`, `head` | **Y** (`height`) | Empty part (no shapes, or all shapes with <2 points) → `pivot {0,0}`, `length 0.0`. > **Alternative considered (not chosen):** `length = max(width, height)`. Simpler and axis-agnostic, but loses the arm/leg axis distinction that the adapter's `position.x` vs `position.y` convention encodes. See §10 Q1. --- ## 4. Editor changes (`scripts/stickman_editor.gd`) ### 4a. New constants ```gdscript const FILE_VERSION := "1.4" # was "1.3" (line 51) const SUPPORTED_VERSIONS: Array[String] = ["1.0", "1.1", "1.2", "1.3", "1.4"] # line 54 # Phase 8: rig proportions (master_rig.tscn rest pose — see spec §2) const PROPORTIONS: Dictionary = { "upper_arm_length": 168.0, "lower_arm_length": 200.0, "upper_leg_length": 200.0, "lower_leg_length": 200.0, "torso_length": 391.5, } ``` ### 4b. New compute helper ```gdscript const X_AXIS_PARTS: PackedStringArray = [ "left_upper_arm", "left_lower_arm", "right_upper_arm", "right_lower_arm", ] func _compute_part_pivot_length(shapes_arr: Array, part_name: String) -> Dictionary: var min_x := INF; var min_y := INF; var max_x := -INF; var max_y := -INF for sd in shapes_arr: if not sd is Dictionary: continue for p in (sd as Dictionary).get("points", []): if not p is Dictionary: continue var px: float = float((p as Dictionary).get("x", 0.0)) var py: float = float((p as Dictionary).get("y", 0.0)) min_x = min(min_x, px); min_y = min(min_y, py) max_x = max(max_x, px); max_y = max(max_y, py) if min_x > max_x or min_y > max_y: return { "pivot": { "x": 0.0, "y": 0.0 }, "length": 0.0 } var pivot := { "x": (min_x + max_x) * 0.5, "y": (min_y + max_y) * 0.5 } var length: float if X_AXIS_PARTS.has(part_name): length = max_x - min_x else: length = max_y - min_y return { "pivot": pivot, "length": length } ``` ### 4c. Wire into serialization **`_collect_all_shape_data()`** (`stickman_editor.gd:363-378`) — extend the per-part dict with `pivot`/`length`: ```gdscript all_data[part_name] = { "shapes": shapes_arr, "position": {"x": pos.x, "y": pos.y}, "rotation": rot, "scale": {"x": scl.x, "y": scl.y}, "pivot": pivot_length["pivot"], "length": pivot_length["length"], } ``` **`_build_json_data()`** (`:381-394`) — add the top-level `proportions` key: ```gdscript return { "version": FILE_VERSION, "stickman_name": _stickman_name_edit.text.strip_edges(), "part_order": _whole_preview.get_part_order(), "proportions": PROPORTIONS.duplicate(), "body_parts": body_parts, "metadata": { "created_at": time_str, "modified_at": time_str }, } ``` No change to `_on_save_file_selected` (`:287-300`) beyond what the above touches. ### 4d. Load path — no change `_apply_json_data` (`:397-476`) ignores unknown keys; `proportions`/`pivot`/`length` are never read. Version `"1.4"` is now accepted by the extended `SUPPORTED_VERSIONS`. --- ## 5. `StkRigAdapter.gd` (runtime adapter) ### 5a. Class shape & API New file `res://scripts/stk_rig_adapter.gd`: ```gdscript class_name StkRigAdapter extends RefCounted ## Standalone runtime adapter: fits an instantiated master_rig.tscn to a ## loaded .stk dictionary (proportions + shapes). Not referenced by the editor. static func apply(stk_data: Dictionary, rig: Node2D) -> void ``` - `rig` is the instantiated `master_rig.tscn` root (`Master`, a `Node2D`). - `stk_data` is the parsed `.stk` dictionary (the adapter reads `proportions` and `body_parts`). - All node access goes through `rig.get_node_or_null(NodePath)` with the fixed paths in §5b; **every lookup is null-guarded** so a malformed/foreign scene fails gracefully (push a warning, skip that op) rather than hard-erroring. - `apply()` calls three private helpers in order: `_fit_bones` → `_recalibrate_ik` → `_mount_shapes`. ### 5b. Part-key → node-path mapping table Paths are **relative to the `rig` root** (`Master`). Bone paths are used by `_fit_bones`; `Body` visual paths by `_mount_shapes`. | `part_key` | Bone node (fitting) | `Body` visual node (mount) | |---|---|---| | `head` | `Skeleton2D/Torso/Head` (not length-fitted) | `Body/Head` (circle `Node2D`) | | `torso` | — (no torso bone) | `Body/Body` (`Line2D`) | | `left_upper_arm` | `Skeleton2D/Torso/LeftUpperArm` | `Body/LeftUpperArm` | | `left_lower_arm` | `Skeleton2D/Torso/LeftUpperArm/LeftLowerArm` | `Body/LeftLowerArm` | | `right_upper_arm` | `Skeleton2D/Torso/RightUpperArm` | `Body/RightUpperArm` | | `right_lower_arm` | `Skeleton2D/Torso/RightUpperArm/RightLowerArm` | `Body/RightLowerArm` | | `left_upper_leg` | `Skeleton2D/Torso/LeftUpperLeg` | `Body/LeftUpperLeg` | | `left_lower_leg` | `Skeleton2D/Torso/LeftUpperLeg/LeftLowerLeg` | `Body/LeftLowerLeg` | | `right_upper_leg` | `Skeleton2D/Torso/RightUpperLeg` | `Body/RightUpperLeg` | | `right_lower_leg` | `Skeleton2D/Torso/RightUpperLeg/RightLowerLeg` | `Body/RightLowerLeg` | The 10 `Body/*` visual nodes and their `RemoteTransform2D` drivers are enumerated in `master_rig.tscn:71-141` (visuals) and `:160-262` (bone `RemoteTransform2D`). Every visual node is already driven by a `RemoteTransform2D` under the matching bone, so mounting geometry **into** these nodes inherits the skeleton's pose for free. ### 5c. Bone fitting (`_fit_bones`) Reads `proportions` (with defaults = §2 values when the key is absent, for robustness). Applies the Phase 8 requirement **verbatim**, plus a marked completion: ``` proportions = stk_data.get("proportions", DEFAULTS) ua = proportions.upper_arm_length la = proportions.lower_arm_length ul = proportions.upper_leg_length ll = proportions.lower_leg_length # Arms — upper length + lower-bone origin on X (requirement verbatim) LeftUpperArm.length = ua ; LeftLowerArm.position.x = -ua RightUpperArm.length = ua ; RightLowerArm.position.x = ua # Legs — upper length + lower-bone origin on Y (requirement verbatim) LeftUpperLeg.length = ul ; LeftLowerLeg.position.y = ul RightUpperLeg.length = ul ; RightLowerLeg.position.y = ul # RECOMMENDED COMPLETION (see note): also fit the lower-bone lengths LeftLowerArm.length = la ; RightLowerArm.length = la LeftLowerLeg.length = ll ; RightLowerLeg.length = ll ``` - The four `*.length = ua/ul` and four `position.x/y` assignments are exactly the requirement's "Bone Fitting Logic". - **Recommended completion:** the requirement omits setting `LeftLowerArm.length` / `RightLowerArm.length` / `LeftLowerLeg.length` / `RightLowerLeg.length` (= `lower_arm_length` / `lower_leg_length`). Without it, lower limbs keep their authored `200.0` and won't scale if proportions differ from defaults. This completion is flagged in §10 Q2; it is safe (a no-op for the default proportions) and makes the adapter actually "fit" all 8 limb bones. - `auto_calculate_length_and_angle` is already `false` on all these bones (`master_rig.tscn:156/172/180/197/205/221/229/245/252`), so direct `.length` writes are authoritative. - Do **not** touch `bone_angle`, `rotation`, or `rest` — those encode the rest-pose orientation and must be preserved. ### 5d. IK target recalibration (`_recalibrate_ik`) ``` ul = upper_leg_length ; ll = lower_leg_length ua = upper_arm_length ; la = lower_arm_length # Leg IK targets (requirement verbatim) IK_Targets/Left_Leg.position.y = ul + ll IK_Targets/Right_Leg.position.y = ul + ll # Hand IK targets — "default rests to match total arm length" IK_Targets/Left_Hand.position.x = -ua IK_Targets/Right_Hand.position.x = ua IK_Targets/Left_Hand.position.y = ELBOW_REST_Y - la IK_Targets/Right_Hand.position.y = ELBOW_REST_Y - la ``` - `ELBOW_REST_Y = -256.0` is the authored elbow height (`Body/LeftUpperArm.position.y`, `master_rig.tscn:116`; also the `LeftElbow` guide joint). Keeping `hand.y = elbow_y − lower_arm_length` preserves the elbow→wrist vertical span while the x-position tracks the (possibly re-fitted) upper-arm length. With default proportions this reproduces the authored hand rest `(±168, −456)` exactly. - `IK_Targets/Head` and `IK_Targets/Torso` are **not** touched (no proportion governs them). - The `TwoBoneIK` `target_nodepath`s already point at `../IK_Targets/{Left,Right}_{Hand,Leg}` (`master_rig.tscn:21-49`), so moving the `Marker2D`s is sufficient — no modification-stack edits required. - The leg value `ul + ll = 400.0` differs from the authored `Left_Leg.y = 376.0` (`master_rig.tscn:283`); the authored pose has a slight knee bend (knee world y ≈ 176). Setting `400.0` is the requirement's intent ("stand straight" default reach). Noted, not overridden. ### 5e. Visual shape mount (`_mount_shapes`) For each of the 10 part keys, mount the part's `.stk` shapes into the corresponding `Body/*` node (§5b). Two equivalent strategies are permitted by the requirement ("replace default Line2D nodes **or** instantiate new nodes"); the spec recommends **updating the existing `Body/*` nodes in place**, which preserves the `RemoteTransform2D` driving and keeps node names stable: 1. **Coordinate transform.** `.stk` shape points are in arbitrary panel-local space (e.g. `test.stk` has points around x∈[150,600]). Transform each point into the `Body` node's local convention: ``` pt_local = (pt - pivot) * scale_factor scale_factor = bone_length / part_length # per part, per axis (see below) ``` where `pivot` and `length` come from `body_parts[part_name]`, and `bone_length` is the corresponding proportion (arms → `upper_arm_length`/`lower_arm_length`; legs → `upper_leg_length`/`lower_leg_length`; torso → `torso_length`; head → leave at 1.0 scale, translate only). This maps the part's `pivot` → the node's origin and stretches the drawn segment to the bone length. 2. **Node construction per shape** (`.stk` shape dict → Godot nodes): - `color = Color.from_string(shape["color"], Color.WHITE)` - **Open** (`closed == false`) → one `Line2D` with `points = transformed`, `width = DEFAULT_LINE_WIDTH`, `default_color = color`. - **Closed** (`closed == true`) → one `Polygon2D` (`polygon = transformed`, `color = color`) for the fill **plus** one `Line2D` (`closed = true`) for the outline — mirroring the editor's fill+outline rendering (`body_part_panel.gd:380-385`). - `DEFAULT_LINE_WIDTH := 16.0` (matches `master_rig.tscn`'s `width = 16.0`; `.stk` stores no width). 3. **Head special case.** `Body/Head` is a `Node2D` with the embedded circle `@tool` script (`master_rig.tscn:73-77`, `radius = 100`). For a `head` part whose shapes are circle-like, set its `radius`/`color` exports from the head bbox; otherwise (or as a uniform v1), replace it with the same `Polygon2D`/`Line2D` treatment as other parts. 4. **Empty part** → remove/clear the corresponding `Body` node's geometry (freeze or hide), so a part the user never drew doesn't render the default `Line2D`. > The coordinate mapping here is the most under-specified piece of the requirement. §5e defines a concrete, deterministic v1 (translate `pivot`→origin, scale along the segment axis to the bone length). The fine-grained fidelity (matching the user's in-preview `rotation`/`scale`/`position` exactly) is intentionally left to the runtime consumer — see §10 Q3. ### 5f. `master_rig.tscn` vs `master_rig2.tscn` - The adapter targets **`master_rig.tscn`** (the clean scene Phase 7 derived `GUIDE_JOINTS` from). It has **no** `metadata/_local_pose_override_enabled_` on its bones. - `master_rig2.tscn` is a variant that (a) adds `metadata/_local_pose_override_enabled_ = true` to every bone, (b) fixes `RightUpperLeg.length` from `90.0` → `200.0` (`master_rig2.tscn:262`), and (c) explicitly sets `enabled = true` on the modification stack. `clear_pose.gd` (an `EditorScript`) exists to strip the pose-override metadata and reset bone scales to `Vector2.ONE`/`rest`. - **Implication:** the adapter is written against `master_rig.tscn` node names (identical in both) and overwrites lengths anyway, so it works with either — but the `90.0` right-leg value in `master_rig.tscn` is a latent bug the adapter must not rely on (it writes `RightUpperLeg.length` from `proportions`, §5c). ### 5g. Editor does **not** verify/import the adapter The editor only *produces* data the adapter *consumes*. There is no editor→adapter reference, no `preload("res://scripts/stk_rig_adapter.gd")`, no scene that imports it. "Verification" is a standalone concern: the adapter is syntax-checked with the project parse check and (optionally) a future headless smoke test (§8). This keeps the editor decoupled from the runtime pipeline. --- ## 6. Files Modified | File | Changes | |---|---| | `scripts/stickman_editor.gd` | `FILE_VERSION` → `"1.4"`; add `"1.4"` to `SUPPORTED_VERSIONS`; add `PROPORTIONS` const + `X_AXIS_PARTS` const; add `_compute_part_pivot_length()`; extend `_collect_all_shape_data()` with `pivot`/`length`; add `proportions` to `_build_json_data()`. | | `scripts/stk_rig_adapter.gd` | **New.** `StkRigAdapter` (`RefCounted`) with `static func apply(stk_data, rig)` + `_fit_bones` / `_recalibrate_ik` / `_mount_shapes` + node-path consts (§5). | | `docs/phase8_spec.md` | This file. | | `README.md` | Document the `.stk` v1.4 `proportions`/`pivot`/`length` keys, the `StkRigAdapter.gd` script, and the runtime-pipeline note (§11). | | `AGENTS.md` | Add a Phase 8 note (v1.4 export + `StkRigAdapter.gd`). | No `.tscn` changes. No `settings.json` change. No load-path change. --- ## 7. Edge Cases & Constraints - **Empty part** — no shapes → `pivot {0,0}`, `length 0.0`; the adapter clears/hides that `Body` node's geometry. Never divide by zero in the mount transform (`scale_factor` guards `length <= 0` → 1.0 or skip). - **Multi-shape parts** — `pivot`/`length` are computed over **all** shapes in the part (the part is treated as one object, matching the Whole Stickman preview semantics). - **`part_length == 0` or `bone_length == 0`** — guard the mount scale; fall back to translation-only. - **Negative scale parts** (Phase 5 mirroring sets `scale.x/y` negative) — `pivot`/`length` are computed from the **unscaled local points** (the panel geometry), not the preview transform, so mirroring does not affect them. (The adapter does not consume part `scale`/`rotation`/`position` in v1 — see Q3.) - **Old files loaded then saved** — v1.0–v1.3 files load unchanged and are written out as `"1.4"` with computed `proportions`/`pivot`/`length` on the next save. - **Adapter with a foreign/malformed scene** — every `get_node_or_null` is null-guarded; missing nodes → push a warning and skip, never crash. - **Adapter with a `.stk` lacking `proportions`** — falls back to the §2 default constants, so the rig still fits to the standard rest pose. --- ## 8. Testing / Verification There is no automated test suite in the repo (no `test/` directory, no GUT addon installed — the `tester` agent's GUT template is aspirational; `glob test*.gd` returns nothing). Verification is manual + the project parse check. 1. **Parse check** — run from `C:\Godot4\stickman` (engine binary per the project: `config/features = "4.7"`, matching the user's 4.7.1 binary): ``` ..\Godot_v4.7.1-stable_win64_console.exe . --headless --check-only --quit ``` > The plain `--check-only` form hangs on renderer init in 4.7.x; use `--headless --check-only --quit` (as established in `docs/phase7_round1_spec.md` §6). The user's stated command `..\Godot_v4.7.1-stable_win64_console.exe . --check-only` is equivalent in intent but should include `--headless --quit`. 2. **Save emits v1.4** — File → Save; inspect the `.stk`: `version == "1.4"`, a top-level `proportions` with the 5 values (`168.0/200.0/200.0/200.0/391.5`), and every `body_parts.*` entry has `pivot {x,y}` + `length` (arm parts' `length` = bbox width; leg/torso/head = bbox height). 3. **Empty part** — a part with no shapes writes `pivot {0,0}` / `length 0.0`; no crash. 4. **Backward-compat load** — load `stickmen/basic.stk` (v1.0) and `stickmen/test.stk` (v1.1); no error; save; result is v1.4 with computed pivot/length/proportions. 5. **Adapter smoke test (standalone, optional/headless)** — a small `--headless` SceneTree script that instantiates `master_rig.tscn`, calls `StkRigAdapter.apply(sample_stk, rig)`, and asserts: `LeftUpperArm.length == 168.0`, `LeftLowerArm.position.x == -168.0`, `LeftUpperLeg.length == 200.0`, `IK_Targets/Left_Leg.position.y == 400.0`, `IK_Targets/Left_Hand.position == (-168.0, -456.0)`. (Add under `test/` later if GUT is introduced; out of scope for this phase.) --- ## 9. Design Decisions (summary) | # | Decision | One-line justification | |---|---|---| | D1 | `proportions` = hardcoded master-rig rest-pose constants (§2) | They describe the rig's joints and are written verbatim into bones; user shapes are the aligned thing, not the measure. | | D2 | `pivot` = local bbox center; `length` = axis-specific extent (arms→Δx, legs/torso/head→Δy) (§3) | Matches the editor's actual rotation pivot and the adapter's `position.x` vs `position.y` bone convention. | | D3 | Version `"1.3" → "1.4"`, write-only metadata, no load migration (§1) | `pivot`/`length`/`proportions` are recomputed on save; old files load unchanged and gain keys on next save. | | D4 | `StkRigAdapter` = `RefCounted` static `apply()`, standalone script (§5a) | No scene/autoload dependency; consumed by a future runtime pipeline, never by the editor. | | D5 | Adapter targets `master_rig.tscn` (not `master_rig2.tscn`) (§5f) | It's the clean scene Phase 7 derived `GUIDE_JOINTS` from; `master_rig2` is a pose-override variant with a fixed right-leg length. | | D6 | Visual mount = update existing `Body/*` nodes in place (§5e) | Preserves `RemoteTransform2D` driving and node names; `.stk` shape → `Line2D`/`Polygon2D` with pivot→origin + length-normalization. | | D7 | Editor never imports/verifies the adapter (§5g) | Keeps the editor decoupled; the adapter is verified standalone (parse check + optional headless smoke test). | | D8 | Bone fitting adds lower-bone `.length` writes beyond the requirement's literal text (§5c) | Without it the lower limbs don't scale; the addition is a safe no-op at default proportions (flagged Q2). | --- ## 10. Open Questions — RESOLVED (user-approved) 1. **`length` axis convention (D2).** ✅ **Axis-specific**: arms→X / legs+torso+head→Y (§3b). 2. **Lower-bone length fitting (D8).** ✅ **Include** the completion `LeftLowerArm.length = lower_arm_length` etc. (§5c). 3. **Visual-mount fidelity (§5e).** ✅ **v1 as specified**: pivot→bone origin + scale to bone length; part `position`/`rotation`/`scale` ignored (noted in adapter docs as a v2 concern). 4. **`proportions` arms (§2).** ✅ **168.0** (bone length, reproduces rest pose). 5. **Head `length` (§3b).** ✅ Vertical bbox height (circle diameter). --- ## 11. README / AGENTS Updates - **README §"File format (`.stk`)"** — bump the example to `"1.4"`; add the top-level `proportions` table and the per-part `pivot`/`length` rows to the Part object table; update the migration note (`v1.0–v1.3` auto-migrate; pivot/length/proportions recomputed on save). - **README new subsection (or §"Project structure")** — document `scripts/stk_rig_adapter.gd`: its `static apply()` API, that it fits an instantiated `master_rig.tscn` to a loaded `.stk`, and that it is a runtime-pipeline utility (not used by the editor). - **AGENTS.md** — add a Phase 8 note: `FILE_VERSION "1.4"`; top-level `proportions` + per-part `pivot`/`length` computed on save in `stickman_editor.gd`; `scripts/stk_rig_adapter.gd` (`class_name StkRigAdapter`) as a standalone runtime adapter targeting `master_rig.tscn`. --- ## 12. Recommended Implementation Order 1. `scripts/stickman_editor.gd` — constants (`FILE_VERSION`, `SUPPORTED_VERSIONS`, `PROPORTIONS`, `X_AXIS_PARTS`) + `_compute_part_pivot_length()`. 2. `scripts/stickman_editor.gd` — wire `pivot`/`length` into `_collect_all_shape_data()` and `proportions` into `_build_json_data()`. 3. `scripts/stk_rig_adapter.gd` — `apply()` + `_fit_bones()` + `_recalibrate_ik()` + `_mount_shapes()` with §5b paths. 4. Manual verification (§8) + `--headless --check-only --quit`. 5. Optional headless adapter smoke test. 6. Doc updates (`README.md`, `AGENTS.md`).