- 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.
28 KiB
Phase 8 — Architectural Specification
Overview
Phase 8 has two deliverables:
- Save-side
.stkexport — when saving/exporting, the editor appends a top-levelproportionsobject (5 rig bone lengths) and, inside eachbody_partsentry, apivot({x, y}) andlength(float). This is the only editor change; the editor does not instantiate the rig. - A standalone runtime adapter
StkRigAdapter.gd— a GDScript utility that takes a loaded.stkdictionary + an instantiatedmaster_rig.tscnnode and re-fits the skeleton (bone lengths), recalibrates the IK targets, and mounts the.stkvector shapes onto the rig'sBody/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 loadsStkRigAdapter.gdand never importsmaster_rig.tscn(see §5f / §8). StkRigAdapter.gdis 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.tscnasGUIDE_JOINTSinscripts/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/scaleinscripts/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 (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):
"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/proportionsare 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.jsonchange (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:
- 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 authoredmaster_rig.tscnbone lengths, not whatever arbitrary size the user happened to draw. - 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.
- 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_lengthis the bone length168.0, not the literal|shoulder - elbow| = sqrt(168² + 8²) ≈ 168.19. The 8 px vertical offset comes from the arm bone'srotation = 0.0477(master_rig.tscn:170), which the adapter does not touch. Writing168.0reproduces the rest pose exactly when the adapter setsLeftUpperArm.length = 168.0and leavesrotationalone. (Writing168.19would over-extend the bone.) The requirement's "distance between shoulder and elbow" is treated as the bone length. torso_lengthhas no corresponding single bone (the spine is theHeadbone authored aty = -391.5); it is informational inproportions(no adapter bone op consumes it).RightUpperLeg.length = 90.0inconsistency.master_rig.tscn:245authors the right upper leg bone at90.0while the left is200.0(:222) andmaster_rig2.tscn:262fixes the right to200.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'sposition.xvsposition.yconvention 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
rigis the instantiatedmaster_rig.tscnroot (Master, aNode2D).stk_datais the parsed.stkdictionary (the adapter readsproportionsandbody_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/uland fourposition.x/yassignments 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 authored200.0and 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_angleis alreadyfalseon all these bones (master_rig.tscn:156/172/180/197/205/221/229/245/252), so direct.lengthwrites are authoritative.- Do not touch
bone_angle,rotation, orrest— 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.0is the authored elbow height (Body/LeftUpperArm.position.y,master_rig.tscn:116; also theLeftElbowguide joint). Keepinghand.y = elbow_y − lower_arm_lengthpreserves 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/HeadandIK_Targets/Torsoare not touched (no proportion governs them).- The
TwoBoneIKtarget_nodepaths already point at../IK_Targets/{Left,Right}_{Hand,Leg}(master_rig.tscn:21-49), so moving theMarker2Ds is sufficient — no modification-stack edits required. - The leg value
ul + ll = 400.0differs from the authoredLeft_Leg.y = 376.0(master_rig.tscn:283); the authored pose has a slight knee bend (knee world y ≈ 176). Setting400.0is 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:
- Coordinate transform.
.stkshape points are in arbitrary panel-local space (e.g.test.stkhas points around x∈[150,600]). Transform each point into theBodynode's local convention:wherept_local = (pt - pivot) * scale_factor scale_factor = bone_length / part_length # per part, per axis (see below)pivotandlengthcome frombody_parts[part_name], andbone_lengthis 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'spivot→ the node's origin and stretches the drawn segment to the bone length. - Node construction per shape (
.stkshape dict → Godot nodes):color = Color.from_string(shape["color"], Color.WHITE)- Open (
closed == false) → oneLine2Dwithpoints = transformed,width = DEFAULT_LINE_WIDTH,default_color = color. - Closed (
closed == true) → onePolygon2D(polygon = transformed,color = color) for the fill plus oneLine2D(closed = true) for the outline — mirroring the editor's fill+outline rendering (body_part_panel.gd:380-385). DEFAULT_LINE_WIDTH := 16.0(matchesmaster_rig.tscn'swidth = 16.0;.stkstores no width).
- Head special case.
Body/Headis aNode2Dwith the embedded circle@toolscript (master_rig.tscn:73-77,radius = 100). For aheadpart whose shapes are circle-like, set itsradius/colorexports from the head bbox; otherwise (or as a uniform v1), replace it with the samePolygon2D/Line2Dtreatment as other parts. - Empty part → remove/clear the corresponding
Bodynode's geometry (freeze or hide), so a part the user never drew doesn't render the defaultLine2D.
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-previewrotation/scale/positionexactly) 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 derivedGUIDE_JOINTSfrom). It has nometadata/_local_pose_override_enabled_on its bones. master_rig2.tscnis a variant that (a) addsmetadata/_local_pose_override_enabled_ = trueto every bone, (b) fixesRightUpperLeg.lengthfrom90.0→200.0(master_rig2.tscn:262), and (c) explicitly setsenabled = trueon the modification stack.clear_pose.gd(anEditorScript) exists to strip the pose-override metadata and reset bone scales toVector2.ONE/rest.- Implication: the adapter is written against
master_rig.tscnnode names (identical in both) and overwrites lengths anyway, so it works with either — but the90.0right-leg value inmaster_rig.tscnis a latent bug the adapter must not rely on (it writesRightUpperLeg.lengthfromproportions, §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 thatBodynode's geometry. Never divide by zero in the mount transform (scale_factorguardslength <= 0→ 1.0 or skip). - Multi-shape parts —
pivot/lengthare computed over all shapes in the part (the part is treated as one object, matching the Whole Stickman preview semantics). part_length == 0orbone_length == 0— guard the mount scale; fall back to translation-only.- Negative scale parts (Phase 5 mirroring sets
scale.x/ynegative) —pivot/lengthare computed from the unscaled local points (the panel geometry), not the preview transform, so mirroring does not affect them. (The adapter does not consume partscale/rotation/positionin v1 — see Q3.) - Old files loaded then saved — v1.0–v1.3 files load unchanged and are written out as
"1.4"with computedproportions/pivot/lengthon the next save. - Adapter with a foreign/malformed scene — every
get_node_or_nullis null-guarded; missing nodes → push a warning and skip, never crash. - Adapter with a
.stklackingproportions— 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.
- 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 --quitThe plain
--check-onlyform hangs on renderer init in 4.7.x; use--headless --check-only --quit(as established indocs/phase7_round1_spec.md§6). The user's stated command..\Godot_v4.7.1-stable_win64_console.exe . --check-onlyis equivalent in intent but should include--headless --quit. - Save emits v1.4 — File → Save; inspect the
.stk:version == "1.4", a top-levelproportionswith the 5 values (168.0/200.0/200.0/200.0/391.5), and everybody_parts.*entry haspivot {x,y}+length(arm parts'length= bbox width; leg/torso/head = bbox height). - Empty part — a part with no shapes writes
pivot {0,0}/length 0.0; no crash. - Backward-compat load — load
stickmen/basic.stk(v1.0) andstickmen/test.stk(v1.1); no error; save; result is v1.4 with computed pivot/length/proportions. - Adapter smoke test (standalone, optional/headless) — a small
--headlessSceneTree script that instantiatesmaster_rig.tscn, callsStkRigAdapter.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 undertest/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)
lengthaxis convention (D2). ✅ Axis-specific: arms→X / legs+torso+head→Y (§3b).- Lower-bone length fitting (D8). ✅ Include the completion
LeftLowerArm.length = lower_arm_lengthetc. (§5c). - Visual-mount fidelity (§5e). ✅ v1 as specified: pivot→bone origin + scale to bone length; part
position/rotation/scaleignored (noted in adapter docs as a v2 concern). proportionsarms (§2). ✅ 168.0 (bone length, reproduces rest pose).- 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-levelproportionstable and the per-partpivot/lengthrows to the Part object table; update the migration note (v1.0–v1.3auto-migrate; pivot/length/proportions recomputed on save). - README new subsection (or §"Project structure") — document
scripts/stk_rig_adapter.gd: itsstatic apply()API, that it fits an instantiatedmaster_rig.tscnto 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-levelproportions+ per-partpivot/lengthcomputed on save instickman_editor.gd;scripts/stk_rig_adapter.gd(class_name StkRigAdapter) as a standalone runtime adapter targetingmaster_rig.tscn.
12. Recommended Implementation Order
scripts/stickman_editor.gd— constants (FILE_VERSION,SUPPORTED_VERSIONS,PROPORTIONS,X_AXIS_PARTS) +_compute_part_pivot_length().scripts/stickman_editor.gd— wirepivot/lengthinto_collect_all_shape_data()andproportionsinto_build_json_data().scripts/stk_rig_adapter.gd—apply()+_fit_bones()+_recalibrate_ik()+_mount_shapes()with §5b paths.- Manual verification (§8) +
--headless --check-only --quit. - Optional headless adapter smoke test.
- Doc updates (
README.md,AGENTS.md).