mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 19:02:23 +08:00
refactor(editor): move mindmap transform to its package (#11115)
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
||||
OverlayIdentifier,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { isMindmapNode } from '@blocksuite/affine-gfx-mindmap';
|
||||
import {
|
||||
type BookmarkBlockModel,
|
||||
ConnectorElementModel,
|
||||
@@ -85,7 +86,6 @@ import {
|
||||
isEmbedSyncedDocBlock,
|
||||
isEmbedYoutubeBlock,
|
||||
isImageBlock,
|
||||
isMindmapNode,
|
||||
} from '../../utils/query.js';
|
||||
import {
|
||||
HandleDirection,
|
||||
|
||||
@@ -4,8 +4,14 @@ import {
|
||||
PresentTool,
|
||||
} from '@blocksuite/affine-block-frame';
|
||||
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
|
||||
import { ConnectorTool } from '@blocksuite/affine-gfx-connector';
|
||||
import { MindMapIndicatorOverlay } from '@blocksuite/affine-gfx-mindmap';
|
||||
import {
|
||||
ConnectorFilter,
|
||||
ConnectorTool,
|
||||
} from '@blocksuite/affine-gfx-connector';
|
||||
import {
|
||||
MindMapDragExtension,
|
||||
MindMapIndicatorOverlay,
|
||||
} from '@blocksuite/affine-gfx-mindmap';
|
||||
import { NoteTool } from '@blocksuite/affine-gfx-note';
|
||||
import { ShapeTool } from '@blocksuite/affine-gfx-shape';
|
||||
import { TextTool } from '@blocksuite/affine-gfx-text';
|
||||
@@ -17,8 +23,6 @@ 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 { MindMapDragExtension } from './element-transform/mind-map-drag.js';
|
||||
import { SnapExtension } from './element-transform/snap-manager.js';
|
||||
import { BrushTool } from './gfx-tool/brush-tool.js';
|
||||
import { DefaultTool } from './gfx-tool/default-tool.js';
|
||||
|
||||
@@ -3,6 +3,11 @@ import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-te
|
||||
import { isNoteBlock } from '@blocksuite/affine-block-surface';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import { mountConnectorLabelEditor } from '@blocksuite/affine-gfx-connector';
|
||||
import {
|
||||
getNearestTranslation,
|
||||
isElementOutsideViewport,
|
||||
isSingleMindMapNode,
|
||||
} from '@blocksuite/affine-gfx-mindmap';
|
||||
import { mountShapeTextEditor, ShapeTool } from '@blocksuite/affine-gfx-shape';
|
||||
import {
|
||||
ConnectorElementModel,
|
||||
@@ -43,11 +48,6 @@ import {
|
||||
} from './utils/consts.js';
|
||||
import { deleteElements } from './utils/crud.js';
|
||||
import { getNextShapeType } from './utils/hotkey-utils.js';
|
||||
import {
|
||||
getNearestTranslation,
|
||||
isElementOutsideViewport,
|
||||
isSingleMindMapNode,
|
||||
} from './utils/mindmap.js';
|
||||
import { isCanvasElement } from './utils/query.js';
|
||||
|
||||
export class EdgelessPageKeyboardManager extends PageKeyboardManager {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getBgGridGap,
|
||||
normalizeWheelDeltaY,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { isSingleMindMapNode } from '@blocksuite/affine-gfx-mindmap';
|
||||
import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape';
|
||||
import {
|
||||
NoteBlockModel,
|
||||
@@ -51,7 +52,6 @@ import { EdgelessClipboardController } from './clipboard/clipboard.js';
|
||||
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
|
||||
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
|
||||
import type { EdgelessRootService } from './edgeless-root-service.js';
|
||||
import { isSingleMindMapNode } from './utils/mindmap.js';
|
||||
import { isCanvasElement } from './utils/query.js';
|
||||
|
||||
export class EdgelessRootBlockComponent extends BlockComponent<
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
@@ -1,455 +0,0 @@
|
||||
import {
|
||||
OverlayIdentifier,
|
||||
type SurfaceBlockComponent,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
containsNode,
|
||||
createFromTree,
|
||||
detachMindmap,
|
||||
findTargetNode,
|
||||
hideNodeConnector,
|
||||
type MindMapIndicatorOverlay,
|
||||
NODE_HORIZONTAL_SPACING,
|
||||
NODE_VERTICAL_SPACING,
|
||||
tryMoveNode,
|
||||
} from '@blocksuite/affine-gfx-mindmap';
|
||||
import {
|
||||
type LayoutType,
|
||||
type LocalConnectorElementModel,
|
||||
MindmapElementModel,
|
||||
type MindmapNode,
|
||||
} from '@blocksuite/affine-model';
|
||||
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';
|
||||
import { isMindmapNode } from '../utils/query';
|
||||
import { calculateResponseArea } from './utils/drag-utils';
|
||||
|
||||
type DragMindMapCtx = {
|
||||
mindmap: MindmapElementModel;
|
||||
node: MindmapNode;
|
||||
/**
|
||||
* Whether the dragged node is the root node of the mind map
|
||||
*/
|
||||
isRoot: boolean;
|
||||
originalMindMapBound: Bound;
|
||||
};
|
||||
|
||||
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 get _indicatorOverlay() {
|
||||
return this.std.getOptional(
|
||||
OverlayIdentifier('mindmap-indicator')
|
||||
) as MindMapIndicatorOverlay | null;
|
||||
}
|
||||
|
||||
private _calcDragResponseArea(mindmap: MindmapElementModel) {
|
||||
calculateResponseArea(mindmap);
|
||||
this._responseAreaUpdated.add(mindmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create handlers that can drag and drop mind map nodes
|
||||
* @param dragMindMapCtx
|
||||
* @param dragState
|
||||
* @returns
|
||||
*/
|
||||
private _createManipulationHandlers(dragMindMapCtx: DragMindMapCtx): {
|
||||
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||
} {
|
||||
let hoveredCtx: {
|
||||
mindmap: MindmapElementModel | null;
|
||||
node: MindmapNode | null;
|
||||
detach?: boolean;
|
||||
abort?: () => void;
|
||||
merge?: () => void;
|
||||
} | null = null;
|
||||
|
||||
return {
|
||||
onDragMove: (context: ExtensionDragMoveContext) => {
|
||||
const { x, y } = context.dragLastPos;
|
||||
const hoveredMindMap = this._getHoveredMindMap([x, y], dragMindMapCtx);
|
||||
const indicator = this._indicatorOverlay;
|
||||
|
||||
if (indicator) {
|
||||
indicator.currentDragPos = [x, y];
|
||||
indicator.refresh();
|
||||
}
|
||||
|
||||
hoveredCtx?.abort?.();
|
||||
|
||||
const hoveredNode = hoveredMindMap
|
||||
? findTargetNode(hoveredMindMap, [x, y])
|
||||
: null;
|
||||
|
||||
hoveredCtx = {
|
||||
mindmap: hoveredMindMap,
|
||||
node: hoveredNode,
|
||||
};
|
||||
|
||||
// hovered on the currently dragged mind map but
|
||||
// 1. not hovered on any node or
|
||||
// 2. hovered on the node that is itself or its children (which is not allowed)
|
||||
// then consider user is trying to drop the node to its original position
|
||||
if (
|
||||
hoveredNode &&
|
||||
hoveredMindMap &&
|
||||
!containsNode(hoveredMindMap, hoveredNode, dragMindMapCtx.node)
|
||||
) {
|
||||
const operation = tryMoveNode(
|
||||
hoveredMindMap,
|
||||
hoveredNode,
|
||||
dragMindMapCtx.mindmap,
|
||||
dragMindMapCtx.node,
|
||||
[x, y],
|
||||
options => this._drawIndicator(options)
|
||||
);
|
||||
|
||||
if (operation) {
|
||||
hoveredCtx.abort = operation.abort;
|
||||
hoveredCtx.merge = operation.merge;
|
||||
}
|
||||
} else if (dragMindMapCtx.isRoot) {
|
||||
dragMindMapCtx.mindmap.layout();
|
||||
hoveredCtx.merge = () => {
|
||||
dragMindMapCtx.mindmap.layout();
|
||||
};
|
||||
} else {
|
||||
// if `hoveredMindMap` is not null
|
||||
// either the node is hovered on the dragged node's children
|
||||
// or the there is no hovered node at all
|
||||
// then consider user is trying to place the node to its original position
|
||||
if (hoveredMindMap) {
|
||||
const { node: draggedNode, mindmap } = dragMindMapCtx;
|
||||
const nodeBound = draggedNode.element.elementBound;
|
||||
|
||||
hoveredCtx.abort = this._drawIndicator({
|
||||
targetMindMap: mindmap,
|
||||
target: draggedNode,
|
||||
sourceMindMap: mindmap,
|
||||
source: draggedNode,
|
||||
newParent: draggedNode.parent!,
|
||||
insertPosition: {
|
||||
type: 'sibling',
|
||||
layoutDir: mindmap.getLayoutDir(draggedNode) as Exclude<
|
||||
LayoutType,
|
||||
LayoutType.BALANCE
|
||||
>,
|
||||
position: y > nodeBound.y + nodeBound.h / 2 ? 'next' : 'prev',
|
||||
},
|
||||
path: mindmap.getPath(draggedNode),
|
||||
});
|
||||
} else {
|
||||
hoveredCtx.detach = true;
|
||||
|
||||
const reset = (hoveredCtx.abort = hideNodeConnector(
|
||||
dragMindMapCtx.mindmap,
|
||||
dragMindMapCtx.node
|
||||
));
|
||||
|
||||
hoveredCtx.abort = () => {
|
||||
reset?.();
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
onDragEnd: (dragEndContext: ExtensionDragEndContext) => {
|
||||
if (hoveredCtx?.merge) {
|
||||
hoveredCtx.merge();
|
||||
} else {
|
||||
hoveredCtx?.abort?.();
|
||||
|
||||
if (hoveredCtx?.detach) {
|
||||
const { x: startX, y: startY } = dragEndContext.dragStartPos;
|
||||
const { x: endX, y: endY } = dragEndContext.dragLastPos;
|
||||
|
||||
dragMindMapCtx.node.element.xywh =
|
||||
dragMindMapCtx.node.element.elementBound
|
||||
.moveDelta(endX - startX, endY - startY)
|
||||
.serialize();
|
||||
|
||||
if (dragMindMapCtx.node !== dragMindMapCtx.mindmap.tree) {
|
||||
detachMindmap(dragMindMapCtx.mindmap, dragMindMapCtx.node);
|
||||
const mindmap = createFromTree(
|
||||
dragMindMapCtx.node,
|
||||
dragMindMapCtx.mindmap.style,
|
||||
dragMindMapCtx.mindmap.layoutType,
|
||||
this.gfx.surface!
|
||||
);
|
||||
|
||||
mindmap.layout();
|
||||
} else {
|
||||
dragMindMapCtx.mindmap.layout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hoveredCtx = null;
|
||||
this._responseAreaUpdated.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create handlers that can translate entire mind map
|
||||
*/
|
||||
private _createTranslationHandlers(ctx: {
|
||||
mindmaps: Set<MindmapElementModel>;
|
||||
nodes: Set<GfxModel>;
|
||||
}): {
|
||||
onDragStart?: (context: ExtensionDragStartContext) => void;
|
||||
onDragMove?: (context: ExtensionDragMoveContext) => void;
|
||||
onDragEnd?: (context: ExtensionDragEndContext) => void;
|
||||
} {
|
||||
return {
|
||||
onDragStart: () => {
|
||||
ctx.nodes.forEach(node => {
|
||||
node.stash('xywh');
|
||||
});
|
||||
},
|
||||
onDragEnd: () => {
|
||||
ctx.mindmaps.forEach(mindmap => {
|
||||
mindmap.layout();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private _drawIndicator(options: {
|
||||
targetMindMap: MindmapElementModel;
|
||||
target: MindmapNode;
|
||||
sourceMindMap: MindmapElementModel;
|
||||
source: MindmapNode;
|
||||
newParent: MindmapNode;
|
||||
insertPosition:
|
||||
| {
|
||||
type: 'sibling';
|
||||
layoutDir: Exclude<LayoutType, LayoutType.BALANCE>;
|
||||
position: 'prev' | 'next';
|
||||
}
|
||||
| { type: 'child'; layoutDir: Exclude<LayoutType, LayoutType.BALANCE> };
|
||||
path: number[];
|
||||
}) {
|
||||
const indicatorOverlay = this._indicatorOverlay;
|
||||
|
||||
if (!indicatorOverlay) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// draw the indicator at given position
|
||||
const { newParent, insertPosition, targetMindMap, target, source, path } =
|
||||
options;
|
||||
const children = newParent.children.filter(
|
||||
node => node.element.id !== source.id
|
||||
);
|
||||
|
||||
indicatorOverlay.setIndicatorInfo({
|
||||
targetMindMap,
|
||||
target,
|
||||
parent: newParent,
|
||||
insertPosition,
|
||||
parentChildren: children,
|
||||
path,
|
||||
});
|
||||
|
||||
return () => {
|
||||
indicatorOverlay.clear();
|
||||
};
|
||||
}
|
||||
|
||||
private _getHoveredMindMap(
|
||||
position: IVec,
|
||||
dragMindMapCtx: DragMindMapCtx
|
||||
): MindmapElementModel | null {
|
||||
const mindmap =
|
||||
(this.gfx
|
||||
.getElementByPoint(position[0], position[1], {
|
||||
all: true,
|
||||
responsePadding: [NODE_HORIZONTAL_SPACING, NODE_VERTICAL_SPACING * 2],
|
||||
})
|
||||
.find(el => {
|
||||
if (!(el instanceof MindmapElementModel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
el === dragMindMapCtx.mindmap &&
|
||||
!dragMindMapCtx.originalMindMapBound.containsPoint(position)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}) as MindmapElementModel) ?? null;
|
||||
|
||||
if (
|
||||
mindmap &&
|
||||
(!this._responseAreaUpdated.has(mindmap) || !mindmap.tree.responseArea)
|
||||
) {
|
||||
this._calcDragResponseArea(mindmap);
|
||||
}
|
||||
|
||||
return mindmap;
|
||||
}
|
||||
|
||||
private _setupDragNodeImage(
|
||||
mindmapNode: MindmapNode,
|
||||
pos: { x: number; y: number }
|
||||
) {
|
||||
const surfaceBlock = this.gfx.surfaceComponent as SurfaceBlockComponent;
|
||||
const renderer = surfaceBlock?.renderer;
|
||||
const indicatorOverlay = this._indicatorOverlay;
|
||||
|
||||
if (!renderer || !indicatorOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeBound = mindmapNode.element.elementBound;
|
||||
|
||||
const canvas = renderer.getCanvasByBound(
|
||||
mindmapNode.element.elementBound,
|
||||
[mindmapNode.element],
|
||||
undefined,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
|
||||
indicatorOverlay.dragNodePos = [nodeBound.x - pos.x, nodeBound.y - pos.y];
|
||||
indicatorOverlay.dragNodeImage = canvas;
|
||||
|
||||
return () => {
|
||||
indicatorOverlay.dragNodeImage = null;
|
||||
indicatorOverlay.currentDragPos = null;
|
||||
};
|
||||
}
|
||||
|
||||
private _updateNodeOpacity(
|
||||
mindmap: MindmapElementModel,
|
||||
mindNode: MindmapNode
|
||||
) {
|
||||
const OPACITY = 0.3;
|
||||
const updatedNodes = new Set<
|
||||
GfxPrimitiveElementModel | LocalConnectorElementModel
|
||||
>();
|
||||
const traverse = (node: MindmapNode, parent: MindmapNode | null) => {
|
||||
node.element.opacity = OPACITY;
|
||||
updatedNodes.add(node.element);
|
||||
|
||||
if (parent) {
|
||||
const connectorId = `#${parent.element.id}-${node.element.id}`;
|
||||
const connector = mindmap.connectors.get(connectorId);
|
||||
|
||||
if (connector) {
|
||||
connector.opacity = OPACITY;
|
||||
updatedNodes.add(connector);
|
||||
}
|
||||
}
|
||||
|
||||
if (node.children.length) {
|
||||
node.children.forEach(child => traverse(child, node));
|
||||
}
|
||||
};
|
||||
|
||||
const parentNode = mindmap.getParentNode(mindNode.element.id) ?? null;
|
||||
|
||||
traverse(mindNode, parentNode);
|
||||
|
||||
return () => {
|
||||
updatedNodes.forEach(el => {
|
||||
el.opacity = 1;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
mindmapBound.x -= NODE_HORIZONTAL_SPACING;
|
||||
mindmapBound.y -= NODE_VERTICAL_SPACING * 2;
|
||||
mindmapBound.w += NODE_HORIZONTAL_SPACING * 2;
|
||||
mindmapBound.h += NODE_VERTICAL_SPACING * 4;
|
||||
|
||||
this._calcDragResponseArea(mindmap);
|
||||
|
||||
const clearDragStatus = isRoot
|
||||
? mindmap.stashTree(mindmapNode)
|
||||
: this._setupDragNodeImage(mindmapNode, context.dragStartPos);
|
||||
const clearOpacity = this._updateNodeOpacity(mindmap, mindmapNode);
|
||||
|
||||
if (!isRoot) {
|
||||
context.elements.splice(0, 1);
|
||||
}
|
||||
|
||||
const mindMapDragCtx: DragMindMapCtx = {
|
||||
mindmap,
|
||||
node: mindmapNode,
|
||||
isRoot,
|
||||
originalMindMapBound: mindmapBound,
|
||||
};
|
||||
|
||||
return {
|
||||
...this._createManipulationHandlers(mindMapDragCtx),
|
||||
clear() {
|
||||
clearOpacity();
|
||||
clearDragStatus?.();
|
||||
if (!isRoot) {
|
||||
context.elements.push(mindmapNode.element);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const mindmapNodes = new Set<GfxModel>();
|
||||
const mindmaps = new Set<MindmapElementModel>();
|
||||
|
||||
context.elements.forEach(el => {
|
||||
if (isMindmapNode(el)) {
|
||||
const mindmap =
|
||||
el.group instanceof MindmapElementModel
|
||||
? el.group
|
||||
: (el as MindmapElementModel);
|
||||
|
||||
mindmaps.add(mindmap);
|
||||
mindmap.childElements.forEach(child => mindmapNodes.add(child));
|
||||
} else if (isGfxGroupCompatibleModel(el)) {
|
||||
el.descendantElements.forEach(desc => {
|
||||
if (desc.group instanceof MindmapElementModel) {
|
||||
mindmaps.add(desc.group);
|
||||
desc.group.childElements.forEach(_el => mindmapNodes.add(_el));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (mindmapNodes.size > 1) {
|
||||
mindmapNodes.forEach(node => context.elements.push(node));
|
||||
return this._createTranslationHandlers({
|
||||
mindmaps,
|
||||
nodes: mindmapNodes,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import {
|
||||
NODE_HORIZONTAL_SPACING,
|
||||
NODE_VERTICAL_SPACING,
|
||||
} from '@blocksuite/affine-gfx-mindmap';
|
||||
import {
|
||||
LayoutType,
|
||||
type MindmapElementModel,
|
||||
type MindmapNode,
|
||||
type MindmapRoot,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
const isOnEdge = (node: MindmapNode, direction: 'tail' | 'head') => {
|
||||
let current = node;
|
||||
|
||||
while (current) {
|
||||
if (!current.parent) return true;
|
||||
|
||||
if (direction === 'tail' && last(current.parent.children) === current) {
|
||||
current = current.parent;
|
||||
} else if (direction === 'head' && current.parent.children[0] === current) {
|
||||
current = current.parent;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const TAIL_RESPONSE_AREA = NODE_HORIZONTAL_SPACING;
|
||||
|
||||
const fillResponseArea = (
|
||||
node: MindmapNode,
|
||||
layoutType: LayoutType,
|
||||
parent: MindmapNode | null
|
||||
) => {
|
||||
// root node
|
||||
if (!parent) {
|
||||
const rootElmBound = node.element.elementBound;
|
||||
const width =
|
||||
layoutType === LayoutType.BALANCE
|
||||
? rootElmBound.w + TAIL_RESPONSE_AREA * 2
|
||||
: rootElmBound.w + TAIL_RESPONSE_AREA;
|
||||
|
||||
node.responseArea = new Bound(
|
||||
layoutType === LayoutType.BALANCE || layoutType === LayoutType.LEFT
|
||||
? rootElmBound.x - TAIL_RESPONSE_AREA
|
||||
: rootElmBound.x,
|
||||
rootElmBound.y,
|
||||
width,
|
||||
rootElmBound.h
|
||||
);
|
||||
|
||||
if (node.detail.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutType === LayoutType.BALANCE) {
|
||||
(node as MindmapRoot).right.forEach(child => {
|
||||
fillResponseArea(child, LayoutType.RIGHT, node);
|
||||
});
|
||||
(node as MindmapRoot).left.forEach(child => {
|
||||
fillResponseArea(child, LayoutType.LEFT, node);
|
||||
});
|
||||
} else {
|
||||
node.children.forEach(child => {
|
||||
fillResponseArea(child, layoutType, node);
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
const nodeBound = node.element.elementBound;
|
||||
const idx = parent.children.indexOf(node) ?? -1;
|
||||
const isLast =
|
||||
idx === (parent.children.length || -1) - 1 && isOnEdge(node, 'tail');
|
||||
const isFirst = idx === 0 && isOnEdge(node, 'head');
|
||||
const upperSpacing = isFirst
|
||||
? NODE_VERTICAL_SPACING * 2
|
||||
: NODE_VERTICAL_SPACING / 2;
|
||||
const lowerSpacing = isLast
|
||||
? NODE_VERTICAL_SPACING * 2
|
||||
: NODE_VERTICAL_SPACING / 2;
|
||||
|
||||
const h = nodeBound.h + upperSpacing + lowerSpacing;
|
||||
const w =
|
||||
(layoutType === LayoutType.RIGHT
|
||||
? node.element.x +
|
||||
node.element.w -
|
||||
(parent.element.x + parent.element.w)
|
||||
: parent.element.x - node.element.x) + TAIL_RESPONSE_AREA;
|
||||
|
||||
node.responseArea = new Bound(
|
||||
layoutType === LayoutType.RIGHT
|
||||
? parent.element.x + parent.element.w
|
||||
: parent.element.x - w,
|
||||
node.element.y - upperSpacing,
|
||||
w,
|
||||
h
|
||||
);
|
||||
|
||||
if (node.children.length > 0 && !node.detail.collapsed) {
|
||||
let responseArea: Bound;
|
||||
|
||||
node.children.forEach(child => {
|
||||
fillResponseArea(child, layoutType, node);
|
||||
|
||||
if (responseArea) {
|
||||
responseArea = responseArea.unite(child.responseArea!);
|
||||
} else {
|
||||
responseArea = child.responseArea!;
|
||||
}
|
||||
});
|
||||
|
||||
node.responseArea.h = responseArea!.h;
|
||||
node.responseArea.y = responseArea!.y;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const balanceLeftRightResponseArea = (tree: MindmapRoot) => {
|
||||
const leftTreeArea = tree.left.reduce((pre: Bound | null, node) => {
|
||||
if (pre) {
|
||||
return pre.unite(node.responseArea!);
|
||||
}
|
||||
return node.responseArea!;
|
||||
}, null);
|
||||
const rightTreeArea = tree.right.reduce((pre: Bound | null, node) => {
|
||||
if (pre) {
|
||||
return pre.unite(node.responseArea!);
|
||||
}
|
||||
return node.responseArea!;
|
||||
}, null);
|
||||
|
||||
if (!leftTreeArea || !rightTreeArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if the height of the left tree and right tree are not equal
|
||||
// expand the response area of lower tree to match the height of the higher tree
|
||||
if (leftTreeArea.h !== rightTreeArea.h) {
|
||||
const isLeftHigher = leftTreeArea.h > rightTreeArea.h;
|
||||
const upperBoundary = isLeftHigher ? leftTreeArea.y : rightTreeArea.y;
|
||||
const bottomBoundary = isLeftHigher
|
||||
? leftTreeArea.y + leftTreeArea.h
|
||||
: rightTreeArea.y + rightTreeArea.h;
|
||||
const targetChildren = isLeftHigher ? tree.right : tree.left;
|
||||
|
||||
const expandEdge = (children: MindmapNode[]) => {
|
||||
const expand = (direction: 'up' | 'down') => {
|
||||
const expandUpperEdge = direction === 'up';
|
||||
const node = direction === 'up' ? children[0] : last(children)!;
|
||||
|
||||
if (!node) return;
|
||||
|
||||
if (node.responseArea) {
|
||||
node.responseArea.h = expandUpperEdge
|
||||
? node.responseArea.h + (node.responseArea.y - upperBoundary)
|
||||
: node.responseArea.h +
|
||||
(bottomBoundary - node.responseArea.y - node.responseArea.h);
|
||||
expandUpperEdge && (node.responseArea.y = upperBoundary);
|
||||
}
|
||||
};
|
||||
|
||||
expand('up');
|
||||
expand('down');
|
||||
};
|
||||
|
||||
expandEdge(targetChildren);
|
||||
}
|
||||
};
|
||||
|
||||
export const calculateResponseArea = (mindmap: MindmapElementModel) => {
|
||||
const layoutDir = mindmap.layoutType;
|
||||
const tree = mindmap.tree;
|
||||
|
||||
switch (layoutDir) {
|
||||
case LayoutType.RIGHT:
|
||||
case LayoutType.LEFT:
|
||||
{
|
||||
fillResponseArea(tree, layoutDir, null);
|
||||
}
|
||||
break;
|
||||
case LayoutType.BALANCE:
|
||||
{
|
||||
fillResponseArea(tree, LayoutType.BALANCE, null);
|
||||
balanceLeftRightResponseArea(tree);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -1,62 +0,0 @@
|
||||
import { MindmapElementModel } from '@blocksuite/affine-model';
|
||||
import type { GfxModel, Viewport } from '@blocksuite/block-std/gfx';
|
||||
|
||||
export function isSingleMindMapNode(els: GfxModel[]) {
|
||||
return els.length === 1 && els[0].group instanceof MindmapElementModel;
|
||||
}
|
||||
|
||||
export function isElementOutsideViewport(
|
||||
viewport: Viewport,
|
||||
element: GfxModel,
|
||||
padding: [number, number] = [0, 0]
|
||||
) {
|
||||
const elementBound = element.elementBound;
|
||||
|
||||
padding[0] /= viewport.zoom;
|
||||
padding[1] /= viewport.zoom;
|
||||
|
||||
elementBound.x -= padding[1];
|
||||
elementBound.w += padding[1];
|
||||
elementBound.y -= padding[0];
|
||||
elementBound.h += padding[0];
|
||||
|
||||
return !viewport.viewportBounds.contains(elementBound);
|
||||
}
|
||||
|
||||
export function getNearestTranslation(
|
||||
viewport: Viewport,
|
||||
element: GfxModel,
|
||||
padding: [number, number] = [0, 0]
|
||||
) {
|
||||
const viewportBound = viewport.viewportBounds;
|
||||
const elementBound = element.elementBound;
|
||||
let dx = 0;
|
||||
let dy = 0;
|
||||
|
||||
if (elementBound.x - padding[1] < viewportBound.x) {
|
||||
dx = viewportBound.x - (elementBound.x - padding[1]);
|
||||
} else if (
|
||||
elementBound.x + elementBound.w + padding[1] >
|
||||
viewportBound.x + viewportBound.w
|
||||
) {
|
||||
dx =
|
||||
viewportBound.x +
|
||||
viewportBound.w -
|
||||
(elementBound.x + elementBound.w + padding[1]);
|
||||
}
|
||||
|
||||
if (elementBound.y - padding[0] < viewportBound.y) {
|
||||
dy = elementBound.y - padding[0] - viewportBound.y;
|
||||
} else if (
|
||||
elementBound.y + elementBound.h + padding[0] >
|
||||
viewportBound.y + viewportBound.h
|
||||
) {
|
||||
dy =
|
||||
elementBound.y +
|
||||
elementBound.h +
|
||||
padding[0] -
|
||||
(viewportBound.y + viewportBound.h);
|
||||
}
|
||||
|
||||
return [dx, dy];
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
type EmbedSyncedDocModel,
|
||||
type EmbedYoutubeModel,
|
||||
type ImageBlockModel,
|
||||
MindmapElementModel,
|
||||
ShapeElementModel,
|
||||
TextElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
isTopLevelBlock,
|
||||
} from '@blocksuite/affine-shared/utils';
|
||||
import type {
|
||||
GfxBlockElementModel,
|
||||
GfxModel,
|
||||
GfxPrimitiveElementModel,
|
||||
GfxToolsFullOptionValue,
|
||||
@@ -33,10 +31,6 @@ import type { PointLocation } from '@blocksuite/global/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
export function isMindmapNode(element: GfxBlockElementModel | GfxModel | null) {
|
||||
return element?.group instanceof MindmapElementModel;
|
||||
}
|
||||
|
||||
export function isEdgelessTextBlock(
|
||||
element: BlockModel | GfxModel | null
|
||||
): element is EdgelessTextBlockModel {
|
||||
|
||||
Reference in New Issue
Block a user