mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
fix: tuning drag and resize snapping (#12657)
### Changed - Better snapping when resize elements <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Improved resizing behavior with enhanced alignment and snapping during element resizing, supporting rotation and multiple element selection. - Alignment lines now display more accurately when resizing elements. - **Refactor** - Resizing logic updated to use scale factors instead of position deltas, enabling smoother and more precise resize operations. - Resize event data now includes richer details about handle positions, scaling, and original bounds. - Coordinate transformations and scaling now account for rotation and aspect ratio locking more robustly. - Cursor updates are disabled during active resize or rotate interactions for a smoother user experience. - **Tests** - Updated resizing tests to use square shapes, ensuring consistent verification of aspect ratio maintenance. <!-- 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 { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { type Bound } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragMoveContext,
|
||||
@@ -74,47 +74,63 @@ export class SnapExtension extends InteractivityExtension {
|
||||
return {};
|
||||
}
|
||||
|
||||
let alignBound: Bound | null = null;
|
||||
|
||||
return {
|
||||
onResizeStart(context) {
|
||||
alignBound = snapOverlay.setMovingElements(context.elements);
|
||||
snapOverlay.setMovingElements(context.elements);
|
||||
},
|
||||
onResizeMove(context) {
|
||||
if (!alignBound || alignBound.w === 0 || alignBound.h === 0) {
|
||||
return;
|
||||
const {
|
||||
handle,
|
||||
originalBound,
|
||||
scaleX,
|
||||
scaleY,
|
||||
handleSign,
|
||||
currentHandlePos,
|
||||
elements,
|
||||
} = context;
|
||||
const rotate = elements.length > 1 ? 0 : elements[0].rotate;
|
||||
const alignDirection: ('vertical' | 'horizontal')[] = [];
|
||||
let switchDirection = false;
|
||||
let nx = handleSign.x;
|
||||
let ny = handleSign.y;
|
||||
|
||||
if (handle.length > 6) {
|
||||
alignDirection.push('vertical', 'horizontal');
|
||||
} else if (rotate % 90 === 0) {
|
||||
nx =
|
||||
handleSign.x * Math.cos((rotate / 180) * Math.PI) -
|
||||
handleSign.y * Math.sin((rotate / 180) * Math.PI);
|
||||
ny =
|
||||
handleSign.x * Math.sin((rotate / 180) * Math.PI) +
|
||||
handleSign.y * Math.cos((rotate / 180) * Math.PI);
|
||||
|
||||
if (Math.abs(nx) > Math.abs(ny)) {
|
||||
alignDirection.push('horizontal');
|
||||
} else {
|
||||
alignDirection.push('vertical');
|
||||
}
|
||||
|
||||
if (rotate % 180 !== 0) {
|
||||
switchDirection = true;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if (alignDirection.length > 0) {
|
||||
const rst = snapOverlay.alignResize(
|
||||
currentHandlePos,
|
||||
alignDirection
|
||||
);
|
||||
|
||||
dx = min * Math.sign(dx) * alignBound.w;
|
||||
dy = min * Math.sign(dy) * alignBound.h;
|
||||
const dx = switchDirection ? ny * rst.dy : nx * rst.dx;
|
||||
const dy = switchDirection ? nx * rst.dx : ny * rst.dy;
|
||||
|
||||
context.suggest({
|
||||
scaleX: scaleX + dx / originalBound.w,
|
||||
scaleY: scaleY + dy / originalBound.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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ConnectorElementModel,
|
||||
MindmapElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { almostEqual, Bound, Point } from '@blocksuite/global/gfx';
|
||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||
|
||||
interface Distance {
|
||||
@@ -586,6 +586,60 @@ export class SnapOverlay extends Overlay {
|
||||
);
|
||||
}
|
||||
|
||||
alignResize(position: IVec, direction: ('vertical' | 'horizontal')[]) {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
|
||||
const { viewport } = this.gfx;
|
||||
const threshold = ALIGN_THRESHOLD / viewport.zoom;
|
||||
const searchBound = new Bound(
|
||||
position[0] - threshold / 2,
|
||||
position[1] - threshold / 2,
|
||||
threshold,
|
||||
threshold
|
||||
);
|
||||
const alignBound = new Bound(position[0], position[1], 0, 0);
|
||||
|
||||
this._intraGraphicAlignLines = {
|
||||
horizontal: [],
|
||||
vertical: [],
|
||||
};
|
||||
this._distributedAlignLines = [];
|
||||
this._updateAlignCandidates(searchBound);
|
||||
|
||||
for (const other of this._referenceBounds.all) {
|
||||
const closestDistances = this._calculateClosestDistances(
|
||||
alignBound,
|
||||
other
|
||||
);
|
||||
|
||||
if (
|
||||
direction.includes('horizontal') &&
|
||||
closestDistances.horiz &&
|
||||
(!this._intraGraphicAlignLines.horizontal.length ||
|
||||
Math.abs(closestDistances.horiz.distance) < Math.abs(rst.dx))
|
||||
) {
|
||||
this._updateXAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
|
||||
if (
|
||||
direction.includes('vertical') &&
|
||||
closestDistances.vert &&
|
||||
(!this._intraGraphicAlignLines.vertical.length ||
|
||||
Math.abs(closestDistances.vert.distance) < Math.abs(rst.dy))
|
||||
) {
|
||||
this._updateYAlignPoint(rst, alignBound, other, closestDistances);
|
||||
}
|
||||
}
|
||||
|
||||
this._intraGraphicAlignLines.horizontal =
|
||||
this._intraGraphicAlignLines.horizontal.slice(0, 1);
|
||||
this._intraGraphicAlignLines.vertical =
|
||||
this._intraGraphicAlignLines.vertical.slice(0, 1);
|
||||
this._renderer?.refresh();
|
||||
|
||||
return rst;
|
||||
}
|
||||
|
||||
align(bound: Bound): { dx: number; dy: number } {
|
||||
const rst = { dx: 0, dy: 0 };
|
||||
const threshold = ALIGN_THRESHOLD / this.gfx.viewport.zoom;
|
||||
|
||||
@@ -374,6 +374,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize' | 'rotate';
|
||||
angle: number;
|
||||
handle: ResizeHandle;
|
||||
flipX?: boolean;
|
||||
flipY?: boolean;
|
||||
pure?: boolean;
|
||||
}) => {
|
||||
if (!options) {
|
||||
@@ -381,8 +383,25 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
return 'default';
|
||||
}
|
||||
|
||||
const { type, angle, handle } = options;
|
||||
const { type, angle, flipX, flipY } = options;
|
||||
let cursor: CursorType = 'default';
|
||||
let handle: ResizeHandle = options.handle;
|
||||
|
||||
if (flipX) {
|
||||
handle = (
|
||||
handle.includes('left')
|
||||
? handle.replace('left', 'right')
|
||||
: handle.replace('right', 'left')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (flipY) {
|
||||
handle = (
|
||||
handle.includes('top')
|
||||
? handle.replace('top', 'bottom')
|
||||
: handle.replace('bottom', 'top')
|
||||
) as ResizeHandle;
|
||||
}
|
||||
|
||||
if (type === 'rotate') {
|
||||
cursor = generateCursorUrl(angle, handle);
|
||||
@@ -626,7 +645,7 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
onResizeStart: () => {
|
||||
this._mode = 'resize';
|
||||
},
|
||||
onResizeUpdate: ({ lockRatio, scaleX, exceed }) => {
|
||||
onResizeUpdate: ({ lockRatio, scaleX, scaleY, exceed }) => {
|
||||
if (lockRatio) {
|
||||
this._scaleDirection = handle;
|
||||
this._scalePercent = `${Math.round(scaleX * 100)}%`;
|
||||
@@ -642,6 +661,8 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
type: 'resize',
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
handle,
|
||||
flipX: scaleX < 0,
|
||||
flipY: scaleY < 0,
|
||||
});
|
||||
},
|
||||
onResizeEnd: () => {
|
||||
@@ -652,6 +673,14 @@ export class EdgelessSelectedRectWidget extends WidgetComponent<RootBlockModel>
|
||||
}
|
||||
},
|
||||
option => {
|
||||
if (
|
||||
['resize', 'rotate'].includes(
|
||||
interaction.activeInteraction$.value?.type ?? ''
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return this._updateCursor({
|
||||
...option,
|
||||
angle: elements.length > 1 ? 0 : (elements[0]?.rotate ?? 0),
|
||||
|
||||
@@ -947,23 +947,34 @@ export class InteractivityManager extends GfxExtension {
|
||||
...options,
|
||||
lockRatio,
|
||||
elements,
|
||||
onResizeMove: ({ dx, dy, handleSign, lockRatio }) => {
|
||||
onResizeMove: ({
|
||||
scaleX,
|
||||
scaleY,
|
||||
originalBound,
|
||||
handleSign,
|
||||
handlePos,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
}) => {
|
||||
const suggested: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
priority?: number;
|
||||
}[] = [];
|
||||
const suggest = (distance: { dx: number; dy: number }) => {
|
||||
const suggest = (distance: { scaleX: number; scaleY: number }) => {
|
||||
suggested.push(distance);
|
||||
};
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeMove?.({
|
||||
dx,
|
||||
dy,
|
||||
scaleX,
|
||||
scaleY,
|
||||
elements,
|
||||
handleSign,
|
||||
handle,
|
||||
handleSign,
|
||||
handlePos,
|
||||
originalBound,
|
||||
currentHandlePos,
|
||||
lockRatio,
|
||||
suggest,
|
||||
});
|
||||
@@ -973,9 +984,9 @@ export class InteractivityManager extends GfxExtension {
|
||||
return (a.priority ?? 0) - (b.priority ?? 0);
|
||||
});
|
||||
|
||||
return last(suggested) ?? { dx, dy };
|
||||
return last(suggested) ?? { scaleX, scaleY };
|
||||
},
|
||||
onResizeStart: ({ data }) => {
|
||||
onResizeStart: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = {
|
||||
type: 'resize',
|
||||
elements,
|
||||
@@ -984,6 +995,8 @@ export class InteractivityManager extends GfxExtension {
|
||||
ext.onResizeStart?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1045,13 +1058,15 @@ export class InteractivityManager extends GfxExtension {
|
||||
|
||||
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
|
||||
},
|
||||
onResizeEnd: ({ data }) => {
|
||||
onResizeEnd: ({ handleSign, handlePos, data }) => {
|
||||
this.activeInteraction$.value = null;
|
||||
|
||||
extensionHandlers.forEach(ext => {
|
||||
ext.onResizeEnd?.({
|
||||
elements,
|
||||
handle,
|
||||
handlePos,
|
||||
handleSign,
|
||||
});
|
||||
});
|
||||
options.onResizeEnd?.();
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Bound,
|
||||
getCommonBoundWithRotation,
|
||||
type IBound,
|
||||
type IPoint,
|
||||
type IVec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
|
||||
@@ -29,7 +30,7 @@ export const DEFAULT_HANDLES: ResizeHandle[] = [
|
||||
'bottom',
|
||||
];
|
||||
|
||||
type ElementInitialSnapshot = Readonly<Required<IBound>>;
|
||||
type ReadonlyIBound = Readonly<Required<IBound>>;
|
||||
|
||||
export interface OptionResize {
|
||||
elements: GfxModel[];
|
||||
@@ -37,16 +38,18 @@ export interface OptionResize {
|
||||
lockRatio: boolean;
|
||||
event: PointerEvent;
|
||||
onResizeMove: (payload: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
originalBound: IBound;
|
||||
|
||||
handleSign: IPoint;
|
||||
|
||||
handlePos: IVec;
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
}) => { dx: number; dy: number };
|
||||
}) => { scaleX: number; scaleY: number };
|
||||
onResizeUpdate: (payload: {
|
||||
lockRatio: boolean;
|
||||
scaleX: number;
|
||||
@@ -59,8 +62,16 @@ export interface OptionResize {
|
||||
matrix: DOMMatrix;
|
||||
}[];
|
||||
}) => void;
|
||||
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||
onResizeStart?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
onResizeEnd?: (payload: {
|
||||
handlePos: IVec;
|
||||
handleSign: IPoint;
|
||||
data: { model: GfxModel }[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export type RotateOption = {
|
||||
@@ -95,11 +106,102 @@ export class ResizeController {
|
||||
this.gfx = option.gfx;
|
||||
}
|
||||
|
||||
getCoordsTransform(originalBound: IBound, handle: ResizeHandle) {
|
||||
const { x: xSign, y: ySign } = this.getHandleSign(handle);
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(
|
||||
originalBound.w / 2 + originalBound.x,
|
||||
originalBound.h / 2 + originalBound.y
|
||||
)
|
||||
.rotate(-(originalBound.rotate ?? 0))
|
||||
.translate(
|
||||
-(originalBound.w / 2 + originalBound.x),
|
||||
-(originalBound.h / 2 + originalBound.y)
|
||||
);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean = false) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
const handlePos = toModel(
|
||||
new DOMPoint(originalBound.w * xSign, originalBound.h * ySign)
|
||||
);
|
||||
|
||||
return {
|
||||
xSign,
|
||||
ySign,
|
||||
originalBound,
|
||||
toLocalM,
|
||||
toLocalRotatedM,
|
||||
toLocal,
|
||||
toModel,
|
||||
handlePos: [handlePos.x, handlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleFromDelta(
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
delta: { dx: number; dy: number },
|
||||
handleStartPos: IVec,
|
||||
lockRatio: boolean
|
||||
) {
|
||||
const { originalBound, xSign, ySign, toModel, toLocal } = transform;
|
||||
const currentPos = toLocal(
|
||||
new DOMPoint(handleStartPos[0] + delta.dx, handleStartPos[1] + delta.dy),
|
||||
true
|
||||
);
|
||||
|
||||
let scaleX = xSign ? currentPos.x / (originalBound.w * xSign) : 1;
|
||||
let scaleY = ySign ? currentPos.y / (originalBound.h * ySign) : 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const finalHandlePos = toModel(
|
||||
new DOMPoint(
|
||||
originalBound.w * xSign * scaleX,
|
||||
originalBound.h * ySign * scaleY
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: [finalHandlePos.x, finalHandlePos.y] as IVec,
|
||||
};
|
||||
}
|
||||
|
||||
getScaleMatrix(
|
||||
{ scaleX, scaleY }: { scaleX: number; scaleY: number },
|
||||
lockRatio: boolean
|
||||
) {
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
return {
|
||||
scaleX,
|
||||
scaleY,
|
||||
scaleM: new DOMMatrix().scaleSelf(scaleX, scaleY),
|
||||
};
|
||||
}
|
||||
|
||||
startResize(options: OptionResize) {
|
||||
const {
|
||||
elements,
|
||||
handle,
|
||||
lockRatio,
|
||||
onResizeStart,
|
||||
onResizeMove,
|
||||
onResizeUpdate,
|
||||
@@ -107,19 +209,32 @@ export class ResizeController {
|
||||
event,
|
||||
} = options;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
h: el.h,
|
||||
rotate: el.rotate,
|
||||
}));
|
||||
const originalBound = getCommonBoundWithRotation(originals);
|
||||
const originalBound: IBound =
|
||||
originals.length > 1
|
||||
? getCommonBoundWithRotation(originals)
|
||||
: {
|
||||
x: originals[0].x,
|
||||
y: originals[0].y,
|
||||
w: originals[0].w,
|
||||
h: originals[0].h,
|
||||
rotate: originals[0].rotate,
|
||||
};
|
||||
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
]);
|
||||
const handleSign = this.getHandleSign(handle);
|
||||
const transform = this.getCoordsTransform(originalBound, handle);
|
||||
const handleSign = {
|
||||
x: transform.xSign,
|
||||
y: transform.ySign,
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||
@@ -130,45 +245,69 @@ export class ResizeController {
|
||||
dx: currPt[0] - startPt[0],
|
||||
dy: currPt[1] - startPt[1],
|
||||
};
|
||||
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||
const shouldLockRatio =
|
||||
options.lockRatio || e.shiftKey || elements.length > 1;
|
||||
const {
|
||||
scaleX,
|
||||
scaleY,
|
||||
handlePos: currentHandlePos,
|
||||
} = this.getScaleFromDelta(
|
||||
transform,
|
||||
delta,
|
||||
transform.handlePos,
|
||||
shouldLockRatio
|
||||
);
|
||||
|
||||
const scale = onResizeMove({
|
||||
scaleX,
|
||||
scaleY,
|
||||
|
||||
originalBound,
|
||||
|
||||
delta = onResizeMove({
|
||||
dx: delta.dx,
|
||||
dy: delta.dy,
|
||||
handleSign,
|
||||
|
||||
handlePos: transform.handlePos,
|
||||
currentHandlePos,
|
||||
|
||||
lockRatio: shouldLockRatio,
|
||||
});
|
||||
const scaleInfo = this.getScaleMatrix(scale, shouldLockRatio);
|
||||
|
||||
if (elements.length === 1) {
|
||||
this.resizeSingle(
|
||||
originals[0],
|
||||
elements[0],
|
||||
shouldLockRatio,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
} else {
|
||||
this.resizeMulti(
|
||||
originalBound,
|
||||
originals,
|
||||
elements,
|
||||
startPt,
|
||||
delta,
|
||||
handleSign,
|
||||
transform,
|
||||
scaleInfo,
|
||||
onResizeUpdate
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStart?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeStart?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
|
||||
const onPointerUp = () => {
|
||||
this.host.removeEventListener('pointermove', onPointerMove);
|
||||
this.host.removeEventListener('pointerup', onPointerUp);
|
||||
|
||||
onResizeEnd?.({ data: elements.map(model => ({ model })) });
|
||||
onResizeEnd?.({
|
||||
handleSign,
|
||||
handlePos: transform.handlePos,
|
||||
data: elements.map(model => ({ model })),
|
||||
});
|
||||
};
|
||||
|
||||
this.host.addEventListener('pointermove', onPointerMove);
|
||||
@@ -176,55 +315,15 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeSingle(
|
||||
orig: ElementInitialSnapshot,
|
||||
orig: ReadonlyIBound,
|
||||
model: GfxModel,
|
||||
lockRatio: boolean,
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<typeof ResizeController.prototype.getCoordsTransform>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
|
||||
const pivot = new DOMPoint(
|
||||
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||
orig.y + (-ySign === 1 ? orig.h : 0)
|
||||
);
|
||||
const toLocalRotatedM = new DOMMatrix()
|
||||
.translate(-pivot.x, -pivot.y)
|
||||
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
|
||||
.rotate(-orig.rotate)
|
||||
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint, withRotation: boolean) =>
|
||||
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||
const toModel = (p: DOMPoint) =>
|
||||
p.matrixTransform(toLocalRotatedM.inverse());
|
||||
|
||||
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
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
|
||||
: 1;
|
||||
|
||||
if (lockRatio) {
|
||||
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
}
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM, toLocalRotatedM, toLocal, toModel } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const [visualTopLeft, visualBottomRight] = [
|
||||
new DOMPoint(orig.x, orig.y),
|
||||
@@ -282,45 +381,14 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private resizeMulti(
|
||||
originalBound: Bound,
|
||||
originals: ElementInitialSnapshot[],
|
||||
originals: ReadonlyIBound[],
|
||||
elements: GfxModel[],
|
||||
startPt: IVec,
|
||||
delta: {
|
||||
dx: number;
|
||||
dy: number;
|
||||
},
|
||||
handleSign: { xSign: number; ySign: number },
|
||||
transform: ReturnType<ResizeController['getCoordsTransform']>,
|
||||
scale: { scaleX: number; scaleY: number; scaleM: DOMMatrix },
|
||||
updateCallback: OptionResize['onResizeUpdate']
|
||||
) {
|
||||
const { xSign, ySign } = handleSign;
|
||||
const pivot = new DOMPoint(
|
||||
originalBound.x + ((-xSign + 1) / 2) * originalBound.w,
|
||||
originalBound.y + ((-ySign + 1) / 2) * originalBound.h
|
||||
);
|
||||
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||
|
||||
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||
|
||||
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
|
||||
? (xSign * (currPtLocal.x - handleLocal.x) + originalBound.w) /
|
||||
originalBound.w
|
||||
: 1;
|
||||
let scaleY = ySign
|
||||
? (ySign * (currPtLocal.y - handleLocal.y) + originalBound.h) /
|
||||
originalBound.h
|
||||
: 1;
|
||||
|
||||
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||
scaleX = Math.sign(scaleX) * min;
|
||||
scaleY = Math.sign(scaleY) * min;
|
||||
|
||||
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||
const { toLocalM } = transform;
|
||||
const { scaleX, scaleY, scaleM } = scale;
|
||||
|
||||
const data = elements.map((model, i) => {
|
||||
const orig = originals[i];
|
||||
@@ -357,7 +425,7 @@ export class ResizeController {
|
||||
startRotate(option: RotateOption) {
|
||||
const { event, elements, onRotateUpdate } = option;
|
||||
|
||||
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||
const originals: ReadonlyIBound[] = elements.map(el => ({
|
||||
x: el.x,
|
||||
y: el.y,
|
||||
w: el.w,
|
||||
@@ -429,7 +497,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateSingle(option: {
|
||||
orig: ElementInitialSnapshot;
|
||||
orig: ReadonlyIBound;
|
||||
model: GfxModel;
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -481,7 +549,7 @@ export class ResizeController {
|
||||
}
|
||||
|
||||
private rotateMulti(option: {
|
||||
origs: ElementInitialSnapshot[];
|
||||
origs: ReadonlyIBound[];
|
||||
models: GfxModel[];
|
||||
startPt: IVec;
|
||||
currentPt: IVec;
|
||||
@@ -567,23 +635,23 @@ export class ResizeController {
|
||||
private getHandleSign(handle: ResizeHandle) {
|
||||
switch (handle) {
|
||||
case 'top-left':
|
||||
return { xSign: -1, ySign: -1 };
|
||||
return { x: -1, y: -1 };
|
||||
case 'top':
|
||||
return { xSign: 0, ySign: -1 };
|
||||
return { x: 0, y: -1 };
|
||||
case 'top-right':
|
||||
return { xSign: 1, ySign: -1 };
|
||||
return { x: 1, y: -1 };
|
||||
case 'right':
|
||||
return { xSign: 1, ySign: 0 };
|
||||
return { x: 1, y: 0 };
|
||||
case 'bottom-right':
|
||||
return { xSign: 1, ySign: 1 };
|
||||
return { x: 1, y: 1 };
|
||||
case 'bottom':
|
||||
return { xSign: 0, ySign: 1 };
|
||||
return { x: 0, y: 1 };
|
||||
case 'bottom-left':
|
||||
return { xSign: -1, ySign: 1 };
|
||||
return { x: -1, y: 1 };
|
||||
case 'left':
|
||||
return { xSign: -1, ySign: 0 };
|
||||
return { x: -1, y: 0 };
|
||||
default:
|
||||
return { xSign: 0, ySign: 0 };
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { IBound, IPoint, IVec } from '@blocksuite/global/gfx';
|
||||
|
||||
import type { GfxModel } from '../../model/model';
|
||||
import type { ResizeHandle } from '../resize/manager';
|
||||
|
||||
@@ -8,6 +10,16 @@ export type ExtensionElementResizeContext = {
|
||||
export type ExtensionElementResizeStartContext = {
|
||||
elements: GfxModel[];
|
||||
|
||||
/**
|
||||
* The position of the handle in the browser coordinate space.
|
||||
*/
|
||||
handlePos: IVec;
|
||||
|
||||
/**
|
||||
* The sign (or normal vector) of the handle.
|
||||
*/
|
||||
handleSign: IPoint;
|
||||
|
||||
handle: ResizeHandle;
|
||||
};
|
||||
|
||||
@@ -16,15 +28,14 @@ export type ExtensionElementResizeEndContext =
|
||||
|
||||
export type ExtensionElementResizeMoveContext =
|
||||
ExtensionElementResizeStartContext & {
|
||||
dx: number;
|
||||
dy: number;
|
||||
scaleX: number;
|
||||
scaleY: number;
|
||||
|
||||
originalBound: IBound;
|
||||
|
||||
currentHandlePos: IVec;
|
||||
|
||||
lockRatio: boolean;
|
||||
|
||||
handleSign: {
|
||||
xSign: number;
|
||||
ySign: number;
|
||||
};
|
||||
|
||||
suggest: (distance: { dx: number; dy: number }) => void;
|
||||
suggest: (distance: { scaleX: number; scaleY: number }) => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user