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 { ConnectionOverlay } from '@blocksuite/affine-block-surface';
|
||||
import { TextTool } from '@blocksuite/affine-gfx-text';
|
||||
import {
|
||||
CanvasEventHandler,
|
||||
ElementTransformManager,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { EdgelessElementToolbarExtension } from './configs/toolbar';
|
||||
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 { ConnectorTool } from './gfx-tool/connector-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 { EraserTool } from './gfx-tool/eraser-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 { TemplateTool } from './gfx-tool/template-tool.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[] = [
|
||||
DefaultTool,
|
||||
@@ -36,10 +44,19 @@ export const EdgelessToolExtension: ExtensionType[] = [
|
||||
PresentTool,
|
||||
];
|
||||
|
||||
export const EdgelessEditExtensions: ExtensionType[] = [
|
||||
ElementTransformManager,
|
||||
ConnectorFilter,
|
||||
SnapExtension,
|
||||
CanvasEventHandler,
|
||||
MindMapDragExtension,
|
||||
FrameHighlightManager,
|
||||
];
|
||||
|
||||
export const EdgelessBuiltInManager: ExtensionType[] = [
|
||||
ConnectionOverlay,
|
||||
MindMapIndicatorOverlay,
|
||||
SnapManager,
|
||||
SnapOverlay,
|
||||
EditPropsMiddlewareBuilder,
|
||||
EdgelessElementToolbarExtension,
|
||||
].flat();
|
||||
@@ -48,4 +65,5 @@ export const EdgelessBuiltInSpecs: ExtensionType[] = [
|
||||
EdgelessRootBlockSpec,
|
||||
EdgelessToolExtension,
|
||||
EdgelessBuiltInManager,
|
||||
EdgelessEditExtensions,
|
||||
].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,
|
||||
type MindmapNode,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { PointerEventState } from '@blocksuite/block-std';
|
||||
import {
|
||||
type DragExtensionInitializeContext,
|
||||
type ExtensionDragEndContext,
|
||||
type ExtensionDragMoveContext,
|
||||
type ExtensionDragStartContext,
|
||||
type GfxModel,
|
||||
type GfxPrimitiveElementModel,
|
||||
isGfxGroupCompatibleModel,
|
||||
TransformExtension,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import type { Bound, IVec } from '@blocksuite/global/gfx';
|
||||
|
||||
import { isSingleMindMapNode } from '../../../utils/mindmap.js';
|
||||
import { isMindmapNode } from '../../../utils/query.js';
|
||||
import { DefaultModeDragType, DefaultToolExt, type DragState } from '../ext.js';
|
||||
import { calculateResponseArea } from './drag-utils.js';
|
||||
import type { MindMapIndicatorOverlay } from './indicator-overlay.js';
|
||||
import { isSingleMindMapNode } from '../utils/mindmap';
|
||||
import { isMindmapNode } from '../utils/query';
|
||||
import { calculateResponseArea } from './utils/drag-utils';
|
||||
import type { MindMapIndicatorOverlay } from './utils/indicator-overlay';
|
||||
|
||||
type DragMindMapCtx = {
|
||||
mindmap: MindmapElementModel;
|
||||
node: MindmapNode;
|
||||
clear: () => void;
|
||||
/**
|
||||
* Whether the dragged node is the root node of the mind map
|
||||
*/
|
||||
isRoot: boolean;
|
||||
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>();
|
||||
|
||||
override supportedDragTypes: DefaultModeDragType[] = [
|
||||
DefaultModeDragType.ContentMoving,
|
||||
];
|
||||
|
||||
private get _indicatorOverlay() {
|
||||
return this.std.getOptional(
|
||||
OverlayIdentifier('mindmap-indicator')
|
||||
@@ -62,9 +64,8 @@ export class MindMapExt extends DefaultToolExt {
|
||||
* @returns
|
||||
*/
|
||||
private _createManipulationHandlers(dragMindMapCtx: DragMindMapCtx): {
|
||||
dragStart?: (evt: PointerEventState) => void;
|
||||
dragMove?: (evt: PointerEventState) => void;
|
||||
dragEnd?: (evt: PointerEventState) => void;
|
||||
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||
} {
|
||||
let hoveredCtx: {
|
||||
mindmap: MindmapElementModel | null;
|
||||
@@ -75,8 +76,8 @@ export class MindMapExt extends DefaultToolExt {
|
||||
} | null = null;
|
||||
|
||||
return {
|
||||
dragMove: (_: PointerEventState) => {
|
||||
const [x, y] = this.defaultTool.dragLastPos;
|
||||
onDragMove: (context: ExtensionDragMoveContext) => {
|
||||
const { x, y } = context.dragLastPos;
|
||||
const hoveredMindMap = this._getHoveredMindMap([x, y], dragMindMapCtx);
|
||||
const indicator = this._indicatorOverlay;
|
||||
|
||||
@@ -166,18 +167,15 @@ export class MindMapExt extends DefaultToolExt {
|
||||
}
|
||||
}
|
||||
},
|
||||
dragEnd: (e: PointerEventState) => {
|
||||
onDragEnd: (dragEndContext: ExtensionDragEndContext) => {
|
||||
if (hoveredCtx?.merge) {
|
||||
hoveredCtx.merge();
|
||||
} else {
|
||||
hoveredCtx?.abort?.();
|
||||
|
||||
if (hoveredCtx?.detach) {
|
||||
const [startX, startY] = this.gfx.viewport.toModelCoord(
|
||||
dragMindMapCtx.startPoint.x,
|
||||
dragMindMapCtx.startPoint.y
|
||||
);
|
||||
const [endX, endY] = this.gfx.viewport.toModelCoord(e.x, e.y);
|
||||
const { x: startX, y: startY } = dragEndContext.dragStartPos;
|
||||
const { x: endX, y: endY } = dragEndContext.dragLastPos;
|
||||
|
||||
dragMindMapCtx.node.element.xywh =
|
||||
dragMindMapCtx.node.element.elementBound
|
||||
@@ -204,7 +202,6 @@ export class MindMapExt extends DefaultToolExt {
|
||||
}
|
||||
|
||||
hoveredCtx = null;
|
||||
dragMindMapCtx.clear();
|
||||
this._responseAreaUpdated.clear();
|
||||
},
|
||||
};
|
||||
@@ -213,24 +210,21 @@ export class MindMapExt extends DefaultToolExt {
|
||||
/**
|
||||
* Create handlers that can translate entire mind map
|
||||
*/
|
||||
private _createTranslationHandlers(
|
||||
_: DragState,
|
||||
ctx: {
|
||||
mindmaps: Set<MindmapElementModel>;
|
||||
nodes: Set<GfxModel>;
|
||||
}
|
||||
): {
|
||||
dragStart?: (evt: PointerEventState) => void;
|
||||
dragMove?: (evt: PointerEventState) => void;
|
||||
dragEnd?: (evt: PointerEventState) => void;
|
||||
private _createTranslationHandlers(ctx: {
|
||||
mindmaps: Set<MindmapElementModel>;
|
||||
nodes: Set<GfxModel>;
|
||||
}): {
|
||||
onDragStart?: (context: ExtensionDragStartContext) => void;
|
||||
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||
} {
|
||||
return {
|
||||
dragStart: (_: PointerEventState) => {
|
||||
onDragStart: () => {
|
||||
ctx.nodes.forEach(node => {
|
||||
node.stash('xywh');
|
||||
});
|
||||
},
|
||||
dragEnd: (_: PointerEventState) => {
|
||||
onDragEnd: () => {
|
||||
ctx.mindmaps.forEach(mindmap => {
|
||||
mindmap.layout();
|
||||
});
|
||||
@@ -317,7 +311,7 @@ export class MindMapExt extends DefaultToolExt {
|
||||
|
||||
private _setupDragNodeImage(
|
||||
mindmapNode: MindmapNode,
|
||||
event: PointerEventState
|
||||
pos: { x: number; y: number }
|
||||
) {
|
||||
const surfaceBlock = this.gfx.surfaceComponent as SurfaceBlockComponent;
|
||||
const renderer = surfaceBlock?.renderer;
|
||||
@@ -329,7 +323,6 @@ export class MindMapExt extends DefaultToolExt {
|
||||
|
||||
const nodeBound = mindmapNode.element.elementBound;
|
||||
|
||||
const pos = this.gfx.viewport.toModelCoord(event.x, event.y);
|
||||
const canvas = renderer.getCanvasByBound(
|
||||
mindmapNode.element.elementBound,
|
||||
[mindmapNode.element],
|
||||
@@ -338,7 +331,7 @@ export class MindMapExt extends DefaultToolExt {
|
||||
false
|
||||
);
|
||||
|
||||
indicatorOverlay.dragNodePos = [nodeBound.x - pos[0], nodeBound.y - pos[1]];
|
||||
indicatorOverlay.dragNodePos = [nodeBound.x - pos.x, nodeBound.y - pos.y];
|
||||
indicatorOverlay.dragNodeImage = canvas;
|
||||
|
||||
return () => {
|
||||
@@ -385,14 +378,10 @@ export class MindMapExt extends DefaultToolExt {
|
||||
};
|
||||
}
|
||||
|
||||
override initDrag(dragState: DragState) {
|
||||
if (dragState.dragType !== DefaultModeDragType.ContentMoving) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isSingleMindMapNode(dragState.movedElements)) {
|
||||
const mindmap = dragState.movedElements[0].group as MindmapElementModel;
|
||||
const mindmapNode = mindmap.getNode(dragState.movedElements[0].id)!;
|
||||
override onDragInitialize(context: DragExtensionInitializeContext) {
|
||||
if (isSingleMindMapNode(context.elements)) {
|
||||
const mindmap = context.elements[0].group as MindmapElementModel;
|
||||
const mindmapNode = mindmap.getNode(context.elements[0].id)!;
|
||||
const mindmapBound = mindmap.elementBound;
|
||||
const isRoot = mindmapNode === mindmap.tree;
|
||||
|
||||
@@ -405,34 +394,36 @@ export class MindMapExt extends DefaultToolExt {
|
||||
|
||||
const clearDragStatus = isRoot
|
||||
? mindmap.stashTree(mindmapNode)
|
||||
: this._setupDragNodeImage(mindmapNode, dragState.event);
|
||||
: this._setupDragNodeImage(mindmapNode, context.dragStartPos);
|
||||
const clearOpacity = this._updateNodeOpacity(mindmap, mindmapNode);
|
||||
|
||||
if (!isRoot) {
|
||||
dragState.movedElements.splice(0, 1);
|
||||
context.elements.splice(0, 1);
|
||||
}
|
||||
|
||||
const mindMapDragCtx: DragMindMapCtx = {
|
||||
mindmap,
|
||||
node: mindmapNode,
|
||||
isRoot,
|
||||
clear: () => {
|
||||
originalMindMapBound: mindmapBound,
|
||||
};
|
||||
|
||||
return {
|
||||
...this._createManipulationHandlers(mindMapDragCtx),
|
||||
clear() {
|
||||
clearOpacity();
|
||||
clearDragStatus?.();
|
||||
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 mindmaps = new Set<MindmapElementModel>();
|
||||
dragState.movedElements.forEach(el => {
|
||||
|
||||
context.elements.forEach(el => {
|
||||
if (isMindmapNode(el)) {
|
||||
const mindmap =
|
||||
el.group instanceof MindmapElementModel
|
||||
@@ -452,8 +443,8 @@ export class MindMapExt extends DefaultToolExt {
|
||||
});
|
||||
|
||||
if (mindmapNodes.size > 1) {
|
||||
mindmapNodes.forEach(node => dragState.movedElements.push(node));
|
||||
return this._createTranslationHandlers(dragState, {
|
||||
mindmapNodes.forEach(node => context.elements.push(node));
|
||||
return this._createTranslationHandlers({
|
||||
mindmaps,
|
||||
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 {
|
||||
/** press alt/option key to clone selected */
|
||||
AltCloning = 'alt-cloning',
|
||||
/** Moving connector label */
|
||||
ConnectorLabelMoving = 'connector-label-moving',
|
||||
/** Moving selected contents */
|
||||
@@ -19,45 +12,3 @@ export enum DefaultModeDragType {
|
||||
/** Expanding the dragging area, select the content covered inside */
|
||||
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 {
|
||||
EdgelessFrameManagerIdentifier,
|
||||
type FrameOverlay,
|
||||
isFrameBlock,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
import { addText, mountTextElementEditor } from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
EdgelessTextBlockModel,
|
||||
FrameBlockModel,
|
||||
NoteBlockModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
@@ -35,11 +33,10 @@ import type { PointerEventState } from '@blocksuite/block-std';
|
||||
import {
|
||||
BaseTool,
|
||||
getTopElements,
|
||||
type GfxBlockElementModel,
|
||||
type GfxModel,
|
||||
type GfxPrimitiveElementModel,
|
||||
isGfxGroupCompatibleModel,
|
||||
type PointTestOptions,
|
||||
TransformManagerIdentifier,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
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 { calPanDelta } from '../utils/panning-utils.js';
|
||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||
import type { SnapManager } from '../utils/snap-manager.js';
|
||||
import {
|
||||
mountConnectorLabelEditor,
|
||||
mountFrameTitleEditor,
|
||||
mountGroupTitleEditor,
|
||||
mountShapeTextEditor,
|
||||
} 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 { 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 = () => {
|
||||
@@ -84,42 +75,19 @@ export class DefaultTool extends BaseTool {
|
||||
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;
|
||||
|
||||
@@ -219,8 +187,6 @@ export class DefaultTool extends BaseTool {
|
||||
});
|
||||
};
|
||||
|
||||
private _wheeling = false;
|
||||
|
||||
dragType = DefaultModeDragType.None;
|
||||
|
||||
enableHover = true;
|
||||
@@ -231,16 +197,6 @@ export class DefaultTool extends BaseTool {
|
||||
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
|
||||
*/
|
||||
@@ -263,12 +219,12 @@ export class DefaultTool extends BaseTool {
|
||||
return this.gfx.selection;
|
||||
}
|
||||
|
||||
private get frameOverlay() {
|
||||
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
|
||||
get elementTransformMgr() {
|
||||
return this.std.getOptional(TransformManagerIdentifier);
|
||||
}
|
||||
|
||||
get snapOverlay() {
|
||||
return this.std.get(OverlayIdentifier('snap-manager')) as SnapManager;
|
||||
private get frameOverlay() {
|
||||
return this.std.get(OverlayIdentifier('frame')) as FrameOverlay;
|
||||
}
|
||||
|
||||
private _addEmptyParagraphBlock(
|
||||
@@ -285,8 +241,6 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
|
||||
private async _cloneContent() {
|
||||
this._lock = true;
|
||||
|
||||
if (!this._edgeless) return;
|
||||
|
||||
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) {
|
||||
const connector = this._selectedConnector;
|
||||
let bounds = this._selectedConnectorLabelBounds;
|
||||
@@ -535,62 +404,15 @@ export class DefaultTool extends BaseTool {
|
||||
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;
|
||||
|
||||
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._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) {
|
||||
@@ -609,36 +431,16 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
|
||||
if (this.dragType === DefaultModeDragType.ContentMoving) {
|
||||
this._disposables.add(
|
||||
this.gfx.viewport.viewportMoved.subscribe(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.setMovingElements(
|
||||
this._toBeMoved
|
||||
);
|
||||
|
||||
this._moveContent(delta, this._alignBound);
|
||||
}
|
||||
})
|
||||
);
|
||||
if (this.elementTransformMgr) {
|
||||
this.doc.captureSync();
|
||||
this.elementTransformMgr.initializeDrag({
|
||||
movingElements: this._toBeMoved,
|
||||
event: event.raw,
|
||||
onDragEnd: () => {
|
||||
this.doc.captureSync();
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -742,7 +544,7 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
|
||||
this._isDoubleClickedOnMask = false;
|
||||
this._supportedExts.forEach(ext => ext.click?.(e));
|
||||
this.elementTransformMgr?.dispatch('click', e);
|
||||
}
|
||||
|
||||
override deactivate() {
|
||||
@@ -819,7 +621,7 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
this._supportedExts.forEach(ext => ext.click?.(e));
|
||||
this.elementTransformMgr?.dispatch('dblclick', e);
|
||||
|
||||
if (
|
||||
e.raw.target &&
|
||||
@@ -832,48 +634,9 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
override dragEnd() {
|
||||
if (this.edgelessSelectionManager.editing) return;
|
||||
|
||||
this._selectedBounds = [];
|
||||
this.snapOverlay.clear();
|
||||
this.frameOverlay.clear();
|
||||
this._toBeMoved = [];
|
||||
this._selectedConnector = null;
|
||||
@@ -897,28 +660,7 @@ export class DefaultTool extends BaseTool {
|
||||
}
|
||||
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: {
|
||||
@@ -956,30 +698,12 @@ export class DefaultTool extends BaseTool {
|
||||
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;
|
||||
if (dragType === DefaultModeDragType.ContentMoving && e.keys.alt) {
|
||||
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() {
|
||||
@@ -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 {
|
||||
this._supportedExts.forEach(ext => ext.pointerDown(e));
|
||||
this.elementTransformMgr?.dispatch('pointerdown', e);
|
||||
}
|
||||
|
||||
override pointerMove(e: PointerEventState) {
|
||||
@@ -1030,20 +749,18 @@ export class DefaultTool extends BaseTool {
|
||||
this.frameOverlay.clear();
|
||||
}
|
||||
|
||||
this._supportedExts.forEach(ext => ext.pointerMove(e));
|
||||
this.elementTransformMgr?.dispatch('pointermove', e);
|
||||
}
|
||||
|
||||
override pointerUp(e: PointerEventState) {
|
||||
this._supportedExts.forEach(ext => ext.pointerUp(e));
|
||||
this.elementTransformMgr?.dispatch('pointerup', e);
|
||||
}
|
||||
|
||||
override tripleClick() {
|
||||
if (this._isDoubleClickedOnMask) return;
|
||||
}
|
||||
|
||||
override unmounted(): void {
|
||||
this._exts.forEach(ext => ext.unmounted());
|
||||
}
|
||||
override unmounted(): void {}
|
||||
}
|
||||
|
||||
declare module '@blocksuite/block-std/gfx' {
|
||||
|
||||
@@ -36,7 +36,7 @@ const ALIGN_THRESHOLD = 8;
|
||||
const DISTRIBUTION_LINE_OFFSET = 1;
|
||||
const STROKE_WIDTH = 2;
|
||||
|
||||
export class SnapManager extends Overlay {
|
||||
export class SnapOverlay extends Overlay {
|
||||
static override overlayName: string = 'snap-manager';
|
||||
|
||||
private _skippedElements: Set<GfxModel> = new Set();
|
||||
@@ -76,8 +76,6 @@ export class SnapManager extends Overlay {
|
||||
};
|
||||
|
||||
override clear() {
|
||||
super.clear();
|
||||
|
||||
this._referenceBounds = {
|
||||
vertical: [],
|
||||
horizontal: [],
|
||||
@@ -89,6 +87,8 @@ export class SnapManager extends Overlay {
|
||||
};
|
||||
this._distributedAlignLines = [];
|
||||
this._skippedElements.clear();
|
||||
|
||||
super.clear();
|
||||
}
|
||||
|
||||
private _alignDistributeHorizontally(
|
||||
|
||||
@@ -12,17 +12,19 @@ import {
|
||||
} from './extensions';
|
||||
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
|
||||
import { SurfaceBlockService } from './surface-service';
|
||||
import { ConnectorElementView } from './view/connector';
|
||||
import { MindMapView } from './view/mindmap';
|
||||
|
||||
const CommonSurfaceBlockSpec: ExtensionType[] = [
|
||||
FlavourExtension('affine:surface'),
|
||||
SurfaceBlockService,
|
||||
MindMapView,
|
||||
EdgelessCRUDExtension,
|
||||
EdgelessLegacySlotExtension,
|
||||
ExportManagerExtension,
|
||||
];
|
||||
|
||||
const ElementModelViews = [MindMapView, ConnectorElementView];
|
||||
|
||||
export const PageSurfaceBlockSpec: ExtensionType[] = [
|
||||
...CommonSurfaceBlockSpec,
|
||||
...SurfaceBlockAdapterExtensions,
|
||||
@@ -32,5 +34,6 @@ export const PageSurfaceBlockSpec: ExtensionType[] = [
|
||||
export const EdgelessSurfaceBlockSpec: ExtensionType[] = [
|
||||
...CommonSurfaceBlockSpec,
|
||||
...EdgelessSurfaceBlockAdapterExtensions,
|
||||
...ElementModelViews,
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
x: 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 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[] = [];
|
||||
|
||||
override supportedDragTypes: DefaultModeDragType[] = [
|
||||
DefaultModeDragType.None,
|
||||
];
|
||||
|
||||
private _callInReverseOrder(
|
||||
callback: (view: GfxElementModelView) => void,
|
||||
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';
|
||||
export { GfxController } from './controller.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 { GridManager } from './grid.js';
|
||||
export { GfxControllerIdentifier } from './identifiers.js';
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
|
||||
import { onSurfaceAdded } from '../../utils/gfx.js';
|
||||
import {
|
||||
type GfxBlockComponent,
|
||||
isGfxBlockComponent,
|
||||
} from '../../view/index.js';
|
||||
import type { GfxController } from '../controller.js';
|
||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
||||
import type { GfxModel } from '../model/model.js';
|
||||
import type { GfxPrimitiveElementModel } from '../model/surface/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) {
|
||||
if (typeof model === 'string') {
|
||||
if (this._viewMap.has(model)) {
|
||||
return this._viewMap.get(model);
|
||||
}
|
||||
get(
|
||||
model: GfxModel | GfxLocalElementModel | string
|
||||
): GfxElementModelView | GfxBlockComponent | null {
|
||||
model = typeof model === 'string' ? model : model.id;
|
||||
|
||||
return this.std.view.getBlock(model) ?? null;
|
||||
} else {
|
||||
if (model instanceof GfxBlockElementModel) {
|
||||
return this.std.view.getBlock(model.id) ?? null;
|
||||
} else {
|
||||
return this._viewMap.get(model.id) ?? null;
|
||||
}
|
||||
if (this._viewMap.has(model)) {
|
||||
return this._viewMap.get(model)!;
|
||||
}
|
||||
|
||||
const blockView = this.std.view.getBlock(model);
|
||||
|
||||
if (blockView && isGfxBlockComponent(blockView)) {
|
||||
return blockView;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
override mounted(): void {
|
||||
|
||||
@@ -6,9 +6,15 @@ import type { Extension } from '@blocksuite/store';
|
||||
|
||||
import type { PointerEventState } from '../../event/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 { 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';
|
||||
|
||||
export type EventsHandlerMap = {
|
||||
@@ -33,7 +39,7 @@ export class GfxElementModelView<
|
||||
| GfxLocalElementModel,
|
||||
RendererContext = object,
|
||||
>
|
||||
implements GfxElementGeometry, Extension
|
||||
implements GfxElementGeometry, Extension, GfxViewTransformInterface
|
||||
{
|
||||
static type: string;
|
||||
|
||||
@@ -166,6 +172,26 @@ export class GfxElementModelView<
|
||||
|
||||
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.
|
||||
* Override this method requires calling `super.onDestroyed()`.
|
||||
|
||||
@@ -4,6 +4,10 @@ import { computed } from '@preact/signals-core';
|
||||
import { nothing } from 'lit';
|
||||
|
||||
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 type { GfxBlockElementModel } from '../../gfx/index.js';
|
||||
import { SurfaceSelection } from '../../selection/index.js';
|
||||
@@ -47,10 +51,13 @@ function handleGfxConnection(instance: GfxBlockComponent) {
|
||||
}
|
||||
|
||||
export abstract class GfxBlockComponent<
|
||||
Model extends GfxBlockElementModel = GfxBlockElementModel,
|
||||
Service extends BlockService = BlockService,
|
||||
WidgetName extends string = string,
|
||||
> extends BlockComponent<Model, Service, WidgetName> {
|
||||
Model extends GfxBlockElementModel = GfxBlockElementModel,
|
||||
Service extends BlockService = BlockService,
|
||||
WidgetName extends string = string,
|
||||
>
|
||||
extends BlockComponent<Model, Service, WidgetName>
|
||||
implements GfxViewTransformInterface
|
||||
{
|
||||
[GfxElementSymbol] = true;
|
||||
|
||||
get gfx() {
|
||||
@@ -62,6 +69,22 @@ export abstract class GfxBlockComponent<
|
||||
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() {
|
||||
const viewport = this.gfx.viewport;
|
||||
const { translateX, translateY, zoom } = viewport;
|
||||
@@ -151,6 +174,22 @@ export function toGfxBlockComponent<
|
||||
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() {
|
||||
return this.std.get(GfxControllerIdentifier);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user