mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
feat(editor): add command for edgeless clipboard (#11173)
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
CanvasElementType,
|
||||
type ClipboardConfigCreationContext,
|
||||
EdgelessCRUDIdentifier,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { Connection } from '@blocksuite/affine-model';
|
||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { BlockStdScope } from '@blocksuite/block-std';
|
||||
import type {
|
||||
GfxPrimitiveElementModel,
|
||||
SerializedElement,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { Bound, type SerializedXYWH, Vec } from '@blocksuite/global/gfx';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType;
|
||||
|
||||
export function createCanvasElement(
|
||||
std: BlockStdScope,
|
||||
clipboardData: SerializedElement,
|
||||
context: ClipboardConfigCreationContext,
|
||||
newXYWH: SerializedXYWH
|
||||
) {
|
||||
if (clipboardData.type === GROUP) {
|
||||
const yMap = new Y.Map();
|
||||
const children = clipboardData.children ?? {};
|
||||
|
||||
for (const [key, value] of Object.entries(children)) {
|
||||
const newKey = context.oldToNewIdMap.get(key);
|
||||
if (!newKey) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied child in group, key: ${key}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
yMap.set(newKey, value);
|
||||
}
|
||||
clipboardData.children = yMap;
|
||||
clipboardData.xywh = newXYWH;
|
||||
} else if (clipboardData.type === MINDMAP) {
|
||||
const yMap = new Y.Map();
|
||||
const children = clipboardData.children ?? {};
|
||||
|
||||
for (const [oldKey, oldValue] of Object.entries(children)) {
|
||||
const newKey = context.oldToNewIdMap.get(oldKey);
|
||||
const newValue = {
|
||||
...oldValue,
|
||||
};
|
||||
if (!newKey) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied node in mind map, key: ${oldKey}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oldValue.parent) {
|
||||
const newParent = context.oldToNewIdMap.get(oldValue.parent);
|
||||
if (!newParent) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied node in mind map, parent: ${oldValue.parent}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
newValue.parent = newParent;
|
||||
}
|
||||
|
||||
yMap.set(newKey, newValue);
|
||||
}
|
||||
clipboardData.children = yMap;
|
||||
} else if (clipboardData.type === CONNECTOR) {
|
||||
const source = clipboardData.source as Connection;
|
||||
const target = clipboardData.target as Connection;
|
||||
|
||||
const oldBound = Bound.deserialize(clipboardData.xywh);
|
||||
const newBound = Bound.deserialize(newXYWH);
|
||||
const offset = Vec.sub([newBound.x, newBound.y], [oldBound.x, oldBound.y]);
|
||||
|
||||
if (source.id) {
|
||||
source.id = context.oldToNewIdMap.get(source.id) ?? source.id;
|
||||
} else if (source.position) {
|
||||
source.position = Vec.add(source.position, offset);
|
||||
}
|
||||
|
||||
if (target.id) {
|
||||
target.id = context.oldToNewIdMap.get(target.id) ?? target.id;
|
||||
} else if (target.position) {
|
||||
target.position = Vec.add(target.position, offset);
|
||||
}
|
||||
} else {
|
||||
clipboardData.xywh = newXYWH;
|
||||
}
|
||||
|
||||
clipboardData.lockedBySelf = false;
|
||||
|
||||
const crud = std.get(EdgelessCRUDIdentifier);
|
||||
|
||||
const id = crud.addElement(
|
||||
clipboardData.type as CanvasElementType,
|
||||
clipboardData
|
||||
);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:paste',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: clipboardData.type as string,
|
||||
});
|
||||
const element = crud.getElementById(id) as GfxPrimitiveElementModel;
|
||||
if (!element) {
|
||||
console.error(`Copy failed: cannot find the copied element, id: ${id}`);
|
||||
return null;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
@@ -2,16 +2,12 @@ import { addAttachments } from '@blocksuite/affine-block-attachment';
|
||||
import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||
import { addImages } from '@blocksuite/affine-block-image';
|
||||
import {
|
||||
CanvasElementType,
|
||||
type ClipboardConfigCreationContext,
|
||||
EdgelessClipboardConfigIdentifier,
|
||||
EdgelessCRUDIdentifier,
|
||||
ExportManager,
|
||||
getSurfaceComponent,
|
||||
SurfaceGroupLikeModel,
|
||||
TextUtils,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { Connection, ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BookmarkStyles,
|
||||
DEFAULT_NOTE_HEIGHT,
|
||||
@@ -48,12 +44,9 @@ import type {
|
||||
import {
|
||||
compareLayer,
|
||||
type GfxBlockElementModel,
|
||||
type GfxCompatibleProps,
|
||||
GfxControllerIdentifier,
|
||||
type GfxModel,
|
||||
type GfxPrimitiveElementModel,
|
||||
type SerializedElement,
|
||||
SortOrder,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
@@ -62,23 +55,16 @@ import {
|
||||
getCommonBound,
|
||||
type IBound,
|
||||
type IVec,
|
||||
type SerializedXYWH,
|
||||
Vec,
|
||||
} from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import {
|
||||
type BlockSnapshot,
|
||||
BlockSnapshotSchema,
|
||||
type SliceSnapshot,
|
||||
} from '@blocksuite/store';
|
||||
import { type BlockSnapshot, type SliceSnapshot } from '@blocksuite/store';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { PageClipboard } from '../../clipboard/index.js';
|
||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||
import { isCanvasElementWithText } from '../utils/query.js';
|
||||
import { createElementsFromClipboardDataCommand } from './command.js';
|
||||
import {
|
||||
createNewPresentationIndexes,
|
||||
edgelessElementsBoundFromRawData,
|
||||
isPureFileInClipboard,
|
||||
prepareClipboardData,
|
||||
tryGetSvgFromClipboard,
|
||||
@@ -86,7 +72,6 @@ import {
|
||||
|
||||
const BLOCKSUITE_SURFACE = 'blocksuite/surface';
|
||||
|
||||
const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType;
|
||||
const IMAGE_PADDING = 5; // for rotated shapes some padding is needed
|
||||
|
||||
interface CanvasExportOptions {
|
||||
@@ -397,107 +382,6 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
}
|
||||
}
|
||||
|
||||
private _createCanvasElement(
|
||||
clipboardData: SerializedElement,
|
||||
context: ClipboardConfigCreationContext,
|
||||
newXYWH: SerializedXYWH
|
||||
): GfxPrimitiveElementModel | null {
|
||||
if (clipboardData.type === GROUP) {
|
||||
const yMap = new Y.Map();
|
||||
const children = clipboardData.children ?? {};
|
||||
|
||||
for (const [key, value] of Object.entries(children)) {
|
||||
const newKey = context.oldToNewIdMap.get(key);
|
||||
if (!newKey) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied child in group, key: ${key}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
yMap.set(newKey, value);
|
||||
}
|
||||
clipboardData.children = yMap;
|
||||
clipboardData.xywh = newXYWH;
|
||||
} else if (clipboardData.type === MINDMAP) {
|
||||
const yMap = new Y.Map();
|
||||
const children = clipboardData.children ?? {};
|
||||
|
||||
for (const [oldKey, oldValue] of Object.entries(children)) {
|
||||
const newKey = context.oldToNewIdMap.get(oldKey);
|
||||
const newValue = {
|
||||
...oldValue,
|
||||
};
|
||||
if (!newKey) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied node in mind map, key: ${oldKey}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oldValue.parent) {
|
||||
const newParent = context.oldToNewIdMap.get(oldValue.parent);
|
||||
if (!newParent) {
|
||||
console.error(
|
||||
`Copy failed: cannot find the copied node in mind map, parent: ${oldValue.parent}`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
newValue.parent = newParent;
|
||||
}
|
||||
|
||||
yMap.set(newKey, newValue);
|
||||
}
|
||||
clipboardData.children = yMap;
|
||||
} else if (clipboardData.type === CONNECTOR) {
|
||||
const source = clipboardData.source as Connection;
|
||||
const target = clipboardData.target as Connection;
|
||||
|
||||
const oldBound = Bound.deserialize(clipboardData.xywh);
|
||||
const newBound = Bound.deserialize(newXYWH);
|
||||
const offset = Vec.sub(
|
||||
[newBound.x, newBound.y],
|
||||
[oldBound.x, oldBound.y]
|
||||
);
|
||||
|
||||
if (source.id) {
|
||||
source.id = context.oldToNewIdMap.get(source.id) ?? source.id;
|
||||
} else if (source.position) {
|
||||
source.position = Vec.add(source.position, offset);
|
||||
}
|
||||
|
||||
if (target.id) {
|
||||
target.id = context.oldToNewIdMap.get(target.id) ?? target.id;
|
||||
} else if (target.position) {
|
||||
target.position = Vec.add(target.position, offset);
|
||||
}
|
||||
} else {
|
||||
clipboardData.xywh = newXYWH;
|
||||
}
|
||||
|
||||
clipboardData.lockedBySelf = false;
|
||||
|
||||
const id = this.crud.addElement(
|
||||
clipboardData.type as CanvasElementType,
|
||||
clipboardData
|
||||
);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
this.std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
|
||||
control: 'canvas:paste',
|
||||
page: 'whiteboard editor',
|
||||
module: 'toolbar',
|
||||
segment: 'toolbar',
|
||||
type: clipboardData.type as string,
|
||||
});
|
||||
const element = this.crud.getElementById(id) as GfxPrimitiveElementModel;
|
||||
if (!element) {
|
||||
console.error(`Copy failed: cannot find the copied element, id: ${id}`);
|
||||
return null;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
private async _edgelessToCanvas(
|
||||
bound: IBound,
|
||||
nodes?: GfxBlockElementModel[],
|
||||
@@ -680,8 +564,14 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
private async _pasteShapesAndBlocks(
|
||||
elementsRawData: (SerializedElement | BlockSnapshot)[]
|
||||
) {
|
||||
const { canvasElements, blockModels } =
|
||||
await this.createElementsFromClipboardData(elementsRawData);
|
||||
const [_, { createdElementsPromise }] = this.std.command.exec(
|
||||
createElementsFromClipboardDataCommand,
|
||||
{
|
||||
elementsRawData,
|
||||
}
|
||||
);
|
||||
if (!createdElementsPromise) return;
|
||||
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||
this._emitSelectionChangeAfterPaste(
|
||||
canvasElements.map(ele => ele.id),
|
||||
blockModels.map(block => block.id)
|
||||
@@ -761,51 +651,6 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
});
|
||||
}
|
||||
|
||||
private _updatePastedElementsIndex(
|
||||
elements: GfxModel[],
|
||||
originalIndexes: Map<string, string>
|
||||
) {
|
||||
function compare(a: GfxModel, b: GfxModel) {
|
||||
if (a instanceof SurfaceGroupLikeModel && a.hasDescendant(b)) {
|
||||
return SortOrder.BEFORE;
|
||||
} else if (b instanceof SurfaceGroupLikeModel && b.hasDescendant(a)) {
|
||||
return SortOrder.AFTER;
|
||||
} else {
|
||||
const aGroups = a.groups as SurfaceGroupLikeModel[];
|
||||
const bGroups = b.groups as SurfaceGroupLikeModel[];
|
||||
|
||||
let i = 1;
|
||||
let aGroup: GfxModel | undefined = aGroups.at(-i);
|
||||
let bGroup: GfxModel | undefined = bGroups.at(-i);
|
||||
|
||||
while (aGroup === bGroup && aGroup) {
|
||||
++i;
|
||||
aGroup = aGroups.at(-i);
|
||||
bGroup = bGroups.at(-i);
|
||||
}
|
||||
|
||||
aGroup = aGroup ?? a;
|
||||
bGroup = bGroup ?? b;
|
||||
|
||||
return originalIndexes.get(aGroup.id) === originalIndexes.get(bGroup.id)
|
||||
? SortOrder.SAME
|
||||
: originalIndexes.get(aGroup.id)! < originalIndexes.get(bGroup.id)!
|
||||
? SortOrder.BEFORE
|
||||
: SortOrder.AFTER;
|
||||
}
|
||||
}
|
||||
|
||||
const idxGenerator = this.gfx.layer.createIndexGenerator();
|
||||
const sortedElements = elements.sort(compare);
|
||||
sortedElements.forEach(ele => {
|
||||
const newIndex = idxGenerator();
|
||||
|
||||
this.crud.updateElement(ele.id, {
|
||||
index: newIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
copy() {
|
||||
document.dispatchEvent(
|
||||
new Event('copy', {
|
||||
@@ -815,126 +660,6 @@ export class EdgelessClipboardController extends PageClipboard {
|
||||
);
|
||||
}
|
||||
|
||||
async createElementsFromClipboardData(
|
||||
elementsRawData: (SerializedElement | BlockSnapshot)[],
|
||||
pasteCenter?: IVec
|
||||
) {
|
||||
let oldCommonBound, pasteX, pasteY;
|
||||
{
|
||||
const lastMousePos = this.toolManager.lastMousePos$.peek();
|
||||
pasteCenter =
|
||||
pasteCenter ??
|
||||
this.gfx.viewport.toModelCoord(lastMousePos.x, lastMousePos.y);
|
||||
const [modelX, modelY] = pasteCenter;
|
||||
oldCommonBound = edgelessElementsBoundFromRawData(elementsRawData);
|
||||
|
||||
pasteX = modelX - oldCommonBound.w / 2;
|
||||
pasteY = modelY - oldCommonBound.h / 2;
|
||||
}
|
||||
|
||||
const getNewXYWH = (oldXYWH: SerializedXYWH) => {
|
||||
const oldBound = Bound.deserialize(oldXYWH);
|
||||
return new Bound(
|
||||
oldBound.x + pasteX - oldCommonBound.x,
|
||||
oldBound.y + pasteY - oldCommonBound.y,
|
||||
oldBound.w,
|
||||
oldBound.h
|
||||
).serialize();
|
||||
};
|
||||
|
||||
// create blocks and canvas elements
|
||||
|
||||
const context: ClipboardConfigCreationContext = {
|
||||
oldToNewIdMap: new Map<string, string>(),
|
||||
originalIndexes: new Map<string, string>(),
|
||||
newPresentationIndexes: createNewPresentationIndexes(
|
||||
elementsRawData,
|
||||
this.std
|
||||
),
|
||||
};
|
||||
|
||||
const blockModels: GfxBlockElementModel[] = [];
|
||||
const canvasElements: GfxPrimitiveElementModel[] = [];
|
||||
const allElements: GfxModel[] = [];
|
||||
|
||||
for (const data of elementsRawData) {
|
||||
const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data);
|
||||
if (blockSnapshot) {
|
||||
const oldId = blockSnapshot.id;
|
||||
|
||||
const config = this.std.getOptional(
|
||||
EdgelessClipboardConfigIdentifier(blockSnapshot.flavour)
|
||||
);
|
||||
if (!config) continue;
|
||||
|
||||
if (typeof blockSnapshot.props.index !== 'string') {
|
||||
console.error(`Block(id: ${oldId}) does not have index property`);
|
||||
continue;
|
||||
}
|
||||
const originalIndex = (blockSnapshot.props as GfxCompatibleProps).index;
|
||||
|
||||
if (typeof blockSnapshot.props.xywh !== 'string') {
|
||||
console.error(`Block(id: ${oldId}) does not have xywh property`);
|
||||
continue;
|
||||
}
|
||||
|
||||
assertType<GfxCompatibleProps & unknown>(blockSnapshot.props);
|
||||
|
||||
blockSnapshot.props.xywh = getNewXYWH(
|
||||
blockSnapshot.props.xywh as SerializedXYWH
|
||||
);
|
||||
blockSnapshot.props.lockedBySelf = false;
|
||||
|
||||
const newId = await config.createBlock(blockSnapshot, context);
|
||||
if (!newId) continue;
|
||||
|
||||
const block = this.doc.getBlock(newId);
|
||||
if (!block) continue;
|
||||
|
||||
assertType<GfxBlockElementModel>(block.model);
|
||||
blockModels.push(block.model);
|
||||
allElements.push(block.model);
|
||||
context.oldToNewIdMap.set(oldId, newId);
|
||||
context.originalIndexes.set(oldId, originalIndex);
|
||||
} else {
|
||||
assertType<SerializedElement>(data);
|
||||
const oldId = data.id;
|
||||
|
||||
const element = this._createCanvasElement(
|
||||
data,
|
||||
context,
|
||||
getNewXYWH(data.xywh)
|
||||
);
|
||||
|
||||
if (!element) continue;
|
||||
|
||||
canvasElements.push(element);
|
||||
allElements.push(element);
|
||||
|
||||
context.oldToNewIdMap.set(oldId, element.id);
|
||||
context.originalIndexes.set(oldId, element.index);
|
||||
}
|
||||
}
|
||||
|
||||
// remap old id to new id for the original index
|
||||
const oldIds = [...context.originalIndexes.keys()];
|
||||
oldIds.forEach(oldId => {
|
||||
const newId = context.oldToNewIdMap.get(oldId);
|
||||
const originalIndex = context.originalIndexes.get(oldId);
|
||||
if (newId && originalIndex) {
|
||||
context.originalIndexes.set(newId, originalIndex);
|
||||
context.originalIndexes.delete(oldId);
|
||||
}
|
||||
});
|
||||
|
||||
this._updatePastedElementsIndex(allElements, context.originalIndexes);
|
||||
|
||||
return {
|
||||
canvasElements: canvasElements,
|
||||
blockModels: blockModels,
|
||||
};
|
||||
}
|
||||
|
||||
override mounted() {
|
||||
if (!navigator.clipboard) {
|
||||
console.error(
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import {
|
||||
type ClipboardConfigCreationContext,
|
||||
EdgelessClipboardConfigIdentifier,
|
||||
EdgelessCRUDIdentifier,
|
||||
SurfaceGroupLikeModel,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { BlockStdScope, Command } from '@blocksuite/block-std';
|
||||
import {
|
||||
type GfxBlockElementModel,
|
||||
type GfxCompatibleProps,
|
||||
GfxControllerIdentifier,
|
||||
type GfxModel,
|
||||
type GfxPrimitiveElementModel,
|
||||
type SerializedElement,
|
||||
SortOrder,
|
||||
} from '@blocksuite/block-std/gfx';
|
||||
import { Bound, type IVec, type SerializedXYWH } from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import { type BlockSnapshot, BlockSnapshotSchema } from '@blocksuite/store';
|
||||
|
||||
import { createCanvasElement } from './canvas';
|
||||
import {
|
||||
createNewPresentationIndexes,
|
||||
edgelessElementsBoundFromRawData,
|
||||
} from './utils';
|
||||
|
||||
interface Input {
|
||||
elementsRawData: (SerializedElement | BlockSnapshot)[];
|
||||
pasteCenter?: IVec;
|
||||
}
|
||||
|
||||
type CreatedElements = {
|
||||
canvasElements: GfxPrimitiveElementModel[];
|
||||
blockModels: GfxBlockElementModel[];
|
||||
};
|
||||
|
||||
interface Output {
|
||||
createdElementsPromise: Promise<CreatedElements>;
|
||||
}
|
||||
|
||||
export const createElementsFromClipboardDataCommand: Command<Input, Output> = (
|
||||
ctx,
|
||||
next
|
||||
) => {
|
||||
const { std, elementsRawData } = ctx;
|
||||
let { pasteCenter } = ctx;
|
||||
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const toolManager = gfx.tool;
|
||||
|
||||
const runner = async (): Promise<CreatedElements> => {
|
||||
let oldCommonBound, pasteX, pasteY;
|
||||
{
|
||||
const lastMousePos = toolManager.lastMousePos$.peek();
|
||||
pasteCenter =
|
||||
pasteCenter ??
|
||||
gfx.viewport.toModelCoord(lastMousePos.x, lastMousePos.y);
|
||||
const [modelX, modelY] = pasteCenter;
|
||||
oldCommonBound = edgelessElementsBoundFromRawData(elementsRawData);
|
||||
|
||||
pasteX = modelX - oldCommonBound.w / 2;
|
||||
pasteY = modelY - oldCommonBound.h / 2;
|
||||
}
|
||||
|
||||
const getNewXYWH = (oldXYWH: SerializedXYWH) => {
|
||||
const oldBound = Bound.deserialize(oldXYWH);
|
||||
return new Bound(
|
||||
oldBound.x + pasteX - oldCommonBound.x,
|
||||
oldBound.y + pasteY - oldCommonBound.y,
|
||||
oldBound.w,
|
||||
oldBound.h
|
||||
).serialize();
|
||||
};
|
||||
|
||||
// create blocks and canvas elements
|
||||
|
||||
const context: ClipboardConfigCreationContext = {
|
||||
oldToNewIdMap: new Map<string, string>(),
|
||||
originalIndexes: new Map<string, string>(),
|
||||
newPresentationIndexes: createNewPresentationIndexes(
|
||||
elementsRawData,
|
||||
std
|
||||
),
|
||||
};
|
||||
|
||||
const blockModels: GfxBlockElementModel[] = [];
|
||||
const canvasElements: GfxPrimitiveElementModel[] = [];
|
||||
const allElements: GfxModel[] = [];
|
||||
|
||||
for (const data of elementsRawData) {
|
||||
const { data: blockSnapshot } = BlockSnapshotSchema.safeParse(data);
|
||||
if (blockSnapshot) {
|
||||
const oldId = blockSnapshot.id;
|
||||
|
||||
const config = std.getOptional(
|
||||
EdgelessClipboardConfigIdentifier(blockSnapshot.flavour)
|
||||
);
|
||||
if (!config) continue;
|
||||
|
||||
if (typeof blockSnapshot.props.index !== 'string') {
|
||||
console.error(`Block(id: ${oldId}) does not have index property`);
|
||||
continue;
|
||||
}
|
||||
const originalIndex = (blockSnapshot.props as GfxCompatibleProps).index;
|
||||
|
||||
if (typeof blockSnapshot.props.xywh !== 'string') {
|
||||
console.error(`Block(id: ${oldId}) does not have xywh property`);
|
||||
continue;
|
||||
}
|
||||
|
||||
assertType<GfxCompatibleProps>(blockSnapshot.props);
|
||||
|
||||
blockSnapshot.props.xywh = getNewXYWH(
|
||||
blockSnapshot.props.xywh as SerializedXYWH
|
||||
);
|
||||
blockSnapshot.props.lockedBySelf = false;
|
||||
|
||||
const newId = await config.createBlock(blockSnapshot, context);
|
||||
if (!newId) continue;
|
||||
|
||||
const block = std.store.getBlock(newId);
|
||||
if (!block) continue;
|
||||
|
||||
assertType<GfxBlockElementModel>(block.model);
|
||||
blockModels.push(block.model);
|
||||
allElements.push(block.model);
|
||||
context.oldToNewIdMap.set(oldId, newId);
|
||||
context.originalIndexes.set(oldId, originalIndex);
|
||||
} else {
|
||||
assertType<SerializedElement>(data);
|
||||
const oldId = data.id;
|
||||
|
||||
const element = createCanvasElement(
|
||||
std,
|
||||
data,
|
||||
context,
|
||||
getNewXYWH(data.xywh)
|
||||
);
|
||||
|
||||
if (!element) continue;
|
||||
|
||||
canvasElements.push(element);
|
||||
allElements.push(element);
|
||||
|
||||
context.oldToNewIdMap.set(oldId, element.id);
|
||||
context.originalIndexes.set(oldId, element.index);
|
||||
}
|
||||
}
|
||||
|
||||
// remap old id to new id for the original index
|
||||
const oldIds = [...context.originalIndexes.keys()];
|
||||
oldIds.forEach(oldId => {
|
||||
const newId = context.oldToNewIdMap.get(oldId);
|
||||
const originalIndex = context.originalIndexes.get(oldId);
|
||||
if (newId && originalIndex) {
|
||||
context.originalIndexes.set(newId, originalIndex);
|
||||
context.originalIndexes.delete(oldId);
|
||||
}
|
||||
});
|
||||
|
||||
updatePastedElementsIndex(std, allElements, context.originalIndexes);
|
||||
|
||||
return {
|
||||
canvasElements: canvasElements,
|
||||
blockModels: blockModels,
|
||||
};
|
||||
};
|
||||
|
||||
return next({
|
||||
createdElementsPromise: runner(),
|
||||
});
|
||||
};
|
||||
|
||||
function updatePastedElementsIndex(
|
||||
std: BlockStdScope,
|
||||
elements: GfxModel[],
|
||||
originalIndexes: Map<string, string>
|
||||
) {
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const crud = std.get(EdgelessCRUDIdentifier);
|
||||
function compare(a: GfxModel, b: GfxModel) {
|
||||
if (a instanceof SurfaceGroupLikeModel && a.hasDescendant(b)) {
|
||||
return SortOrder.BEFORE;
|
||||
} else if (b instanceof SurfaceGroupLikeModel && b.hasDescendant(a)) {
|
||||
return SortOrder.AFTER;
|
||||
} else {
|
||||
const aGroups = a.groups as SurfaceGroupLikeModel[];
|
||||
const bGroups = b.groups as SurfaceGroupLikeModel[];
|
||||
|
||||
let i = 1;
|
||||
let aGroup: GfxModel | undefined = aGroups.at(-i);
|
||||
let bGroup: GfxModel | undefined = bGroups.at(-i);
|
||||
|
||||
while (aGroup === bGroup && aGroup) {
|
||||
++i;
|
||||
aGroup = aGroups.at(-i);
|
||||
bGroup = bGroups.at(-i);
|
||||
}
|
||||
|
||||
aGroup = aGroup ?? a;
|
||||
bGroup = bGroup ?? b;
|
||||
|
||||
return originalIndexes.get(aGroup.id) === originalIndexes.get(bGroup.id)
|
||||
? SortOrder.SAME
|
||||
: originalIndexes.get(aGroup.id)! < originalIndexes.get(bGroup.id)!
|
||||
? SortOrder.BEFORE
|
||||
: SortOrder.AFTER;
|
||||
}
|
||||
}
|
||||
|
||||
const idxGenerator = gfx.layer.createIndexGenerator();
|
||||
const sortedElements = elements.sort(compare);
|
||||
sortedElements.forEach(ele => {
|
||||
const newIndex = idxGenerator();
|
||||
|
||||
crud.updateElement(ele.id, {
|
||||
index: newIndex,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -49,7 +49,7 @@ import { effect } from '@preact/signals-core';
|
||||
import clamp from 'lodash-es/clamp';
|
||||
import last from 'lodash-es/last';
|
||||
|
||||
import { EdgelessClipboardController } from '../clipboard/clipboard.js';
|
||||
import { createElementsFromClipboardDataCommand } from '../clipboard/command.js';
|
||||
import { prepareCloneData } from '../utils/clone-utils.js';
|
||||
import { calPanDelta } from '../utils/panning-utils.js';
|
||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||
@@ -238,18 +238,18 @@ export class DefaultTool extends BaseTool {
|
||||
private async _cloneContent() {
|
||||
if (!this._edgeless) return;
|
||||
|
||||
const clipboardController = this.std.getOptional(
|
||||
EdgelessClipboardController
|
||||
);
|
||||
if (!clipboardController) return;
|
||||
const snapshot = prepareCloneData(this._toBeMoved, this.std);
|
||||
|
||||
const bound = getCommonBoundWithRotation(this._toBeMoved);
|
||||
const { canvasElements, blockModels } =
|
||||
await clipboardController.createElementsFromClipboardData(
|
||||
snapshot,
|
||||
bound.center
|
||||
);
|
||||
const [_, { createdElementsPromise }] = this.std.command.exec(
|
||||
createElementsFromClipboardDataCommand,
|
||||
{
|
||||
elementsRawData: snapshot,
|
||||
pasteCenter: bound.center,
|
||||
}
|
||||
);
|
||||
if (!createdElementsPromise) return;
|
||||
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||
|
||||
this._toBeMoved = [...canvasElements, ...blockModels];
|
||||
this.edgelessSelectionManager.set({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './clipboard/clipboard';
|
||||
export * from './clipboard/command';
|
||||
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
|
||||
export * from './components/toolbar/template/template-type.js';
|
||||
export * from './edgeless-root-block.js';
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { getCommonBoundWithRotation } from '@blocksuite/global/gfx';
|
||||
import groupBy from 'lodash-es/groupBy';
|
||||
|
||||
import { EdgelessClipboardController } from '../clipboard/clipboard.js';
|
||||
import { createElementsFromClipboardDataCommand } from '../clipboard/command.js';
|
||||
import { getSortedCloneElements, prepareCloneData } from './clone-utils.js';
|
||||
import {
|
||||
isEdgelessTextBlock,
|
||||
@@ -34,7 +34,6 @@ export async function duplicate(
|
||||
elements: GfxModel[],
|
||||
select = true
|
||||
) {
|
||||
const edgelessClipboard = edgeless.std.get(EdgelessClipboardController);
|
||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||
|
||||
const surface = getSurfaceComponent(edgeless.std);
|
||||
@@ -45,11 +44,15 @@ export async function duplicate(
|
||||
totalBound.x += totalBound.w + offset;
|
||||
|
||||
const snapshot = prepareCloneData(copyElements, edgeless.std);
|
||||
const { canvasElements, blockModels } =
|
||||
await edgelessClipboard.createElementsFromClipboardData(
|
||||
snapshot,
|
||||
totalBound.center
|
||||
);
|
||||
const [_, { createdElementsPromise }] = edgeless.std.command.exec(
|
||||
createElementsFromClipboardDataCommand,
|
||||
{
|
||||
elementsRawData: snapshot,
|
||||
pasteCenter: totalBound.center,
|
||||
}
|
||||
);
|
||||
if (!createdElementsPromise) return;
|
||||
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||
|
||||
const newElements = [...canvasElements, ...blockModels];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user