From aa052096c1a7508bbb42917b2f40461b8965401a Mon Sep 17 00:00:00 2001 From: L-Sun Date: Thu, 14 Aug 2025 10:08:36 +0800 Subject: [PATCH] feat(editor): brush and highlighter dom renderer (#13464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### PR Dependency Tree * **PR #13464** 👈 * **PR #13465** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * DOM-based SVG rendering for Brush and Highlighter with zoom, rotation, layering and improved visualization. * **Refactor** * Consolidated renderer exports into a single entry point for simpler integration. * **Chores** * Updated view registrations to include the new DOM renderer extensions. * Improved highlighter sizing consistency based on serialized bounds. * **Revert** * Removed highlighter renderer registration from the shape module. #### PR Dependency Tree * **PR #13464** 👈 * **PR #13465** * **PR #13471** * **PR #13472** * **PR #13473** This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) --- blocksuite/affine/gfx/brush/src/index.ts | 2 +- .../gfx/brush/src/renderer/dom/brush.ts | 72 ++++++++++++++++++ .../gfx/brush/src/renderer/dom/highlighter.ts | 76 +++++++++++++++++++ .../gfx/brush/src/renderer/dom/index.ts | 2 + .../element/brush.ts} | 0 .../src/renderer/element}/highlighter.ts | 0 .../gfx/brush/src/renderer/element/index.ts | 2 + .../affine/gfx/brush/src/renderer/index.ts | 2 + blocksuite/affine/gfx/brush/src/view.ts | 10 ++- .../gfx/shape/src/element-renderer/index.ts | 1 - blocksuite/affine/gfx/shape/src/view.ts | 6 +- .../src/elements/highlighter/highlighter.ts | 2 +- 12 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 blocksuite/affine/gfx/brush/src/renderer/dom/brush.ts create mode 100644 blocksuite/affine/gfx/brush/src/renderer/dom/highlighter.ts create mode 100644 blocksuite/affine/gfx/brush/src/renderer/dom/index.ts rename blocksuite/affine/gfx/brush/src/{element-renderer.ts => renderer/element/brush.ts} (100%) rename blocksuite/affine/gfx/{shape/src/element-renderer => brush/src/renderer/element}/highlighter.ts (100%) create mode 100644 blocksuite/affine/gfx/brush/src/renderer/element/index.ts create mode 100644 blocksuite/affine/gfx/brush/src/renderer/index.ts diff --git a/blocksuite/affine/gfx/brush/src/index.ts b/blocksuite/affine/gfx/brush/src/index.ts index ba4514785..5f8644af7 100644 --- a/blocksuite/affine/gfx/brush/src/index.ts +++ b/blocksuite/affine/gfx/brush/src/index.ts @@ -1,7 +1,7 @@ export * from './adapter'; export * from './brush-tool'; -export * from './element-renderer'; export * from './eraser-tool'; export * from './highlighter-tool'; +export * from './renderer'; export * from './toolbar/configs'; export * from './toolbar/senior-tool'; diff --git a/blocksuite/affine/gfx/brush/src/renderer/dom/brush.ts b/blocksuite/affine/gfx/brush/src/renderer/dom/brush.ts new file mode 100644 index 000000000..88b15ae80 --- /dev/null +++ b/blocksuite/affine/gfx/brush/src/renderer/dom/brush.ts @@ -0,0 +1,72 @@ +import { + DomElementRendererExtension, + type DomRenderer, +} from '@blocksuite/affine-block-surface'; +import type { BrushElementModel } from '@blocksuite/affine-model'; +import { DefaultTheme } from '@blocksuite/affine-model'; + +export const BrushDomRendererExtension = DomElementRendererExtension( + 'brush', + ( + model: BrushElementModel, + domElement: HTMLElement, + renderer: DomRenderer + ) => { + const { zoom } = renderer.viewport; + const [, , w, h] = model.deserializedXYWH; + + // Early return if invalid dimensions + if (w <= 0 || h <= 0) { + return; + } + + // Early return if no commands + if (!model.commands) { + return; + } + + // Clear previous content + domElement.innerHTML = ''; + + // Get color value + const color = renderer.getColorValue(model.color, DefaultTheme.black, true); + + // Create SVG element + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.style.position = 'absolute'; + svg.style.left = '0'; + svg.style.top = '0'; + svg.style.width = `${w * zoom}px`; + svg.style.height = `${h * zoom}px`; + svg.style.overflow = 'visible'; + svg.style.pointerEvents = 'none'; + svg.setAttribute('viewBox', `0 0 ${w} ${h}`); + + // Apply rotation transform + if (model.rotate !== 0) { + svg.style.transform = `rotate(${model.rotate}deg)`; + svg.style.transformOrigin = 'center'; + } + + // Create path element for the brush stroke + const pathElement = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ); + pathElement.setAttribute('d', model.commands); + pathElement.setAttribute('fill', color); + pathElement.setAttribute('stroke', 'none'); + + svg.append(pathElement); + domElement.replaceChildren(svg); + + // Set element size and position + domElement.style.width = `${w * zoom}px`; + domElement.style.height = `${h * zoom}px`; + domElement.style.overflow = 'visible'; + domElement.style.pointerEvents = 'none'; + + // Set z-index for layering + domElement.style.zIndex = renderer.layerManager.getZIndex(model).toString(); + } +); diff --git a/blocksuite/affine/gfx/brush/src/renderer/dom/highlighter.ts b/blocksuite/affine/gfx/brush/src/renderer/dom/highlighter.ts new file mode 100644 index 000000000..aa9583b03 --- /dev/null +++ b/blocksuite/affine/gfx/brush/src/renderer/dom/highlighter.ts @@ -0,0 +1,76 @@ +import { + DomElementRendererExtension, + type DomRenderer, +} from '@blocksuite/affine-block-surface'; +import type { HighlighterElementModel } from '@blocksuite/affine-model'; +import { DefaultTheme } from '@blocksuite/affine-model'; + +export const HighlighterDomRendererExtension = DomElementRendererExtension( + 'highlighter', + ( + model: HighlighterElementModel, + domElement: HTMLElement, + renderer: DomRenderer + ) => { + const { zoom } = renderer.viewport; + const [, , w, h] = model.deserializedXYWH; + + // Early return if invalid dimensions + if (w <= 0 || h <= 0) { + return; + } + + // Early return if no commands + if (!model.commands) { + return; + } + + // Clear previous content + domElement.innerHTML = ''; + + // Get color value + const color = renderer.getColorValue( + model.color, + DefaultTheme.hightlighterColor, + true + ); + + // Create SVG element + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.style.position = 'absolute'; + svg.style.left = '0'; + svg.style.top = '0'; + svg.style.width = `${w * zoom}px`; + svg.style.height = `${h * zoom}px`; + svg.style.overflow = 'visible'; + svg.style.pointerEvents = 'none'; + svg.setAttribute('viewBox', `0 0 ${w} ${h}`); + + // Apply rotation transform + if (model.rotate !== 0) { + svg.style.transform = `rotate(${model.rotate}deg)`; + svg.style.transformOrigin = 'center'; + } + + // Create path element for the highlighter stroke + const pathElement = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ); + pathElement.setAttribute('d', model.commands); + pathElement.setAttribute('fill', color); + pathElement.setAttribute('stroke', 'none'); + + svg.append(pathElement); + domElement.replaceChildren(svg); + + // Set element size and position + domElement.style.width = `${w * zoom}px`; + domElement.style.height = `${h * zoom}px`; + domElement.style.overflow = 'visible'; + domElement.style.pointerEvents = 'none'; + + // Set z-index for layering + domElement.style.zIndex = renderer.layerManager.getZIndex(model).toString(); + } +); diff --git a/blocksuite/affine/gfx/brush/src/renderer/dom/index.ts b/blocksuite/affine/gfx/brush/src/renderer/dom/index.ts new file mode 100644 index 000000000..c1ad53858 --- /dev/null +++ b/blocksuite/affine/gfx/brush/src/renderer/dom/index.ts @@ -0,0 +1,2 @@ +export { BrushDomRendererExtension } from './brush'; +export { HighlighterDomRendererExtension } from './highlighter'; diff --git a/blocksuite/affine/gfx/brush/src/element-renderer.ts b/blocksuite/affine/gfx/brush/src/renderer/element/brush.ts similarity index 100% rename from blocksuite/affine/gfx/brush/src/element-renderer.ts rename to blocksuite/affine/gfx/brush/src/renderer/element/brush.ts diff --git a/blocksuite/affine/gfx/shape/src/element-renderer/highlighter.ts b/blocksuite/affine/gfx/brush/src/renderer/element/highlighter.ts similarity index 100% rename from blocksuite/affine/gfx/shape/src/element-renderer/highlighter.ts rename to blocksuite/affine/gfx/brush/src/renderer/element/highlighter.ts diff --git a/blocksuite/affine/gfx/brush/src/renderer/element/index.ts b/blocksuite/affine/gfx/brush/src/renderer/element/index.ts new file mode 100644 index 000000000..4cd27f5e0 --- /dev/null +++ b/blocksuite/affine/gfx/brush/src/renderer/element/index.ts @@ -0,0 +1,2 @@ +export { BrushElementRendererExtension } from './brush'; +export { HighlighterElementRendererExtension } from './highlighter'; diff --git a/blocksuite/affine/gfx/brush/src/renderer/index.ts b/blocksuite/affine/gfx/brush/src/renderer/index.ts new file mode 100644 index 000000000..ff5f9b83f --- /dev/null +++ b/blocksuite/affine/gfx/brush/src/renderer/index.ts @@ -0,0 +1,2 @@ +export * from './dom'; +export * from './element'; diff --git a/blocksuite/affine/gfx/brush/src/view.ts b/blocksuite/affine/gfx/brush/src/view.ts index e0c61c5ee..2d81abd73 100644 --- a/blocksuite/affine/gfx/brush/src/view.ts +++ b/blocksuite/affine/gfx/brush/src/view.ts @@ -5,9 +5,14 @@ import { import { BrushTool } from './brush-tool'; import { effects } from './effects'; -import { BrushElementRendererExtension } from './element-renderer'; import { EraserTool } from './eraser-tool'; import { HighlighterTool } from './highlighter-tool'; +import { + BrushDomRendererExtension, + BrushElementRendererExtension, + HighlighterDomRendererExtension, + HighlighterElementRendererExtension, +} from './renderer'; import { brushToolbarExtension, highlighterToolbarExtension, @@ -30,6 +35,9 @@ export class BrushViewExtension extends ViewExtensionProvider { context.register(HighlighterTool); context.register(BrushElementRendererExtension); + context.register(BrushDomRendererExtension); + context.register(HighlighterElementRendererExtension); + context.register(HighlighterDomRendererExtension); context.register(brushToolbarExtension); context.register(highlighterToolbarExtension); diff --git a/blocksuite/affine/gfx/shape/src/element-renderer/index.ts b/blocksuite/affine/gfx/shape/src/element-renderer/index.ts index f2bd9c395..03f4044b0 100644 --- a/blocksuite/affine/gfx/shape/src/element-renderer/index.ts +++ b/blocksuite/affine/gfx/shape/src/element-renderer/index.ts @@ -1,2 +1 @@ -export * from './highlighter'; export * from './shape'; diff --git a/blocksuite/affine/gfx/shape/src/view.ts b/blocksuite/affine/gfx/shape/src/view.ts index 37d2da932..12b562d75 100644 --- a/blocksuite/affine/gfx/shape/src/view.ts +++ b/blocksuite/affine/gfx/shape/src/view.ts @@ -4,10 +4,7 @@ import { } from '@blocksuite/affine-ext-loader'; import { effects } from './effects'; -import { - HighlighterElementRendererExtension, - ShapeElementRendererExtension, -} from './element-renderer'; +import { ShapeElementRendererExtension } from './element-renderer'; import { ShapeDomRendererExtension } from './element-renderer/shape-dom'; import { ShapeElementView, ShapeViewInteraction } from './element-view'; import { ShapeTool } from './shape-tool'; @@ -24,7 +21,6 @@ export class ShapeViewExtension extends ViewExtensionProvider { override setup(context: ViewExtensionContext) { super.setup(context); if (this.isEdgeless(context.scope)) { - context.register(HighlighterElementRendererExtension); context.register(ShapeElementRendererExtension); context.register(ShapeDomRendererExtension); context.register(ShapeElementView); diff --git a/blocksuite/affine/model/src/elements/highlighter/highlighter.ts b/blocksuite/affine/model/src/elements/highlighter/highlighter.ts index 258093d85..9d446086a 100644 --- a/blocksuite/affine/model/src/elements/highlighter/highlighter.ts +++ b/blocksuite/affine/model/src/elements/highlighter/highlighter.ts @@ -131,7 +131,7 @@ export class HighlighterElementModel extends GfxPrimitiveElementModel { - const oldBound = instance.elementBound; + const oldBound = Bound.fromXYWH(instance.deserializedXYWH); if ( lineWidth === instance.lineWidth ||