refactor(editor): move export manager to surface block extensions (#10231)

This commit is contained in:
Saul-Mirone
2025-02-18 04:39:05 +00:00
parent 31f8e92a4b
commit c3e924d4cb
20 changed files with 128 additions and 119 deletions
@@ -24,9 +24,11 @@
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.7",
"fractional-indexing": "^3.2.0",
"html2canvas": "^1.4.1",
"lit": "^3.2.0",
"lodash.chunk": "^4.2.0",
"nanoid": "^5.0.7",
"pdf-lib": "^1.17.1",
"yjs": "^13.6.21",
"zod": "^3.23.8"
},
@@ -1,11 +1,8 @@
import {
type CanvasRenderer,
SurfaceElementModel,
} from '@blocksuite/affine-block-surface';
import {
FrameBlockModel,
GroupElementModel,
ImageBlockModel,
type NoteBlockModel,
type RootBlockModel,
} from '@blocksuite/affine-model';
import { FetchUtils } from '@blocksuite/affine-shared/adapters';
@@ -13,32 +10,32 @@ import {
CANVAS_EXPORT_IGNORE_TAGS,
DEFAULT_IMAGE_PROXY_ENDPOINT,
} from '@blocksuite/affine-shared/consts';
import type { Viewport } from '@blocksuite/affine-shared/types';
import {
isInsidePageEditor,
matchModels,
} from '@blocksuite/affine-shared/utils';
import {
type BlockComponent,
type BlockStdScope,
type EditorHost,
StdIdentifier,
} from '@blocksuite/block-std';
import type {
import {
GfxBlockElementModel,
GfxPrimitiveElementModel,
type GfxController,
GfxControllerIdentifier,
type GfxPrimitiveElementModel,
} from '@blocksuite/block-std/gfx';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import type { IBound } from '@blocksuite/global/utils';
import { Bound } from '@blocksuite/global/utils';
import { Bound, deserializeXYWH } from '@blocksuite/global/utils';
import type { ExtensionType, Store } from '@blocksuite/store';
import {
getBlockComponentByModel,
getRootByEditorHost,
} from '../../_common/utils/index.js';
import type { EdgelessRootBlockComponent } from '../../root-block/edgeless/edgeless-root-block.js';
import { getBlocksInFrameBound } from '../../root-block/edgeless/frame-manager.js';
import { xywhArrayToObject } from '../../root-block/edgeless/utils/convert.js';
import { getBackgroundGrid } from '../../root-block/edgeless/utils/query.js';
import { SurfaceElementModel } from '../../element-model/base.js';
import type { CanvasRenderer } from '../../renderer/canvas-renderer.js';
import type { SurfaceBlockComponent } from '../../surface-block.js';
import { getBgGridGap } from '../../utils/get-bg-grip-gap.js';
import { FileExporter } from './file-exporter.js';
// oxlint-disable-next-line typescript/consistent-type-imports
@@ -47,6 +44,7 @@ type Html2CanvasFunction = typeof import('html2canvas').default;
export type ExportOptions = {
imageProxyEndpoint: string;
};
export class ExportManager {
private readonly _exportOptions: ExportOptions = {
imageProxyEndpoint: DEFAULT_IMAGE_PROXY_ENDPOINT,
@@ -151,7 +149,9 @@ export class ExportManager {
}
const rootModel = this.doc.root;
const rootComponent = this.doc.root
? getBlockComponentByModel(this.editorHost, rootModel)
? rootModel
? this.editorHost.view.getBlock(rootModel.id)
: null
: null;
const imageCard = rootComponent?.querySelector(
'affine-image-fallback-card'
@@ -398,12 +398,11 @@ export class ExportManager {
const rootModel = this.doc.root;
if (!rootModel) return;
const edgeless = getBlockComponentByModel(
this.editorHost,
rootModel
) as EdgelessRootBlockComponent;
const bound = edgeless.gfx.elementsBound;
return this.edgelessToCanvas(edgeless.surface.renderer, bound, edgeless);
const gfx = this.editorHost.std.get(GfxControllerIdentifier);
const surfaceBlock = gfx.surfaceComponent as SurfaceBlockComponent | null;
if (!surfaceBlock) return;
const bound = gfx.elementsBound;
return this.edgelessToCanvas(surfaceBlock.renderer, bound, gfx);
}
}
@@ -411,7 +410,7 @@ export class ExportManager {
async edgelessToCanvas(
surfaceRenderer: CanvasRenderer,
bound: IBound,
edgeless?: EdgelessRootBlockComponent,
gfx: GfxController,
nodes?: GfxBlockElementModel[],
surfaces?: GfxPrimitiveElementModel[],
edgelessBackground?: {
@@ -449,7 +448,7 @@ export class ExportManager {
backgroundColor: containerComputedStyle.getPropertyValue(
'--affine-background-primary-color'
),
size: getBackgroundGrid(edgelessBackground.zoom, true).gap,
size: getBgGridGap(edgelessBackground.zoom),
gridColor: containerComputedStyle.getPropertyValue(
'--affine-edgeless-grid-color'
),
@@ -457,9 +456,7 @@ export class ExportManager {
}
const blocks =
nodes ??
edgeless?.service.gfx.getElementsByBound(bound, { type: 'block' }) ??
[];
nodes ?? gfx.getElementsByBound(bound, { type: 'block' }) ?? [];
for (const block of blocks) {
if (matchModels(block, [ImageBlockModel])) {
if (!block.sourceId) return;
@@ -592,3 +589,66 @@ export const ExportManagerExtension: ExtensionType = {
di.add(ExportManager, [StdIdentifier]);
},
};
function xywhArrayToObject(element: GfxBlockElementModel) {
const [x, y, w, h] = deserializeXYWH(element.xywh);
return { x, y, w, h };
}
function getNotesInFrameBound(
doc: Store,
frame: FrameBlockModel,
fullyContained: boolean = true
): NoteBlockModel[] {
const bound = Bound.deserialize(frame.xywh);
return (doc.getBlockByFlavour('affine:note') as NoteBlockModel[]).filter(
ele => {
const xywh = Bound.deserialize(ele.xywh);
return fullyContained
? bound.contains(xywh)
: bound.isPointInBound([xywh.x, xywh.y]);
}
);
}
function getBlocksInFrameBound(
doc: Store,
model: FrameBlockModel,
fullyContained: boolean = true
) {
const bound = Bound.deserialize(model.xywh);
const surface = model.surface;
if (!surface) return [];
return (
getNotesInFrameBound(doc, model, fullyContained) as GfxBlockElementModel[]
).concat(
surface.children.filter((ele): ele is GfxBlockElementModel => {
if (ele.id === model.id) return false;
if (ele instanceof GfxBlockElementModel) {
const blockBound = Bound.deserialize(ele.xywh);
return fullyContained
? bound.contains(blockBound)
: bound.containsPoint([blockBound.x, blockBound.y]);
}
return false;
})
);
}
type RootBlockComponent = BlockComponent & {
viewportElement: HTMLElement;
viewport: Viewport;
};
function getRootByEditorHost(
editorHost: EditorHost
): RootBlockComponent | null {
const model = editorHost.doc.root;
if (!model) return null;
const root = editorHost.view.getBlock(model.id);
return root as RootBlockComponent | null;
}
@@ -1,4 +1,4 @@
/* eslint-disable no-control-regex */
/* oxlint-disable no-control-regex */
// Context: Lean towards breaking out any localizable content into constants so it's
// easier to track content we may need to localize in the future. (i18n)
const UNTITLED_PAGE_NAME = 'Untitled';
@@ -0,0 +1 @@
export { ExportManager, ExportManagerExtension } from './export-manager.js';
@@ -1,3 +1,4 @@
export * from './crud-extension';
export * from './export-manager';
export * from './legacy-slot-extension';
export * from './query';
@@ -80,6 +80,7 @@ export {
addNote,
addNoteAtPoint,
generateElementId,
getBgGridGap,
getLastPropsKey,
getSurfaceBlock,
normalizeWheelDeltaY,
@@ -10,6 +10,7 @@ import {
EdgelessCRUDExtension,
EdgelessLegacySlotExtension,
} from './extensions';
import { ExportManagerExtension } from './extensions/export-manager/export-manager';
import { SurfaceBlockService } from './surface-service';
import { MindMapView } from './view/mindmap';
@@ -19,6 +20,7 @@ const CommonSurfaceBlockSpec: ExtensionType[] = [
MindMapView,
EdgelessCRUDExtension,
EdgelessLegacySlotExtension,
ExportManagerExtension,
];
export const PageSurfaceBlockSpec: ExtensionType[] = [
@@ -0,0 +1,10 @@
import { clamp } from '@blocksuite/global/utils';
import { GRID_GAP_MAX, GRID_GAP_MIN } from '../consts';
export function getBgGridGap(zoom: number) {
const step = zoom < 0.5 ? 2 : 1 / (Math.floor(zoom) || 1);
const gap = clamp(20 * step * zoom, GRID_GAP_MIN, GRID_GAP_MAX);
return gap;
}
@@ -34,6 +34,7 @@ export function normalizeWheelDeltaY(delta: number, zoom = 1) {
}
export { addNote, addNoteAtPoint } from './add-note';
export { getBgGridGap } from './get-bg-grip-gap';
export { getLastPropsKey } from './get-last-props-key';
export { getSurfaceBlock } from './get-surface-block';
export * from './mindmap/style-svg.js';
-1
View File
@@ -62,7 +62,6 @@
"lz-string": "^1.5.0",
"minimatch": "^10.0.1",
"nanoid": "^5.0.7",
"pdf-lib": "^1.17.1",
"shiki": "^2.0.0",
"simple-xml-to-json": "^1.2.2",
"yjs": "^13.6.21",
-4
View File
@@ -8,10 +8,6 @@ import { isCanvasElement } from './root-block/edgeless/utils/query.js';
export * from './_common/adapters/index.js';
export { type NavigatorMode } from './_common/edgeless/frame/consts.js';
export {
ExportManager,
ExportManagerExtension,
} from './_common/export-manager/export-manager.js';
export * from './_common/test-utils/test-utils.js';
export * from './_common/transformers/index.js';
export { type AbstractEditor } from './_common/types.js';
@@ -9,7 +9,6 @@ import {
import { FlavourExtension } from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { ExportManagerExtension } from '../../_common/export-manager/export-manager';
import { RootBlockAdapterExtensions } from '../adapters/extension';
import {
docRemoteSelectionWidget,
@@ -29,7 +28,6 @@ export const CommonSpecs: ExtensionType[] = [
DocModeService,
ThemeService,
EmbedOptionService,
ExportManagerExtension,
PageViewportServiceExtension,
DNDAPIExtension,
FileDropExtension,
@@ -3,6 +3,7 @@ import { addImages } from '@blocksuite/affine-block-image';
import {
CanvasElementType,
EdgelessCRUDIdentifier,
ExportManager,
SurfaceGroupLikeModel,
TextUtils,
} from '@blocksuite/affine-block-surface';
@@ -68,7 +69,6 @@ import {
import DOMPurify from 'dompurify';
import * as Y from 'yjs';
import { ExportManager } from '../../../_common/export-manager/export-manager.js';
import { getRootByEditorHost } from '../../../_common/utils/query.js';
import { ClipboardAdapter } from '../../clipboard/adapter.js';
import { PageClipboard } from '../../clipboard/index.js';
@@ -4,6 +4,7 @@ import type {
} from '@blocksuite/affine-block-surface';
import {
EdgelessLegacySlotIdentifier,
getBgGridGap,
normalizeWheelDeltaY,
} from '@blocksuite/affine-block-surface';
import {
@@ -51,7 +52,7 @@ import { EdgelessClipboardController } from './clipboard/clipboard.js';
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { getBackgroundGrid, isCanvasElement } from './utils/query.js';
import { isCanvasElement } from './utils/query.js';
import { mountShapeTextEditor } from './utils/text.js';
export class EdgelessRootBlockComponent extends BlockComponent<
@@ -106,7 +107,7 @@ export class EdgelessRootBlockComponent extends BlockComponent<
private readonly _refreshLayerViewport = requestThrottledConnectedFrame(
() => {
const { zoom, translateX, translateY } = this.gfx.viewport;
const { gap } = getBackgroundGrid(zoom, true);
const gap = getBgGridGap(zoom);
if (this.backgroundElm) {
this.backgroundElm.style.setProperty(
@@ -1,6 +1,7 @@
import type {
SurfaceBlockComponent,
SurfaceBlockModel,
import {
getBgGridGap,
type SurfaceBlockComponent,
type SurfaceBlockModel,
} from '@blocksuite/affine-block-surface';
import type { EdgelessPreviewer } from '@blocksuite/affine-block-surface-ref';
import type { RootBlockModel } from '@blocksuite/affine-model';
@@ -23,7 +24,7 @@ import { styleMap } from 'lit/directives/style-map.js';
import type { EdgelessRootBlockWidgetName } from '../types.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { getBackgroundGrid, isCanvasElement } from './utils/query.js';
import { isCanvasElement } from './utils/query.js';
export class EdgelessRootPreviewBlockComponent
extends BlockComponent<
@@ -73,7 +74,7 @@ export class EdgelessRootPreviewBlockComponent
private readonly _refreshLayerViewport = requestThrottledConnectedFrame(
() => {
const { zoom, translateX, translateY } = this.service.viewport;
const { gap } = getBackgroundGrid(zoom, true);
const gap = getBgGridGap(zoom);
this.background.style.setProperty(
'background-position',
@@ -1,6 +1,6 @@
import type { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { Overlay } from '@blocksuite/affine-block-surface';
import type { FrameBlockModel, NoteBlockModel } from '@blocksuite/affine-model';
import type { FrameBlockModel } from '@blocksuite/affine-model';
import { EditPropsStore } from '@blocksuite/affine-shared/services';
import {
generateKeyBetweenV2,
@@ -21,7 +21,6 @@ import {
type IVec,
type SerializedXYWH,
} from '@blocksuite/global/utils';
import type { Store } from '@blocksuite/store';
import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
@@ -462,47 +461,3 @@ export class EdgelessFrameManager extends GfxExtension {
this._disposable.dispose();
}
}
export function getNotesInFrameBound(
doc: Store,
frame: FrameBlockModel,
fullyContained: boolean = true
) {
const bound = Bound.deserialize(frame.xywh);
return (doc.getBlockByFlavour('affine:note') as NoteBlockModel[]).filter(
ele => {
const xywh = Bound.deserialize(ele.xywh);
return fullyContained
? bound.contains(xywh)
: bound.isPointInBound([xywh.x, xywh.y]);
}
) as NoteBlockModel[];
}
export function getBlocksInFrameBound(
doc: Store,
model: FrameBlockModel,
fullyContained: boolean = true
) {
const bound = Bound.deserialize(model.xywh);
const surface = model.surface;
if (!surface) return [];
return (
getNotesInFrameBound(doc, model, fullyContained) as GfxBlockElementModel[]
).concat(
surface.children.filter(ele => {
if (ele.id === model.id) return false;
if (ele instanceof GfxBlockElementModel) {
const blockBound = Bound.deserialize(ele.xywh);
return fullyContained
? bound.contains(blockBound)
: bound.containsPoint([blockBound.x, blockBound.y]);
}
return false;
}) as GfxBlockElementModel[]
);
}
@@ -1,7 +0,0 @@
import type { GfxBlockElementModel } from '@blocksuite/block-std/gfx';
import { deserializeXYWH } from '@blocksuite/global/utils';
export function xywhArrayToObject(element: GfxBlockElementModel) {
const [x, y, w, h] = deserializeXYWH(element.xywh);
return { x, y, w, h };
}
@@ -1,8 +1,4 @@
import {
type CanvasElementWithText,
GRID_GAP_MAX,
GRID_GAP_MIN,
} from '@blocksuite/affine-block-surface';
import type { CanvasElementWithText } from '@blocksuite/affine-block-surface';
import {
type AttachmentBlockModel,
type BookmarkBlockModel,
@@ -34,7 +30,7 @@ import type {
Viewport,
} from '@blocksuite/block-std/gfx';
import type { PointLocation } from '@blocksuite/global/utils';
import { Bound, clamp } from '@blocksuite/global/utils';
import { Bound } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import type { Connectable } from '../../../_common/utils/index.js';
@@ -228,18 +224,6 @@ export function getCursorMode(edgelessTool: GfxToolsFullOptionValue | null) {
}
}
export function getBackgroundGrid(zoom: number, showGrid: boolean) {
const step = zoom < 0.5 ? 2 : 1 / (Math.floor(zoom) || 1);
const gap = clamp(20 * step * zoom, GRID_GAP_MIN, GRID_GAP_MAX);
return {
gap,
grid: showGrid
? 'radial-gradient(var(--affine-edgeless-grid-color) 1px, var(--affine-background-primary-color) 1px)'
: 'unset',
};
}
export type SelectableProps = {
bound: Bound;
rotate: number;
@@ -1,12 +1,14 @@
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
import { ExportManager } from '@blocksuite/affine-block-surface';
import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref';
import { isTopLevelBlock } from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import {
GfxControllerIdentifier,
type GfxModel,
} from '@blocksuite/block-std/gfx';
import { assertExists, Bound } from '@blocksuite/global/utils';
import { ExportManager } from '../../../_common/export-manager/export-manager.js';
export const edgelessToBlob = async (
host: EditorHost,
options: {
@@ -19,12 +21,13 @@ export const edgelessToBlob = async (
const exportManager = host.std.get(ExportManager);
const bound = Bound.deserialize(edgelessElement.xywh);
const isBlock = isTopLevelBlock(edgelessElement);
const gfx = host.std.get(GfxControllerIdentifier);
return exportManager
.edgelessToCanvas(
options.surfaceRenderer,
bound,
undefined,
gfx,
isBlock ? [edgelessElement] : undefined,
isBlock ? undefined : [edgelessElement],
{ zoom: options.surfaceRenderer.viewport.zoom }
+2 -1
View File
@@ -3645,9 +3645,11 @@ __metadata:
"@toeverything/theme": "npm:^1.1.7"
"@types/lodash.chunk": "npm:^4.2.9"
fractional-indexing: "npm:^3.2.0"
html2canvas: "npm:^1.4.1"
lit: "npm:^3.2.0"
lodash.chunk: "npm:^4.2.0"
nanoid: "npm:^5.0.7"
pdf-lib: "npm:^1.17.1"
vitest: "npm:3.0.5"
yjs: "npm:^13.6.21"
zod: "npm:^3.23.8"
@@ -3958,7 +3960,6 @@ __metadata:
lz-string: "npm:^1.5.0"
minimatch: "npm:^10.0.1"
nanoid: "npm:^5.0.7"
pdf-lib: "npm:^1.17.1"
shiki: "npm:^2.0.0"
simple-xml-to-json: "npm:^1.2.2"
vitest: "npm:3.0.5"