diff --git a/packages/studio/src/components/SelectedOutlineOverlay.tsx b/packages/studio/src/components/SelectedOutlineOverlay.tsx index 8ab2a0e8e0c..a4b2497f54b 100644 --- a/packages/studio/src/components/SelectedOutlineOverlay.tsx +++ b/packages/studio/src/components/SelectedOutlineOverlay.tsx @@ -171,6 +171,7 @@ type SelectedOutlineRotationDragTarget = { readonly keyframeDisplayOffset: number; readonly nodePath: SequencePropsSubscriptionKey; readonly schema: SequenceSchema; + readonly transformOriginValue: string; }; export type SelectedOutlineDragState = { @@ -328,6 +329,36 @@ export const getSelectedOutlineRotationCornerInfo = ( }; }; +export const getSelectedOutlineRotationPivot = ({ + dimensions, + points, + transformOriginValue, +}: { + readonly dimensions: SelectedOutline['dimensions']; + readonly points: SelectedOutline['points']; + readonly transformOriginValue: string; +}): OutlinePoint => { + if (dimensions === null) { + return getOutlineCenter(points); + } + + const parsed = parseTransformOrigin(transformOriginValue); + if (parsed === null) { + return getOutlineCenter(points); + } + + const uv = parsedTransformOriginToUv({ + parsed, + width: dimensions.width, + height: dimensions.height, + }); + if (uv === null) { + return getOutlineCenter(points); + } + + return getUvHandlePosition(points, uv); +}; + const rectToPoints = ( elementRect: DOMRect, containerRect: DOMRect, @@ -2204,7 +2235,14 @@ const SelectedOutlineRotationCornerHandle: React.FC<{ forceSpecificCursor(cornerInfo.cursor); const svgRect = svg.getBoundingClientRect(); - const center = svgPointToClientPoint(cornerInfo.center, svgRect); + const center = svgPointToClientPoint( + getSelectedOutlineRotationPivot({ + dimensions: outline.dimensions, + points: outline.points, + transformOriginValue: rotationDrag.transformOriginValue, + }), + svgRect, + ); const dragStates = getSelectedOutlineRotationDragStates({ dragTargets: selected ? allRotationDragTargets : [rotationDrag], getDragOverrides, @@ -2347,6 +2385,8 @@ const SelectedOutlineRotationCornerHandle: React.FC<{ cornerInfo, getDragOverrides, onDraggingChange, + outline.dimensions, + outline.points, onSelect, rotationDrag, selected, @@ -2683,6 +2723,23 @@ export const SelectedOutlineOverlay: React.FC<{ controls?.schema[transformOriginFieldKey]; const transformOriginPropStatus = nodePropStatuses?.[transformOriginFieldKey]; + const rotationSourceFrame = timelinePosition - keyframeDisplayOffset; + const transformOriginValueForRotation = + transformOriginFieldSchema?.type === 'transform-origin' && + (transformOriginPropStatus?.status === 'static' || + transformOriginPropStatus?.status === 'keyframed') + ? String( + Internals.getEffectiveVisualModeValue({ + propStatus: transformOriginPropStatus, + dragOverrideValue: (getDragOverrides(nodePath) ?? {})[ + transformOriginFieldKey + ], + defaultValue: transformOriginFieldSchema.default, + frame: rotationSourceFrame, + shouldResortToDefaultValueIfUndefined: true, + }) ?? transformOriginFieldSchema.default, + ) + : '50% 50%'; const canDragStatus = propStatus?.status === 'static' || (propStatus?.status === 'keyframed' && @@ -2800,6 +2857,7 @@ export const SelectedOutlineOverlay: React.FC<{ keyframeDisplayOffset, nodePath, schema: controls.schema, + transformOriginValue: transformOriginValueForRotation, } : null, transformOriginDrag: canTransformOriginDrag diff --git a/packages/studio/src/test/timeline-selection.test.ts b/packages/studio/src/test/timeline-selection.test.ts index 5dd35bc1d85..814581994dd 100644 --- a/packages/studio/src/test/timeline-selection.test.ts +++ b/packages/studio/src/test/timeline-selection.test.ts @@ -27,6 +27,7 @@ import { getSelectedOutlineRotationDeltaDegrees, getSelectedOutlineRotationDragChanges, getSelectedOutlineRotationDragValues, + getSelectedOutlineRotationPivot, getSelectedOutlineScaleDragChanges, getSelectedOutlineScaleDragValues, getSelectedOutlineScaleEdgeInfo, @@ -2155,6 +2156,7 @@ test('Selected outline corner dragging rotates selected sequences', () => { keyframeDisplayOffset: 30, nodePath: firstNodePath, schema, + transformOriginValue: '50% 50%', }, }, { @@ -2170,6 +2172,7 @@ test('Selected outline corner dragging rotates selected sequences', () => { keyframeDisplayOffset: 30, nodePath: secondNodePath, schema, + transformOriginValue: '50% 50%', }, }, ] satisfies SelectedOutlineRotationDragState[]; @@ -2237,6 +2240,7 @@ test('Selected outline corner dragging keyframed rotation adds a keyframe at the keyframeDisplayOffset: 30, nodePath, schema, + transformOriginValue: '50% 50%', }, }, ] satisfies SelectedOutlineRotationDragState[]; @@ -2303,6 +2307,52 @@ test('Selected outline rotation corners use the outline corners and center', () expect(topRight.cursor).toContain('") 12 12, alias'); }); +test('Selected outline rotation pivot follows transform origin', () => { + const points = [ + {x: 0, y: 0}, + {x: 100, y: 0}, + {x: 100, y: 50}, + {x: 0, y: 50}, + ] as const; + const dimensions = {width: 100, height: 50}; + + expect( + getSelectedOutlineRotationPivot({ + dimensions, + points, + transformOriginValue: 'center', + }), + ).toEqual({x: 50, y: 25}); + expect( + getSelectedOutlineRotationPivot({ + dimensions, + points, + transformOriginValue: 'left bottom', + }), + ).toEqual({x: 0, y: 50}); + expect( + getSelectedOutlineRotationPivot({ + dimensions, + points, + transformOriginValue: '25px top', + }), + ).toEqual({x: 25, y: 0}); + expect( + getSelectedOutlineRotationPivot({ + dimensions, + points, + transformOriginValue: 'calc(50% + 1px) center', + }), + ).toEqual({x: 50, y: 25}); + expect( + getSelectedOutlineRotationPivot({ + dimensions: null, + points, + transformOriginValue: 'left top', + }), + ).toEqual({x: 50, y: 25}); +}); + test('Selected outline rotation cursors use the outline rotation', () => { const points = [ {x: 0, y: 0},