feat(editor): add basic image support in turbo renderer (#11620)

This PR adds basic support for image block:

![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/lEGcysB4lFTEbCwZ8jMv/4785fc76-fe09-4002-b3fb-aafa9cac34bb.png)
This commit is contained in:
doodlewind
2025-04-11 10:16:07 +00:00
parent e73d68cac4
commit e1e5e8fc14
11 changed files with 141 additions and 1 deletions

View File

@@ -7,5 +7,7 @@ export { ImageProxyService } from './image-proxy-service';
export * from './image-service';
export * from './image-spec';
export * from './styles';
export * from './turbo/image-layout-handler';
export * from './turbo/image-painter.worker';
export { addImages, downloadImageBlob, uploadBlobForImage } from './utils';
export { ImageSelection } from '@blocksuite/affine-shared/selection';

View File

@@ -0,0 +1,69 @@
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
import {
BlockLayoutHandlerExtension,
BlockLayoutHandlersIdentifier,
} from '@blocksuite/affine-gfx-turbo-renderer';
import type { Container } from '@blocksuite/global/di';
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { ImageLayout } from './image-painter.worker';
export class ImageLayoutHandlerExtension extends BlockLayoutHandlerExtension<ImageLayout> {
readonly blockType = 'affine:image';
static override setup(di: Container) {
di.addImpl(
BlockLayoutHandlersIdentifier('image'),
ImageLayoutHandlerExtension
);
}
override queryLayout(
model: BlockModel,
host: EditorHost,
viewportRecord: ViewportRecord
): ImageLayout | null {
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
if (!component) return null;
const imageContainer = component.querySelector('.affine-image-container');
if (!imageContainer) return null;
const resizableImg = component.querySelector(
'.resizable-img'
) as HTMLElement;
if (!resizableImg) return null;
const { zoom, viewScale } = viewportRecord;
const rect = resizableImg.getBoundingClientRect();
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
rect.x,
rect.y,
]);
const imageLayout: ImageLayout = {
type: 'affine:image',
blockId: model.id,
rect: {
x: modelX,
y: modelY,
w: rect.width / zoom / viewScale,
h: rect.height / zoom / viewScale,
},
};
return imageLayout;
}
calculateBound(layout: ImageLayout) {
const rect: Rect = layout.rect;
return {
rect,
subRects: [rect],
};
}
}

View File

@@ -0,0 +1,56 @@
import type {
BlockLayout,
BlockLayoutPainter,
} from '@blocksuite/affine-gfx-turbo-renderer';
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
export interface ImageLayout extends BlockLayout {
type: 'affine:image';
rect: {
x: number;
y: number;
w: number;
h: number;
};
}
function isImageLayout(layout: BlockLayout): layout is ImageLayout {
return layout.type === 'affine:image';
}
class ImageLayoutPainter implements BlockLayoutPainter {
paint(
ctx: OffscreenCanvasRenderingContext2D,
layout: BlockLayout,
layoutBaseX: number,
layoutBaseY: number
): void {
if (!isImageLayout(layout)) {
console.warn(
'Expected image layout but received different format:',
layout
);
return;
}
// For now, just paint a white rectangle
const x = layout.rect.x - layoutBaseX;
const y = layout.rect.y - layoutBaseY;
const width = layout.rect.w;
const height = layout.rect.h;
// Draw a white rectangle with border
ctx.fillStyle = 'white';
ctx.fillRect(x, y, width, height);
// Add a border
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, width, height);
}
}
export const ImageLayoutPainterExtension = BlockLayoutPainterExtension(
'affine:image',
ImageLayoutPainter
);