From 411784c12f292b7af1565ec22b024862211bc055 Mon Sep 17 00:00:00 2001 From: xigua Date: Thu, 27 Jun 2024 00:10:26 +0800 Subject: [PATCH] refactor: copy paste --- packages/common/src/common.ts | 11 +- packages/core/src/clipboard.ts | 161 +++++++++++++++--- packages/core/src/editor.ts | 6 +- packages/core/src/graphs/canvas.ts | 6 + packages/core/src/graphs/graphics/graphics.ts | 17 +- packages/core/src/graphs/rect.ts | 7 +- packages/core/src/graphs/regular_polygon.ts | 4 +- packages/core/src/graphs/star.ts | 4 +- packages/core/src/scene/scene_graph.ts | 34 ++-- packages/core/src/service/group_and_record.ts | 2 +- packages/core/src/transaction.ts | 20 ++- 11 files changed, 215 insertions(+), 57 deletions(-) diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 50ef763a..4d57de84 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -7,10 +7,19 @@ export const noop = () => { /** * 生成唯一 ID */ -export const genId = () => { +export const genUuid = () => { return uuidv4(); }; +export const increaseIdGenerator = () => { + let count = 0; + return () => { + const id = String(count); + count++; + return id; + }; +}; + export const objectNameGenerator = { maxIdxMap: new Map(), gen(type: string) { diff --git a/packages/core/src/clipboard.ts b/packages/core/src/clipboard.ts index b3e28031..07f5565f 100644 --- a/packages/core/src/clipboard.ts +++ b/packages/core/src/clipboard.ts @@ -1,10 +1,19 @@ -import { arrMap, noop, omit } from '@suika/common'; +import { genUuid, increaseIdGenerator, noop } from '@suika/common'; +import { + boxToRect, + invertMatrix, + mergeBoxes, + multiplyMatrix, +} from '@suika/geo'; +import { generateNKeysBetween } from 'fractional-indexing'; -import { AddGraphCmd } from './commands/add_graphs'; import { type Editor } from './editor'; -import { SuikaGraphics } from './graphs'; +import { type GraphicsAttrs, isFrameGraphics, SuikaGraphics } from './graphs'; +import { isCanvasGraphics } from './graphs/canvas'; import { toSVG } from './to_svg'; +import { Transaction } from './transaction'; import { type IEditorPaperData } from './type'; +import { getChildNodeSet } from './utils'; /** * Clipboard Manager @@ -88,23 +97,125 @@ export class ClipboardManager { } private getSelectedItemsSnapshot() { - const selectedItems = this.editor.selectedElements.getItems(); + const selectedItems = SuikaGraphics.sortGraphics( + this.editor.selectedElements.getItems(), + ); if (selectedItems.length === 0) { return null; } - // remove id attr - const copiedData = arrMap(selectedItems, (item) => - omit(item.getAttrs(), 'id'), - ); + const idGenerator = increaseIdGenerator(); + const replacedIdMap = new Map(); + const copiedData: GraphicsAttrs[] = []; + + for (const item of selectedItems) { + const attrs = item.getAttrs(); + attrs.transform = item.getWorldTransform(); + attrs.parentIndex = undefined; + const tmpId = idGenerator(); + replacedIdMap.set(attrs.id, tmpId); + attrs.id = tmpId; + + copiedData.push(attrs); + } + + const childNodes = getChildNodeSet(selectedItems); + for (const item of childNodes) { + const attrs = item.getAttrs(); + const tmpId = idGenerator(); + replacedIdMap.set(attrs.id, tmpId); + attrs.id = tmpId; + + if (attrs.parentIndex) { + attrs.parentIndex.guid = replacedIdMap.get(attrs.parentIndex.guid)!; + } + + copiedData.push(attrs); + } return JSON.stringify({ appVersion: this.editor.appVersion, paperId: this.editor.paperId, - data: JSON.stringify(copiedData), + data: copiedData, }); } + /** + * update parentIndex.guid and transform for attributes array + * @param attrsArr attribute array + * @returns top parent count + */ + private updateAttrsParentIndex(attrsArr: GraphicsAttrs[]): number { + /** + * TODO: to finish + * (逻辑待梳理) + * 如果选中一个 group 节点。按顺序粘贴子节点中的到最上方 + * 如果选中一个非 group 节点,按顺序粘贴到它的上方 + * 如果选中多个节点。等价于选中最靠上的那一个节点,应用上面两种情况之一的效果 + * + * 选中单个 group 后,然后粘贴的位置依旧是这个 group,在 group 后粘贴。 + */ + let left: string | null = null; + let right: string | null = null; + const firstGraphics = + SuikaGraphics.sortGraphics(this.editor.selectedElements.getItems()).at( + -1, + ) ?? this.editor.doc.getCurrCanvas(); + let parent = firstGraphics; + + if (isCanvasGraphics(firstGraphics) || isFrameGraphics(firstGraphics)) { + left = firstGraphics.getMaxChildIndex(); + } else { + parent = firstGraphics.getParent()!; + left = firstGraphics.getSortIndex(); + const nextSibling = firstGraphics.getNextSibling(); + right = nextSibling ? nextSibling.getSortIndex() : null; + } + + const parentId = parent.attrs.id; + const parentInvertWorldTf = invertMatrix(parent.getWorldTransform()); + + let i = 0; + while (i < attrsArr.length) { + const attrs = attrsArr[i]; + if (attrs.parentIndex) { + break; + } + i++; + } + + const topGraphicsCount = i; + const sortIndies = generateNKeysBetween(left, right, topGraphicsCount); + + // top parent node + const oldNewIdMap = new Map(); + for (let j = 0; j < topGraphicsCount; j++) { + const attrs = attrsArr[j]; + attrs.parentIndex = { + guid: parentId, + position: sortIndies[j], + }; + + const newId = genUuid(); + oldNewIdMap.set(attrs.id, newId); + attrs.id = newId; + + attrs.transform = multiplyMatrix(parentInvertWorldTf, attrs.transform); + } + + // child node + while (i < attrsArr.length) { + const attrs = attrsArr[i]; + const newId = genUuid(); + oldNewIdMap.set(attrs.id, newId); + attrs.id = newId; + attrs.parentIndex!.guid = oldNewIdMap.get(attrs.parentIndex!.guid)!; + i++; + } + + return topGraphicsCount; + } + private addGraphsFromClipboard(dataStr: string): void; private addGraphsFromClipboard(dataStr: string, x: number, y: number): void; private addGraphsFromClipboard(dataStr: string, x?: number, y?: number) { @@ -130,20 +241,23 @@ export class ClipboardManager { } const editor = this.editor; - const pastedGraphs = editor.sceneGraph.parseStrAndAddGraphics( - pastedData.data, - ); - if (pastedGraphs.length === 0) { + if (pastedData.data.length === 0) { return; } - // TODO: duplicated objectName should be renamed - editor.commandManager.pushCommand( - new AddGraphCmd('pasted graphs', editor, pastedGraphs), + const topGraphicsCount = this.updateAttrsParentIndex(pastedData.data); + const pastedGraphicsArr = editor.sceneGraph.createGraphicsArr( + pastedData.data, ); - editor.selectedElements.setItems(pastedGraphs); + editor.sceneGraph.addItems(pastedGraphicsArr); + editor.sceneGraph.initGraphicsTree(pastedGraphicsArr); - const boundingRect = editor.selectedElements.getBoundingRect()!; + const selectedItems = pastedGraphicsArr.slice(0, topGraphicsCount); + editor.selectedElements.setItems(selectedItems); + + const boundingRect = boxToRect( + mergeBoxes(selectedItems.map((item) => item.getBbox())), + ); if ( (x === undefined || y === undefined) && pastedData.paperId !== editor.paperId @@ -157,9 +271,18 @@ export class ClipboardManager { const dx = x - boundingRect.x; const dy = y - boundingRect.y; if (dx || dy) { - SuikaGraphics.dMove(pastedGraphs, dx, dy); + SuikaGraphics.dMove(selectedItems, dx, dy); } } + + // TODO: duplicated objectName should be renamed + + const transaction = new Transaction(editor); + transaction + .addNewIds(pastedGraphicsArr.map((item) => item.attrs.id)) + .updateParentSize(selectedItems) + .commit('pasted graphs'); + editor.render(); } diff --git a/packages/core/src/editor.ts b/packages/core/src/editor.ts index 14f88a06..ec20f319 100644 --- a/packages/core/src/editor.ts +++ b/packages/core/src/editor.ts @@ -1,5 +1,5 @@ import { - genId, + genUuid, sceneCoordsToViewportUtil, viewportCoordsToSceneUtil, } from '@suika/common'; @@ -155,7 +155,7 @@ export class Editor { ); this.sceneGraph.addItems([canvas]); } - this.paperId ??= genId(); + this.paperId ??= genUuid(); this.autoSaveGraphs.autoSave(); // 设置初始视口 @@ -186,7 +186,7 @@ export class Editor { this.sceneGraph.load(data.data); this.commandManager.clearRecords(); this.paperId = data.paperId; - this.paperId ??= genId(); + this.paperId ??= genUuid(); } destroy() { this.containerElement.removeChild(this.canvasElement); diff --git a/packages/core/src/graphs/canvas.ts b/packages/core/src/graphs/canvas.ts index 583d9109..7884026f 100644 --- a/packages/core/src/graphs/canvas.ts +++ b/packages/core/src/graphs/canvas.ts @@ -24,3 +24,9 @@ export class SuikaCanvas extends SuikaGraphics { return identityMatrix(); } } + +export const isCanvasGraphics = ( + graphics: SuikaGraphics, +): graphics is SuikaCanvas => { + return graphics instanceof SuikaCanvas; +}; diff --git a/packages/core/src/graphs/graphics/graphics.ts b/packages/core/src/graphs/graphics/graphics.ts index 8664c8b2..63f59209 100644 --- a/packages/core/src/graphs/graphics/graphics.ts +++ b/packages/core/src/graphs/graphics/graphics.ts @@ -1,7 +1,7 @@ import { calcCoverScale, cloneDeep, - genId, + genUuid, objectNameGenerator, omit, parseRGBToHex, @@ -81,7 +81,7 @@ export class SuikaGraphics { } this.attrs = { ...attrs } as ATTRS; - this.attrs.id ??= genId(); + this.attrs.id ??= genUuid(); this.attrs.transform = transform; if (this.attrs.objectName) { @@ -906,6 +906,19 @@ export class SuikaGraphics { return this.attrs.parentIndex?.position ?? ''; } + getNextSibling() { + const parent = this.getParent(); + if (!parent) { + return null; + } + const children = parent.getChildren(); + const index = children.findIndex((item) => item === this); + if (index == -1) { + console.warn('index should not be -1!'); + } + return children[index + 1] ?? null; + } + getSortIndexPath() { const path: string[] = []; // eslint-disable-next-line @typescript-eslint/no-this-alias diff --git a/packages/core/src/graphs/rect.ts b/packages/core/src/graphs/rect.ts index 2abd3d0e..edf95b02 100644 --- a/packages/core/src/graphs/rect.ts +++ b/packages/core/src/graphs/rect.ts @@ -1,4 +1,4 @@ -import { parseHexToRGBA, parseRGBAStr } from '@suika/common'; +import { cloneDeep, parseHexToRGBA, parseRGBAStr } from '@suika/common'; import { type IMatrixArr, type IPoint, @@ -38,7 +38,10 @@ export class SuikaRect extends SuikaGraphics { } override getAttrs(): RectAttrs { - return { ...this.attrs, cornerRadius: this.attrs.cornerRadius ?? 0 }; + return cloneDeep({ + ...this.attrs, + cornerRadius: this.attrs.cornerRadius ?? 0, + }); } override toJSON() { diff --git a/packages/core/src/graphs/regular_polygon.ts b/packages/core/src/graphs/regular_polygon.ts index fab7e8be..858a5501 100644 --- a/packages/core/src/graphs/regular_polygon.ts +++ b/packages/core/src/graphs/regular_polygon.ts @@ -1,4 +1,4 @@ -import { parseHexToRGBA, parseRGBAStr } from '@suika/common'; +import { cloneDeep, parseHexToRGBA, parseRGBAStr } from '@suika/common'; import { getPointsBbox, getRegularPolygon, @@ -40,7 +40,7 @@ export class SuikaRegularPolygon extends SuikaGraphics { } override getAttrs(): RegularPolygonAttrs { - return { ...this.attrs, count: this.attrs.count }; + return cloneDeep({ ...this.attrs, count: this.attrs.count }); } override toJSON() { diff --git a/packages/core/src/graphs/star.ts b/packages/core/src/graphs/star.ts index a72e2dae..d6b1072b 100644 --- a/packages/core/src/graphs/star.ts +++ b/packages/core/src/graphs/star.ts @@ -1,4 +1,4 @@ -import { parseHexToRGBA, parseRGBAStr } from '@suika/common'; +import { cloneDeep, parseHexToRGBA, parseRGBAStr } from '@suika/common'; import { getPointsBbox, getStar, @@ -41,7 +41,7 @@ export class SuikaStar extends SuikaGraphics { } override getAttrs(): StarAttrs { - return { ...this.attrs, count: this.attrs.count }; + return cloneDeep({ ...this.attrs, count: this.attrs.count }); } override toJSON() { diff --git a/packages/core/src/scene/scene_graph.ts b/packages/core/src/scene/scene_graph.ts index 1c5dde7a..517e1297 100644 --- a/packages/core/src/scene/scene_graph.ts +++ b/packages/core/src/scene/scene_graph.ts @@ -56,12 +56,8 @@ export class SceneGraph { this.grid = new Grid(editor); } - addItems(graphicsArr: SuikaGraphics[], idx?: number) { - if (idx === undefined) { - this.children.push(...graphicsArr); - } else { - this.children.splice(idx, 0, ...graphicsArr); - } + addItems(graphicsArr: SuikaGraphics[]) { + this.children.push(...graphicsArr); for (const graph of graphicsArr) { this.editor.doc.graphicsStore.add(graph); @@ -346,10 +342,8 @@ export class SceneGraph { return JSON.stringify(paperData); } - parseStrAndAddGraphics(info: GraphicsAttrs[]) { - const data: GraphicsAttrs[] = info; - - const newChildren: SuikaGraphics[] = []; + createGraphicsArr(data: GraphicsAttrs[]) { + const children: SuikaGraphics[] = []; for (const attrs of data) { const type = attrs.type; const Ctor = graphCtorMap[type!]; @@ -357,15 +351,14 @@ export class SceneGraph { console.error(`Unsupported graph type "${attrs.type}", ignore it`); continue; } - newChildren.push(new Ctor(attrs as any, { doc: this.editor.doc })); + children.push(new Ctor(attrs as any, { doc: this.editor.doc })); } - this.addItems(newChildren); - return newChildren; + return children; } - initGraphicsTree() { + initGraphicsTree(graphicsArr: SuikaGraphics[]) { const canvasGraphics = this.editor.doc.graphicsStore.getCanvas(); - for (const graphics of this.children) { + for (const graphics of graphicsArr) { const parent = graphics.getParent() ?? canvasGraphics; if (parent && parent !== graphics) { parent.insertChild(graphics, graphics.attrs.parentIndex?.position); @@ -374,15 +367,10 @@ export class SceneGraph { } load(info: GraphicsAttrs[]) { - /** - * 1. 找出 document 节点,额外保存起来。 - * document 只能有一个,否则为错误数据 - * - * 2. 构造成树结构 - */ this.children = []; - this.parseStrAndAddGraphics(info); - this.initGraphicsTree(); + const graphicsArr = this.createGraphicsArr(info); + this.addItems(graphicsArr); + this.initGraphicsTree(graphicsArr); } on(eventName: 'render', handler: () => void) { diff --git a/packages/core/src/service/group_and_record.ts b/packages/core/src/service/group_and_record.ts index e0ed77b8..94f405d2 100644 --- a/packages/core/src/service/group_and_record.ts +++ b/packages/core/src/service/group_and_record.ts @@ -62,7 +62,7 @@ export const groupAndRecord = ( const groupInvertTf = invertMatrix(group.getWorldTransform()); const transaction = new Transaction(editor); - transaction.newId(group.attrs.id); + transaction.addNewIds([group.attrs.id]); for (const graphics of graphicsArr) { transaction.recordOld(graphics.attrs.id, { diff --git a/packages/core/src/transaction.ts b/packages/core/src/transaction.ts index 33e80280..00faf5f2 100644 --- a/packages/core/src/transaction.ts +++ b/packages/core/src/transaction.ts @@ -8,23 +8,30 @@ export class Transaction { private updatedAttrsMap = new Map>(); private removedIds = new Set(); private newIds = new Set(); + private isCommitDone = false; constructor(private editor: Editor) {} recordOld(id: string, attrs: Partial) { this.originAttrsMap.set(id, attrs); + return this; } update(id: string, attrs: Partial) { this.updatedAttrsMap.set(id, attrs); + return this; } remove(id: string) { this.removedIds.add(id); + return this; } - newId(id: string) { - this.newIds.add(id); + addNewIds(ids: string[]) { + for (const id of ids) { + this.newIds.add(id); + } + return this; } updateParentSize(elements: SuikaGraphics[]) { @@ -34,6 +41,7 @@ export class Transaction { this.originAttrsMap, this.updatedAttrsMap, ); + return this; } updateNodeSize(idSet: Set) { @@ -43,9 +51,15 @@ export class Transaction { this.originAttrsMap, this.updatedAttrsMap, ); + return this; } commit(desc: string) { + if (this.isCommitDone) { + console.error('It had committed before, can not commit again!'); + return; + } + // TODO: check duplicated id between removeIds and newIds this.editor.commandManager.pushCommand( new UpdateGraphicsAttrsCmd( @@ -57,5 +71,7 @@ export class Transaction { this.newIds, ), ); + + this.isCommitDone = true; } }