Files
AFFiNE-Mirror/blocksuite/blocks/src/root-block/edgeless/gfx-tool/default-tool.ts

1068 lines
30 KiB
TypeScript

import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
import {
ConnectorUtils,
OverlayIdentifier,
} from '@blocksuite/affine-block-surface';
import { focusTextModel } from '@blocksuite/affine-components/rich-text';
import type {
EdgelessTextBlockModel,
FrameBlockModel,
NoteBlockModel,
} from '@blocksuite/affine-model';
import {
ConnectorElementModel,
GroupElementModel,
MindmapElementModel,
ShapeElementModel,
TextElementModel,
} from '@blocksuite/affine-model';
import {
FeatureFlagService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import {
clamp,
handleNativeRangeAtPoint,
resetNativeSelection,
} from '@blocksuite/affine-shared/utils';
import type { PointerEventState } from '@blocksuite/block-std';
import {
BaseTool,
getTopElements,
type GfxBlockElementModel,
GfxExtensionIdentifier,
type GfxModel,
type GfxPrimitiveElementModel,
isGfxGroupCompatibleModel,
type PointTestOptions,
} from '@blocksuite/block-std/gfx';
import type { IVec } from '@blocksuite/global/utils';
import {
Bound,
DisposableGroup,
getCommonBoundWithRotation,
last,
noop,
Vec,
} from '@blocksuite/global/utils';
import { effect } from '@preact/signals-core';
import { isSingleMindMapNode } from '../../../_common/edgeless/mindmap/index.js';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
import type { EdgelessFrameManager, FrameOverlay } from '../frame-manager.js';
import { prepareCloneData } from '../utils/clone-utils.js';
import { calPanDelta } from '../utils/panning-utils.js';
import {
isCanvasElement,
isEdgelessTextBlock,
isFrameBlock,
isNoteBlock,
} from '../utils/query.js';
import type { EdgelessSnapManager } from '../utils/snap-manager.js';
import {
addText,
mountConnectorLabelEditor,
mountFrameTitleEditor,
mountGroupTitleEditor,
mountShapeTextEditor,
mountTextElementEditor,
} from '../utils/text.js';
import { fitToScreen } from '../utils/viewport.js';
import { CanvasElementEventExt } from './default-tool-ext/event-ext.js';
import type { DefaultToolExt } from './default-tool-ext/ext.js';
import { DefaultModeDragType } from './default-tool-ext/ext.js';
import { MindMapExt } from './default-tool-ext/mind-map-ext/mind-map-ext.js';
export class DefaultTool extends BaseTool {
static override toolName: string = 'default';
private _accumulateDelta: IVec = [0, 0];
private _alignBound = new Bound();
private _autoPanTimer: number | null = null;
private readonly _clearDisposable = () => {
if (this._disposables) {
this._disposables.dispose();
this._disposables = null;
}
};
private readonly _clearSelectingState = () => {
this._stopAutoPanning();
this._clearDisposable();
this._wheeling = false;
};
private _disposables: DisposableGroup | null = null;
private _extHandlers: {
dragStart?: (evt: PointerEventState) => void;
dragMove?: (evt: PointerEventState) => void;
dragEnd?: (evt: PointerEventState) => void;
}[] = [];
private _exts: DefaultToolExt[] = [];
private _hoveredFrame: FrameBlockModel | null = null;
// Do not select the text, when click again after activating the note.
private _isDoubleClickedOnMask = false;
private _lock = false;
private readonly _panViewport = (delta: IVec) => {
this._accumulateDelta[0] += delta[0];
this._accumulateDelta[1] += delta[1];
this.gfx.viewport.applyDeltaCenter(delta[0], delta[1]);
};
private readonly _pendingUpdates = new Map<
GfxBlockElementModel | GfxPrimitiveElementModel,
Partial<GfxBlockElementModel>
>();
private _rafId: number | null = null;
private _selectedBounds: Bound[] = [];
// For moving the connector label
private _selectedConnector: ConnectorElementModel | null = null;
private _selectedConnectorLabelBounds: Bound | null = null;
private _selectionRectTransition: null | {
w: number;
h: number;
startX: number;
startY: number;
endX: number;
endY: number;
} = null;
private readonly _startAutoPanning = (delta: IVec) => {
this._panViewport(delta);
this._updateSelectingState(delta);
this._stopAutoPanning();
this._autoPanTimer = window.setInterval(() => {
this._panViewport(delta);
this._updateSelectingState(delta);
}, 30);
};
private readonly _stopAutoPanning = () => {
if (this._autoPanTimer) {
clearTimeout(this._autoPanTimer);
this._autoPanTimer = null;
}
};
private _toBeMoved: GfxModel[] = [];
private readonly _updateSelectingState = (delta: IVec = [0, 0]) => {
const { gfx } = this;
if (gfx.keyboard.spaceKey$.peek() && this._selectionRectTransition) {
/* Move the selection if space is pressed */
const curDraggingViewArea = this.controller.draggingViewArea$.peek();
const { w, h, startX, startY, endX, endY } =
this._selectionRectTransition;
const { endX: lastX, endY: lastY } = curDraggingViewArea;
const dx = lastX + delta[0] - endX + this._accumulateDelta[0];
const dy = lastY + delta[1] - endY + this._accumulateDelta[1];
this.controller.draggingViewArea$.value = {
...curDraggingViewArea,
x: Math.min(startX + dx, lastX),
y: Math.min(startY + dy, lastY),
w,
h,
startX: startX + dx,
startY: startY + dy,
};
} else {
const curDraggingArea = this.controller.draggingViewArea$.peek();
const newStartX = curDraggingArea.startX - delta[0];
const newStartY = curDraggingArea.startY - delta[1];
this.controller.draggingViewArea$.value = {
...curDraggingArea,
startX: newStartX,
startY: newStartY,
x: Math.min(newStartX, curDraggingArea.endX),
y: Math.min(newStartY, curDraggingArea.endY),
w: Math.abs(curDraggingArea.endX - newStartX),
h: Math.abs(curDraggingArea.endY - newStartY),
};
}
const { x, y, w, h } = this.controller.draggingArea$.peek();
const bound = new Bound(x, y, w, h);
let elements = gfx.getElementsByBound(bound).filter(el => {
if (isFrameBlock(el)) {
return el.childElements.length === 0 || bound.contains(el.elementBound);
}
if (el instanceof MindmapElementModel) {
return bound.contains(el.elementBound);
}
return true;
});
elements = getTopElements(elements).filter(el => !el.isLocked());
const set = new Set(
gfx.keyboard.shiftKey$.peek()
? [...elements, ...gfx.selection.selectedElements]
: elements
);
this.edgelessSelectionManager.set({
elements: Array.from(set).map(element => element.id),
editing: false,
});
};
private _wheeling = false;
dragType = DefaultModeDragType.None;
enableHover = true;
private get _edgeless(): EdgelessRootBlockComponent | null {
const block = this.std.view.getBlock(this.doc.root!.id);
return (block as EdgelessRootBlockComponent) ?? null;
}
private get _frameMgr() {
return this.std.get(
GfxExtensionIdentifier('frame-manager')
) as EdgelessFrameManager;
}
private get _supportedExts() {
return this._exts.filter(ext =>
ext.supportedDragTypes.includes(this.dragType)
);
}
/**
* Get the end position of the dragging area in the model coordinate
*/
get dragLastPos() {
const { endX, endY } = this.controller.draggingArea$.peek();
return [endX, endY] as IVec;
}
/**
* Get the start position of the dragging area in the model coordinate
*/
get dragStartPos() {
const { startX, startY } = this.controller.draggingArea$.peek();
return [startX, startY] as IVec;
}
get edgelessSelectionManager() {
return this.gfx.selection;
}
private get frameOverlay() {
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
}
get snapOverlay() {
return this.std.get(
OverlayIdentifier('snap-manager')
) as EdgelessSnapManager;
}
private _addEmptyParagraphBlock(
block: NoteBlockModel | EdgelessTextBlockModel
) {
const blockId = this.doc.addBlock(
'affine:paragraph',
{ type: 'text' },
block.id
);
if (blockId) {
focusTextModel(this.std, blockId);
}
}
private async _cloneContent() {
this._lock = true;
if (!this._edgeless) return;
const clipboardController = this._edgeless?.clipboardController;
const snapshot = prepareCloneData(this._toBeMoved, this.std);
const bound = getCommonBoundWithRotation(this._toBeMoved);
const { canvasElements, blockModels } =
await clipboardController.createElementsFromClipboardData(
snapshot,
bound.center
);
this._toBeMoved = [...canvasElements, ...blockModels];
this.edgelessSelectionManager.set({
elements: this._toBeMoved.map(e => e.id),
editing: false,
});
}
private _determineDragType(e: PointerEventState): DefaultModeDragType {
const { x, y } = e;
// Is dragging started from current selected rect
if (this.edgelessSelectionManager.isInSelectedRect(x, y)) {
if (this.edgelessSelectionManager.selectedElements.length === 1) {
let selected = this.edgelessSelectionManager.selectedElements[0];
// double check
const currentSelected = this._pick(x, y);
if (
!isFrameBlock(selected) &&
!(selected instanceof GroupElementModel) &&
currentSelected &&
currentSelected !== selected
) {
selected = currentSelected;
this.edgelessSelectionManager.set({
elements: [selected.id],
editing: false,
});
}
if (
isCanvasElement(selected) &&
ConnectorUtils.isConnectorWithLabel(selected) &&
(selected as ConnectorElementModel).labelIncludesPoint(
this.gfx.viewport.toModelCoord(x, y)
)
) {
this._selectedConnector = selected as ConnectorElementModel;
this._selectedConnectorLabelBounds = Bound.fromXYWH(
this._selectedConnector.labelXYWH!
);
return DefaultModeDragType.ConnectorLabelMoving;
}
}
return this.edgelessSelectionManager.editing
? DefaultModeDragType.NativeEditing
: DefaultModeDragType.ContentMoving;
} else {
const selected = this._pick(x, y);
if (selected) {
this.edgelessSelectionManager.set({
elements: [selected.id],
editing: false,
});
if (
isCanvasElement(selected) &&
ConnectorUtils.isConnectorWithLabel(selected) &&
(selected as ConnectorElementModel).labelIncludesPoint(
this.gfx.viewport.toModelCoord(x, y)
)
) {
this._selectedConnector = selected as ConnectorElementModel;
this._selectedConnectorLabelBounds = Bound.fromXYWH(
this._selectedConnector.labelXYWH!
);
return DefaultModeDragType.ConnectorLabelMoving;
}
return DefaultModeDragType.ContentMoving;
} else {
return DefaultModeDragType.Selecting;
}
}
}
private _filterConnectedConnector() {
this._toBeMoved = this._toBeMoved.filter(ele => {
// eslint-disable-next-line sonarjs/no-collapsible-if
if (
ele instanceof ConnectorElementModel &&
ele.source?.id &&
ele.target?.id
) {
if (
this._toBeMoved.some(e => e.id === ele.source.id) &&
this._toBeMoved.some(e => e.id === ele.target.id)
) {
return false;
}
}
return true;
});
}
private _isDraggable(element: GfxModel) {
return !(
element instanceof ConnectorElementModel &&
!ConnectorUtils.isConnectorAndBindingsAllSelected(
element,
this._toBeMoved
)
);
}
private _moveContent(
[dx, dy]: IVec,
alignBound: Bound,
shifted?: boolean,
shouldClone?: boolean
) {
alignBound.x += dx;
alignBound.y += dy;
const alignRst = this.snapOverlay.align(alignBound);
const delta = [dx + alignRst.dx, dy + alignRst.dy];
if (shifted) {
const angle = Math.abs(Math.atan2(delta[1], delta[0]));
const direction =
angle < Math.PI / 4 || angle > 3 * (Math.PI / 4) ? 'x' : 'y';
delta[direction === 'x' ? 1 : 0] = 0;
}
this._toBeMoved.forEach((element, index) => {
const isGraphicElement = isCanvasElement(element);
if (isGraphicElement && !this._isDraggable(element)) return;
let bound = this._selectedBounds[index];
if (shouldClone) bound = bound.clone();
bound.x += delta[0];
bound.y += delta[1];
if (isGraphicElement) {
if (!this._lock) {
this._lock = true;
this.doc.captureSync();
}
if (element instanceof ConnectorElementModel) {
element.moveTo(bound);
}
}
this._scheduleUpdate(element, {
xywh: bound.serialize(),
});
});
this._hoveredFrame = this._frameMgr.getFrameFromPoint(
this.dragLastPos,
this._toBeMoved.filter(ele => isFrameBlock(ele))
);
this._hoveredFrame && !this._hoveredFrame.isLocked()
? this.frameOverlay.highlight(this._hoveredFrame)
: this.frameOverlay.clear();
}
private _moveLabel(delta: IVec) {
const connector = this._selectedConnector;
let bounds = this._selectedConnectorLabelBounds;
if (!connector || !bounds) return;
bounds = bounds.clone();
const center = connector.getNearestPoint(
Vec.add(bounds.center, delta) as IVec
);
const distance = connector.getOffsetDistanceByPoint(center as IVec);
bounds.center = center;
this.gfx.updateElement(connector, {
labelXYWH: bounds.toXYWH(),
labelOffset: {
distance,
},
});
}
private _pick(x: number, y: number, options?: PointTestOptions) {
const modelPos = this.gfx.viewport.toModelCoord(x, y);
const tryGetLockedAncestor = (e: GfxModel | null) => {
if (e?.isLockedByAncestor()) {
return e.groups.findLast(group => group.isLocked());
}
return e;
};
const frameByPickingTitle = last(
this.gfx
.getElementByPoint(modelPos[0], modelPos[1], {
...options,
all: true,
})
.filter(
el => isFrameBlock(el) && el.externalBound?.isPointInBound(modelPos)
)
);
if (frameByPickingTitle) return tryGetLockedAncestor(frameByPickingTitle);
const result = this.gfx.getElementInGroup(
modelPos[0],
modelPos[1],
options
);
if (result instanceof MindmapElementModel) {
const picked = this.gfx.getElementByPoint(modelPos[0], modelPos[1], {
...((options ?? {}) as PointTestOptions),
all: true,
});
let pickedIdx = picked.length - 1;
while (pickedIdx >= 0) {
const element = picked[pickedIdx];
if (element === result) {
pickedIdx -= 1;
continue;
}
break;
}
return tryGetLockedAncestor(picked[pickedIdx]) ?? null;
}
// if the frame has title, it only can be picked by clicking the title
if (isFrameBlock(result) && result.externalXYWH) {
return null;
}
return tryGetLockedAncestor(result);
}
private _scheduleUpdate(
element: GfxBlockElementModel | GfxPrimitiveElementModel,
updates: Partial<GfxBlockElementModel>
) {
this._pendingUpdates.set(element, updates);
if (this._rafId !== null) return;
this._rafId = requestAnimationFrame(() => {
this._pendingUpdates.forEach((updates, element) => {
this.gfx.updateElement(element, updates);
});
this._pendingUpdates.clear();
this._rafId = null;
});
}
private initializeDragState(
dragType: DefaultModeDragType,
event: PointerEventState
) {
this.dragType = dragType;
if (
(this._toBeMoved.length &&
this._toBeMoved.every(
ele => !(ele.group instanceof MindmapElementModel)
)) ||
(isSingleMindMapNode(this._toBeMoved) &&
this._toBeMoved[0].id ===
(this._toBeMoved[0].group as MindmapElementModel).tree.id)
) {
const mindmap = this._toBeMoved[0].group as MindmapElementModel;
this._alignBound = this.snapOverlay.setupAlignables(this._toBeMoved, [
mindmap,
...(mindmap?.childElements || []),
]);
}
this._clearDisposable();
this._disposables = new DisposableGroup();
const ctx = {
movedElements: this._toBeMoved,
dragType,
event,
};
this._extHandlers = this._supportedExts.map(ext => ext.initDrag(ctx));
this._selectedBounds = this._toBeMoved.map(element =>
Bound.deserialize(element.xywh)
);
// If the drag type is selecting, set up the dragging area disposable group
// If the viewport updates when dragging, should update the dragging area and selection
if (this.dragType === DefaultModeDragType.Selecting) {
this._disposables.add(
this.gfx.viewport.viewportUpdated.on(() => {
if (
this.dragType === DefaultModeDragType.Selecting &&
this.controller.dragging$.peek() &&
!this._autoPanTimer
) {
this._updateSelectingState();
}
})
);
return;
}
if (this.dragType === DefaultModeDragType.ContentMoving) {
this._disposables.add(
this.gfx.viewport.viewportMoved.on(delta => {
if (
this.dragType === DefaultModeDragType.ContentMoving &&
this.controller.dragging$.peek() &&
!this._autoPanTimer
) {
if (
this._toBeMoved.every(ele => {
return !this._isDraggable(ele);
})
) {
return;
}
if (!this._wheeling) {
this._wheeling = true;
this._selectedBounds = this._toBeMoved.map(element =>
Bound.deserialize(element.xywh)
);
}
this._alignBound = this.snapOverlay.setupAlignables(
this._toBeMoved
);
this._moveContent(delta, this._alignBound);
}
})
);
return;
}
}
override activate(_: Record<string, unknown>): void {
if (this.gfx.selection.lastSurfaceSelections.length) {
this.gfx.selection.set(this.gfx.selection.lastSurfaceSelections);
}
}
override click(e: PointerEventState) {
if (this.doc.readonly) return;
const selected = this._pick(e.x, e.y, {
ignoreTransparent: true,
});
if (selected) {
const { selectedIds, surfaceSelections } = this.edgelessSelectionManager;
const editing = surfaceSelections[0]?.editing ?? false;
// click active canvas text, edgeless text block and note block
if (
selectedIds.length === 1 &&
selectedIds[0] === selected.id &&
editing
) {
// edgeless text block and note block
if (
(isNoteBlock(selected) || isEdgelessTextBlock(selected)) &&
selected.children.length === 0
) {
this._addEmptyParagraphBlock(selected);
}
// canvas text
return;
}
// click non-active edgeless text block and note block, and then enter editing
if (
!selected.isLocked() &&
!e.keys.shift &&
selectedIds.length === 1 &&
(isNoteBlock(selected) || isEdgelessTextBlock(selected)) &&
((selectedIds[0] === selected.id && !editing) ||
(editing && selectedIds[0] !== selected.id))
) {
// issue #1809
// If the previously selected element is a noteBlock and is in an active state,
// then the currently clicked noteBlock should also be in an active state when selected.
this.edgelessSelectionManager.set({
elements: [selected.id],
editing: true,
});
this._edgeless?.updateComplete
.then(() => {
// check if block has children blocks, if not, add a paragraph block and focus on it
if (selected.children.length === 0) {
this._addEmptyParagraphBlock(selected);
} else {
const block = this.std.host.view.getBlock(selected.id);
if (block) {
const rect = block
.querySelector('.affine-block-children-container')!
.getBoundingClientRect();
const offsetY = 8 * this.gfx.viewport.zoom;
const offsetX = 2 * this.gfx.viewport.zoom;
const x = clamp(
e.raw.clientX,
rect.left + offsetX,
rect.right - offsetX
);
const y = clamp(
e.raw.clientY,
rect.top + offsetY,
rect.bottom - offsetY
);
handleNativeRangeAtPoint(x, y);
} else {
handleNativeRangeAtPoint(e.raw.clientX, e.raw.clientY);
}
}
})
.catch(console.error);
return;
}
this.edgelessSelectionManager.set({
// hold shift key to multi select or de-select element
elements: e.keys.shift
? this.edgelessSelectionManager.has(selected.id)
? selectedIds.filter(id => id !== selected.id)
: [...selectedIds, selected.id]
: [selected.id],
editing: false,
});
} else if (!e.keys.shift) {
this.edgelessSelectionManager.clear();
resetNativeSelection(null);
}
this._isDoubleClickedOnMask = false;
this._supportedExts.forEach(ext => ext.click?.(e));
}
override deactivate() {
this._stopAutoPanning();
this._clearDisposable();
this._accumulateDelta = [0, 0];
noop();
}
override doubleClick(e: PointerEventState) {
if (this.doc.readonly) {
const viewport = this.gfx.viewport;
if (viewport.zoom === 1) {
// Fit to Screen
fitToScreen(
[...this.gfx.layer.blocks, ...this.gfx.layer.canvasElements],
this.gfx.viewport
);
} else {
// Zoom to 100% and Center
const [x, y] = viewport.toModelCoord(e.x, e.y);
viewport.setViewport(1, [x, y], true);
}
return;
}
const selected = this._pick(e.x, e.y, {
hitThreshold: 10,
});
if (!this._edgeless) {
return;
}
if (!selected) {
const textFlag = this.doc
.get(FeatureFlagService)
.getFlag('enable_edgeless_text');
if (textFlag) {
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
this.std.command.exec(insertEdgelessTextCommand, { x, y });
} else {
addText(this._edgeless, e);
}
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:dbclick',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'text',
});
return;
} else {
if (selected.isLocked()) return;
const [x, y] = this.gfx.viewport.toModelCoord(e.x, e.y);
if (selected instanceof TextElementModel) {
mountTextElementEditor(selected, this._edgeless, {
x,
y,
});
return;
}
if (selected instanceof ShapeElementModel) {
mountShapeTextEditor(selected, this._edgeless);
return;
}
if (selected instanceof ConnectorElementModel) {
mountConnectorLabelEditor(selected, this._edgeless, [x, y]);
return;
}
if (isFrameBlock(selected)) {
mountFrameTitleEditor(selected, this._edgeless);
return;
}
if (selected instanceof GroupElementModel) {
mountGroupTitleEditor(selected, this._edgeless);
return;
}
}
this._supportedExts.forEach(ext => ext.click?.(e));
if (
e.raw.target &&
e.raw.target instanceof HTMLElement &&
e.raw.target.classList.contains('affine-note-mask')
) {
this.click(e);
this._isDoubleClickedOnMask = true;
return;
}
}
override dragEnd(e: PointerEventState) {
this._extHandlers.forEach(handler => handler.dragEnd?.(e));
this._toBeMoved.forEach(el => {
this.doc.transact(() => {
el.pop('xywh');
});
if (el instanceof ConnectorElementModel) {
el.pop('labelXYWH');
}
});
{
const frameManager = this._frameMgr;
const toBeMovedTopElements = getTopElements(
this._toBeMoved.map(el =>
el.group instanceof MindmapElementModel ? el.group : el
)
);
if (this._hoveredFrame) {
frameManager.addElementsToFrame(
this._hoveredFrame,
toBeMovedTopElements
);
} else {
// only apply to root nodes of trees
toBeMovedTopElements.forEach(element =>
frameManager.removeFromParentFrame(element)
);
}
}
if (this._lock) {
this.doc.captureSync();
this._lock = false;
}
if (this.edgelessSelectionManager.editing) return;
this._selectedBounds = [];
this.snapOverlay.cleanupAlignables();
this.frameOverlay.clear();
this._toBeMoved = [];
this._selectedConnector = null;
this._selectedConnectorLabelBounds = null;
this._clearSelectingState();
this.dragType = DefaultModeDragType.None;
}
override dragMove(e: PointerEventState) {
const { viewport } = this.gfx;
switch (this.dragType) {
case DefaultModeDragType.Selecting: {
// Record the last drag pointer position for auto panning and view port updating
this._updateSelectingState();
const moveDelta = calPanDelta(viewport, e);
if (moveDelta) {
this._startAutoPanning(moveDelta);
} else {
this._stopAutoPanning();
}
break;
}
case DefaultModeDragType.AltCloning:
case DefaultModeDragType.ContentMoving: {
if (
this._toBeMoved.length &&
this._toBeMoved.every(ele => {
return !this._isDraggable(ele);
})
) {
return;
}
if (this._wheeling) {
this._wheeling = false;
}
const dx = this.dragLastPos[0] - this.dragStartPos[0];
const dy = this.dragLastPos[1] - this.dragStartPos[1];
const alignBound = this._alignBound.clone();
const shifted = e.keys.shift || this.gfx.keyboard.shiftKey$.peek();
this._moveContent([dx, dy], alignBound, shifted, true);
this._extHandlers.forEach(handler => handler.dragMove?.(e));
break;
}
case DefaultModeDragType.ConnectorLabelMoving: {
const dx = this.dragLastPos[0] - this.dragStartPos[0];
const dy = this.dragLastPos[1] - this.dragStartPos[1];
this._moveLabel([dx, dy]);
break;
}
case DefaultModeDragType.NativeEditing: {
// TODO reset if drag out of note
break;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
override async dragStart(e: PointerEventState) {
if (this.edgelessSelectionManager.editing) return;
// Determine the drag type based on the current state and event
let dragType = this._determineDragType(e);
const elements = this.edgelessSelectionManager.selectedElements;
if (elements.some(e => e.isLocked())) return;
const toBeMoved = new Set(elements);
elements.forEach(element => {
if (isGfxGroupCompatibleModel(element)) {
element.descendantElements.forEach(ele => {
toBeMoved.add(ele);
});
}
});
this._toBeMoved = Array.from(toBeMoved);
// If alt key is pressed and content is moving, clone the content
if (e.keys.alt && dragType === DefaultModeDragType.ContentMoving) {
dragType = DefaultModeDragType.AltCloning;
await this._cloneContent();
}
this._filterConnectedConnector();
// Connector needs to be updated first
this._toBeMoved.sort((a, _) =>
a instanceof ConnectorElementModel ? -1 : 1
);
// Set up drag state
this.initializeDragState(dragType, e);
// stash the state
this._toBeMoved.forEach(ele => {
ele.stash('xywh');
if (ele instanceof ConnectorElementModel) {
ele.stash('labelXYWH');
}
});
this._extHandlers.forEach(handler => handler.dragStart?.(e));
}
override mounted() {
this.disposable.add(
effect(() => {
const pressed = this.gfx.keyboard.spaceKey$.value;
if (pressed) {
const currentDraggingArea = this.controller.draggingViewArea$.peek();
this._selectionRectTransition = {
w: currentDraggingArea.w,
h: currentDraggingArea.h,
startX: currentDraggingArea.startX,
startY: currentDraggingArea.startY,
endX: currentDraggingArea.endX,
endY: currentDraggingArea.endY,
};
} else {
this._selectionRectTransition = null;
}
})
);
this._exts = [MindMapExt, CanvasElementEventExt].map(
constructor => new constructor(this)
);
this._exts.forEach(ext => ext.mounted());
}
override pointerDown(e: PointerEventState): void {
this._supportedExts.forEach(ext => ext.pointerDown(e));
}
override pointerMove(e: PointerEventState) {
const hovered = this._pick(e.x, e.y, {
hitThreshold: 10,
});
if (
isFrameBlock(hovered) &&
hovered.externalBound?.isPointInBound(
this.gfx.viewport.toModelCoord(e.x, e.y)
)
) {
this.frameOverlay.highlight(hovered);
} else {
this.frameOverlay.clear();
}
this._supportedExts.forEach(ext => ext.pointerMove(e));
}
override pointerUp(e: PointerEventState) {
this._supportedExts.forEach(ext => ext.pointerUp(e));
}
override tripleClick() {
if (this._isDoubleClickedOnMask) return;
}
override unmounted(): void {
this._exts.forEach(ext => ext.unmounted());
}
}
declare module '@blocksuite/block-std/gfx' {
interface GfxToolsMap {
default: DefaultTool;
}
}