Lane E S15 gate 1.2: re-bake sail_post top_anchor 0.90 -> 1.00 per the gate 1.1 ruling; factory emits the per-type anchor ratings manifest (anchor_ratings.gen.js) from the re-imported GLBs, ambiguity-aware, --only-guarded; asset_report carries per-asset anchor extras
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c182fc14cb
commit
e46ff18648
@ -14,6 +14,7 @@
|
||||
"ref_capsule",
|
||||
"ref_capsule_mesh"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -36,6 +37,23 @@
|
||||
"tree_gum_01",
|
||||
"trunk"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "branch_anchor_01",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 1.0
|
||||
},
|
||||
{
|
||||
"node": "branch_anchor_02",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.88
|
||||
},
|
||||
{
|
||||
"node": "branch_anchor_03",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.76
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -56,6 +74,18 @@
|
||||
"tree_gum_02",
|
||||
"trunk"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "branch_anchor_01",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 1.0
|
||||
},
|
||||
{
|
||||
"node": "branch_anchor_02",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.88
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -79,6 +109,23 @@
|
||||
"tree_jacaranda_01",
|
||||
"trunk"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "branch_anchor_01",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.95
|
||||
},
|
||||
{
|
||||
"node": "branch_anchor_02",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.52
|
||||
},
|
||||
{
|
||||
"node": "branch_anchor_03",
|
||||
"anchor_type": "tree",
|
||||
"rating_hint": 0.4
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -94,6 +141,7 @@
|
||||
"fence_post",
|
||||
"post"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -110,6 +158,7 @@
|
||||
"palings",
|
||||
"rails"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -128,6 +177,7 @@
|
||||
"hinge_axis",
|
||||
"hinges"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -153,6 +203,30 @@
|
||||
"window_glow",
|
||||
"window_light_anchor"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "fascia_anchor_01",
|
||||
"anchor_type": "house",
|
||||
"rating_hint": 0.35,
|
||||
"collateral": "gutter"
|
||||
},
|
||||
{
|
||||
"node": "fascia_anchor_02",
|
||||
"anchor_type": "house",
|
||||
"rating_hint": 0.35,
|
||||
"collateral": "gutter"
|
||||
},
|
||||
{
|
||||
"node": "fascia_anchor_03",
|
||||
"anchor_type": "house",
|
||||
"rating_hint": 0.35,
|
||||
"collateral": "gutter"
|
||||
},
|
||||
{
|
||||
"node": "window_light_anchor",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -178,6 +252,12 @@
|
||||
"window_glow",
|
||||
"window_light_anchor"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "window_light_anchor",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -200,6 +280,32 @@
|
||||
"posts",
|
||||
"roof"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "beam_anchor_01",
|
||||
"anchor_type": "carport",
|
||||
"rating_hint": 0.22,
|
||||
"collateral": "carport"
|
||||
},
|
||||
{
|
||||
"node": "beam_anchor_02",
|
||||
"anchor_type": "carport",
|
||||
"rating_hint": 0.22,
|
||||
"collateral": "carport"
|
||||
},
|
||||
{
|
||||
"node": "post_anchor_01",
|
||||
"anchor_type": "carport_post",
|
||||
"rating_hint": 0.3,
|
||||
"collateral": "carport"
|
||||
},
|
||||
{
|
||||
"node": "post_anchor_02",
|
||||
"anchor_type": "carport_post",
|
||||
"rating_hint": 0.3,
|
||||
"collateral": "carport"
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -218,6 +324,7 @@
|
||||
"posts",
|
||||
"roof_down"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -237,6 +344,24 @@
|
||||
"swing_set_01",
|
||||
"swings"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "crossbar",
|
||||
"tie_off": false
|
||||
},
|
||||
{
|
||||
"node": "frame_anchor_01",
|
||||
"anchor_type": "swing_frame",
|
||||
"rating_hint": 0.45,
|
||||
"collateral": "swing_set"
|
||||
},
|
||||
{
|
||||
"node": "frame_anchor_02",
|
||||
"anchor_type": "swing_frame",
|
||||
"rating_hint": 0.45,
|
||||
"collateral": "swing_set"
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -254,6 +379,12 @@
|
||||
"swing_set_01_wrecked",
|
||||
"swings"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "crossbar",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -272,6 +403,12 @@
|
||||
"shed_01",
|
||||
"shell"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "door_anchor",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -289,6 +426,12 @@
|
||||
"table_frame",
|
||||
"table_top"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "pickup_anchor",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -308,6 +451,7 @@
|
||||
"plants_tattered",
|
||||
"soil"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -327,6 +471,13 @@
|
||||
"sail_post",
|
||||
"top_anchor"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "top_anchor",
|
||||
"anchor_type": "post",
|
||||
"rating_hint": 1.0
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -344,6 +495,7 @@
|
||||
"ladder_base",
|
||||
"ladder_top"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -360,6 +512,7 @@
|
||||
"pin",
|
||||
"shackle"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -376,6 +529,7 @@
|
||||
"carabiner",
|
||||
"gate"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -393,6 +547,7 @@
|
||||
"eye_b",
|
||||
"turnbuckle"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -411,6 +566,7 @@
|
||||
"rim",
|
||||
"tramp_01"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -429,6 +585,7 @@
|
||||
"wheelie_bin_01",
|
||||
"wheels"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -446,6 +603,7 @@
|
||||
"mast",
|
||||
"washing_line_01"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -461,6 +619,7 @@
|
||||
"garden_gnome_01",
|
||||
"gnome"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -479,6 +638,7 @@
|
||||
"wheel_front",
|
||||
"wheel_rear"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -497,6 +657,7 @@
|
||||
"shards",
|
||||
"stump"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -512,6 +673,7 @@
|
||||
"hail_stone_01",
|
||||
"stone"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -531,6 +693,12 @@
|
||||
"head",
|
||||
"poke_tip"
|
||||
],
|
||||
"anchors": [
|
||||
{
|
||||
"node": "grip_anchor",
|
||||
"tie_off": false
|
||||
}
|
||||
],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
},
|
||||
@ -548,6 +716,7 @@
|
||||
"palings",
|
||||
"rails"
|
||||
],
|
||||
"anchors": [],
|
||||
"status": "PASS",
|
||||
"problems": []
|
||||
}
|
||||
|
||||
@ -70,6 +70,16 @@ DEBRIS_DIR = os.path.join(MODELS_DIR, "debris")
|
||||
TEXTURES_DIR = os.path.join(MODELS_DIR, "textures")
|
||||
CONTACT_SHEET = os.path.join(SCRIPT_DIR, "contact_sheet.png")
|
||||
REPORT_JSON = os.path.join(SCRIPT_DIR, "asset_report.json")
|
||||
# SPRINT15 gate 1.2 — the ratings manifest the RUNTIME reads. A baked
|
||||
# rating_hint used to reach the sim only through adoptAnchor, which only runs
|
||||
# for anchors that name a GLB `node` — every plain-JSON post defaulted to
|
||||
# `?? 1` no matter what this factory baked, and factory and sim silently
|
||||
# disagreed for fourteen sprints. This module is the missing wire: per
|
||||
# anchor_type, the factory's baked extras, generated HERE so it cannot be
|
||||
# hand-edited into a second source of truth. world.js resolves a node-less
|
||||
# JSON anchor's type through it; e.test.js pins runtime === manifest === GLB.
|
||||
RATINGS_GEN_JS = os.path.join(REPO_ROOT, "web", "world", "js",
|
||||
"anchor_ratings.gen.js")
|
||||
|
||||
# The 3D-STORE library on this box. PLAN3D §2 lists ~/Documents/3D-STORE (that
|
||||
# path is from the M1 Ultra); on the M3 Ultra the library lives here. Checked in
|
||||
@ -1684,7 +1694,16 @@ def build_sail_post(name):
|
||||
|
||||
e = add_empty("top_anchor", (0, 0, H - 0.02), size=0.2)
|
||||
e["anchor_type"] = "post"
|
||||
e["rating_hint"] = 0.9
|
||||
# SPRINT15 gate 1.1 RULING (THREADS [A] 2026-07-18): the honest post is
|
||||
# 1.00 — the shipped value becomes canon. The 0.90 baked here from Sprint 1
|
||||
# never reached the sim: every shipped post is a plain JSON entry with no
|
||||
# `node`, so adoptAnchor never ran and `?? 1` won. Every measured number in
|
||||
# the repo (separation pins, winnable-lines tables, night 5's whisker) was
|
||||
# measured at effective 1.00; 0.90 was design intent that never shipped.
|
||||
# A weaker honest post later is a DESIGN CHANGE proposed with evidence,
|
||||
# wearing its own name — not a bugfix. Do not lower this without
|
||||
# re-opening that ruling.
|
||||
e["rating_hint"] = 1.0
|
||||
above.append(e)
|
||||
for o in above:
|
||||
parent_keep_transform(o, rake)
|
||||
@ -3137,6 +3156,104 @@ def add_label(cam, text):
|
||||
return t
|
||||
|
||||
|
||||
def extract_anchor_meta(objs):
|
||||
"""The anchor-family custom props (`anchor_type`, `rating_hint`, `tie_off`,
|
||||
`collateral`), read off the RE-IMPORTED GLB rather than the build graph —
|
||||
so the report describes what actually shipped through the exporter, not
|
||||
what this script intended. Same provenance rule as the dims/nodes checks.
|
||||
"""
|
||||
out = []
|
||||
for o in objs:
|
||||
keys = set(o.keys())
|
||||
if "anchor_type" not in keys and "tie_off" not in keys:
|
||||
continue
|
||||
m = {"node": o.name}
|
||||
if "anchor_type" in keys:
|
||||
m["anchor_type"] = str(o["anchor_type"])
|
||||
if "rating_hint" in keys:
|
||||
m["rating_hint"] = round(float(o["rating_hint"]), 4)
|
||||
if "tie_off" in keys:
|
||||
m["tie_off"] = bool(o["tie_off"])
|
||||
if "collateral" in keys:
|
||||
m["collateral"] = str(o["collateral"])
|
||||
out.append(m)
|
||||
return sorted(out, key=lambda m: m["node"])
|
||||
|
||||
|
||||
def build_ratings_manifest(report):
|
||||
"""Collapse the per-asset anchor meta into per-TYPE ratings the runtime can
|
||||
resolve. The contract:
|
||||
|
||||
· `tie_off: false` nodes are DENIALS, not anchors — excluded.
|
||||
· a type where every rated node in the palette agrees on
|
||||
(rating_hint, collateral) is UNAMBIGUOUS and lands in `types` — a
|
||||
node-less JSON anchor of that type takes these extras at runtime.
|
||||
· a type whose nodes disagree (tree: 1.0/0.88/0.76 down the trunk) is
|
||||
AMBIGUOUS — listed with every variant so the runtime can refuse to
|
||||
guess. A node-less anchor of an ambiguous type is a site bug; e.test
|
||||
pins that no shipped site declares one.
|
||||
· a node that carries `anchor_type` but no `rating_hint` and no denial
|
||||
violates RULE 1 (no silent anchors) and fails the build loudly here,
|
||||
before the GLB can ship.
|
||||
"""
|
||||
per_type = {}
|
||||
for a in report:
|
||||
for m in a.get("anchors", []):
|
||||
if m.get("tie_off") is False:
|
||||
continue # explicit denial: not an anchor
|
||||
ty = m.get("anchor_type")
|
||||
if ty is None:
|
||||
continue
|
||||
if "rating_hint" not in m:
|
||||
raise RuntimeError(
|
||||
f"{a['name']}/{m['node']} carries anchor_type={ty!r} with "
|
||||
f"no rating_hint and no tie_off denial — RULE 1 (no silent "
|
||||
f"anchors). Rate it or not_a_tie_off() it.")
|
||||
key = (m["rating_hint"], m.get("collateral"))
|
||||
per_type.setdefault(ty, {}).setdefault(key, []).append(
|
||||
f"{a['name']}/{m['node']}")
|
||||
types, ambiguous = {}, {}
|
||||
for ty in sorted(per_type):
|
||||
variants = per_type[ty]
|
||||
if len(variants) == 1:
|
||||
(hint, coll), sources = next(iter(variants.items()))
|
||||
entry = {"rating_hint": hint, "sources": sorted(sources)}
|
||||
if coll is not None:
|
||||
entry["collateral"] = coll
|
||||
types[ty] = entry
|
||||
else:
|
||||
ambiguous[ty] = {"variants": [
|
||||
dict(rating_hint=h, sources=sorted(srcs),
|
||||
**({"collateral": c} if c is not None else {}))
|
||||
for (h, c), srcs in sorted(variants.items(),
|
||||
key=lambda kv: (-kv[0][0],
|
||||
kv[0][1] or ""))
|
||||
]}
|
||||
return {"types": types, "ambiguous": ambiguous}
|
||||
|
||||
|
||||
def write_ratings_module(manifest):
|
||||
body = json.dumps(manifest, indent=2, sort_keys=True)
|
||||
header = (
|
||||
"// GENERATED by tools/blender/build_yard_assets.py — do NOT hand-edit.\n"
|
||||
"// Regenerate: blender -b -P tools/blender/build_yard_assets.py\n"
|
||||
"//\n"
|
||||
"// SPRINT15 gate 1.2 — the wire between the factory and the sim.\n"
|
||||
"// adoptAnchor only carries a baked rating_hint to anchors that name a\n"
|
||||
"// GLB `node`; a plain-JSON anchor (every shipped sail post) defaulted\n"
|
||||
"// to `?? 1` regardless of the bake, and nobody could see the\n"
|
||||
"// disagreement. This module is the factory's bakes, resolvable by\n"
|
||||
"// anchor TYPE at runtime: `types` are unambiguous (every rated node\n"
|
||||
"// of that type agrees), `ambiguous` lists the disagreements so the\n"
|
||||
"// runtime can refuse to guess. ratings.js is the reader; e.test.js\n"
|
||||
"// pins runtime === this module === the GLBs, so none of the three\n"
|
||||
"// can drift from the others silently again.\n"
|
||||
)
|
||||
with open(RATINGS_GEN_JS, "w") as f:
|
||||
f.write(header + "export const FACTORY_ANCHOR_RATINGS = "
|
||||
+ "Object.freeze(" + body + ");\n")
|
||||
|
||||
|
||||
def verify_all(only=None):
|
||||
todo = [a for a in ASSETS if only is None or a["name"] in only]
|
||||
print(f"\n--- VERIFY {len(todo)} GLBs (re-import + assert + render) ---\n")
|
||||
@ -3202,8 +3319,8 @@ def verify_all(only=None):
|
||||
for p in problems:
|
||||
print(f" -> {p}")
|
||||
report.append(dict(name=name, dims=dims, tris=tris,
|
||||
nodes=sorted(names), status=status,
|
||||
problems=problems))
|
||||
nodes=sorted(names), anchors=extract_anchor_meta(objs),
|
||||
status=status, problems=problems))
|
||||
return report, failures, thumbs
|
||||
|
||||
|
||||
@ -3293,6 +3410,16 @@ def main():
|
||||
json.dump(dict(blender=bpy.app.version_string, assets=report,
|
||||
debris=debris), f, indent=2)
|
||||
print(f" report -> {REPORT_JSON}")
|
||||
# The runtime manifest is only honest when built from the WHOLE
|
||||
# palette — an --only run would collapse "every node of this type
|
||||
# agrees" to a sample of one and could flip an ambiguous type to
|
||||
# unambiguous. So partial runs leave it untouched, loudly.
|
||||
if only is None:
|
||||
write_ratings_module(build_ratings_manifest(report))
|
||||
print(f" ratings -> {RATINGS_GEN_JS}")
|
||||
else:
|
||||
print(" ratings -> NOT rewritten (--only run is a partial "
|
||||
"palette; run the full factory to regenerate)")
|
||||
|
||||
print("\n" + "=" * 72)
|
||||
if failures:
|
||||
|
||||
100
web/world/js/anchor_ratings.gen.js
Normal file
100
web/world/js/anchor_ratings.gen.js
Normal file
@ -0,0 +1,100 @@
|
||||
// GENERATED by tools/blender/build_yard_assets.py — do NOT hand-edit.
|
||||
// Regenerate: blender -b -P tools/blender/build_yard_assets.py
|
||||
//
|
||||
// SPRINT15 gate 1.2 — the wire between the factory and the sim.
|
||||
// adoptAnchor only carries a baked rating_hint to anchors that name a
|
||||
// GLB `node`; a plain-JSON anchor (every shipped sail post) defaulted
|
||||
// to `?? 1` regardless of the bake, and nobody could see the
|
||||
// disagreement. This module is the factory's bakes, resolvable by
|
||||
// anchor TYPE at runtime: `types` are unambiguous (every rated node
|
||||
// of that type agrees), `ambiguous` lists the disagreements so the
|
||||
// runtime can refuse to guess. ratings.js is the reader; e.test.js
|
||||
// pins runtime === this module === the GLBs, so none of the three
|
||||
// can drift from the others silently again.
|
||||
export const FACTORY_ANCHOR_RATINGS = Object.freeze({
|
||||
"ambiguous": {
|
||||
"tree": {
|
||||
"variants": [
|
||||
{
|
||||
"rating_hint": 1.0,
|
||||
"sources": [
|
||||
"tree_gum_01/branch_anchor_01",
|
||||
"tree_gum_02/branch_anchor_01"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rating_hint": 0.95,
|
||||
"sources": [
|
||||
"tree_jacaranda_01/branch_anchor_01"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rating_hint": 0.88,
|
||||
"sources": [
|
||||
"tree_gum_01/branch_anchor_02",
|
||||
"tree_gum_02/branch_anchor_02"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rating_hint": 0.76,
|
||||
"sources": [
|
||||
"tree_gum_01/branch_anchor_03"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rating_hint": 0.52,
|
||||
"sources": [
|
||||
"tree_jacaranda_01/branch_anchor_02"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rating_hint": 0.4,
|
||||
"sources": [
|
||||
"tree_jacaranda_01/branch_anchor_03"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"types": {
|
||||
"carport": {
|
||||
"collateral": "carport",
|
||||
"rating_hint": 0.22,
|
||||
"sources": [
|
||||
"carport_01/beam_anchor_01",
|
||||
"carport_01/beam_anchor_02"
|
||||
]
|
||||
},
|
||||
"carport_post": {
|
||||
"collateral": "carport",
|
||||
"rating_hint": 0.3,
|
||||
"sources": [
|
||||
"carport_01/post_anchor_01",
|
||||
"carport_01/post_anchor_02"
|
||||
]
|
||||
},
|
||||
"house": {
|
||||
"collateral": "gutter",
|
||||
"rating_hint": 0.35,
|
||||
"sources": [
|
||||
"house_yardside/fascia_anchor_01",
|
||||
"house_yardside/fascia_anchor_02",
|
||||
"house_yardside/fascia_anchor_03"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"rating_hint": 1.0,
|
||||
"sources": [
|
||||
"sail_post/top_anchor"
|
||||
]
|
||||
},
|
||||
"swing_frame": {
|
||||
"collateral": "swing_set",
|
||||
"rating_hint": 0.45,
|
||||
"sources": [
|
||||
"swing_set_01/frame_anchor_01",
|
||||
"swing_set_01/frame_anchor_02"
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user