Add the fused hand prototype (hand_l_proto/hand_r_proto) and record the finger-articulation decision
Full build / Import + headless tests (push) Successful in 42s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-04 14:49:27 +01:00
1 parent 44dba010a7
commit 58d1e79a12
5 files changed
+47 -5

No files matched your search

+26
View File
@@ -1007,6 +1007,32 @@ def _hands_and_shoes(mats, spec):
return parts
def _hand_proto(mats, spec, side=-1.0):
"""Connected hand: palm, four fingers and a thumb fused into one mesh.
The shipped hand is separate primitive parts that read as a mitten at
conversation distance; fusing the finger bases into the palm gives a
connected hand with readable finger separation. `side` is −1 (left, x<0)
or +1 (right, x>0).
"""
hs = _hands_and_shoes(mats, spec)
def _cx(o):
xs = [v.co.x for v in o.data.vertices]
return (min(xs) + max(xs)) / 2
hand = [o for o in hs
if o.name.startswith(("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip"))
and (_cx(o) < 0 if side < 0 else _cx(o) > 0)]
obj = join(hand, "HandL" if side < 0 else "HandR")
bm = bmesh.new()
bm.from_mesh(obj.data)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.006)
bm.to_mesh(obj.data)
bm.free()
return obj
def _clothing(spec, mats):
layer = spec["layer"]
cloth = mats["cloth"]
+7 -2
View File
@@ -53,7 +53,8 @@ PROTOTYPE = "npc_casual" # canonical one-variant prototype
# The four buildable parts. `head` is the bare head shell; `face` is the shell
# plus nose/eyes/mouth/ears; `hand_l`/`hand_r` are the left/right hand geometry.
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto")
PART_NAMES = ("head", "face", "hand_l", "hand_r", "head_proto",
"hand_l_proto", "hand_r_proto")
_HAND_PREFIXES = ("Palm", "Finger", "Fingertip", "Thumb", "ThumbTip")
@@ -104,6 +105,10 @@ def build_part(name, variant, out, subdiv=48):
obj = ba.join(ba._face(mats, spec), "face")
elif name == "head_proto":
obj = ba.join(ba._head_proto(mats, spec, subdiv=subdiv), "head_proto")
elif name == "hand_l_proto":
obj = ba._hand_proto(mats, spec, side=-1.0)
elif name == "hand_r_proto":
obj = ba._hand_proto(mats, spec, side=1.0)
else:
side = -1.0 if name == "hand_l" else 1.0
hs = ba._hands_and_shoes(mats, spec)
@@ -155,7 +160,7 @@ def extract_part(name, variant, out):
if mesh is None:
raise SystemExit(f"no npc_base mesh in {src}")
group = ("Head" if name in ("head", "face", "head_proto")
else f"Hand{'L' if name == 'hand_l' else 'R'}")
else f"Hand{'L' if name in ('hand_l', 'hand_l_proto') else 'R'}")
return _extract_group(mesh, group, out)
+2 -1
View File
@@ -11,7 +11,8 @@ class CliTests(unittest.TestCase):
def test_part_names_are_known(self):
self.assertEqual(
set(npc_parts.PART_NAMES),
{"head", "face", "hand_l", "hand_r", "head_proto"},
{"head", "face", "hand_l", "hand_r", "head_proto",
"hand_l_proto", "hand_r_proto"},
)
def test_blender_commands_fail_cleanly_without_bpy(self):