mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
feat: add ElementTransformManager for edgeless element basic manipulation (#10824)
### Overview:
We've been working with some legacy code in the default-tool and edgeless-selected-rect modules, which are responsible for fundamental operations like moving, resizing, and rotating elements. Currently, these operations are hardcoded, making it challenging to extend functionalities without diving deep into the code.
### What's Changing:
Introducing `ElementTransformManager` to streamline the handling of basic transformations (move, resize, rotate) while allowing the business logic to dictate when these actions occur.
Providing two ways to extend the transformations behaviour:
- Extends inside element view definition: Elements can decide how to handle move/resize events, such as enforcing size constraints.
- Extension mechanism provided by this manager: Adjust or completely override default drag behaviors, like snapping elements into alignment.
### Code Examples:
Delegate element movement to TransformManager:
```typescript
class DefaultTool {
override dragStart(event) {
if(this.dragType === DragType.ContentMoving) {
const transformManager = this.std.get(TransformManagerIdentifier);
transformManager.startDrag({ selectedElements, event });
}
}
}
```
Enforce minimum width inside view definition:
```typescript
class EdgelessNoteBlock extends GfxBlockComponent {
onResizeDelta({ dw, dh }) {
const bound = this.model.elementBound;
bound.w = Math.min(MAX_WIDTH, bound.w + dw);
bound.h = Math.min(MAX_HEIGHT, bound.h + dh);
this.model.xywh = bound.serialize();
}
}
```
Use extension to implement element snapping:
```typescript
import { TransformerExtension } from '@blocksuite/std/gfx';
// Just extends the TransformerExtension
class SnapManager extends TransformerExtension {
static override key = 'snap-manager';
onDragInitialize() {
return {
onDragMove(context) {
const { dx, dy } = this.getAlignmentMoveDistance(context.elements);
context.dx = dx;
context.dy = dy;
}
}
}
}
```
### Others
The migration will be divided into several PRs. This PR mostly focus on refactoring elements movement part of `default-tool`.
- Delegate elements movement to `TransformManager`
- Rewrite the default tool extension into `TransformManager` extension
- Add drag handler interface to gfx view (both `GfxBlockComponent` and `GfxElementModelView`) to allow element to define how it gonna react on drag
This commit is contained in:
@@ -1,14 +1,22 @@
|
|||||||
import { PresentTool } from '@blocksuite/affine-block-frame';
|
import { PresentTool } from '@blocksuite/affine-block-frame';
|
||||||
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
|
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
|
||||||
import { TextTool } from '@blocksuite/affine-gfx-text';
|
import { TextTool } from '@blocksuite/affine-gfx-text';
|
||||||
|
import {
|
||||||
|
CanvasEventHandler,
|
||||||
|
ElementTransformManager,
|
||||||
|
} from '@blocksuite/block-std/gfx';
|
||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
|
|
||||||
import { EdgelessElementToolbarExtension } from './configs/toolbar';
|
import { EdgelessElementToolbarExtension } from './configs/toolbar';
|
||||||
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
|
import { EdgelessRootBlockSpec } from './edgeless-root-spec.js';
|
||||||
|
import { ConnectorFilter } from './element-transform/connector-filter.js';
|
||||||
|
import { FrameHighlightManager } from './element-transform/frame-highlight-manager.js';
|
||||||
|
import { MindMapDragExtension } from './element-transform/mind-map-drag.js';
|
||||||
|
import { SnapExtension } from './element-transform/snap-manager.js';
|
||||||
|
import { MindMapIndicatorOverlay } from './element-transform/utils/indicator-overlay.js';
|
||||||
import { BrushTool } from './gfx-tool/brush-tool.js';
|
import { BrushTool } from './gfx-tool/brush-tool.js';
|
||||||
import { ConnectorTool } from './gfx-tool/connector-tool.js';
|
import { ConnectorTool } from './gfx-tool/connector-tool.js';
|
||||||
import { DefaultTool } from './gfx-tool/default-tool.js';
|
import { DefaultTool } from './gfx-tool/default-tool.js';
|
||||||
import { MindMapIndicatorOverlay } from './gfx-tool/default-tool-ext/mind-map-ext/indicator-overlay.js';
|
|
||||||
import { EmptyTool } from './gfx-tool/empty-tool.js';
|
import { EmptyTool } from './gfx-tool/empty-tool.js';
|
||||||
import { EraserTool } from './gfx-tool/eraser-tool.js';
|
import { EraserTool } from './gfx-tool/eraser-tool.js';
|
||||||
import { FrameTool } from './gfx-tool/frame-tool.js';
|
import { FrameTool } from './gfx-tool/frame-tool.js';
|
||||||
@@ -18,7 +26,7 @@ import { PanTool } from './gfx-tool/pan-tool.js';
|
|||||||
import { ShapeTool } from './gfx-tool/shape-tool.js';
|
import { ShapeTool } from './gfx-tool/shape-tool.js';
|
||||||
import { TemplateTool } from './gfx-tool/template-tool.js';
|
import { TemplateTool } from './gfx-tool/template-tool.js';
|
||||||
import { EditPropsMiddlewareBuilder } from './middlewares/base.js';
|
import { EditPropsMiddlewareBuilder } from './middlewares/base.js';
|
||||||
import { SnapManager } from './utils/snap-manager.js';
|
import { SnapOverlay } from './utils/snap-manager.js';
|
||||||
|
|
||||||
export const EdgelessToolExtension: ExtensionType[] = [
|
export const EdgelessToolExtension: ExtensionType[] = [
|
||||||
DefaultTool,
|
DefaultTool,
|
||||||
@@ -36,10 +44,19 @@ export const EdgelessToolExtension: ExtensionType[] = [
|
|||||||
PresentTool,
|
PresentTool,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const EdgelessEditExtensions: ExtensionType[] = [
|
||||||
|
ElementTransformManager,
|
||||||
|
ConnectorFilter,
|
||||||
|
SnapExtension,
|
||||||
|
CanvasEventHandler,
|
||||||
|
MindMapDragExtension,
|
||||||
|
FrameHighlightManager,
|
||||||
|
];
|
||||||
|
|
||||||
export const EdgelessBuiltInManager: ExtensionType[] = [
|
export const EdgelessBuiltInManager: ExtensionType[] = [
|
||||||
ConnectionOverlay,
|
ConnectionOverlay,
|
||||||
MindMapIndicatorOverlay,
|
MindMapIndicatorOverlay,
|
||||||
SnapManager,
|
SnapOverlay,
|
||||||
EditPropsMiddlewareBuilder,
|
EditPropsMiddlewareBuilder,
|
||||||
EdgelessElementToolbarExtension,
|
EdgelessElementToolbarExtension,
|
||||||
].flat();
|
].flat();
|
||||||
@@ -48,4 +65,5 @@ export const EdgelessBuiltInSpecs: ExtensionType[] = [
|
|||||||
EdgelessRootBlockSpec,
|
EdgelessRootBlockSpec,
|
||||||
EdgelessToolExtension,
|
EdgelessToolExtension,
|
||||||
EdgelessBuiltInManager,
|
EdgelessBuiltInManager,
|
||||||
|
EdgelessEditExtensions,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
type DragExtensionInitializeContext,
|
||||||
|
TransformExtension,
|
||||||
|
} from '@blocksuite/block-std/gfx';
|
||||||
|
|
||||||
|
export class ConnectorFilter extends TransformExtension {
|
||||||
|
static override key = 'connector-filter';
|
||||||
|
override onDragInitialize(context: DragExtensionInitializeContext) {
|
||||||
|
let hasConnectorFlag = false;
|
||||||
|
|
||||||
|
const elementSet = new Set(context.elements.map(elem => elem.id));
|
||||||
|
const elements = context.elements.filter(elem => {
|
||||||
|
if (elem instanceof ConnectorElementModel) {
|
||||||
|
const sourceElemNotFound =
|
||||||
|
elem.source.id && !elementSet.has(elem.source.id);
|
||||||
|
const targetElemNotFound =
|
||||||
|
elem.target.id && !elementSet.has(elem.target.id);
|
||||||
|
|
||||||
|
// If either source or target element is not found, then remove the connector
|
||||||
|
if (sourceElemNotFound || targetElemNotFound) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasConnectorFlag = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasConnectorFlag) {
|
||||||
|
// connector needs to be updated first
|
||||||
|
elements.sort((a, _) => (a instanceof ConnectorElementModel ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
type EdgelessFrameManager,
|
||||||
|
type FrameOverlay,
|
||||||
|
isFrameBlock,
|
||||||
|
} from '@blocksuite/affine-block-frame';
|
||||||
|
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
|
import {
|
||||||
|
type FrameBlockModel,
|
||||||
|
MindmapElementModel,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
type DragExtensionInitializeContext,
|
||||||
|
type ExtensionDragEndContext,
|
||||||
|
type ExtensionDragMoveContext,
|
||||||
|
type ExtensionDragStartContext,
|
||||||
|
getTopElements,
|
||||||
|
GfxExtensionIdentifier,
|
||||||
|
TransformExtension,
|
||||||
|
} from '@blocksuite/block-std/gfx';
|
||||||
|
|
||||||
|
export class FrameHighlightManager extends TransformExtension {
|
||||||
|
static override key = 'frame-highlight-manager';
|
||||||
|
|
||||||
|
get frameMgr() {
|
||||||
|
return this.std.getOptional(
|
||||||
|
GfxExtensionIdentifier('frame-manager')
|
||||||
|
) as EdgelessFrameManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
get frameHighlightOverlay() {
|
||||||
|
return this.std.getOptional(OverlayIdentifier('frame')) as FrameOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onDragInitialize(_: DragExtensionInitializeContext): {
|
||||||
|
onDragStart?: (context: ExtensionDragStartContext) => void;
|
||||||
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||||
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||||
|
clear?: () => void;
|
||||||
|
} {
|
||||||
|
if (!this.frameMgr || !this.frameHighlightOverlay) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let hoveredFrame: FrameBlockModel | null = null;
|
||||||
|
const { frameMgr, frameHighlightOverlay } = this;
|
||||||
|
let draggedFrames: FrameBlockModel[] = [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDragStart(context) {
|
||||||
|
draggedFrames = context.elements
|
||||||
|
.map(elem => elem.model)
|
||||||
|
.filter(model => isFrameBlock(model));
|
||||||
|
},
|
||||||
|
onDragMove(context) {
|
||||||
|
const { dragLastPos } = context;
|
||||||
|
|
||||||
|
hoveredFrame = frameMgr.getFrameFromPoint(
|
||||||
|
[dragLastPos.x, dragLastPos.y],
|
||||||
|
draggedFrames
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hoveredFrame && !hoveredFrame.isLocked()) {
|
||||||
|
frameHighlightOverlay.highlight(hoveredFrame);
|
||||||
|
} else {
|
||||||
|
frameHighlightOverlay.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd(context) {
|
||||||
|
const topElements = getTopElements(
|
||||||
|
context.elements.map(elem =>
|
||||||
|
elem.model.group instanceof MindmapElementModel
|
||||||
|
? elem.model.group
|
||||||
|
: elem.model
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hoveredFrame) {
|
||||||
|
frameMgr.addElementsToFrame(hoveredFrame, topElements);
|
||||||
|
} else {
|
||||||
|
topElements.forEach(elem => frameMgr.removeFromParentFrame(elem));
|
||||||
|
}
|
||||||
|
|
||||||
|
frameHighlightOverlay.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,39 +11,41 @@ import {
|
|||||||
MindmapElementModel,
|
MindmapElementModel,
|
||||||
type MindmapNode,
|
type MindmapNode,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import type { PointerEventState } from '@blocksuite/block-std';
|
|
||||||
import {
|
import {
|
||||||
|
type DragExtensionInitializeContext,
|
||||||
|
type ExtensionDragEndContext,
|
||||||
|
type ExtensionDragMoveContext,
|
||||||
|
type ExtensionDragStartContext,
|
||||||
type GfxModel,
|
type GfxModel,
|
||||||
type GfxPrimitiveElementModel,
|
type GfxPrimitiveElementModel,
|
||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
|
TransformExtension,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import type { Bound, IVec } from '@blocksuite/global/gfx';
|
import type { Bound, IVec } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
import { isSingleMindMapNode } from '../../../utils/mindmap.js';
|
import { isSingleMindMapNode } from '../utils/mindmap';
|
||||||
import { isMindmapNode } from '../../../utils/query.js';
|
import { isMindmapNode } from '../utils/query';
|
||||||
import { DefaultModeDragType, DefaultToolExt, type DragState } from '../ext.js';
|
import { calculateResponseArea } from './utils/drag-utils';
|
||||||
import { calculateResponseArea } from './drag-utils.js';
|
import type { MindMapIndicatorOverlay } from './utils/indicator-overlay';
|
||||||
import type { MindMapIndicatorOverlay } from './indicator-overlay.js';
|
|
||||||
|
|
||||||
type DragMindMapCtx = {
|
type DragMindMapCtx = {
|
||||||
mindmap: MindmapElementModel;
|
mindmap: MindmapElementModel;
|
||||||
node: MindmapNode;
|
node: MindmapNode;
|
||||||
clear: () => void;
|
|
||||||
/**
|
/**
|
||||||
* Whether the dragged node is the root node of the mind map
|
* Whether the dragged node is the root node of the mind map
|
||||||
*/
|
*/
|
||||||
isRoot: boolean;
|
isRoot: boolean;
|
||||||
originalMindMapBound: Bound;
|
originalMindMapBound: Bound;
|
||||||
startPoint: PointerEventState;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MindMapExt extends DefaultToolExt {
|
export class MindMapDragExtension extends TransformExtension {
|
||||||
|
static override key = 'mind-map-drag';
|
||||||
|
/**
|
||||||
|
* The response area of the mind map is calculated in real time.
|
||||||
|
* It only needs to be calculated once when the mind map is dragged.
|
||||||
|
*/
|
||||||
private readonly _responseAreaUpdated = new Set<MindmapElementModel>();
|
private readonly _responseAreaUpdated = new Set<MindmapElementModel>();
|
||||||
|
|
||||||
override supportedDragTypes: DefaultModeDragType[] = [
|
|
||||||
DefaultModeDragType.ContentMoving,
|
|
||||||
];
|
|
||||||
|
|
||||||
private get _indicatorOverlay() {
|
private get _indicatorOverlay() {
|
||||||
return this.std.getOptional(
|
return this.std.getOptional(
|
||||||
OverlayIdentifier('mindmap-indicator')
|
OverlayIdentifier('mindmap-indicator')
|
||||||
@@ -62,9 +64,8 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
private _createManipulationHandlers(dragMindMapCtx: DragMindMapCtx): {
|
private _createManipulationHandlers(dragMindMapCtx: DragMindMapCtx): {
|
||||||
dragStart?: (evt: PointerEventState) => void;
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||||
dragMove?: (evt: PointerEventState) => void;
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||||
dragEnd?: (evt: PointerEventState) => void;
|
|
||||||
} {
|
} {
|
||||||
let hoveredCtx: {
|
let hoveredCtx: {
|
||||||
mindmap: MindmapElementModel | null;
|
mindmap: MindmapElementModel | null;
|
||||||
@@ -75,8 +76,8 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dragMove: (_: PointerEventState) => {
|
onDragMove: (context: ExtensionDragMoveContext) => {
|
||||||
const [x, y] = this.defaultTool.dragLastPos;
|
const { x, y } = context.dragLastPos;
|
||||||
const hoveredMindMap = this._getHoveredMindMap([x, y], dragMindMapCtx);
|
const hoveredMindMap = this._getHoveredMindMap([x, y], dragMindMapCtx);
|
||||||
const indicator = this._indicatorOverlay;
|
const indicator = this._indicatorOverlay;
|
||||||
|
|
||||||
@@ -166,18 +167,15 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dragEnd: (e: PointerEventState) => {
|
onDragEnd: (dragEndContext: ExtensionDragEndContext) => {
|
||||||
if (hoveredCtx?.merge) {
|
if (hoveredCtx?.merge) {
|
||||||
hoveredCtx.merge();
|
hoveredCtx.merge();
|
||||||
} else {
|
} else {
|
||||||
hoveredCtx?.abort?.();
|
hoveredCtx?.abort?.();
|
||||||
|
|
||||||
if (hoveredCtx?.detach) {
|
if (hoveredCtx?.detach) {
|
||||||
const [startX, startY] = this.gfx.viewport.toModelCoord(
|
const { x: startX, y: startY } = dragEndContext.dragStartPos;
|
||||||
dragMindMapCtx.startPoint.x,
|
const { x: endX, y: endY } = dragEndContext.dragLastPos;
|
||||||
dragMindMapCtx.startPoint.y
|
|
||||||
);
|
|
||||||
const [endX, endY] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
|
||||||
|
|
||||||
dragMindMapCtx.node.element.xywh =
|
dragMindMapCtx.node.element.xywh =
|
||||||
dragMindMapCtx.node.element.elementBound
|
dragMindMapCtx.node.element.elementBound
|
||||||
@@ -204,7 +202,6 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hoveredCtx = null;
|
hoveredCtx = null;
|
||||||
dragMindMapCtx.clear();
|
|
||||||
this._responseAreaUpdated.clear();
|
this._responseAreaUpdated.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -213,24 +210,21 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
/**
|
/**
|
||||||
* Create handlers that can translate entire mind map
|
* Create handlers that can translate entire mind map
|
||||||
*/
|
*/
|
||||||
private _createTranslationHandlers(
|
private _createTranslationHandlers(ctx: {
|
||||||
_: DragState,
|
mindmaps: Set<MindmapElementModel>;
|
||||||
ctx: {
|
nodes: Set<GfxModel>;
|
||||||
mindmaps: Set<MindmapElementModel>;
|
}): {
|
||||||
nodes: Set<GfxModel>;
|
onDragStart?: (context: ExtensionDragStartContext) => void;
|
||||||
}
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||||
): {
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||||
dragStart?: (evt: PointerEventState) => void;
|
|
||||||
dragMove?: (evt: PointerEventState) => void;
|
|
||||||
dragEnd?: (evt: PointerEventState) => void;
|
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
dragStart: (_: PointerEventState) => {
|
onDragStart: () => {
|
||||||
ctx.nodes.forEach(node => {
|
ctx.nodes.forEach(node => {
|
||||||
node.stash('xywh');
|
node.stash('xywh');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
dragEnd: (_: PointerEventState) => {
|
onDragEnd: () => {
|
||||||
ctx.mindmaps.forEach(mindmap => {
|
ctx.mindmaps.forEach(mindmap => {
|
||||||
mindmap.layout();
|
mindmap.layout();
|
||||||
});
|
});
|
||||||
@@ -317,7 +311,7 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
|
|
||||||
private _setupDragNodeImage(
|
private _setupDragNodeImage(
|
||||||
mindmapNode: MindmapNode,
|
mindmapNode: MindmapNode,
|
||||||
event: PointerEventState
|
pos: { x: number; y: number }
|
||||||
) {
|
) {
|
||||||
const surfaceBlock = this.gfx.surfaceComponent as SurfaceBlockComponent;
|
const surfaceBlock = this.gfx.surfaceComponent as SurfaceBlockComponent;
|
||||||
const renderer = surfaceBlock?.renderer;
|
const renderer = surfaceBlock?.renderer;
|
||||||
@@ -329,7 +323,6 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
|
|
||||||
const nodeBound = mindmapNode.element.elementBound;
|
const nodeBound = mindmapNode.element.elementBound;
|
||||||
|
|
||||||
const pos = this.gfx.viewport.toModelCoord(event.x, event.y);
|
|
||||||
const canvas = renderer.getCanvasByBound(
|
const canvas = renderer.getCanvasByBound(
|
||||||
mindmapNode.element.elementBound,
|
mindmapNode.element.elementBound,
|
||||||
[mindmapNode.element],
|
[mindmapNode.element],
|
||||||
@@ -338,7 +331,7 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
indicatorOverlay.dragNodePos = [nodeBound.x - pos[0], nodeBound.y - pos[1]];
|
indicatorOverlay.dragNodePos = [nodeBound.x - pos.x, nodeBound.y - pos.y];
|
||||||
indicatorOverlay.dragNodeImage = canvas;
|
indicatorOverlay.dragNodeImage = canvas;
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -385,14 +378,10 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
override initDrag(dragState: DragState) {
|
override onDragInitialize(context: DragExtensionInitializeContext) {
|
||||||
if (dragState.dragType !== DefaultModeDragType.ContentMoving) {
|
if (isSingleMindMapNode(context.elements)) {
|
||||||
return {};
|
const mindmap = context.elements[0].group as MindmapElementModel;
|
||||||
}
|
const mindmapNode = mindmap.getNode(context.elements[0].id)!;
|
||||||
|
|
||||||
if (isSingleMindMapNode(dragState.movedElements)) {
|
|
||||||
const mindmap = dragState.movedElements[0].group as MindmapElementModel;
|
|
||||||
const mindmapNode = mindmap.getNode(dragState.movedElements[0].id)!;
|
|
||||||
const mindmapBound = mindmap.elementBound;
|
const mindmapBound = mindmap.elementBound;
|
||||||
const isRoot = mindmapNode === mindmap.tree;
|
const isRoot = mindmapNode === mindmap.tree;
|
||||||
|
|
||||||
@@ -405,34 +394,36 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
|
|
||||||
const clearDragStatus = isRoot
|
const clearDragStatus = isRoot
|
||||||
? mindmap.stashTree(mindmapNode)
|
? mindmap.stashTree(mindmapNode)
|
||||||
: this._setupDragNodeImage(mindmapNode, dragState.event);
|
: this._setupDragNodeImage(mindmapNode, context.dragStartPos);
|
||||||
const clearOpacity = this._updateNodeOpacity(mindmap, mindmapNode);
|
const clearOpacity = this._updateNodeOpacity(mindmap, mindmapNode);
|
||||||
|
|
||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
dragState.movedElements.splice(0, 1);
|
context.elements.splice(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mindMapDragCtx: DragMindMapCtx = {
|
const mindMapDragCtx: DragMindMapCtx = {
|
||||||
mindmap,
|
mindmap,
|
||||||
node: mindmapNode,
|
node: mindmapNode,
|
||||||
isRoot,
|
isRoot,
|
||||||
clear: () => {
|
originalMindMapBound: mindmapBound,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...this._createManipulationHandlers(mindMapDragCtx),
|
||||||
|
clear() {
|
||||||
clearOpacity();
|
clearOpacity();
|
||||||
clearDragStatus?.();
|
clearDragStatus?.();
|
||||||
if (!isRoot) {
|
if (!isRoot) {
|
||||||
dragState.movedElements.push(mindmapNode.element);
|
context.elements.push(mindmapNode.element);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
originalMindMapBound: mindmapBound,
|
|
||||||
startPoint: dragState.event,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return this._createManipulationHandlers(mindMapDragCtx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mindmapNodes = new Set<GfxModel>();
|
const mindmapNodes = new Set<GfxModel>();
|
||||||
const mindmaps = new Set<MindmapElementModel>();
|
const mindmaps = new Set<MindmapElementModel>();
|
||||||
dragState.movedElements.forEach(el => {
|
|
||||||
|
context.elements.forEach(el => {
|
||||||
if (isMindmapNode(el)) {
|
if (isMindmapNode(el)) {
|
||||||
const mindmap =
|
const mindmap =
|
||||||
el.group instanceof MindmapElementModel
|
el.group instanceof MindmapElementModel
|
||||||
@@ -452,8 +443,8 @@ export class MindMapExt extends DefaultToolExt {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (mindmapNodes.size > 1) {
|
if (mindmapNodes.size > 1) {
|
||||||
mindmapNodes.forEach(node => dragState.movedElements.push(node));
|
mindmapNodes.forEach(node => context.elements.push(node));
|
||||||
return this._createTranslationHandlers(dragState, {
|
return this._createTranslationHandlers({
|
||||||
mindmaps,
|
mindmaps,
|
||||||
nodes: mindmapNodes,
|
nodes: mindmapNodes,
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
|
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
type DragExtensionInitializeContext,
|
||||||
|
type ExtensionDragMoveContext,
|
||||||
|
type GfxModel,
|
||||||
|
TransformExtension,
|
||||||
|
} from '@blocksuite/block-std/gfx';
|
||||||
|
import type { Bound } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import type { SnapOverlay } from '../utils/snap-manager';
|
||||||
|
|
||||||
|
export class SnapExtension extends TransformExtension {
|
||||||
|
static override key = 'snap-manager';
|
||||||
|
|
||||||
|
get snapOverlay() {
|
||||||
|
return this.std.getOptional(
|
||||||
|
OverlayIdentifier('snap-manager')
|
||||||
|
) as SnapOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
override onDragInitialize(initContext: DragExtensionInitializeContext) {
|
||||||
|
const snapOverlay = this.snapOverlay;
|
||||||
|
|
||||||
|
if (!snapOverlay) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let alignBound: Bound;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDragStart() {
|
||||||
|
alignBound = snapOverlay.setMovingElements(
|
||||||
|
initContext.elements,
|
||||||
|
initContext.elements.reduce((pre, elem) => {
|
||||||
|
if (elem.group instanceof MindmapElementModel) {
|
||||||
|
pre.push(elem.group);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pre;
|
||||||
|
}, [] as GfxModel[])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDragMove(context: ExtensionDragMoveContext) {
|
||||||
|
if (
|
||||||
|
context.elements.length === 0 ||
|
||||||
|
alignBound.w === 0 ||
|
||||||
|
alignBound.h === 0
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBound = alignBound.moveDelta(context.dx, context.dy);
|
||||||
|
const alignRst = snapOverlay.align(currentBound);
|
||||||
|
|
||||||
|
context.dx = alignRst.dx + context.dx;
|
||||||
|
context.dy = alignRst.dy + context.dy;
|
||||||
|
},
|
||||||
|
onDragEnd() {
|
||||||
|
snapOverlay.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
import type { PointerEventState } from '@blocksuite/block-std';
|
|
||||||
import type { GfxModel } from '@blocksuite/block-std/gfx';
|
|
||||||
|
|
||||||
import type { DefaultTool } from '../default-tool.js';
|
|
||||||
|
|
||||||
export enum DefaultModeDragType {
|
export enum DefaultModeDragType {
|
||||||
/** press alt/option key to clone selected */
|
|
||||||
AltCloning = 'alt-cloning',
|
|
||||||
/** Moving connector label */
|
/** Moving connector label */
|
||||||
ConnectorLabelMoving = 'connector-label-moving',
|
ConnectorLabelMoving = 'connector-label-moving',
|
||||||
/** Moving selected contents */
|
/** Moving selected contents */
|
||||||
@@ -19,45 +12,3 @@ export enum DefaultModeDragType {
|
|||||||
/** Expanding the dragging area, select the content covered inside */
|
/** Expanding the dragging area, select the content covered inside */
|
||||||
Selecting = 'selecting',
|
Selecting = 'selecting',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DragState = {
|
|
||||||
movedElements: GfxModel[];
|
|
||||||
dragType: DefaultModeDragType;
|
|
||||||
event: PointerEventState;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class DefaultToolExt {
|
|
||||||
readonly supportedDragTypes: DefaultModeDragType[] = [];
|
|
||||||
|
|
||||||
get gfx() {
|
|
||||||
return this.defaultTool.gfx;
|
|
||||||
}
|
|
||||||
|
|
||||||
get std() {
|
|
||||||
return this.defaultTool.std;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(protected defaultTool: DefaultTool) {}
|
|
||||||
|
|
||||||
click(_evt: PointerEventState) {}
|
|
||||||
|
|
||||||
dblClick(_evt: PointerEventState) {}
|
|
||||||
|
|
||||||
initDrag(_: DragState): {
|
|
||||||
dragStart?: (evt: PointerEventState) => void;
|
|
||||||
dragMove?: (evt: PointerEventState) => void;
|
|
||||||
dragEnd?: (evt: PointerEventState) => void;
|
|
||||||
} {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
mounted() {}
|
|
||||||
|
|
||||||
pointerDown(_evt: PointerEventState) {}
|
|
||||||
|
|
||||||
pointerMove(_evt: PointerEventState) {}
|
|
||||||
|
|
||||||
pointerUp(_evt: PointerEventState) {}
|
|
||||||
|
|
||||||
unmounted() {}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
import { insertEdgelessTextCommand } from '@blocksuite/affine-block-edgeless-text';
|
||||||
import {
|
import {
|
||||||
EdgelessFrameManagerIdentifier,
|
|
||||||
type FrameOverlay,
|
type FrameOverlay,
|
||||||
isFrameBlock,
|
isFrameBlock,
|
||||||
} from '@blocksuite/affine-block-frame';
|
} from '@blocksuite/affine-block-frame';
|
||||||
@@ -12,7 +11,6 @@ import {
|
|||||||
import { addText, mountTextElementEditor } from '@blocksuite/affine-gfx-text';
|
import { addText, mountTextElementEditor } from '@blocksuite/affine-gfx-text';
|
||||||
import type {
|
import type {
|
||||||
EdgelessTextBlockModel,
|
EdgelessTextBlockModel,
|
||||||
FrameBlockModel,
|
|
||||||
NoteBlockModel,
|
NoteBlockModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
@@ -35,11 +33,10 @@ import type { PointerEventState } from '@blocksuite/block-std';
|
|||||||
import {
|
import {
|
||||||
BaseTool,
|
BaseTool,
|
||||||
getTopElements,
|
getTopElements,
|
||||||
type GfxBlockElementModel,
|
|
||||||
type GfxModel,
|
type GfxModel,
|
||||||
type GfxPrimitiveElementModel,
|
|
||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
type PointTestOptions,
|
type PointTestOptions,
|
||||||
|
TransformManagerIdentifier,
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import type { IVec } from '@blocksuite/global/gfx';
|
import type { IVec } from '@blocksuite/global/gfx';
|
||||||
@@ -53,25 +50,19 @@ import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
|
|||||||
import { prepareCloneData } from '../utils/clone-utils.js';
|
import { prepareCloneData } from '../utils/clone-utils.js';
|
||||||
import { calPanDelta } from '../utils/panning-utils.js';
|
import { calPanDelta } from '../utils/panning-utils.js';
|
||||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||||
import type { SnapManager } from '../utils/snap-manager.js';
|
|
||||||
import {
|
import {
|
||||||
mountConnectorLabelEditor,
|
mountConnectorLabelEditor,
|
||||||
mountFrameTitleEditor,
|
mountFrameTitleEditor,
|
||||||
mountGroupTitleEditor,
|
mountGroupTitleEditor,
|
||||||
mountShapeTextEditor,
|
mountShapeTextEditor,
|
||||||
} from '../utils/text.js';
|
} from '../utils/text.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 { 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 {
|
export class DefaultTool extends BaseTool {
|
||||||
static override toolName: string = 'default';
|
static override toolName: string = 'default';
|
||||||
|
|
||||||
private _accumulateDelta: IVec = [0, 0];
|
private _accumulateDelta: IVec = [0, 0];
|
||||||
|
|
||||||
private _alignBound = new Bound();
|
|
||||||
|
|
||||||
private _autoPanTimer: number | null = null;
|
private _autoPanTimer: number | null = null;
|
||||||
|
|
||||||
private readonly _clearDisposable = () => {
|
private readonly _clearDisposable = () => {
|
||||||
@@ -84,42 +75,19 @@ export class DefaultTool extends BaseTool {
|
|||||||
private readonly _clearSelectingState = () => {
|
private readonly _clearSelectingState = () => {
|
||||||
this._stopAutoPanning();
|
this._stopAutoPanning();
|
||||||
this._clearDisposable();
|
this._clearDisposable();
|
||||||
|
|
||||||
this._wheeling = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private _disposables: DisposableGroup | null = null;
|
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.
|
// Do not select the text, when click again after activating the note.
|
||||||
private _isDoubleClickedOnMask = false;
|
private _isDoubleClickedOnMask = false;
|
||||||
|
|
||||||
private _lock = false;
|
|
||||||
|
|
||||||
private readonly _panViewport = (delta: IVec) => {
|
private readonly _panViewport = (delta: IVec) => {
|
||||||
this._accumulateDelta[0] += delta[0];
|
this._accumulateDelta[0] += delta[0];
|
||||||
this._accumulateDelta[1] += delta[1];
|
this._accumulateDelta[1] += delta[1];
|
||||||
this.gfx.viewport.applyDeltaCenter(delta[0], 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
|
// For moving the connector label
|
||||||
private _selectedConnector: ConnectorElementModel | null = null;
|
private _selectedConnector: ConnectorElementModel | null = null;
|
||||||
|
|
||||||
@@ -219,8 +187,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private _wheeling = false;
|
|
||||||
|
|
||||||
dragType = DefaultModeDragType.None;
|
dragType = DefaultModeDragType.None;
|
||||||
|
|
||||||
enableHover = true;
|
enableHover = true;
|
||||||
@@ -231,16 +197,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
return (block as EdgelessRootBlockComponent) ?? null;
|
return (block as EdgelessRootBlockComponent) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _frameMgr() {
|
|
||||||
return this.std.get(EdgelessFrameManagerIdentifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 the end position of the dragging area in the model coordinate
|
||||||
*/
|
*/
|
||||||
@@ -263,12 +219,12 @@ export class DefaultTool extends BaseTool {
|
|||||||
return this.gfx.selection;
|
return this.gfx.selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get frameOverlay() {
|
get elementTransformMgr() {
|
||||||
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
|
return this.std.getOptional(TransformManagerIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
get snapOverlay() {
|
private get frameOverlay() {
|
||||||
return this.std.get(OverlayIdentifier('snap-manager')) as SnapManager;
|
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _addEmptyParagraphBlock(
|
private _addEmptyParagraphBlock(
|
||||||
@@ -285,8 +241,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _cloneContent() {
|
private async _cloneContent() {
|
||||||
this._lock = true;
|
|
||||||
|
|
||||||
if (!this._edgeless) return;
|
if (!this._edgeless) return;
|
||||||
|
|
||||||
const clipboardController = this._edgeless?.clipboardController;
|
const clipboardController = this._edgeless?.clipboardController;
|
||||||
@@ -374,91 +328,6 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private _moveLabel(delta: IVec) {
|
||||||
const connector = this._selectedConnector;
|
const connector = this._selectedConnector;
|
||||||
let bounds = this._selectedConnectorLabelBounds;
|
let bounds = this._selectedConnectorLabelBounds;
|
||||||
@@ -535,62 +404,15 @@ export class DefaultTool extends BaseTool {
|
|||||||
return tryGetLockedAncestor(result);
|
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(
|
private initializeDragState(
|
||||||
dragType: DefaultModeDragType,
|
dragType: DefaultModeDragType,
|
||||||
event: PointerEventState
|
event: PointerEventState
|
||||||
) {
|
) {
|
||||||
this.dragType = dragType;
|
this.dragType = dragType;
|
||||||
|
|
||||||
const mindmaps: MindmapElementModel[] = this._toBeMoved.reduce(
|
|
||||||
(pre, elem) => {
|
|
||||||
if (
|
|
||||||
elem.group instanceof MindmapElementModel &&
|
|
||||||
!pre.includes(elem.group)
|
|
||||||
) {
|
|
||||||
pre.push(elem.group);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pre;
|
|
||||||
},
|
|
||||||
[] as MindmapElementModel[]
|
|
||||||
);
|
|
||||||
|
|
||||||
this._alignBound = this.snapOverlay.setMovingElements(this._toBeMoved, [
|
|
||||||
...mindmaps,
|
|
||||||
...mindmaps.flatMap(m => m.childElements),
|
|
||||||
]);
|
|
||||||
|
|
||||||
this._clearDisposable();
|
this._clearDisposable();
|
||||||
this._disposables = new DisposableGroup();
|
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 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 the viewport updates when dragging, should update the dragging area and selection
|
||||||
if (this.dragType === DefaultModeDragType.Selecting) {
|
if (this.dragType === DefaultModeDragType.Selecting) {
|
||||||
@@ -609,36 +431,16 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.dragType === DefaultModeDragType.ContentMoving) {
|
if (this.dragType === DefaultModeDragType.ContentMoving) {
|
||||||
this._disposables.add(
|
if (this.elementTransformMgr) {
|
||||||
this.gfx.viewport.viewportMoved.subscribe(delta => {
|
this.doc.captureSync();
|
||||||
if (
|
this.elementTransformMgr.initializeDrag({
|
||||||
this.dragType === DefaultModeDragType.ContentMoving &&
|
movingElements: this._toBeMoved,
|
||||||
this.controller.dragging$.peek() &&
|
event: event.raw,
|
||||||
!this._autoPanTimer
|
onDragEnd: () => {
|
||||||
) {
|
this.doc.captureSync();
|
||||||
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.setMovingElements(
|
|
||||||
this._toBeMoved
|
|
||||||
);
|
|
||||||
|
|
||||||
this._moveContent(delta, this._alignBound);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,7 +544,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._isDoubleClickedOnMask = false;
|
this._isDoubleClickedOnMask = false;
|
||||||
this._supportedExts.forEach(ext => ext.click?.(e));
|
this.elementTransformMgr?.dispatch('click', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override deactivate() {
|
override deactivate() {
|
||||||
@@ -819,7 +621,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._supportedExts.forEach(ext => ext.click?.(e));
|
this.elementTransformMgr?.dispatch('dblclick', e);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
e.raw.target &&
|
e.raw.target &&
|
||||||
@@ -832,48 +634,9 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragEnd(e: PointerEventState) {
|
override dragEnd() {
|
||||||
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;
|
if (this.edgelessSelectionManager.editing) return;
|
||||||
|
|
||||||
this._selectedBounds = [];
|
|
||||||
this.snapOverlay.clear();
|
|
||||||
this.frameOverlay.clear();
|
this.frameOverlay.clear();
|
||||||
this._toBeMoved = [];
|
this._toBeMoved = [];
|
||||||
this._selectedConnector = null;
|
this._selectedConnector = null;
|
||||||
@@ -897,28 +660,7 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DefaultModeDragType.AltCloning:
|
|
||||||
case DefaultModeDragType.ContentMoving: {
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case DefaultModeDragType.ConnectorLabelMoving: {
|
case DefaultModeDragType.ConnectorLabelMoving: {
|
||||||
@@ -956,30 +698,12 @@ export class DefaultTool extends BaseTool {
|
|||||||
this._toBeMoved = Array.from(toBeMoved);
|
this._toBeMoved = Array.from(toBeMoved);
|
||||||
|
|
||||||
// If alt key is pressed and content is moving, clone the content
|
// If alt key is pressed and content is moving, clone the content
|
||||||
if (e.keys.alt && dragType === DefaultModeDragType.ContentMoving) {
|
if (dragType === DefaultModeDragType.ContentMoving && e.keys.alt) {
|
||||||
dragType = DefaultModeDragType.AltCloning;
|
|
||||||
await this._cloneContent();
|
await this._cloneContent();
|
||||||
}
|
}
|
||||||
this._filterConnectedConnector();
|
|
||||||
|
|
||||||
// Connector needs to be updated first
|
|
||||||
this._toBeMoved.sort((a, _) =>
|
|
||||||
a instanceof ConnectorElementModel ? -1 : 1
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set up drag state
|
// Set up drag state
|
||||||
this.initializeDragState(dragType, e);
|
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() {
|
override mounted() {
|
||||||
@@ -1003,15 +727,10 @@ export class DefaultTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this._exts = [MindMapExt, CanvasElementEventExt].map(
|
|
||||||
constructor => new constructor(this)
|
|
||||||
);
|
|
||||||
this._exts.forEach(ext => ext.mounted());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override pointerDown(e: PointerEventState): void {
|
override pointerDown(e: PointerEventState): void {
|
||||||
this._supportedExts.forEach(ext => ext.pointerDown(e));
|
this.elementTransformMgr?.dispatch('pointerdown', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override pointerMove(e: PointerEventState) {
|
override pointerMove(e: PointerEventState) {
|
||||||
@@ -1030,20 +749,18 @@ export class DefaultTool extends BaseTool {
|
|||||||
this.frameOverlay.clear();
|
this.frameOverlay.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
this._supportedExts.forEach(ext => ext.pointerMove(e));
|
this.elementTransformMgr?.dispatch('pointermove', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override pointerUp(e: PointerEventState) {
|
override pointerUp(e: PointerEventState) {
|
||||||
this._supportedExts.forEach(ext => ext.pointerUp(e));
|
this.elementTransformMgr?.dispatch('pointerup', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
override tripleClick() {
|
override tripleClick() {
|
||||||
if (this._isDoubleClickedOnMask) return;
|
if (this._isDoubleClickedOnMask) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
override unmounted(): void {
|
override unmounted(): void {}
|
||||||
this._exts.forEach(ext => ext.unmounted());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@blocksuite/block-std/gfx' {
|
declare module '@blocksuite/block-std/gfx' {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const ALIGN_THRESHOLD = 8;
|
|||||||
const DISTRIBUTION_LINE_OFFSET = 1;
|
const DISTRIBUTION_LINE_OFFSET = 1;
|
||||||
const STROKE_WIDTH = 2;
|
const STROKE_WIDTH = 2;
|
||||||
|
|
||||||
export class SnapManager extends Overlay {
|
export class SnapOverlay extends Overlay {
|
||||||
static override overlayName: string = 'snap-manager';
|
static override overlayName: string = 'snap-manager';
|
||||||
|
|
||||||
private _skippedElements: Set<GfxModel> = new Set();
|
private _skippedElements: Set<GfxModel> = new Set();
|
||||||
@@ -76,8 +76,6 @@ export class SnapManager extends Overlay {
|
|||||||
};
|
};
|
||||||
|
|
||||||
override clear() {
|
override clear() {
|
||||||
super.clear();
|
|
||||||
|
|
||||||
this._referenceBounds = {
|
this._referenceBounds = {
|
||||||
vertical: [],
|
vertical: [],
|
||||||
horizontal: [],
|
horizontal: [],
|
||||||
@@ -89,6 +87,8 @@ export class SnapManager extends Overlay {
|
|||||||
};
|
};
|
||||||
this._distributedAlignLines = [];
|
this._distributedAlignLines = [];
|
||||||
this._skippedElements.clear();
|
this._skippedElements.clear();
|
||||||
|
|
||||||
|
super.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _alignDistributeHorizontally(
|
private _alignDistributeHorizontally(
|
||||||
|
|||||||
@@ -12,17 +12,19 @@ import {
|
|||||||
} from './extensions';
|
} from './extensions';
|
||||||
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
|
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
|
||||||
import { SurfaceBlockService } from './surface-service';
|
import { SurfaceBlockService } from './surface-service';
|
||||||
|
import { ConnectorElementView } from './view/connector';
|
||||||
import { MindMapView } from './view/mindmap';
|
import { MindMapView } from './view/mindmap';
|
||||||
|
|
||||||
const CommonSurfaceBlockSpec: ExtensionType[] = [
|
const CommonSurfaceBlockSpec: ExtensionType[] = [
|
||||||
FlavourExtension('affine:surface'),
|
FlavourExtension('affine:surface'),
|
||||||
SurfaceBlockService,
|
SurfaceBlockService,
|
||||||
MindMapView,
|
|
||||||
EdgelessCRUDExtension,
|
EdgelessCRUDExtension,
|
||||||
EdgelessLegacySlotExtension,
|
EdgelessLegacySlotExtension,
|
||||||
ExportManagerExtension,
|
ExportManagerExtension,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ElementModelViews = [MindMapView, ConnectorElementView];
|
||||||
|
|
||||||
export const PageSurfaceBlockSpec: ExtensionType[] = [
|
export const PageSurfaceBlockSpec: ExtensionType[] = [
|
||||||
...CommonSurfaceBlockSpec,
|
...CommonSurfaceBlockSpec,
|
||||||
...SurfaceBlockAdapterExtensions,
|
...SurfaceBlockAdapterExtensions,
|
||||||
@@ -32,5 +34,6 @@ export const PageSurfaceBlockSpec: ExtensionType[] = [
|
|||||||
export const EdgelessSurfaceBlockSpec: ExtensionType[] = [
|
export const EdgelessSurfaceBlockSpec: ExtensionType[] = [
|
||||||
...CommonSurfaceBlockSpec,
|
...CommonSurfaceBlockSpec,
|
||||||
...EdgelessSurfaceBlockAdapterExtensions,
|
...EdgelessSurfaceBlockAdapterExtensions,
|
||||||
|
...ElementModelViews,
|
||||||
BlockViewExtension('affine:surface', literal`affine-surface`),
|
BlockViewExtension('affine:surface', literal`affine-surface`),
|
||||||
];
|
];
|
||||||
|
|||||||
27
blocksuite/affine/blocks/block-surface/src/view/connector.ts
Normal file
27
blocksuite/affine/blocks/block-surface/src/view/connector.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||||
|
import {
|
||||||
|
type DragEndContext,
|
||||||
|
type DragMoveContext,
|
||||||
|
type DragStartContext,
|
||||||
|
GfxElementModelView,
|
||||||
|
} from '@blocksuite/block-std/gfx';
|
||||||
|
|
||||||
|
export class ConnectorElementView extends GfxElementModelView<ConnectorElementModel> {
|
||||||
|
static override type = 'connector';
|
||||||
|
|
||||||
|
override onDragStart = (context: DragStartContext) => {
|
||||||
|
super.onDragStart(context);
|
||||||
|
this.model.stash('labelXYWH');
|
||||||
|
};
|
||||||
|
|
||||||
|
override onDragEnd = (context: DragEndContext) => {
|
||||||
|
super.onDragEnd(context);
|
||||||
|
this.model.stash('labelXYWH');
|
||||||
|
};
|
||||||
|
|
||||||
|
override onDragMove = (context: DragMoveContext) => {
|
||||||
|
const { dx, dy, currentBound } = context;
|
||||||
|
|
||||||
|
this.model.moveTo(currentBound.moveDelta(dx, dy));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -181,6 +181,15 @@ export class GfxController extends LifeCycleWatcher {
|
|||||||
return last(picked) ?? null;
|
return last(picked) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the top element in the given point.
|
||||||
|
* If the element is in a group, the group will be returned.
|
||||||
|
* If the group is currently selected, the child element will be returned.
|
||||||
|
* @param x
|
||||||
|
* @param y
|
||||||
|
* @param options
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
getElementInGroup(
|
getElementInGroup(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Bound } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import type { GfxBlockComponent } from '../../view';
|
||||||
|
import type { GfxModel } from '../model/model';
|
||||||
|
import type { GfxElementModelView } from '../view/view';
|
||||||
|
|
||||||
|
export type DragInitializationOption = {
|
||||||
|
movingElements: GfxModel[];
|
||||||
|
event: PointerEvent | MouseEvent;
|
||||||
|
onDragEnd?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DragExtensionInitializeContext = {
|
||||||
|
/**
|
||||||
|
* The elements that are being dragged.
|
||||||
|
* The extension can modify this array to add or remove dragging elements.
|
||||||
|
*/
|
||||||
|
elements: GfxModel[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent the default drag behavior. The following drag events will not be triggered.
|
||||||
|
*/
|
||||||
|
preventDefault: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start position of the drag in model space.
|
||||||
|
*/
|
||||||
|
dragStartPos: Readonly<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionBaseEvent = {
|
||||||
|
/**
|
||||||
|
* The elements that respond to the event.
|
||||||
|
*/
|
||||||
|
elements: {
|
||||||
|
view: GfxBlockComponent | GfxElementModelView;
|
||||||
|
originalBound: Bound;
|
||||||
|
model: GfxModel;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mouse event
|
||||||
|
*/
|
||||||
|
event: PointerEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start position of the drag in model space.
|
||||||
|
*/
|
||||||
|
dragStartPos: Readonly<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last position of the drag in model space.
|
||||||
|
*/
|
||||||
|
dragLastPos: Readonly<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionDragStartContext = ExtensionBaseEvent;
|
||||||
|
|
||||||
|
export type ExtensionDragMoveContext = ExtensionBaseEvent & {
|
||||||
|
dx: number;
|
||||||
|
dy: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExtensionDragEndContext = ExtensionDragMoveContext;
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import type { PointerEventState } from '@blocksuite/block-std';
|
|
||||||
import type { GfxElementModelView } from '@blocksuite/block-std/gfx';
|
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import last from 'lodash-es/last';
|
import last from 'lodash-es/last';
|
||||||
|
|
||||||
import { DefaultModeDragType, DefaultToolExt } from './ext.js';
|
import type { PointerEventState } from '../../../event';
|
||||||
|
import type { GfxElementModelView } from '../../view/view';
|
||||||
|
import { TransformExtension } from '../transform-manager';
|
||||||
|
|
||||||
|
export class CanvasEventHandler extends TransformExtension {
|
||||||
|
static override key = 'canvas-event-handler';
|
||||||
|
|
||||||
export class CanvasElementEventExt extends DefaultToolExt {
|
|
||||||
private _currentStackedElm: GfxElementModelView[] = [];
|
private _currentStackedElm: GfxElementModelView[] = [];
|
||||||
|
|
||||||
override supportedDragTypes: DefaultModeDragType[] = [
|
|
||||||
DefaultModeDragType.None,
|
|
||||||
];
|
|
||||||
|
|
||||||
private _callInReverseOrder(
|
private _callInReverseOrder(
|
||||||
callback: (view: GfxElementModelView) => void,
|
callback: (view: GfxElementModelView) => void,
|
||||||
arr = this._currentStackedElm
|
arr = this._currentStackedElm
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
import {
|
||||||
|
type Container,
|
||||||
|
createIdentifier,
|
||||||
|
type ServiceIdentifier,
|
||||||
|
} from '@blocksuite/global/di';
|
||||||
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
|
import { Bound, Point } from '@blocksuite/global/gfx';
|
||||||
|
import { Extension } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { PointerEventState } from '../../event/state/pointer.js';
|
||||||
|
import { type GfxController } from '../controller.js';
|
||||||
|
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||||
|
import { GfxControllerIdentifier } from '../identifiers.js';
|
||||||
|
import { type SupportedEvent } from '../view/view.js';
|
||||||
|
import type {
|
||||||
|
DragExtensionInitializeContext,
|
||||||
|
DragInitializationOption,
|
||||||
|
ExtensionDragEndContext,
|
||||||
|
ExtensionDragMoveContext,
|
||||||
|
ExtensionDragStartContext,
|
||||||
|
} from './drag.js';
|
||||||
|
|
||||||
|
type ExtensionPointerHandler = Exclude<
|
||||||
|
SupportedEvent,
|
||||||
|
'pointerleave' | 'pointerenter'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const TransformManagerIdentifier = GfxExtensionIdentifier(
|
||||||
|
'element-transform-manager'
|
||||||
|
) as ServiceIdentifier<ElementTransformManager>;
|
||||||
|
|
||||||
|
const CAMEL_CASE_MAP: {
|
||||||
|
[key in ExtensionPointerHandler]: keyof Pick<
|
||||||
|
TransformExtension,
|
||||||
|
'click' | 'dblClick' | 'pointerDown' | 'pointerMove' | 'pointerUp'
|
||||||
|
>;
|
||||||
|
} = {
|
||||||
|
click: 'click',
|
||||||
|
dblclick: 'dblClick',
|
||||||
|
pointerdown: 'pointerDown',
|
||||||
|
pointermove: 'pointerMove',
|
||||||
|
pointerup: 'pointerUp',
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ElementTransformManager extends GfxExtension {
|
||||||
|
static override key = 'element-transform-manager';
|
||||||
|
|
||||||
|
private readonly _disposable = new DisposableGroup();
|
||||||
|
|
||||||
|
override mounted(): void {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
override unmounted(): void {
|
||||||
|
this._disposable.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
get transformExtensions() {
|
||||||
|
return this.std.provider.getAll(TransformExtensionIdentifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
get keyboard() {
|
||||||
|
return this.gfx.keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _safeExecute(fn: () => void, errorMessage: string) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(errorMessage, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
||||||
|
const transformExtensions = this.transformExtensions;
|
||||||
|
|
||||||
|
transformExtensions.forEach(ext => {
|
||||||
|
const handlerMethodName = CAMEL_CASE_MAP[eventName];
|
||||||
|
|
||||||
|
if (ext[handlerMethodName]) {
|
||||||
|
this._safeExecute(() => {
|
||||||
|
ext[handlerMethodName](evt);
|
||||||
|
}, `Error while executing extension \`${handlerMethodName}\` handler`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDrag(options: DragInitializationOption) {
|
||||||
|
let cancelledByExt = false;
|
||||||
|
|
||||||
|
const context: DragExtensionInitializeContext = {
|
||||||
|
/**
|
||||||
|
* The elements that are being dragged
|
||||||
|
*/
|
||||||
|
elements: options.movingElements,
|
||||||
|
|
||||||
|
preventDefault: () => {
|
||||||
|
cancelledByExt = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
dragStartPos: Point.from(
|
||||||
|
this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
|
options.event.x,
|
||||||
|
options.event.y,
|
||||||
|
])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const extension = this.transformExtensions;
|
||||||
|
const activeExtensionHandlers = Array.from(
|
||||||
|
extension.values().map(ext => {
|
||||||
|
return ext.onDragInitialize(context);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cancelledByExt) {
|
||||||
|
activeExtensionHandlers.forEach(handler => handler.clear?.());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = this.std.host;
|
||||||
|
const { event } = options;
|
||||||
|
const internal = {
|
||||||
|
elements: context.elements.map(model => {
|
||||||
|
return {
|
||||||
|
view: this.gfx.view.get(model)!,
|
||||||
|
originalBound: Bound.deserialize(model.xywh),
|
||||||
|
model: model,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
dragStartPos: Point.from(
|
||||||
|
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let dragLastPos = internal.dragStartPos;
|
||||||
|
let lastEvent = event;
|
||||||
|
|
||||||
|
const viewportWatcher = this.gfx.viewport.viewportMoved.subscribe(() => {
|
||||||
|
onDragMove(lastEvent as PointerEvent);
|
||||||
|
});
|
||||||
|
const onDragMove = (event: PointerEvent) => {
|
||||||
|
dragLastPos = Point.from(
|
||||||
|
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
||||||
|
);
|
||||||
|
|
||||||
|
const moveContext: ExtensionDragMoveContext = {
|
||||||
|
...internal,
|
||||||
|
event,
|
||||||
|
dragLastPos,
|
||||||
|
dx: dragLastPos.x - internal.dragStartPos.x,
|
||||||
|
dy: dragLastPos.y - internal.dragStartPos.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
// If shift key is pressed, restrict the movement to one direction
|
||||||
|
if (this.keyboard.shiftKey$.peek()) {
|
||||||
|
const angle = Math.abs(Math.atan2(moveContext.dy, moveContext.dx));
|
||||||
|
const direction =
|
||||||
|
angle < Math.PI / 4 || angle > 3 * (Math.PI / 4) ? 'dy' : 'dx';
|
||||||
|
|
||||||
|
moveContext[direction] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._safeExecute(() => {
|
||||||
|
activeExtensionHandlers.forEach(handler =>
|
||||||
|
handler.onDragMove?.(moveContext)
|
||||||
|
);
|
||||||
|
}, 'Error while executing extension `onDragMove`');
|
||||||
|
|
||||||
|
internal.elements.forEach(element => {
|
||||||
|
const { view, originalBound } = element;
|
||||||
|
|
||||||
|
view.onDragMove({
|
||||||
|
currentBound: originalBound,
|
||||||
|
dx: moveContext.dx,
|
||||||
|
dy: moveContext.dy,
|
||||||
|
elements: internal.elements,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onDragEnd = (event: PointerEvent) => {
|
||||||
|
host.removeEventListener('pointermove', onDragMove, false);
|
||||||
|
host.removeEventListener('pointerup', onDragEnd, false);
|
||||||
|
viewportWatcher.unsubscribe();
|
||||||
|
|
||||||
|
dragLastPos = Point.from(
|
||||||
|
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
||||||
|
);
|
||||||
|
const endContext: ExtensionDragEndContext = {
|
||||||
|
...internal,
|
||||||
|
event,
|
||||||
|
dragLastPos,
|
||||||
|
dx: dragLastPos.x - internal.dragStartPos.x,
|
||||||
|
dy: dragLastPos.y - internal.dragStartPos.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._safeExecute(() => {
|
||||||
|
activeExtensionHandlers.forEach(handler =>
|
||||||
|
handler.onDragEnd?.(endContext)
|
||||||
|
);
|
||||||
|
}, 'Error while executing extension `onDragEnd` handler');
|
||||||
|
|
||||||
|
this.std.store.transact(() => {
|
||||||
|
internal.elements.forEach(element => {
|
||||||
|
const { view, originalBound } = element;
|
||||||
|
|
||||||
|
view.onDragEnd({
|
||||||
|
currentBound: originalBound.moveDelta(endContext.dx, endContext.dy),
|
||||||
|
dx: endContext.dx,
|
||||||
|
dy: endContext.dy,
|
||||||
|
elements: internal.elements,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._safeExecute(() => {
|
||||||
|
activeExtensionHandlers.forEach(handler => handler.clear?.());
|
||||||
|
}, 'Error while executing extension `clear` handler');
|
||||||
|
|
||||||
|
options.onDragEnd?.();
|
||||||
|
};
|
||||||
|
const listenEvent = () => {
|
||||||
|
host.addEventListener('pointermove', onDragMove, false);
|
||||||
|
host.addEventListener('pointerup', onDragEnd, false);
|
||||||
|
};
|
||||||
|
const dragStart = () => {
|
||||||
|
internal.elements.forEach(({ view, originalBound }) => {
|
||||||
|
view.onDragStart({
|
||||||
|
currentBound: originalBound,
|
||||||
|
elements: internal.elements,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dragStartContext: ExtensionDragStartContext = {
|
||||||
|
...internal,
|
||||||
|
event: event as PointerEvent,
|
||||||
|
dragLastPos,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._safeExecute(() => {
|
||||||
|
activeExtensionHandlers.forEach(handler =>
|
||||||
|
handler.onDragStart?.(dragStartContext)
|
||||||
|
);
|
||||||
|
}, 'Error while executing extension `onDragStart` handler');
|
||||||
|
};
|
||||||
|
|
||||||
|
listenEvent();
|
||||||
|
dragStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TransformExtensionIdentifier =
|
||||||
|
createIdentifier<TransformExtension>('element-transform-extension');
|
||||||
|
|
||||||
|
export class TransformExtension extends Extension {
|
||||||
|
static key: string;
|
||||||
|
|
||||||
|
get std() {
|
||||||
|
return this.gfx.std;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(protected readonly gfx: GfxController) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
mounted() {}
|
||||||
|
|
||||||
|
unmounted() {}
|
||||||
|
|
||||||
|
click(_: PointerEventState) {}
|
||||||
|
|
||||||
|
dblClick(_: PointerEventState) {}
|
||||||
|
|
||||||
|
pointerDown(_: PointerEventState) {}
|
||||||
|
|
||||||
|
pointerMove(_: PointerEventState) {}
|
||||||
|
|
||||||
|
pointerUp(_: PointerEventState) {}
|
||||||
|
|
||||||
|
onDragInitialize(_: DragExtensionInitializeContext): {
|
||||||
|
onDragStart?: (context: ExtensionDragStartContext) => void;
|
||||||
|
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||||
|
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||||
|
clear?: () => void;
|
||||||
|
} {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static override setup(di: Container) {
|
||||||
|
if (!this.key) {
|
||||||
|
throw new BlockSuiteError(
|
||||||
|
ErrorCode.ValueNotExists,
|
||||||
|
'key is not defined in the TransformExtension'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
di.add(
|
||||||
|
this as unknown as { new (gfx: GfxController): TransformExtension },
|
||||||
|
[GfxControllerIdentifier]
|
||||||
|
);
|
||||||
|
di.addImpl(TransformExtensionIdentifier(this.key), provider =>
|
||||||
|
provider.get(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { Bound } from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import type { GfxBlockComponent } from '../../view';
|
||||||
|
import type { GfxModel } from '../model/model';
|
||||||
|
import type { GfxElementModelView } from '../view/view';
|
||||||
|
|
||||||
|
export type DragStartContext = {
|
||||||
|
/**
|
||||||
|
* The elements that are being dragged
|
||||||
|
*/
|
||||||
|
elements: {
|
||||||
|
view: GfxBlockComponent | GfxElementModelView;
|
||||||
|
originalBound: Bound;
|
||||||
|
model: GfxModel;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bound of element when drag starts
|
||||||
|
*/
|
||||||
|
currentBound: Bound;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DragMoveContext = DragStartContext & {
|
||||||
|
/**
|
||||||
|
* The delta x of current drag position compared to the start position in model coordinate.
|
||||||
|
*/
|
||||||
|
dx: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delta y of current drag position compared to the start position in model coordinate.
|
||||||
|
*/
|
||||||
|
dy: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DragEndContext = DragMoveContext;
|
||||||
|
|
||||||
|
export type GfxViewTransformInterface = {
|
||||||
|
onDragStart: (context: DragStartContext) => void;
|
||||||
|
onDragMove: (context: DragMoveContext) => void;
|
||||||
|
onDragEnd: (context: DragEndContext) => void;
|
||||||
|
onRotate: (context: {}) => void;
|
||||||
|
onResize: (context: {}) => void;
|
||||||
|
};
|
||||||
@@ -12,6 +12,25 @@ export {
|
|||||||
} from '../utils/tree.js';
|
} from '../utils/tree.js';
|
||||||
export { GfxController } from './controller.js';
|
export { GfxController } from './controller.js';
|
||||||
export type { CursorType, StandardCursor } from './cursor.js';
|
export type { CursorType, StandardCursor } from './cursor.js';
|
||||||
|
export type {
|
||||||
|
DragExtensionInitializeContext,
|
||||||
|
DragInitializationOption,
|
||||||
|
ExtensionDragEndContext,
|
||||||
|
ExtensionDragMoveContext,
|
||||||
|
ExtensionDragStartContext,
|
||||||
|
} from './element-transform/drag.js';
|
||||||
|
export { CanvasEventHandler } from './element-transform/extension/canvas-event-handler.js';
|
||||||
|
export {
|
||||||
|
ElementTransformManager,
|
||||||
|
TransformExtension,
|
||||||
|
TransformExtensionIdentifier,
|
||||||
|
TransformManagerIdentifier,
|
||||||
|
} from './element-transform/transform-manager.js';
|
||||||
|
export type {
|
||||||
|
DragEndContext,
|
||||||
|
DragMoveContext,
|
||||||
|
DragStartContext,
|
||||||
|
} from './element-transform/view-transform.js';
|
||||||
export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
|
export { GfxExtension, GfxExtensionIdentifier } from './extension.js';
|
||||||
export { GridManager } from './grid.js';
|
export { GridManager } from './grid.js';
|
||||||
export { GfxControllerIdentifier } from './identifiers.js';
|
export { GfxControllerIdentifier } from './identifiers.js';
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
|
|
||||||
import { onSurfaceAdded } from '../../utils/gfx.js';
|
import { onSurfaceAdded } from '../../utils/gfx.js';
|
||||||
|
import {
|
||||||
|
type GfxBlockComponent,
|
||||||
|
isGfxBlockComponent,
|
||||||
|
} from '../../view/index.js';
|
||||||
import type { GfxController } from '../controller.js';
|
import type { GfxController } from '../controller.js';
|
||||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||||
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
|
||||||
import type { GfxModel } from '../model/model.js';
|
import type { GfxModel } from '../model/model.js';
|
||||||
import type { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
import type { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
||||||
import type { GfxLocalElementModel } from '../model/surface/local-element-model.js';
|
import type { GfxLocalElementModel } from '../model/surface/local-element-model.js';
|
||||||
@@ -34,20 +37,22 @@ export class ViewManager extends GfxExtension {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get(model: GfxModel | GfxLocalElementModel | string) {
|
get(
|
||||||
if (typeof model === 'string') {
|
model: GfxModel | GfxLocalElementModel | string
|
||||||
if (this._viewMap.has(model)) {
|
): GfxElementModelView | GfxBlockComponent | null {
|
||||||
return this._viewMap.get(model);
|
model = typeof model === 'string' ? model : model.id;
|
||||||
}
|
|
||||||
|
|
||||||
return this.std.view.getBlock(model) ?? null;
|
if (this._viewMap.has(model)) {
|
||||||
} else {
|
return this._viewMap.get(model)!;
|
||||||
if (model instanceof GfxBlockElementModel) {
|
|
||||||
return this.std.view.getBlock(model.id) ?? null;
|
|
||||||
} else {
|
|
||||||
return this._viewMap.get(model.id) ?? null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blockView = this.std.view.getBlock(model);
|
||||||
|
|
||||||
|
if (blockView && isGfxBlockComponent(blockView)) {
|
||||||
|
return blockView;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
override mounted(): void {
|
override mounted(): void {
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ import type { Extension } from '@blocksuite/store';
|
|||||||
|
|
||||||
import type { PointerEventState } from '../../event/index.js';
|
import type { PointerEventState } from '../../event/index.js';
|
||||||
import type { EditorHost } from '../../view/index.js';
|
import type { EditorHost } from '../../view/index.js';
|
||||||
|
import type {
|
||||||
|
DragEndContext,
|
||||||
|
DragMoveContext,
|
||||||
|
DragStartContext,
|
||||||
|
GfxViewTransformInterface,
|
||||||
|
} from '../element-transform/view-transform.js';
|
||||||
import type { GfxController } from '../index.js';
|
import type { GfxController } from '../index.js';
|
||||||
import type { GfxElementGeometry, PointTestOptions } from '../model/base.js';
|
import type { GfxElementGeometry, PointTestOptions } from '../model/base.js';
|
||||||
import type { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
||||||
import type { GfxLocalElementModel } from '../model/surface/local-element-model.js';
|
import type { GfxLocalElementModel } from '../model/surface/local-element-model.js';
|
||||||
|
|
||||||
export type EventsHandlerMap = {
|
export type EventsHandlerMap = {
|
||||||
@@ -33,7 +39,7 @@ export class GfxElementModelView<
|
|||||||
| GfxLocalElementModel,
|
| GfxLocalElementModel,
|
||||||
RendererContext = object,
|
RendererContext = object,
|
||||||
>
|
>
|
||||||
implements GfxElementGeometry, Extension
|
implements GfxElementGeometry, Extension, GfxViewTransformInterface
|
||||||
{
|
{
|
||||||
static type: string;
|
static type: string;
|
||||||
|
|
||||||
@@ -166,6 +172,26 @@ export class GfxElementModelView<
|
|||||||
|
|
||||||
onCreated() {}
|
onCreated() {}
|
||||||
|
|
||||||
|
onDragStart(_: DragStartContext) {
|
||||||
|
if (this.model instanceof GfxPrimitiveElementModel) {
|
||||||
|
this.model.stash('xywh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd(_: DragEndContext) {
|
||||||
|
if (this.model instanceof GfxPrimitiveElementModel) {
|
||||||
|
this.model.pop('xywh');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragMove({ dx, dy, currentBound }: DragMoveContext) {
|
||||||
|
this.model.xywh = currentBound.moveDelta(dx, dy).serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize = () => {};
|
||||||
|
|
||||||
|
onRotate = () => {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the view is destroyed.
|
* Called when the view is destroyed.
|
||||||
* Override this method requires calling `super.onDestroyed()`.
|
* Override this method requires calling `super.onDestroyed()`.
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { computed } from '@preact/signals-core';
|
|||||||
import { nothing } from 'lit';
|
import { nothing } from 'lit';
|
||||||
|
|
||||||
import type { BlockService } from '../../extension/index.js';
|
import type { BlockService } from '../../extension/index.js';
|
||||||
|
import type {
|
||||||
|
DragMoveContext,
|
||||||
|
GfxViewTransformInterface,
|
||||||
|
} from '../../gfx/element-transform/view-transform.js';
|
||||||
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
import { GfxControllerIdentifier } from '../../gfx/identifiers.js';
|
||||||
import type { GfxBlockElementModel } from '../../gfx/index.js';
|
import type { GfxBlockElementModel } from '../../gfx/index.js';
|
||||||
import { SurfaceSelection } from '../../selection/index.js';
|
import { SurfaceSelection } from '../../selection/index.js';
|
||||||
@@ -47,10 +51,13 @@ function handleGfxConnection(instance: GfxBlockComponent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class GfxBlockComponent<
|
export abstract class GfxBlockComponent<
|
||||||
Model extends GfxBlockElementModel = GfxBlockElementModel,
|
Model extends GfxBlockElementModel = GfxBlockElementModel,
|
||||||
Service extends BlockService = BlockService,
|
Service extends BlockService = BlockService,
|
||||||
WidgetName extends string = string,
|
WidgetName extends string = string,
|
||||||
> extends BlockComponent<Model, Service, WidgetName> {
|
>
|
||||||
|
extends BlockComponent<Model, Service, WidgetName>
|
||||||
|
implements GfxViewTransformInterface
|
||||||
|
{
|
||||||
[GfxElementSymbol] = true;
|
[GfxElementSymbol] = true;
|
||||||
|
|
||||||
get gfx() {
|
get gfx() {
|
||||||
@@ -62,6 +69,22 @@ export abstract class GfxBlockComponent<
|
|||||||
handleGfxConnection(this);
|
handleGfxConnection(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDragMove = ({ dx, dy, currentBound }: DragMoveContext) => {
|
||||||
|
this.model.xywh = currentBound.moveDelta(dx, dy).serialize();
|
||||||
|
};
|
||||||
|
|
||||||
|
onDragStart() {
|
||||||
|
this.model.stash('xywh');
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd() {
|
||||||
|
this.model.pop('xywh');
|
||||||
|
}
|
||||||
|
|
||||||
|
onRotate() {}
|
||||||
|
|
||||||
|
onResize() {}
|
||||||
|
|
||||||
getCSSTransform() {
|
getCSSTransform() {
|
||||||
const viewport = this.gfx.viewport;
|
const viewport = this.gfx.viewport;
|
||||||
const { translateX, translateY, zoom } = viewport;
|
const { translateX, translateY, zoom } = viewport;
|
||||||
@@ -151,6 +174,22 @@ export function toGfxBlockComponent<
|
|||||||
return selection.is(SurfaceSelection);
|
return selection.is(SurfaceSelection);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDragMove({ dx, dy, currentBound }: DragMoveContext) {
|
||||||
|
this.model.xywh = currentBound.moveDelta(dx, dy).serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragStart() {
|
||||||
|
this.model.stash('xywh');
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd() {
|
||||||
|
this.model.pop('xywh');
|
||||||
|
}
|
||||||
|
|
||||||
|
onRotate() {}
|
||||||
|
|
||||||
|
onResize() {}
|
||||||
|
|
||||||
get gfx() {
|
get gfx() {
|
||||||
return this.std.get(GfxControllerIdentifier);
|
return this.std.get(GfxControllerIdentifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,10 +207,7 @@ test.describe('frame panel', () => {
|
|||||||
end: { x: number; y: number },
|
end: { x: number; y: number },
|
||||||
comparison: 'greaterThan' | 'lessThan'
|
comparison: 'greaterThan' | 'lessThan'
|
||||||
) {
|
) {
|
||||||
await page.mouse.move(start.x, start.y);
|
await dragBetweenCoords(page, start, end);
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(end.x, end.y);
|
|
||||||
await page.mouse.up();
|
|
||||||
await waitNextFrame(page);
|
await waitNextFrame(page);
|
||||||
|
|
||||||
const newNoteRect = await edgelessNote.boundingBox();
|
const newNoteRect = await edgelessNote.boundingBox();
|
||||||
|
|||||||
@@ -201,11 +201,12 @@ export async function dragView(
|
|||||||
to: IVec,
|
to: IVec,
|
||||||
editorIndex = 0
|
editorIndex = 0
|
||||||
) {
|
) {
|
||||||
|
const steps = 10;
|
||||||
const [x1, y1] = await toViewCoord(page, from, editorIndex);
|
const [x1, y1] = await toViewCoord(page, from, editorIndex);
|
||||||
const [x2, y2] = await toViewCoord(page, to, editorIndex);
|
const [x2, y2] = await toViewCoord(page, to, editorIndex);
|
||||||
await page.mouse.move(x1, y1);
|
await page.mouse.move(x1, y1);
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(x2, y2);
|
await page.mouse.move(x2, y2, { steps });
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user