88
99from typing import TYPE_CHECKING
1010
11+ from mhfu_port import continuity
1112from PySide6 .QtCore import QSignalBlocker , Qt
1213from PySide6 .QtWidgets import QVBoxLayout , QWidget
1314
5051 clips .UNCHECKABLE : "unchecked" ,
5152}
5253UNPLACED = "in no anim: the base monster has no anim left for it"
54+ ORDERS = (("id" , "By id" ), ("fit" , "Fits after the one on screen" ))
55+ FIT_COLUMN = 5
5356
5457
5558def _page () -> tuple [QWidget , QVBoxLayout ]:
@@ -110,12 +113,27 @@ def __init__(self, ws: MonsterWorkspace, studio: Studio) -> None:
110113 )
111114 self .filter .textChanged .connect (self ._filter )
112115 lay .addWidget (self .filter )
116+ self .order = kit .Segmented (
117+ list (ORDERS ),
118+ tip = "By id: the original's order. Fits after the one on screen: best first by how"
119+ " well each clip's first pose continues where the clip on screen ends, to find the"
120+ " next part of a chain." ,
121+ on = lambda v : self .studio .act (
122+ "order clips" , lambda : setattr (self .ws , "clip_order" , v )
123+ )(),
124+ current = ws .clip_order ,
125+ )
126+ self .order_note = kit .label (role = "muted" )
127+ lay .addWidget (kit .row (self .order , stretch = True ))
128+ lay .addWidget (self .order_note )
113129 self .table = kit .Table (
114- ["Id" , "Anim" , "Frames" , "Travel" , "Name" ],
130+ ["Id" , "Anim" , "Frames" , "Travel" , "Name" , "Fit" , "Used in" ],
115131 tip = "Every clip of the original by its MHP3rd id (stream x 100 + slot), and the anim"
116132 " that plays it: what a script passes to force it. Click one, or step with the arrow"
117133 " keys, to play it from the start; a clip that repeats says loop after its frames;"
118- " Travel is how far the clip carries the body." ,
134+ " Travel is how far the clip carries the body; Fit, when ordered by it, is the mean"
135+ " angle between the clip on screen's last pose and this clip's first; Used in names"
136+ " the moves that play it." ,
119137 swatch_column = 1 ,
120138 )
121139 self .table .picked .connect (self ._clicked )
@@ -374,8 +392,9 @@ def _table(self, vocab: Vocabulary) -> None:
374392 kit .put (self .filter , ws .clip_filter )
375393 needle = ws .clip_filter .strip ().lower ()
376394 speed = DEFAULT_SPEED if vp is None or vp .actor is None else vp .playback .speed
395+ fit = self ._fits ()
377396 rows , data , colors , tips , levels = [], [], [], [], []
378- for r in self ._rows :
397+ for r in rated_first ( self ._rows , fit ) :
379398 cov = None if r .entry is None else vocab .coverage .slots .get (r .entry )
380399 text = " " .join (
381400 [
@@ -384,6 +403,7 @@ def _table(self, vocab: Vocabulary) -> None:
384403 r .name or "unnamed" ,
385404 kind_text (cov ),
386405 r .label ,
406+ * r .used ,
387407 ]
388408 )
389409 if needle and needle not in text .lower ():
@@ -396,11 +416,13 @@ def _table(self, vocab: Vocabulary) -> None:
396416 f"{ r .frames } loop" if r .loop else str (r .frames ),
397417 f"{ net :.0f} " if net >= 1.0 else "" ,
398418 r .name ,
419+ fit_text (fit .get (r .id )) if r .id is not None else "" ,
420+ ", " .join (r .used ),
399421 )
400422 )
401423 data .append (r .key )
402424 colors .append (None if cov is None else COVERAGE [cov .kind ])
403- tips .append (row_tip (r , cov , speed ))
425+ tips .append (row_tip (r , cov , speed , fit . get ( r . id ) if r . id is not None else None ))
404426 levels .append (level_of (cov ))
405427 playing = self ._playing ()
406428 with QSignalBlocker (self .table ):
@@ -410,6 +432,19 @@ def _table(self, vocab: Vocabulary) -> None:
410432 else :
411433 self .table .select_data (playing )
412434
435+ def _fits (self ) -> dict [int , float ]:
436+ """How well each clip follows the one on screen, when the table is ordered by it."""
437+ ws = self .ws
438+ kit .put (self .order , ws .clip_order )
439+ by_fit = ws .clip_order == "fit"
440+ self .table .setColumnHidden (FIT_COLUMN , not by_fit )
441+ fit = ws .fits_after () if by_fit else {}
442+ self .order_note .setVisible (by_fit and not fit )
443+ self .order_note .setText (
444+ ws .ends_note or "Play a clip of the original to rank the rest by it"
445+ )
446+ return fit
447+
413448 def _editor (self , vocab : Vocabulary ) -> None :
414449 ws = self .ws
415450 row = next ((r for r in self ._rows if r .key == self ._picked ), None )
@@ -476,12 +511,24 @@ def level_of(cov: SlotCoverage | None) -> Level | None:
476511 return "warning" if cov is not None and cov .kind == clips .FILLER else None
477512
478513
479- def row_tip (r : SourceClip , cov : SlotCoverage | None , speed : float ) -> str :
514+ def rated_first (rows : list [SourceClip ], fit : dict [int , float ]) -> list [SourceClip ]:
515+ """`rows` with the clips `fit` rates first, best first, the rest as they were."""
516+ place = {cid : k for k , cid in enumerate (fit )}
517+ return sorted (rows , key = lambda r : len (place ) if r .id is None else place .get (r .id , len (place )))
518+
519+
520+ def fit_text (degrees : float | None ) -> str :
521+ return "" if degrees is None else f"{ degrees :.0f} \u00b0 { continuity .word (degrees )} "
522+
523+
524+ def row_tip (r : SourceClip , cov : SlotCoverage | None , speed : float , fit : float | None = None ) -> str :
480525 lines = []
481526 if r .id is not None :
482527 lines .append (f"MHP3rd clip { r .id } : stream { r .stream } , slot { r .id % 100 } " )
483528 lines .append (cov .why () if cov else UNPLACED if r .entry is None else "Kind unknown" )
484529 lines .append (f"{ wall_clock (r .frames , speed ):.2f} s at speed { speed :.2f} " )
530+ if fit is not None :
531+ lines .append (f"{ fit_text (fit )} from the end of the clip on screen" )
485532 if r .label :
486533 lines .append (r .label )
487534 return "\n " .join (lines )
0 commit comments