@@ -12,6 +12,7 @@ import {
1212 KeyframeDiamondContextMenu ,
1313 type KeyframeDiamondContextMenuState ,
1414} from "../../player/components/KeyframeDiamondContextMenu" ;
15+ import type { TimelineKeyframeTarget } from "../../player/components/timelineKeyframeIdentity" ;
1516import {
1617 commitAddKeyframe ,
1718 commitAddWaypoint ,
@@ -96,9 +97,13 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
9697 const [ draft , setDraft ] = useState < Draft | null > ( null ) ;
9798 const [ ghost , setGhost ] = useState < { x : number ; y : number ; segIndex : number } | null > ( null ) ;
9899 const [ hoverNode , setHoverNode ] = useState < number | null > ( null ) ;
99- // Right-click context menu on a keyframe node — same delete actions as the
100- // timeline keyframe diamond.
101- const [ kfMenu , setKfMenu ] = useState < KeyframeDiamondContextMenuState | null > ( null ) ;
100+ // Right-click context menu on a path node — same delete actions as the
101+ // timeline keyframe diamond. The node it was opened on rides along, because
102+ // which entries apply depends on whether it is a keyframe or a waypoint.
103+ const [ kfMenu , setKfMenu ] = useState < {
104+ state : KeyframeDiamondContextMenuState ;
105+ ref : MotionNodeRef ;
106+ } | null > ( null ) ;
102107 // The keyframe % selected by clicking its node — highlighted, and the next drag
103108 // modifies it rather than adding a keyframe.
104109 const activeKeyframePct = usePlayerStore ( ( s ) => s . activeKeyframePct ) ;
@@ -436,21 +441,47 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
436441 } ;
437442
438443 const elementId = selection ?. id ?? null ;
439- // Right-click a keyframe node → the timeline's keyframe context menu (delete
440- // this keyframe / delete all), so motion-path keyframes are removable in place.
444+ // Right-click any path node → the timeline's keyframe context menu (delete this
445+ // one / delete all), so path nodes are removable in place. Waypoints open it
446+ // too: returning early for them let the browser's own context menu open over
447+ // the editor overlay, which is never what a right-click on a node should do.
441448 const onNodeContextMenu = ( e : React . MouseEvent , ref : MotionNodeRef ) => {
442- if ( ref . type !== "keyframe" || ! animId || ! elementId || ! timelineElement ) return ;
449+ if ( ! animId || ! elementId || ! timelineElement ) return ;
443450 e . preventDefault ( ) ;
444451 e . stopPropagation ( ) ;
445452 setKfMenu ( {
446- x : e . clientX ,
447- y : e . clientY ,
448- element : timelineElement ,
449- elementId,
450- percentage : ref . pct ,
451- tweenPercentage : ref . pct ,
453+ ref,
454+ state : {
455+ x : e . clientX ,
456+ y : e . clientY ,
457+ element : timelineElement ,
458+ elementId,
459+ // A waypoint carries no percentage of its own: one tween-level ease owns
460+ // every segment, and its index is the identity the delete acts on. The
461+ // menu never reads this for a waypoint, because the only entry that
462+ // would (Move to Playhead) is hidden below.
463+ percentage : ref . type === "keyframe" ? ref . pct : 0 ,
464+ tweenPercentage : ref . type === "keyframe" ? ref . pct : 0 ,
465+ } ,
452466 } ) ;
453467 } ;
468+ const menuRef = kfMenu ?. ref ;
469+ // Deleting one node: by tween-% for a keyframe, by path index for a waypoint.
470+ // A waypoint delete is offered on the same condition as the hover x badge,
471+ // because removeMotionPathPointInScript refuses to drop an arc below two
472+ // anchors and the entry would silently do nothing.
473+ const onMenuDelete =
474+ menuRef === undefined || ! animId
475+ ? undefined
476+ : menuRef . type === "keyframe"
477+ ? ( _elId : string , keyframe : TimelineKeyframeTarget ) => {
478+ handleGsapRemoveKeyframe ( animId , keyframe . percentage ) ;
479+ }
480+ : removable
481+ ? ( ) => {
482+ void commitRemoveWaypoint ( animId , menuRef . index , commitMutation ) ;
483+ }
484+ : undefined ;
454485
455486 return (
456487 < >
@@ -532,14 +563,17 @@ export const MotionPathOverlay = memo(function MotionPathOverlay({
532563 </ svg >
533564 { kfMenu && (
534565 < KeyframeDiamondContextMenu
535- state = { kfMenu }
566+ state = { kfMenu . state }
536567 onClose = { ( ) => setKfMenu ( null ) }
537- onDelete = { ( _elId , keyframe ) =>
538- animId && handleGsapRemoveKeyframe ( animId , keyframe . percentage )
539- }
568+ onDelete = { onMenuDelete }
540569 onDeleteAll = { ( ) => animId && handleGsapRemoveAllKeyframes ( animId ) }
541- onMoveToPlayhead = { ( _element , keyframe ) =>
542- animId && handleGsapMoveKeyframeToPlayhead ( animId , keyframe . percentage )
570+ // Retiming needs a percentage of this node's own, which a waypoint
571+ // does not have.
572+ onMoveToPlayhead = {
573+ kfMenu . ref . type === "keyframe"
574+ ? ( _element , keyframe ) =>
575+ animId && handleGsapMoveKeyframeToPlayhead ( animId , keyframe . percentage )
576+ : undefined
543577 }
544578 />
545579 ) }
0 commit comments