From a21a0acebad03f2d8c7fa28344556a0f3f9e0307 Mon Sep 17 00:00:00 2001 From: L-Sun Date: Sun, 8 Sep 2024 13:44:07 +0800 Subject: [PATCH] refactor(edgeless): move element tree utilities to std --- .../src/element-model/mindmap.ts | 15 +- .../model/src/blocks/frame/frame-model.ts | 25 +- .../affine/model/src/elements/group/group.ts | 14 +- .../rects/edgeless-selected-rect.ts | 16 +- .../src/root-block/edgeless/frame-manager.ts | 60 ++-- .../root-block/edgeless/tools/default-tool.ts | 31 +- .../root-block/edgeless/tools/frame-tool.ts | 2 +- .../root-block/edgeless/utils/clone-utils.ts | 45 ++- .../src/root-block/edgeless/utils/tree.ts | 82 ------ .../edgeless-copilot-panel/toolbar-entry.ts | 22 +- .../release-from-group-button.ts | 6 +- .../block-std/src/gfx/gfx-block-model.ts | 24 +- packages/framework/block-std/src/gfx/index.ts | 6 + .../src/gfx/surface/element-model.ts | 40 ++- .../src/gfx/surface/surface-model.ts | 15 +- packages/framework/block-std/src/gfx/tree.ts | 264 ++++++++++++++++++ .../framework/block-std/src/utils/layer.ts | 9 +- 17 files changed, 462 insertions(+), 214 deletions(-) delete mode 100644 packages/blocks/src/root-block/edgeless/utils/tree.ts create mode 100644 packages/framework/block-std/src/gfx/tree.ts diff --git a/packages/affine/block-surface/src/element-model/mindmap.ts b/packages/affine/block-surface/src/element-model/mindmap.ts index 5d0735c1209b..469d8841a2e6 100644 --- a/packages/affine/block-surface/src/element-model/mindmap.ts +++ b/packages/affine/block-surface/src/element-model/mindmap.ts @@ -1,5 +1,6 @@ import type { BaseElementProps, + GfxModel, SerializedElement, SurfaceBlockModel, } from '@blocksuite/block-std/gfx'; @@ -856,24 +857,24 @@ export class MindmapElementModel extends GfxGroupLikeElementModel { - element.children?.forEach(child => { + const remove = (node: MindmapNode) => { + node.children?.forEach(child => { remove(child); }); - this.children?.delete(element.id); - removedDescendants.push(element.id); + this.children?.delete(node.id); + removedDescendants.push(node.id); }; surface.doc.transact(() => { - remove(this._nodeMap.get(id)!); + remove(this._nodeMap.get(element.id)!); }); queueMicrotask(() => { diff --git a/packages/affine/model/src/blocks/frame/frame-model.ts b/packages/affine/model/src/blocks/frame/frame-model.ts index b4fc73955a24..54f76d419498 100644 --- a/packages/affine/model/src/blocks/frame/frame-model.ts +++ b/packages/affine/model/src/blocks/frame/frame-model.ts @@ -1,9 +1,11 @@ import { + getDescendantElementsImpl, type GfxBlockElementModel, type GfxContainerElement, gfxContainerSymbol, type GfxElementGeometry, type GfxModel, + hasDescendantElementImpl, type PointTestOptions, SurfaceBlockModel, } from '@blocksuite/block-std/gfx'; @@ -71,11 +73,10 @@ export class FrameBlockModel return [...(this.childElementIds ? Object.keys(this.childElementIds) : [])]; } - addChild(element: BlockSuite.EdgelessModel | string): void { - const id = typeof element === 'string' ? element : element.id; + addChild(element: GfxModel) { this.doc.transact(() => { if (!this.childElementIds) this.childElementIds = {}; - this.childElementIds[id] = true; + this.childElementIds[element.id] = true; }); } @@ -83,9 +84,16 @@ export class FrameBlockModel return this.elementBound.contains(bound); } - hasDescendant(element: string | GfxModel): boolean { - const id = typeof element === 'string' ? element : element.id; - return !!this.childElementIds?.[id]; + getDescendantElements(): GfxModel[] { + return getDescendantElementsImpl(this); + } + + hasChild(element: GfxModel): boolean { + return this.childElementIds ? element.id in this.childElementIds : false; + } + + hasDescendantElement(element: GfxModel): boolean { + return hasDescendantElementImpl(this, element); } override includesPoint(x: number, y: number, _: PointTestOptions): boolean { @@ -100,10 +108,9 @@ export class FrameBlockModel ); } - removeChild(element: BlockSuite.EdgelessModel | string): void { - const id = typeof element === 'string' ? element : element.id; + removeChild(element: GfxModel): void { this.doc.transact(() => { - this.childElementIds && delete this.childElementIds[id]; + this.childElementIds && delete this.childElementIds[element.id]; }); } } diff --git a/packages/affine/model/src/elements/group/group.ts b/packages/affine/model/src/elements/group/group.ts index 54cdb3535ad5..0a1b10cb7c65 100644 --- a/packages/affine/model/src/elements/group/group.ts +++ b/packages/affine/model/src/elements/group/group.ts @@ -1,5 +1,6 @@ import type { BaseElementProps, + GfxModel, SerializedElement, } from '@blocksuite/block-std/gfx'; import type { Y } from '@blocksuite/store'; @@ -58,13 +59,9 @@ export class GroupElementModel extends GfxGroupLikeElementModel { - this.children.set(id, true); + this.children.set(element.id, true); }); } @@ -80,13 +77,12 @@ export class GroupElementModel extends GfxGroupLikeElementModel { - this.children.delete(id); + this.children.delete(element.id); }); } diff --git a/packages/blocks/src/root-block/edgeless/components/rects/edgeless-selected-rect.ts b/packages/blocks/src/root-block/edgeless/components/rects/edgeless-selected-rect.ts index 00145ff7acf0..909c03e626ca 100644 --- a/packages/blocks/src/root-block/edgeless/components/rects/edgeless-selected-rect.ts +++ b/packages/blocks/src/root-block/edgeless/components/rects/edgeless-selected-rect.ts @@ -83,7 +83,6 @@ import { isImageBlock, isNoteBlock, } from '../../utils/query.js'; -import { getTopElements } from '../../utils/tree.js'; import '../auto-complete/edgeless-auto-complete.js'; import '../connector/connector-handle.js'; import { HandleDirection } from '../resize/resize-handles.js'; @@ -1081,6 +1080,7 @@ export class EdgelessSelectedRect extends WithDisposable(LitElement) { #adjustFrame(frame: FrameBlockModel, bound: Bound) { const frameManager = this.edgeless.service.frame; + const treeManager = this.surface.model.tree; const oldChildren = frameManager.getChildElementsInFrame(frame); @@ -1088,13 +1088,13 @@ export class EdgelessSelectedRect extends WithDisposable(LitElement) { xywh: bound.serialize(), }); - const newChildren = getTopElements( - frameManager.getElementsInFrameBound(frame) - ).concat( - oldChildren.filter(oldChild => { - return frame.intersectsBound(oldChild.elementBound); - }) - ); + const newChildren = treeManager + .getTopElements(frameManager.getElementsInFrameBound(frame)) + .concat( + oldChildren.filter(oldChild => { + return frame.intersectsBound(oldChild.elementBound); + }) + ); frameManager.removeAllChildrenFromFrame(frame); frameManager.addElementsToFrame(frame, newChildren); diff --git a/packages/blocks/src/root-block/edgeless/frame-manager.ts b/packages/blocks/src/root-block/edgeless/frame-manager.ts index 6d6c8d3b212a..1a1d81d4a996 100644 --- a/packages/blocks/src/root-block/edgeless/frame-manager.ts +++ b/packages/blocks/src/root-block/edgeless/frame-manager.ts @@ -7,7 +7,11 @@ import { renderableInEdgeless, } from '@blocksuite/affine-block-surface'; import { GroupElementModel } from '@blocksuite/affine-model'; -import { isGfxContainerElm } from '@blocksuite/block-std/gfx'; +import { + getTopElements, + type GfxModel, + isGfxContainerElm, +} from '@blocksuite/block-std/gfx'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; import { Bound, @@ -22,7 +26,6 @@ import type { EdgelessRootService } from '../../index.js'; import { GfxBlockModel } from './block-model.js'; import { edgelessElementsBound } from './utils/bound-utils.js'; import { isFrameBlock } from './utils/query.js'; -import { getTopElements } from './utils/tree.js'; const MIN_FRAME_WIDTH = 800; const MIN_FRAME_HEIGHT = 640; @@ -33,7 +36,7 @@ export class FrameOverlay extends Overlay { private _frame: FrameBlockModel | null = null; - private _innerElements: BlockSuite.EdgelessModel[] = []; + private _innerElements: GfxModel[] = []; private get _frameManager() { return this._edgelessService.frame; @@ -117,7 +120,7 @@ export class EdgelessFrameManager { } constructor(private _rootService: EdgelessRootService) { - this._watchElementAddedOrDeleted(); + this._watchElementAdded(); } private _addFrameBlock(bound: Bound) { @@ -145,13 +148,19 @@ export class EdgelessFrameManager { return frameModel; } - private _watchElementAddedOrDeleted() { + private _watchElementAdded() { this._disposable.add( this._rootService.surface.elementAdded.on(({ id, local }) => { const element = this._rootService.surface.getElementById(id); if (element && local) { const frame = this.getFrameFromPoint(element.elementBound.center); + // if the container created with a frame, skip it. + // |<-- Container |< -- frame -->| Container -->| + if (isGfxContainerElm(element) && frame && element.hasChild(frame)) { + return; + } + // TODO(@L-Sun): refactor this in a tree manager if (element instanceof GroupElementModel) { if (frame && element.hasChild(frame)) return; @@ -208,10 +217,7 @@ export class EdgelessFrameManager { /** * Reset parent of elements to the frame */ - addElementsToFrame( - frame: FrameBlockModel, - elements: BlockSuite.EdgelessModel[] - ) { + addElementsToFrame(frame: FrameBlockModel, elements: GfxModel[]) { if (frame.childElementIds === undefined) { elements = [...elements, ...this.getChildElementsInFrame(frame)]; frame.childElementIds = {}; @@ -223,25 +229,8 @@ export class EdgelessFrameManager { if (elements.length === 0) return; - this._rootService.doc.transact(() => { - elements.forEach(element => { - // TODO(@L-Sun): refactor this. This branch is avoid circle, but it's better to handle in a tree manager - if (isGfxContainerElm(element) && element.childIds.includes(frame.id)) { - if (isFrameBlock(element)) { - this.removeParentFrame(frame); - } else if (element instanceof GroupElementModel) { - // eslint-disable-next-line unicorn/prefer-dom-node-remove - element.removeChild(frame.id); - } - } - - const parentFrame = this.getParentFrame(element); - if (parentFrame) { - // eslint-disable-next-line unicorn/prefer-dom-node-remove - parentFrame.removeChild(element); - } - frame.addChild(element); - }); + elements.forEach(element => { + frame.addChild(element); }); } @@ -263,7 +252,7 @@ export class EdgelessFrameManager { return frameModel; } - createFrameOnElements(elements: BlockSuite.EdgelessModel[]) { + createFrameOnElements(elements: GfxModel[]) { let bound = edgelessElementsBound( this._rootService.selection.selectedElements ); @@ -318,7 +307,7 @@ export class EdgelessFrameManager { * 1. The frame doesn't have `childElements`, return all elements in the frame bound but not owned by another frame. * 2. Return all child elements of the frame if `childElements` exists. */ - getChildElementsInFrame(frame: FrameBlockModel): BlockSuite.EdgelessModel[] { + getChildElementsInFrame(frame: FrameBlockModel): GfxModel[] { if (frame.childElementIds === undefined) { return this.getElementsInFrameBound(frame).filter( element => this.getParentFrame(element) !== null @@ -338,7 +327,7 @@ export class EdgelessFrameManager { */ getElementsInFrameBound(frame: FrameBlockModel, fullyContained = true) { const bound = Bound.deserialize(frame.xywh); - const elements: BlockSuite.EdgelessModel[] = this._rootService.gfx.grid + const elements: GfxModel[] = this._rootService.gfx.grid .search(bound, fullyContained) .filter(element => element !== frame); @@ -358,10 +347,9 @@ export class EdgelessFrameManager { return null; } - getParentFrame(element: BlockSuite.EdgelessModel) { - return this.frames.find(frame => { - return frame.childIds.includes(element.id); - }); + getParentFrame(element: GfxModel) { + const container = element.container; + return container && isFrameBlock(container) ? container : null; } removeAllChildrenFromFrame(frame: FrameBlockModel) { @@ -370,7 +358,7 @@ export class EdgelessFrameManager { }); } - removeParentFrame(element: BlockSuite.EdgelessModel) { + removeParentFrame(element: GfxModel) { const parentFrame = this.getParentFrame(element); if (parentFrame) { // eslint-disable-next-line unicorn/prefer-dom-node-remove diff --git a/packages/blocks/src/root-block/edgeless/tools/default-tool.ts b/packages/blocks/src/root-block/edgeless/tools/default-tool.ts index 7c07d51394aa..8846ce27aba9 100644 --- a/packages/blocks/src/root-block/edgeless/tools/default-tool.ts +++ b/packages/blocks/src/root-block/edgeless/tools/default-tool.ts @@ -5,7 +5,6 @@ import type { NoteBlockModel, } from '@blocksuite/affine-model'; import type { PointerEventState } from '@blocksuite/block-std'; -import type { PointTestOptions } from '@blocksuite/block-std/gfx'; import type { IVec } from '@blocksuite/global/utils'; import { @@ -26,6 +25,10 @@ import { handleNativeRangeAtPoint, resetNativeSelection, } from '@blocksuite/affine-shared/utils'; +import { + isGfxContainerElm, + type PointTestOptions, +} from '@blocksuite/block-std/gfx'; import { Bound, DisposableGroup, @@ -54,7 +57,6 @@ import { mountShapeTextEditor, mountTextElementEditor, } from '../utils/text.js'; -import { getAllDescendantElements, getTopElements } from '../utils/tree.js'; import { EdgelessToolController } from './edgeless-tool.js'; export enum DefaultModeDragType { @@ -223,7 +225,10 @@ export class DefaultToolController extends EdgelessToolController { const h = Math.abs(startY - curY); const bound = new Bound(x, y, w, h); - const elements = getTopElements(service.gfx.getElementsByBound(bound)); + const treeManager = this._surface.model.tree; + const elements = treeManager.getTopElements( + service.gfx.getElementsByBound(bound) + ); const set = new Set( tools.shiftKey ? [...elements, ...selection.selectedElements] : elements @@ -923,7 +928,8 @@ export class DefaultToolController extends EdgelessToolController { { const frameManager = this._service.frame; - const toBeMovedTopElements = getTopElements(this._toBeMoved); + const treeManager = this._surface.model.tree; + const toBeMovedTopElements = treeManager.getTopElements(this._toBeMoved); if (this._hoveredFrame) { frameManager.addElementsToFrame( this._hoveredFrame, @@ -1021,17 +1027,18 @@ export class DefaultToolController extends EdgelessToolController { const elements = this.edgelessSelectionManager.selectedElements; const toBeMoved = new Set(elements); + const tree = this._surface.model.tree; elements.forEach(element => { if (element.group instanceof MindmapElementModel && elements.length > 1) { - getAllDescendantElements(element.group).forEach(ele => - toBeMoved.add(ele) - ); - } else { - getAllDescendantElements(element).forEach(ele => { + tree + .getDescendantElements(element.group) + .forEach(ele => toBeMoved.add(ele)); + } else if (isGfxContainerElm(element)) { + tree.getDescendantElements(element).forEach(ele => { if (ele.group instanceof MindmapElementModel) { - getAllDescendantElements(ele.group).forEach(_el => - toBeMoved.add(_el) - ); + tree + .getDescendantElements(ele.group) + .forEach(_el => toBeMoved.add(_el)); } toBeMoved.add(ele); }); diff --git a/packages/blocks/src/root-block/edgeless/tools/frame-tool.ts b/packages/blocks/src/root-block/edgeless/tools/frame-tool.ts index c924c0f1d39c..26bcb1a7c82c 100644 --- a/packages/blocks/src/root-block/edgeless/tools/frame-tool.ts +++ b/packages/blocks/src/root-block/edgeless/tools/frame-tool.ts @@ -3,10 +3,10 @@ import type { PointerEventState } from '@blocksuite/block-std'; import type { IPoint, IVec } from '@blocksuite/global/utils'; import { TelemetryProvider } from '@blocksuite/affine-shared/services'; +import { getTopElements } from '@blocksuite/block-std/gfx'; import { Bound, noop, Vec } from '@blocksuite/global/utils'; import { DocCollection } from '@blocksuite/store'; -import { getTopElements } from '../utils/tree.js'; import { EdgelessToolController } from './edgeless-tool.js'; type FrameTool = { diff --git a/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts b/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts index 571f5e31c4eb..2a7fc7f2fc83 100644 --- a/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts +++ b/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts @@ -15,32 +15,41 @@ import { GroupElementModel, } from '@blocksuite/affine-model'; import { + type GfxModel, isGfxContainerElm, type SerializedElement, } from '@blocksuite/block-std/gfx'; import { type BlockSnapshot, Job } from '@blocksuite/store'; import { GfxBlockModel } from '../block-model.js'; -import { getAllDescendantElements, getTopElements } from './tree.js'; /** * return all elements in the tree of the elements */ -export function getSortedCloneElements(elements: BlockSuite.EdgelessModel[]) { - const set = new Set(); +export function getSortedCloneElements(elements: GfxModel[]) { + if (elements.length === 0) return []; + const surface = elements[0].surface; + if (!surface) return []; + + const treeManager = surface.tree; + + const set = new Set(); elements.forEach(element => { // this element subtree has been added if (set.has(element)) return; - getAllDescendantElements(element, true).map(descendant => - set.add(descendant) - ); + set.add(element); + if (isGfxContainerElm(element)) { + treeManager + .getDescendantElements(element) + .map(descendant => set.add(descendant)); + } }); return sortEdgelessElements([...set]); } export async function prepareCloneData( - elements: BlockSuite.EdgelessModel[], + elements: GfxModel[], std: BlockStdScope ) { const job = new Job({ @@ -56,8 +65,8 @@ export async function prepareCloneData( } export async function serializeElement( - element: BlockSuite.EdgelessModel, - elements: BlockSuite.EdgelessModel[], + element: GfxModel, + elements: GfxModel[], job: Job ) { if (element instanceof GfxBlockModel) { @@ -75,7 +84,7 @@ export async function serializeElement( export function serializeConnector( connector: ConnectorElementModel, - elements: BlockSuite.EdgelessModel[] + elements: GfxModel[] ) { const sourceId = connector.source?.id; const targetId = connector.target?.id; @@ -99,18 +108,24 @@ export function serializeConnector( * @param elements edgeless model list * @returns sorted edgeless model list */ -export function sortEdgelessElements(elements: BlockSuite.EdgelessModel[]) { +export function sortEdgelessElements(elements: GfxModel[]) { // Since each element has a parent-child relationship, and from-to connector relationship // the child element must be added before the parent element // and the connected elements must be added before the connector element // To achieve this, we do a post-order traversal of the tree - const result: BlockSuite.EdgelessModel[] = []; + if (elements.length === 0) return []; + const surface = elements[0].surface; + if (!surface) return []; + + const result: GfxModel[] = []; + + const treeManager = surface.tree; - const topElements = getTopElements(elements); + const topElements = treeManager.getTopElements(elements); // the connector element must be added after the connected elements - const moveConnectorToEnd = (elements: BlockSuite.EdgelessModel[]) => { + const moveConnectorToEnd = (elements: GfxModel[]) => { const connectors = elements.filter( element => element instanceof ConnectorElementModel ); @@ -120,7 +135,7 @@ export function sortEdgelessElements(elements: BlockSuite.EdgelessModel[]) { return [...rest, ...connectors]; }; - const traverse = (element: BlockSuite.EdgelessModel) => { + const traverse = (element: GfxModel) => { if (isGfxContainerElm(element)) { moveConnectorToEnd(element.childElements).forEach(child => traverse(child) diff --git a/packages/blocks/src/root-block/edgeless/utils/tree.ts b/packages/blocks/src/root-block/edgeless/utils/tree.ts deleted file mode 100644 index 0e72f5dd1bfa..000000000000 --- a/packages/blocks/src/root-block/edgeless/utils/tree.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { isGfxContainerElm } from '@blocksuite/block-std/gfx'; - -/** - * Get the top elements from the list of elements, which are in some tree structures. - * - * For example: a list `[F1, F2, E6, E1, G1, E5, E2, E3, E4]`, - * and they are in the edgeless container tree structure: - * ``` - * F1 F2 E6 - * / \ | - * E1 G1 E5 - * / \ - * E2 G2* - * / \ - * E3 E4 - * ``` - * where the star symbol `*` represent it is not in the list. - * - * The result should be `[F1, F2, E6, E3, E4]`. - */ -export function getTopElements(elements: BlockSuite.EdgelessModel[]) { - const topElements = new Set(elements); - const visitedElements = new Map(); - elements.forEach(element => { - visitedElements.set(element, false); - }); - - const traverse = (element: BlockSuite.EdgelessModel) => { - // Skip if not in the list - if (!visitedElements.has(element)) return; - - // Skip if already visited, its children also are already visited - if (visitedElements.get(element)) return; - - visitedElements.set(element, true); - - if (isGfxContainerElm(element)) { - element.childElements.forEach(child => { - topElements.delete(child); - traverse(child); - }); - } - }; - - visitedElements.forEach((_, element) => { - traverse(element); - }); - - return [...topElements]; -} - -/** - * Get all descendant elements of the given element. - */ -export function getAllDescendantElements( - element: BlockSuite.EdgelessModel, - includeSelf = false -) { - const elements: BlockSuite.EdgelessModel[] = []; - - const traverse = (element: BlockSuite.EdgelessModel) => { - elements.push(element); - - if (isGfxContainerElm(element)) { - element.childElements.forEach(child => { - traverse(child); - }); - } - }; - - if (includeSelf) { - traverse(element); - } else { - if (isGfxContainerElm(element)) { - element.childElements.forEach(child => { - traverse(child); - }); - } - } - - return elements; -} diff --git a/packages/blocks/src/root-block/widgets/edgeless-copilot-panel/toolbar-entry.ts b/packages/blocks/src/root-block/widgets/edgeless-copilot-panel/toolbar-entry.ts index 9a0e6c563669..1e9f78281716 100644 --- a/packages/blocks/src/root-block/widgets/edgeless-copilot-panel/toolbar-entry.ts +++ b/packages/blocks/src/root-block/widgets/edgeless-copilot-panel/toolbar-entry.ts @@ -1,5 +1,6 @@ import { AIStarIcon } from '@blocksuite/affine-components/icons'; import { type EditorHost, WithDisposable } from '@blocksuite/block-std'; +import { isGfxContainerElm } from '@blocksuite/block-std/gfx'; import { css, html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; @@ -7,7 +8,7 @@ import type { AIItemGroupConfig } from '../../../_common/components/ai-item/type import type { EdgelessRootBlockComponent } from '../../edgeless/edgeless-root-block.js'; import type { CopilotSelectionController } from '../../edgeless/tools/copilot-tool.js'; -import { getAllDescendantElements } from '../../edgeless/utils/tree.js'; +import { sortEdgelessElements } from '../../edgeless/utils/clone-utils.js'; @customElement('edgeless-copilot-toolbar-entry') export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) { @@ -22,12 +23,23 @@ export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) { `; private _showCopilotPanel() { - const selectedElements = this.edgeless.service.selection.selectedElements; + const treeManager = this.edgeless.surfaceBlockModel.tree; + const selectedElements = sortEdgelessElements( + this.edgeless.service.selection.selectedElements + ); const toBeSelected = new Set(selectedElements); + selectedElements.forEach(element => { - getAllDescendantElements(element).forEach(descendant => { - toBeSelected.add(descendant); - }); + // its descendants are already selected + if (toBeSelected.has(element)) return; + + toBeSelected.add(element); + + if (isGfxContainerElm(element)) { + treeManager.getDescendantElements(element).forEach(descendant => { + toBeSelected.add(descendant); + }); + } }); this.edgeless.service.tool.setEdgelessTool({ diff --git a/packages/blocks/src/root-block/widgets/element-toolbar/release-from-group-button.ts b/packages/blocks/src/root-block/widgets/element-toolbar/release-from-group-button.ts index d8595f8df42b..2d19a4a2faea 100644 --- a/packages/blocks/src/root-block/widgets/element-toolbar/release-from-group-button.ts +++ b/packages/blocks/src/root-block/widgets/element-toolbar/release-from-group-button.ts @@ -11,13 +11,13 @@ export class EdgelessReleaseFromGroupButton extends WithDisposable(LitElement) { private _releaseFromGroup() { const service = this.edgeless.service; const element = service.selection.firstElement; + const treeManager = this.edgeless.surface.model.tree; if (!(element.group instanceof GroupElementModel)) return; const group = element.group; - // eslint-disable-next-line unicorn/prefer-dom-node-remove - group.removeChild(element); + treeManager.removeChildFromContainer(element); element.index = service.layer.generateIndex( 'flavour' in element ? element.flavour : element.type @@ -25,7 +25,7 @@ export class EdgelessReleaseFromGroupButton extends WithDisposable(LitElement) { const parent = group.group; if (parent instanceof GroupElementModel) { - parent.addChild(element.id); + treeManager.addChildToContainer(parent, element); } } diff --git a/packages/framework/block-std/src/gfx/gfx-block-model.ts b/packages/framework/block-std/src/gfx/gfx-block-model.ts index 1a1a97f6a7ca..55b5faf81777 100644 --- a/packages/framework/block-std/src/gfx/gfx-block-model.ts +++ b/packages/framework/block-std/src/gfx/gfx-block-model.ts @@ -20,6 +20,7 @@ import { BlockModel } from '@blocksuite/store'; import type { EditorHost } from '../view/index.js'; import type { GfxCompatibleProps, + GfxContainerElement, GfxElementGeometry, GfxGroupLikeElementModel, GfxPrimitiveElementModel, @@ -40,6 +41,10 @@ export class GfxBlockElementModel< rotate = 0; + get container(): (GfxModel & GfxContainerElement) | null { + return this.surface?.getContainer(this.id) ?? null; + } + get elementBound() { const bound = Bound.deserialize(this.xywh); return Bound.from(getBoundsWithRotation({ ...bound, rotate: this.rotate })); @@ -58,23 +63,22 @@ export class GfxBlockElementModel< } get group(): GfxGroupLikeElementModel | null { - const surface = this.doc - .getBlocks() - .find(block => block instanceof SurfaceBlockModel); - - if (!surface) return null; + if (!this.surface) return null; - return (surface as SurfaceBlockModel).getGroup(this.id) ?? null; + return this.surface.getGroup(this.id) ?? null; } get groups(): GfxGroupLikeElementModel[] { + if (!this.surface) return []; + + return this.surface.getGroups(this.id); + } + + get surface(): SurfaceBlockModel | null { const surface = this.doc .getBlocks() .find(block => block instanceof SurfaceBlockModel); - - if (!surface) return []; - - return (surface as SurfaceBlockModel).getGroups(this.id); + return surface ?? null; } containsBound(bounds: Bound): boolean { diff --git a/packages/framework/block-std/src/gfx/index.ts b/packages/framework/block-std/src/gfx/index.ts index 51c987b8d5a5..912fee71ecf7 100644 --- a/packages/framework/block-std/src/gfx/index.ts +++ b/packages/framework/block-std/src/gfx/index.ts @@ -33,6 +33,12 @@ export { type SurfaceBlockProps, type SurfaceMiddleware, } from './surface/surface-model.js'; +export { + getAncestorContainersImpl, + getDescendantElementsImpl, + getTopElements, + hasDescendantElementImpl, +} from './tree.js'; export * from './viewport.js'; export { GfxViewportElement } from './viewport-element.js'; diff --git a/packages/framework/block-std/src/gfx/surface/element-model.ts b/packages/framework/block-std/src/gfx/surface/element-model.ts index c179050aa594..beeedd253830 100644 --- a/packages/framework/block-std/src/gfx/surface/element-model.ts +++ b/packages/framework/block-std/src/gfx/surface/element-model.ts @@ -20,6 +20,10 @@ import { createMutex } from 'lib0/mutex'; import type { GfxBlockElementModel, GfxModel } from '../gfx-block-model.js'; import type { SurfaceBlockModel } from './surface-model.js'; +import { + getDescendantElementsImpl, + hasDescendantElementImpl, +} from '../tree.js'; import { convertProps, field, @@ -94,7 +98,13 @@ export interface GfxContainerElement extends GfxCompatibleProps { [gfxContainerSymbol]: true; childIds: string[]; childElements: GfxModel[]; - hasDescendant(element: string | GfxModel): boolean; + + addChild(element: GfxModel): void; + removeChild(element: GfxModel): void; + hasChild(element: GfxModel): boolean; + + hasDescendantElement(element: GfxModel): boolean; + getDescendantElements(): GfxModel[]; } export abstract class GfxPrimitiveElementModel< @@ -134,6 +144,10 @@ export abstract class GfxPrimitiveElementModel< return true; } + get container() { + return this.surface.getContainer(this.id); + } + get deserializedXYWH() { if (!this._lastXYWH || this.xywh !== this._lastXYWH) { const xywh = this.xywh; @@ -466,6 +480,8 @@ export abstract class GfxGroupLikeElementModel< }); } + abstract addChild(element: GfxModel): void; + /** * @deprecated Use `getAllDescendantElements` instead. * Get all descendants of this group @@ -485,33 +501,29 @@ export abstract class GfxGroupLikeElementModel< }, [] as GfxModel[]); } + getDescendantElements(): GfxModel[] { + return getDescendantElementsImpl(this); + } + /** * The actual field that stores the children of the group. * It should be a ymap decorated with `@field`. */ - hasChild(element: string | GfxModel) { - return ( - (typeof element === 'string' - ? this.children?.has(element) - : this.children?.has(element.id)) ?? false - ); + hasChild(element: GfxModel) { + return this.childElements.includes(element); } /** * Check if the group has the given descendant. */ - hasDescendant(element: string | GfxModel) { - const groups = this.surface.getGroups( - typeof element === 'string' ? element : element.id - ); - - return groups.some(group => group.id === this.id); + hasDescendantElement(element: GfxModel): boolean { + return hasDescendantElementImpl(this, element); } /** * Remove the child from the group */ - abstract removeChild(id: string): void; + abstract removeChild(element: GfxModel): void; /** * Set the new value of the childIds diff --git a/packages/framework/block-std/src/gfx/surface/surface-model.ts b/packages/framework/block-std/src/gfx/surface/surface-model.ts index 99197967f3a0..7e234ecc7899 100644 --- a/packages/framework/block-std/src/gfx/surface/surface-model.ts +++ b/packages/framework/block-std/src/gfx/surface/surface-model.ts @@ -3,13 +3,14 @@ import type { Boxed, Y } from '@blocksuite/store'; import { type Constructor, Slot } from '@blocksuite/global/utils'; import { BlockModel, DocCollection, nanoid } from '@blocksuite/store'; +import { TreeManager } from '../tree.js'; import { createDecoratorState } from './decorators/common.js'; import { initializeObservers, initializeWatchers } from './decorators/index.js'; -import { syncElementFromY } from './element-model.js'; import { type BaseElementProps, GfxGroupLikeElementModel, GfxPrimitiveElementModel, + syncElementFromY, } from './element-model.js'; export type SurfaceBlockProps = { @@ -56,6 +57,8 @@ export class SurfaceBlockModel extends BlockModel { protected _surfaceBlockModel = true; + protected _tree = new TreeManager(this); + elementAdded = new Slot<{ id: string; local: boolean }>(); elementRemoved = new Slot<{ @@ -319,6 +322,11 @@ export class SurfaceBlockModel extends BlockModel { }); } + private _initTreeWatcher() { + const disposable = this._tree.watch(); + this.deleted.on(() => disposable.dispose()); + } + private _propsToY(type: string, props: Record) { const ctor = this._elementCtorMap[type]; @@ -436,6 +444,7 @@ export class SurfaceBlockModel extends BlockModel { protected _init() { this._initElementModels(); + this._initTreeWatcher(); this._watchGroupRelationChange(); this.applyMiddlewares(); } @@ -481,6 +490,10 @@ export class SurfaceBlockModel extends BlockModel { this.hooks.remove.dispose(); } + getContainer(elementId: string) { + return this._tree.getContainer(elementId); + } + getElementById(id: string): GfxPrimitiveElementModel | null { return this._elementModels.get(id)?.model ?? null; } diff --git a/packages/framework/block-std/src/gfx/tree.ts b/packages/framework/block-std/src/gfx/tree.ts new file mode 100644 index 000000000000..0da30f16e931 --- /dev/null +++ b/packages/framework/block-std/src/gfx/tree.ts @@ -0,0 +1,264 @@ +import { DisposableGroup } from '@blocksuite/global/utils'; + +import { GfxBlockElementModel, type GfxModel } from './gfx-block-model.js'; +import { + type GfxContainerElement, + GfxGroupLikeElementModel, + isGfxContainerElm, +} from './surface/element-model.js'; +import { SurfaceBlockModel } from './surface/surface-model.js'; + +/** + * Get the top elements from the list of elements, which are in some tree structures. + * + * For example: a list `[C1, E1, C2, E2, E2, E3, E4, C4, E6]`, + * and they are in the elements tree like: + * ``` + * C1 C4 E6 + * / \ | + * E1 C2 E5 + * / \ + * E2 C3* + * / \ + * E3 E4 + * ``` + * where the star symbol `*` denote it is not in the list. + * + * The result should be `[F1, F2, E6, E3, E4]`. + */ +export function getTopElements(elements: GfxModel[]): GfxModel[] { + const results = new Set(elements); + + elements = [...new Set(elements)]; + + elements.forEach(e1 => { + elements.forEach(e2 => { + if (isGfxContainerElm(e1) && e1.hasDescendantElement(e2)) { + results.delete(e2); + } + }); + }); + + return [...results]; +} + +export class TreeManager { + private _elementToContainer = new Map(); + + constructor(readonly surface: SurfaceBlockModel) {} + + getContainer(elementId: string): (GfxModel & GfxContainerElement) | null { + const containerId = this._elementToContainer.get(elementId); + if (!containerId) return null; + + const canvasElement = this.surface.getElementById(containerId); + if (isGfxContainerElm(canvasElement)) return canvasElement; + + const blockModel = this.surface.doc.getBlock(containerId)?.model; + if (isGfxContainerElm(blockModel)) { + return blockModel as GfxContainerElement & GfxModel; + } + + console.warn( + `the found container id(${containerId}) dose not refer to a container element(${canvasElement || blockModel})` + ); + return null; + } + + watch() { + const onGfxModelAdded = (model: GfxModel) => { + if (!isGfxContainerElm(model)) return; + model.childElements.forEach(child => { + const prevContainer = this.getContainer(child.id); + // eslint-disable-next-line unicorn/prefer-dom-node-remove + prevContainer?.removeChild(child); + + this._elementToContainer.set(child.id, model.id); + }); + }; + + const onGfxModelDeleted = (model: GfxModel) => { + const container = this.getContainer(model.id); + // eslint-disable-next-line unicorn/prefer-dom-node-remove + container?.removeChild(model); + + if (isGfxContainerElm(model)) { + model.childElements.forEach(child => { + this._elementToContainer.delete(child.id); + }); + } + }; + + const onGfxContainerUpdated = (model: GfxModel) => { + if (!isGfxContainerElm(model)) return; + + const previousChildrenIds = new Set(); + this._elementToContainer.forEach((containerId, elementId) => { + if (containerId === model.id) previousChildrenIds.add(elementId); + }); + + model.childIds.forEach(childId => { + this._elementToContainer.set(childId, model.id); + previousChildrenIds.delete(childId); + }); + + previousChildrenIds.forEach(prevChildId => { + this._elementToContainer.delete(prevChildId); + }); + }; + + const disposable = new DisposableGroup(); + + // Graphic Block Elements + + const { doc } = this.surface; + const elements = doc + .getBlocks() + .filter( + model => + model instanceof GfxBlockElementModel && + (model.parent instanceof SurfaceBlockModel || + model.parent?.role === 'root') + ) as GfxModel[]; + + elements.forEach(el => { + if (isGfxContainerElm(el)) { + el.childElements.forEach(child => { + this._elementToContainer.set(child.id, el.id); + }); + } + }); + + disposable.add( + doc.slots.blockUpdated.on(payload => { + if (payload.type === 'add') { + const { model } = payload; + if (model instanceof GfxBlockElementModel) { + onGfxModelAdded(model); + } + } else if (payload.type === 'delete') { + const { model } = payload; + if (model instanceof GfxBlockElementModel) { + onGfxModelDeleted(model); + } + } else if (payload.type === 'update') { + const model = doc.getBlock(payload.id)?.model; + if (!(model instanceof GfxBlockElementModel)) return; + if (!isGfxContainerElm(model)) return; + + // Since the implement of GfxContainer may be different, + // listen to the change of the children of container based on `blockUpdated` is difficult. + // TODO(@L-Sun): remove this speed up branch if we can listen the change of children of container + if ( + payload.flavour === 'affine:frame' && + payload.props.key !== 'childElementIds' + ) { + return; + } + + onGfxContainerUpdated( + model as GfxBlockElementModel & GfxContainerElement + ); + } + }) + ); + + // Canvas Elements + + this.surface.elementModels.forEach(el => { + if (isGfxContainerElm(el)) { + el.childElements.forEach(child => { + this._elementToContainer.set(child.id, el.id); + }); + } + }); + + disposable.add( + this.surface.elementAdded.on(({ id }) => { + const element = this.surface.getElementById(id); + element && onGfxModelAdded(element); + }) + ); + + disposable.add( + this.surface.elementRemoved.on(({ model }) => { + onGfxModelDeleted(model); + }) + ); + + disposable.add( + this.surface.elementUpdated.on(({ id, oldValues }) => { + const element = this.surface.getElementById(id); + if (!isGfxContainerElm(element)) return; + + // Since the implement of GfxContainer may be different, + // listen to the change of the children of container is difficult + // TODO(@L-Sun): remove this speed up branch if we can listen the change of children of container + if ( + element instanceof GfxGroupLikeElementModel && + !oldValues['childIds'] + ) + return; + + onGfxContainerUpdated(element); + }) + ); + + return disposable; + } +} + +function traverse( + element: GfxModel, + preCallback?: (element: GfxModel) => void | boolean, + postCallBack?: (element: GfxModel) => void +) { + if (preCallback) { + const interrupt = preCallback(element); + if (interrupt) return; + } + + if (isGfxContainerElm(element)) { + element.childElements.forEach(child => { + traverse(child, preCallback, postCallBack); + }); + } + + postCallBack && postCallBack(element); +} + +export function getAncestorContainersImpl(element: GfxModel) { + const containers: (GfxContainerElement & GfxModel)[] = []; + + let container = element.container; + while (container) { + containers.push(container); + container = container.container; + } + + return containers; +} + +export function getDescendantElementsImpl( + container: GfxContainerElement +): GfxModel[] { + const results: GfxModel[] = []; + container.childElements.forEach(child => { + traverse(child, element => { + results.push(element); + }); + }); + return results; +} + +export function hasDescendantElementImpl( + container: GfxContainerElement, + element: GfxModel +): boolean { + let _container = element.container; + while (_container) { + if (_container === container) return true; + _container = _container.container; + } + return false; +} diff --git a/packages/framework/block-std/src/utils/layer.ts b/packages/framework/block-std/src/utils/layer.ts index 21f0cb83b673..41ac43e636fe 100644 --- a/packages/framework/block-std/src/utils/layer.ts +++ b/packages/framework/block-std/src/utils/layer.ts @@ -97,9 +97,14 @@ export function renderableInEdgeless( * @returns */ export function compare(a: GfxModel, b: GfxModel) { - if (isGfxContainerElm(a) && a.hasDescendant(b)) { + const surface = a.surface ?? b.surface; + if (!surface) return SortOrder.SAME; + + const { tree } = surface; + + if (isGfxContainerElm(a) && tree.hasDescendantElement(a, b)) { return SortOrder.BEFORE; - } else if (isGfxContainerElm(b) && b.hasDescendant(a)) { + } else if (isGfxContainerElm(b) && tree.hasDescendantElement(b, a)) { return SortOrder.AFTER; } else { const aGroups = a.groups as GfxContainerElement[];