Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 89 additions & 1 deletion packages/studio/src/components/SelectedOutlineOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,42 @@ const rectToPoints = (
];
};

type SvgViewport = {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
};

type SvgScreenCtm = Pick<DOMMatrixReadOnly, 'a' | 'b' | 'c' | 'd' | 'e' | 'f'>;

export const getTransformedSvgViewportPoints = ({
viewport,
ctm,
containerRect,
}: {
readonly viewport: SvgViewport;
readonly ctm: SvgScreenCtm;
readonly containerRect: Pick<DOMRect, 'left' | 'top'>;
}): 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,
Expand All @@ -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,
Expand All @@ -374,6 +455,10 @@ const getElementOutlinePoints = (
return null;
}

if (isSvgSvgElement(element)) {
return getSvgSvgElementOutlinePoints(element, containerRect);
}

const quads = getBoxQuadsPonyfill(element, {
box: 'border',
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1436,6 +1521,7 @@ const SelectedOutlineTransformOriginHandle: React.FC<{
cy={position.y}
r={4}
stroke={BLUE}
fill="none"
strokeWidth={2}
vectorEffect="non-scaling-stroke"
/>
Expand Down Expand Up @@ -1850,6 +1936,7 @@ const SelectedOutlineScaleEdgeLine: React.FC<{
}

onDraggingChange(true);
forceSpecificCursor(edgeInfo.cursor);

const startPointer = {x: event.clientX, y: event.clientY};
const dragStates = getSelectedOutlineScaleDragStates({
Expand Down Expand Up @@ -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({
Expand Down
16 changes: 16 additions & 0 deletions packages/studio/src/test/timeline-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getSelectedOutlineScaleEdgeInfo,
getSelectedSequenceKeys,
getSequencesWithSelectableOutlines,
getTransformedSvgViewportPoints,
type SelectedOutlineDragState,
type SelectedOutlineRotationDragState,
type SelectedOutlineScaleDragState,
Expand Down Expand Up @@ -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},
Expand Down
Loading