From 8109c718c726cdfa79c497d4f77e9ab391c881bc Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Fri, 21 Mar 2025 06:13:11 +0000 Subject: [PATCH] feat(editor): gfx shape package (#11060) --- blocksuite/affine/all/package.json | 2 + blocksuite/affine/all/src/gfx/shape.ts | 1 + blocksuite/affine/all/tsconfig.json | 1 + .../affine/blocks/block-root/package.json | 1 + .../auto-complete/auto-complete-panel.ts | 12 +- .../auto-complete/edgeless-auto-complete.ts | 2 +- .../components/auto-complete/utils.ts | 2 +- .../edgeless/components/panel/shape-panel.ts | 7 +- .../toolbar/shape/shape-draggable.ts | 8 +- .../toolbar/shape/shape-tool-button.ts | 2 +- .../components/toolbar/shape/utils.ts | 3 +- .../src/edgeless/configs/toolbar/shape.ts | 8 +- .../src/edgeless/edgeless-builtin-spec.ts | 2 +- .../src/edgeless/edgeless-keyboard.ts | 7 +- .../src/edgeless/edgeless-root-block.ts | 2 +- .../src/edgeless/gfx-tool/default-tool.ts | 2 +- .../block-root/src/edgeless/gfx-tool/index.ts | 1 - .../src/edgeless/gfx-tool/note-tool.ts | 6 +- .../block-root/src/edgeless/utils/consts.ts | 26 -- .../src/edgeless/utils/hotkey-utils.ts | 3 +- .../block-root/src/edgeless/utils/text.ts | 45 --- .../src/edgeless/utils/tool-overlay.ts | 326 +----------------- .../affine/blocks/block-root/src/effects.ts | 16 +- .../affine/blocks/block-root/tsconfig.json | 1 + .../affine/blocks/block-surface/src/consts.ts | 6 + .../affine/blocks/block-surface/src/index.ts | 3 +- .../src/renderer/tool-overlay.ts | 42 +++ blocksuite/affine/gfx/shape/package.json | 44 +++ blocksuite/affine/gfx/shape/src/consts.ts | 19 + .../affine/gfx/shape/src/draggable/index.ts | 2 + .../shape/src/draggable}/shape-menu.ts | 10 +- .../src/draggable}/shape-tool-element.ts | 22 +- blocksuite/affine/gfx/shape/src/effects.ts | 19 + blocksuite/affine/gfx/shape/src/index.ts | 6 + .../affine/gfx/shape/src/overlay/diamond.ts | 23 ++ .../affine/gfx/shape/src/overlay/ellipse.ts | 15 + .../affine/gfx/shape/src/overlay/factory.ts | 34 ++ .../affine/gfx/shape/src/overlay/index.ts | 3 + .../affine/gfx/shape/src/overlay/rect.ts | 15 + .../gfx/shape/src/overlay/rounded-rect.ts | 32 ++ .../gfx/shape/src/overlay/shape-overlay.ts | 109 ++++++ .../affine/gfx/shape/src/overlay/shape.ts | 27 ++ .../affine/gfx/shape/src/overlay/triangle.ts | 22 ++ .../affine/gfx/shape/src/overlay/utils.ts | 56 +++ .../gfx-tool => gfx/shape/src}/shape-tool.ts | 8 +- .../src}/text/edgeless-shape-text-editor.ts | 0 blocksuite/affine/gfx/shape/src/text/index.ts | 1 + blocksuite/affine/gfx/shape/src/text/text.ts | 52 +++ .../shape => gfx/shape/src/toolbar}/icons.ts | 0 .../affine/gfx/shape/src/toolbar/index.ts | 2 + .../shape/src/toolbar}/shape-menu-config.ts | 2 +- blocksuite/affine/gfx/shape/tsconfig.json | 19 + tools/utils/src/workspace.gen.ts | 17 + tsconfig.json | 1 + yarn.lock | 28 ++ 55 files changed, 667 insertions(+), 458 deletions(-) create mode 100644 blocksuite/affine/all/src/gfx/shape.ts create mode 100644 blocksuite/affine/blocks/block-surface/src/renderer/tool-overlay.ts create mode 100644 blocksuite/affine/gfx/shape/package.json create mode 100644 blocksuite/affine/gfx/shape/src/consts.ts create mode 100644 blocksuite/affine/gfx/shape/src/draggable/index.ts rename blocksuite/affine/{blocks/block-root/src/edgeless/components/toolbar/shape => gfx/shape/src/draggable}/shape-menu.ts (94%) rename blocksuite/affine/{blocks/block-root/src/edgeless/components/toolbar/shape => gfx/shape/src/draggable}/shape-tool-element.ts (93%) create mode 100644 blocksuite/affine/gfx/shape/src/effects.ts create mode 100644 blocksuite/affine/gfx/shape/src/index.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/diamond.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/ellipse.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/factory.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/index.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/rect.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/rounded-rect.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/shape-overlay.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/shape.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/triangle.ts create mode 100644 blocksuite/affine/gfx/shape/src/overlay/utils.ts rename blocksuite/affine/{blocks/block-root/src/edgeless/gfx-tool => gfx/shape/src}/shape-tool.ts (97%) rename blocksuite/affine/{blocks/block-root/src/edgeless/components => gfx/shape/src}/text/edgeless-shape-text-editor.ts (100%) create mode 100644 blocksuite/affine/gfx/shape/src/text/index.ts create mode 100644 blocksuite/affine/gfx/shape/src/text/text.ts rename blocksuite/affine/{blocks/block-root/src/edgeless/components/toolbar/shape => gfx/shape/src/toolbar}/icons.ts (100%) create mode 100644 blocksuite/affine/gfx/shape/src/toolbar/index.ts rename blocksuite/affine/{blocks/block-root/src/edgeless/components/toolbar/shape => gfx/shape/src/toolbar}/shape-menu-config.ts (95%) create mode 100644 blocksuite/affine/gfx/shape/tsconfig.json diff --git a/blocksuite/affine/all/package.json b/blocksuite/affine/all/package.json index 5ac93e0d7a..14a85082fc 100644 --- a/blocksuite/affine/all/package.json +++ b/blocksuite/affine/all/package.json @@ -33,6 +33,7 @@ "@blocksuite/affine-fragment-doc-title": "workspace:*", "@blocksuite/affine-fragment-frame-panel": "workspace:*", "@blocksuite/affine-fragment-outline": "workspace:*", + "@blocksuite/affine-gfx-shape": "workspace:*", "@blocksuite/affine-gfx-text": "workspace:*", "@blocksuite/affine-gfx-turbo-renderer": "workspace:*", "@blocksuite/affine-inline-footnote": "workspace:*", @@ -112,6 +113,7 @@ "./fragments/frame-panel": "./src/fragments/frame-panel.ts", "./fragments/outline": "./src/fragments/outline.ts", "./gfx/text": "./src/gfx/text.ts", + "./gfx/shape": "./src/gfx/shape/index.ts", "./gfx/turbo-renderer": "./src/gfx/turbo-renderer.ts", "./components/block-selection": "./src/components/block-selection.ts", "./components/block-zero-width": "./src/components/block-zero-width.ts", diff --git a/blocksuite/affine/all/src/gfx/shape.ts b/blocksuite/affine/all/src/gfx/shape.ts new file mode 100644 index 0000000000..27ac482c4f --- /dev/null +++ b/blocksuite/affine/all/src/gfx/shape.ts @@ -0,0 +1 @@ +export * from '@blocksuite/affine-gfx-shape'; diff --git a/blocksuite/affine/all/tsconfig.json b/blocksuite/affine/all/tsconfig.json index 531467f280..6dfcef990d 100644 --- a/blocksuite/affine/all/tsconfig.json +++ b/blocksuite/affine/all/tsconfig.json @@ -30,6 +30,7 @@ { "path": "../fragments/fragment-doc-title" }, { "path": "../fragments/fragment-frame-panel" }, { "path": "../fragments/fragment-outline" }, + { "path": "../gfx/shape" }, { "path": "../gfx/text" }, { "path": "../gfx/turbo-renderer" }, { "path": "../inlines/footnote" }, diff --git a/blocksuite/affine/blocks/block-root/package.json b/blocksuite/affine/blocks/block-root/package.json index 377934d028..4f6fd925eb 100644 --- a/blocksuite/affine/blocks/block-root/package.json +++ b/blocksuite/affine/blocks/block-root/package.json @@ -27,6 +27,7 @@ "@blocksuite/affine-block-table": "workspace:*", "@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-fragment-doc-title": "workspace:*", + "@blocksuite/affine-gfx-shape": "workspace:*", "@blocksuite/affine-gfx-text": "workspace:*", "@blocksuite/affine-inline-latex": "workspace:*", "@blocksuite/affine-inline-link": "workspace:*", diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/auto-complete-panel.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/auto-complete-panel.ts index 14e8da6ca1..303528f117 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/auto-complete-panel.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/auto-complete-panel.ts @@ -4,6 +4,12 @@ import { EdgelessCRUDIdentifier, } from '@blocksuite/affine-block-surface'; import { FontFamilyIcon } from '@blocksuite/affine-components/icons'; +import { + mountShapeTextEditor, + SHAPE_OVERLAY_HEIGHT, + SHAPE_OVERLAY_WIDTH, + ShapeComponentConfig, +} from '@blocksuite/affine-gfx-shape'; import { mountTextElementEditor } from '@blocksuite/affine-gfx-text'; import type { Connection, @@ -53,12 +59,6 @@ import { styleMap } from 'lit/directives/style-map.js'; import * as Y from 'yjs'; import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; -import { - SHAPE_OVERLAY_HEIGHT, - SHAPE_OVERLAY_WIDTH, -} from '../../utils/consts.js'; -import { mountShapeTextEditor } from '../../utils/text.js'; -import { ShapeComponentConfig } from '../toolbar/shape/shape-menu-config.js'; import { type AUTO_COMPLETE_TARGET_TYPE, AutoCompleteFrameOverlay, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/edgeless-auto-complete.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/edgeless-auto-complete.ts index b677fa385c..e7c280a1f3 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/edgeless-auto-complete.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/edgeless-auto-complete.ts @@ -8,6 +8,7 @@ import { OverlayIdentifier, type RoughCanvas, } from '@blocksuite/affine-block-surface'; +import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape'; import type { Connection, ConnectorElementModel, @@ -42,7 +43,6 @@ import { classMap } from 'lit/directives/class-map.js'; import { styleMap } from 'lit/directives/style-map.js'; import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; -import { mountShapeTextEditor } from '../../utils/text.js'; import type { SelectedRect } from '../rects/edgeless-selected-rect.js'; import { EdgelessAutoCompletePanel } from './auto-complete-panel.js'; import { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/utils.ts index e3801ad6cc..62fc7c2090 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/utils.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/auto-complete/utils.ts @@ -3,6 +3,7 @@ import { Overlay, type RoughCanvas, } from '@blocksuite/affine-block-surface'; +import { type Shape, ShapeFactory } from '@blocksuite/affine-gfx-shape'; import { type Connection, getShapeRadius, @@ -20,7 +21,6 @@ import { assertType } from '@blocksuite/global/utils'; import * as Y from 'yjs'; import type { EdgelessRootBlockComponent } from '../../edgeless-root-block.js'; -import { type Shape, ShapeFactory } from '../../utils/tool-overlay.js'; export enum Direction { Right, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/shape-panel.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/shape-panel.ts index 62b9d35bfc..dcd3438e31 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/shape-panel.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/panel/shape-panel.ts @@ -1,12 +1,13 @@ +import { + ShapeComponentConfig, + type ShapeTool, +} from '@blocksuite/affine-gfx-shape'; import { ShapeStyle } from '@blocksuite/affine-model'; import { css, html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { Subject } from 'rxjs'; -import type { ShapeTool } from '../../gfx-tool/shape-tool.js'; -import { ShapeComponentConfig } from '../toolbar/shape/shape-menu-config.js'; - export class EdgelessShapePanel extends LitElement { static override styles = css` :host { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-draggable.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-draggable.ts index e967d86ac1..e92f77b1b0 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-draggable.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-draggable.ts @@ -2,6 +2,12 @@ import { CanvasElementType, EdgelessCRUDIdentifier, } from '@blocksuite/affine-block-surface'; +import { + ellipseSvg, + roundedSvg, + ShapeTool, + triangleSvg, +} from '@blocksuite/affine-gfx-shape'; import { getShapeRadius, getShapeType, @@ -20,10 +26,8 @@ import { classMap } from 'lit/directives/class-map.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; -import { ShapeTool } from '../../../gfx-tool/shape-tool.js'; import { EdgelessDraggableElementController } from '../common/draggable/draggable-element.controller.js'; import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js'; -import { ellipseSvg, roundedSvg, triangleSvg } from './icons.js'; import type { DraggableShape } from './utils.js'; import { buildVariablesObject } from './utils.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-button.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-button.ts index 95f7c0926b..e1288848c9 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-button.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-button.ts @@ -1,8 +1,8 @@ +import { ShapeTool } from '@blocksuite/affine-gfx-shape'; import { type ShapeName, ShapeType } from '@blocksuite/affine-model'; import { SignalWatcher } from '@blocksuite/global/lit'; import { css, html, LitElement } from 'lit'; -import { ShapeTool } from '../../../gfx-tool/shape-tool.js'; import { EdgelessToolbarToolMixin } from '../mixins/tool.mixin.js'; import type { DraggableShape } from './utils.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/utils.ts index 57307d4283..5747defc13 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/utils.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/utils.ts @@ -1,7 +1,6 @@ +import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape'; import { render, type TemplateResult } from 'lit'; -import type { ShapeToolOption } from '../../../gfx-tool/shape-tool.js'; - type TransformState = { /** horizental offset base on center */ x?: number | string; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts index 4ac6e3d82c..19f9c78c33 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/configs/toolbar/shape.ts @@ -7,6 +7,11 @@ import { type PickColorEvent, } from '@blocksuite/affine-components/color-picker'; import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel'; +import { + mountShapeTextEditor, + ShapeComponentConfig, + type ShapeToolOption, +} from '@blocksuite/affine-gfx-shape'; import { type Color, DefaultTheme, @@ -34,9 +39,6 @@ import { AddTextIcon, ShapeIcon } from '@blocksuite/icons/lit'; import { html } from 'lit'; import isEqual from 'lodash-es/isEqual'; -import type { ShapeToolOption } from '../..'; -import { ShapeComponentConfig } from '../../components/toolbar/shape/shape-menu-config'; -import { mountShapeTextEditor } from '../../utils/text'; import { LINE_STYLE_LIST } from './consts'; import { createMindmapLayoutActionMenu, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts index b6fb166cb0..be8fbe94c9 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-builtin-spec.ts @@ -4,6 +4,7 @@ import { PresentTool, } from '@blocksuite/affine-block-frame'; import { ConnectionOverlay } from '@blocksuite/affine-block-surface'; +import { ShapeTool } from '@blocksuite/affine-gfx-shape'; import { TextTool } from '@blocksuite/affine-gfx-text'; import { CanvasEventHandler, @@ -25,7 +26,6 @@ import { EraserTool } from './gfx-tool/eraser-tool.js'; import { LassoTool } from './gfx-tool/lasso-tool.js'; import { NoteTool } from './gfx-tool/note-tool.js'; import { PanTool } from './gfx-tool/pan-tool.js'; -import { ShapeTool } from './gfx-tool/shape-tool.js'; import { TemplateTool } from './gfx-tool/template-tool.js'; import { EditPropsMiddlewareBuilder } from './middlewares/base.js'; import { SnapOverlay } from './utils/snap-manager.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-keyboard.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-keyboard.ts index b942db4bb7..1c304e3287 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-keyboard.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-keyboard.ts @@ -2,6 +2,7 @@ import { insertLinkByQuickSearchCommand } from '@blocksuite/affine-block-bookmar import { EdgelessTextBlockComponent } from '@blocksuite/affine-block-edgeless-text'; import { isNoteBlock } from '@blocksuite/affine-block-surface'; import { toast } from '@blocksuite/affine-components/toast'; +import { mountShapeTextEditor, ShapeTool } from '@blocksuite/affine-gfx-shape'; import { ConnectorElementModel, ConnectorMode, @@ -34,7 +35,6 @@ import { Bound, getCommonBound } from '@blocksuite/global/gfx'; import { PageKeyboardManager } from '../keyboard/keyboard-manager.js'; import type { EdgelessRootBlockComponent } from './edgeless-root-block.js'; import { LassoTool } from './gfx-tool/lasso-tool.js'; -import { ShapeTool } from './gfx-tool/shape-tool.js'; import { DEFAULT_NOTE_CHILD_FLAVOUR, DEFAULT_NOTE_CHILD_TYPE, @@ -48,10 +48,7 @@ import { isSingleMindMapNode, } from './utils/mindmap.js'; import { isCanvasElement } from './utils/query.js'; -import { - mountConnectorLabelEditor, - mountShapeTextEditor, -} from './utils/text.js'; +import { mountConnectorLabelEditor } from './utils/text.js'; export class EdgelessPageKeyboardManager extends PageKeyboardManager { get gfx() { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts index 66e024d0d9..2dd4b4815e 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/edgeless-root-block.ts @@ -7,6 +7,7 @@ import { getBgGridGap, normalizeWheelDeltaY, } from '@blocksuite/affine-block-surface'; +import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape'; import { NoteBlockModel, NoteDisplayMode, @@ -52,7 +53,6 @@ import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js'; import type { EdgelessRootService } from './edgeless-root-service.js'; import { isSingleMindMapNode } from './utils/mindmap.js'; import { isCanvasElement } from './utils/query.js'; -import { mountShapeTextEditor } from './utils/text.js'; export class EdgelessRootBlockComponent extends BlockComponent< RootBlockModel, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts index 873a751a50..1de01e5f83 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/default-tool.ts @@ -8,6 +8,7 @@ import { isNoteBlock, OverlayIdentifier, } from '@blocksuite/affine-block-surface'; +import { mountShapeTextEditor } from '@blocksuite/affine-gfx-shape'; import { addText, mountTextElementEditor } from '@blocksuite/affine-gfx-text'; import type { EdgelessTextBlockModel, @@ -53,7 +54,6 @@ import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js'; import { mountConnectorLabelEditor, mountGroupTitleEditor, - mountShapeTextEditor, } from '../utils/text.js'; import { DefaultModeDragType } from './default-tool-ext/ext.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/index.ts b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/index.ts index dd2f9ab4ce..4f336404ac 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/index.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/index.ts @@ -6,5 +6,4 @@ export { EraserTool } from './eraser-tool.js'; export { LassoTool, type LassoToolOption } from './lasso-tool.js'; export { NoteTool, type NoteToolOption } from './note-tool.js'; export { PanTool, type PanToolOption } from './pan-tool.js'; -export { ShapeTool, type ShapeToolOption } from './shape-tool.js'; export { TemplateTool } from './template-tool.js'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/note-tool.ts b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/note-tool.ts index ced3d08b78..54486dbfbb 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/note-tool.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/note-tool.ts @@ -1,5 +1,8 @@ import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface'; -import { addNote } from '@blocksuite/affine-block-surface'; +import { + addNote, + EXCLUDING_MOUSE_OUT_CLASS_LIST, +} from '@blocksuite/affine-block-surface'; import { DEFAULT_NOTE_HEIGHT, DEFAULT_NOTE_WIDTH, @@ -12,7 +15,6 @@ import { BaseTool } from '@blocksuite/block-std/gfx'; import { Point } from '@blocksuite/global/gfx'; import { effect } from '@preact/signals-core'; -import { EXCLUDING_MOUSE_OUT_CLASS_LIST } from '../utils/consts.js'; import { DraggingNoteOverlay, NoteOverlay } from '../utils/tool-overlay.js'; export type NoteToolOption = { diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/consts.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/consts.ts index 2511f99553..7f77673092 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/consts.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/consts.ts @@ -1,9 +1,3 @@ -import { - DEFAULT_ROUGHNESS, - LineWidth, - StrokeStyle, -} from '@blocksuite/affine-model'; - export const NOTE_OVERLAY_OFFSET_X = 6; export const NOTE_OVERLAY_OFFSET_Y = 6; export const NOTE_OVERLAY_WIDTH = 100; @@ -14,20 +8,6 @@ export const NOTE_OVERLAY_TEXT_COLOR = '--affine-icon-color'; export const NOTE_OVERLAY_LIGHT_BACKGROUND_COLOR = 'rgba(252, 252, 253, 1)'; export const NOTE_OVERLAY_DARK_BACKGROUND_COLOR = 'rgb(32, 32, 32)'; -export const SHAPE_OVERLAY_WIDTH = 100; -export const SHAPE_OVERLAY_HEIGHT = 100; -export const SHAPE_OVERLAY_OFFSET_X = 6; -export const SHAPE_OVERLAY_OFFSET_Y = 6; -export const SHAPE_OVERLAY_OPTIONS = { - seed: 666, - roughness: DEFAULT_ROUGHNESS, - strokeStyle: StrokeStyle.Solid, - strokeLineDash: [] as number[], - stroke: 'black', - strokeWidth: LineWidth.Two, - fill: 'transparent', -}; - export const DEFAULT_NOTE_CHILD_FLAVOUR = 'affine:paragraph'; export const DEFAULT_NOTE_CHILD_TYPE = 'text'; export const DEFAULT_NOTE_TIP = 'Text'; @@ -36,12 +16,6 @@ export const FIT_TO_SCREEN_PADDING = 100; export const ATTACHED_DISTANCE = 20; -export const EXCLUDING_MOUSE_OUT_CLASS_LIST = [ - 'affine-note-mask', - 'edgeless-block-portal-note', - 'affine-block-children-container', -]; - export const SurfaceColor = '#6046FE'; export const NoteColor = '#1E96EB'; export const BlendColor = '#7D91FF'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/hotkey-utils.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/hotkey-utils.ts index 4718275caa..fcd217d72f 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/hotkey-utils.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/hotkey-utils.ts @@ -1,7 +1,6 @@ +import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape'; import { ShapeType } from '@blocksuite/affine-model'; -import type { ShapeToolOption } from '../gfx-tool/shape-tool.js'; - const shapeMap: Record = { [ShapeType.Rect]: 0, [ShapeType.Ellipse]: 1, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts index 6ea29c6683..129b97f45d 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/text.ts @@ -3,7 +3,6 @@ import type { ConnectorElementModel, GroupElementModel, } from '@blocksuite/affine-model'; -import { ShapeElementModel } from '@blocksuite/affine-model'; import type { BlockComponent } from '@blocksuite/block-std'; import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; @@ -13,50 +12,6 @@ import * as Y from 'yjs'; import { EdgelessConnectorLabelEditor } from '../components/text/edgeless-connector-label-editor.js'; import { EdgelessGroupTitleEditor } from '../components/text/edgeless-group-title-editor.js'; -import { EdgelessShapeTextEditor } from '../components/text/edgeless-shape-text-editor.js'; - -export function mountShapeTextEditor( - shapeElement: ShapeElementModel, - edgeless: BlockComponent -) { - const mountElm = edgeless.querySelector('.edgeless-mount-point'); - if (!mountElm) { - throw new BlockSuiteError( - ErrorCode.ValueNotExists, - "edgeless block's mount point does not exist" - ); - } - - const gfx = edgeless.std.get(GfxControllerIdentifier); - const crud = edgeless.std.get(EdgelessCRUDIdentifier); - - const updatedElement = crud.getElementById(shapeElement.id); - - if (!(updatedElement instanceof ShapeElementModel)) { - console.error('Cannot mount text editor on a non-shape element'); - return; - } - - gfx.tool.setTool('default'); - gfx.selection.set({ - elements: [shapeElement.id], - editing: true, - }); - - if (!shapeElement.text) { - const text = new Y.Text(); - edgeless.std - .get(EdgelessCRUDIdentifier) - .updateElement(shapeElement.id, { text }); - } - - const shapeEditor = new EdgelessShapeTextEditor(); - shapeEditor.element = updatedElement; - shapeEditor.edgeless = edgeless; - shapeEditor.mountEditor = mountShapeTextEditor; - - mountElm.append(shapeEditor); -} export function mountGroupTitleEditor( group: GroupElementModel, diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/utils/tool-overlay.ts b/blocksuite/affine/blocks/block-root/src/edgeless/utils/tool-overlay.ts index fdae24c26c..795c108e9a 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/utils/tool-overlay.ts +++ b/blocksuite/affine/blocks/block-root/src/edgeless/utils/tool-overlay.ts @@ -1,25 +1,14 @@ import { - type Options, - Overlay, - type RoughCanvas, type SurfaceBlockComponent, + ToolOverlay, } from '@blocksuite/affine-block-surface'; -import { - type Color, - DefaultTheme, - shapeMethods, - type ShapeStyle, -} from '@blocksuite/affine-model'; +import { type Color, DefaultTheme } from '@blocksuite/affine-model'; import { ThemeProvider } from '@blocksuite/affine-shared/services'; import type { GfxController, GfxToolsMap } from '@blocksuite/block-std/gfx'; -import { DisposableGroup } from '@blocksuite/global/disposable'; import type { XYWH } from '@blocksuite/global/gfx'; -import { Bound } from '@blocksuite/global/gfx'; -import { assertType, noop } from '@blocksuite/global/utils'; import { effect } from '@preact/signals-core'; import { Subject } from 'rxjs'; -import type { ShapeTool } from '../gfx-tool/shape-tool.js'; import { NOTE_OVERLAY_CORNER_RADIUS, NOTE_OVERLAY_HEIGHT, @@ -28,319 +17,8 @@ import { NOTE_OVERLAY_STOKE_COLOR, NOTE_OVERLAY_TEXT_COLOR, NOTE_OVERLAY_WIDTH, - SHAPE_OVERLAY_HEIGHT, - SHAPE_OVERLAY_OFFSET_X, - SHAPE_OVERLAY_OFFSET_Y, - SHAPE_OVERLAY_WIDTH, } from '../utils/consts.js'; -const drawRoundedRect = (ctx: CanvasRenderingContext2D, xywh: XYWH) => { - const [x, y, w, h] = xywh; - const width = w; - const height = h; - const radius = 0.1; - const cornerRadius = Math.min(width * radius, height * radius); - ctx.moveTo(x + cornerRadius, y); - ctx.arcTo(x + width, y, x + width, y + height, cornerRadius); - ctx.arcTo(x + width, y + height, x, y + height, cornerRadius); - ctx.arcTo(x, y + height, x, y, cornerRadius); - ctx.arcTo(x, y, x + width, y, cornerRadius); -}; - -const drawGeneralShape = ( - ctx: CanvasRenderingContext2D, - type: string, - xywh: XYWH, - options: Options -) => { - ctx.setLineDash(options.strokeLineDash ?? []); - ctx.strokeStyle = options.stroke ?? 'transparent'; - ctx.lineWidth = options.strokeWidth ?? 2; - ctx.fillStyle = options.fill ?? 'transparent'; - - ctx.beginPath(); - - const bound = Bound.fromXYWH(xywh); - switch (type) { - case 'rect': - shapeMethods.rect.draw(ctx, bound); - break; - case 'triangle': - shapeMethods.triangle.draw(ctx, bound); - break; - case 'diamond': - shapeMethods.diamond.draw(ctx, bound); - break; - case 'ellipse': - shapeMethods.ellipse.draw(ctx, bound); - break; - case 'roundedRect': - drawRoundedRect(ctx, xywh); - break; - default: - throw new Error(`Unknown shape type: ${type}`); - } - - ctx.closePath(); - - ctx.fill(); - ctx.stroke(); -}; - -export abstract class Shape { - options: Options; - - shapeStyle: ShapeStyle; - - type: string; - - xywh: XYWH; - - constructor( - xywh: XYWH, - type: string, - options: Options, - shapeStyle: ShapeStyle - ) { - this.xywh = xywh; - this.type = type; - this.options = options; - this.shapeStyle = shapeStyle; - } - - abstract draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void; -} - -export class RectShape extends Shape { - draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - if (this.shapeStyle === 'Scribbled') { - const [x, y, w, h] = this.xywh; - rc.rectangle(x, y, w, h, this.options); - } else { - drawGeneralShape(ctx, 'rect', this.xywh, this.options); - } - } -} - -export class TriangleShape extends Shape { - draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - if (this.shapeStyle === 'Scribbled') { - const [x, y, w, h] = this.xywh; - rc.polygon( - [ - [x + w / 2, y], - [x, y + h], - [x + w, y + h], - ], - this.options - ); - } else { - drawGeneralShape(ctx, 'triangle', this.xywh, this.options); - } - } -} - -export class DiamondShape extends Shape { - draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - if (this.shapeStyle === 'Scribbled') { - const [x, y, w, h] = this.xywh; - rc.polygon( - [ - [x + w / 2, y], - [x + w, y + h / 2], - [x + w / 2, y + h], - [x, y + h / 2], - ], - this.options - ); - } else { - drawGeneralShape(ctx, 'diamond', this.xywh, this.options); - } - } -} - -export class EllipseShape extends Shape { - draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - if (this.shapeStyle === 'Scribbled') { - const [x, y, w, h] = this.xywh; - rc.ellipse(x + w / 2, y + h / 2, w, h, this.options); - } else { - drawGeneralShape(ctx, 'ellipse', this.xywh, this.options); - } - } -} - -export class RoundedRectShape extends Shape { - draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - if (this.shapeStyle === 'Scribbled') { - const [x, y, w, h] = this.xywh; - const radius = 0.1; - const r = Math.min(w * radius, h * radius); - const x0 = x + r; - const x1 = x + w - r; - const y0 = y + r; - const y1 = y + h - r; - const path = ` - M${x0},${y} L${x1},${y} - A${r},${r} 0 0 1 ${x1},${y0} - L${x1},${y1} - A${r},${r} 0 0 1 ${x1 - r},${y1} - L${x0 + r},${y1} - A${r},${r} 0 0 1 ${x0},${y1 - r} - L${x0},${y0} - A${r},${r} 0 0 1 ${x0 + r},${y} - `; - - rc.path(path, this.options); - } else { - drawGeneralShape(ctx, 'roundedRect', this.xywh, this.options); - } - } -} - -export class ShapeFactory { - static createShape( - xywh: XYWH, - type: string, - options: Options, - shapeStyle: ShapeStyle - ): Shape { - switch (type) { - case 'rect': - return new RectShape(xywh, type, options, shapeStyle); - case 'triangle': - return new TriangleShape(xywh, type, options, shapeStyle); - case 'diamond': - return new DiamondShape(xywh, type, options, shapeStyle); - case 'ellipse': - return new EllipseShape(xywh, type, options, shapeStyle); - case 'roundedRect': - return new RoundedRectShape(xywh, type, options, shapeStyle); - default: - throw new Error(`Unknown shape type: ${type}`); - } - } -} - -class ToolOverlay extends Overlay { - protected disposables = new DisposableGroup(); - - globalAlpha: number; - - x: number; - - y: number; - - constructor(gfx: GfxController) { - super(gfx); - this.x = 0; - this.y = 0; - this.globalAlpha = 0; - this.gfx = gfx; - this.disposables.add( - this.gfx.viewport.viewportUpdated.subscribe(() => { - // when viewport is updated, we should keep the overlay in the same position - // to get last mouse position and convert it to model coordinates - const pos = this.gfx.tool.lastMousePos$.value; - const [x, y] = this.gfx.viewport.toModelCoord(pos.x, pos.y); - this.x = x; - this.y = y; - }) - ); - } - - override dispose(): void { - this.disposables.dispose(); - } - - render(_ctx: CanvasRenderingContext2D, _rc: RoughCanvas): void { - noop(); - } -} - -export class ShapeOverlay extends ToolOverlay { - shape: Shape; - - constructor( - gfx: GfxController, - type: string, - options: Options, - style: { - shapeStyle: ShapeStyle; - fillColor: Color; - strokeColor: Color; - } - ) { - super(gfx); - const xywh = [ - this.x, - this.y, - SHAPE_OVERLAY_WIDTH, - SHAPE_OVERLAY_HEIGHT, - ] as XYWH; - const { shapeStyle, fillColor, strokeColor } = style; - const fill = this.gfx.std - .get(ThemeProvider) - .getColorValue(fillColor, DefaultTheme.shapeFillColor, true); - const stroke = this.gfx.std - .get(ThemeProvider) - .getColorValue(strokeColor, DefaultTheme.shapeStrokeColor, true); - - options.fill = fill; - options.stroke = stroke; - - this.shape = ShapeFactory.createShape(xywh, type, options, shapeStyle); - this.disposables.add( - effect(() => { - const currentTool = this.gfx.tool.currentTool$.value; - - if (currentTool?.toolName !== 'shape') return; - - assertType(currentTool); - - const { shapeName } = currentTool.activatedOption; - const newOptions = { - ...options, - }; - - let { x, y } = this; - if (shapeName === 'roundedRect' || shapeName === 'rect') { - x += SHAPE_OVERLAY_OFFSET_X; - y += SHAPE_OVERLAY_OFFSET_Y; - } - const w = - shapeName === 'roundedRect' - ? SHAPE_OVERLAY_WIDTH + 40 - : SHAPE_OVERLAY_WIDTH; - const xywh = [x, y, w, SHAPE_OVERLAY_HEIGHT] as XYWH; - this.shape = ShapeFactory.createShape( - xywh, - shapeName, - newOptions, - shapeStyle - ); - - (this.gfx.surfaceComponent as SurfaceBlockComponent).refresh(); - }) - ); - } - - override render(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { - ctx.globalAlpha = this.globalAlpha; - let { x, y } = this; - const { type } = this.shape; - if (type === 'roundedRect' || type === 'rect') { - x += SHAPE_OVERLAY_OFFSET_X; - y += SHAPE_OVERLAY_OFFSET_Y; - } - const w = - type === 'roundedRect' ? SHAPE_OVERLAY_WIDTH + 40 : SHAPE_OVERLAY_WIDTH; - const xywh = [x, y, w, SHAPE_OVERLAY_HEIGHT] as XYWH; - this.shape.xywh = xywh; - this.shape.draw(ctx, rc); - } -} - export class NoteOverlay extends ToolOverlay { backgroundColor = 'transparent'; diff --git a/blocksuite/affine/blocks/block-root/src/effects.ts b/blocksuite/affine/blocks/block-root/src/effects.ts index 044579981f..07edea9f0d 100644 --- a/blocksuite/affine/blocks/block-root/src/effects.ts +++ b/blocksuite/affine/blocks/block-root/src/effects.ts @@ -1,3 +1,4 @@ +import { effects as gfxShapeEffects } from '@blocksuite/affine-gfx-shape/effects'; import { effects as gfxCanvasTextEffects } from '@blocksuite/affine-gfx-text/effects'; import { EdgelessAutoCompletePanel } from './edgeless/components/auto-complete/auto-complete-panel.js'; @@ -28,7 +29,6 @@ import { } from './edgeless/components/rects/edgeless-selected-rect.js'; import { EdgelessConnectorLabelEditor } from './edgeless/components/text/edgeless-connector-label-editor.js'; import { EdgelessGroupTitleEditor } from './edgeless/components/text/edgeless-group-title-editor.js'; -import { EdgelessShapeTextEditor } from './edgeless/components/text/edgeless-shape-text-editor.js'; import { EdgelessBrushMenu } from './edgeless/components/toolbar/brush/brush-menu.js'; import { EdgelessBrushToolButton } from './edgeless/components/toolbar/brush/brush-tool-button.js'; import { EdgelessSlideMenu } from './edgeless/components/toolbar/common/slide-menu.js'; @@ -54,9 +54,7 @@ import { EdgelessNavigatorSettingButton } from './edgeless/components/toolbar/pr import { EdgelessPresentButton } from './edgeless/components/toolbar/present/present-button.js'; import { PresentationToolbar } from './edgeless/components/toolbar/presentation-toolbar.js'; import { EdgelessToolbarShapeDraggable } from './edgeless/components/toolbar/shape/shape-draggable.js'; -import { EdgelessShapeMenu } from './edgeless/components/toolbar/shape/shape-menu.js'; import { EdgelessShapeToolButton } from './edgeless/components/toolbar/shape/shape-tool-button.js'; -import { EdgelessShapeToolElement } from './edgeless/components/toolbar/shape/shape-tool-element.js'; import { OverlayScrollbar } from './edgeless/components/toolbar/template/overlay-scrollbar.js'; import { AffineTemplateLoading } from './edgeless/components/toolbar/template/template-loading.js'; import { EdgelessTemplatePanel } from './edgeless/components/toolbar/template/template-panel.js'; @@ -128,6 +126,7 @@ function registerRootComponents() { function registerGfxEffects() { gfxCanvasTextEffects(); + gfxShapeEffects(); } function registerWidgets() { @@ -180,7 +179,6 @@ function registerEdgelessToolbarComponents() { customElements.define('edgeless-frame-menu', EdgelessFrameMenu); customElements.define('edgeless-mindmap-menu', EdgelessMindmapMenu); customElements.define('edgeless-note-menu', EdgelessNoteMenu); - customElements.define('edgeless-shape-menu', EdgelessShapeMenu); customElements.define('edgeless-text-menu', EdgelessTextMenu); customElements.define('edgeless-slide-menu', EdgelessSlideMenu); @@ -231,7 +229,6 @@ function registerEdgelessEditorComponents() { 'edgeless-connector-label-editor', EdgelessConnectorLabelEditor ); - customElements.define('edgeless-shape-text-editor', EdgelessShapeTextEditor); customElements.define( 'edgeless-group-title-editor', EdgelessGroupTitleEditor @@ -283,12 +280,6 @@ function registerMiscComponents() { // Mindmap components customElements.define('mindmap-import-placeholder', MindMapPlaceholder); - // Shape components - customElements.define( - 'edgeless-shape-tool-element', - EdgelessShapeToolElement - ); - // Connector components customElements.define('edgeless-connector-handle', EdgelessConnectorHandle); } @@ -317,7 +308,6 @@ declare global { 'edgeless-selected-rect': EdgelessSelectedRectWidget; 'edgeless-connector-label-editor': EdgelessConnectorLabelEditor; 'edgeless-group-title-editor': EdgelessGroupTitleEditor; - 'edgeless-shape-text-editor': EdgelessShapeTextEditor; 'edgeless-toolbar-widget': EdgelessToolbarWidget; 'presentation-toolbar': PresentationToolbar; 'edgeless-brush-menu': EdgelessBrushMenu; @@ -341,9 +331,7 @@ declare global { 'edgeless-navigator-setting-button': EdgelessNavigatorSettingButton; 'edgeless-present-button': EdgelessPresentButton; 'edgeless-toolbar-shape-draggable': EdgelessToolbarShapeDraggable; - 'edgeless-shape-menu': EdgelessShapeMenu; 'edgeless-shape-tool-button': EdgelessShapeToolButton; - 'edgeless-shape-tool-element': EdgelessShapeToolElement; 'overlay-scrollbar': OverlayScrollbar; 'affine-template-loading': AffineTemplateLoading; 'edgeless-templates-panel': EdgelessTemplatePanel; diff --git a/blocksuite/affine/blocks/block-root/tsconfig.json b/blocksuite/affine/blocks/block-root/tsconfig.json index 772da73811..bb9545f0a1 100644 --- a/blocksuite/affine/blocks/block-root/tsconfig.json +++ b/blocksuite/affine/blocks/block-root/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../block-table" }, { "path": "../../components" }, { "path": "../../fragments/fragment-doc-title" }, + { "path": "../../gfx/shape" }, { "path": "../../gfx/text" }, { "path": "../../inlines/latex" }, { "path": "../../inlines/link" }, diff --git a/blocksuite/affine/blocks/block-surface/src/consts.ts b/blocksuite/affine/blocks/block-surface/src/consts.ts index 6fcf8bd4a8..fb195d9487 100644 --- a/blocksuite/affine/blocks/block-surface/src/consts.ts +++ b/blocksuite/affine/blocks/block-surface/src/consts.ts @@ -16,3 +16,9 @@ export interface IModelCoord { x: number; y: number; } + +export const EXCLUDING_MOUSE_OUT_CLASS_LIST = [ + 'affine-note-mask', + 'edgeless-block-portal-note', + 'affine-block-children-container', +]; diff --git a/blocksuite/affine/blocks/block-surface/src/index.ts b/blocksuite/affine/blocks/block-surface/src/index.ts index 0391714077..e26f5317d2 100644 --- a/blocksuite/affine/blocks/block-surface/src/index.ts +++ b/blocksuite/affine/blocks/block-surface/src/index.ts @@ -1,6 +1,6 @@ // oxlint-disable-next-line @typescript-eslint/triple-slash-reference /// -export { type IModelCoord, ZOOM_MAX, ZOOM_MIN, ZOOM_STEP } from './consts.js'; +export * from './consts.js'; export { GRID_GAP_MAX, GRID_GAP_MIN } from './consts.js'; export { SurfaceElementModel, @@ -29,6 +29,7 @@ export { export { fitContent } from './renderer/elements/shape/utils.js'; export * from './renderer/elements/type.js'; export { Overlay, OverlayIdentifier } from './renderer/overlay.js'; +export { ToolOverlay } from './renderer/tool-overlay.js'; export { MindMapView } from './view/mindmap.js'; import { getCursorByCoord, diff --git a/blocksuite/affine/blocks/block-surface/src/renderer/tool-overlay.ts b/blocksuite/affine/blocks/block-surface/src/renderer/tool-overlay.ts new file mode 100644 index 0000000000..9eeeb2b482 --- /dev/null +++ b/blocksuite/affine/blocks/block-surface/src/renderer/tool-overlay.ts @@ -0,0 +1,42 @@ +import type { GfxController } from '@blocksuite/block-std/gfx'; +import { DisposableGroup } from '@blocksuite/global/disposable'; +import { noop } from '@blocksuite/global/utils'; + +import type { RoughCanvas } from '../utils/rough/canvas'; +import { Overlay } from './overlay'; + +export class ToolOverlay extends Overlay { + protected disposables = new DisposableGroup(); + + globalAlpha: number; + + x: number; + + y: number; + + constructor(gfx: GfxController) { + super(gfx); + this.x = 0; + this.y = 0; + this.globalAlpha = 0; + this.gfx = gfx; + this.disposables.add( + this.gfx.viewport.viewportUpdated.subscribe(() => { + // when viewport is updated, we should keep the overlay in the same position + // to get last mouse position and convert it to model coordinates + const pos = this.gfx.tool.lastMousePos$.value; + const [x, y] = this.gfx.viewport.toModelCoord(pos.x, pos.y); + this.x = x; + this.y = y; + }) + ); + } + + override dispose(): void { + this.disposables.dispose(); + } + + render(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + noop([ctx, rc]); + } +} diff --git a/blocksuite/affine/gfx/shape/package.json b/blocksuite/affine/gfx/shape/package.json new file mode 100644 index 0000000000..6e2ceecb1c --- /dev/null +++ b/blocksuite/affine/gfx/shape/package.json @@ -0,0 +1,44 @@ +{ + "name": "@blocksuite/affine-gfx-shape", + "description": "Gfx shape for BlockSuite.", + "type": "module", + "scripts": { + "build": "tsc" + }, + "sideEffects": false, + "keywords": [], + "author": "toeverything", + "license": "MIT", + "dependencies": { + "@blocksuite/affine-block-surface": "workspace:*", + "@blocksuite/affine-components": "workspace:*", + "@blocksuite/affine-model": "workspace:*", + "@blocksuite/affine-rich-text": "workspace:*", + "@blocksuite/affine-shared": "workspace:*", + "@blocksuite/block-std": "workspace:*", + "@blocksuite/global": "workspace:*", + "@blocksuite/icons": "^2.2.6", + "@blocksuite/store": "workspace:*", + "@lit/context": "^1.1.2", + "@preact/signals-core": "^1.8.0", + "@toeverything/theme": "^1.1.12", + "@types/lodash-es": "^4.17.12", + "lit": "^3.2.0", + "lodash-es": "^4.17.21", + "minimatch": "^10.0.1", + "rxjs": "^7.8.1", + "yjs": "^13.6.21", + "zod": "^3.23.8" + }, + "exports": { + ".": "./src/index.ts", + "./effects": "./src/effects.ts" + }, + "files": [ + "src", + "dist", + "!src/__tests__", + "!dist/__tests__" + ], + "version": "0.20.0" +} diff --git a/blocksuite/affine/gfx/shape/src/consts.ts b/blocksuite/affine/gfx/shape/src/consts.ts new file mode 100644 index 0000000000..1a9e3f5133 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/consts.ts @@ -0,0 +1,19 @@ +import { + DEFAULT_ROUGHNESS, + LineWidth, + StrokeStyle, +} from '@blocksuite/affine-model'; + +export const SHAPE_OVERLAY_WIDTH = 100; +export const SHAPE_OVERLAY_HEIGHT = 100; +export const SHAPE_OVERLAY_OFFSET_X = 6; +export const SHAPE_OVERLAY_OFFSET_Y = 6; +export const SHAPE_OVERLAY_OPTIONS = { + seed: 666, + roughness: DEFAULT_ROUGHNESS, + strokeStyle: StrokeStyle.Solid, + strokeLineDash: [] as number[], + stroke: 'black', + strokeWidth: LineWidth.Two, + fill: 'transparent', +}; diff --git a/blocksuite/affine/gfx/shape/src/draggable/index.ts b/blocksuite/affine/gfx/shape/src/draggable/index.ts new file mode 100644 index 0000000000..ceb895bd3f --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/draggable/index.ts @@ -0,0 +1,2 @@ +export * from './shape-menu'; +export * from './shape-tool-element'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu.ts b/blocksuite/affine/gfx/shape/src/draggable/shape-menu.ts similarity index 94% rename from blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu.ts rename to blocksuite/affine/gfx/shape/src/draggable/shape-menu.ts index ff57f56021..f7861928ac 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu.ts +++ b/blocksuite/affine/gfx/shape/src/draggable/shape-menu.ts @@ -12,6 +12,8 @@ import { ThemeProvider, } from '@blocksuite/affine-shared/services'; import type { ColorEvent } from '@blocksuite/affine-shared/utils'; +import type { BlockComponent } from '@blocksuite/block-std'; +import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { StyleGeneralIcon, StyleScribbleIcon } from '@blocksuite/icons/lit'; import { computed, effect, type Signal, signal } from '@preact/signals-core'; @@ -19,8 +21,7 @@ import { css, html, LitElement } from 'lit'; import { property } from 'lit/decorators.js'; import { when } from 'lit/directives/when.js'; -import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js'; -import { ShapeComponentConfig } from './shape-menu-config.js'; +import { ShapeComponentConfig } from '../toolbar'; export class EdgelessShapeMenu extends SignalWatcher( WithDisposable(LitElement) @@ -55,7 +56,7 @@ export class EdgelessShapeMenu extends SignalWatcher( private readonly _shapeName$: Signal = signal(ShapeType.Rect); @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; + accessor edgeless!: BlockComponent; private readonly _props$ = computed(() => { const shapeName: ShapeName = this._shapeName$.value; @@ -109,9 +110,10 @@ export class EdgelessShapeMenu extends SignalWatcher( override connectedCallback(): void { super.connectedCallback(); + const gfx = this.edgeless.std.get(GfxControllerIdentifier); this._disposables.add( effect(() => { - const value = this.edgeless.gfx.tool.currentToolOption$.value; + const value = gfx.tool.currentToolOption$.value; if (value && value.type === 'shape') { this._shapeName$.value = value.shapeName; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-element.ts b/blocksuite/affine/gfx/shape/src/draggable/shape-tool-element.ts similarity index 93% rename from blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-element.ts rename to blocksuite/affine/gfx/shape/src/draggable/shape-tool-element.ts index a7adca5be0..fa986d2cd6 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-tool-element.ts +++ b/blocksuite/affine/gfx/shape/src/draggable/shape-tool-element.ts @@ -8,6 +8,8 @@ import { type ShapeName, type ShapeStyle, } from '@blocksuite/affine-model'; +import type { BlockComponent } from '@blocksuite/block-std'; +import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; import { Bound } from '@blocksuite/global/gfx'; import { WithDisposable } from '@blocksuite/global/lit'; import { sleep } from '@blocksuite/global/utils'; @@ -20,10 +22,9 @@ import { } from 'lit'; import { property, query, state } from 'lit/decorators.js'; -import type { EdgelessRootBlockComponent } from '../../../edgeless-root-block.js'; -import { ShapeTool } from '../../../gfx-tool/shape-tool.js'; +import { ShapeTool } from '../shape-tool'; -export interface Shape { +interface Shape { name: ShapeName; svg: TemplateResult<1>; } @@ -76,13 +77,17 @@ export class EdgelessShapeToolElement extends WithDisposable(LitElement) { return this.edgeless.std.get(EdgelessCRUDIdentifier); } + get gfx() { + return this.edgeless.std.get(GfxControllerIdentifier); + } + private readonly _addShape = (coord: Coord, padding: Coord) => { const width = 100; const height = 100; const { x: edgelessX, y: edgelessY } = this.edgeless.getBoundingClientRect(); - const zoom = this.edgeless.service.viewport.zoom; - const [modelX, modelY] = this.edgeless.service.viewport.toModelCoord( + const zoom = this.gfx.viewport.zoom; + const [modelX, modelY] = this.gfx.viewport.toModelCoord( coord.x - edgelessX - width * padding.x * zoom, coord.y - edgelessY - height * padding.y * zoom ); @@ -104,7 +109,8 @@ export class EdgelessShapeToolElement extends WithDisposable(LitElement) { return; } this._dragging = false; - this.edgeless.gfx.tool.setTool('default'); + // @ts-expect-error FIXME: resolve after gfx tool refactor + this.gfx.tool.setTool('default'); if (this._isOutside) { const rect = this._shapeElement.getBoundingClientRect(); this._backupShapeElement.style.setProperty('transition', 'none'); @@ -131,7 +137,7 @@ export class EdgelessShapeToolElement extends WithDisposable(LitElement) { if (!this._dragging) { return; } - const controller = this.edgeless.gfx.tool.currentTool$.peek(); + const controller = this.gfx.tool.currentTool$.peek(); if (controller instanceof ShapeTool) { controller.clearOverlay(); } @@ -298,7 +304,7 @@ export class EdgelessShapeToolElement extends WithDisposable(LitElement) { private accessor _startCoord: Coord = { x: -1, y: -1 }; @property({ attribute: false }) - accessor edgeless!: EdgelessRootBlockComponent; + accessor edgeless!: BlockComponent; @property({ attribute: false }) accessor getContainerRect!: () => DOMRect; diff --git a/blocksuite/affine/gfx/shape/src/effects.ts b/blocksuite/affine/gfx/shape/src/effects.ts new file mode 100644 index 0000000000..54e65ad51e --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/effects.ts @@ -0,0 +1,19 @@ +import { EdgelessShapeMenu, EdgelessShapeToolElement } from './draggable'; +import { EdgelessShapeTextEditor } from './text/edgeless-shape-text-editor'; + +export function effects() { + customElements.define('edgeless-shape-text-editor', EdgelessShapeTextEditor); + customElements.define('edgeless-shape-menu', EdgelessShapeMenu); + customElements.define( + 'edgeless-shape-tool-element', + EdgelessShapeToolElement + ); +} + +declare global { + interface HTMLElementTagNameMap { + 'edgeless-shape-text-editor': EdgelessShapeTextEditor; + 'edgeless-shape-menu': EdgelessShapeMenu; + 'edgeless-shape-tool-element': EdgelessShapeToolElement; + } +} diff --git a/blocksuite/affine/gfx/shape/src/index.ts b/blocksuite/affine/gfx/shape/src/index.ts new file mode 100644 index 0000000000..e458e71350 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/index.ts @@ -0,0 +1,6 @@ +export * from './consts'; +export * from './draggable'; +export * from './overlay'; +export * from './shape-tool'; +export * from './text'; +export * from './toolbar'; diff --git a/blocksuite/affine/gfx/shape/src/overlay/diamond.ts b/blocksuite/affine/gfx/shape/src/overlay/diamond.ts new file mode 100644 index 0000000000..e7d02aff85 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/diamond.ts @@ -0,0 +1,23 @@ +import type { RoughCanvas } from '@blocksuite/affine-block-surface'; + +import { Shape } from './shape'; +import { drawGeneralShape } from './utils'; + +export class DiamondShape extends Shape { + draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + if (this.shapeStyle === 'Scribbled') { + const [x, y, w, h] = this.xywh; + rc.polygon( + [ + [x + w / 2, y], + [x + w, y + h / 2], + [x + w / 2, y + h], + [x, y + h / 2], + ], + this.options + ); + } else { + drawGeneralShape(ctx, 'diamond', this.xywh, this.options); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/ellipse.ts b/blocksuite/affine/gfx/shape/src/overlay/ellipse.ts new file mode 100644 index 0000000000..4841ba0b79 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/ellipse.ts @@ -0,0 +1,15 @@ +import type { RoughCanvas } from '@blocksuite/affine-block-surface'; + +import { Shape } from './shape'; +import { drawGeneralShape } from './utils'; + +export class EllipseShape extends Shape { + draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + if (this.shapeStyle === 'Scribbled') { + const [x, y, w, h] = this.xywh; + rc.ellipse(x + w / 2, y + h / 2, w, h, this.options); + } else { + drawGeneralShape(ctx, 'ellipse', this.xywh, this.options); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/factory.ts b/blocksuite/affine/gfx/shape/src/overlay/factory.ts new file mode 100644 index 0000000000..73ccccfdf9 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/factory.ts @@ -0,0 +1,34 @@ +import type { Options } from '@blocksuite/affine-block-surface'; +import type { ShapeStyle } from '@blocksuite/affine-model'; +import type { XYWH } from '@blocksuite/global/gfx'; + +import { DiamondShape } from './diamond'; +import { EllipseShape } from './ellipse'; +import { RectShape } from './rect'; +import { RoundedRectShape } from './rounded-rect'; +import type { Shape } from './shape'; +import { TriangleShape } from './triangle'; + +export class ShapeFactory { + static createShape( + xywh: XYWH, + type: string, + options: Options, + shapeStyle: ShapeStyle + ): Shape { + switch (type) { + case 'rect': + return new RectShape(xywh, type, options, shapeStyle); + case 'triangle': + return new TriangleShape(xywh, type, options, shapeStyle); + case 'diamond': + return new DiamondShape(xywh, type, options, shapeStyle); + case 'ellipse': + return new EllipseShape(xywh, type, options, shapeStyle); + case 'roundedRect': + return new RoundedRectShape(xywh, type, options, shapeStyle); + default: + throw new Error(`Unknown shape type: ${type}`); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/index.ts b/blocksuite/affine/gfx/shape/src/overlay/index.ts new file mode 100644 index 0000000000..1756a62c2d --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/index.ts @@ -0,0 +1,3 @@ +export * from './factory'; +export * from './shape'; +export * from './shape-overlay'; diff --git a/blocksuite/affine/gfx/shape/src/overlay/rect.ts b/blocksuite/affine/gfx/shape/src/overlay/rect.ts new file mode 100644 index 0000000000..0bf24c208a --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/rect.ts @@ -0,0 +1,15 @@ +import type { RoughCanvas } from '@blocksuite/affine-block-surface'; + +import { Shape } from './shape'; +import { drawGeneralShape } from './utils'; + +export class RectShape extends Shape { + draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + if (this.shapeStyle === 'Scribbled') { + const [x, y, w, h] = this.xywh; + rc.rectangle(x, y, w, h, this.options); + } else { + drawGeneralShape(ctx, 'rect', this.xywh, this.options); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/rounded-rect.ts b/blocksuite/affine/gfx/shape/src/overlay/rounded-rect.ts new file mode 100644 index 0000000000..034c47464f --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/rounded-rect.ts @@ -0,0 +1,32 @@ +import type { RoughCanvas } from '@blocksuite/affine-block-surface'; + +import { Shape } from './shape'; +import { drawGeneralShape } from './utils'; + +export class RoundedRectShape extends Shape { + draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + if (this.shapeStyle === 'Scribbled') { + const [x, y, w, h] = this.xywh; + const radius = 0.1; + const r = Math.min(w * radius, h * radius); + const x0 = x + r; + const x1 = x + w - r; + const y0 = y + r; + const y1 = y + h - r; + const path = ` + M${x0},${y} L${x1},${y} + A${r},${r} 0 0 1 ${x1},${y0} + L${x1},${y1} + A${r},${r} 0 0 1 ${x1 - r},${y1} + L${x0 + r},${y1} + A${r},${r} 0 0 1 ${x0},${y1 - r} + L${x0},${y0} + A${r},${r} 0 0 1 ${x0 + r},${y} + `; + + rc.path(path, this.options); + } else { + drawGeneralShape(ctx, 'roundedRect', this.xywh, this.options); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/shape-overlay.ts b/blocksuite/affine/gfx/shape/src/overlay/shape-overlay.ts new file mode 100644 index 0000000000..edfcf62598 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/shape-overlay.ts @@ -0,0 +1,109 @@ +import { + type Options, + type RoughCanvas, + type SurfaceBlockComponent, + ToolOverlay, +} from '@blocksuite/affine-block-surface'; +import { + type Color, + DefaultTheme, + type ShapeStyle, +} from '@blocksuite/affine-model'; +import { ThemeProvider } from '@blocksuite/affine-shared/services'; +import type { GfxController } from '@blocksuite/block-std/gfx'; +import type { XYWH } from '@blocksuite/global/gfx'; +import { assertType } from '@blocksuite/global/utils'; +import { effect } from '@preact/signals-core'; + +import { + SHAPE_OVERLAY_HEIGHT, + SHAPE_OVERLAY_OFFSET_X, + SHAPE_OVERLAY_OFFSET_Y, + SHAPE_OVERLAY_WIDTH, +} from '../consts'; +import type { ShapeTool } from '../shape-tool'; +import { ShapeFactory } from './factory'; +import type { Shape } from './shape'; + +export class ShapeOverlay extends ToolOverlay { + shape: Shape; + + constructor( + gfx: GfxController, + type: string, + options: Options, + style: { + shapeStyle: ShapeStyle; + fillColor: Color; + strokeColor: Color; + } + ) { + super(gfx); + const xywh = [ + this.x, + this.y, + SHAPE_OVERLAY_WIDTH, + SHAPE_OVERLAY_HEIGHT, + ] as XYWH; + const { shapeStyle, fillColor, strokeColor } = style; + const fill = this.gfx.std + .get(ThemeProvider) + .getColorValue(fillColor, DefaultTheme.shapeFillColor, true); + const stroke = this.gfx.std + .get(ThemeProvider) + .getColorValue(strokeColor, DefaultTheme.shapeStrokeColor, true); + + options.fill = fill; + options.stroke = stroke; + + this.shape = ShapeFactory.createShape(xywh, type, options, shapeStyle); + this.disposables.add( + effect(() => { + const currentTool = this.gfx.tool.currentTool$.value; + + if (currentTool?.toolName !== 'shape') return; + + assertType(currentTool); + + const { shapeName } = currentTool.activatedOption; + const newOptions = { + ...options, + }; + + let { x, y } = this; + if (shapeName === 'roundedRect' || shapeName === 'rect') { + x += SHAPE_OVERLAY_OFFSET_X; + y += SHAPE_OVERLAY_OFFSET_Y; + } + const w = + shapeName === 'roundedRect' + ? SHAPE_OVERLAY_WIDTH + 40 + : SHAPE_OVERLAY_WIDTH; + const xywh = [x, y, w, SHAPE_OVERLAY_HEIGHT] as XYWH; + this.shape = ShapeFactory.createShape( + xywh, + shapeName, + newOptions, + shapeStyle + ); + + (this.gfx.surfaceComponent as SurfaceBlockComponent).refresh(); + }) + ); + } + + override render(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + ctx.globalAlpha = this.globalAlpha; + let { x, y } = this; + const { type } = this.shape; + if (type === 'roundedRect' || type === 'rect') { + x += SHAPE_OVERLAY_OFFSET_X; + y += SHAPE_OVERLAY_OFFSET_Y; + } + const w = + type === 'roundedRect' ? SHAPE_OVERLAY_WIDTH + 40 : SHAPE_OVERLAY_WIDTH; + const xywh = [x, y, w, SHAPE_OVERLAY_HEIGHT] as XYWH; + this.shape.xywh = xywh; + this.shape.draw(ctx, rc); + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/shape.ts b/blocksuite/affine/gfx/shape/src/overlay/shape.ts new file mode 100644 index 0000000000..a3c93d6931 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/shape.ts @@ -0,0 +1,27 @@ +import type { Options, RoughCanvas } from '@blocksuite/affine-block-surface'; +import type { ShapeStyle } from '@blocksuite/affine-model'; +import type { XYWH } from '@blocksuite/global/gfx'; + +export abstract class Shape { + options: Options; + + shapeStyle: ShapeStyle; + + type: string; + + xywh: XYWH; + + constructor( + xywh: XYWH, + type: string, + options: Options, + shapeStyle: ShapeStyle + ) { + this.xywh = xywh; + this.type = type; + this.options = options; + this.shapeStyle = shapeStyle; + } + + abstract draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void; +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/triangle.ts b/blocksuite/affine/gfx/shape/src/overlay/triangle.ts new file mode 100644 index 0000000000..4aed5d716d --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/triangle.ts @@ -0,0 +1,22 @@ +import type { RoughCanvas } from '@blocksuite/affine-block-surface'; + +import { Shape } from './shape'; +import { drawGeneralShape } from './utils'; + +export class TriangleShape extends Shape { + draw(ctx: CanvasRenderingContext2D, rc: RoughCanvas): void { + if (this.shapeStyle === 'Scribbled') { + const [x, y, w, h] = this.xywh; + rc.polygon( + [ + [x + w / 2, y], + [x, y + h], + [x + w, y + h], + ], + this.options + ); + } else { + drawGeneralShape(ctx, 'triangle', this.xywh, this.options); + } + } +} diff --git a/blocksuite/affine/gfx/shape/src/overlay/utils.ts b/blocksuite/affine/gfx/shape/src/overlay/utils.ts new file mode 100644 index 0000000000..1e60faaf17 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/overlay/utils.ts @@ -0,0 +1,56 @@ +import type { Options } from '@blocksuite/affine-block-surface'; +import { shapeMethods } from '@blocksuite/affine-model'; +import { Bound, type XYWH } from '@blocksuite/global/gfx'; + +export const drawGeneralShape = ( + ctx: CanvasRenderingContext2D, + type: string, + xywh: XYWH, + options: Options +) => { + ctx.setLineDash(options.strokeLineDash ?? []); + ctx.strokeStyle = options.stroke ?? 'transparent'; + ctx.lineWidth = options.strokeWidth ?? 2; + ctx.fillStyle = options.fill ?? 'transparent'; + + ctx.beginPath(); + + const bound = Bound.fromXYWH(xywh); + switch (type) { + case 'rect': + shapeMethods.rect.draw(ctx, bound); + break; + case 'triangle': + shapeMethods.triangle.draw(ctx, bound); + break; + case 'diamond': + shapeMethods.diamond.draw(ctx, bound); + break; + case 'ellipse': + shapeMethods.ellipse.draw(ctx, bound); + break; + case 'roundedRect': + drawRoundedRect(ctx, xywh); + break; + default: + throw new Error(`Unknown shape type: ${type}`); + } + + ctx.closePath(); + + ctx.fill(); + ctx.stroke(); +}; + +function drawRoundedRect(ctx: CanvasRenderingContext2D, xywh: XYWH): void { + const [x, y, w, h] = xywh; + const width = w; + const height = h; + const radius = 0.1; + const cornerRadius = Math.min(width * radius, height * radius); + ctx.moveTo(x + cornerRadius, y); + ctx.arcTo(x + width, y, x + width, y + height, cornerRadius); + ctx.arcTo(x + width, y + height, x, y + height, cornerRadius); + ctx.arcTo(x, y + height, x, y, cornerRadius); + ctx.arcTo(x, y, x + width, y, cornerRadius); +} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/shape-tool.ts b/blocksuite/affine/gfx/shape/src/shape-tool.ts similarity index 97% rename from blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/shape-tool.ts rename to blocksuite/affine/gfx/shape/src/shape-tool.ts index 3d905993fc..425c1c75c5 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/gfx-tool/shape-tool.ts +++ b/blocksuite/affine/gfx/shape/src/shape-tool.ts @@ -1,5 +1,6 @@ import { CanvasElementType, + EXCLUDING_MOUSE_OUT_CLASS_LIST, type SurfaceBlockComponent, } from '@blocksuite/affine-block-surface'; import type { ShapeElementModel, ShapeName } from '@blocksuite/affine-model'; @@ -17,12 +18,11 @@ import { Bound } from '@blocksuite/global/gfx'; import { effect } from '@preact/signals-core'; import { - EXCLUDING_MOUSE_OUT_CLASS_LIST, SHAPE_OVERLAY_HEIGHT, SHAPE_OVERLAY_OPTIONS, SHAPE_OVERLAY_WIDTH, -} from '../utils/consts.js'; -import { ShapeOverlay } from '../utils/tool-overlay.js'; +} from './consts.js'; +import { ShapeOverlay } from './overlay/shape-overlay.js'; export type ShapeToolOption = { shapeName: ShapeName; @@ -181,6 +181,7 @@ export class ShapeTool extends BaseTool { const element = this.gfx.getElementById(id); if (!element) return; + // @ts-expect-error FIXME: resolve after gfx tool refactor this.gfx.tool.setTool('default'); this.gfx.selection.set({ elements: [element.id], @@ -257,6 +258,7 @@ export class ShapeTool extends BaseTool { const element = this.gfx.getElementById(id); if (!element) return; + // @ts-expect-error FIXME: resolve after gfx tool refactor this.controller.setTool('default'); this.gfx.selection.set({ elements: [element.id], diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/text/edgeless-shape-text-editor.ts b/blocksuite/affine/gfx/shape/src/text/edgeless-shape-text-editor.ts similarity index 100% rename from blocksuite/affine/blocks/block-root/src/edgeless/components/text/edgeless-shape-text-editor.ts rename to blocksuite/affine/gfx/shape/src/text/edgeless-shape-text-editor.ts diff --git a/blocksuite/affine/gfx/shape/src/text/index.ts b/blocksuite/affine/gfx/shape/src/text/index.ts new file mode 100644 index 0000000000..1a9ac1460f --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/text/index.ts @@ -0,0 +1 @@ +export * from './text'; diff --git a/blocksuite/affine/gfx/shape/src/text/text.ts b/blocksuite/affine/gfx/shape/src/text/text.ts new file mode 100644 index 0000000000..737833e328 --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/text/text.ts @@ -0,0 +1,52 @@ +import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface'; +import { ShapeElementModel } from '@blocksuite/affine-model'; +import type { BlockComponent } from '@blocksuite/block-std'; +import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx'; +import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions'; +import * as Y from 'yjs'; + +import { EdgelessShapeTextEditor } from './edgeless-shape-text-editor'; + +export function mountShapeTextEditor( + shapeElement: ShapeElementModel, + edgeless: BlockComponent +) { + const mountElm = edgeless.querySelector('.edgeless-mount-point'); + if (!mountElm) { + throw new BlockSuiteError( + ErrorCode.ValueNotExists, + "edgeless block's mount point does not exist" + ); + } + + const gfx = edgeless.std.get(GfxControllerIdentifier); + const crud = edgeless.std.get(EdgelessCRUDIdentifier); + + const updatedElement = crud.getElementById(shapeElement.id); + + if (!(updatedElement instanceof ShapeElementModel)) { + console.error('Cannot mount text editor on a non-shape element'); + return; + } + + // @ts-expect-error FIXME: resolve after gfx tool refactor + gfx.tool.setTool('default'); + gfx.selection.set({ + elements: [shapeElement.id], + editing: true, + }); + + if (!shapeElement.text) { + const text = new Y.Text(); + edgeless.std + .get(EdgelessCRUDIdentifier) + .updateElement(shapeElement.id, { text }); + } + + const shapeEditor = new EdgelessShapeTextEditor(); + shapeEditor.element = updatedElement; + shapeEditor.edgeless = edgeless; + shapeEditor.mountEditor = mountShapeTextEditor; + + mountElm.append(shapeEditor); +} diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/icons.ts b/blocksuite/affine/gfx/shape/src/toolbar/icons.ts similarity index 100% rename from blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/icons.ts rename to blocksuite/affine/gfx/shape/src/toolbar/icons.ts diff --git a/blocksuite/affine/gfx/shape/src/toolbar/index.ts b/blocksuite/affine/gfx/shape/src/toolbar/index.ts new file mode 100644 index 0000000000..81630f688d --- /dev/null +++ b/blocksuite/affine/gfx/shape/src/toolbar/index.ts @@ -0,0 +1,2 @@ +export * from './icons'; +export * from './shape-menu-config'; diff --git a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu-config.ts b/blocksuite/affine/gfx/shape/src/toolbar/shape-menu-config.ts similarity index 95% rename from blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu-config.ts rename to blocksuite/affine/gfx/shape/src/toolbar/shape-menu-config.ts index a4b57ea0ad..ed95c6200f 100644 --- a/blocksuite/affine/blocks/block-root/src/edgeless/components/toolbar/shape/shape-menu-config.ts +++ b/blocksuite/affine/gfx/shape/src/toolbar/shape-menu-config.ts @@ -1,3 +1,4 @@ +import type { ShapeToolOption } from '@blocksuite/affine-gfx-shape'; import { ShapeType } from '@blocksuite/affine-model'; import { DiamondIcon, @@ -8,7 +9,6 @@ import { } from '@blocksuite/icons/lit'; import type { TemplateResult } from 'lit'; -import type { ShapeToolOption } from '../../../gfx-tool/shape-tool'; import { ScribbledDiamondIcon, ScribbledEllipseIcon, diff --git a/blocksuite/affine/gfx/shape/tsconfig.json b/blocksuite/affine/gfx/shape/tsconfig.json new file mode 100644 index 0000000000..46d91a2e30 --- /dev/null +++ b/blocksuite/affine/gfx/shape/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" + }, + "include": ["./src"], + "references": [ + { "path": "../../blocks/block-surface" }, + { "path": "../../components" }, + { "path": "../../model" }, + { "path": "../../rich-text" }, + { "path": "../../shared" }, + { "path": "../../../framework/block-std" }, + { "path": "../../../framework/global" }, + { "path": "../../../framework/store" } + ] +} diff --git a/tools/utils/src/workspace.gen.ts b/tools/utils/src/workspace.gen.ts index 7fbdd9aec4..b07acbb141 100644 --- a/tools/utils/src/workspace.gen.ts +++ b/tools/utils/src/workspace.gen.ts @@ -28,6 +28,7 @@ export const PackageList = [ 'blocksuite/affine/fragments/fragment-doc-title', 'blocksuite/affine/fragments/fragment-frame-panel', 'blocksuite/affine/fragments/fragment-outline', + 'blocksuite/affine/gfx/shape', 'blocksuite/affine/gfx/text', 'blocksuite/affine/gfx/turbo-renderer', 'blocksuite/affine/inlines/footnote', @@ -303,6 +304,7 @@ export const PackageList = [ 'blocksuite/affine/blocks/block-table', 'blocksuite/affine/components', 'blocksuite/affine/fragments/fragment-doc-title', + 'blocksuite/affine/gfx/shape', 'blocksuite/affine/gfx/text', 'blocksuite/affine/inlines/latex', 'blocksuite/affine/inlines/link', @@ -435,6 +437,20 @@ export const PackageList = [ 'blocksuite/framework/store', ], }, + { + location: 'blocksuite/affine/gfx/shape', + name: '@blocksuite/affine-gfx-shape', + workspaceDependencies: [ + 'blocksuite/affine/blocks/block-surface', + 'blocksuite/affine/components', + 'blocksuite/affine/model', + 'blocksuite/affine/rich-text', + 'blocksuite/affine/shared', + 'blocksuite/framework/block-std', + 'blocksuite/framework/global', + 'blocksuite/framework/store', + ], + }, { location: 'blocksuite/affine/gfx/text', name: '@blocksuite/affine-gfx-text', @@ -1020,6 +1036,7 @@ export type PackageName = | '@blocksuite/affine-fragment-doc-title' | '@blocksuite/affine-fragment-frame-panel' | '@blocksuite/affine-fragment-outline' + | '@blocksuite/affine-gfx-shape' | '@blocksuite/affine-gfx-text' | '@blocksuite/affine-gfx-turbo-renderer' | '@blocksuite/affine-inline-footnote' diff --git a/tsconfig.json b/tsconfig.json index 6dae7c5932..edcf96cc65 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -75,6 +75,7 @@ { "path": "./blocksuite/affine/fragments/fragment-doc-title" }, { "path": "./blocksuite/affine/fragments/fragment-frame-panel" }, { "path": "./blocksuite/affine/fragments/fragment-outline" }, + { "path": "./blocksuite/affine/gfx/shape" }, { "path": "./blocksuite/affine/gfx/text" }, { "path": "./blocksuite/affine/gfx/turbo-renderer" }, { "path": "./blocksuite/affine/inlines/footnote" }, diff --git a/yarn.lock b/yarn.lock index a1ab8e43a4..3702c9052d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2739,6 +2739,7 @@ __metadata: "@blocksuite/affine-block-table": "workspace:*" "@blocksuite/affine-components": "workspace:*" "@blocksuite/affine-fragment-doc-title": "workspace:*" + "@blocksuite/affine-gfx-shape": "workspace:*" "@blocksuite/affine-gfx-text": "workspace:*" "@blocksuite/affine-inline-latex": "workspace:*" "@blocksuite/affine-inline-link": "workspace:*" @@ -2972,6 +2973,32 @@ __metadata: languageName: unknown linkType: soft +"@blocksuite/affine-gfx-shape@workspace:*, @blocksuite/affine-gfx-shape@workspace:blocksuite/affine/gfx/shape": + version: 0.0.0-use.local + resolution: "@blocksuite/affine-gfx-shape@workspace:blocksuite/affine/gfx/shape" + dependencies: + "@blocksuite/affine-block-surface": "workspace:*" + "@blocksuite/affine-components": "workspace:*" + "@blocksuite/affine-model": "workspace:*" + "@blocksuite/affine-rich-text": "workspace:*" + "@blocksuite/affine-shared": "workspace:*" + "@blocksuite/block-std": "workspace:*" + "@blocksuite/global": "workspace:*" + "@blocksuite/icons": "npm:^2.2.6" + "@blocksuite/store": "workspace:*" + "@lit/context": "npm:^1.1.2" + "@preact/signals-core": "npm:^1.8.0" + "@toeverything/theme": "npm:^1.1.12" + "@types/lodash-es": "npm:^4.17.12" + lit: "npm:^3.2.0" + lodash-es: "npm:^4.17.21" + minimatch: "npm:^10.0.1" + rxjs: "npm:^7.8.1" + yjs: "npm:^13.6.21" + zod: "npm:^3.23.8" + languageName: unknown + linkType: soft + "@blocksuite/affine-gfx-text@workspace:*, @blocksuite/affine-gfx-text@workspace:blocksuite/affine/gfx/text": version: 0.0.0-use.local resolution: "@blocksuite/affine-gfx-text@workspace:blocksuite/affine/gfx/text" @@ -3433,6 +3460,7 @@ __metadata: "@blocksuite/affine-fragment-doc-title": "workspace:*" "@blocksuite/affine-fragment-frame-panel": "workspace:*" "@blocksuite/affine-fragment-outline": "workspace:*" + "@blocksuite/affine-gfx-shape": "workspace:*" "@blocksuite/affine-gfx-text": "workspace:*" "@blocksuite/affine-gfx-turbo-renderer": "workspace:*" "@blocksuite/affine-inline-footnote": "workspace:*"