diff --git a/packages/studio/src/components/SelectedOutlineOverlay.tsx b/packages/studio/src/components/SelectedOutlineOverlay.tsx index 79944443a2f..8ab2a0e8e0c 100644 --- a/packages/studio/src/components/SelectedOutlineOverlay.tsx +++ b/packages/studio/src/components/SelectedOutlineOverlay.tsx @@ -345,6 +345,42 @@ const rectToPoints = ( ]; }; +type SvgViewport = { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +}; + +type SvgScreenCtm = Pick; + +export const getTransformedSvgViewportPoints = ({ + viewport, + ctm, + containerRect, +}: { + readonly viewport: SvgViewport; + readonly ctm: SvgScreenCtm; + readonly containerRect: Pick; +}): SelectedOutline['points'] => { + const transformPoint = (x: number, y: number): OutlinePoint => ({ + x: ctm.a * x + ctm.c * y + ctm.e - containerRect.left, + y: ctm.b * x + ctm.d * y + ctm.f - containerRect.top, + }); + + const left = viewport.x; + const top = viewport.y; + const right = viewport.x + viewport.width; + const bottom = viewport.y + viewport.height; + + return [ + transformPoint(left, top), + transformPoint(right, top), + transformPoint(right, bottom), + transformPoint(left, bottom), + ]; +}; + const quadToPoints = ( quad: DOMQuad, containerRect: DOMRect, @@ -364,6 +400,51 @@ const quadToPoints = ( ]; }; +const isSvgSvgElement = (element: Element): element is SVGSVGElement => { + const ownerSvgSvgElement = element.ownerDocument.defaultView?.SVGSVGElement; + return ( + (typeof SVGSVGElement !== 'undefined' && + element instanceof SVGSVGElement) || + (ownerSvgSvgElement !== undefined && element instanceof ownerSvgSvgElement) + ); +}; + +const getSvgSvgElementViewport = (element: SVGSVGElement): SvgViewport => { + const viewBox = element.viewBox.baseVal; + if (viewBox.width > 0 && viewBox.height > 0) { + return { + x: viewBox.x, + y: viewBox.y, + width: viewBox.width, + height: viewBox.height, + }; + } + + return { + x: 0, + y: 0, + width: element.width.baseVal.value, + height: element.height.baseVal.value, + }; +}; + +const getSvgSvgElementOutlinePoints = ( + element: SVGSVGElement, + containerRect: DOMRect, +): SelectedOutline['points'] | null => { + const ctm = element.getScreenCTM(); + const viewport = getSvgSvgElementViewport(element); + if (ctm === null || (viewport.width === 0 && viewport.height === 0)) { + return null; + } + + return getTransformedSvgViewportPoints({ + viewport, + ctm, + containerRect, + }); +}; + const getElementOutlinePoints = ( element: Element, containerRect: DOMRect, @@ -374,6 +455,10 @@ const getElementOutlinePoints = ( return null; } + if (isSvgSvgElement(element)) { + return getSvgSvgElementOutlinePoints(element, containerRect); + } + const quads = getBoxQuadsPonyfill(element, { box: 'border', }); @@ -770,7 +855,7 @@ export type SelectedOutlineScaleEdge = 'top' | 'right' | 'bottom' | 'left'; type SelectedOutlineScaleEdgeInfo = { readonly axis: 'x' | 'y'; - readonly cursor: React.CSSProperties['cursor']; + readonly cursor: string; readonly end: OutlinePoint; readonly extent: number; readonly normal: OutlinePoint; @@ -1436,6 +1521,7 @@ const SelectedOutlineTransformOriginHandle: React.FC<{ cy={position.y} r={4} stroke={BLUE} + fill="none" strokeWidth={2} vectorEffect="non-scaling-stroke" /> @@ -1850,6 +1936,7 @@ const SelectedOutlineScaleEdgeLine: React.FC<{ } onDraggingChange(true); + forceSpecificCursor(edgeInfo.cursor); const startPointer = {x: event.clientX, y: event.clientY}; const dragStates = getSelectedOutlineScaleDragStates({ @@ -1908,6 +1995,7 @@ const SelectedOutlineScaleEdgeLine: React.FC<{ window.removeEventListener('pointermove', onPointerMove); window.removeEventListener('pointerup', onPointerUp); window.removeEventListener('pointercancel', onPointerUp); + stopForcingSpecificCursor(); onDraggingChange(false); const changes = getSelectedOutlineScaleDragChanges({ diff --git a/packages/studio/src/test/timeline-selection.test.ts b/packages/studio/src/test/timeline-selection.test.ts index a610a3dd823..5dd35bc1d85 100644 --- a/packages/studio/src/test/timeline-selection.test.ts +++ b/packages/studio/src/test/timeline-selection.test.ts @@ -32,6 +32,7 @@ import { getSelectedOutlineScaleEdgeInfo, getSelectedSequenceKeys, getSequencesWithSelectableOutlines, + getTransformedSvgViewportPoints, type SelectedOutlineDragState, type SelectedOutlineRotationDragState, type SelectedOutlineScaleDragState, @@ -1392,6 +1393,21 @@ test('UV coordinate constraints still clamp to schema min and max', () => { ).toEqual([0, 1]); }); +test('SVG viewport outline points are projected through the screen CTM', () => { + const points = getTransformedSvgViewportPoints({ + viewport: {x: 0, y: 0, width: 100, height: 50}, + ctm: {a: 0, b: 1, c: -1, d: 0, e: 75, f: -25}, + containerRect: {left: 10, top: 20}, + }); + + expect(points).toEqual([ + {x: 65, y: -45}, + {x: 65, y: 55}, + {x: 15, y: 55}, + {x: 15, y: -45}, + ]); +}); + test('UV handle connection lines connect fields from schema metadata', () => { const points = [ {x: 0, y: 0},