From 87b543d047b00ef4656aed993dbdd61fabc12dee 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 --- .../model/src/blocks/frame/frame-model.ts | 38 ++- .../affine/model/src/elements/group/group.ts | 23 +- .../model/src/elements/mindmap/mindmap.ts | 24 +- .../auto-complete/auto-complete-panel.ts | 14 +- .../components/auto-complete/utils.ts | 34 ++- .../rects/edgeless-selected-rect.ts | 2 +- .../edgeless/edgeless-root-service.ts | 15 +- .../src/root-block/edgeless/frame-manager.ts | 36 +-- .../root-block/edgeless/tools/default-tool.ts | 18 +- .../root-block/edgeless/tools/frame-tool.ts | 2 +- .../root-block/edgeless/utils/clone-utils.ts | 38 ++- .../src/root-block/edgeless/utils/group.ts | 5 +- .../src/root-block/edgeless/utils/tree.ts | 82 ------ .../edgeless-copilot-panel/toolbar-entry.ts | 21 +- .../release-from-group-button.ts | 3 +- .../block-std/src/gfx/gfx-block-model.ts | 24 +- packages/framework/block-std/src/gfx/index.ts | 6 + .../src/gfx/surface/element-model.ts | 61 ++-- .../src/gfx/surface/surface-model.ts | 99 ++----- packages/framework/block-std/src/gfx/tree.ts | 277 ++++++++++++++++++ .../framework/block-std/src/utils/layer.ts | 3 + .../__tests__/edgeless/surface-model.spec.ts | 4 - tests/edgeless/group/clipboard.spec.ts | 2 +- 23 files changed, 516 insertions(+), 315 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/model/src/blocks/frame/frame-model.ts b/packages/affine/model/src/blocks/frame/frame-model.ts index 89da409bb4361..f9541b8f8de45 100644 --- a/packages/affine/model/src/blocks/frame/frame-model.ts +++ b/packages/affine/model/src/blocks/frame/frame-model.ts @@ -1,10 +1,15 @@ +import type { + GfxBlockElementModel, + GfxContainerElement, + GfxElementGeometry, + GfxModel, + PointTestOptions, +} from '@blocksuite/block-std/gfx'; + import { - type GfxBlockElementModel, - type GfxContainerElement, + descendantElementsImpl, gfxContainerSymbol, - type GfxElementGeometry, - type GfxModel, - type PointTestOptions, + hasDescendantElementImpl, SurfaceBlockModel, } from '@blocksuite/block-std/gfx'; import { Bound, type SerializedXYWH } from '@blocksuite/global/utils'; @@ -71,10 +76,13 @@ export class FrameBlockModel return [...(this.childElementIds ? Object.keys(this.childElementIds) : [])]; } - addChild(element: BlockSuite.EdgelessModel | string): void { - const id = typeof element === 'string' ? element : element.id; + get descendantElements(): GfxModel[] { + return descendantElementsImpl(this); + } + + addChild(element: GfxModel) { this.doc.transact(() => { - this.childElementIds = { ...this.childElementIds, [id]: true }; + this.childElementIds = { ...this.childElementIds, [element.id]: true }; }); } @@ -99,9 +107,12 @@ 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]; + hasChild(element: GfxModel): boolean { + return this.childElementIds ? element.id in this.childElementIds : false; + } + + hasDescendant(element: GfxModel): boolean { + return hasDescendantElementImpl(this, element); } override includesPoint(x: number, y: number, _: PointTestOptions): boolean { @@ -116,10 +127,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 54cdb3535ad5f..9bed5820dcc11 100644 --- a/packages/affine/model/src/elements/group/group.ts +++ b/packages/affine/model/src/elements/group/group.ts @@ -1,7 +1,9 @@ import type { BaseElementProps, + GfxModel, SerializedElement, } from '@blocksuite/block-std/gfx'; +import type { IVec, PointLocation } from '@blocksuite/global/utils'; import type { Y } from '@blocksuite/store'; import { @@ -10,13 +12,7 @@ import { local, observe, } from '@blocksuite/block-std/gfx'; -import { - Bound, - type IVec, - keys, - linePolygonIntersects, - type PointLocation, -} from '@blocksuite/global/utils'; +import { Bound, keys, linePolygonIntersects } from '@blocksuite/global/utils'; import { DocCollection } from '@blocksuite/store'; type GroupElementProps = BaseElementProps & { @@ -58,13 +54,9 @@ export class GroupElementModel extends GfxGroupLikeElementModel { - this.children.set(id, true); + this.children.set(element.id, true); }); } @@ -80,13 +72,12 @@ export class GroupElementModel extends GfxGroupLikeElementModel { - this.children.delete(id); + this.children.delete(element.id); }); } diff --git a/packages/affine/model/src/elements/mindmap/mindmap.ts b/packages/affine/model/src/elements/mindmap/mindmap.ts index 4d32b81cdb76b..706f0b537b32b 100644 --- a/packages/affine/model/src/elements/mindmap/mindmap.ts +++ b/packages/affine/model/src/elements/mindmap/mindmap.ts @@ -1,5 +1,6 @@ import type { BaseElementProps, + GfxModel, SerializedElement, } from '@blocksuite/block-std/gfx'; import type { SerializedXYWH, XYWH } from '@blocksuite/global/utils'; @@ -16,6 +17,7 @@ import { deserializeXYWH, keys, last, + noop, pick, } from '@blocksuite/global/utils'; import { DocCollection, type Y } from '@blocksuite/store'; @@ -227,6 +229,14 @@ 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/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts b/packages/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts index 16bb3940e6d74..a4ac9e940f041 100644 --- a/packages/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts +++ b/packages/blocks/src/root-block/edgeless/components/auto-complete/auto-complete-panel.ts @@ -1,7 +1,6 @@ import type { Connection, ConnectorElementModel, - NoteBlockModel, ShapeElementModel, } from '@blocksuite/affine-model'; import type { XYWH } from '@blocksuite/global/utils'; @@ -25,6 +24,7 @@ import { FontWeight, getShapeName, GroupElementModel, + NoteBlockModel, ShapeStyle, TextElementModel, } from '@blocksuite/affine-model'; @@ -184,11 +184,13 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) { }, doc.root?.id ); + const note = doc.getBlock(id)?.model; + assertInstanceOf(note, NoteBlockModel); doc.addBlock('affine:paragraph', { type: 'text' }, id); const group = this.currentSource.group; if (group instanceof GroupElementModel) { - group.addChild(id); + group.addChild(note); } this.connector.target = { id, @@ -245,11 +247,15 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) { y: bound.y, }); if (!textId) return; + + const textElement = edgelessService.getElementById(textId); + if (!textElement) return; + edgelessService.updateElement(this.connector.id, { target: { id: textId, position }, }); if (this.currentSource.group instanceof GroupElementModel) { - this.currentSource.group.addChild(textId); + this.currentSource.group.addChild(textElement); } this.edgeless.service.selection.set({ @@ -275,7 +281,7 @@ export class EdgelessAutoCompletePanel extends WithDisposable(LitElement) { target: { id: textId, position }, }); if (this.currentSource.group instanceof GroupElementModel) { - this.currentSource.group.addChild(textId); + this.currentSource.group.addChild(textElement); } this.edgeless.service.selection.set({ diff --git a/packages/blocks/src/root-block/edgeless/components/auto-complete/utils.ts b/packages/blocks/src/root-block/edgeless/components/auto-complete/utils.ts index f29c32712c3b7..68caac38377e1 100644 --- a/packages/blocks/src/root-block/edgeless/components/auto-complete/utils.ts +++ b/packages/blocks/src/root-block/edgeless/components/auto-complete/utils.ts @@ -1,3 +1,4 @@ +import type { GfxModel } from '@blocksuite/block-std/gfx'; import type { XYWH } from '@blocksuite/global/utils'; import { @@ -16,7 +17,8 @@ import { type ShapeName, type ShapeStyle, } from '@blocksuite/affine-model'; -import { assertExists, Bound } from '@blocksuite/global/utils'; +import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; +import { assertType, Bound } from '@blocksuite/global/utils'; import { DocCollection } from '@blocksuite/store'; import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; @@ -277,12 +279,15 @@ export function createEdgelessElement( let id; const { service } = edgeless; + let element: GfxModel | null = null; + if (isShape(current)) { id = service.addElement(current.type, { ...current.serialize(), text: new DocCollection.Y.Text(), xywh: bound.serialize(), }); + element = service.getElementById(id); } else { const { doc } = edgeless; id = doc.addBlock( @@ -295,16 +300,32 @@ export function createEdgelessElement( }, edgeless.model.id ); - const note = doc.getBlockById(id) as NoteBlockModel; - assertExists(note); + const note = doc.getBlock(id)?.model; + if (!note) { + throw new BlockSuiteError( + ErrorCode.GfxBlockElementError, + 'Note block is not found after creation' + ); + } + assertType(note); doc.updateBlock(note, () => { note.edgeless.collapse = true; }); doc.addBlock('affine:paragraph', {}, note.id); + + element = note; + } + + if (!element) { + throw new BlockSuiteError( + ErrorCode.GfxBlockElementError, + 'Element is not found after creation' + ); } + const group = current.group; if (group instanceof GroupElementModel) { - group.addChild(id); + group.addChild(element); } return id; } @@ -320,9 +341,10 @@ export function createShapeElement( radius: getShapeRadius(targetType), text: new DocCollection.Y.Text(), }); + const element = service.getElementById(id); const group = current.group; - if (group instanceof GroupElementModel) { - group.addChild(id); + if (group instanceof GroupElementModel && element) { + group.addChild(element); } return 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 077aec7b7a8db..584b8d0493962 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 @@ -33,6 +33,7 @@ import { requestThrottledConnectedFrame, stopPropagation, } from '@blocksuite/affine-shared/utils'; +import { getTopElements } from '@blocksuite/block-std/gfx'; import { assertType, Bound, @@ -81,7 +82,6 @@ import { isImageBlock, isNoteBlock, } from '../../utils/query.js'; -import { getTopElements } from '../../utils/tree.js'; import { HandleDirection } from '../resize/resize-handles.js'; import { ResizeHandles, type ResizeMode } from '../resize/resize-handles.js'; import { HandleResizeManager } from '../resize/resize-manager.js'; diff --git a/packages/blocks/src/root-block/edgeless/edgeless-root-service.ts b/packages/blocks/src/root-block/edgeless/edgeless-root-service.ts index a4fd0abde7878..30de5bc3789b3 100644 --- a/packages/blocks/src/root-block/edgeless/edgeless-root-service.ts +++ b/packages/blocks/src/root-block/edgeless/edgeless-root-service.ts @@ -307,14 +307,15 @@ export class EdgelessRootService extends RootService implements SurfaceContext { if (parent !== null) { selection.selectedElements.forEach(element => { // eslint-disable-next-line unicorn/prefer-dom-node-remove - parent.removeChild(element.id); + parent.removeChild(element); }); } const groupId = this.createGroup(selection.selectedElements); + const group = this.surface.getElementById(groupId); - if (parent !== null) { - parent.addChild(groupId); + if (parent !== null && group) { + parent.addChild(group); } selection.set({ @@ -440,7 +441,7 @@ export class EdgelessRootService extends RootService implements SurfaceContext { const { activeGroup } = selectionManager; const first = picked; - if (activeGroup && picked && activeGroup.hasDescendant(picked.id)) { + if (activeGroup && picked && activeGroup.hasDescendant(picked)) { let index = results.length - 1; while ( @@ -531,12 +532,12 @@ export class EdgelessRootService extends RootService implements SurfaceContext { if (parent !== null) { // eslint-disable-next-line unicorn/prefer-dom-node-remove - parent.removeChild(group.id); + parent.removeChild(group); } elements.forEach(element => { // eslint-disable-next-line unicorn/prefer-dom-node-remove - group.removeChild(element.id); + group.removeChild(element); }); // keep relative index order of group children after ungroup @@ -550,7 +551,7 @@ export class EdgelessRootService extends RootService implements SurfaceContext { if (parent !== null) { elements.forEach(element => { - parent.addChild(element.id); + parent.addChild(element); }); } diff --git a/packages/blocks/src/root-block/edgeless/frame-manager.ts b/packages/blocks/src/root-block/edgeless/frame-manager.ts index 679f0f1c236ff..d987344c1089a 100644 --- a/packages/blocks/src/root-block/edgeless/frame-manager.ts +++ b/packages/blocks/src/root-block/edgeless/frame-manager.ts @@ -7,6 +7,7 @@ import { MindmapElementModel, } from '@blocksuite/affine-model'; import { + getTopElements, type GfxModel, isGfxContainerElm, renderableInEdgeless, @@ -30,7 +31,6 @@ import { GfxBlockModel } from './block-model.js'; import { edgelessElementsBound } from './utils/bound-utils.js'; import { areSetsEqual } from './utils/misc.js'; import { isFrameBlock } from './utils/query.js'; -import { getAllDescendantElements, getTopElements } from './utils/tree.js'; const MIN_FRAME_WIDTH = 800; const MIN_FRAME_HEIGHT = 640; @@ -142,7 +142,7 @@ export class EdgelessFrameManager { } constructor(private _rootService: EdgelessRootService) { - this._watchElementAddedOrDeleted(); + this._watchElementAdded(); } private _addChildrenToLegacyFrame(frame: FrameBlockModel) { @@ -180,13 +180,19 @@ export class EdgelessFrameManager { return frameModel; } - private _watchElementAddedOrDeleted() { + private _watchElementAdded() { this._disposable.add( this._rootService.surface.elementAdded.on(({ id, local }) => { let 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.group instanceof MindmapElementModel) { element = element.group; @@ -259,23 +265,8 @@ export class EdgelessFrameManager { if (elements.length === 0) return; - // Remove other relations 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); }); frame.addChildren(elements); @@ -395,9 +386,8 @@ export class EdgelessFrameManager { } getParentFrame(element: GfxModel) { - return this.frames.find(frame => { - return frame.childIds.includes(element.id); - }); + const container = element.container; + return container && isFrameBlock(container) ? container : null; } removeAllChildrenFromFrame(frame: FrameBlockModel) { @@ -412,7 +402,7 @@ export class EdgelessFrameManager { // this is a workaround to avoid this if (element.group instanceof MindmapElementModel) element = element.group; if (element instanceof MindmapElementModel) { - [element, ...getAllDescendantElements(element)].forEach(child => { + [element, ...element.descendantElements].forEach(child => { const parentFrame = this.getParentFrame(child); if (!parentFrame) return; // 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 cf4a8cc9d0c56..bccb310bc5811 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 { ConnectorUtils, MindmapUtils } from '@blocksuite/affine-block-surface'; @@ -23,6 +22,11 @@ import { handleNativeRangeAtPoint, resetNativeSelection, } from '@blocksuite/affine-shared/utils'; +import { + getTopElements, + isGfxContainerElm, + type PointTestOptions, +} from '@blocksuite/block-std/gfx'; import { Bound, DisposableGroup, @@ -51,7 +55,6 @@ import { mountShapeTextEditor, mountTextElementEditor, } from '../utils/text.js'; -import { getAllDescendantElements, getTopElements } from '../utils/tree.js'; import { EdgelessToolController } from './edgeless-tool.js'; export enum DefaultModeDragType { @@ -1032,11 +1035,12 @@ export class DefaultToolController extends EdgelessToolController { const toBeMoved = new Set(elements); elements.forEach(element => { if (element.group instanceof MindmapElementModel && elements.length > 1) { - getAllDescendantElements(element.group).forEach(ele => - toBeMoved.add(ele) - ); - } else { - getAllDescendantElements(element).forEach(ele => { + element.group.descendantElements.forEach(ele => toBeMoved.add(ele)); + } else if (isGfxContainerElm(element)) { + element.descendantElements.forEach(ele => { + if (ele.group instanceof MindmapElementModel) { + ele.group.descendantElements.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 64acf99902b2e..39118ba17dfed 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 49b8f0a35bfea..76be77301ed69 100644 --- a/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts +++ b/packages/blocks/src/root-block/edgeless/utils/clone-utils.ts @@ -13,32 +13,38 @@ import { MindmapElementModel, } from '@blocksuite/affine-model'; import { + getTopElements, + 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 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)) { + element.descendantElements.map(descendant => set.add(descendant)); + } }); return sortEdgelessElements([...set]); } export async function prepareCloneData( - elements: BlockSuite.EdgelessModel[], + elements: GfxModel[], std: BlockStdScope ) { elements = sortEdgelessElements(elements); @@ -55,8 +61,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) { @@ -74,7 +80,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; @@ -98,18 +104,22 @@ 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 topElements = 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 ); @@ -119,7 +129,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/group.ts b/packages/blocks/src/root-block/edgeless/utils/group.ts index 6412855ab32a5..3dd461da70809 100644 --- a/packages/blocks/src/root-block/edgeless/utils/group.ts +++ b/packages/blocks/src/root-block/edgeless/utils/group.ts @@ -3,9 +3,10 @@ export function getElementsWithoutGroup(elements: BlockSuite.EdgelessModel[]) { const set = new Set(); elements.forEach(element => { - // TODO(@L-Sun) Use `getAllDescendantElements` instead if (element instanceof GroupElementModel) { - element.descendants().forEach(child => set.add(child)); + element.descendantElements + .filter(descendant => !(descendant instanceof GroupElementModel)) + .forEach(descendant => set.add(descendant)); } else { set.add(element); } 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 0e72f5dd1bfa7..0000000000000 --- 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 478f6212717dc..791fb39b1f763 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,6 +1,7 @@ import type { EditorHost } from '@blocksuite/block-std'; import { AIStarIcon } from '@blocksuite/affine-components/icons'; +import { isGfxContainerElm } from '@blocksuite/block-std/gfx'; import { WithDisposable } from '@blocksuite/global/utils'; import { css, html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; @@ -9,7 +10,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'; export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) { static override styles = css` @@ -23,12 +24,22 @@ export class EdgelessCopilotToolbarEntry extends WithDisposable(LitElement) { `; private _showCopilotPanel() { - const selectedElements = this.edgeless.service.selection.selectedElements; + 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)) { + element.descendantElements.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 36d5bdd0ec792..0a64e6f1359e9 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 @@ -14,7 +14,6 @@ export class EdgelessReleaseFromGroupButton extends WithDisposable(LitElement) { if (!(element.group instanceof GroupElementModel)) return; const group = element.group; - // eslint-disable-next-line unicorn/prefer-dom-node-remove group.removeChild(element); @@ -22,7 +21,7 @@ export class EdgelessReleaseFromGroupButton extends WithDisposable(LitElement) { const parent = group.group; if (parent instanceof GroupElementModel) { - parent.addChild(element.id); + parent.addChild(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 1a1a97f6a7ca8..55b5faf81777e 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 5b6db20bf1d40..3e3b10c7304a0 100644 --- a/packages/framework/block-std/src/gfx/index.ts +++ b/packages/framework/block-std/src/gfx/index.ts @@ -38,6 +38,12 @@ export { type SurfaceBlockProps, type SurfaceMiddleware, } from './surface/surface-model.js'; +export { + descendantElementsImpl, + getAncestorContainersImpl, + 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 74bdb76727107..03bc8cfb2fd73 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,7 @@ import type { EditorHost } from '../../view/index.js'; import type { GfxBlockElementModel, GfxModel } from '../gfx-block-model.js'; import type { SurfaceBlockModel } from './surface-model.js'; +import { descendantElementsImpl, hasDescendantElementImpl } from '../tree.js'; import { convertProps, field, @@ -87,14 +88,25 @@ export interface GfxElementGeometry { export const gfxContainerSymbol = Symbol('GfxContainerElement'); export const isGfxContainerElm = (elm: unknown): elm is GfxContainerElement => { - return (elm as GfxContainerElement)[gfxContainerSymbol] === true; + if (typeof elm !== 'object' || elm === null) return false; + return gfxContainerSymbol in elm && elm[gfxContainerSymbol] === true; }; export interface GfxContainerElement extends GfxCompatibleProps { [gfxContainerSymbol]: true; childIds: string[]; + + /** + * ! Note that `childElements` may not match the `childIds` during doc loading stage. + */ childElements: GfxModel[]; - hasDescendant(element: string | GfxModel): boolean; + descendantElements: GfxModel[]; + + addChild(element: GfxModel): void; + removeChild(element: GfxModel): void; + hasChild(element: GfxModel): boolean; + + hasDescendant(element: GfxModel): boolean; } export abstract class GfxPrimitiveElementModel< @@ -134,6 +146,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; @@ -419,6 +435,10 @@ export abstract class GfxGroupLikeElementModel< return this._childIds; } + get descendantElements(): GfxModel[] { + return descendantElementsImpl(this); + } + get xywh() { if ( !this._local.has('xywh') || @@ -466,52 +486,27 @@ export abstract class GfxGroupLikeElementModel< }); } - /** - * @deprecated Use `getAllDescendantElements` instead. - * Get all descendants of this group - * @param withoutGroup if true, will not include group element - */ - descendants(withoutGroup = true) { - return this.childElements.reduce((prev, child) => { - if (child instanceof GfxGroupLikeElementModel) { - prev = prev.concat(child.descendants()); - - !withoutGroup && prev.push(child as GfxPrimitiveElementModel); - } else { - prev.push(child); - } - - return prev; - }, [] as GfxModel[]); - } + abstract addChild(element: GfxModel): void; /** * 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); + hasDescendant(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 99197967f3a08..1b87a5a9dcf12 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,15 @@ import type { Boxed, Y } from '@blocksuite/store'; import { type Constructor, Slot } from '@blocksuite/global/utils'; import { BlockModel, DocCollection, nanoid } from '@blocksuite/store'; +import { GfxBlockElementModel } from '../gfx-block-model.js'; +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 = { @@ -48,12 +50,8 @@ export class SurfaceBlockModel extends BlockModel { } >(); - protected _elementToGroup = new Map(); - protected _elementTypeMap = new Map(); - protected _groupToElements = new Map(); - protected _surfaceBlockModel = true; elementAdded = new Slot<{ id: string; local: boolean }>(); @@ -80,6 +78,8 @@ export class SurfaceBlockModel extends BlockModel { }>(), }; + tree = new TreeManager(this); + get elementModels() { const models: GfxPrimitiveElementModel[] = []; this._elementModels.forEach(model => models.push(model.model)); @@ -270,7 +270,6 @@ export class SurfaceBlockModel extends BlockModel { case 'delete': if (this._elementModels.has(id)) { const { model, unmount } = this._elementModels.get(id)!; - this._elementToGroup.delete(id); removeFromType(model.type, model); this._elementModels.delete(id); deletedElements.push({ model, unmount }); @@ -319,6 +318,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]; @@ -331,88 +335,31 @@ export class SurfaceBlockModel extends BlockModel { } private _watchGroupRelationChange() { - const addToGroup = (elementId: string, groupId: string) => { - this._elementToGroup.set(elementId, groupId); - this._groupToElements.set( - groupId, - (this._groupToElements.get(groupId) || []).concat(elementId) - ); - }; - const removeFromGroup = (elementId: string, groupId: string) => { - if (this._elementToGroup.has(elementId)) { - const group = this._elementToGroup.get(elementId)!; - if (group === groupId) { - this._elementToGroup.delete(elementId); - } - } - - if (this._groupToElements.has(groupId)) { - const elements = this._groupToElements.get(groupId)!; - const index = elements.indexOf(elementId); - - if (index !== -1) { - elements.splice(index, 1); - elements.length === 0 && this._groupToElements.delete(groupId); - } - } - }; const isGroup = ( element: GfxPrimitiveElementModel ): element is GfxGroupLikeElementModel => element instanceof GfxGroupLikeElementModel; - this.elementModels.forEach(model => { - if (isGroup(model)) { - model.childIds.forEach(childId => { - addToGroup(childId, model.id); - }); - } - }); - this.elementUpdated.on(({ id, oldValues }) => { const element = this.getElementById(id)!; if (isGroup(element) && oldValues['childIds']) { - (oldValues['childIds'] as string[]).forEach(childId => { - removeFromGroup(childId, id); - }); - - element.childIds.forEach(childId => { - addToGroup(childId, id); - }); - if (element.childIds.length === 0) { this.removeElement(id); } } }); - this.elementAdded.on(({ id }) => { - const element = this.getElementById(id)!; - - if (isGroup(element)) { - element.childIds.forEach(childId => { - addToGroup(childId, id); - }); - } - }); - - this.elementRemoved.on(({ id, model }) => { - if (isGroup(model)) { - const children = [...(this._groupToElements.get(id) || [])]; - - children.forEach(childId => removeFromGroup(childId, id)); - } - }); - - const disposeGroup = this.doc.slots.blockUpdated.on(({ type, id }) => { + const disposeGroup = this.doc.slots.blockUpdated.on(payload => { + const { type, id } = payload; switch (type) { case 'delete': { + const { model } = payload; const group = this.getGroup(id); - if (group) { + if (group && model instanceof GfxBlockElementModel) { // eslint-disable-next-line unicorn/prefer-dom-node-remove - group.removeChild(id); + group.removeChild(model); } } } @@ -436,6 +383,7 @@ export class SurfaceBlockModel extends BlockModel { protected _init() { this._initElementModels(); + this._initTreeWatcher(); this._watchGroupRelationChange(); this.applyMiddlewares(); } @@ -481,6 +429,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; } @@ -493,8 +445,9 @@ export class SurfaceBlockModel extends BlockModel { T extends GfxGroupLikeElementModel = GfxGroupLikeElementModel, >(id: string): T | null { - return this._elementToGroup.has(id) - ? (this.getElementById(this._elementToGroup.get(id)!) as T) + const container = this.getContainer(id); + return container instanceof GfxGroupLikeElementModel + ? (container as T) : null; } @@ -531,7 +484,6 @@ export class SurfaceBlockModel extends BlockModel { this.doc.transact(() => { const element = this.getElementById(id)!; - const group = this.getGroup(id); if (element instanceof GfxGroupLikeElementModel) { element.childIds.forEach(childId => { @@ -543,11 +495,6 @@ export class SurfaceBlockModel extends BlockModel { }); } - if (group) { - // eslint-disable-next-line unicorn/prefer-dom-node-remove - group.removeChild(id); - } - this.elements.getValue()!.delete(id); this.hooks.remove.emit({ 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 0000000000000..d93a7c8f84397 --- /dev/null +++ b/packages/framework/block-std/src/gfx/tree.ts @@ -0,0 +1,277 @@ +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.hasDescendant(e2)) { + results.delete(e2); + } + }); + }); + + return [...results]; +} + +export class TreeManager { + private _elementToContainer = new Map< + string, + GfxModel & GfxContainerElement + >(); + + private _watched = false; + + constructor(readonly surface: SurfaceBlockModel) {} + + getContainer(elementId: string): (GfxModel & GfxContainerElement) | null { + const container = this._elementToContainer.get(elementId); + return container ?? null; + } + + /** + * Watch the container relationship of the elements in the surface. + * You should call this method only once. + */ + watch() { + const disposable = new DisposableGroup(); + + if (this._watched) { + console.warn('TreeManager is already watched'); + return disposable; + } + + 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); + }); + }; + + 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 => { + if (this._elementToContainer.get(child.id) === model) + this._elementToContainer.delete(child.id); + }); + } + this._elementToContainer.delete(model.id); + }; + + const onGfxContainerUpdated = (model: GfxModel & GfxContainerElement) => { + if (!isGfxContainerElm(model)) return; + + const previousChildrenIds = new Set(); + this._elementToContainer.forEach((container, elementId) => { + if (container === model) previousChildrenIds.add(elementId); + }); + + model.childIds.forEach(childId => { + this._elementToContainer.set(childId, model); + previousChildrenIds.delete(childId); + }); + + previousChildrenIds.forEach(prevChildId => { + if (this._elementToContainer.get(prevChildId) === model) + this._elementToContainer.delete(prevChildId); + }); + }; + + // 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)) { + // we use `childIds` here because some blocks in doc may not be ready + el.childIds.forEach(childId => { + this._elementToContainer.set(childId, el); + }); + } + }); + + 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)) { + // we use `childIds` here because some blocks in doc may not be ready + el.childIds.forEach(childId => { + this._elementToContainer.set(childId, el); + }); + } + }); + + 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); + }) + ); + + disposable.add(() => { + this._watched = false; + this._elementToContainer.clear(); + }); + + this._watched = true; + + 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 descendantElementsImpl( + 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 21f0cb83b673b..41d3ea43fa52a 100644 --- a/packages/framework/block-std/src/utils/layer.ts +++ b/packages/framework/block-std/src/utils/layer.ts @@ -97,6 +97,9 @@ export function renderableInEdgeless( * @returns */ export function compare(a: GfxModel, b: GfxModel) { + const surface = a.surface ?? b.surface; + if (!surface) return SortOrder.SAME; + if (isGfxContainerElm(a) && a.hasDescendant(b)) { return SortOrder.BEFORE; } else if (isGfxContainerElm(b) && b.hasDescendant(a)) { diff --git a/packages/presets/src/__tests__/edgeless/surface-model.spec.ts b/packages/presets/src/__tests__/edgeless/surface-model.spec.ts index 2af3bcf30fa53..d7a7b54b90075 100644 --- a/packages/presets/src/__tests__/edgeless/surface-model.spec.ts +++ b/packages/presets/src/__tests__/edgeless/surface-model.spec.ts @@ -181,10 +181,6 @@ describe('group', () => { model.removeElement(groupId); expect(model.getGroup(id)).toBeNull(); expect(model.getGroup(id2)).toBeNull(); - // @ts-ignore - expect(model._elementToGroup.get(id)).toBeUndefined(); - // @ts-ignore - expect(model._elementToGroup.get(id2)).toBeUndefined(); }); test('children can be updated with a plain object', () => { diff --git a/tests/edgeless/group/clipboard.spec.ts b/tests/edgeless/group/clipboard.spec.ts index a04a398ebab12..ca2c5bff31da2 100644 --- a/tests/edgeless/group/clipboard.spec.ts +++ b/tests/edgeless/group/clipboard.spec.ts @@ -144,6 +144,6 @@ test.describe('group clipboard', () => { await pasteByKeyboard(page, true); await waitNextFrame(page, 500); const sortedIds = await getAllSortedIds(page); - expect(sortedIds.length).toBe(12); + expect(sortedIds.length).toBe(10); }); });