Files
stickman/docs/phase8_spec.md
T
ryan 48d98ce0fe Add StkRigAdapter for runtime skeleton fitting and shape mounting
- Implemented StkRigAdapter class to adapt a master rig to a loaded .stk dictionary.
- Added methods for fitting bone lengths, recalibrating IK targets, and mounting vector shapes.
- Defined constants for default proportions and bone paths.
- Included error handling for missing nodes and invalid data structures.
2026-08-18 17:12:54 -04:00

28 KiB
Raw Blame History

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:

{
  "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 (HipsNeck).

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):

"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.0v1.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 LeftShoulderLeftElbow 168.0
lower_arm_length Skeleton2D/Torso/LeftUpperArm/LeftLowerArm.length (:181); guide LeftElbowLeftWrist 200.0
upper_leg_length Skeleton2D/Torso/LeftUpperLeg.length (:222); guide HipsLeftKnee 200.0
lower_leg_length Skeleton2D/Torso/LeftUpperLeg/LeftLowerLeg.length (:230); guide LeftKneeLeftAnkle 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

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

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:

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:

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:

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_nodepaths already point at ../IK_Targets/{Left,Right}_{Hand,Leg} (master_rig.tscn:21-49), so moving the Marker2Ds 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.0200.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 partspivot/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.0v1.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.0v1.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.

  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.gdapply() + _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).