11# SPDX-License-Identifier: GPL-3.0-or-later
22# SPDX-FileCopyrightText: 2026 sp00ktober
33"""A monster PAC as the studio opens it: `mhfu_port.model.Model` with a port manifest's clip
4- names, the file's build id and `pose()`."""
4+ names, the file's build id and `pose()`, its tail tip on the tail unless `severed` ."""
55
66from __future__ import annotations
77
88from collections .abc import Sequence
9+ from functools import cached_property
910from pathlib import Path
1011from typing import Any , Self
1112
12- from mhfu_port import build , layout
13+ from mhfu_port import build , fk , layout , mesh , rig
1314from mhfu_port .data import Data
1415from mhfu_port .manifest import Manifest
1516from mhfu_port .model import MHP3RD , Clip , Model
@@ -32,6 +33,8 @@ def __init__(self, *args: Any, **kw: Any) -> None:
3233 None if self .path is None or self .pac is None else build_id (self .path .name , self .pac )
3334 )
3435 """`name@digest` of the file, what clip labels are keyed to."""
36+ self .severed = False
37+ """Draw the stump: the tail tip hidden, not posed on the tail."""
3538
3639 @classmethod
3740 def from_bytes (
@@ -97,17 +100,51 @@ def attach_manifest(self, m: Manifest) -> None:
97100 by_slot .setdefault (at , []).append (name )
98101 self .rename (by_slot )
99102
103+ # the tail tip
104+
105+ @cached_property
106+ def tip (self ) -> rig .Tip | None :
107+ """`rig.tip_of` the skeleton; None, with a note, where no offset pairs the chain."""
108+ try :
109+ return rig .tip_of (self .skeleton )
110+ except ValueError as e :
111+ self .notes .append (f"the tail tip stays where it is stored: { e } " )
112+ return None
113+
114+ def tip_groups (self ) -> list [int ]:
115+ """The groups riding only the tip's chain (`mesh.tip_parts`)."""
116+ if self .tip is None :
117+ return []
118+ rows = [
119+ [
120+ list (zip (js , ws , strict = True ))
121+ for js , ws in zip (g .skin .joints .tolist (), g .skin .weights .tolist (), strict = True )
122+ ]
123+ for g in self .groups
124+ ]
125+ return mesh .tip_parts (rows , self .tip .joints )
126+
127+ def hidden (self ) -> set [int ]:
128+ """The groups not drawn: the tip's, when `severed`."""
129+ return set (self .tip_groups ()) if self .severed else set ()
130+
131+ def world (self , rot : fk .Floats | None = None , loc : fk .Floats | None = None ) -> fk .Floats :
132+ """`fk.Rig.world`, the tip's chain posed as its carriers unless `severed`, as the game
133+ does while the tail is whole."""
134+ world = self .rig .world (rot , loc )
135+ return world if self .severed or self .tip is None else fk .attach (world , self .tip .pairs )
136+
100137 def clip_table (self ) -> dict [int , tuple [int , bool ]]:
101138 return {c .slot : (c .frames , c .loop ) for c in self .clips }
102139
103140 def pose (self , key : int | str | Clip , frame : float = 0.0 ) -> Pose :
104141 """Frames outside the clip hold per channel."""
105142 c = self .clip (key )
106143 rot , loc = self .curves (c ).at (float (frame ))
107- return Pose (self .rig , float (frame ), self .rig . world (rot , loc ), c .slot )
144+ return Pose (self .rig , float (frame ), self .world (rot , loc ), c .slot )
108145
109146 def bind_pose (self ) -> Pose :
110- return Pose (self .rig , 0.0 , self .rig . bind_world )
147+ return Pose (self .rig , 0.0 , self .world () )
111148
112149 # reporting
113150
0 commit comments