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

View File

@@ -1,594 +0,0 @@
import {
type CanvasRenderer,
SurfaceElementModel,
} from '@blocksuite/affine-block-surface';
import {
FrameBlockModel,
GroupElementModel,
ImageBlockModel,
type RootBlockModel,
} from '@blocksuite/affine-model';
import { FetchUtils } from '@blocksuite/affine-shared/adapters';
import {
CANVAS_EXPORT_IGNORE_TAGS,
DEFAULT_IMAGE_PROXY_ENDPOINT,
} from '@blocksuite/affine-shared/consts';
import {
isInsidePageEditor,
matchModels,
} from '@blocksuite/affine-shared/utils';
import {
type BlockStdScope,
type EditorHost,
StdIdentifier,
} from '@blocksuite/block-std';
import type {
GfxBlockElementModel,
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 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 { FileExporter } from './file-exporter.js';
// oxlint-disable-next-line typescript/consistent-type-imports
type Html2CanvasFunction = typeof import('html2canvas').default;
export type ExportOptions = {
imageProxyEndpoint: string;
};
export class ExportManager {
private readonly _exportOptions: ExportOptions = {
imageProxyEndpoint: DEFAULT_IMAGE_PROXY_ENDPOINT,
};
private readonly _replaceRichTextWithSvgElement = (element: HTMLElement) => {
const richList = Array.from(element.querySelectorAll('.inline-editor'));
richList.forEach(rich => {
const svgEle = this._elementToSvgElement(
rich.cloneNode(true) as HTMLElement,
rich.clientWidth,
rich.clientHeight + 1
);
rich.parentElement?.append(svgEle);
rich.remove();
});
};
replaceImgSrcWithSvg = async (element: HTMLElement) => {
const imgList = Array.from(element.querySelectorAll('img'));
// Create an array of promises
const promises = imgList.map(img => {
return FetchUtils.fetchImage(
img.src,
undefined,
this._exportOptions.imageProxyEndpoint
)
.then(response => response && response.blob())
.then(async blob => {
if (!blob) return;
// If the file type is SVG, set svg width and height
if (blob.type === 'image/svg+xml') {
// Parse the SVG
const parser = new DOMParser();
const svgDoc = parser.parseFromString(
await blob.text(),
'image/svg+xml'
);
const svgElement =
svgDoc.documentElement as unknown as SVGSVGElement;
// Check if the SVG has width and height attributes
if (
!svgElement.hasAttribute('width') &&
!svgElement.hasAttribute('height')
) {
// Get the viewBox
const viewBox = svgElement.viewBox.baseVal;
// Set the SVG width and height
svgElement.setAttribute('width', `${viewBox.width}px`);
svgElement.setAttribute('height', `${viewBox.height}px`);
}
// Replace the img src with the modified SVG
const serializer = new XMLSerializer();
const newSvgStr = serializer.serializeToString(svgElement);
img.src =
'data:image/svg+xml;charset=utf-8,' +
encodeURIComponent(newSvgStr);
}
});
});
// Wait for all promises to resolve
await Promise.all(promises);
};
get doc(): Store {
return this.std.store;
}
get editorHost(): EditorHost {
return this.std.host;
}
constructor(readonly std: BlockStdScope) {}
private _checkCanContinueToCanvas(pathName: string, editorMode: boolean) {
if (
location.pathname !== pathName ||
isInsidePageEditor(this.editorHost) !== editorMode
) {
throw new BlockSuiteError(
ErrorCode.EdgelessExportError,
'Unable to export content to canvas'
);
}
}
private async _checkReady() {
const pathname = location.pathname;
const editorMode = isInsidePageEditor(this.editorHost);
const promise = new Promise((resolve, reject) => {
let count = 0;
const checkReactRender = setInterval(() => {
try {
this._checkCanContinueToCanvas(pathname, editorMode);
} catch (e) {
clearInterval(checkReactRender);
reject(e);
}
const rootModel = this.doc.root;
const rootComponent = this.doc.root
? getBlockComponentByModel(this.editorHost, rootModel)
: null;
const imageCard = rootComponent?.querySelector(
'affine-image-fallback-card'
);
const isReady =
!imageCard || imageCard.getAttribute('imageState') === '0';
if (rootComponent && isReady) {
clearInterval(checkReactRender);
resolve(true);
}
count++;
if (count > 10 * 60) {
clearInterval(checkReactRender);
resolve(false);
}
}, 100);
});
return promise;
}
private _createCanvas(bound: IBound, fillStyle: string) {
const canvas = document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
canvas.width = (bound.w + 100) * dpr;
canvas.height = (bound.h + 100) * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = fillStyle;
ctx.fillRect(0, 0, canvas.width, canvas.height);
return { canvas, ctx };
}
private _disableMediaPrint() {
document.querySelectorAll('.media-print').forEach(mediaPrint => {
mediaPrint.classList.add('hide');
});
}
private async _docToCanvas(): Promise<HTMLCanvasElement | void> {
const html2canvas = (await import('html2canvas')).default;
if (!(html2canvas instanceof Function)) return;
const pathname = location.pathname;
const editorMode = isInsidePageEditor(this.editorHost);
const rootComponent = getRootByEditorHost(this.editorHost);
if (!rootComponent) return;
const viewportElement = rootComponent.viewportElement;
if (!viewportElement) return;
const pageContainer = viewportElement.querySelector(
'.affine-page-root-block-container'
);
const rect = pageContainer?.getBoundingClientRect();
const { viewport } = rootComponent;
if (!viewport) return;
const pageWidth = rect?.width;
const pageLeft = rect?.left ?? 0;
const viewportHeight = viewportElement?.scrollHeight;
const html2canvasOption = {
ignoreElements: function (element: Element) {
if (
CANVAS_EXPORT_IGNORE_TAGS.includes(element.tagName) ||
element.classList.contains('dg')
) {
return true;
} else if (
(element.classList.contains('close') &&
element.parentElement?.classList.contains(
'meta-data-expanded-title'
)) ||
(element.classList.contains('expand') &&
element.parentElement?.classList.contains('meta-data'))
) {
// the close and expand buttons in affine-doc-meta-data is not needed to be showed
return true;
} else {
return false;
}
},
onclone: async (_documentClone: Document, element: HTMLElement) => {
element.style.height = `${viewportHeight}px`;
this._replaceRichTextWithSvgElement(element);
await this.replaceImgSrcWithSvg(element);
},
backgroundColor: window.getComputedStyle(viewportElement).backgroundColor,
x: pageLeft - viewport.left,
width: pageWidth,
height: viewportHeight,
useCORS: this._exportOptions.imageProxyEndpoint ? false : true,
proxy: this._exportOptions.imageProxyEndpoint,
};
let data: HTMLCanvasElement;
try {
this._enableMediaPrint();
data = await html2canvas(
viewportElement as HTMLElement,
html2canvasOption
);
} finally {
this._disableMediaPrint();
}
this._checkCanContinueToCanvas(pathname, editorMode);
return data;
}
private _drawEdgelessBackground(
ctx: CanvasRenderingContext2D,
{
size,
backgroundColor,
gridColor,
}: {
size: number;
backgroundColor: string;
gridColor: string;
}
) {
const svgImg = `<svg width='${ctx.canvas.width}px' height='${ctx.canvas.height}px' xmlns='http://www.w3.org/2000/svg' style='background-size:${size}px ${size}px;background-color:${backgroundColor}; background-image: radial-gradient(${gridColor} 1px, ${backgroundColor} 1px)'></svg>`;
const img = new Image();
const cleanup = () => {
img.onload = null;
img.onerror = null;
};
return new Promise<void>((resolve, reject) => {
img.onload = () => {
cleanup();
ctx.drawImage(img, 0, 0);
resolve();
};
img.onerror = e => {
cleanup();
reject(e);
};
img.src = `data:image/svg+xml,${encodeURIComponent(svgImg)}`;
});
}
private _elementToSvgElement(
node: HTMLElement,
width: number,
height: number
) {
const xmlns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(xmlns, 'svg');
const foreignObject = document.createElementNS(xmlns, 'foreignObject');
svg.setAttribute('width', `${width}`);
svg.setAttribute('height', `${height}`);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
foreignObject.setAttribute('width', '100%');
foreignObject.setAttribute('height', '100%');
foreignObject.setAttribute('x', '0');
foreignObject.setAttribute('y', '0');
foreignObject.setAttribute('externalResourcesRequired', 'true');
svg.append(foreignObject);
foreignObject.append(node);
return svg;
}
private _enableMediaPrint() {
document.querySelectorAll('.media-print').forEach(mediaPrint => {
mediaPrint.classList.remove('hide');
});
}
private async _html2canvas(
htmlElement: HTMLElement,
options: Parameters<Html2CanvasFunction>[1] = {}
) {
const html2canvas = (await import('html2canvas'))
.default as unknown as Html2CanvasFunction;
const html2canvasOption = {
ignoreElements: function (element: Element) {
if (
CANVAS_EXPORT_IGNORE_TAGS.includes(element.tagName) ||
element.classList.contains('dg')
) {
return true;
} else {
return false;
}
},
onclone: async (documentClone: Document, element: HTMLElement) => {
// html2canvas can't support transform feature
element.style.setProperty('transform', 'none');
const layer = element.classList.contains('.affine-edgeless-layer')
? element
: null;
if (layer instanceof HTMLElement) {
layer.style.setProperty('transform', 'none');
}
const boxShadowEles = documentClone.querySelectorAll(
"[style*='box-shadow']"
);
boxShadowEles.forEach(function (element) {
if (element instanceof HTMLElement) {
element.style.setProperty('box-shadow', 'none');
}
});
this._replaceRichTextWithSvgElement(element);
await this.replaceImgSrcWithSvg(element);
},
useCORS: this._exportOptions.imageProxyEndpoint ? false : true,
proxy: this._exportOptions.imageProxyEndpoint,
};
let data: HTMLCanvasElement;
try {
this._enableMediaPrint();
data = await html2canvas(
htmlElement,
Object.assign(html2canvasOption, options)
);
} finally {
this._disableMediaPrint();
}
return data;
}
private async _toCanvas(): Promise<HTMLCanvasElement | void> {
try {
await this._checkReady();
} catch (e: unknown) {
console.error('Failed to export to canvas');
console.error(e);
return;
}
if (isInsidePageEditor(this.editorHost)) {
return this._docToCanvas();
} else {
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);
}
}
// TODO: refactor of this part
async edgelessToCanvas(
surfaceRenderer: CanvasRenderer,
bound: IBound,
edgeless?: EdgelessRootBlockComponent,
nodes?: GfxBlockElementModel[],
surfaces?: GfxPrimitiveElementModel[],
edgelessBackground?: {
zoom: number;
}
): Promise<HTMLCanvasElement | undefined> {
const rootModel = this.doc.root;
if (!rootModel) return;
const pathname = location.pathname;
const editorMode = isInsidePageEditor(this.editorHost);
const rootComponent = getRootByEditorHost(this.editorHost);
if (!rootComponent) return;
const viewportElement = rootComponent.viewportElement;
if (!viewportElement) return;
const containerComputedStyle = window.getComputedStyle(viewportElement);
const html2canvas = (element: HTMLElement) =>
this._html2canvas(element, {
backgroundColor: containerComputedStyle.backgroundColor,
});
const container = rootComponent.querySelector(
'.affine-block-children-container'
);
if (!container) return;
const { ctx, canvas } = this._createCanvas(
bound,
window.getComputedStyle(container).backgroundColor
);
if (edgelessBackground) {
await this._drawEdgelessBackground(ctx, {
backgroundColor: containerComputedStyle.getPropertyValue(
'--affine-background-primary-color'
),
size: getBackgroundGrid(edgelessBackground.zoom, true).gap,
gridColor: containerComputedStyle.getPropertyValue(
'--affine-edgeless-grid-color'
),
});
}
const blocks =
nodes ??
edgeless?.service.gfx.getElementsByBound(bound, { type: 'block' }) ??
[];
for (const block of blocks) {
if (matchModels(block, [ImageBlockModel])) {
if (!block.sourceId) return;
const blob = await block.doc.blobSync.get(block.sourceId);
if (!blob) return;
const blobToImage = (blob: Blob) =>
new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = URL.createObjectURL(blob);
});
const blockBound = xywhArrayToObject(block);
ctx.drawImage(
await blobToImage(blob),
blockBound.x - bound.x,
blockBound.y - bound.y,
blockBound.w,
blockBound.h
);
}
const blockComponent = this.editorHost.view.getBlock(block.id);
if (blockComponent) {
const blockBound = xywhArrayToObject(block);
const canvasData = await this._html2canvas(
blockComponent as HTMLElement
);
ctx.drawImage(
canvasData,
blockBound.x - bound.x + 50,
blockBound.y - bound.y + 50,
blockBound.w,
blockBound.h
);
}
if (matchModels(block, [FrameBlockModel])) {
// TODO(@L-Sun): use children of frame instead of bound
const blocksInsideFrame = getBlocksInFrameBound(this.doc, block, false);
const frameBound = Bound.deserialize(block.xywh);
// eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < blocksInsideFrame.length; i++) {
const element = blocksInsideFrame[i];
const htmlElement = this.editorHost.view.getBlock(block.id);
const blockBound = xywhArrayToObject(element);
const canvasData = await html2canvas(htmlElement as HTMLElement);
ctx.drawImage(
canvasData,
blockBound.x - bound.x + 50,
blockBound.y - bound.y + 50,
blockBound.w,
(blockBound.w / canvasData.width) * canvasData.height
);
}
const surfaceCanvas = surfaceRenderer.getCanvasByBound(frameBound);
ctx.drawImage(surfaceCanvas, 50, 50, frameBound.w, frameBound.h);
}
this._checkCanContinueToCanvas(pathname, editorMode);
}
if (surfaces?.length) {
const surfaceElements = surfaces.flatMap(element =>
element instanceof GroupElementModel
? (element.childElements.filter(
el => el instanceof SurfaceElementModel
) as SurfaceElementModel[])
: element
);
const surfaceCanvas = surfaceRenderer.getCanvasByBound(
bound,
surfaceElements
);
ctx.drawImage(surfaceCanvas, 50, 50, bound.w, bound.h);
}
return canvas;
}
async exportPdf() {
const rootModel = this.doc.root;
if (!rootModel) return;
const canvasImage = await this._toCanvas();
if (!canvasImage) {
return;
}
const PDFLib = await import('pdf-lib');
const pdfDoc = await PDFLib.PDFDocument.create();
const page = pdfDoc.addPage([canvasImage.width, canvasImage.height]);
const imageEmbed = await pdfDoc.embedPng(canvasImage.toDataURL('PNG'));
const { width, height } = imageEmbed.scale(1);
page.drawImage(imageEmbed, {
x: 0,
y: 0,
width,
height,
});
const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true });
FileExporter.exportFile(
(rootModel as RootBlockModel).title.toString() + '.pdf',
pdfBase64
);
}
async exportPng() {
const rootModel = this.doc.root;
if (!rootModel) return;
const canvasImage = await this._toCanvas();
if (!canvasImage) {
return;
}
FileExporter.exportPng(
(this.doc.root as RootBlockModel).title.toString(),
canvasImage.toDataURL('image/png')
);
}
}
export const ExportManagerExtension: ExtensionType = {
setup: di => {
di.add(ExportManager, [StdIdentifier]);
},
};

View File

@@ -1,81 +0,0 @@
/* eslint-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';
/** Tools for exporting files to device. For example, via browser download. */
export const FileExporter = {
/**
* Create a download for the user's browser.
*
* @param filename
* @param text
* @param mimeType like `"text/plain"`, `"text/html"`, `"application/javascript"`, etc. See {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types mdn docs List of MIME types}.
*
* @remarks
* Only accepts data in utf-8 encoding (html files, javascript source, text files, etc).
*
* @example
* const todoMDText = `# Todo items
* [ ] Item 1
* [ ] Item 2
* `
* FileExporter.exportFile("Todo list.md", todoMDText, "text/plain")
*
* @example
* const stateJsonContent = JSON.stringify({ a: 1, b: 2, c: 3 })
* FileExporter.exportFile("state.json", jsonContent, "application/json")
*/
exportFile(filename: string, dataURL: string) {
const element = document.createElement('a');
element.setAttribute('href', dataURL);
const safeFilename = getSafeFileName(filename);
element.setAttribute('download', safeFilename);
element.style.display = 'none';
document.body.append(element);
element.click();
element.remove();
},
exportPng(docTitle: string | undefined, dataURL: string) {
const title = docTitle?.trim() || UNTITLED_PAGE_NAME;
FileExporter.exportFile(title + '.png', dataURL);
},
};
function getSafeFileName(string: string) {
const replacement = ' ';
const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g;
const windowsReservedNameRegex = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
const reControlChars = /[\u0000-\u001F\u0080-\u009F]/g;
const reTrailingPeriods = /\.+$/;
const allowedLength = 50;
function trimRepeated(string: string, target: string) {
const escapeStringRegexp = target
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
.replace(/-/g, '\\x2d');
const regex = new RegExp(`(?:${escapeStringRegexp}){2,}`, 'g');
return string.replace(regex, target);
}
string = string
.normalize('NFD')
.replace(filenameReservedRegex, replacement)
.replace(reControlChars, replacement)
.replace(reTrailingPeriods, '');
string = trimRepeated(string, replacement);
string = windowsReservedNameRegex.test(string)
? string + replacement
: string;
const extIndex = string.lastIndexOf('.');
const filename = string.slice(0, extIndex).trim();
const extension = string.slice(extIndex);
string =
filename.slice(0, Math.max(1, allowedLength - extension.length)) +
extension;
return string;
}

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';

View File

@@ -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,

View File

@@ -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';

View File

@@ -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(

View File

@@ -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',

View File

@@ -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[]
);
}

View File

@@ -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 };
}

View File

@@ -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;

View File

@@ -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 }