mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-09 21:25:45 +08:00
feat(editor): gfx template package (#11480)
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
import { generateElementId, sortIndex } from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import type { BlockSnapshot, SnapshotNode } from '@blocksuite/store';
|
||||
|
||||
import type { SlotBlockPayload, TemplateJob } from './template.js';
|
||||
|
||||
export const replaceIdMiddleware = (job: TemplateJob) => {
|
||||
const regeneratedIdMap = new Map<string, string>();
|
||||
|
||||
job.slots.beforeInsert.subscribe(payload => {
|
||||
switch (payload.type) {
|
||||
case 'block':
|
||||
regenerateBlockId(payload.data);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
const regenerateBlockId = (data: SlotBlockPayload['data']) => {
|
||||
const { blockJson } = data;
|
||||
const newId = regeneratedIdMap.has(blockJson.id)
|
||||
? regeneratedIdMap.get(blockJson.id)!
|
||||
: job.model.doc.workspace.idGenerator();
|
||||
|
||||
if (!regeneratedIdMap.has(blockJson.id)) {
|
||||
regeneratedIdMap.set(blockJson.id, newId);
|
||||
}
|
||||
|
||||
blockJson.id = newId;
|
||||
|
||||
data.parent = data.parent
|
||||
? (regeneratedIdMap.get(data.parent) ?? data.parent)
|
||||
: undefined;
|
||||
|
||||
if (blockJson.flavour === 'affine:surface-ref') {
|
||||
assertType<
|
||||
SnapshotNode<{
|
||||
reference: string;
|
||||
}>
|
||||
>(blockJson);
|
||||
|
||||
blockJson.props['reference'] =
|
||||
regeneratedIdMap.get(blockJson.props['reference']) ?? '';
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
const elements: Record<string, Record<string, unknown>> = {};
|
||||
const defered: string[] = [];
|
||||
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([id, val]) => {
|
||||
const newId = generateElementId();
|
||||
|
||||
regeneratedIdMap.set(id, newId);
|
||||
val.id = newId;
|
||||
elements[newId] = val;
|
||||
|
||||
if (['connector', 'group'].includes(val['type'] as string)) {
|
||||
defered.push(newId);
|
||||
}
|
||||
});
|
||||
|
||||
blockJson.children.forEach(block => {
|
||||
regeneratedIdMap.set(block.id, job.model.doc.workspace.idGenerator());
|
||||
});
|
||||
|
||||
defered.forEach(id => {
|
||||
const element = elements[id]!;
|
||||
|
||||
switch (element['type'] as string) {
|
||||
case 'group':
|
||||
{
|
||||
const children = element['children'] as {
|
||||
json: Record<string, boolean>;
|
||||
};
|
||||
const newChildrenJson: Record<string, boolean> = {};
|
||||
|
||||
Object.entries(children.json).forEach(([key, val]) => {
|
||||
newChildrenJson[regeneratedIdMap.get(key) ?? key] = val;
|
||||
});
|
||||
|
||||
children.json = newChildrenJson;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'connector':
|
||||
{
|
||||
const target = element['target'] as { id?: string };
|
||||
|
||||
if (target.id) {
|
||||
element['target'] = {
|
||||
...target,
|
||||
id: regeneratedIdMap.get(target.id),
|
||||
};
|
||||
}
|
||||
|
||||
const source = element['source'] as { id?: string };
|
||||
|
||||
if (source.id) {
|
||||
element['source'] = {
|
||||
...source,
|
||||
id: regeneratedIdMap.get(source.id),
|
||||
};
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
blockJson.props.elements = elements;
|
||||
}
|
||||
|
||||
// remap childElementIds of frame
|
||||
if (blockJson.flavour === 'affine:frame') {
|
||||
assertType<Record<string, boolean>>(blockJson.props.childElementIds);
|
||||
const newChildElementIds: Record<string, boolean> = {};
|
||||
Object.entries(blockJson.props.childElementIds).forEach(([key, val]) => {
|
||||
newChildElementIds[regeneratedIdMap.get(key) ?? key] = val;
|
||||
});
|
||||
blockJson.props.childElementIds = newChildElementIds;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const createInsertPlaceMiddleware = (targetPlace: Bound) => {
|
||||
return (job: TemplateJob) => {
|
||||
if (job.type !== 'template') return;
|
||||
|
||||
let templateBound: Bound | null = null;
|
||||
let offset: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'template') {
|
||||
templateBound = blockData.bound;
|
||||
|
||||
if (templateBound) {
|
||||
offset = {
|
||||
x: targetPlace.x - templateBound.x,
|
||||
y: targetPlace.y - templateBound.y,
|
||||
};
|
||||
|
||||
templateBound.x = targetPlace.x;
|
||||
templateBound.y = targetPlace.y;
|
||||
}
|
||||
} else {
|
||||
if (templateBound && offset) changePosition(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const ignoreType = new Set(['group', 'connector']);
|
||||
const changePosition = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.props.xywh) {
|
||||
const bound = Bound.deserialize(blockJson.props['xywh'] as string);
|
||||
|
||||
blockJson.props['xywh'] = new Bound(
|
||||
bound.x + offset.x,
|
||||
bound.y + offset.y,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, val]) => {
|
||||
const type = val['type'] as string;
|
||||
|
||||
if (ignoreType.has(type) && val['xywh']) {
|
||||
delete val['xywh'];
|
||||
}
|
||||
|
||||
if (val['xywh']) {
|
||||
const bound = Bound.deserialize(val['xywh'] as string);
|
||||
|
||||
val['xywh'] = new Bound(
|
||||
bound.x + offset.x,
|
||||
bound.y + offset.y,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
}
|
||||
|
||||
if (type === 'connector') {
|
||||
(['target', 'source'] as const).forEach(prop => {
|
||||
const propVal = val[prop];
|
||||
assertType<ConnectorElementModel['target']>(propVal);
|
||||
|
||||
if (propVal['id'] || !propVal['position']) return;
|
||||
const pos = propVal['position'];
|
||||
|
||||
propVal['position'] = [pos[0] + offset.x, pos[1] + offset.y];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createStickerMiddleware = (
|
||||
center: {
|
||||
x: number;
|
||||
y: number;
|
||||
},
|
||||
getIndex: () => string
|
||||
) => {
|
||||
return (job: TemplateJob) => {
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'block') {
|
||||
changeInserPosition(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const changeInserPosition = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.flavour === 'affine:image' && blockJson.props.xywh) {
|
||||
const bound = Bound.deserialize(blockJson.props['xywh'] as string);
|
||||
|
||||
blockJson.props['xywh'] = new Bound(
|
||||
center.x - bound.w / 2,
|
||||
center.y - bound.h / 2,
|
||||
bound.w,
|
||||
bound.h
|
||||
).serialize();
|
||||
|
||||
blockJson.props.index = getIndex();
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const createRegenerateIndexMiddleware = (
|
||||
generateIndex: () => string
|
||||
) => {
|
||||
return (job: TemplateJob) => {
|
||||
job.slots.beforeInsert.subscribe(blockData => {
|
||||
if (blockData.type === 'template') {
|
||||
generateIndexMap();
|
||||
}
|
||||
|
||||
if (blockData.type === 'block') {
|
||||
resetIndex(blockData.data.blockJson);
|
||||
}
|
||||
});
|
||||
|
||||
const indexMap = new Map<string, string>();
|
||||
|
||||
const generateIndexMap = () => {
|
||||
const indexList: {
|
||||
id: string;
|
||||
index: string;
|
||||
flavour: string;
|
||||
element?: boolean;
|
||||
}[] = [];
|
||||
const frameList: {
|
||||
id: string;
|
||||
index: string;
|
||||
}[] = [];
|
||||
const groupIndexMap = new Map<
|
||||
string,
|
||||
{
|
||||
index: string;
|
||||
id: string;
|
||||
}
|
||||
>();
|
||||
|
||||
job.walk(block => {
|
||||
if (block.props.index) {
|
||||
if (block.flavour === 'affine:frame') {
|
||||
frameList.push({
|
||||
id: block.id,
|
||||
index: block.props.index as string,
|
||||
});
|
||||
} else {
|
||||
indexList.push({
|
||||
id: block.id,
|
||||
index: block.props.index as string,
|
||||
flavour: block.flavour,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (block.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
block.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, element]) => {
|
||||
indexList.push({
|
||||
index: element['index'] as string,
|
||||
flavour: element['type'] as string,
|
||||
id: element['id'] as string,
|
||||
element: true,
|
||||
});
|
||||
|
||||
if (element['type'] === 'group') {
|
||||
const children = element['children'] as {
|
||||
json: Record<string, boolean>;
|
||||
};
|
||||
const groupIndex = {
|
||||
index: element['index'] as string,
|
||||
id: element['id'] as string,
|
||||
};
|
||||
|
||||
Object.keys(children.json).forEach(key => {
|
||||
groupIndexMap.set(key, groupIndex);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
indexList.sort((a, b) => sortIndex(a, b, groupIndexMap));
|
||||
frameList.sort((a, b) => sortIndex(a, b, groupIndexMap));
|
||||
|
||||
frameList.forEach(index => {
|
||||
indexMap.set(index.id, generateIndex());
|
||||
});
|
||||
|
||||
indexList.forEach(index => {
|
||||
indexMap.set(index.id, generateIndex());
|
||||
});
|
||||
};
|
||||
const resetIndex = (blockJson: BlockSnapshot) => {
|
||||
if (blockJson.props.index) {
|
||||
blockJson.props.index =
|
||||
indexMap.get(blockJson.id) ?? blockJson.props.index;
|
||||
}
|
||||
|
||||
if (blockJson.flavour === 'affine:surface') {
|
||||
Object.entries(
|
||||
blockJson.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, element]) => {
|
||||
if (element['index']) {
|
||||
element['index'] = indexMap.get(element['id'] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,432 @@
|
||||
import {
|
||||
getSurfaceBlock,
|
||||
type SurfaceBlockModel,
|
||||
type SurfaceBlockTransformer,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type { ConnectorElementModel } from '@blocksuite/affine-model';
|
||||
import { BlockSuiteError } from '@blocksuite/global/exceptions';
|
||||
import { Bound, getCommonBound } from '@blocksuite/global/gfx';
|
||||
import { assertType } from '@blocksuite/global/utils';
|
||||
import type { BlockStdScope } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
import {
|
||||
type BlockModel,
|
||||
type BlockSnapshot,
|
||||
type DocSnapshot,
|
||||
DocSnapshotSchema,
|
||||
type SnapshotNode,
|
||||
type Transformer,
|
||||
} from '@blocksuite/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import type * as Y from 'yjs';
|
||||
|
||||
import {
|
||||
createInsertPlaceMiddleware,
|
||||
createRegenerateIndexMiddleware,
|
||||
createStickerMiddleware,
|
||||
replaceIdMiddleware,
|
||||
} from './template-middlewares';
|
||||
/**
|
||||
* Those block contains other block's id
|
||||
* should defer the loading
|
||||
*/
|
||||
const DEFERED_BLOCK = [
|
||||
'affine:surface',
|
||||
'affine:surface-ref',
|
||||
'affine:frame',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Those block should not be inserted directly
|
||||
* it should be merged with current existing block
|
||||
*/
|
||||
const MERGE_BLOCK = ['affine:surface', 'affine:page'] as const;
|
||||
|
||||
type MergeBlockFlavour = (typeof MERGE_BLOCK)[number];
|
||||
|
||||
/**
|
||||
* Template type will affect the inserting behaviour
|
||||
*/
|
||||
const TEMPLATE_TYPES = ['template', 'sticker'] as const;
|
||||
|
||||
type TemplateType = (typeof TEMPLATE_TYPES)[number];
|
||||
|
||||
export type SlotBlockPayload = {
|
||||
type: 'block';
|
||||
data: {
|
||||
blockJson: BlockSnapshot;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SlotPayload =
|
||||
| SlotBlockPayload
|
||||
| {
|
||||
type: 'template';
|
||||
template: DocSnapshot;
|
||||
bound: Bound | null;
|
||||
};
|
||||
|
||||
export type TemplateJobConfig = {
|
||||
model: SurfaceBlockModel;
|
||||
type: string;
|
||||
middlewares: ((job: TemplateJob) => void)[];
|
||||
};
|
||||
|
||||
export class TemplateJob {
|
||||
static middlewares: ((job: TemplateJob) => void)[] = [];
|
||||
|
||||
private _template: DocSnapshot | null = null;
|
||||
|
||||
job: Transformer;
|
||||
|
||||
model: SurfaceBlockModel;
|
||||
|
||||
slots = {
|
||||
beforeInsert: new Subject<
|
||||
| SlotBlockPayload
|
||||
| {
|
||||
type: 'template';
|
||||
template: DocSnapshot;
|
||||
bound: Bound | null;
|
||||
}
|
||||
>(),
|
||||
};
|
||||
|
||||
type: TemplateType;
|
||||
|
||||
constructor({ model, type, middlewares }: TemplateJobConfig) {
|
||||
this.job = model.doc.getTransformer();
|
||||
this.model = model;
|
||||
this.type = TEMPLATE_TYPES.includes(type as TemplateType)
|
||||
? (type as TemplateType)
|
||||
: 'template';
|
||||
|
||||
middlewares.forEach(middleware => middleware(this));
|
||||
TemplateJob.middlewares.forEach(middleware => middleware(this));
|
||||
}
|
||||
|
||||
static create(options: {
|
||||
model: SurfaceBlockModel;
|
||||
type: string;
|
||||
middlewares: ((job: TemplateJob) => void)[];
|
||||
}) {
|
||||
return new TemplateJob(options);
|
||||
}
|
||||
|
||||
private _getMergeBlockId(modelData: BlockSnapshot) {
|
||||
switch (modelData.flavour as MergeBlockFlavour) {
|
||||
case 'affine:page':
|
||||
return this.model.doc.root!.id;
|
||||
case 'affine:surface':
|
||||
return this.model.id;
|
||||
}
|
||||
}
|
||||
|
||||
private _getTemplateBound() {
|
||||
const bounds: Bound[] = [];
|
||||
|
||||
this.walk(block => {
|
||||
if (block.props.xywh) {
|
||||
bounds.push(Bound.deserialize(block.props['xywh'] as string));
|
||||
}
|
||||
|
||||
if (block.flavour === 'affine:surface') {
|
||||
const ignoreType = new Set(['connector', 'group']);
|
||||
|
||||
Object.entries(
|
||||
block.props.elements as Record<string, Record<string, unknown>>
|
||||
).forEach(([_, val]) => {
|
||||
const type = val['type'] as string;
|
||||
|
||||
if (val['xywh'] && !ignoreType.has(type)) {
|
||||
bounds.push(Bound.deserialize(val['xywh'] as string));
|
||||
}
|
||||
|
||||
if (type === 'connector') {
|
||||
(['target', 'source'] as const).forEach(prop => {
|
||||
const propVal = val[prop];
|
||||
assertType<ConnectorElementModel['source']>(propVal);
|
||||
|
||||
if (propVal['id'] || !propVal['position']) return;
|
||||
|
||||
const pos = propVal['position'];
|
||||
|
||||
if (pos) {
|
||||
bounds.push(new Bound(pos[0], pos[1], 0, 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return getCommonBound(bounds);
|
||||
}
|
||||
|
||||
private _insertToDoc(
|
||||
modelDataList: {
|
||||
flavour: string;
|
||||
json: BlockSnapshot;
|
||||
modelData: SnapshotNode<object> | null;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[]
|
||||
) {
|
||||
const doc = this.model.doc;
|
||||
const mergeIdMapping = new Map<string, string>();
|
||||
const deferInserting: typeof modelDataList = [];
|
||||
|
||||
const insert = (
|
||||
data: (typeof modelDataList)[number],
|
||||
defered: boolean = true
|
||||
) => {
|
||||
const { flavour, json, modelData, parent, index } = data;
|
||||
const isMergeBlock = MERGE_BLOCK.includes(flavour as MergeBlockFlavour);
|
||||
|
||||
if (isMergeBlock) {
|
||||
mergeIdMapping.set(json.id, this._getMergeBlockId(json));
|
||||
}
|
||||
|
||||
if (
|
||||
defered &&
|
||||
DEFERED_BLOCK.includes(flavour as (typeof DEFERED_BLOCK)[number])
|
||||
) {
|
||||
deferInserting.push(data);
|
||||
return;
|
||||
} else {
|
||||
if (isMergeBlock) {
|
||||
this._mergeProps(
|
||||
json,
|
||||
this.model.doc.getModelById(
|
||||
this._getMergeBlockId(json)
|
||||
) as BlockModel
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!modelData) {
|
||||
return;
|
||||
}
|
||||
|
||||
doc.addBlock(
|
||||
modelData.flavour,
|
||||
{
|
||||
...modelData.props,
|
||||
id: modelData.id,
|
||||
},
|
||||
parent ? (mergeIdMapping.get(parent) ?? parent) : undefined,
|
||||
index
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
modelDataList.forEach(data => insert(data));
|
||||
deferInserting.forEach(data => insert(data, false));
|
||||
}
|
||||
|
||||
private async _jsonToModelData(json: BlockSnapshot) {
|
||||
const job = this.job;
|
||||
const defered: {
|
||||
snapshot: BlockSnapshot;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[] = [];
|
||||
const modelDataList: {
|
||||
flavour: string;
|
||||
json: BlockSnapshot;
|
||||
modelData: SnapshotNode<object> | null;
|
||||
parent?: string;
|
||||
index?: number;
|
||||
}[] = [];
|
||||
const toModel = async (
|
||||
snapshot: BlockSnapshot,
|
||||
parent?: string,
|
||||
index?: number,
|
||||
defer: boolean = true
|
||||
) => {
|
||||
if (
|
||||
defer &&
|
||||
DEFERED_BLOCK.includes(
|
||||
snapshot.flavour as (typeof DEFERED_BLOCK)[number]
|
||||
)
|
||||
) {
|
||||
defered.push({
|
||||
snapshot,
|
||||
parent,
|
||||
index,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const slotData = {
|
||||
blockJson: snapshot,
|
||||
parent,
|
||||
index,
|
||||
};
|
||||
|
||||
this.slots.beforeInsert.next({ type: 'block', data: slotData });
|
||||
|
||||
/**
|
||||
* merge block should not be converted to model data
|
||||
*/
|
||||
const modelData = MERGE_BLOCK.includes(
|
||||
snapshot.flavour as MergeBlockFlavour
|
||||
)
|
||||
? null
|
||||
: ((await job.snapshotToModelData(snapshot)) ?? null);
|
||||
|
||||
modelDataList.push({
|
||||
flavour: snapshot.flavour,
|
||||
json: snapshot,
|
||||
modelData,
|
||||
parent,
|
||||
index,
|
||||
});
|
||||
|
||||
if (snapshot.children) {
|
||||
let index = 0;
|
||||
for (const child of snapshot.children) {
|
||||
await toModel(child, snapshot.id, index);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await toModel(json);
|
||||
|
||||
for (const json of defered) {
|
||||
await toModel(json.snapshot, json.parent, json.index, false);
|
||||
}
|
||||
|
||||
return modelDataList;
|
||||
}
|
||||
|
||||
private _mergeProps(from: BlockSnapshot, to: BlockModel) {
|
||||
switch (from.flavour as MergeBlockFlavour) {
|
||||
case 'affine:page':
|
||||
break;
|
||||
case 'affine:surface':
|
||||
this._mergeSurfaceElements(
|
||||
from.props.elements as Record<string, Record<string, unknown>>,
|
||||
(to as SurfaceBlockModel).elements.getValue()!
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _mergeSurfaceElements(
|
||||
from: Record<string, Record<string, unknown>>,
|
||||
to: Y.Map<Y.Map<unknown>>
|
||||
) {
|
||||
const schema = this.model.doc.schema.get('affine:surface');
|
||||
const surfaceTransformer = schema?.transformer?.(
|
||||
new Map()
|
||||
) as SurfaceBlockTransformer;
|
||||
|
||||
this.model.doc.transact(() => {
|
||||
const defered: [string, Record<string, unknown>][] = [];
|
||||
|
||||
Object.entries(from).forEach(([id, val]) => {
|
||||
if (['connector', 'group'].includes(val.type as string)) {
|
||||
defered.push([id, val]);
|
||||
} else {
|
||||
to.set(id, surfaceTransformer.elementFromJSON(val));
|
||||
}
|
||||
});
|
||||
|
||||
defered.forEach(([key, val]) => {
|
||||
to.set(key, surfaceTransformer.elementFromJSON(val));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async insertTemplate(template: unknown) {
|
||||
DocSnapshotSchema.parse(template);
|
||||
|
||||
assertType<DocSnapshot>(template);
|
||||
|
||||
this._template = template;
|
||||
|
||||
const templateBound = this._getTemplateBound();
|
||||
|
||||
this.slots.beforeInsert.next({
|
||||
type: 'template',
|
||||
template: template,
|
||||
bound: templateBound,
|
||||
});
|
||||
|
||||
const modelDataList = await this._jsonToModelData(template.blocks);
|
||||
|
||||
this._insertToDoc(modelDataList);
|
||||
|
||||
return templateBound;
|
||||
}
|
||||
|
||||
walk(callback: (block: BlockSnapshot, template: DocSnapshot) => void) {
|
||||
if (!this._template) {
|
||||
throw new Error('Template not loaded, please call insertTemplate first');
|
||||
}
|
||||
|
||||
const iterate = (block: BlockSnapshot, template: DocSnapshot) => {
|
||||
callback(block, template);
|
||||
|
||||
if (block.children) {
|
||||
block.children.forEach(child => iterate(child, template));
|
||||
}
|
||||
};
|
||||
|
||||
iterate(this._template.blocks, this._template);
|
||||
}
|
||||
}
|
||||
|
||||
export function createTemplateJob(
|
||||
std: BlockStdScope,
|
||||
type: 'template' | 'sticker',
|
||||
center?: { x: number; y: number }
|
||||
) {
|
||||
const surface = getSurfaceBlock(std.store);
|
||||
if (!surface) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.NoSurfaceModelError,
|
||||
'This doc is missing surface block in edgeless.'
|
||||
);
|
||||
}
|
||||
|
||||
const gfx = std.get(GfxControllerIdentifier);
|
||||
const middlewares: ((job: TemplateJob) => void)[] = [];
|
||||
const { layer, viewport } = gfx;
|
||||
const blocks = layer.blocks;
|
||||
const elements = layer.canvasElements;
|
||||
|
||||
if (type === 'template') {
|
||||
const bounds = [...blocks, ...elements].map(i => Bound.deserialize(i.xywh));
|
||||
const currentContentBound = getCommonBound(bounds);
|
||||
|
||||
if (currentContentBound) {
|
||||
currentContentBound.x += currentContentBound.w + 20 / viewport.zoom;
|
||||
middlewares.push(createInsertPlaceMiddleware(currentContentBound));
|
||||
}
|
||||
|
||||
const idxGenerator = layer.createIndexGenerator();
|
||||
|
||||
middlewares.push(createRegenerateIndexMiddleware(() => idxGenerator()));
|
||||
}
|
||||
|
||||
if (type === 'sticker') {
|
||||
middlewares.push(
|
||||
createStickerMiddleware(center || viewport.center, () =>
|
||||
layer.generateIndex()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
middlewares.push(replaceIdMiddleware);
|
||||
|
||||
return TemplateJob.create({
|
||||
model: surface,
|
||||
type,
|
||||
middlewares,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user