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 { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame';
|
||||||
import { addImages } from '@blocksuite/affine-block-image';
|
import { addImages } from '@blocksuite/affine-block-image';
|
||||||
import {
|
import {
|
||||||
CanvasElementType,
|
|
||||||
type ClipboardConfigCreationContext,
|
|
||||||
EdgelessClipboardConfigIdentifier,
|
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
ExportManager,
|
ExportManager,
|
||||||
getSurfaceComponent,
|
getSurfaceComponent,
|
||||||
SurfaceGroupLikeModel,
|
|
||||||
TextUtils,
|
TextUtils,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import type { Connection, ShapeElementModel } from '@blocksuite/affine-model';
|
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
BookmarkStyles,
|
BookmarkStyles,
|
||||||
DEFAULT_NOTE_HEIGHT,
|
DEFAULT_NOTE_HEIGHT,
|
||||||
@@ -48,12 +44,9 @@ import type {
|
|||||||
import {
|
import {
|
||||||
compareLayer,
|
compareLayer,
|
||||||
type GfxBlockElementModel,
|
type GfxBlockElementModel,
|
||||||
type GfxCompatibleProps,
|
|
||||||
GfxControllerIdentifier,
|
GfxControllerIdentifier,
|
||||||
type GfxModel,
|
|
||||||
type GfxPrimitiveElementModel,
|
type GfxPrimitiveElementModel,
|
||||||
type SerializedElement,
|
type SerializedElement,
|
||||||
SortOrder,
|
|
||||||
} from '@blocksuite/block-std/gfx';
|
} from '@blocksuite/block-std/gfx';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
@@ -62,23 +55,16 @@ import {
|
|||||||
getCommonBound,
|
getCommonBound,
|
||||||
type IBound,
|
type IBound,
|
||||||
type IVec,
|
type IVec,
|
||||||
type SerializedXYWH,
|
|
||||||
Vec,
|
Vec,
|
||||||
} from '@blocksuite/global/gfx';
|
} from '@blocksuite/global/gfx';
|
||||||
import { assertType } from '@blocksuite/global/utils';
|
import { type BlockSnapshot, type SliceSnapshot } from '@blocksuite/store';
|
||||||
import {
|
|
||||||
type BlockSnapshot,
|
|
||||||
BlockSnapshotSchema,
|
|
||||||
type SliceSnapshot,
|
|
||||||
} from '@blocksuite/store';
|
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { PageClipboard } from '../../clipboard/index.js';
|
import { PageClipboard } from '../../clipboard/index.js';
|
||||||
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
import { getSortedCloneElements } from '../utils/clone-utils.js';
|
||||||
import { isCanvasElementWithText } from '../utils/query.js';
|
import { isCanvasElementWithText } from '../utils/query.js';
|
||||||
|
import { createElementsFromClipboardDataCommand } from './command.js';
|
||||||
import {
|
import {
|
||||||
createNewPresentationIndexes,
|
|
||||||
edgelessElementsBoundFromRawData,
|
|
||||||
isPureFileInClipboard,
|
isPureFileInClipboard,
|
||||||
prepareClipboardData,
|
prepareClipboardData,
|
||||||
tryGetSvgFromClipboard,
|
tryGetSvgFromClipboard,
|
||||||
@@ -86,7 +72,6 @@ import {
|
|||||||
|
|
||||||
const BLOCKSUITE_SURFACE = 'blocksuite/surface';
|
const BLOCKSUITE_SURFACE = 'blocksuite/surface';
|
||||||
|
|
||||||
const { GROUP, MINDMAP, CONNECTOR } = CanvasElementType;
|
|
||||||
const IMAGE_PADDING = 5; // for rotated shapes some padding is needed
|
const IMAGE_PADDING = 5; // for rotated shapes some padding is needed
|
||||||
|
|
||||||
interface CanvasExportOptions {
|
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(
|
private async _edgelessToCanvas(
|
||||||
bound: IBound,
|
bound: IBound,
|
||||||
nodes?: GfxBlockElementModel[],
|
nodes?: GfxBlockElementModel[],
|
||||||
@@ -680,8 +564,14 @@ export class EdgelessClipboardController extends PageClipboard {
|
|||||||
private async _pasteShapesAndBlocks(
|
private async _pasteShapesAndBlocks(
|
||||||
elementsRawData: (SerializedElement | BlockSnapshot)[]
|
elementsRawData: (SerializedElement | BlockSnapshot)[]
|
||||||
) {
|
) {
|
||||||
const { canvasElements, blockModels } =
|
const [_, { createdElementsPromise }] = this.std.command.exec(
|
||||||
await this.createElementsFromClipboardData(elementsRawData);
|
createElementsFromClipboardDataCommand,
|
||||||
|
{
|
||||||
|
elementsRawData,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!createdElementsPromise) return;
|
||||||
|
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||||
this._emitSelectionChangeAfterPaste(
|
this._emitSelectionChangeAfterPaste(
|
||||||
canvasElements.map(ele => ele.id),
|
canvasElements.map(ele => ele.id),
|
||||||
blockModels.map(block => block.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() {
|
copy() {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new Event('copy', {
|
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() {
|
override mounted() {
|
||||||
if (!navigator.clipboard) {
|
if (!navigator.clipboard) {
|
||||||
console.error(
|
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 clamp from 'lodash-es/clamp';
|
||||||
import last from 'lodash-es/last';
|
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 { prepareCloneData } from '../utils/clone-utils.js';
|
||||||
import { calPanDelta } from '../utils/panning-utils.js';
|
import { calPanDelta } from '../utils/panning-utils.js';
|
||||||
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
|
||||||
@@ -238,18 +238,18 @@ export class DefaultTool extends BaseTool {
|
|||||||
private async _cloneContent() {
|
private async _cloneContent() {
|
||||||
if (!this._edgeless) return;
|
if (!this._edgeless) return;
|
||||||
|
|
||||||
const clipboardController = this.std.getOptional(
|
|
||||||
EdgelessClipboardController
|
|
||||||
);
|
|
||||||
if (!clipboardController) return;
|
|
||||||
const snapshot = prepareCloneData(this._toBeMoved, this.std);
|
const snapshot = prepareCloneData(this._toBeMoved, this.std);
|
||||||
|
|
||||||
const bound = getCommonBoundWithRotation(this._toBeMoved);
|
const bound = getCommonBoundWithRotation(this._toBeMoved);
|
||||||
const { canvasElements, blockModels } =
|
const [_, { createdElementsPromise }] = this.std.command.exec(
|
||||||
await clipboardController.createElementsFromClipboardData(
|
createElementsFromClipboardDataCommand,
|
||||||
snapshot,
|
{
|
||||||
bound.center
|
elementsRawData: snapshot,
|
||||||
);
|
pasteCenter: bound.center,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!createdElementsPromise) return;
|
||||||
|
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||||
|
|
||||||
this._toBeMoved = [...canvasElements, ...blockModels];
|
this._toBeMoved = [...canvasElements, ...blockModels];
|
||||||
this.edgelessSelectionManager.set({
|
this.edgelessSelectionManager.set({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './clipboard/clipboard';
|
export * from './clipboard/clipboard';
|
||||||
|
export * from './clipboard/command';
|
||||||
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
|
export { EdgelessTemplatePanel } from './components/toolbar/template/template-panel.js';
|
||||||
export * from './components/toolbar/template/template-type.js';
|
export * from './components/toolbar/template/template-type.js';
|
||||||
export * from './edgeless-root-block.js';
|
export * from './edgeless-root-block.js';
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { getCommonBoundWithRotation } from '@blocksuite/global/gfx';
|
import { getCommonBoundWithRotation } from '@blocksuite/global/gfx';
|
||||||
import groupBy from 'lodash-es/groupBy';
|
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 { getSortedCloneElements, prepareCloneData } from './clone-utils.js';
|
||||||
import {
|
import {
|
||||||
isEdgelessTextBlock,
|
isEdgelessTextBlock,
|
||||||
@@ -34,7 +34,6 @@ export async function duplicate(
|
|||||||
elements: GfxModel[],
|
elements: GfxModel[],
|
||||||
select = true
|
select = true
|
||||||
) {
|
) {
|
||||||
const edgelessClipboard = edgeless.std.get(EdgelessClipboardController);
|
|
||||||
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
const gfx = edgeless.std.get(GfxControllerIdentifier);
|
||||||
|
|
||||||
const surface = getSurfaceComponent(edgeless.std);
|
const surface = getSurfaceComponent(edgeless.std);
|
||||||
@@ -45,11 +44,15 @@ export async function duplicate(
|
|||||||
totalBound.x += totalBound.w + offset;
|
totalBound.x += totalBound.w + offset;
|
||||||
|
|
||||||
const snapshot = prepareCloneData(copyElements, edgeless.std);
|
const snapshot = prepareCloneData(copyElements, edgeless.std);
|
||||||
const { canvasElements, blockModels } =
|
const [_, { createdElementsPromise }] = edgeless.std.command.exec(
|
||||||
await edgelessClipboard.createElementsFromClipboardData(
|
createElementsFromClipboardDataCommand,
|
||||||
snapshot,
|
{
|
||||||
totalBound.center
|
elementsRawData: snapshot,
|
||||||
);
|
pasteCenter: totalBound.center,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!createdElementsPromise) return;
|
||||||
|
const { canvasElements, blockModels } = await createdElementsPromise;
|
||||||
|
|
||||||
const newElements = [...canvasElements, ...blockModels];
|
const newElements = [...canvasElements, ...blockModels];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user