mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat: support snap when resizing element (#12563)
Fixes [BS-2753](https://linear.app/affine-design/issue/BS-2753/) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added snapping support when resizing elements, improving alignment and precision during resize operations. - Introduced new resize event handlers allowing extensions to customize resize behavior with start, move, and end callbacks. - **Bug Fixes** - Improved handling of snapping state to prevent errors during drag and resize actions. - **Tests** - Updated resizing tests to ensure consistent snapping behavior by removing default elements that could interfere with test results. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||||
import type { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import {
|
import {
|
||||||
type DragExtensionInitializeContext,
|
type DragExtensionInitializeContext,
|
||||||
type ExtensionDragMoveContext,
|
type ExtensionDragMoveContext,
|
||||||
@@ -28,7 +28,7 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
let alignBound: Bound;
|
let alignBound: Bound | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
@@ -46,6 +46,7 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
onDragMove(context: ExtensionDragMoveContext) {
|
onDragMove(context: ExtensionDragMoveContext) {
|
||||||
if (
|
if (
|
||||||
context.elements.length === 0 ||
|
context.elements.length === 0 ||
|
||||||
|
!alignBound ||
|
||||||
alignBound.w === 0 ||
|
alignBound.w === 0 ||
|
||||||
alignBound.h === 0
|
alignBound.h === 0
|
||||||
) {
|
) {
|
||||||
@@ -58,11 +59,65 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
context.dx = alignRst.dx + context.dx;
|
context.dx = alignRst.dx + context.dx;
|
||||||
context.dy = alignRst.dy + context.dy;
|
context.dy = alignRst.dy + context.dy;
|
||||||
},
|
},
|
||||||
onDragEnd() {
|
clear() {
|
||||||
|
alignBound = null;
|
||||||
snapOverlay.clear();
|
snapOverlay.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.action.onElementResize(() => {
|
||||||
|
const snapOverlay = this.snapOverlay;
|
||||||
|
|
||||||
|
if (!snapOverlay) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let alignBound: Bound | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart(context) {
|
||||||
|
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||||
|
},
|
||||||
|
onResizeMove(context) {
|
||||||
|
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handle, handleSign, lockRatio } = context;
|
||||||
|
let { dx, dy } = context;
|
||||||
|
|
||||||
|
if (lockRatio) {
|
||||||
|
const min = Math.min(
|
||||||
|
Math.abs(dx / alignBound.w),
|
||||||
|
Math.abs(dy / alignBound.h)
|
||||||
|
);
|
||||||
|
|
||||||
|
dx = min * Math.sign(dx) * alignBound.w;
|
||||||
|
dy = min * Math.sign(dy) * alignBound.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBound = new Bound(
|
||||||
|
alignBound.x +
|
||||||
|
(handle.includes('left') ? -dx * handleSign.xSign : 0),
|
||||||
|
alignBound.y +
|
||||||
|
(handle.includes('top') ? -dy * handleSign.ySign : 0),
|
||||||
|
Math.abs(alignBound.w + dx * handleSign.xSign),
|
||||||
|
Math.abs(alignBound.h + dy * handleSign.ySign)
|
||||||
|
);
|
||||||
|
const alignRst = snapOverlay.align(currentBound);
|
||||||
|
|
||||||
|
context.suggest({
|
||||||
|
dx: alignRst.dx + context.dx,
|
||||||
|
dy: alignRst.dy + context.dy,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onResizeEnd() {
|
||||||
|
alignBound = null;
|
||||||
|
snapOverlay.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import type {
|
|||||||
ExtensionDragMoveContext,
|
ExtensionDragMoveContext,
|
||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
} from '../types/drag.js';
|
} from '../types/drag.js';
|
||||||
|
import type {
|
||||||
|
ExtensionElementResizeContext,
|
||||||
|
ExtensionElementResizeEndContext,
|
||||||
|
ExtensionElementResizeMoveContext,
|
||||||
|
ExtensionElementResizeStartContext,
|
||||||
|
} from '../types/resize.js';
|
||||||
import type { ExtensionElementSelectContext } from '../types/select.js';
|
import type { ExtensionElementSelectContext } from '../types/select.js';
|
||||||
|
|
||||||
export const InteractivityExtensionIdentifier =
|
export const InteractivityExtensionIdentifier =
|
||||||
@@ -100,7 +106,7 @@ export class InteractivityEventAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActionContextMap = {
|
export type ActionContextMap = {
|
||||||
dragInitialize: {
|
dragInitialize: {
|
||||||
context: DragExtensionInitializeContext;
|
context: DragExtensionInitializeContext;
|
||||||
returnType: {
|
returnType: {
|
||||||
@@ -119,6 +125,14 @@ type ActionContextMap = {
|
|||||||
| undefined
|
| undefined
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
elementResize: {
|
||||||
|
context: ExtensionElementResizeContext;
|
||||||
|
returnType: {
|
||||||
|
onResizeStart?: (context: ExtensionElementResizeStartContext) => void;
|
||||||
|
onResizeMove?: (context: ExtensionElementResizeMoveContext) => void;
|
||||||
|
onResizeEnd?: (context: ExtensionElementResizeEndContext) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
elementSelect: {
|
elementSelect: {
|
||||||
context: ExtensionElementSelectContext;
|
context: ExtensionElementSelectContext;
|
||||||
returnType: void;
|
returnType: void;
|
||||||
@@ -144,6 +158,18 @@ export class InteractivityActionAPI {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onElementResize(
|
||||||
|
handler: (
|
||||||
|
ctx: ActionContextMap['elementResize']['context']
|
||||||
|
) => ActionContextMap['elementResize']['returnType']
|
||||||
|
) {
|
||||||
|
this._handlers['elementResize'] = handler;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return delete this._handlers['elementResize'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onRequestElementsClone(
|
onRequestElementsClone(
|
||||||
handler: (
|
handler: (
|
||||||
ctx: ActionContextMap['elementsClone']['context']
|
ctx: ActionContextMap['elementsClone']['context']
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
|||||||
import type { GfxElementModelView } from '../view/view.js';
|
import type { GfxElementModelView } from '../view/view.js';
|
||||||
import { createInteractionContext, type SupportedEvents } from './event.js';
|
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||||
import {
|
import {
|
||||||
|
type ActionContextMap,
|
||||||
type InteractivityActionAPI,
|
type InteractivityActionAPI,
|
||||||
type InteractivityEventAPI,
|
type InteractivityEventAPI,
|
||||||
InteractivityExtensionIdentifier,
|
InteractivityExtensionIdentifier,
|
||||||
@@ -882,7 +883,11 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
handleElementResize(
|
handleElementResize(
|
||||||
options: Omit<
|
options: Omit<
|
||||||
OptionResize,
|
OptionResize,
|
||||||
'lockRatio' | 'onResizeStart' | 'onResizeEnd' | 'onResizeUpdate'
|
| 'lockRatio'
|
||||||
|
| 'onResizeStart'
|
||||||
|
| 'onResizeEnd'
|
||||||
|
| 'onResizeUpdate'
|
||||||
|
| 'onResizeMove'
|
||||||
> & {
|
> & {
|
||||||
onResizeStart?: () => void;
|
onResizeStart?: () => void;
|
||||||
onResizeEnd?: () => void;
|
onResizeEnd?: () => void;
|
||||||
@@ -910,6 +915,23 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
const elements = Array.from(viewConfigMap.values()).map(
|
const elements = Array.from(viewConfigMap.values()).map(
|
||||||
config => config.view.model
|
config => config.view.model
|
||||||
) as GfxModel[];
|
) as GfxModel[];
|
||||||
|
const extensionHandlers = this.interactExtensions.values().reduce(
|
||||||
|
(handlers, ext) => {
|
||||||
|
const extHandlers = (ext.action as InteractivityActionAPI).emit(
|
||||||
|
'elementResize',
|
||||||
|
{
|
||||||
|
elements,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extHandlers) {
|
||||||
|
handlers.push(extHandlers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers;
|
||||||
|
},
|
||||||
|
[] as ActionContextMap['elementResize']['returnType'][]
|
||||||
|
);
|
||||||
let lockRatio = false;
|
let lockRatio = false;
|
||||||
|
|
||||||
viewConfigMap.forEach(config => {
|
viewConfigMap.forEach(config => {
|
||||||
@@ -925,11 +947,46 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
...options,
|
...options,
|
||||||
lockRatio,
|
lockRatio,
|
||||||
elements,
|
elements,
|
||||||
|
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
|
||||||
|
const suggested: {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
priority?: number;
|
||||||
|
}[] = [];
|
||||||
|
const suggest = (distance: { dx: number; dy: number }) => {
|
||||||
|
suggested.push(distance);
|
||||||
|
};
|
||||||
|
|
||||||
|
extensionHandlers.forEach(ext => {
|
||||||
|
ext.onResizeMove?.({
|
||||||
|
dx,
|
||||||
|
dy,
|
||||||
|
elements,
|
||||||
|
handleSign,
|
||||||
|
handle,
|
||||||
|
lockRatio,
|
||||||
|
suggest,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suggested.sort((a, b) => {
|
||||||
|
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return last(suggested) ?? { dx, dy };
|
||||||
|
},
|
||||||
onResizeStart: ({ data }) => {
|
onResizeStart: ({ data }) => {
|
||||||
this.activeInteraction$.value = {
|
this.activeInteraction$.value = {
|
||||||
type: 'resize',
|
type: 'resize',
|
||||||
elements,
|
elements,
|
||||||
};
|
};
|
||||||
|
extensionHandlers.forEach(ext => {
|
||||||
|
ext.onResizeStart?.({
|
||||||
|
elements,
|
||||||
|
handle,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
options.onResizeStart?.();
|
options.onResizeStart?.();
|
||||||
data.forEach(({ model }) => {
|
data.forEach(({ model }) => {
|
||||||
if (!viewConfigMap.has(model.id)) {
|
if (!viewConfigMap.has(model.id)) {
|
||||||
@@ -990,6 +1047,13 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
},
|
},
|
||||||
onResizeEnd: ({ data }) => {
|
onResizeEnd: ({ data }) => {
|
||||||
this.activeInteraction$.value = null;
|
this.activeInteraction$.value = null;
|
||||||
|
|
||||||
|
extensionHandlers.forEach(ext => {
|
||||||
|
ext.onResizeEnd?.({
|
||||||
|
elements,
|
||||||
|
handle,
|
||||||
|
});
|
||||||
|
});
|
||||||
options.onResizeEnd?.();
|
options.onResizeEnd?.();
|
||||||
this.std.store.transact(() => {
|
this.std.store.transact(() => {
|
||||||
data.forEach(({ model }) => {
|
data.forEach(({ model }) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Bound,
|
Bound,
|
||||||
getCommonBoundWithRotation,
|
getCommonBoundWithRotation,
|
||||||
|
type IBound,
|
||||||
type IVec,
|
type IVec,
|
||||||
} from '@blocksuite/global/gfx';
|
} from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
@@ -28,19 +29,24 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
|
|||||||
'bottom',
|
'bottom',
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ElementInitialSnapshot {
|
type ElementInitialSnapshot = Readonly<Required<IBound>>;
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
rotate: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OptionResize {
|
export interface OptionResize {
|
||||||
elements: GfxModel[];
|
elements: GfxModel[];
|
||||||
handle: ResizeHandle;
|
handle: ResizeHandle;
|
||||||
lockRatio: boolean;
|
lockRatio: boolean;
|
||||||
event: PointerEvent;
|
event: PointerEvent;
|
||||||
|
onResizeMove: (payload: {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
|
||||||
|
handleSign: {
|
||||||
|
xSign: number;
|
||||||
|
ySign: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
lockRatio: boolean;
|
||||||
|
}) => { dx: number; dy: number };
|
||||||
onResizeUpdate: (payload: {
|
onResizeUpdate: (payload: {
|
||||||
lockRatio: boolean;
|
lockRatio: boolean;
|
||||||
scaleX: number;
|
scaleX: number;
|
||||||
@@ -95,6 +101,7 @@ export class ResizeController {
|
|||||||
handle,
|
handle,
|
||||||
lockRatio,
|
lockRatio,
|
||||||
onResizeStart,
|
onResizeStart,
|
||||||
|
onResizeMove,
|
||||||
onResizeUpdate,
|
onResizeUpdate,
|
||||||
onResizeEnd,
|
onResizeEnd,
|
||||||
event,
|
event,
|
||||||
@@ -107,35 +114,49 @@ export class ResizeController {
|
|||||||
h: el.h,
|
h: el.h,
|
||||||
rotate: el.rotate,
|
rotate: el.rotate,
|
||||||
}));
|
}));
|
||||||
|
const originalBound = getCommonBoundWithRotation(originals);
|
||||||
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
event.clientX,
|
event.clientX,
|
||||||
event.clientY,
|
event.clientY,
|
||||||
]);
|
]);
|
||||||
|
const handleSign = this.getHandleSign(handle);
|
||||||
|
|
||||||
const onPointerMove = (e: PointerEvent) => {
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
e.clientX,
|
e.clientX,
|
||||||
e.clientY,
|
e.clientY,
|
||||||
]);
|
]);
|
||||||
|
let delta = {
|
||||||
|
dx: currPt[0] - startPt[0],
|
||||||
|
dy: currPt[1] - startPt[1],
|
||||||
|
};
|
||||||
const shouldLockRatio = lockRatio || e.shiftKey;
|
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||||
|
|
||||||
|
delta = onResizeMove({
|
||||||
|
dx: delta.dx,
|
||||||
|
dy: delta.dy,
|
||||||
|
handleSign,
|
||||||
|
lockRatio: shouldLockRatio,
|
||||||
|
});
|
||||||
|
|
||||||
if (elements.length === 1) {
|
if (elements.length === 1) {
|
||||||
this.resizeSingle(
|
this.resizeSingle(
|
||||||
originals[0],
|
originals[0],
|
||||||
elements[0],
|
elements[0],
|
||||||
shouldLockRatio,
|
shouldLockRatio,
|
||||||
startPt,
|
startPt,
|
||||||
currPt,
|
delta,
|
||||||
handle,
|
handleSign,
|
||||||
onResizeUpdate
|
onResizeUpdate
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.resizeMulti(
|
this.resizeMulti(
|
||||||
|
originalBound,
|
||||||
originals,
|
originals,
|
||||||
elements,
|
elements,
|
||||||
handle,
|
|
||||||
currPt,
|
|
||||||
startPt,
|
startPt,
|
||||||
|
delta,
|
||||||
|
handleSign,
|
||||||
onResizeUpdate
|
onResizeUpdate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -159,11 +180,14 @@ export class ResizeController {
|
|||||||
model: GfxModel,
|
model: GfxModel,
|
||||||
lockRatio: boolean,
|
lockRatio: boolean,
|
||||||
startPt: IVec,
|
startPt: IVec,
|
||||||
currPt: IVec,
|
delta: {
|
||||||
handle: ResizeHandle,
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
},
|
||||||
|
handleSign: { xSign: number; ySign: number },
|
||||||
updateCallback: OptionResize['onResizeUpdate']
|
updateCallback: OptionResize['onResizeUpdate']
|
||||||
) {
|
) {
|
||||||
const { xSign, ySign } = this.getHandleSign(handle);
|
const { xSign, ySign } = handleSign;
|
||||||
|
|
||||||
const pivot = new DOMPoint(
|
const pivot = new DOMPoint(
|
||||||
orig.x + (-xSign === 1 ? orig.w : 0),
|
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||||
@@ -181,8 +205,11 @@ export class ResizeController {
|
|||||||
const toModel = (p: DOMPoint) =>
|
const toModel = (p: DOMPoint) =>
|
||||||
p.matrixTransform(toLocalRotatedM.inverse());
|
p.matrixTransform(toLocalRotatedM.inverse());
|
||||||
|
|
||||||
const currPtLocal = toLocal(new DOMPoint(currPt[0], currPt[1]), true);
|
|
||||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
||||||
|
const currPtLocal = toLocal(
|
||||||
|
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
let scaleX = xSign
|
let scaleX = xSign
|
||||||
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||||
@@ -255,34 +282,38 @@ export class ResizeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resizeMulti(
|
private resizeMulti(
|
||||||
|
originalBound: Bound,
|
||||||
originals: ElementInitialSnapshot[],
|
originals: ElementInitialSnapshot[],
|
||||||
elements: GfxModel[],
|
elements: GfxModel[],
|
||||||
handle: ResizeHandle,
|
|
||||||
currPt: IVec,
|
|
||||||
startPt: IVec,
|
startPt: IVec,
|
||||||
|
delta: {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
},
|
||||||
|
handleSign: { xSign: number; ySign: number },
|
||||||
updateCallback: OptionResize['onResizeUpdate']
|
updateCallback: OptionResize['onResizeUpdate']
|
||||||
) {
|
) {
|
||||||
const commonBound = getCommonBoundWithRotation(originals);
|
const { xSign, ySign } = handleSign;
|
||||||
const { xSign, ySign } = this.getHandleSign(handle);
|
|
||||||
|
|
||||||
const pivot = new DOMPoint(
|
const pivot = new DOMPoint(
|
||||||
commonBound.x + ((-xSign + 1) / 2) * commonBound.w,
|
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||||
commonBound.y + ((-ySign + 1) / 2) * commonBound.h
|
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||||
);
|
);
|
||||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||||
|
|
||||||
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||||
|
|
||||||
const currPtLocal = toLocal(new DOMPoint(currPt[0], currPt[1]));
|
|
||||||
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
||||||
|
const currPtLocal = toLocal(
|
||||||
|
new DOMPoint(startPt[0] + delta.dx, startPt[1] + delta.dy)
|
||||||
|
);
|
||||||
|
|
||||||
let scaleX = xSign
|
let scaleX = xSign
|
||||||
? (xSign * (currPtLocal.x - handleLocal.x) + commonBound.w) /
|
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
|
||||||
commonBound.w
|
originalBound.w
|
||||||
: 1;
|
: 1;
|
||||||
let scaleY = ySign
|
let scaleY = ySign
|
||||||
? (ySign * (currPtLocal.y - handleLocal.y) + commonBound.h) /
|
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
|
||||||
commonBound.h
|
originalBound.h
|
||||||
: 1;
|
: 1;
|
||||||
|
|
||||||
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { GfxModel } from '../../model/model';
|
||||||
|
import type { ResizeHandle } from '../resize/manager';
|
||||||
|
|
||||||
|
export type ExtensionElementResizeContext = {
|
||||||
|
elements: GfxModel[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionElementResizeStartContext = {
|
||||||
|
elements: GfxModel[];
|
||||||
|
|
||||||
|
handle: ResizeHandle;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionElementResizeEndContext =
|
||||||
|
ExtensionElementResizeStartContext;
|
||||||
|
|
||||||
|
export type ExtensionElementResizeMoveContext =
|
||||||
|
ExtensionElementResizeStartContext & {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
|
||||||
|
lockRatio: boolean;
|
||||||
|
|
||||||
|
handleSign: {
|
||||||
|
xSign: number;
|
||||||
|
ySign: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
suggest: (distance: { dx: number; dy: number }) => void;
|
||||||
|
};
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
|
selectElementInEdgeless,
|
||||||
switchEditorMode,
|
switchEditorMode,
|
||||||
zoomResetByKeyboard,
|
zoomResetByKeyboard,
|
||||||
} from '../utils/actions/edgeless.js';
|
} from '../utils/actions/edgeless.js';
|
||||||
import {
|
import {
|
||||||
addBasicBrushElement,
|
addBasicBrushElement,
|
||||||
addBasicRectShapeElement,
|
addBasicRectShapeElement,
|
||||||
|
clickView,
|
||||||
dragBetweenCoords,
|
dragBetweenCoords,
|
||||||
enterPlaygroundRoom,
|
enterPlaygroundRoom,
|
||||||
initEmptyEdgelessState,
|
initEmptyEdgelessState,
|
||||||
|
pressBackspace,
|
||||||
resizeElementByHandle,
|
resizeElementByHandle,
|
||||||
} from '../utils/actions/index.js';
|
} from '../utils/actions/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -19,10 +22,15 @@ import { test } from '../utils/playwright.js';
|
|||||||
test.describe('resizing shapes and aspect ratio will be maintained', () => {
|
test.describe('resizing shapes and aspect ratio will be maintained', () => {
|
||||||
test('positive adjustment', async ({ page }) => {
|
test('positive adjustment', async ({ page }) => {
|
||||||
await enterPlaygroundRoom(page);
|
await enterPlaygroundRoom(page);
|
||||||
await initEmptyEdgelessState(page);
|
const { noteId } = await initEmptyEdgelessState(page);
|
||||||
await switchEditorMode(page);
|
await switchEditorMode(page);
|
||||||
await zoomResetByKeyboard(page);
|
await zoomResetByKeyboard(page);
|
||||||
|
|
||||||
|
// delete note to avoid snapping to it
|
||||||
|
await clickView(page, [0, 0]);
|
||||||
|
await selectElementInEdgeless(page, [noteId]);
|
||||||
|
await pressBackspace(page);
|
||||||
|
|
||||||
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
|
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
|
||||||
await page.mouse.click(110, 110);
|
await page.mouse.click(110, 110);
|
||||||
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
|
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
|
||||||
@@ -50,10 +58,15 @@ test.describe('resizing shapes and aspect ratio will be maintained', () => {
|
|||||||
|
|
||||||
test('negative adjustment', async ({ page }) => {
|
test('negative adjustment', async ({ page }) => {
|
||||||
await enterPlaygroundRoom(page);
|
await enterPlaygroundRoom(page);
|
||||||
await initEmptyEdgelessState(page);
|
const { noteId } = await initEmptyEdgelessState(page);
|
||||||
await switchEditorMode(page);
|
await switchEditorMode(page);
|
||||||
await zoomResetByKeyboard(page);
|
await zoomResetByKeyboard(page);
|
||||||
|
|
||||||
|
// delete note to avoid snapping to it
|
||||||
|
await clickView(page, [0, 0]);
|
||||||
|
await selectElementInEdgeless(page, [noteId]);
|
||||||
|
await pressBackspace(page);
|
||||||
|
|
||||||
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
|
await addBasicBrushElement(page, { x: 100, y: 100 }, { x: 200, y: 200 });
|
||||||
await page.mouse.click(110, 110);
|
await page.mouse.click(110, 110);
|
||||||
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
|
await assertEdgelessSelectedRect(page, [98, 98, 104, 104]);
|
||||||
|
|||||||
Reference in New Issue
Block a user