arkai2025 Claude Opus 4.8 (1M context) commited on
Commit
2f0e70d
·
1 Parent(s): 48ec659

refactor(parts): split catalog into parts_data.py + add category schema

Browse files

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

src/buddy_fusion/parts.py CHANGED
@@ -1,81 +1,45 @@
1
- """Hand-authored additive part templates for the template-assembly edit path.
2
 
3
  Each part's boxes are in LOCAL coordinates: x is the part's own left-right center
4
  (0), z is centered on 0, y is the part's bottom (0 = part base). The assembler
5
  (assembler.py) computes the target creature's bounding box and face-aligns the
6
- part to the chosen anchor, so local coords only describe the part's own shape.
7
- `symmetric` parts author the LEFT half (x<=0); the assembler mirrors across x.
8
  See docs/superpowers/specs/2026-06-15-template-part-assembly-edit-design.md.
9
  """
10
  from __future__ import annotations
11
 
 
 
12
  ANCHORS = ("top", "rear", "front", "left", "right", "bottom")
13
 
14
- PARTS: dict[str, dict] = {
15
- "horns": {
16
- "desc": "a pair of curved horns",
17
- "default_anchor": "top", "default_scale": 1, "symmetric": True,
18
- "boxes": [
19
- {"x": -1, "y": 0, "z": 0, "w": 1, "h": 2, "d": 1, "color": "#efe2c0"},
20
- {"x": -1, "y": 2, "z": 1, "w": 1, "h": 1, "d": 1, "color": "#efe2c0"},
21
- ],
22
- },
23
- "wings": {
24
- "desc": "a pair of wings on the sides",
25
- "default_anchor": "left", "default_scale": 1, "symmetric": True,
26
- "boxes": [
27
- {"x": -1, "y": 1, "z": 0, "w": 1, "h": 3, "d": 4, "color": "#cfe6ff"},
28
- ],
29
- },
30
- "tail": {
31
- "desc": "a long swishy tail at the back",
32
- "default_anchor": "rear", "default_scale": 1, "symmetric": False,
33
- "boxes": [
34
- {"x": 0, "y": 1, "z": 0, "w": 1, "h": 1, "d": 3, "color": "#caa06f"},
35
- {"x": 0, "y": 2, "z": 2, "w": 1, "h": 1, "d": 2, "color": "#caa06f"},
36
- ],
37
- },
38
- "ears": {
39
- "desc": "two pointy ears on top",
40
- "default_anchor": "top", "default_scale": 1, "symmetric": True,
41
- "boxes": [
42
- {"x": -1, "y": 0, "z": 0, "w": 1, "h": 2, "d": 1, "color": "#caa06f"},
43
- ],
44
- },
45
- "house": {
46
- "desc": "a small house / hut",
47
- "default_anchor": "rear", "default_scale": 2, "symmetric": False,
48
- "boxes": [
49
- {"x": 0, "y": 0, "z": 0, "w": 4, "h": 3, "d": 4, "color": "#d8b08c"},
50
- {"x": 0, "y": 3, "z": 0, "w": 5, "h": 2, "d": 5, "color": "#b5532e"},
51
- {"x": 0, "y": 0, "z": 2, "w": 1, "h": 2, "d": 1, "color": "#6b4423"},
52
- ],
53
- },
54
- "hat": {
55
- "desc": "a hat / cap on top of the head",
56
- "default_anchor": "top", "default_scale": 1, "symmetric": False,
57
- "boxes": [
58
- {"x": 0, "y": 0, "z": 0, "w": 5, "h": 1, "d": 5, "color": "#3a3550"},
59
- {"x": 0, "y": 1, "z": 0, "w": 3, "h": 2, "d": 3, "color": "#3a3550"},
60
- ],
61
- },
62
- "crown": {
63
- "desc": "a small crown on top",
64
- "default_anchor": "top", "default_scale": 1, "symmetric": False,
65
- "boxes": [
66
- {"x": 0, "y": 0, "z": 0, "w": 4, "h": 1, "d": 4, "color": "#ffd34d"},
67
- {"x": -1, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
68
- {"x": 1, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
69
- {"x": 0, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
70
- ],
71
- },
72
- }
73
 
74
 
75
  def part_ids() -> list[str]:
76
  return list(PARTS.keys())
77
 
78
 
 
 
 
 
 
 
 
 
 
 
79
  def catalog_text() -> str:
80
- """One line per part: `id: desc` fed to the selector so it picks by id."""
81
- return "\n".join(f"- {pid}: {p['desc']}" for pid, p in PARTS.items())
 
 
 
 
 
 
 
1
+ """Helpers over the part catalog (data lives in parts_data.py).
2
 
3
  Each part's boxes are in LOCAL coordinates: x is the part's own left-right center
4
  (0), z is centered on 0, y is the part's bottom (0 = part base). The assembler
5
  (assembler.py) computes the target creature's bounding box and face-aligns the
6
+ part to the chosen anchor. `symmetric` parts author the LEFT half (x<=0); the
7
+ assembler mirrors across x.
8
  See docs/superpowers/specs/2026-06-15-template-part-assembly-edit-design.md.
9
  """
10
  from __future__ import annotations
11
 
12
+ from .parts_data import PARTS
13
+
14
  ANCHORS = ("top", "rear", "front", "left", "right", "bottom")
15
 
16
+ # Display/grouping order for the catalog + the selector prompt.
17
+ CATEGORIES = (
18
+ "horns", "ears", "tails", "wings", "headwear",
19
+ "face", "back", "limbs", "props", "accessories",
20
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
 
23
  def part_ids() -> list[str]:
24
  return list(PARTS.keys())
25
 
26
 
27
+ def parts_by_category() -> dict[str, list[str]]:
28
+ """category -> [part ids], in CATEGORIES order then insertion order. Any part
29
+ whose category is not in CATEGORIES is grouped under 'other'."""
30
+ out: dict[str, list[str]] = {c: [] for c in CATEGORIES}
31
+ for pid, p in PARTS.items():
32
+ cat = p.get("category", "other")
33
+ out.setdefault(cat, []).append(pid)
34
+ return {c: ids for c, ids in out.items() if ids}
35
+
36
+
37
  def catalog_text() -> str:
38
+ """Catalog for the selector prompt, grouped under category headers so the
39
+ small model sees structure when picking from a large set."""
40
+ lines: list[str] = []
41
+ for cat, ids in parts_by_category().items():
42
+ lines.append(f"[{cat}]")
43
+ for pid in ids:
44
+ lines.append(f"- {pid}: {PARTS[pid]['desc']}")
45
+ return "\n".join(lines)
src/buddy_fusion/parts_data.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The part catalog data: ~100 hand-authored additive voxel parts, grouped by
2
+ `category`. Box-layouts are in LOCAL coordinates (x,z = center, y = bottom);
3
+ `symmetric` parts author the LEFT half (x<=0) and the assembler/renderer mirror
4
+ across x. Kept separate from parts.py (helpers) like compendium_data.py.
5
+
6
+ Schema per entry: {desc, category, default_anchor, default_scale, symmetric, boxes}.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ # Filled in batches by category. Seeded with the original 7 so nothing regresses.
11
+ PARTS: dict[str, dict] = {
12
+ "horns": {
13
+ "desc": "a pair of curved horns", "category": "horns",
14
+ "default_anchor": "top", "default_scale": 1, "symmetric": True,
15
+ "boxes": [
16
+ {"x": -1, "y": 0, "z": 0, "w": 1, "h": 2, "d": 1, "color": "#efe2c0"},
17
+ {"x": -1, "y": 2, "z": 1, "w": 1, "h": 1, "d": 1, "color": "#efe2c0"},
18
+ ],
19
+ },
20
+ "wings": {
21
+ "desc": "a pair of wings on the sides", "category": "wings",
22
+ "default_anchor": "left", "default_scale": 1, "symmetric": True,
23
+ "boxes": [
24
+ {"x": -1, "y": 1, "z": 0, "w": 1, "h": 3, "d": 4, "color": "#cfe6ff"},
25
+ ],
26
+ },
27
+ "tail": {
28
+ "desc": "a long swishy tail at the back", "category": "tails",
29
+ "default_anchor": "rear", "default_scale": 1, "symmetric": False,
30
+ "boxes": [
31
+ {"x": 0, "y": 1, "z": 0, "w": 1, "h": 1, "d": 3, "color": "#caa06f"},
32
+ {"x": 0, "y": 2, "z": 2, "w": 1, "h": 1, "d": 2, "color": "#caa06f"},
33
+ ],
34
+ },
35
+ "ears": {
36
+ "desc": "two pointy ears on top", "category": "ears",
37
+ "default_anchor": "top", "default_scale": 1, "symmetric": True,
38
+ "boxes": [
39
+ {"x": -1, "y": 0, "z": 0, "w": 1, "h": 2, "d": 1, "color": "#caa06f"},
40
+ ],
41
+ },
42
+ "house": {
43
+ "desc": "a small house / hut", "category": "props",
44
+ "default_anchor": "rear", "default_scale": 2, "symmetric": False,
45
+ "boxes": [
46
+ {"x": 0, "y": 0, "z": 0, "w": 4, "h": 3, "d": 4, "color": "#d8b08c"},
47
+ {"x": 0, "y": 3, "z": 0, "w": 5, "h": 2, "d": 5, "color": "#b5532e"},
48
+ {"x": 0, "y": 0, "z": 2, "w": 1, "h": 2, "d": 1, "color": "#6b4423"},
49
+ ],
50
+ },
51
+ "hat": {
52
+ "desc": "a hat / cap on top of the head", "category": "headwear",
53
+ "default_anchor": "top", "default_scale": 1, "symmetric": False,
54
+ "boxes": [
55
+ {"x": 0, "y": 0, "z": 0, "w": 5, "h": 1, "d": 5, "color": "#3a3550"},
56
+ {"x": 0, "y": 1, "z": 0, "w": 3, "h": 2, "d": 3, "color": "#3a3550"},
57
+ ],
58
+ },
59
+ "crown": {
60
+ "desc": "a small crown on top", "category": "headwear",
61
+ "default_anchor": "top", "default_scale": 1, "symmetric": False,
62
+ "boxes": [
63
+ {"x": 0, "y": 0, "z": 0, "w": 4, "h": 1, "d": 4, "color": "#ffd34d"},
64
+ {"x": -1, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
65
+ {"x": 1, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
66
+ {"x": 0, "y": 1, "z": 0, "w": 1, "h": 1, "d": 1, "color": "#ffd34d"},
67
+ ],
68
+ },
69
+ }
tests/test_parts.py CHANGED
@@ -1,4 +1,4 @@
1
- from buddy_fusion.parts import PARTS, part_ids, catalog_text, ANCHORS
2
 
3
 
4
  def test_every_part_is_well_formed():
@@ -6,6 +6,8 @@ def test_every_part_is_well_formed():
6
  for pid, p in PARTS.items():
7
  assert p["desc"] and isinstance(p["desc"], str)
8
  assert p["default_anchor"] in ANCHORS
 
 
9
  assert isinstance(p["boxes"], list) and p["boxes"]
10
  for b in p["boxes"]:
11
  for k in ("x", "y", "z", "w", "h", "d"):
@@ -14,7 +16,14 @@ def test_every_part_is_well_formed():
14
  assert isinstance(b["color"], str) and b["color"].startswith("#")
15
 
16
 
17
- def test_catalog_text_lists_every_part():
 
18
  text = catalog_text()
19
  for pid in part_ids():
20
  assert pid in text
 
 
 
 
 
 
 
1
+ from buddy_fusion.parts import PARTS, part_ids, catalog_text, ANCHORS, CATEGORIES, parts_by_category
2
 
3
 
4
  def test_every_part_is_well_formed():
 
6
  for pid, p in PARTS.items():
7
  assert p["desc"] and isinstance(p["desc"], str)
8
  assert p["default_anchor"] in ANCHORS
9
+ assert p["category"] in CATEGORIES, f"{pid}: bad category {p.get('category')!r}"
10
+ assert isinstance(p["symmetric"], bool)
11
  assert isinstance(p["boxes"], list) and p["boxes"]
12
  for b in p["boxes"]:
13
  for k in ("x", "y", "z", "w", "h", "d"):
 
16
  assert isinstance(b["color"], str) and b["color"].startswith("#")
17
 
18
 
19
+ def test_ids_unique_and_catalog_lists_every_part():
20
+ assert len(part_ids()) == len(set(part_ids()))
21
  text = catalog_text()
22
  for pid in part_ids():
23
  assert pid in text
24
+
25
+
26
+ def test_parts_by_category_covers_all():
27
+ grouped = parts_by_category()
28
+ flat = [pid for ids in grouped.values() for pid in ids]
29
+ assert sorted(flat) == sorted(part_ids())