mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-24 18:02:47 +08:00
refactor(editor): rewrite resize and rotate (#12054)
### Changed
This pr split the old `edgeless-selected-rect` into four focused modules:
- `edgeless-selected-rect`: Provide an entry point for user operation on view layer only, no further logic here.
- `GfxViewInteractionExtension`: Allow you to plug in custom resize/rotate behaviors for block or canvas element. If you don’t register an extension, it falls back to the default behaviours.
- `InteractivityManager`: Provide the API that accepts resize/rotate requests, invokes any custom behaviors you’ve registered, tracks the lifecycle and intermediate state, then hands off to the math engine.
- `ResizeController`: A pure math engine that listens for pointer moves and pointer ups and calculates new sizes, positions, and angles. It doesn’t call any business APIs.
### Customizing an element’s resize/rotate behavior
Call `GfxViewInteractionExtension` with the element’s flavour or type plus a config object. In the config you can define:
- `resizeConstraint` (min/max width & height, lock ratio)
- `handleResize(context)` method that returns an object containing `beforeResize`、`onResizeStart`、`onResizeMove`、`onResizeEnd`
- `handleRotate(context)` method that returns an object containing `beforeRotate`、`onRotateStart`、`onRotateMove`、`onRotateEnd`
```typescript
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
GfxViewInteractionExtension(
flavourOrElementType,
{
resizeConstraint: {
minWidth,
maxWidth,
lockRatio,
minHeight,
maxHeight
},
handleResize(context) {
return {
beforeResize(context) {},
onResizeStart(context) {},
onResizeMove(context) {},
onResizeEnd(context) {}
};
},
handleRotate(context) {
return {
beforeRotate(context) {},
onRotateStart(context) {},
onRotateMove(context) {},
onRotateEnd(context) {}
};
}
}
);
```
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit
- **New Features**
- Added interaction extensions for edgeless variants of attachment, bookmark, edgeless text, embedded docs, images, notes, frames, AI chat blocks, and various embed blocks (Figma, GitHub, HTML, iframe, Loom, YouTube).
- Introduced interaction extensions for graphical elements including connectors, groups, mind maps, shapes, and text, supporting constrained resizing and rotation disabling where applicable.
- Implemented a unified interaction extension framework enabling configurable resize and rotate lifecycle handlers.
- Enhanced autocomplete overlay behavior based on selection context.
- **Refactor**
- Removed legacy resize manager and element-specific resize/rotate logic, replacing with a centralized, extensible interaction system.
- Simplified resize handle rendering to a data-driven approach with improved cursor management.
- Replaced complex cursor rotation calculations with fixed-angle mappings for resize handles.
- Streamlined selection rectangle component to use interactivity services for resize and rotate handling.
- **Bug Fixes**
- Fixed connector update triggers to reduce unnecessary updates.
- Improved resize constraints enforcement and interaction state tracking.
- **Tests**
- Refined end-to-end tests to use higher-level resize utilities and added finer-grained assertions on element dimensions.
- Enhanced mouse movement granularity in drag tests for better simulation fidelity.
- **Chores**
- Added new workspace dependencies and project references for the interaction framework modules.
- Extended public API exports to include new interaction types and extensions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
import { AttachmentBlockStyles } from '@blocksuite/affine-model';
|
import {
|
||||||
|
AttachmentBlockSchema,
|
||||||
|
AttachmentBlockStyles,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EMBED_CARD_HEIGHT,
|
EMBED_CARD_HEIGHT,
|
||||||
EMBED_CARD_WIDTH,
|
EMBED_CARD_WIDTH,
|
||||||
} from '@blocksuite/affine-shared/consts';
|
} from '@blocksuite/affine-shared/consts';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { AttachmentBlockComponent } from './attachment-block.js';
|
import { AttachmentBlockComponent } from './attachment-block.js';
|
||||||
@@ -48,3 +52,21 @@ declare global {
|
|||||||
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
|
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const AttachmentBlockInteraction = GfxViewInteractionExtension(
|
||||||
|
AttachmentBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
lockRatio: true,
|
||||||
|
},
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate: context => {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
|||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
|
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
|
||||||
|
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
|
||||||
import { AttachmentDropOption } from './attachment-service.js';
|
import { AttachmentDropOption } from './attachment-service.js';
|
||||||
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
@@ -26,6 +27,7 @@ export const AttachmentBlockSpec: ExtensionType[] = [
|
|||||||
AttachmentEmbedConfigExtension(),
|
AttachmentEmbedConfigExtension(),
|
||||||
AttachmentEmbedService,
|
AttachmentEmbedService,
|
||||||
AttachmentBlockAdapterExtensions,
|
AttachmentBlockAdapterExtensions,
|
||||||
|
AttachmentBlockInteraction,
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
|
|||||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
|
||||||
import { AttachmentDropOption } from './attachment-service.js';
|
import { AttachmentDropOption } from './attachment-service.js';
|
||||||
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
@@ -44,6 +45,7 @@ export class AttachmentViewExtension extends ViewExtensionProvider {
|
|||||||
]);
|
]);
|
||||||
if (this.isEdgeless(context.scope)) {
|
if (this.isEdgeless(context.scope)) {
|
||||||
context.register(EdgelessClipboardAttachmentConfig);
|
context.register(EdgelessClipboardAttachmentConfig);
|
||||||
|
context.register(AttachmentBlockInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EMBED_CARD_HEIGHT,
|
EMBED_CARD_HEIGHT,
|
||||||
EMBED_CARD_WIDTH,
|
EMBED_CARD_WIDTH,
|
||||||
} from '@blocksuite/affine-shared/consts';
|
} from '@blocksuite/affine-shared/consts';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { BookmarkBlockComponent } from './bookmark-block.js';
|
import { BookmarkBlockComponent } from './bookmark-block.js';
|
||||||
@@ -50,6 +52,24 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const BookmarkBlockInteraction = GfxViewInteractionExtension(
|
||||||
|
BookmarkBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
lockRatio: true,
|
||||||
|
},
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
|
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
|||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
import { BookmarkBlockAdapterExtensions } from './adapters/extension';
|
import { BookmarkBlockAdapterExtensions } from './adapters/extension';
|
||||||
|
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
|
||||||
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
|
||||||
@@ -16,6 +17,7 @@ export const BookmarkBlockSpec: ExtensionType[] = [
|
|||||||
? literal`affine-edgeless-bookmark`
|
? literal`affine-edgeless-bookmark`
|
||||||
: literal`affine-bookmark`;
|
: literal`affine-bookmark`;
|
||||||
}),
|
}),
|
||||||
|
BookmarkBlockInteraction,
|
||||||
BookmarkBlockAdapterExtensions,
|
BookmarkBlockAdapterExtensions,
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
BookmarkSlashMenuConfigExtension,
|
BookmarkSlashMenuConfigExtension,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
|||||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
|
||||||
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config';
|
import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config';
|
||||||
@@ -36,6 +37,7 @@ export class BookmarkViewExtension extends ViewExtensionProvider {
|
|||||||
const isEdgeless = this.isEdgeless(context.scope);
|
const isEdgeless = this.isEdgeless(context.scope);
|
||||||
if (isEdgeless) {
|
if (isEdgeless) {
|
||||||
context.register(EdgelessClipboardBookmarkConfig);
|
context.register(EdgelessClipboardBookmarkConfig);
|
||||||
|
context.register(BookmarkBlockInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
EDGELESS_TEXT_BLOCK_MIN_HEIGHT,
|
EDGELESS_TEXT_BLOCK_MIN_HEIGHT,
|
||||||
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
||||||
type EdgelessTextBlockModel,
|
type EdgelessTextBlockModel,
|
||||||
|
EdgelessTextBlockSchema,
|
||||||
ListBlockModel,
|
ListBlockModel,
|
||||||
ParagraphBlockModel,
|
ParagraphBlockModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
@@ -21,7 +22,10 @@ import {
|
|||||||
GfxBlockComponent,
|
GfxBlockComponent,
|
||||||
TextSelection,
|
TextSelection,
|
||||||
} from '@blocksuite/std';
|
} from '@blocksuite/std';
|
||||||
import type { SelectedContext } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
type SelectedContext,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { css, html } from 'lit';
|
import { css, html } from 'lit';
|
||||||
import { query, state } from 'lit/decorators.js';
|
import { query, state } from 'lit/decorators.js';
|
||||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
@@ -420,3 +424,69 @@ declare global {
|
|||||||
'affine-edgeless-text': EdgelessTextBlockComponent;
|
'affine-edgeless-text': EdgelessTextBlockComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EdgelessTextInteraction =
|
||||||
|
GfxViewInteractionExtension<EdgelessTextBlockComponent>(
|
||||||
|
EdgelessTextBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
lockRatio: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
|
allowedHandlers: [
|
||||||
|
'top-left',
|
||||||
|
'top-right',
|
||||||
|
'left',
|
||||||
|
'right',
|
||||||
|
'bottom-left',
|
||||||
|
'bottom-right',
|
||||||
|
],
|
||||||
|
minWidth: EDGELESS_TEXT_BLOCK_MIN_WIDTH,
|
||||||
|
},
|
||||||
|
handleResize: context => {
|
||||||
|
const { model, view } = context;
|
||||||
|
const initialScale = model.props.scale;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('scale');
|
||||||
|
model.stash('hasMaxWidth');
|
||||||
|
},
|
||||||
|
onResizeMove(context) {
|
||||||
|
const { originalBound, newBound, constraint, lockRatio } = context;
|
||||||
|
|
||||||
|
if (lockRatio) {
|
||||||
|
const originalRealWidth = originalBound.w / initialScale;
|
||||||
|
const newScale = newBound.w / originalRealWidth;
|
||||||
|
|
||||||
|
model.props.scale = newScale;
|
||||||
|
model.props.xywh = newBound.serialize();
|
||||||
|
} else {
|
||||||
|
if (!view.checkWidthOverflow(newBound.w)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRealWidth = clamp(
|
||||||
|
newBound.w / initialScale,
|
||||||
|
constraint.minWidth,
|
||||||
|
constraint.maxWidth
|
||||||
|
);
|
||||||
|
|
||||||
|
const curBound = Bound.deserialize(model.xywh);
|
||||||
|
|
||||||
|
model.props.xywh = Bound.serialize({
|
||||||
|
...newBound,
|
||||||
|
w: newRealWidth * initialScale,
|
||||||
|
h: curBound.h,
|
||||||
|
});
|
||||||
|
model.props.hasMaxWidth = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResizeEnd(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('scale');
|
||||||
|
model.pop('hasMaxWidth');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { BlockViewExtension } from '@blocksuite/std';
|
|||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import { EdgelessTextInteraction } from './edgeless-text-block';
|
||||||
|
|
||||||
export const EdgelessTextBlockSpec: ExtensionType[] = [
|
export const EdgelessTextBlockSpec: ExtensionType[] = [
|
||||||
BlockViewExtension('affine:edgeless-text', literal`affine-edgeless-text`),
|
BlockViewExtension('affine:edgeless-text', literal`affine-edgeless-text`),
|
||||||
|
EdgelessTextInteraction,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { BlockViewExtension } from '@blocksuite/std';
|
|||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
import { EdgelessClipboardEdgelessTextConfig } from './edgeless-clipboard-config';
|
import { EdgelessClipboardEdgelessTextConfig } from './edgeless-clipboard-config';
|
||||||
|
import { EdgelessTextInteraction } from './edgeless-text-block';
|
||||||
import { edgelessTextToolbarExtension } from './edgeless-toolbar';
|
import { edgelessTextToolbarExtension } from './edgeless-toolbar';
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export class EdgelessTextViewExtension extends ViewExtensionProvider {
|
|||||||
]);
|
]);
|
||||||
context.register(edgelessTextToolbarExtension);
|
context.register(edgelessTextToolbarExtension);
|
||||||
context.register(EdgelessClipboardEdgelessTextConfig);
|
context.register(EdgelessClipboardEdgelessTextConfig);
|
||||||
|
context.register(EdgelessTextInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { toEdgelessEmbedBlock } from '@blocksuite/affine-block-embed';
|
import {
|
||||||
|
createEmbedEdgelessBlockInteraction,
|
||||||
|
toEdgelessEmbedBlock,
|
||||||
|
} from '@blocksuite/affine-block-embed';
|
||||||
import {
|
import {
|
||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
reassociateConnectorsCommand,
|
reassociateConnectorsCommand,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
|
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EMBED_CARD_HEIGHT,
|
EMBED_CARD_HEIGHT,
|
||||||
EMBED_CARD_WIDTH,
|
EMBED_CARD_WIDTH,
|
||||||
@@ -61,3 +65,7 @@ export class EmbedEdgelessLinkedDocBlockComponent extends toEdgelessEmbedBlock(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmbedLinkedDocInteraction = createEmbedEdgelessBlockInteraction(
|
||||||
|
EmbedLinkedDocBlockSchema.model.flavour
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedLinkedDocBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
|
import { LinkedDocSlashMenuConfigExtension } from './configs/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { EmbedLinkedDocInteraction } from './embed-edgeless-linked-doc-block';
|
||||||
|
|
||||||
const flavour = EmbedLinkedDocBlockSchema.model.flavour;
|
const flavour = EmbedLinkedDocBlockSchema.model.flavour;
|
||||||
|
|
||||||
@@ -27,5 +28,6 @@ export const EmbedLinkedDocViewExtensions: ExtensionType[] = [
|
|||||||
: literal`affine-embed-linked-doc-block`;
|
: literal`affine-embed-linked-doc-block`;
|
||||||
}),
|
}),
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
|
EmbedLinkedDocInteraction,
|
||||||
LinkedDocSlashMenuConfigExtension,
|
LinkedDocSlashMenuConfigExtension,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export * from './adapters';
|
|||||||
export * from './commands';
|
export * from './commands';
|
||||||
export { LinkedDocSlashMenuConfigIdentifier } from './configs/slash-menu';
|
export { LinkedDocSlashMenuConfigIdentifier } from './configs/slash-menu';
|
||||||
export * from './edgeless-clipboard-config';
|
export * from './edgeless-clipboard-config';
|
||||||
|
export * from './embed-edgeless-linked-doc-block';
|
||||||
export * from './embed-linked-doc-block';
|
export * from './embed-linked-doc-block';
|
||||||
export * from './embed-linked-doc-spec';
|
export * from './embed-linked-doc-spec';
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import {
|
|||||||
EdgelessCRUDIdentifier,
|
EdgelessCRUDIdentifier,
|
||||||
reassociateConnectorsCommand,
|
reassociateConnectorsCommand,
|
||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import type { AliasInfo } from '@blocksuite/affine-model';
|
import {
|
||||||
|
type AliasInfo,
|
||||||
|
EmbedSyncedDocBlockSchema,
|
||||||
|
SYNCED_MIN_HEIGHT,
|
||||||
|
SYNCED_MIN_WIDTH,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import {
|
import {
|
||||||
EMBED_CARD_HEIGHT,
|
EMBED_CARD_HEIGHT,
|
||||||
EMBED_CARD_WIDTH,
|
EMBED_CARD_WIDTH,
|
||||||
@@ -12,8 +17,9 @@ import {
|
|||||||
ThemeExtensionIdentifier,
|
ThemeExtensionIdentifier,
|
||||||
ThemeProvider,
|
ThemeProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound, clamp } from '@blocksuite/global/gfx';
|
||||||
import { type BlockComponent, BlockStdScope } from '@blocksuite/std';
|
import { type BlockComponent, BlockStdScope } from '@blocksuite/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { html, nothing } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
import { query, queryAsync } from 'lit/decorators.js';
|
import { query, queryAsync } from 'lit/decorators.js';
|
||||||
import { choose } from 'lit/directives/choose.js';
|
import { choose } from 'lit/directives/choose.js';
|
||||||
@@ -199,3 +205,60 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
|
|||||||
|
|
||||||
override accessor useCaptionEditor = true;
|
override accessor useCaptionEditor = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmbedSyncedDocInteraction =
|
||||||
|
GfxViewInteractionExtension<EmbedEdgelessSyncedDocBlockComponent>(
|
||||||
|
EmbedSyncedDocBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
minWidth: SYNCED_MIN_WIDTH,
|
||||||
|
minHeight: SYNCED_MIN_HEIGHT,
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: ({ model }) => {
|
||||||
|
const initialScale = model.props.scale ?? 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart: context => {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('scale');
|
||||||
|
},
|
||||||
|
onResizeMove: context => {
|
||||||
|
const { lockRatio, originalBound, constraint, newBound } = context;
|
||||||
|
|
||||||
|
let scale = initialScale;
|
||||||
|
const realWidth = originalBound.w / initialScale;
|
||||||
|
|
||||||
|
if (lockRatio) {
|
||||||
|
scale = newBound.w / realWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = newBound.w / scale;
|
||||||
|
|
||||||
|
newBound.w =
|
||||||
|
clamp(newWidth, constraint.minWidth, constraint.maxWidth) * scale;
|
||||||
|
newBound.h =
|
||||||
|
clamp(newBound.h, constraint.minHeight, constraint.maxHeight) *
|
||||||
|
scale;
|
||||||
|
|
||||||
|
model.props.scale = scale;
|
||||||
|
model.xywh = newBound.serialize();
|
||||||
|
},
|
||||||
|
onResizeEnd: context => {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('scale');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
|
|
||||||
import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedSyncedDocBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { EmbedSyncedDocInteraction } from './embed-edgeless-synced-doc-block';
|
||||||
import { HeightInitializationExtension } from './init-height-extension';
|
import { HeightInitializationExtension } from './init-height-extension';
|
||||||
|
|
||||||
const flavour = EmbedSyncedDocBlockSchema.model.flavour;
|
const flavour = EmbedSyncedDocBlockSchema.model.flavour;
|
||||||
@@ -29,4 +30,5 @@ export const EmbedSyncedDocViewExtensions: ExtensionType[] = [
|
|||||||
}),
|
}),
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
HeightInitializationExtension,
|
HeightInitializationExtension,
|
||||||
|
EmbedSyncedDocInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from './adapters';
|
|||||||
export * from './commands';
|
export * from './commands';
|
||||||
export * from './configs';
|
export * from './configs';
|
||||||
export * from './edgeless-clipboard-config';
|
export * from './edgeless-clipboard-config';
|
||||||
|
export * from './embed-edgeless-synced-doc-block';
|
||||||
export * from './embed-synced-doc-block';
|
export * from './embed-synced-doc-block';
|
||||||
export * from './embed-synced-doc-spec';
|
export * from './embed-synced-doc-spec';
|
||||||
export { SYNCED_MIN_HEIGHT, SYNCED_MIN_WIDTH } from '@blocksuite/affine-model';
|
export { SYNCED_MIN_HEIGHT, SYNCED_MIN_WIDTH } from '@blocksuite/affine-model';
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedLinkedDocConfig,
|
EdgelessClipboardEmbedLinkedDocConfig,
|
||||||
|
EmbedLinkedDocInteraction,
|
||||||
EmbedLinkedDocViewExtensions,
|
EmbedLinkedDocViewExtensions,
|
||||||
} from './embed-linked-doc-block';
|
} from './embed-linked-doc-block';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedSyncedDocConfig,
|
EdgelessClipboardEmbedSyncedDocConfig,
|
||||||
|
EmbedSyncedDocInteraction,
|
||||||
EmbedSyncedDocViewExtensions,
|
EmbedSyncedDocViewExtensions,
|
||||||
} from './embed-synced-doc-block';
|
} from './embed-synced-doc-block';
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ export class EmbedDocViewExtension extends ViewExtensionProvider {
|
|||||||
context.register([
|
context.register([
|
||||||
EdgelessClipboardEmbedLinkedDocConfig,
|
EdgelessClipboardEmbedLinkedDocConfig,
|
||||||
EdgelessClipboardEmbedSyncedDocConfig,
|
EdgelessClipboardEmbedSyncedDocConfig,
|
||||||
|
EmbedLinkedDocInteraction,
|
||||||
|
EmbedSyncedDocInteraction,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ import {
|
|||||||
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { findAncestorModel } from '@blocksuite/affine-shared/utils';
|
import { findAncestorModel } from '@blocksuite/affine-shared/utils';
|
||||||
import type { BlockService } from '@blocksuite/std';
|
import type { BlockService } from '@blocksuite/std';
|
||||||
import type { GfxCompatibleProps } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
type GfxCompatibleProps,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
type ResizeConstraint,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import type { BlockModel } from '@blocksuite/store';
|
import type { BlockModel } from '@blocksuite/store';
|
||||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||||
import type { TemplateResult } from 'lit';
|
import type { TemplateResult } from 'lit';
|
||||||
@@ -163,3 +167,31 @@ export class EmbedBlockComponent<
|
|||||||
|
|
||||||
override accessor useZeroWidth = true;
|
override accessor useZeroWidth = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createEmbedEdgelessBlockInteraction = (
|
||||||
|
flavour: string,
|
||||||
|
config?: {
|
||||||
|
resizeConstraint?: ResizeConstraint;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const resizeConstraint = Object.assign(
|
||||||
|
{
|
||||||
|
lockRatio: true,
|
||||||
|
},
|
||||||
|
config?.resizeConstraint ?? {}
|
||||||
|
);
|
||||||
|
const rotateConstraint = {
|
||||||
|
rotatable: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return GfxViewInteractionExtension(flavour, {
|
||||||
|
resizeConstraint,
|
||||||
|
handleRotate() {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set(rotateConstraint);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { EmbedFigmaBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
|
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
|
||||||
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
||||||
import { EmbedFigmaBlockComponent } from './embed-figma-block.js';
|
import { EmbedFigmaBlockComponent } from './embed-figma-block.js';
|
||||||
|
|
||||||
export class EmbedEdgelessBlockComponent extends toEdgelessEmbedBlock(
|
export class EmbedEdgelessBlockComponent extends toEdgelessEmbedBlock(
|
||||||
EmbedFigmaBlockComponent
|
EmbedFigmaBlockComponent
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export const EmbedFigmaBlockInteraction = createEmbedEdgelessBlockInteraction(
|
||||||
|
EmbedFigmaBlockSchema.model.flavour
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
||||||
import { EmbedFigmaBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedFigmaBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { embedFigmaSlashMenuConfig } from './configs/slash-menu';
|
import { embedFigmaSlashMenuConfig } from './configs/slash-menu';
|
||||||
|
import { EmbedFigmaBlockInteraction } from './embed-edgeless-figma-block';
|
||||||
import { EmbedFigmaBlockComponent } from './embed-figma-block';
|
import { EmbedFigmaBlockComponent } from './embed-figma-block';
|
||||||
import { EmbedFigmaBlockOptionConfig } from './embed-figma-service';
|
import { EmbedFigmaBlockOptionConfig } from './embed-figma-service';
|
||||||
|
|
||||||
@@ -35,4 +36,5 @@ export const EmbedFigmaViewExtensions: ExtensionType[] = [
|
|||||||
EmbedFigmaBlockOptionConfig,
|
EmbedFigmaBlockOptionConfig,
|
||||||
createBuiltinToolbarConfigExtension(flavour, EmbedFigmaBlockComponent),
|
createBuiltinToolbarConfigExtension(flavour, EmbedFigmaBlockComponent),
|
||||||
SlashMenuConfigExtension(flavour, embedFigmaSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, embedFigmaSlashMenuConfig),
|
||||||
|
EmbedFigmaBlockInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { EmbedGithubBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
|
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
|
||||||
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
||||||
import { EmbedGithubBlockComponent } from './embed-github-block.js';
|
import { EmbedGithubBlockComponent } from './embed-github-block.js';
|
||||||
|
|
||||||
export class EmbedEdgelessGithubBlockComponent extends toEdgelessEmbedBlock(
|
export class EmbedEdgelessGithubBlockComponent extends toEdgelessEmbedBlock(
|
||||||
EmbedGithubBlockComponent
|
EmbedGithubBlockComponent
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export const EmbedGithubBlockInteraction = createEmbedEdgelessBlockInteraction(
|
||||||
|
EmbedGithubBlockSchema.model.flavour
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
||||||
import { EmbedGithubBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedGithubBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { embedGithubSlashMenuConfig } from './configs/slash-menu';
|
import { embedGithubSlashMenuConfig } from './configs/slash-menu';
|
||||||
|
import { EmbedGithubBlockInteraction } from './embed-edgeless-github-block';
|
||||||
import { EmbedGithubBlockComponent } from './embed-github-block';
|
import { EmbedGithubBlockComponent } from './embed-github-block';
|
||||||
import {
|
import {
|
||||||
EmbedGithubBlockOptionConfig,
|
EmbedGithubBlockOptionConfig,
|
||||||
@@ -38,6 +39,7 @@ export const EmbedGithubViewExtensions: ExtensionType[] = [
|
|||||||
: literal`affine-embed-github-block`;
|
: literal`affine-embed-github-block`;
|
||||||
}),
|
}),
|
||||||
EmbedGithubBlockOptionConfig,
|
EmbedGithubBlockOptionConfig,
|
||||||
|
EmbedGithubBlockInteraction,
|
||||||
createBuiltinToolbarConfigExtension(flavour, EmbedGithubBlockComponent),
|
createBuiltinToolbarConfigExtension(flavour, EmbedGithubBlockComponent),
|
||||||
SlashMenuConfigExtension(flavour, embedGithubSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, embedGithubSlashMenuConfig),
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
|
import { EmbedHtmlBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
|
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
|
||||||
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
||||||
import { EmbedHtmlBlockComponent } from './embed-html-block.js';
|
import { EmbedHtmlBlockComponent } from './embed-html-block.js';
|
||||||
|
import { EMBED_HTML_MIN_HEIGHT, EMBED_HTML_MIN_WIDTH } from './styles.js';
|
||||||
|
|
||||||
export class EmbedEdgelessHtmlBlockComponent extends toEdgelessEmbedBlock(
|
export class EmbedEdgelessHtmlBlockComponent extends toEdgelessEmbedBlock(
|
||||||
EmbedHtmlBlockComponent
|
EmbedHtmlBlockComponent
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export const EmbedEdgelessHtmlBlockInteraction =
|
||||||
|
createEmbedEdgelessBlockInteraction(EmbedHtmlBlockSchema.model.flavour, {
|
||||||
|
resizeConstraint: {
|
||||||
|
minWidth: EMBED_HTML_MIN_WIDTH,
|
||||||
|
minHeight: EMBED_HTML_MIN_HEIGHT,
|
||||||
|
lockRatio: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ExtensionType } from '@blocksuite/store';
|
|||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { EmbedEdgelessHtmlBlockInteraction } from './embed-edgeless-html-block';
|
||||||
|
|
||||||
const flavour = EmbedHtmlBlockSchema.model.flavour;
|
const flavour = EmbedHtmlBlockSchema.model.flavour;
|
||||||
|
|
||||||
@@ -23,4 +24,5 @@ export const EmbedHtmlViewExtensions: ExtensionType[] = [
|
|||||||
: literal`affine-embed-html-block`;
|
: literal`affine-embed-html-block`;
|
||||||
}),
|
}),
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
|
EmbedEdgelessHtmlBlockInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { EmbedIframeBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
import { Bound, clamp } from '@blocksuite/global/gfx';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { html } from 'lit/static-html.js';
|
import { html } from 'lit/static-html.js';
|
||||||
|
|
||||||
@@ -53,3 +55,65 @@ export class EmbedEdgelessIframeBlockComponent extends toGfxBlockComponent(
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EmbedIframeInteraction =
|
||||||
|
GfxViewInteractionExtension<EmbedEdgelessIframeBlockComponent>(
|
||||||
|
EmbedIframeBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
minWidth: 218,
|
||||||
|
minHeight: 44,
|
||||||
|
maxWidth: 3400,
|
||||||
|
maxHeight: 2200,
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize: context => {
|
||||||
|
const { model } = context;
|
||||||
|
const initialScale = model.props.scale$.peek();
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('scale');
|
||||||
|
},
|
||||||
|
onResizeMove(context) {
|
||||||
|
const { newBound, originalBound, lockRatio, constraint } = context;
|
||||||
|
const { minWidth, maxWidth, minHeight, maxHeight } = constraint;
|
||||||
|
|
||||||
|
let scale = initialScale;
|
||||||
|
const originalRealWidth = originalBound.w / scale;
|
||||||
|
|
||||||
|
// update scale if resize is proportional
|
||||||
|
if (lockRatio) {
|
||||||
|
scale = newBound.w / originalRealWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRealWidth = clamp(newBound.w / scale, minWidth, maxWidth);
|
||||||
|
let newRealHeight = clamp(newBound.h / scale, minHeight, maxHeight);
|
||||||
|
|
||||||
|
newBound.w = newRealWidth * scale;
|
||||||
|
newBound.h = newRealHeight * scale;
|
||||||
|
|
||||||
|
model.props.xywh = newBound.serialize();
|
||||||
|
if (scale !== initialScale) {
|
||||||
|
model.props.scale = scale;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResizeEnd(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('scale');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { EmbedIframeBlockAdapterExtensions } from './adapters';
|
import { EmbedIframeBlockAdapterExtensions } from './adapters';
|
||||||
import { embedIframeSlashMenuConfig } from './configs/slash-menu/slash-menu';
|
import { embedIframeSlashMenuConfig } from './configs/slash-menu/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { EmbedIframeInteraction } from './embed-edgeless-iframe-block';
|
||||||
|
|
||||||
const flavour = EmbedIframeBlockSchema.model.flavour;
|
const flavour = EmbedIframeBlockSchema.model.flavour;
|
||||||
|
|
||||||
@@ -31,4 +32,5 @@ export const EmbedIframeViewExtensions: ExtensionType[] = [
|
|||||||
}),
|
}),
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
SlashMenuConfigExtension(flavour, embedIframeSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, embedIframeSlashMenuConfig),
|
||||||
|
EmbedIframeInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { EmbedLoomBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
|
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
|
||||||
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
||||||
import { EmbedLoomBlockComponent } from './embed-loom-block.js';
|
import { EmbedLoomBlockComponent } from './embed-loom-block.js';
|
||||||
|
|
||||||
export class EmbedEdgelessLoomBlockComponent extends toEdgelessEmbedBlock(
|
export class EmbedEdgelessLoomBlockComponent extends toEdgelessEmbedBlock(
|
||||||
EmbedLoomBlockComponent
|
EmbedLoomBlockComponent
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export const EmbedLoomBlockInteraction = createEmbedEdgelessBlockInteraction(
|
||||||
|
EmbedLoomBlockSchema.model.flavour
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
||||||
import { EmbedLoomBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedLoomBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { embedLoomSlashMenuConfig } from './configs/slash-menu';
|
import { embedLoomSlashMenuConfig } from './configs/slash-menu';
|
||||||
|
import { EmbedLoomBlockInteraction } from './embed-edgeless-loom-bock';
|
||||||
import { EmbedLoomBlockComponent } from './embed-loom-block';
|
import { EmbedLoomBlockComponent } from './embed-loom-block';
|
||||||
import {
|
import {
|
||||||
EmbedLoomBlockOptionConfig,
|
EmbedLoomBlockOptionConfig,
|
||||||
@@ -40,4 +41,5 @@ export const EmbedLoomViewExtensions: ExtensionType[] = [
|
|||||||
EmbedLoomBlockOptionConfig,
|
EmbedLoomBlockOptionConfig,
|
||||||
createBuiltinToolbarConfigExtension(flavour, EmbedLoomBlockComponent),
|
createBuiltinToolbarConfigExtension(flavour, EmbedLoomBlockComponent),
|
||||||
SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, embedLoomSlashMenuConfig),
|
||||||
|
EmbedLoomBlockInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
|
import { EmbedYoutubeBlockSchema } from '@blocksuite/affine-model';
|
||||||
|
|
||||||
|
import { createEmbedEdgelessBlockInteraction } from '../common/embed-block-element.js';
|
||||||
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
import { toEdgelessEmbedBlock } from '../common/to-edgeless-embed-block.js';
|
||||||
import { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
|
import { EmbedYoutubeBlockComponent } from './embed-youtube-block.js';
|
||||||
|
|
||||||
export class EmbedEdgelessYoutubeBlockComponent extends toEdgelessEmbedBlock(
|
export class EmbedEdgelessYoutubeBlockComponent extends toEdgelessEmbedBlock(
|
||||||
EmbedYoutubeBlockComponent
|
EmbedYoutubeBlockComponent
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
export const EmbedYoutubeBlockInteraction = createEmbedEdgelessBlockInteraction(
|
||||||
|
EmbedYoutubeBlockSchema.model.flavour
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from '../configs/toolbar';
|
||||||
import { EmbedYoutubeBlockAdapterExtensions } from './adapters/extension';
|
import { EmbedYoutubeBlockAdapterExtensions } from './adapters/extension';
|
||||||
import { embedYoutubeSlashMenuConfig } from './configs/slash-menu';
|
import { embedYoutubeSlashMenuConfig } from './configs/slash-menu';
|
||||||
|
import { EmbedYoutubeBlockInteraction } from './embed-edgeless-youtube-block';
|
||||||
import { EmbedYoutubeBlockComponent } from './embed-youtube-block';
|
import { EmbedYoutubeBlockComponent } from './embed-youtube-block';
|
||||||
import {
|
import {
|
||||||
EmbedYoutubeBlockOptionConfig,
|
EmbedYoutubeBlockOptionConfig,
|
||||||
@@ -40,4 +41,5 @@ export const EmbedYoutubeViewExtensions: ExtensionType[] = [
|
|||||||
EmbedYoutubeBlockOptionConfig,
|
EmbedYoutubeBlockOptionConfig,
|
||||||
createBuiltinToolbarConfigExtension(flavour, EmbedYoutubeBlockComponent),
|
createBuiltinToolbarConfigExtension(flavour, EmbedYoutubeBlockComponent),
|
||||||
SlashMenuConfigExtension('affine:embed-youtube', embedYoutubeSlashMenuConfig),
|
SlashMenuConfigExtension('affine:embed-youtube', embedYoutubeSlashMenuConfig),
|
||||||
|
EmbedYoutubeBlockInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ export const EmbedExtensions: ExtensionType[] = [
|
|||||||
export { createEmbedBlockHtmlAdapterMatcher } from './common/adapters/html';
|
export { createEmbedBlockHtmlAdapterMatcher } from './common/adapters/html';
|
||||||
export { createEmbedBlockMarkdownAdapterMatcher } from './common/adapters/markdown';
|
export { createEmbedBlockMarkdownAdapterMatcher } from './common/adapters/markdown';
|
||||||
export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text';
|
export { createEmbedBlockPlainTextAdapterMatcher } from './common/adapters/plain-text';
|
||||||
export { EmbedBlockComponent } from './common/embed-block-element';
|
export {
|
||||||
|
createEmbedEdgelessBlockInteraction,
|
||||||
|
EmbedBlockComponent,
|
||||||
|
} from './common/embed-block-element';
|
||||||
export * from './common/embed-note-content-styles';
|
export * from './common/embed-note-content-styles';
|
||||||
export { insertEmbedCard } from './common/insert-embed-card';
|
export { insertEmbedCard } from './common/insert-embed-card';
|
||||||
export * from './common/render-linked-doc';
|
export * from './common/render-linked-doc';
|
||||||
|
|||||||
@@ -8,26 +8,32 @@ import {
|
|||||||
EdgelessClipboardEmbedFigmaConfig,
|
EdgelessClipboardEmbedFigmaConfig,
|
||||||
EmbedFigmaViewExtensions,
|
EmbedFigmaViewExtensions,
|
||||||
} from './embed-figma-block';
|
} from './embed-figma-block';
|
||||||
|
import { EmbedFigmaBlockInteraction } from './embed-figma-block/embed-edgeless-figma-block';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedGithubConfig,
|
EdgelessClipboardEmbedGithubConfig,
|
||||||
EmbedGithubViewExtensions,
|
EmbedGithubViewExtensions,
|
||||||
} from './embed-github-block';
|
} from './embed-github-block';
|
||||||
|
import { EmbedGithubBlockInteraction } from './embed-github-block/embed-edgeless-github-block';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedHtmlConfig,
|
EdgelessClipboardEmbedHtmlConfig,
|
||||||
EmbedHtmlViewExtensions,
|
EmbedHtmlViewExtensions,
|
||||||
} from './embed-html-block';
|
} from './embed-html-block';
|
||||||
|
import { EmbedEdgelessHtmlBlockInteraction } from './embed-html-block/embed-edgeless-html-block';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedIframeConfig,
|
EdgelessClipboardEmbedIframeConfig,
|
||||||
EmbedIframeViewExtensions,
|
EmbedIframeViewExtensions,
|
||||||
} from './embed-iframe-block';
|
} from './embed-iframe-block';
|
||||||
|
import { EmbedIframeInteraction } from './embed-iframe-block/embed-edgeless-iframe-block';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedLoomConfig,
|
EdgelessClipboardEmbedLoomConfig,
|
||||||
EmbedLoomViewExtensions,
|
EmbedLoomViewExtensions,
|
||||||
} from './embed-loom-block';
|
} from './embed-loom-block';
|
||||||
|
import { EmbedLoomBlockInteraction } from './embed-loom-block/embed-edgeless-loom-bock';
|
||||||
import {
|
import {
|
||||||
EdgelessClipboardEmbedYoutubeConfig,
|
EdgelessClipboardEmbedYoutubeConfig,
|
||||||
EmbedYoutubeViewExtensions,
|
EmbedYoutubeViewExtensions,
|
||||||
} from './embed-youtube-block';
|
} from './embed-youtube-block';
|
||||||
|
import { EmbedYoutubeBlockInteraction } from './embed-youtube-block/embed-edgeless-youtube-block';
|
||||||
|
|
||||||
export class EmbedViewExtension extends ViewExtensionProvider {
|
export class EmbedViewExtension extends ViewExtensionProvider {
|
||||||
override name = 'affine-embed-block';
|
override name = 'affine-embed-block';
|
||||||
@@ -54,6 +60,12 @@ export class EmbedViewExtension extends ViewExtensionProvider {
|
|||||||
EdgelessClipboardEmbedLoomConfig,
|
EdgelessClipboardEmbedLoomConfig,
|
||||||
EdgelessClipboardEmbedYoutubeConfig,
|
EdgelessClipboardEmbedYoutubeConfig,
|
||||||
EdgelessClipboardEmbedIframeConfig,
|
EdgelessClipboardEmbedIframeConfig,
|
||||||
|
EmbedFigmaBlockInteraction,
|
||||||
|
EmbedGithubBlockInteraction,
|
||||||
|
EmbedEdgelessHtmlBlockInteraction,
|
||||||
|
EmbedLoomBlockInteraction,
|
||||||
|
EmbedYoutubeBlockInteraction,
|
||||||
|
EmbedIframeInteraction,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { DefaultTheme, type FrameBlockModel } from '@blocksuite/affine-model';
|
import { OverlayIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
|
import {
|
||||||
|
DefaultTheme,
|
||||||
|
type FrameBlockModel,
|
||||||
|
FrameBlockSchema,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import { GfxBlockComponent } from '@blocksuite/std';
|
import { GfxBlockComponent } from '@blocksuite/std';
|
||||||
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
type BoxSelectionContext,
|
||||||
|
getTopElements,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
type SelectedContext,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { state } from 'lit/decorators.js';
|
import { state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
EdgelessFrameManagerIdentifier,
|
||||||
|
type FrameOverlay,
|
||||||
|
} from './frame-manager';
|
||||||
|
|
||||||
export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
export class FrameBlockComponent extends GfxBlockComponent<FrameBlockModel> {
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
@@ -115,3 +130,64 @@ declare global {
|
|||||||
'affine-frame': FrameBlockComponent;
|
'affine-frame': FrameBlockComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const FrameBlockInteraction =
|
||||||
|
GfxViewInteractionExtension<FrameBlockComponent>(
|
||||||
|
FrameBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
handleResize: context => {
|
||||||
|
const { model, std } = context;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart(context): void {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('childElementIds');
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeMove(context): void {
|
||||||
|
const { newBound } = context;
|
||||||
|
const frameManager = std.getOptional(
|
||||||
|
EdgelessFrameManagerIdentifier
|
||||||
|
);
|
||||||
|
const overlay = std.getOptional(
|
||||||
|
OverlayIdentifier('frame')
|
||||||
|
) as FrameOverlay;
|
||||||
|
|
||||||
|
model.xywh = newBound.serialize();
|
||||||
|
|
||||||
|
if (!frameManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldChildren = frameManager.getChildElementsInFrame(model);
|
||||||
|
|
||||||
|
const newChildren = getTopElements(
|
||||||
|
frameManager.getElementsInFrameBound(model)
|
||||||
|
).concat(
|
||||||
|
oldChildren.filter(oldChild => {
|
||||||
|
return model.intersectsBound(oldChild.elementBound);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
frameManager.removeAllChildrenFromFrame(model);
|
||||||
|
frameManager.addElementsToFrame(model, newChildren);
|
||||||
|
|
||||||
|
overlay?.highlight(model, true, false);
|
||||||
|
},
|
||||||
|
onResizeEnd(context): void {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('childElementIds');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate(context): void {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BlockViewExtension } from '@blocksuite/std';
|
|||||||
import type { ExtensionType } from '@blocksuite/store';
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
import { literal } from 'lit/static-html.js';
|
import { literal } from 'lit/static-html.js';
|
||||||
|
|
||||||
|
import { FrameBlockInteraction } from './frame-block';
|
||||||
import { EdgelessFrameManager, FrameOverlay } from './frame-manager';
|
import { EdgelessFrameManager, FrameOverlay } from './frame-manager';
|
||||||
|
|
||||||
const flavour = FrameBlockSchema.model.flavour;
|
const flavour = FrameBlockSchema.model.flavour;
|
||||||
@@ -11,4 +12,5 @@ export const FrameBlockSpec: ExtensionType[] = [
|
|||||||
BlockViewExtension(flavour, literal`affine-frame`),
|
BlockViewExtension(flavour, literal`affine-frame`),
|
||||||
FrameOverlay,
|
FrameOverlay,
|
||||||
EdgelessFrameManager,
|
EdgelessFrameManager,
|
||||||
|
FrameBlockInteraction,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { EdgelessClipboardFrameConfig } from './edgeless-clipboard-config';
|
import { EdgelessClipboardFrameConfig } from './edgeless-clipboard-config';
|
||||||
import { frameQuickTool } from './edgeless-toolbar';
|
import { frameQuickTool } from './edgeless-toolbar';
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
|
import { FrameBlockInteraction } from './frame-block';
|
||||||
import { FrameHighlightManager } from './frame-highlight-manager';
|
import { FrameHighlightManager } from './frame-highlight-manager';
|
||||||
import { FrameBlockSpec } from './frame-spec';
|
import { FrameBlockSpec } from './frame-spec';
|
||||||
import { FrameTool } from './frame-tool';
|
import { FrameTool } from './frame-tool';
|
||||||
@@ -32,6 +33,7 @@ export class FrameViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(frameToolbarExtension);
|
context.register(frameToolbarExtension);
|
||||||
context.register(edgelessNavigatorBgWidget);
|
context.register(edgelessNavigatorBgWidget);
|
||||||
context.register(EdgelessClipboardFrameConfig);
|
context.register(EdgelessClipboardFrameConfig);
|
||||||
|
context.register(FrameBlockInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption';
|
|||||||
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
|
import { getLoadingIconWith } from '@blocksuite/affine-components/icons';
|
||||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||||
import { ResourceController } from '@blocksuite/affine-components/resource';
|
import { ResourceController } from '@blocksuite/affine-components/resource';
|
||||||
import type { ImageBlockModel } from '@blocksuite/affine-model';
|
import {
|
||||||
|
type ImageBlockModel,
|
||||||
|
ImageBlockSchema,
|
||||||
|
} from '@blocksuite/affine-model';
|
||||||
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||||
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
|
import { BrokenImageIcon, ImageIcon } from '@blocksuite/icons/lit';
|
||||||
import { GfxBlockComponent } from '@blocksuite/std';
|
import { GfxBlockComponent } from '@blocksuite/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { computed } from '@preact/signals-core';
|
import { computed } from '@preact/signals-core';
|
||||||
import { css, html } from 'lit';
|
import { css, html } from 'lit';
|
||||||
import { query } from 'lit/decorators.js';
|
import { query } from 'lit/decorators.js';
|
||||||
@@ -172,6 +176,15 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
accessor resizableImg!: HTMLDivElement;
|
accessor resizableImg!: HTMLDivElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ImageEdgelessBlockInteraction = GfxViewInteractionExtension(
|
||||||
|
ImageBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
lockRatio: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-edgeless-image': ImageEdgelessBlockComponent;
|
'affine-edgeless-image': ImageEdgelessBlockComponent;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { literal } from 'lit/static-html.js';
|
|||||||
|
|
||||||
import { imageSlashMenuConfig } from './configs/slash-menu';
|
import { imageSlashMenuConfig } from './configs/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { ImageEdgelessBlockInteraction } from './image-edgeless-block';
|
||||||
import { ImageDropOption } from './image-service';
|
import { ImageDropOption } from './image-service';
|
||||||
|
|
||||||
const flavour = ImageBlockSchema.model.flavour;
|
const flavour = ImageBlockSchema.model.flavour;
|
||||||
@@ -22,6 +23,7 @@ export const ImageBlockSpec: ExtensionType[] = [
|
|||||||
return literal`affine-image`;
|
return literal`affine-image`;
|
||||||
}),
|
}),
|
||||||
ImageDropOption,
|
ImageDropOption,
|
||||||
|
ImageEdgelessBlockInteraction,
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
SlashMenuConfigExtension(flavour, imageSlashMenuConfig),
|
SlashMenuConfigExtension(flavour, imageSlashMenuConfig),
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
|
|
||||||
import { EdgelessClipboardImageConfig } from './edgeless-clipboard-config';
|
import { EdgelessClipboardImageConfig } from './edgeless-clipboard-config';
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
|
import { ImageEdgelessBlockInteraction } from './image-edgeless-block';
|
||||||
import { ImageBlockSpec } from './image-spec';
|
import { ImageBlockSpec } from './image-spec';
|
||||||
|
|
||||||
export class ImageViewExtension extends ViewExtensionProvider {
|
export class ImageViewExtension extends ViewExtensionProvider {
|
||||||
@@ -20,6 +21,7 @@ export class ImageViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(ImageBlockSpec);
|
context.register(ImageBlockSpec);
|
||||||
if (this.isEdgeless(context.scope)) {
|
if (this.isEdgeless(context.scope)) {
|
||||||
context.register(EdgelessClipboardImageConfig);
|
context.register(EdgelessClipboardImageConfig);
|
||||||
|
context.register(ImageEdgelessBlockInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||||
import type { DocTitle } from '@blocksuite/affine-fragment-doc-title';
|
import type { DocTitle } from '@blocksuite/affine-fragment-doc-title';
|
||||||
import { NoteDisplayMode } from '@blocksuite/affine-model';
|
import { NoteBlockSchema, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||||
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
import { focusTextModel } from '@blocksuite/affine-rich-text';
|
||||||
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
import { EDGELESS_BLOCK_CHILD_PADDING } from '@blocksuite/affine-shared/consts';
|
||||||
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
|
||||||
@@ -10,7 +10,11 @@ import {
|
|||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import { Bound } from '@blocksuite/global/gfx';
|
import { Bound } from '@blocksuite/global/gfx';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||||
import type { BoxSelectionContext, SelectedContext } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
type BoxSelectionContext,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
type SelectedContext,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { html, nothing, type PropertyValues } from 'lit';
|
import { html, nothing, type PropertyValues } from 'lit';
|
||||||
import { query, state } from 'lit/decorators.js';
|
import { query, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
@@ -432,3 +436,62 @@ declare global {
|
|||||||
[AFFINE_EDGELESS_NOTE]: EdgelessNoteBlockComponent;
|
[AFFINE_EDGELESS_NOTE]: EdgelessNoteBlockComponent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EdgelessNoteInteraction =
|
||||||
|
GfxViewInteractionExtension<EdgelessNoteBlockComponent>(
|
||||||
|
NoteBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
minWidth: 170 + 24 * 2,
|
||||||
|
minHeight: 92,
|
||||||
|
},
|
||||||
|
handleRotate: () => {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleResize: ({ model }) => {
|
||||||
|
const initialScale: number = model.props.edgeless.scale ?? 1;
|
||||||
|
return {
|
||||||
|
onResizeStart(context): void {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('edgeless');
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeMove(context): void {
|
||||||
|
const { originalBound, newBound, lockRatio, constraint } = context;
|
||||||
|
const { minWidth, minHeight } = constraint;
|
||||||
|
|
||||||
|
let scale = initialScale;
|
||||||
|
let edgelessProp = { ...model.props.edgeless };
|
||||||
|
const originalRealWidth = originalBound.w / scale;
|
||||||
|
|
||||||
|
if (lockRatio) {
|
||||||
|
scale = newBound.w / originalRealWidth;
|
||||||
|
edgelessProp.scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
newBound.w = clamp(newBound.w, minWidth, Number.MAX_SAFE_INTEGER);
|
||||||
|
newBound.h = clamp(newBound.h, minHeight, Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
|
if (newBound.h > minHeight * scale) {
|
||||||
|
edgelessProp.collapse = true;
|
||||||
|
edgelessProp.collapsedHeight = newBound.h / scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.props.edgeless = edgelessProp;
|
||||||
|
model.props.xywh = newBound.serialize();
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeEnd(context): void {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('edgeless');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from './adapters/index';
|
} from './adapters/index';
|
||||||
import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
|
import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
|
||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
|
import { EdgelessNoteInteraction } from './note-edgeless-block';
|
||||||
import { NoteKeymapExtension } from './note-keymap.js';
|
import { NoteKeymapExtension } from './note-keymap.js';
|
||||||
|
|
||||||
const flavour = NoteBlockSchema.model.flavour;
|
const flavour = NoteBlockSchema.model.flavour;
|
||||||
@@ -28,4 +29,5 @@ export const EdgelessNoteBlockSpec: ExtensionType[] = [
|
|||||||
NoteSlashMenuConfigExtension,
|
NoteSlashMenuConfigExtension,
|
||||||
createBuiltinToolbarConfigExtension(flavour),
|
createBuiltinToolbarConfigExtension(flavour),
|
||||||
NoteKeymapExtension,
|
NoteKeymapExtension,
|
||||||
|
EdgelessNoteInteraction,
|
||||||
].flat();
|
].flat();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { NoteSlashMenuConfigExtension } from './configs/slash-menu';
|
|||||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||||
import { EdgelessClipboardNoteConfig } from './edgeless-clipboard-config';
|
import { EdgelessClipboardNoteConfig } from './edgeless-clipboard-config';
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
|
import { EdgelessNoteInteraction } from './note-edgeless-block';
|
||||||
import { NoteKeymapExtension } from './note-keymap';
|
import { NoteKeymapExtension } from './note-keymap';
|
||||||
|
|
||||||
const flavour = NoteBlockSchema.model.flavour;
|
const flavour = NoteBlockSchema.model.flavour;
|
||||||
@@ -38,6 +39,7 @@ export class NoteViewExtension extends ViewExtensionProvider {
|
|||||||
);
|
);
|
||||||
context.register(createBuiltinToolbarConfigExtension(flavour));
|
context.register(createBuiltinToolbarConfigExtension(flavour));
|
||||||
context.register(EdgelessClipboardNoteConfig);
|
context.register(EdgelessClipboardNoteConfig);
|
||||||
|
context.register(EdgelessNoteInteraction);
|
||||||
} else {
|
} else {
|
||||||
context.register(BlockViewExtension(flavour, literal`affine-note`));
|
context.register(BlockViewExtension(flavour, literal`affine-note`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -691,6 +691,12 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_disposables.add(
|
||||||
|
gfx.selection.slots.updated.subscribe(() => {
|
||||||
|
this.requestUpdate();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
_disposables.add(() => this.removeOverlay());
|
_disposables.add(() => this.removeOverlay());
|
||||||
|
|
||||||
_disposables.add(
|
_disposables.add(
|
||||||
@@ -716,6 +722,15 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _canAutoComplete() {
|
||||||
|
const selection = this.gfx.selection;
|
||||||
|
return (
|
||||||
|
selection.selectedElements.length === 1 &&
|
||||||
|
(selection.selectedElements[0] instanceof ShapeElementModel ||
|
||||||
|
isNoteBlock(selection.selectedElements[0]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
removeOverlay() {
|
removeOverlay() {
|
||||||
this._timer && clearTimeout(this._timer);
|
this._timer && clearTimeout(this._timer);
|
||||||
const surface = getSurfaceComponent(this.std);
|
const surface = getSurfaceComponent(this.std);
|
||||||
@@ -727,7 +742,10 @@ export class EdgelessAutoComplete extends WithDisposable(LitElement) {
|
|||||||
const isShape = this.current instanceof ShapeElementModel;
|
const isShape = this.current instanceof ShapeElementModel;
|
||||||
const isMindMap = this.current.group instanceof MindmapElementModel;
|
const isMindMap = this.current.group instanceof MindmapElementModel;
|
||||||
|
|
||||||
if (this._isMoving || (this._isHover && !isShape)) {
|
if (
|
||||||
|
this._isMoving ||
|
||||||
|
(this._isHover && !isShape && this._canAutoComplete())
|
||||||
|
) {
|
||||||
this.removeOverlay();
|
this.removeOverlay();
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
|||||||
import type { IVec } from '@blocksuite/global/gfx';
|
import type { ResizeHandle } from '@blocksuite/std/gfx';
|
||||||
import { html, nothing } from 'lit';
|
import { html, nothing } from 'lit';
|
||||||
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
export enum HandleDirection {
|
export enum HandleDirection {
|
||||||
Bottom = 'bottom',
|
Bottom = 'bottom',
|
||||||
@@ -12,62 +13,51 @@ export enum HandleDirection {
|
|||||||
TopRight = 'top-right',
|
TopRight = 'top-right',
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResizeHandle(
|
function ResizeHandleRenderer(
|
||||||
handleDirection: HandleDirection,
|
handle: ResizeHandle,
|
||||||
onPointerDown?: (e: PointerEvent, direction: HandleDirection) => void,
|
rotatable: boolean,
|
||||||
updateCursor?: (
|
onPointerDown?: (e: PointerEvent, direction: ResizeHandle) => void,
|
||||||
dragging: boolean,
|
updateCursor?: (options?: {
|
||||||
options?: {
|
type: 'resize' | 'rotate';
|
||||||
type: 'resize' | 'rotate';
|
handle: ResizeHandle;
|
||||||
target?: HTMLElement;
|
}) => void
|
||||||
point?: IVec;
|
|
||||||
}
|
|
||||||
) => void,
|
|
||||||
hideEdgeHandle?: boolean
|
|
||||||
) {
|
) {
|
||||||
const handlerPointerDown = (e: PointerEvent) => {
|
const handlerPointerDown = (e: PointerEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onPointerDown && onPointerDown(e, handleDirection);
|
onPointerDown && onPointerDown(e, handle);
|
||||||
};
|
};
|
||||||
|
|
||||||
const pointerEnter = (type: 'resize' | 'rotate') => (e: PointerEvent) => {
|
const pointerEnter = (type: 'resize' | 'rotate') => (e: PointerEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.buttons === 1 || !updateCursor) return;
|
if (e.buttons === 1 || !updateCursor) return;
|
||||||
|
|
||||||
const { clientX, clientY } = e;
|
updateCursor({ type, handle });
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
const point: IVec = [clientX, clientY];
|
|
||||||
|
|
||||||
updateCursor(true, { type, point, target });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const pointerLeave = (e: PointerEvent) => {
|
const pointerLeave = (e: PointerEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.buttons === 1 || !updateCursor) return;
|
if (e.buttons === 1 || !updateCursor) return;
|
||||||
|
|
||||||
updateCursor(false);
|
updateCursor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rotationTpl =
|
const rotationTpl =
|
||||||
handleDirection === HandleDirection.Top ||
|
handle.length > 6 && rotatable
|
||||||
handleDirection === HandleDirection.Bottom ||
|
? html`<div
|
||||||
handleDirection === HandleDirection.Left ||
|
|
||||||
handleDirection === HandleDirection.Right
|
|
||||||
? nothing
|
|
||||||
: html`<div
|
|
||||||
class="rotate"
|
class="rotate"
|
||||||
@pointerover=${pointerEnter('rotate')}
|
@pointerover=${pointerEnter('rotate')}
|
||||||
@pointerout=${pointerLeave}
|
@pointerout=${pointerLeave}
|
||||||
></div>`;
|
></div>`
|
||||||
|
: nothing;
|
||||||
|
|
||||||
return html`<div
|
return html`<div
|
||||||
class="handle"
|
class="handle"
|
||||||
aria-label=${handleDirection}
|
aria-label=${handle}
|
||||||
@pointerdown=${handlerPointerDown}
|
@pointerdown=${handlerPointerDown}
|
||||||
>
|
>
|
||||||
${rotationTpl}
|
${rotationTpl}
|
||||||
<div
|
<div
|
||||||
class="resize${hideEdgeHandle && ' transparent-handle'}"
|
class="resize transparent-handle"
|
||||||
@pointerover=${pointerEnter('resize')}
|
@pointerover=${pointerEnter('resize')}
|
||||||
@pointerout=${pointerLeave}
|
@pointerout=${pointerLeave}
|
||||||
></div>
|
></div>
|
||||||
@@ -85,135 +75,21 @@ function ResizeHandle(
|
|||||||
*/
|
*/
|
||||||
export type ResizeMode = 'edge' | 'all' | 'none' | 'corner' | 'edgeAndCorner';
|
export type ResizeMode = 'edge' | 'all' | 'none' | 'corner' | 'edgeAndCorner';
|
||||||
|
|
||||||
export function ResizeHandles(
|
export function RenderResizeHandles(
|
||||||
resizeMode: ResizeMode,
|
resizeHandles: ResizeHandle[],
|
||||||
onPointerDown: (e: PointerEvent, direction: HandleDirection) => void,
|
rotatable: boolean,
|
||||||
updateCursor?: (
|
onPointerDown: (e: PointerEvent, direction: ResizeHandle) => void,
|
||||||
dragging: boolean,
|
updateCursor?: (options?: {
|
||||||
options?: {
|
type: 'resize' | 'rotate';
|
||||||
type: 'resize' | 'rotate';
|
handle: ResizeHandle;
|
||||||
target?: HTMLElement;
|
}) => void
|
||||||
point?: IVec;
|
|
||||||
}
|
|
||||||
) => void
|
|
||||||
) {
|
) {
|
||||||
const getCornerHandles = () => {
|
return html`
|
||||||
const handleTopLeft = ResizeHandle(
|
${repeat(
|
||||||
HandleDirection.TopLeft,
|
resizeHandles,
|
||||||
onPointerDown,
|
handle => handle,
|
||||||
updateCursor
|
handle =>
|
||||||
);
|
ResizeHandleRenderer(handle, rotatable, onPointerDown, updateCursor)
|
||||||
const handleTopRight = ResizeHandle(
|
)}
|
||||||
HandleDirection.TopRight,
|
`;
|
||||||
onPointerDown,
|
|
||||||
updateCursor
|
|
||||||
);
|
|
||||||
const handleBottomLeft = ResizeHandle(
|
|
||||||
HandleDirection.BottomLeft,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor
|
|
||||||
);
|
|
||||||
const handleBottomRight = ResizeHandle(
|
|
||||||
HandleDirection.BottomRight,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
handleTopLeft,
|
|
||||||
handleTopRight,
|
|
||||||
handleBottomLeft,
|
|
||||||
handleBottomRight,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const getEdgeHandles = (hideEdgeHandle?: boolean) => {
|
|
||||||
const handleLeft = ResizeHandle(
|
|
||||||
HandleDirection.Left,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor,
|
|
||||||
hideEdgeHandle
|
|
||||||
);
|
|
||||||
const handleRight = ResizeHandle(
|
|
||||||
HandleDirection.Right,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor,
|
|
||||||
hideEdgeHandle
|
|
||||||
);
|
|
||||||
return { handleLeft, handleRight };
|
|
||||||
};
|
|
||||||
const getEdgeVerticalHandles = (hideEdgeHandle?: boolean) => {
|
|
||||||
const handleTop = ResizeHandle(
|
|
||||||
HandleDirection.Top,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor,
|
|
||||||
hideEdgeHandle
|
|
||||||
);
|
|
||||||
const handleBottom = ResizeHandle(
|
|
||||||
HandleDirection.Bottom,
|
|
||||||
onPointerDown,
|
|
||||||
updateCursor,
|
|
||||||
hideEdgeHandle
|
|
||||||
);
|
|
||||||
return { handleTop, handleBottom };
|
|
||||||
};
|
|
||||||
switch (resizeMode) {
|
|
||||||
case 'corner': {
|
|
||||||
const {
|
|
||||||
handleTopLeft,
|
|
||||||
handleTopRight,
|
|
||||||
handleBottomLeft,
|
|
||||||
handleBottomRight,
|
|
||||||
} = getCornerHandles();
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
return html`
|
|
||||||
${handleTopLeft}
|
|
||||||
${handleTopRight}
|
|
||||||
${handleBottomLeft}
|
|
||||||
${handleBottomRight}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
case 'edge': {
|
|
||||||
const { handleLeft, handleRight } = getEdgeHandles();
|
|
||||||
return html`${handleLeft} ${handleRight}`;
|
|
||||||
}
|
|
||||||
case 'all': {
|
|
||||||
const {
|
|
||||||
handleTopLeft,
|
|
||||||
handleTopRight,
|
|
||||||
handleBottomLeft,
|
|
||||||
handleBottomRight,
|
|
||||||
} = getCornerHandles();
|
|
||||||
const { handleLeft, handleRight } = getEdgeHandles(true);
|
|
||||||
const { handleTop, handleBottom } = getEdgeVerticalHandles(true);
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
return html`
|
|
||||||
${handleTopLeft}
|
|
||||||
${handleTop}
|
|
||||||
${handleTopRight}
|
|
||||||
${handleRight}
|
|
||||||
${handleBottomRight}
|
|
||||||
${handleBottom}
|
|
||||||
${handleBottomLeft}
|
|
||||||
${handleLeft}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
case 'edgeAndCorner': {
|
|
||||||
const {
|
|
||||||
handleTopLeft,
|
|
||||||
handleTopRight,
|
|
||||||
handleBottomLeft,
|
|
||||||
handleBottomRight,
|
|
||||||
} = getCornerHandles();
|
|
||||||
const { handleLeft, handleRight } = getEdgeHandles(true);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
${handleTopLeft} ${handleTopRight} ${handleRight} ${handleBottomRight}
|
|
||||||
${handleBottomLeft} ${handleLeft}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
case 'none': {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,705 +0,0 @@
|
|||||||
import { NOTE_MIN_WIDTH } from '@blocksuite/affine-model';
|
|
||||||
import {
|
|
||||||
Bound,
|
|
||||||
getQuadBoundWithRotation,
|
|
||||||
type IPoint,
|
|
||||||
type IVec,
|
|
||||||
type PointLocation,
|
|
||||||
rotatePoints,
|
|
||||||
} from '@blocksuite/global/gfx';
|
|
||||||
|
|
||||||
import type { SelectableProps } from '../../utils/query.js';
|
|
||||||
import { HandleDirection, type ResizeMode } from './resize-handles.js';
|
|
||||||
|
|
||||||
// 15deg
|
|
||||||
const SHIFT_LOCKING_ANGLE = Math.PI / 12;
|
|
||||||
|
|
||||||
type DragStartHandler = () => void;
|
|
||||||
type DragEndHandler = () => void;
|
|
||||||
|
|
||||||
type ResizeMoveHandler = (
|
|
||||||
bounds: Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
bound: Bound;
|
|
||||||
path?: PointLocation[];
|
|
||||||
matrix?: DOMMatrix;
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
direction: HandleDirection
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
type RotateMoveHandler = (point: IPoint, rotate: number) => void;
|
|
||||||
|
|
||||||
export class HandleResizeManager {
|
|
||||||
private _aspectRatio = 1;
|
|
||||||
|
|
||||||
private _bounds = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
bound: Bound;
|
|
||||||
rotate: number;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current rect of selected elements, it may change during resizing or moving
|
|
||||||
*/
|
|
||||||
private _currentRect = new DOMRect();
|
|
||||||
|
|
||||||
private _dragDirection: HandleDirection = HandleDirection.Left;
|
|
||||||
|
|
||||||
private _dragging = false;
|
|
||||||
|
|
||||||
private _dragPos: {
|
|
||||||
start: { x: number; y: number };
|
|
||||||
end: { x: number; y: number };
|
|
||||||
} = {
|
|
||||||
start: { x: 0, y: 0 },
|
|
||||||
end: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
private _locked = false;
|
|
||||||
|
|
||||||
private readonly _onDragEnd: DragEndHandler;
|
|
||||||
|
|
||||||
private readonly _onDragStart: DragStartHandler;
|
|
||||||
|
|
||||||
private readonly _onResizeMove: ResizeMoveHandler;
|
|
||||||
|
|
||||||
private readonly _onRotateMove: RotateMoveHandler;
|
|
||||||
|
|
||||||
private _origin: { x: number; y: number } = { x: 0, y: 0 };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Record inital rect of selected elements
|
|
||||||
*/
|
|
||||||
private _originalRect = new DOMRect();
|
|
||||||
|
|
||||||
private _proportion = false;
|
|
||||||
|
|
||||||
private _proportional = false;
|
|
||||||
|
|
||||||
private _resizeMode: ResizeMode = 'none';
|
|
||||||
|
|
||||||
private _rotate = 0;
|
|
||||||
|
|
||||||
private _rotation = false;
|
|
||||||
|
|
||||||
private _shiftKey = false;
|
|
||||||
|
|
||||||
private _target: HTMLElement | null = null;
|
|
||||||
|
|
||||||
private _zoom = 1;
|
|
||||||
|
|
||||||
onPointerDown = (
|
|
||||||
e: PointerEvent,
|
|
||||||
direction: HandleDirection,
|
|
||||||
proportional = false
|
|
||||||
) => {
|
|
||||||
// Prevent selection action from being triggered
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
this._locked = false;
|
|
||||||
this._target = e.target as HTMLElement;
|
|
||||||
this._dragDirection = direction;
|
|
||||||
this._dragPos.start = { x: e.x, y: e.y };
|
|
||||||
this._dragPos.end = { x: e.x, y: e.y };
|
|
||||||
this._rotation = this._target.classList.contains('rotate');
|
|
||||||
this._proportional = proportional;
|
|
||||||
|
|
||||||
if (this._rotation) {
|
|
||||||
const rect = this._target
|
|
||||||
.closest('.affine-edgeless-selected-rect')
|
|
||||||
?.getBoundingClientRect();
|
|
||||||
if (!rect) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { left, top, right, bottom } = rect;
|
|
||||||
const x = (left + right) / 2;
|
|
||||||
const y = (top + bottom) / 2;
|
|
||||||
// center of `selected-rect` in viewport
|
|
||||||
this._origin = { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
this._dragging = true;
|
|
||||||
this._onDragStart();
|
|
||||||
|
|
||||||
const _onPointerMove = ({ x, y, shiftKey }: PointerEvent) => {
|
|
||||||
if (this._resizeMode === 'none') return;
|
|
||||||
|
|
||||||
this._shiftKey = shiftKey;
|
|
||||||
this._dragPos.end = { x, y };
|
|
||||||
|
|
||||||
const proportional = this._proportional || this._shiftKey;
|
|
||||||
|
|
||||||
if (this._rotation) {
|
|
||||||
this._onRotate(proportional);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._onResize(proportional);
|
|
||||||
};
|
|
||||||
|
|
||||||
const _onPointerUp = (_: PointerEvent) => {
|
|
||||||
this._dragging = false;
|
|
||||||
this._onDragEnd();
|
|
||||||
|
|
||||||
const { x, y, width, height } = this._currentRect;
|
|
||||||
this._originalRect = new DOMRect(x, y, width, height);
|
|
||||||
|
|
||||||
this._locked = true;
|
|
||||||
this._shiftKey = false;
|
|
||||||
this._rotation = false;
|
|
||||||
this._dragPos = {
|
|
||||||
start: { x: 0, y: 0 },
|
|
||||||
end: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
document.removeEventListener('pointermove', _onPointerMove);
|
|
||||||
document.removeEventListener('pointerup', _onPointerUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('pointermove', _onPointerMove);
|
|
||||||
document.addEventListener('pointerup', _onPointerUp);
|
|
||||||
};
|
|
||||||
|
|
||||||
get bounds() {
|
|
||||||
return this._bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currentRect() {
|
|
||||||
return this._currentRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragDirection() {
|
|
||||||
return this._dragDirection;
|
|
||||||
}
|
|
||||||
|
|
||||||
get dragging() {
|
|
||||||
return this._dragging;
|
|
||||||
}
|
|
||||||
|
|
||||||
get originalRect() {
|
|
||||||
return this._originalRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
get rotation() {
|
|
||||||
return this._rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
onDragStart: DragStartHandler,
|
|
||||||
onResizeMove: ResizeMoveHandler,
|
|
||||||
onRotateMove: RotateMoveHandler,
|
|
||||||
onDragEnd: DragEndHandler
|
|
||||||
) {
|
|
||||||
this._onDragStart = onDragStart;
|
|
||||||
this._onResizeMove = onResizeMove;
|
|
||||||
this._onRotateMove = onRotateMove;
|
|
||||||
this._onDragEnd = onDragEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onResize(proportion: boolean) {
|
|
||||||
const {
|
|
||||||
_aspectRatio,
|
|
||||||
_dragDirection,
|
|
||||||
_dragPos,
|
|
||||||
_rotate,
|
|
||||||
_resizeMode,
|
|
||||||
_zoom,
|
|
||||||
_originalRect,
|
|
||||||
_currentRect,
|
|
||||||
} = this;
|
|
||||||
proportion ||= this._proportion;
|
|
||||||
|
|
||||||
const isAll = _resizeMode === 'all';
|
|
||||||
const isCorner = _resizeMode === 'corner';
|
|
||||||
const isEdgeAndCorner = _resizeMode === 'edgeAndCorner';
|
|
||||||
|
|
||||||
const {
|
|
||||||
start: { x: startX, y: startY },
|
|
||||||
end: { x: endX, y: endY },
|
|
||||||
} = _dragPos;
|
|
||||||
|
|
||||||
const { left: minX, top: minY, right: maxX, bottom: maxY } = _originalRect;
|
|
||||||
const original = {
|
|
||||||
w: maxX - minX,
|
|
||||||
h: maxY - minY,
|
|
||||||
cx: (minX + maxX) / 2,
|
|
||||||
cy: (minY + maxY) / 2,
|
|
||||||
};
|
|
||||||
const rect = { ...original };
|
|
||||||
const scale = { x: 1, y: 1 };
|
|
||||||
const flip = { x: 1, y: 1 };
|
|
||||||
const direction = { x: 1, y: 1 };
|
|
||||||
const fixedPoint = new DOMPoint(0, 0);
|
|
||||||
const draggingPoint = new DOMPoint(0, 0);
|
|
||||||
|
|
||||||
const deltaX = (endX - startX) / _zoom;
|
|
||||||
const deltaY = (endY - startY) / _zoom;
|
|
||||||
|
|
||||||
const m0 = new DOMMatrix()
|
|
||||||
.translateSelf(original.cx, original.cy)
|
|
||||||
.rotateSelf(_rotate)
|
|
||||||
.translateSelf(-original.cx, -original.cy);
|
|
||||||
|
|
||||||
if (isCorner || isAll || isEdgeAndCorner) {
|
|
||||||
switch (_dragDirection) {
|
|
||||||
case HandleDirection.TopLeft: {
|
|
||||||
direction.x = -1;
|
|
||||||
direction.y = -1;
|
|
||||||
fixedPoint.x = maxX;
|
|
||||||
fixedPoint.y = maxY;
|
|
||||||
draggingPoint.x = minX;
|
|
||||||
draggingPoint.y = minY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.TopRight: {
|
|
||||||
direction.x = 1;
|
|
||||||
direction.y = -1;
|
|
||||||
fixedPoint.x = minX;
|
|
||||||
fixedPoint.y = maxY;
|
|
||||||
draggingPoint.x = maxX;
|
|
||||||
draggingPoint.y = minY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.BottomRight: {
|
|
||||||
direction.x = 1;
|
|
||||||
direction.y = 1;
|
|
||||||
fixedPoint.x = minX;
|
|
||||||
fixedPoint.y = minY;
|
|
||||||
draggingPoint.x = maxX;
|
|
||||||
draggingPoint.y = maxY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.BottomLeft: {
|
|
||||||
direction.x = -1;
|
|
||||||
direction.y = 1;
|
|
||||||
fixedPoint.x = maxX;
|
|
||||||
fixedPoint.y = minY;
|
|
||||||
draggingPoint.x = minX;
|
|
||||||
draggingPoint.y = maxY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Left: {
|
|
||||||
direction.x = -1;
|
|
||||||
direction.y = 1;
|
|
||||||
fixedPoint.x = maxX;
|
|
||||||
fixedPoint.y = original.cy;
|
|
||||||
draggingPoint.x = minX;
|
|
||||||
draggingPoint.y = original.cy;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Right: {
|
|
||||||
direction.x = 1;
|
|
||||||
direction.y = 1;
|
|
||||||
fixedPoint.x = minX;
|
|
||||||
fixedPoint.y = original.cy;
|
|
||||||
draggingPoint.x = maxX;
|
|
||||||
draggingPoint.y = original.cy;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Top: {
|
|
||||||
const cx = (minX + maxX) / 2;
|
|
||||||
direction.x = 1;
|
|
||||||
direction.y = -1;
|
|
||||||
fixedPoint.x = cx;
|
|
||||||
fixedPoint.y = maxY;
|
|
||||||
draggingPoint.x = cx;
|
|
||||||
draggingPoint.y = minY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Bottom: {
|
|
||||||
const cx = (minX + maxX) / 2;
|
|
||||||
direction.x = 1;
|
|
||||||
direction.y = 1;
|
|
||||||
fixedPoint.x = cx;
|
|
||||||
fixedPoint.y = minY;
|
|
||||||
draggingPoint.x = cx;
|
|
||||||
draggingPoint.y = maxY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// force adjustment by aspect ratio
|
|
||||||
proportion ||= this._bounds.size > 1;
|
|
||||||
|
|
||||||
const fp = fixedPoint.matrixTransform(m0);
|
|
||||||
let dp = draggingPoint.matrixTransform(m0);
|
|
||||||
|
|
||||||
dp.x += deltaX;
|
|
||||||
dp.y += deltaY;
|
|
||||||
|
|
||||||
if (
|
|
||||||
_dragDirection === HandleDirection.Left ||
|
|
||||||
_dragDirection === HandleDirection.Right ||
|
|
||||||
_dragDirection === HandleDirection.Top ||
|
|
||||||
_dragDirection === HandleDirection.Bottom
|
|
||||||
) {
|
|
||||||
const dpo = draggingPoint.matrixTransform(m0);
|
|
||||||
const coorPoint: IVec = [0, 0];
|
|
||||||
const [[x1, y1]] = rotatePoints([[dpo.x, dpo.y]], coorPoint, -_rotate);
|
|
||||||
const [[x2, y2]] = rotatePoints([[dp.x, dp.y]], coorPoint, -_rotate);
|
|
||||||
const point = { x: 0, y: 0 };
|
|
||||||
if (
|
|
||||||
_dragDirection === HandleDirection.Left ||
|
|
||||||
_dragDirection === HandleDirection.Right
|
|
||||||
) {
|
|
||||||
point.x = x2;
|
|
||||||
point.y = y1;
|
|
||||||
} else {
|
|
||||||
point.x = x1;
|
|
||||||
point.y = y2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [[x3, y3]] = rotatePoints(
|
|
||||||
[[point.x, point.y]],
|
|
||||||
coorPoint,
|
|
||||||
_rotate
|
|
||||||
);
|
|
||||||
|
|
||||||
dp.x = x3;
|
|
||||||
dp.y = y3;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cx = (fp.x + dp.x) / 2;
|
|
||||||
const cy = (fp.y + dp.y) / 2;
|
|
||||||
|
|
||||||
const m1 = new DOMMatrix()
|
|
||||||
.translateSelf(cx, cy)
|
|
||||||
.rotateSelf(-_rotate)
|
|
||||||
.translateSelf(-cx, -cy);
|
|
||||||
|
|
||||||
const f = fp.matrixTransform(m1);
|
|
||||||
const d = dp.matrixTransform(m1);
|
|
||||||
|
|
||||||
switch (_dragDirection) {
|
|
||||||
case HandleDirection.TopLeft: {
|
|
||||||
rect.w = f.x - d.x;
|
|
||||||
rect.h = f.y - d.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.TopRight: {
|
|
||||||
rect.w = d.x - f.x;
|
|
||||||
rect.h = f.y - d.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.BottomRight: {
|
|
||||||
rect.w = d.x - f.x;
|
|
||||||
rect.h = d.y - f.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.BottomLeft: {
|
|
||||||
rect.w = f.x - d.x;
|
|
||||||
rect.h = d.y - f.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Left: {
|
|
||||||
rect.w = f.x - d.x;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Right: {
|
|
||||||
rect.w = d.x - f.x;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Top: {
|
|
||||||
rect.h = f.y - d.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Bottom: {
|
|
||||||
rect.h = d.y - f.y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.cx = (d.x + f.x) / 2;
|
|
||||||
rect.cy = (d.y + f.y) / 2;
|
|
||||||
scale.x = rect.w / original.w;
|
|
||||||
scale.y = rect.h / original.h;
|
|
||||||
flip.x = scale.x < 0 ? -1 : 1;
|
|
||||||
flip.y = scale.y < 0 ? -1 : 1;
|
|
||||||
|
|
||||||
const isDraggingCorner =
|
|
||||||
_dragDirection === HandleDirection.TopLeft ||
|
|
||||||
_dragDirection === HandleDirection.TopRight ||
|
|
||||||
_dragDirection === HandleDirection.BottomRight ||
|
|
||||||
_dragDirection === HandleDirection.BottomLeft;
|
|
||||||
|
|
||||||
// lock aspect ratio
|
|
||||||
if (proportion && isDraggingCorner) {
|
|
||||||
const newAspectRatio = Math.abs(rect.w / rect.h);
|
|
||||||
if (_aspectRatio < newAspectRatio) {
|
|
||||||
scale.y = Math.abs(scale.x) * flip.y;
|
|
||||||
rect.h = scale.y * original.h;
|
|
||||||
} else {
|
|
||||||
scale.x = Math.abs(scale.y) * flip.x;
|
|
||||||
rect.w = scale.x * original.w;
|
|
||||||
}
|
|
||||||
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
|
|
||||||
draggingPoint.y = fixedPoint.y + rect.h * direction.y;
|
|
||||||
|
|
||||||
dp = draggingPoint.matrixTransform(m0);
|
|
||||||
|
|
||||||
rect.cx = (fp.x + dp.x) / 2;
|
|
||||||
rect.cy = (fp.y + dp.y) / 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// handle notes
|
|
||||||
switch (_dragDirection) {
|
|
||||||
case HandleDirection.Left: {
|
|
||||||
direction.x = -1;
|
|
||||||
fixedPoint.x = maxX;
|
|
||||||
draggingPoint.x = minX + deltaX;
|
|
||||||
rect.w = fixedPoint.x - draggingPoint.x;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case HandleDirection.Right: {
|
|
||||||
direction.x = 1;
|
|
||||||
fixedPoint.x = minX;
|
|
||||||
draggingPoint.x = maxX + deltaX;
|
|
||||||
rect.w = draggingPoint.x - fixedPoint.x;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scale.x = rect.w / original.w;
|
|
||||||
flip.x = scale.x < 0 ? -1 : 1;
|
|
||||||
|
|
||||||
if (Math.abs(rect.w) < NOTE_MIN_WIDTH) {
|
|
||||||
rect.w = NOTE_MIN_WIDTH * flip.x;
|
|
||||||
scale.x = rect.w / original.w;
|
|
||||||
draggingPoint.x = fixedPoint.x + rect.w * direction.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
rect.cx = (draggingPoint.x + fixedPoint.x) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = Math.abs(rect.w);
|
|
||||||
const height = Math.abs(rect.h);
|
|
||||||
const x = rect.cx - width / 2;
|
|
||||||
const y = rect.cy - height / 2;
|
|
||||||
|
|
||||||
_currentRect.x = x;
|
|
||||||
_currentRect.y = y;
|
|
||||||
_currentRect.width = width;
|
|
||||||
_currentRect.height = height;
|
|
||||||
|
|
||||||
const newBounds = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
bound: Bound;
|
|
||||||
path?: PointLocation[];
|
|
||||||
matrix?: DOMMatrix;
|
|
||||||
}
|
|
||||||
>();
|
|
||||||
|
|
||||||
let process: (value: SelectableProps, key: string) => void;
|
|
||||||
|
|
||||||
if (isCorner || isAll || isEdgeAndCorner) {
|
|
||||||
if (this._bounds.size === 1) {
|
|
||||||
process = (_, id) => {
|
|
||||||
newBounds.set(id, {
|
|
||||||
bound: new Bound(x, y, width, height),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const fp = fixedPoint.matrixTransform(m0);
|
|
||||||
const m2 = new DOMMatrix()
|
|
||||||
.translateSelf(fp.x, fp.y)
|
|
||||||
.rotateSelf(_rotate)
|
|
||||||
.translateSelf(-fp.x, -fp.y)
|
|
||||||
.scaleSelf(scale.x, scale.y, 1, fp.x, fp.y, 0)
|
|
||||||
.translateSelf(fp.x, fp.y)
|
|
||||||
.rotateSelf(-_rotate)
|
|
||||||
.translateSelf(-fp.x, -fp.y);
|
|
||||||
|
|
||||||
// TODO: on same rotate
|
|
||||||
process = ({ bound: { x, y, w, h }, path }, id) => {
|
|
||||||
const cx = x + w / 2;
|
|
||||||
const cy = y + h / 2;
|
|
||||||
const center = new DOMPoint(cx, cy).matrixTransform(m2);
|
|
||||||
const newWidth = Math.abs(w * scale.x);
|
|
||||||
const newHeight = Math.abs(h * scale.y);
|
|
||||||
|
|
||||||
newBounds.set(id, {
|
|
||||||
bound: new Bound(
|
|
||||||
center.x - newWidth / 2,
|
|
||||||
center.y - newHeight / 2,
|
|
||||||
newWidth,
|
|
||||||
newHeight
|
|
||||||
),
|
|
||||||
matrix: m2,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// include notes, <---->
|
|
||||||
const m2 = new DOMMatrix().scaleSelf(
|
|
||||||
scale.x,
|
|
||||||
scale.y,
|
|
||||||
1,
|
|
||||||
fixedPoint.x,
|
|
||||||
fixedPoint.y,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
process = ({ bound: { x, y, w, h }, rotate = 0, path }, id) => {
|
|
||||||
const cx = x + w / 2;
|
|
||||||
const cy = y + h / 2;
|
|
||||||
|
|
||||||
const center = new DOMPoint(cx, cy).matrixTransform(m2);
|
|
||||||
|
|
||||||
let newWidth: number;
|
|
||||||
let newHeight: number;
|
|
||||||
|
|
||||||
// TODO: determine if it is a note
|
|
||||||
if (rotate) {
|
|
||||||
const { width } = getQuadBoundWithRotation({ x, y, w, h, rotate });
|
|
||||||
const hrw = width / 2;
|
|
||||||
|
|
||||||
center.y = cy;
|
|
||||||
|
|
||||||
if (_currentRect.width <= width) {
|
|
||||||
newWidth = w * (_currentRect.width / width);
|
|
||||||
newHeight = newWidth / (w / h);
|
|
||||||
center.x = _currentRect.left + _currentRect.width / 2;
|
|
||||||
} else {
|
|
||||||
const p = (cx - hrw - _originalRect.left) / _originalRect.width;
|
|
||||||
const lx = _currentRect.left + p * _currentRect.width + hrw;
|
|
||||||
center.x = Math.max(
|
|
||||||
_currentRect.left + hrw,
|
|
||||||
Math.min(lx, _currentRect.left + _currentRect.width - hrw)
|
|
||||||
);
|
|
||||||
newWidth = w;
|
|
||||||
newHeight = h;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newWidth = Math.abs(w * scale.x);
|
|
||||||
newHeight = Math.abs(h * scale.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
newBounds.set(id, {
|
|
||||||
bound: new Bound(
|
|
||||||
center.x - newWidth / 2,
|
|
||||||
center.y - newHeight / 2,
|
|
||||||
newWidth,
|
|
||||||
newHeight
|
|
||||||
),
|
|
||||||
matrix: m2,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this._bounds.forEach(process);
|
|
||||||
this._onResizeMove(newBounds, this._dragDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _onRotate(shiftKey = false) {
|
|
||||||
const {
|
|
||||||
_originalRect: { left: minX, top: minY, right: maxX, bottom: maxY },
|
|
||||||
_dragPos: {
|
|
||||||
start: { x: startX, y: startY },
|
|
||||||
end: { x: endX, y: endY },
|
|
||||||
},
|
|
||||||
_origin: { x: centerX, y: centerY },
|
|
||||||
_rotate,
|
|
||||||
} = this;
|
|
||||||
|
|
||||||
const startRad = Math.atan2(startY - centerY, startX - centerX);
|
|
||||||
const endRad = Math.atan2(endY - centerY, endX - centerX);
|
|
||||||
let deltaRad = endRad - startRad;
|
|
||||||
|
|
||||||
// snap angle
|
|
||||||
// 15deg * n = 0, 15, 30, 45, ... 360
|
|
||||||
if (shiftKey) {
|
|
||||||
const prevRad = (_rotate * Math.PI) / 180;
|
|
||||||
let angle = prevRad + deltaRad;
|
|
||||||
angle += SHIFT_LOCKING_ANGLE / 2;
|
|
||||||
angle -= angle % SHIFT_LOCKING_ANGLE;
|
|
||||||
deltaRad = angle - prevRad;
|
|
||||||
}
|
|
||||||
|
|
||||||
const delta = (deltaRad * 180) / Math.PI;
|
|
||||||
|
|
||||||
let x = endX;
|
|
||||||
let y = endY;
|
|
||||||
if (shiftKey) {
|
|
||||||
const point = new DOMPoint(startX, startY).matrixTransform(
|
|
||||||
new DOMMatrix()
|
|
||||||
.translateSelf(centerX, centerY)
|
|
||||||
.rotateSelf(delta)
|
|
||||||
.translateSelf(-centerX, -centerY)
|
|
||||||
);
|
|
||||||
x = point.x;
|
|
||||||
y = point.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._onRotateMove(
|
|
||||||
// center of element in suface
|
|
||||||
{ x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
|
|
||||||
delta
|
|
||||||
);
|
|
||||||
|
|
||||||
this._dragPos.start = { x, y };
|
|
||||||
this._rotate += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
onPressShiftKey(pressed: boolean) {
|
|
||||||
if (!this._target) return;
|
|
||||||
if (this._locked) return;
|
|
||||||
|
|
||||||
if (this._shiftKey === pressed) return;
|
|
||||||
this._shiftKey = pressed;
|
|
||||||
|
|
||||||
const proportional = this._proportional || this._shiftKey;
|
|
||||||
|
|
||||||
if (this._rotation) {
|
|
||||||
this._onRotate(proportional);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._onResize(proportional);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBounds(bounds: Map<string, SelectableProps>) {
|
|
||||||
this._bounds = bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRectPosition(delta: { x: number; y: number }) {
|
|
||||||
this._currentRect.x += delta.x;
|
|
||||||
this._currentRect.y += delta.y;
|
|
||||||
this._originalRect.x = this._currentRect.x;
|
|
||||||
this._originalRect.y = this._currentRect.y;
|
|
||||||
|
|
||||||
return this._originalRect;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState(
|
|
||||||
resizeMode: ResizeMode,
|
|
||||||
rotate: number,
|
|
||||||
zoom: number,
|
|
||||||
position?: { x: number; y: number },
|
|
||||||
originalRect?: DOMRect,
|
|
||||||
proportion = false
|
|
||||||
) {
|
|
||||||
this._resizeMode = resizeMode;
|
|
||||||
this._rotate = rotate;
|
|
||||||
this._zoom = zoom;
|
|
||||||
this._proportion = proportion;
|
|
||||||
|
|
||||||
if (position) {
|
|
||||||
this._currentRect.x = position.x;
|
|
||||||
this._currentRect.y = position.y;
|
|
||||||
this._originalRect.x = this._currentRect.x;
|
|
||||||
this._originalRect.y = this._currentRect.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalRect) {
|
|
||||||
this._originalRect = originalRect;
|
|
||||||
this._aspectRatio = originalRect.width / originalRect.height;
|
|
||||||
this._currentRect = DOMRect.fromRect(originalRect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +1,65 @@
|
|||||||
import type { IVec } from '@blocksuite/global/gfx';
|
import type {
|
||||||
import { normalizeDegAngle, Vec } from '@blocksuite/global/gfx';
|
CursorType,
|
||||||
import type { CursorType, StandardCursor } from '@blocksuite/std/gfx';
|
ResizeHandle,
|
||||||
|
StandardCursor,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
|
const rotateCursorMap: {
|
||||||
|
[key in ResizeHandle]: number;
|
||||||
|
} = {
|
||||||
|
'top-right': 0,
|
||||||
|
'bottom-right': 90,
|
||||||
|
'bottom-left': 180,
|
||||||
|
'top-left': 270,
|
||||||
|
|
||||||
|
// not used
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
};
|
||||||
|
|
||||||
export function generateCursorUrl(
|
export function generateCursorUrl(
|
||||||
angle = 0,
|
angle = 0,
|
||||||
|
handle: ResizeHandle,
|
||||||
fallback: StandardCursor = 'default'
|
fallback: StandardCursor = 'default'
|
||||||
): CursorType {
|
): CursorType {
|
||||||
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
|
angle = ((angle % 360) + 360) % 360;
|
||||||
|
return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cg transform='rotate(${rotateCursorMap[handle] + angle} 16 16)'%3E%3Cpath fill='white' d='M13.7,18.5h3.9l0-1.5c0-1.4-1.2-2.6-2.6-2.6h-1.5v3.9l-5.8-5.8l5.8-5.8v3.9h2.3c3.1,0,5.6,2.5,5.6,5.6v2.3h3.9l-5.8,5.8L13.7,18.5z'/%3E%3Cpath d='M20.4,19.4v-3.2c0-2.6-2.1-4.7-4.7-4.7h-3.2l0,0V9L9,12.6l3.6,3.6v-2.6l0,0H15c1.9,0,3.5,1.6,3.5,3.5v2.4l0,0h-2.6l3.6,3.6l3.6-3.6L20.4,19.4L20.4,19.4z'/%3E%3C/g%3E%3C/svg%3E") 16 16, ${fallback}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESIZE_CURSORS: CursorType[] = [
|
const handleToRotateMap: {
|
||||||
'ew-resize',
|
[key in ResizeHandle]: number;
|
||||||
'nwse-resize',
|
} = {
|
||||||
'ns-resize',
|
'top-left': 45,
|
||||||
'nesw-resize',
|
'top-right': 135,
|
||||||
];
|
'bottom-right': 45,
|
||||||
export function rotateResizeCursor(angle: number): StandardCursor {
|
'bottom-left': 135,
|
||||||
const a = Math.round(angle / (Math.PI / 4));
|
left: 0,
|
||||||
const cursor = RESIZE_CURSORS[a % RESIZE_CURSORS.length];
|
right: 0,
|
||||||
return cursor as StandardCursor;
|
top: 90,
|
||||||
}
|
bottom: 90,
|
||||||
|
};
|
||||||
export function calcAngle(target: HTMLElement, point: IVec, offset = 0) {
|
|
||||||
const rect = target
|
const rotateToHandleMap: {
|
||||||
.closest('.affine-edgeless-selected-rect')
|
[key: number]: StandardCursor;
|
||||||
?.getBoundingClientRect();
|
} = {
|
||||||
|
0: 'ew-resize',
|
||||||
if (!rect) {
|
45: 'nwse-resize',
|
||||||
console.error('rect not found when calc angle');
|
90: 'ns-resize',
|
||||||
return 0;
|
135: 'nesw-resize',
|
||||||
}
|
};
|
||||||
const { left, top, right, bottom } = rect;
|
|
||||||
const center = Vec.med([left, top], [right, bottom]);
|
export function getRotatedResizeCursor(option: {
|
||||||
return normalizeDegAngle(
|
handle: ResizeHandle;
|
||||||
((Vec.angle(center, point) + offset) * 180) / Math.PI
|
angle: number;
|
||||||
);
|
}) {
|
||||||
}
|
const angle =
|
||||||
|
(Math.round(
|
||||||
export function calcAngleWithRotation(
|
(handleToRotateMap[option.handle] + ((option.angle + 360) % 360)) / 45
|
||||||
target: HTMLElement,
|
) %
|
||||||
point: IVec,
|
4) *
|
||||||
rect: DOMRect,
|
45;
|
||||||
rotate: number
|
|
||||||
) {
|
return rotateToHandleMap[angle] || 'default';
|
||||||
const handle = target.parentElement;
|
|
||||||
const ariaLabel = handle?.getAttribute('aria-label');
|
|
||||||
const { left, top, right, bottom, width, height } = rect;
|
|
||||||
const size = Math.min(width, height);
|
|
||||||
const sx = size / width;
|
|
||||||
const sy = size / height;
|
|
||||||
const center = Vec.med([left, top], [right, bottom]);
|
|
||||||
const draggingPoint = [0, 0];
|
|
||||||
|
|
||||||
switch (ariaLabel) {
|
|
||||||
case 'top-left': {
|
|
||||||
draggingPoint[0] = left;
|
|
||||||
draggingPoint[1] = top;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'top-right': {
|
|
||||||
draggingPoint[0] = right;
|
|
||||||
draggingPoint[1] = top;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'bottom-right': {
|
|
||||||
draggingPoint[0] = right;
|
|
||||||
draggingPoint[1] = bottom;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'bottom-left': {
|
|
||||||
draggingPoint[0] = left;
|
|
||||||
draggingPoint[1] = bottom;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dp = new DOMMatrix()
|
|
||||||
.translateSelf(center[0], center[1])
|
|
||||||
.rotateSelf(rotate)
|
|
||||||
.translateSelf(-center[0], -center[1])
|
|
||||||
.transformPoint(new DOMPoint(...draggingPoint));
|
|
||||||
|
|
||||||
const m = new DOMMatrix()
|
|
||||||
.translateSelf(dp.x, dp.y)
|
|
||||||
.rotateSelf(rotate)
|
|
||||||
.translateSelf(-dp.x, -dp.y)
|
|
||||||
.scaleSelf(sx, sy, 1, dp.x, dp.y, 0)
|
|
||||||
.translateSelf(dp.x, dp.y)
|
|
||||||
.rotateSelf(-rotate)
|
|
||||||
.translateSelf(-dp.x, -dp.y);
|
|
||||||
|
|
||||||
const c = new DOMPoint(...center).matrixTransform(m);
|
|
||||||
|
|
||||||
return normalizeDegAngle((Vec.angle([c.x, c.y], point) * 180) / Math.PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calcAngleEdgeWithRotation(target: HTMLElement, rotate: number) {
|
|
||||||
let angleWithEdge = 0;
|
|
||||||
const handle = target.parentElement;
|
|
||||||
const ariaLabel = handle?.getAttribute('aria-label');
|
|
||||||
switch (ariaLabel) {
|
|
||||||
case 'top': {
|
|
||||||
angleWithEdge = 270;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'bottom': {
|
|
||||||
angleWithEdge = 90;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'left': {
|
|
||||||
angleWithEdge = 180;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'right': {
|
|
||||||
angleWithEdge = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return angleWithEdge + rotate;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getResizeLabel(target: HTMLElement) {
|
|
||||||
const handle = target.parentElement;
|
|
||||||
const ariaLabel = handle?.getAttribute('aria-label');
|
|
||||||
return ariaLabel;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,3 @@ export const ATTACHED_DISTANCE = 20;
|
|||||||
export const SurfaceColor = '#6046FE';
|
export const SurfaceColor = '#6046FE';
|
||||||
export const NoteColor = '#1E96EB';
|
export const NoteColor = '#1E96EB';
|
||||||
export const BlendColor = '#7D91FF';
|
export const BlendColor = '#7D91FF';
|
||||||
|
|
||||||
export const AI_CHAT_BLOCK_MIN_WIDTH = 260;
|
|
||||||
export const AI_CHAT_BLOCK_MIN_HEIGHT = 160;
|
|
||||||
export const AI_CHAT_BLOCK_MAX_WIDTH = 320;
|
|
||||||
export const AI_CHAT_BLOCK_MAX_HEIGHT = 300;
|
|
||||||
|
|
||||||
export const EMBED_IFRAME_BLOCK_MIN_WIDTH = 218;
|
|
||||||
export const EMBED_IFRAME_BLOCK_MIN_HEIGHT = 44;
|
|
||||||
export const EMBED_IFRAME_BLOCK_MAX_WIDTH = 3400;
|
|
||||||
export const EMBED_IFRAME_BLOCK_MAX_HEIGHT = 2200;
|
|
||||||
|
|||||||
@@ -62,10 +62,7 @@ export const connectorWatcher: SurfaceMiddleware = (
|
|||||||
if (
|
if (
|
||||||
'type' in element &&
|
'type' in element &&
|
||||||
element.type === 'connector' &&
|
element.type === 'connector' &&
|
||||||
(props['mode'] !== undefined ||
|
(props['mode'] !== undefined || props['target'] || props['source'])
|
||||||
props['target'] ||
|
|
||||||
props['source'] ||
|
|
||||||
(props['xywh'] && !(element as ConnectorElementModel).updatingPath))
|
|
||||||
) {
|
) {
|
||||||
addToUpdateList(element as ConnectorElementModel);
|
addToUpdateList(element as ConnectorElementModel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ConnectorElementRendererExtension } from './element-renderer';
|
|||||||
import { ConnectorFilter } from './element-transform';
|
import { ConnectorFilter } from './element-transform';
|
||||||
import { connectorToolbarExtension } from './toolbar/config';
|
import { connectorToolbarExtension } from './toolbar/config';
|
||||||
import { connectorQuickTool } from './toolbar/quick-tool';
|
import { connectorQuickTool } from './toolbar/quick-tool';
|
||||||
import { ConnectorElementView } from './view/view';
|
import { ConnectorElementView, ConnectorInteraction } from './view/view';
|
||||||
|
|
||||||
export class ConnectorViewExtension extends ViewExtensionProvider {
|
export class ConnectorViewExtension extends ViewExtensionProvider {
|
||||||
override name = 'affine-connector-gfx';
|
override name = 'affine-connector-gfx';
|
||||||
@@ -30,6 +30,7 @@ export class ConnectorViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(connectorQuickTool);
|
context.register(connectorQuickTool);
|
||||||
context.register(connectorToolbarExtension);
|
context.register(connectorToolbarExtension);
|
||||||
context.register(ConnectionOverlay);
|
context.register(ConnectionOverlay);
|
||||||
|
context.register(ConnectorInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type DragStartContext,
|
type DragStartContext,
|
||||||
generateKeyBetween,
|
generateKeyBetween,
|
||||||
GfxElementModelView,
|
GfxElementModelView,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { mountConnectorLabelEditor } from '../text/edgeless-connector-label-editor';
|
import { mountConnectorLabelEditor } from '../text/edgeless-connector-label-editor';
|
||||||
@@ -174,3 +175,72 @@ export class ConnectorElementView extends GfxElementModelView<ConnectorElementMo
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ConnectorInteraction =
|
||||||
|
GfxViewInteractionExtension<ConnectorElementView>(ConnectorElementView.type, {
|
||||||
|
handleResize: ({ model, gfx }) => {
|
||||||
|
const initialPath = model.absolutePath;
|
||||||
|
|
||||||
|
return {
|
||||||
|
beforeResize(context): void {
|
||||||
|
const { elements } = context;
|
||||||
|
// show the handles only when connector is selected along with
|
||||||
|
// its source and target elements
|
||||||
|
if (
|
||||||
|
elements.length === 1 ||
|
||||||
|
(model.source.id &&
|
||||||
|
!elements.some(el => el.model.id === model.source.id)) ||
|
||||||
|
(model.target.id &&
|
||||||
|
!elements.some(el => el.model.id === model.target.id))
|
||||||
|
) {
|
||||||
|
context.set({
|
||||||
|
allowedHandlers: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeStart(): void {
|
||||||
|
model.stash('labelXYWH');
|
||||||
|
model.stash('source');
|
||||||
|
model.stash('target');
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeMove(context): void {
|
||||||
|
const { matrix } = context;
|
||||||
|
const props = model.resize(initialPath, matrix);
|
||||||
|
|
||||||
|
gfx.updateElement(model, props);
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeEnd(): void {
|
||||||
|
model.pop('labelXYWH');
|
||||||
|
model.pop('source');
|
||||||
|
model.pop('target');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleRotate({ model, gfx }) {
|
||||||
|
const initialPath = model.absolutePath;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onRotateStart(): void {
|
||||||
|
model.stash('labelXYWH');
|
||||||
|
model.stash('source');
|
||||||
|
model.stash('target');
|
||||||
|
},
|
||||||
|
|
||||||
|
onRotateMove(context): void {
|
||||||
|
const { matrix } = context;
|
||||||
|
const props = model.resize(initialPath, matrix);
|
||||||
|
|
||||||
|
gfx.updateElement(model, props);
|
||||||
|
},
|
||||||
|
|
||||||
|
onRotateEnd(): void {
|
||||||
|
model.pop('labelXYWH');
|
||||||
|
model.pop('source');
|
||||||
|
model.pop('target');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { GroupElementModel } from '@blocksuite/affine-model';
|
import { GroupElementModel } from '@blocksuite/affine-model';
|
||||||
import { GfxElementModelView } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
GfxElementModelView,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { mountGroupTitleEditor } from './text/edgeless-group-title-editor';
|
import { mountGroupTitleEditor } from './text/edgeless-group-title-editor';
|
||||||
|
|
||||||
@@ -29,3 +32,26 @@ export class GroupElementView extends GfxElementModelView<GroupElementModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const GroupInteraction = GfxViewInteractionExtension<GroupElementView>(
|
||||||
|
GroupElementView.type,
|
||||||
|
{
|
||||||
|
handleResize(context) {
|
||||||
|
const empty = () => {};
|
||||||
|
context.model.descendantElements.forEach(elm => {
|
||||||
|
if (elm instanceof GroupElementModel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.add(elm);
|
||||||
|
});
|
||||||
|
context.delete(context.model);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart: empty,
|
||||||
|
onResizeMove: empty,
|
||||||
|
onResizeEnd: empty,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
|
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
import { GroupElementRendererExtension } from './element-renderer';
|
import { GroupElementRendererExtension } from './element-renderer';
|
||||||
import { GroupElementView } from './element-view';
|
import { GroupElementView, GroupInteraction } from './element-view';
|
||||||
import { groupToolbarExtension } from './toolbar/config';
|
import { groupToolbarExtension } from './toolbar/config';
|
||||||
|
|
||||||
export class GroupViewExtension extends ViewExtensionProvider {
|
export class GroupViewExtension extends ViewExtensionProvider {
|
||||||
@@ -22,6 +22,7 @@ export class GroupViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(GroupElementView);
|
context.register(GroupElementView);
|
||||||
if (this.isEdgeless(context.scope)) {
|
if (this.isEdgeless(context.scope)) {
|
||||||
context.register(groupToolbarExtension);
|
context.register(groupToolbarExtension);
|
||||||
|
context.register(GroupInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
shapeMindmapToolbarExtension,
|
shapeMindmapToolbarExtension,
|
||||||
} from './toolbar/config';
|
} from './toolbar/config';
|
||||||
import { mindMapSeniorTool } from './toolbar/senior-tool';
|
import { mindMapSeniorTool } from './toolbar/senior-tool';
|
||||||
import { MindMapView } from './view/view';
|
import { MindMapInteraction, MindMapView } from './view/view';
|
||||||
|
|
||||||
export class MindmapViewExtension extends ViewExtensionProvider {
|
export class MindmapViewExtension extends ViewExtensionProvider {
|
||||||
override name = 'affine-mindmap-gfx';
|
override name = 'affine-mindmap-gfx';
|
||||||
@@ -31,5 +31,6 @@ export class MindmapViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(MindMapView);
|
context.register(MindMapView);
|
||||||
context.register(MindMapDragExtension);
|
context.register(MindMapDragExtension);
|
||||||
context.register(MindMapIndicatorOverlay);
|
context.register(MindMapIndicatorOverlay);
|
||||||
|
context.register(MindMapInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { PointerEventState } from '@blocksuite/std';
|
|||||||
import {
|
import {
|
||||||
type BoxSelectionContext,
|
type BoxSelectionContext,
|
||||||
GfxElementModelView,
|
GfxElementModelView,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
type SelectedContext,
|
type SelectedContext,
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
@@ -381,3 +382,12 @@ export class MindMapView extends GfxElementModelView<MindmapElementModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const MindMapInteraction = GfxViewInteractionExtension(
|
||||||
|
MindMapView.type,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
allowedHandlers: [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ShapeElementModel } from '@blocksuite/affine-model';
|
import { ShapeElementModel } from '@blocksuite/affine-model';
|
||||||
import { GfxElementModelView } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
GfxElementModelView,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
|
import { normalizeShapeBound } from './element-renderer';
|
||||||
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
|
import { mountShapeTextEditor } from './text/edgeless-shape-text-editor';
|
||||||
|
|
||||||
export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
||||||
@@ -26,3 +30,16 @@ export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ShapeViewInteraction =
|
||||||
|
GfxViewInteractionExtension<ShapeElementView>(ShapeElementView.type, {
|
||||||
|
handleResize: () => {
|
||||||
|
return {
|
||||||
|
onResizeMove({ newBound, model }) {
|
||||||
|
const normalizedBound = normalizeShapeBound(model, newBound);
|
||||||
|
|
||||||
|
model.xywh = normalizedBound.serialize();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
HighlighterElementRendererExtension,
|
HighlighterElementRendererExtension,
|
||||||
ShapeElementRendererExtension,
|
ShapeElementRendererExtension,
|
||||||
} from './element-renderer';
|
} from './element-renderer';
|
||||||
import { ShapeElementView } from './element-view';
|
import { ShapeElementView, ShapeViewInteraction } from './element-view';
|
||||||
import { ShapeTool } from './shape-tool';
|
import { ShapeTool } from './shape-tool';
|
||||||
import { shapeSeniorTool, shapeToolbarExtension } from './toolbar';
|
import { shapeSeniorTool, shapeToolbarExtension } from './toolbar';
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ export class ShapeViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(ShapeTool);
|
context.register(ShapeTool);
|
||||||
context.register(shapeSeniorTool);
|
context.register(shapeSeniorTool);
|
||||||
context.register(shapeToolbarExtension);
|
context.register(shapeToolbarExtension);
|
||||||
|
context.register(ShapeViewInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import type { TextElementModel } from '@blocksuite/affine-model';
|
import type { TextElementModel } from '@blocksuite/affine-model';
|
||||||
import { GfxElementModelView } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
GfxElementModelView,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { mountTextElementEditor } from './edgeless-text-editor';
|
import { mountTextElementEditor } from './edgeless-text-editor';
|
||||||
|
import { normalizeTextBound } from './element-renderer';
|
||||||
|
|
||||||
export class TextElementView extends GfxElementModelView<TextElementModel> {
|
export class TextElementView extends GfxElementModelView<TextElementModel> {
|
||||||
static override type: string = 'text';
|
static override type: string = 'text';
|
||||||
@@ -26,3 +30,66 @@ export class TextElementView extends GfxElementModelView<TextElementModel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TextInteraction = GfxViewInteractionExtension<TextElementView>(
|
||||||
|
TextElementView.type,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
lockRatio: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
|
||||||
|
},
|
||||||
|
handleResize({ model }) {
|
||||||
|
let initialFontSize = model.fontSize;
|
||||||
|
return {
|
||||||
|
onResizeStart(context) {
|
||||||
|
const { handle } = context;
|
||||||
|
|
||||||
|
context.default(context);
|
||||||
|
|
||||||
|
if (handle === 'left' || handle === 'right') {
|
||||||
|
model.stash('hasMaxWidth');
|
||||||
|
}
|
||||||
|
model.stash('fontSize');
|
||||||
|
},
|
||||||
|
onResizeMove(context) {
|
||||||
|
const { handle, newBound, originalBound } = context;
|
||||||
|
if (handle === 'left' || handle === 'right') {
|
||||||
|
const {
|
||||||
|
text: yText,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
hasMaxWidth,
|
||||||
|
} = model;
|
||||||
|
// If the width of the text element has been changed by dragging,
|
||||||
|
// We need to set hasMaxWidth to true for wrapping the text
|
||||||
|
const normalizedBound = normalizeTextBound(
|
||||||
|
{
|
||||||
|
yText,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontStyle,
|
||||||
|
fontWeight,
|
||||||
|
hasMaxWidth,
|
||||||
|
},
|
||||||
|
newBound,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
model.xywh = normalizedBound.serialize();
|
||||||
|
model.hasMaxWidth = true;
|
||||||
|
} else {
|
||||||
|
model.xywh = newBound.serialize();
|
||||||
|
model.fontSize = initialFontSize * (newBound.w / originalBound.w);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResizeEnd(context) {
|
||||||
|
context.default(context);
|
||||||
|
|
||||||
|
model.pop('fontSize');
|
||||||
|
model.pop('hasMaxWidth');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import { DblClickAddEdgelessText } from './dblclick-add-edgeless-text';
|
import { DblClickAddEdgelessText } from './dblclick-add-edgeless-text';
|
||||||
import { effects } from './effects';
|
import { effects } from './effects';
|
||||||
import { TextElementRendererExtension } from './element-renderer';
|
import { TextElementRendererExtension } from './element-renderer';
|
||||||
import { TextElementView } from './element-view';
|
import { TextElementView, TextInteraction } from './element-view';
|
||||||
import { TextTool } from './tool';
|
import { TextTool } from './tool';
|
||||||
import { textToolbarExtension } from './toolbar';
|
import { textToolbarExtension } from './toolbar';
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ export class TextViewExtension extends ViewExtensionProvider {
|
|||||||
context.register(TextTool);
|
context.register(TextTool);
|
||||||
context.register(textToolbarExtension);
|
context.register(textToolbarExtension);
|
||||||
context.register(DblClickAddEdgelessText);
|
context.register(DblClickAddEdgelessText);
|
||||||
|
context.register(TextInteraction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,28 +339,15 @@ export class ConnectorElementModel extends GfxPrimitiveElementModel<ConnectorEle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(bounds: Bound, originalPath: PointLocation[], matrix: DOMMatrix) {
|
resize(originalPath: PointLocation[], matrix: DOMMatrix) {
|
||||||
this.updatingPath = false;
|
this.updatingPath = false;
|
||||||
|
|
||||||
const path = this.resizePath(originalPath, matrix);
|
const path = this.resizePath(originalPath, matrix);
|
||||||
|
|
||||||
// the property assignment order matters
|
|
||||||
this.xywh = bounds.serialize();
|
|
||||||
this.path = path.map(p => p.clone().setVec(Vec.sub(p, bounds.tl)));
|
|
||||||
|
|
||||||
const props: {
|
const props: {
|
||||||
labelXYWH?: XYWH;
|
|
||||||
source?: Connection;
|
source?: Connection;
|
||||||
target?: Connection;
|
target?: Connection;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
// Updates Connector's Label position.
|
|
||||||
if (this.hasLabel()) {
|
|
||||||
const [cx, cy] = this.getPointByOffsetDistance(this.labelOffset.distance);
|
|
||||||
const [, , w, h] = this.labelXYWH!;
|
|
||||||
props.labelXYWH = [cx - w / 2, cy - h / 2, w, h];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.source.id) {
|
if (!this.source.id) {
|
||||||
props.source = {
|
props.source = {
|
||||||
...this.source,
|
...this.source,
|
||||||
|
|||||||
@@ -26,10 +26,21 @@ export type {
|
|||||||
ExtensionDragMoveContext,
|
ExtensionDragMoveContext,
|
||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
GfxInteractivityContext,
|
GfxInteractivityContext,
|
||||||
|
GfxViewInteractionConfig,
|
||||||
|
ResizeConstraint,
|
||||||
|
ResizeEndContext,
|
||||||
|
ResizeHandle,
|
||||||
|
ResizeMoveContext,
|
||||||
|
ResizeStartContext,
|
||||||
|
RotateConstraint,
|
||||||
|
RotateEndContext,
|
||||||
|
RotateMoveContext,
|
||||||
|
RotateStartContext,
|
||||||
SelectedContext,
|
SelectedContext,
|
||||||
} from './interactivity/index.js';
|
} from './interactivity/index.js';
|
||||||
export {
|
export {
|
||||||
GfxViewEventManager,
|
GfxViewEventManager,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
InteractivityExtension,
|
InteractivityExtension,
|
||||||
InteractivityIdentifier,
|
InteractivityIdentifier,
|
||||||
InteractivityManager,
|
InteractivityManager,
|
||||||
|
|||||||
118
blocksuite/framework/std/src/gfx/interactivity/extension/view.ts
Normal file
118
blocksuite/framework/std/src/gfx/interactivity/extension/view.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
|
import type { ExtensionType } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { BlockStdScope } from '../../../scope';
|
||||||
|
import type { GfxBlockComponent } from '../../../view';
|
||||||
|
import type { GfxController, GfxModel } from '../..';
|
||||||
|
import type { GfxElementModelView } from '../../view/view';
|
||||||
|
import type {
|
||||||
|
BeforeResizeContext,
|
||||||
|
BeforeRotateContext,
|
||||||
|
ResizeConstraint,
|
||||||
|
ResizeEndContext,
|
||||||
|
ResizeMoveContext,
|
||||||
|
ResizeStartContext,
|
||||||
|
RotateEndContext,
|
||||||
|
RotateMoveContext,
|
||||||
|
RotateStartContext,
|
||||||
|
} from '../types/view';
|
||||||
|
|
||||||
|
type ExtendedViewContext<
|
||||||
|
T extends GfxBlockComponent | GfxElementModelView,
|
||||||
|
Context,
|
||||||
|
> = {
|
||||||
|
/**
|
||||||
|
* The default function of the interaction.
|
||||||
|
* If the interaction is handled by the extension, the default function will not be executed.
|
||||||
|
* But extension can choose to call the default function by `context.default(context)` if needed.
|
||||||
|
*/
|
||||||
|
default: (context: Context) => void;
|
||||||
|
|
||||||
|
model: T['model'];
|
||||||
|
|
||||||
|
view: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewInteractionHandleContext<
|
||||||
|
T extends GfxBlockComponent | GfxElementModelView,
|
||||||
|
> = {
|
||||||
|
std: BlockStdScope;
|
||||||
|
gfx: GfxController;
|
||||||
|
view: T;
|
||||||
|
model: T['model'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to add an element to resize list.
|
||||||
|
* @param model
|
||||||
|
*/
|
||||||
|
add(element: GfxModel): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to remove an element from resize list.
|
||||||
|
* @param element
|
||||||
|
*/
|
||||||
|
delete(element: GfxModel): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GfxViewInteractionConfig<
|
||||||
|
T extends GfxBlockComponent | GfxElementModelView =
|
||||||
|
| GfxBlockComponent
|
||||||
|
| GfxElementModelView,
|
||||||
|
> = {
|
||||||
|
readonly resizeConstraint?: ResizeConstraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The function that will be called when the view is resized.
|
||||||
|
* You can add or delete the resize element before resize starts in this function.,
|
||||||
|
* And return handlers to customize the resize behavior.
|
||||||
|
* @param context
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
handleResize?: (context: ViewInteractionHandleContext<T>) => {
|
||||||
|
/**
|
||||||
|
* Called before resize starts. When this method is called, the resize elements are confirmed and will not be changed.
|
||||||
|
* You can set the resize constraint in this method.
|
||||||
|
* @param context
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
beforeResize?: (context: BeforeResizeContext) => void;
|
||||||
|
onResizeStart?(
|
||||||
|
context: ResizeStartContext & ExtendedViewContext<T, ResizeStartContext>
|
||||||
|
): void;
|
||||||
|
onResizeMove?(
|
||||||
|
context: ResizeMoveContext & ExtendedViewContext<T, ResizeMoveContext>
|
||||||
|
): void;
|
||||||
|
onResizeEnd?(
|
||||||
|
context: ResizeEndContext & ExtendedViewContext<T, ResizeEndContext>
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRotate?: (context: ViewInteractionHandleContext<T>) => {
|
||||||
|
beforeRotate?: (context: BeforeRotateContext) => void;
|
||||||
|
onRotateStart?(
|
||||||
|
context: RotateStartContext & ExtendedViewContext<T, RotateStartContext>
|
||||||
|
): void;
|
||||||
|
onRotateMove?(
|
||||||
|
context: RotateMoveContext & ExtendedViewContext<T, RotateMoveContext>
|
||||||
|
): void;
|
||||||
|
onRotateEnd?(
|
||||||
|
context: RotateEndContext & ExtendedViewContext<T, RotateEndContext>
|
||||||
|
): void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GfxViewInteractionIdentifier =
|
||||||
|
createIdentifier<GfxViewInteractionConfig>('GfxViewInteraction');
|
||||||
|
|
||||||
|
export function GfxViewInteractionExtension<
|
||||||
|
T extends GfxBlockComponent | GfxElementModelView,
|
||||||
|
>(viewType: string, config: GfxViewInteractionConfig<T>): ExtensionType {
|
||||||
|
return {
|
||||||
|
setup(di) {
|
||||||
|
di.addImpl(
|
||||||
|
GfxViewInteractionIdentifier(viewType),
|
||||||
|
() => config as GfxViewInteractionConfig
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
export type { GfxInteractivityContext } from './event.js';
|
export type { GfxInteractivityContext } from './event.js';
|
||||||
export { InteractivityExtension } from './extension/base.js';
|
export { InteractivityExtension } from './extension/base.js';
|
||||||
|
export {
|
||||||
|
type GfxViewInteractionConfig,
|
||||||
|
GfxViewInteractionExtension,
|
||||||
|
GfxViewInteractionIdentifier,
|
||||||
|
} from './extension/view.js';
|
||||||
export { GfxViewEventManager } from './gfx-view-event-handler.js';
|
export { GfxViewEventManager } from './gfx-view-event-handler.js';
|
||||||
export { InteractivityIdentifier, InteractivityManager } from './manager.js';
|
export { InteractivityIdentifier, InteractivityManager } from './manager.js';
|
||||||
|
export { type ResizeHandle } from './resize/manager.js';
|
||||||
export type {
|
export type {
|
||||||
DragExtensionInitializeContext,
|
DragExtensionInitializeContext,
|
||||||
DragInitializationOption,
|
DragInitializationOption,
|
||||||
@@ -15,5 +21,13 @@ export type {
|
|||||||
DragMoveContext,
|
DragMoveContext,
|
||||||
DragStartContext,
|
DragStartContext,
|
||||||
GfxViewTransformInterface,
|
GfxViewTransformInterface,
|
||||||
|
ResizeConstraint,
|
||||||
|
ResizeEndContext,
|
||||||
|
ResizeMoveContext,
|
||||||
|
ResizeStartContext,
|
||||||
|
RotateConstraint,
|
||||||
|
RotateEndContext,
|
||||||
|
RotateMoveContext,
|
||||||
|
RotateStartContext,
|
||||||
SelectedContext,
|
SelectedContext,
|
||||||
} from './types/view.js';
|
} from './types/view.js';
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
import { type ServiceIdentifier } from '@blocksuite/global/di';
|
import { type ServiceIdentifier } from '@blocksuite/global/di';
|
||||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||||
import { Bound, Point } from '@blocksuite/global/gfx';
|
import { Bound, clamp, Point } from '@blocksuite/global/gfx';
|
||||||
|
import { signal } from '@preact/signals-core';
|
||||||
|
|
||||||
import type { PointerEventState } from '../../event/state/pointer.js';
|
import type { PointerEventState } from '../../event/state/pointer.js';
|
||||||
import { getTopElements } from '../../utils/tree.js';
|
import { getTopElements } from '../../utils/tree.js';
|
||||||
|
import type { GfxBlockComponent } from '../../view/index.js';
|
||||||
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
||||||
|
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
||||||
import type { GfxModel } from '../model/model.js';
|
import type { GfxModel } from '../model/model.js';
|
||||||
|
import type { GfxElementModelView } from '../view/view.js';
|
||||||
import { createInteractionContext, type SupportedEvents } from './event.js';
|
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||||
import {
|
import {
|
||||||
type InteractivityActionAPI,
|
type InteractivityActionAPI,
|
||||||
type InteractivityEventAPI,
|
type InteractivityEventAPI,
|
||||||
InteractivityExtensionIdentifier,
|
InteractivityExtensionIdentifier,
|
||||||
} from './extension/base.js';
|
} from './extension/base.js';
|
||||||
|
import {
|
||||||
|
type GfxViewInteractionConfig,
|
||||||
|
GfxViewInteractionIdentifier,
|
||||||
|
} from './extension/view.js';
|
||||||
import { GfxViewEventManager } from './gfx-view-event-handler.js';
|
import { GfxViewEventManager } from './gfx-view-event-handler.js';
|
||||||
|
import {
|
||||||
|
DEFAULT_HANDLES,
|
||||||
|
type OptionResize,
|
||||||
|
ResizeController,
|
||||||
|
type ResizeHandle,
|
||||||
|
type RotateOption,
|
||||||
|
} from './resize/manager.js';
|
||||||
import type { RequestElementsCloneContext } from './types/clone.js';
|
import type { RequestElementsCloneContext } from './types/clone.js';
|
||||||
import type {
|
import type {
|
||||||
DragExtensionInitializeContext,
|
DragExtensionInitializeContext,
|
||||||
@@ -21,7 +36,11 @@ import type {
|
|||||||
ExtensionDragMoveContext,
|
ExtensionDragMoveContext,
|
||||||
ExtensionDragStartContext,
|
ExtensionDragStartContext,
|
||||||
} from './types/drag.js';
|
} from './types/drag.js';
|
||||||
import type { BoxSelectionContext } from './types/view.js';
|
import type {
|
||||||
|
BoxSelectionContext,
|
||||||
|
ResizeConstraint,
|
||||||
|
RotateConstraint,
|
||||||
|
} from './types/view.js';
|
||||||
|
|
||||||
type ExtensionPointerHandler = Exclude<
|
type ExtensionPointerHandler = Exclude<
|
||||||
SupportedEvents,
|
SupportedEvents,
|
||||||
@@ -46,6 +65,11 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeInteraction$ = signal<null | {
|
||||||
|
type: 'move' | 'resize' | 'rotate';
|
||||||
|
elements: GfxModel[];
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
override unmounted(): void {
|
override unmounted(): void {
|
||||||
this._disposable.dispose();
|
this._disposable.dispose();
|
||||||
this.interactExtensions.forEach(ext => {
|
this.interactExtensions.forEach(ext => {
|
||||||
@@ -76,6 +100,10 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
dispatchEvent(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
dispatchEvent(eventName: ExtensionPointerHandler, evt: PointerEventState) {
|
||||||
|
if (this.activeInteraction$.peek()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { context, preventDefaultState } = createInteractionContext(evt);
|
const { context, preventDefaultState } = createInteractionContext(evt);
|
||||||
const extensions = this.interactExtensions;
|
const extensions = this.interactExtensions;
|
||||||
|
|
||||||
@@ -247,6 +275,8 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const onDragEnd = (event: PointerEvent) => {
|
const onDragEnd = (event: PointerEvent) => {
|
||||||
|
this.activeInteraction$.value = null;
|
||||||
|
|
||||||
host.removeEventListener('pointermove', onDragMove, false);
|
host.removeEventListener('pointermove', onDragMove, false);
|
||||||
host.removeEventListener('pointerup', onDragEnd, false);
|
host.removeEventListener('pointerup', onDragEnd, false);
|
||||||
viewportWatcher.unsubscribe();
|
viewportWatcher.unsubscribe();
|
||||||
@@ -292,6 +322,11 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
host.addEventListener('pointerup', onDragEnd, false);
|
host.addEventListener('pointerup', onDragEnd, false);
|
||||||
};
|
};
|
||||||
const dragStart = () => {
|
const dragStart = () => {
|
||||||
|
this.activeInteraction$.value = {
|
||||||
|
type: 'move',
|
||||||
|
elements: context.elements,
|
||||||
|
};
|
||||||
|
|
||||||
internal.elements.forEach(({ view, originalBound }) => {
|
internal.elements.forEach(({ view, originalBound }) => {
|
||||||
view.onDragStart({
|
view.onDragStart({
|
||||||
currentBound: originalBound,
|
currentBound: originalBound,
|
||||||
@@ -316,6 +351,519 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
dragStart();
|
dragStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleElementRotate(
|
||||||
|
options: Omit<
|
||||||
|
RotateOption,
|
||||||
|
'onRotateStart' | 'onRotateEnd' | 'onRotateUpdate'
|
||||||
|
> & {
|
||||||
|
onRotateUpdate?: (payload: {
|
||||||
|
currentAngle: number;
|
||||||
|
delta: number;
|
||||||
|
}) => void;
|
||||||
|
onRotateStart?: () => void;
|
||||||
|
onRotateEnd?: () => void;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { rotatable, viewConfigMap, initialRotate } =
|
||||||
|
this._getViewRotateConfig(options.elements);
|
||||||
|
|
||||||
|
if (!rotatable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = new ResizeController({ gfx: this.gfx });
|
||||||
|
const elements = Array.from(viewConfigMap.values()).map(
|
||||||
|
config => config.view.model
|
||||||
|
) as GfxModel[];
|
||||||
|
|
||||||
|
handler.startRotate({
|
||||||
|
...options,
|
||||||
|
elements,
|
||||||
|
onRotateStart: payload => {
|
||||||
|
this.activeInteraction$.value = {
|
||||||
|
type: 'rotate',
|
||||||
|
elements,
|
||||||
|
};
|
||||||
|
options.onRotateStart?.();
|
||||||
|
payload.data.forEach(({ model }) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onRotateStart({
|
||||||
|
default: defaultHandlers.onRotateStart as () => void,
|
||||||
|
constraint,
|
||||||
|
model,
|
||||||
|
view,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onRotateUpdate: payload => {
|
||||||
|
options.onRotateUpdate?.({
|
||||||
|
currentAngle: initialRotate + payload.delta,
|
||||||
|
delta: payload.delta,
|
||||||
|
});
|
||||||
|
payload.data.forEach(
|
||||||
|
({
|
||||||
|
model,
|
||||||
|
newBound,
|
||||||
|
originalBound,
|
||||||
|
newRotate,
|
||||||
|
originalRotate,
|
||||||
|
matrix,
|
||||||
|
}) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onRotateMove({
|
||||||
|
model,
|
||||||
|
newBound,
|
||||||
|
originalBound,
|
||||||
|
newRotate,
|
||||||
|
originalRotate,
|
||||||
|
default: defaultHandlers.onRotateMove as () => void,
|
||||||
|
constraint,
|
||||||
|
view,
|
||||||
|
matrix,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onRotateEnd: payload => {
|
||||||
|
this.activeInteraction$.value = null;
|
||||||
|
options.onRotateEnd?.();
|
||||||
|
this.std.store.transact(() => {
|
||||||
|
payload.data.forEach(({ model }) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onRotateEnd({
|
||||||
|
default: defaultHandlers.onRotateEnd as () => void,
|
||||||
|
view,
|
||||||
|
model,
|
||||||
|
constraint,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getViewRotateConfig(elements: GfxModel[]) {
|
||||||
|
const deleted = new Set<GfxModel>();
|
||||||
|
const added = new Set<GfxModel>();
|
||||||
|
const del = (model: GfxModel) => {
|
||||||
|
deleted.add(model);
|
||||||
|
};
|
||||||
|
const add = (model: GfxModel) => {
|
||||||
|
added.add(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewRotateHandlers = Required<
|
||||||
|
ReturnType<Required<GfxViewInteractionConfig>['handleRotate']>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const viewConfigMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
model: GfxModel;
|
||||||
|
view: GfxElementModelView | GfxBlockComponent;
|
||||||
|
handlers: ViewRotateHandlers;
|
||||||
|
defaultHandlers: ViewRotateHandlers;
|
||||||
|
constraint: Required<RotateConstraint>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
const addToConfigMap = (model: GfxModel) => {
|
||||||
|
const flavourOrType = 'type' in model ? model.type : model.flavour;
|
||||||
|
const interactionConfig = this.std.getOptional(
|
||||||
|
GfxViewInteractionIdentifier(flavourOrType)
|
||||||
|
);
|
||||||
|
const view = this.gfx.view.get(model);
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHandlers: ViewRotateHandlers = {
|
||||||
|
beforeRotate: () => {},
|
||||||
|
onRotateStart: context => {
|
||||||
|
if (!context.constraint.rotatable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model instanceof GfxBlockElementModel) {
|
||||||
|
if (Object.hasOwn(model.props, 'rotate')) {
|
||||||
|
// @ts-expect-error prop existence has been checked
|
||||||
|
model.stash('rotate');
|
||||||
|
model.stash('xywh');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.stash('rotate');
|
||||||
|
model.stash('xywh');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRotateEnd: context => {
|
||||||
|
if (!context.constraint.rotatable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model instanceof GfxBlockElementModel) {
|
||||||
|
if (Object.hasOwn(model.props, 'rotate')) {
|
||||||
|
// @ts-expect-error prop existence has been checked
|
||||||
|
model.pop('rotate');
|
||||||
|
model.pop('xywh');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.pop('rotate');
|
||||||
|
model.pop('xywh');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRotateMove: context => {
|
||||||
|
if (!context.constraint.rotatable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newBound, newRotate } = context;
|
||||||
|
model.rotate = newRotate;
|
||||||
|
model.xywh = newBound.serialize();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const handlers = interactionConfig?.handleRotate?.({
|
||||||
|
std: this.std,
|
||||||
|
gfx: this.gfx,
|
||||||
|
view,
|
||||||
|
model,
|
||||||
|
delete: del,
|
||||||
|
add,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewConfigMap.set(model.id, {
|
||||||
|
model,
|
||||||
|
view,
|
||||||
|
defaultHandlers,
|
||||||
|
handlers: Object.assign({}, defaultHandlers, handlers ?? {}),
|
||||||
|
constraint: {
|
||||||
|
rotatable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
elements.forEach(addToConfigMap);
|
||||||
|
|
||||||
|
deleted.forEach(model => {
|
||||||
|
if (viewConfigMap.has(model.id)) {
|
||||||
|
viewConfigMap.delete(model.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
added.forEach(model => {
|
||||||
|
if (viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToConfigMap(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
const views = Array.from(viewConfigMap.values().map(item => item.view));
|
||||||
|
|
||||||
|
let rotatable = true;
|
||||||
|
viewConfigMap.forEach(config => {
|
||||||
|
const handlers = config.handlers;
|
||||||
|
|
||||||
|
handlers.beforeRotate({
|
||||||
|
set: (newConstraint: RotateConstraint) => {
|
||||||
|
Object.assign(config.constraint, newConstraint);
|
||||||
|
rotatable = rotatable && config.constraint.rotatable;
|
||||||
|
},
|
||||||
|
elements: views,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
initialRotate: views.length > 1 ? 0 : (views[0]?.model.rotate ?? 0),
|
||||||
|
rotatable,
|
||||||
|
viewConfigMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getViewResizeConfig(elements: GfxModel[]) {
|
||||||
|
const deleted = new Set<GfxModel>();
|
||||||
|
const added = new Set<GfxModel>();
|
||||||
|
const del = (model: GfxModel) => {
|
||||||
|
deleted.add(model);
|
||||||
|
};
|
||||||
|
const add = (model: GfxModel) => {
|
||||||
|
added.add(model);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ViewResizeHandlers = Required<
|
||||||
|
ReturnType<Required<GfxViewInteractionConfig>['handleResize']>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const viewConfigMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
model: GfxModel;
|
||||||
|
view: GfxElementModelView | GfxBlockComponent;
|
||||||
|
constraint: Required<ResizeConstraint>;
|
||||||
|
handlers: ViewResizeHandlers;
|
||||||
|
defaultHandlers: ViewResizeHandlers;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
const addToConfigMap = (model: GfxModel) => {
|
||||||
|
const flavourOrType = 'type' in model ? model.type : model.flavour;
|
||||||
|
const interactionConfig = this.std.getOptional(
|
||||||
|
GfxViewInteractionIdentifier(flavourOrType)
|
||||||
|
);
|
||||||
|
const view = this.gfx.view.get(model);
|
||||||
|
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultHandlers: ViewResizeHandlers = {
|
||||||
|
beforeResize: () => {},
|
||||||
|
onResizeStart: () => {
|
||||||
|
model.stash('xywh');
|
||||||
|
},
|
||||||
|
onResizeEnd: () => {
|
||||||
|
model.pop('xywh');
|
||||||
|
},
|
||||||
|
onResizeMove: context => {
|
||||||
|
const { newBound, constraint } = context;
|
||||||
|
const { minWidth, minHeight, maxWidth, maxHeight } = constraint;
|
||||||
|
|
||||||
|
newBound.w = clamp(newBound.w, minWidth, maxWidth);
|
||||||
|
newBound.h = clamp(newBound.h, minHeight, maxHeight);
|
||||||
|
|
||||||
|
model.xywh = newBound.serialize();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const handlers = interactionConfig?.handleResize?.({
|
||||||
|
std: this.std,
|
||||||
|
gfx: this.gfx,
|
||||||
|
view,
|
||||||
|
model,
|
||||||
|
delete: del,
|
||||||
|
add,
|
||||||
|
});
|
||||||
|
|
||||||
|
viewConfigMap.set(model.id, {
|
||||||
|
model,
|
||||||
|
view,
|
||||||
|
constraint: {
|
||||||
|
lockRatio: false,
|
||||||
|
allowedHandlers: DEFAULT_HANDLES,
|
||||||
|
minHeight: 2,
|
||||||
|
minWidth: 2,
|
||||||
|
maxHeight: 5000000,
|
||||||
|
maxWidth: 5000000,
|
||||||
|
...interactionConfig?.resizeConstraint,
|
||||||
|
},
|
||||||
|
defaultHandlers,
|
||||||
|
handlers: Object.assign({}, defaultHandlers, handlers ?? {}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
elements.forEach(addToConfigMap);
|
||||||
|
|
||||||
|
deleted.forEach(model => {
|
||||||
|
if (viewConfigMap.has(model.id)) {
|
||||||
|
viewConfigMap.delete(model.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
added.forEach(model => {
|
||||||
|
if (viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addToConfigMap(model);
|
||||||
|
});
|
||||||
|
|
||||||
|
const views = Array.from(viewConfigMap.values().map(item => item.view));
|
||||||
|
let allowedHandlers = new Set(DEFAULT_HANDLES);
|
||||||
|
|
||||||
|
viewConfigMap.forEach(config => {
|
||||||
|
const currConstraint: Required<ResizeConstraint> = config.constraint;
|
||||||
|
|
||||||
|
config.handlers.beforeResize({
|
||||||
|
set: (newConstraint: ResizeConstraint) => {
|
||||||
|
Object.assign(currConstraint, newConstraint);
|
||||||
|
},
|
||||||
|
elements: views,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.constraint = currConstraint;
|
||||||
|
|
||||||
|
const currentAllowedHandlers = new Set(currConstraint.allowedHandlers);
|
||||||
|
allowedHandlers.forEach(h => {
|
||||||
|
if (!currentAllowedHandlers.has(h)) {
|
||||||
|
allowedHandlers.delete(h);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowedHandlers: Array.from(allowedHandlers) as ResizeHandle[],
|
||||||
|
viewConfigMap,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRotateConfig(options: { elements: GfxModel[] }) {
|
||||||
|
return this._getViewRotateConfig(options.elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
getResizeHandlers(options: { elements: GfxModel[] }) {
|
||||||
|
return this._getViewResizeConfig(options.elements).allowedHandlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleElementResize(
|
||||||
|
options: Omit<
|
||||||
|
OptionResize,
|
||||||
|
'lockRatio' | 'onResizeStart' | 'onResizeEnd' | 'onResizeUpdate'
|
||||||
|
> & {
|
||||||
|
onResizeStart?: () => void;
|
||||||
|
onResizeEnd?: () => void;
|
||||||
|
onResizeUpdate?: (payload: {
|
||||||
|
lockRatio: boolean;
|
||||||
|
scaleX: number;
|
||||||
|
scaleY: number;
|
||||||
|
exceed: {
|
||||||
|
w: boolean;
|
||||||
|
h: boolean;
|
||||||
|
};
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { viewConfigMap, allowedHandlers } = this._getViewResizeConfig(
|
||||||
|
options.elements
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allowedHandlers.includes(options.handle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handle } = options;
|
||||||
|
const controller = new ResizeController({ gfx: this.gfx });
|
||||||
|
const elements = Array.from(viewConfigMap.values()).map(
|
||||||
|
config => config.view.model
|
||||||
|
) as GfxModel[];
|
||||||
|
let lockRatio = false;
|
||||||
|
|
||||||
|
viewConfigMap.forEach(config => {
|
||||||
|
const { lockRatio: lockRatioConfig } = config.constraint;
|
||||||
|
|
||||||
|
lockRatio =
|
||||||
|
lockRatio ||
|
||||||
|
lockRatioConfig === true ||
|
||||||
|
(Array.isArray(lockRatioConfig) && lockRatioConfig.includes(handle));
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.startResize({
|
||||||
|
...options,
|
||||||
|
lockRatio,
|
||||||
|
elements,
|
||||||
|
onResizeStart: ({ data }) => {
|
||||||
|
this.activeInteraction$.value = {
|
||||||
|
type: 'resize',
|
||||||
|
elements,
|
||||||
|
};
|
||||||
|
options.onResizeStart?.();
|
||||||
|
data.forEach(({ model }) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onResizeStart({
|
||||||
|
handle,
|
||||||
|
default: defaultHandlers.onResizeStart as () => void,
|
||||||
|
constraint,
|
||||||
|
model,
|
||||||
|
view,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onResizeUpdate: ({ data, scaleX, scaleY, lockRatio }) => {
|
||||||
|
const exceed = {
|
||||||
|
w: false,
|
||||||
|
h: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
data.forEach(
|
||||||
|
({ model, newBound, originalBound, lockRatio, matrix }) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onResizeMove({
|
||||||
|
model,
|
||||||
|
newBound,
|
||||||
|
originalBound,
|
||||||
|
handle,
|
||||||
|
default: defaultHandlers.onResizeMove as () => void,
|
||||||
|
constraint,
|
||||||
|
view,
|
||||||
|
lockRatio,
|
||||||
|
matrix,
|
||||||
|
});
|
||||||
|
|
||||||
|
exceed.w =
|
||||||
|
exceed.w ||
|
||||||
|
model.w === constraint.minWidth ||
|
||||||
|
model.w === constraint.maxWidth;
|
||||||
|
exceed.h =
|
||||||
|
exceed.h ||
|
||||||
|
model.h === constraint.minHeight ||
|
||||||
|
model.h === constraint.maxHeight;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
options.onResizeUpdate?.({ scaleX, scaleY, lockRatio, exceed });
|
||||||
|
},
|
||||||
|
onResizeEnd: ({ data }) => {
|
||||||
|
this.activeInteraction$.value = null;
|
||||||
|
options.onResizeEnd?.();
|
||||||
|
this.std.store.transact(() => {
|
||||||
|
data.forEach(({ model }) => {
|
||||||
|
if (!viewConfigMap.has(model.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { handlers, defaultHandlers, view, constraint } =
|
||||||
|
viewConfigMap.get(model.id)!;
|
||||||
|
|
||||||
|
handlers.onResizeEnd({
|
||||||
|
default: defaultHandlers.onResizeEnd as () => void,
|
||||||
|
view,
|
||||||
|
model,
|
||||||
|
constraint,
|
||||||
|
handle,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
requestElementClone(options: RequestElementsCloneContext) {
|
requestElementClone(options: RequestElementsCloneContext) {
|
||||||
const extensions = this.interactExtensions;
|
const extensions = this.interactExtensions;
|
||||||
|
|
||||||
|
|||||||
558
blocksuite/framework/std/src/gfx/interactivity/resize/manager.ts
Normal file
558
blocksuite/framework/std/src/gfx/interactivity/resize/manager.ts
Normal file
@@ -0,0 +1,558 @@
|
|||||||
|
import {
|
||||||
|
Bound,
|
||||||
|
getCommonBoundWithRotation,
|
||||||
|
type IVec,
|
||||||
|
} from '@blocksuite/global/gfx';
|
||||||
|
|
||||||
|
import type { GfxController } from '../..';
|
||||||
|
import type { GfxModel } from '../../model/model';
|
||||||
|
|
||||||
|
export type ResizeHandle =
|
||||||
|
| 'top-left'
|
||||||
|
| 'top'
|
||||||
|
| 'top-right'
|
||||||
|
| 'right'
|
||||||
|
| 'bottom-right'
|
||||||
|
| 'bottom'
|
||||||
|
| 'bottom-left'
|
||||||
|
| 'left';
|
||||||
|
|
||||||
|
export const DEFAULT_HANDLES: ResizeHandle[] = [
|
||||||
|
'top-left',
|
||||||
|
'top-right',
|
||||||
|
'bottom-left',
|
||||||
|
'bottom-right',
|
||||||
|
'left',
|
||||||
|
'right',
|
||||||
|
'top',
|
||||||
|
'bottom',
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ElementInitialSnapshot {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
rotate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OptionResize {
|
||||||
|
elements: GfxModel[];
|
||||||
|
handle: ResizeHandle;
|
||||||
|
lockRatio: boolean;
|
||||||
|
event: PointerEvent;
|
||||||
|
onResizeUpdate: (payload: {
|
||||||
|
lockRatio: boolean;
|
||||||
|
scaleX: number;
|
||||||
|
scaleY: number;
|
||||||
|
data: {
|
||||||
|
model: GfxModel;
|
||||||
|
originalBound: Bound;
|
||||||
|
newBound: Bound;
|
||||||
|
lockRatio: boolean;
|
||||||
|
matrix: DOMMatrix;
|
||||||
|
}[];
|
||||||
|
}) => void;
|
||||||
|
onResizeStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||||
|
onResizeEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RotateOption = {
|
||||||
|
elements: GfxModel[];
|
||||||
|
event: PointerEvent;
|
||||||
|
|
||||||
|
onRotateUpdate: (payload: {
|
||||||
|
delta: number;
|
||||||
|
data: {
|
||||||
|
model: GfxModel;
|
||||||
|
newBound: Bound;
|
||||||
|
originalBound: Bound;
|
||||||
|
originalRotate: number;
|
||||||
|
newRotate: number;
|
||||||
|
matrix: DOMMatrix;
|
||||||
|
}[];
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
onRotateStart?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||||
|
|
||||||
|
onRotateEnd?: (payload: { data: { model: GfxModel }[] }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ResizeController {
|
||||||
|
private readonly gfx: GfxController;
|
||||||
|
|
||||||
|
get host() {
|
||||||
|
return this.gfx.std.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(option: { gfx: GfxController }) {
|
||||||
|
this.gfx = option.gfx;
|
||||||
|
}
|
||||||
|
|
||||||
|
startResize(options: OptionResize) {
|
||||||
|
const {
|
||||||
|
elements,
|
||||||
|
handle,
|
||||||
|
lockRatio,
|
||||||
|
onResizeStart,
|
||||||
|
onResizeUpdate,
|
||||||
|
onResizeEnd,
|
||||||
|
event,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||||
|
x: el.x,
|
||||||
|
y: el.y,
|
||||||
|
w: el.w,
|
||||||
|
h: el.h,
|
||||||
|
rotate: el.rotate,
|
||||||
|
}));
|
||||||
|
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
|
event.clientX,
|
||||||
|
event.clientY,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
|
const currPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
|
e.clientX,
|
||||||
|
e.clientY,
|
||||||
|
]);
|
||||||
|
const shouldLockRatio = lockRatio || e.shiftKey;
|
||||||
|
|
||||||
|
if (elements.length === 1) {
|
||||||
|
this.resizeSingle(
|
||||||
|
originals[0],
|
||||||
|
elements[0],
|
||||||
|
shouldLockRatio,
|
||||||
|
startPt,
|
||||||
|
currPt,
|
||||||
|
handle,
|
||||||
|
onResizeUpdate
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.resizeMulti(
|
||||||
|
originals,
|
||||||
|
elements,
|
||||||
|
handle,
|
||||||
|
currPt,
|
||||||
|
startPt,
|
||||||
|
onResizeUpdate
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onResizeStart?.({ data: elements.map(model => ({ model })) });
|
||||||
|
|
||||||
|
const onPointerUp = () => {
|
||||||
|
this.host.removeEventListener('pointermove', onPointerMove);
|
||||||
|
this.host.removeEventListener('pointerup', onPointerUp);
|
||||||
|
|
||||||
|
onResizeEnd?.({ data: elements.map(model => ({ model })) });
|
||||||
|
};
|
||||||
|
|
||||||
|
this.host.addEventListener('pointermove', onPointerMove);
|
||||||
|
this.host.addEventListener('pointerup', onPointerUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeSingle(
|
||||||
|
orig: ElementInitialSnapshot,
|
||||||
|
model: GfxModel,
|
||||||
|
lockRatio: boolean,
|
||||||
|
startPt: IVec,
|
||||||
|
currPt: IVec,
|
||||||
|
handle: ResizeHandle,
|
||||||
|
updateCallback: OptionResize['onResizeUpdate']
|
||||||
|
) {
|
||||||
|
const { xSign, ySign } = this.getHandleSign(handle);
|
||||||
|
|
||||||
|
const pivot = new DOMPoint(
|
||||||
|
orig.x + (-xSign === 1 ? orig.w : 0),
|
||||||
|
orig.y + (-ySign === 1 ? orig.h : 0)
|
||||||
|
);
|
||||||
|
const toLocalRotatedM = new DOMMatrix()
|
||||||
|
.translate(-pivot.x, -pivot.y)
|
||||||
|
.translate(orig.w / 2 + orig.x, orig.h / 2 + orig.y)
|
||||||
|
.rotate(-orig.rotate)
|
||||||
|
.translate(-(orig.w / 2 + orig.x), -(orig.h / 2 + orig.y));
|
||||||
|
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||||
|
|
||||||
|
const toLocal = (p: DOMPoint, withRotation: boolean) =>
|
||||||
|
p.matrixTransform(withRotation ? toLocalRotatedM : toLocalM);
|
||||||
|
const toModel = (p: DOMPoint) =>
|
||||||
|
p.matrixTransform(toLocalRotatedM.inverse());
|
||||||
|
|
||||||
|
const currPtLocal = toLocal(new DOMPoint(currPt[0], currPt[1]), true);
|
||||||
|
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]), true);
|
||||||
|
|
||||||
|
let scaleX = xSign
|
||||||
|
? (xSign * (currPtLocal.x - handleLocal.x) + orig.w) / orig.w
|
||||||
|
: 1;
|
||||||
|
let scaleY = ySign
|
||||||
|
? (ySign * (currPtLocal.y - handleLocal.y) + orig.h) / orig.h
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
if (lockRatio) {
|
||||||
|
const min = Math.min(Math.abs(scaleX), Math.abs(scaleY));
|
||||||
|
scaleX = Math.sign(scaleX) * min;
|
||||||
|
scaleY = Math.sign(scaleY) * min;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||||
|
|
||||||
|
const [visualTopLeft, visualBottomRight] = [
|
||||||
|
new DOMPoint(orig.x, orig.y),
|
||||||
|
new DOMPoint(orig.x + orig.w, orig.y + orig.h),
|
||||||
|
].map(p => {
|
||||||
|
const localP = toLocal(p, false);
|
||||||
|
const scaledP = localP.matrixTransform(scaleM);
|
||||||
|
|
||||||
|
return toModel(scaledP);
|
||||||
|
});
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x:
|
||||||
|
Math.min(visualTopLeft.x, visualBottomRight.x) +
|
||||||
|
Math.abs(visualBottomRight.x - visualTopLeft.x) / 2,
|
||||||
|
y:
|
||||||
|
Math.min(visualTopLeft.y, visualBottomRight.y) +
|
||||||
|
Math.abs(visualBottomRight.y - visualTopLeft.y) / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreM = new DOMMatrix()
|
||||||
|
.translate(center.x, center.y)
|
||||||
|
.rotate(-orig.rotate)
|
||||||
|
.translate(-center.x, -center.y);
|
||||||
|
|
||||||
|
// only used to provide the matrix information
|
||||||
|
const finalM = restoreM
|
||||||
|
.multiply(toLocalRotatedM.inverse())
|
||||||
|
.multiply(scaleM)
|
||||||
|
.multiply(toLocalM);
|
||||||
|
|
||||||
|
const [topLeft, bottomRight] = [visualTopLeft, visualBottomRight].map(p => {
|
||||||
|
return p.matrixTransform(restoreM);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCallback({
|
||||||
|
lockRatio,
|
||||||
|
scaleX,
|
||||||
|
scaleY,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
model: model,
|
||||||
|
originalBound: new Bound(orig.x, orig.y, orig.w, orig.h),
|
||||||
|
newBound: new Bound(
|
||||||
|
Math.min(topLeft.x, bottomRight.x),
|
||||||
|
Math.min(bottomRight.y, topLeft.y),
|
||||||
|
Math.abs(bottomRight.x - topLeft.x),
|
||||||
|
Math.abs(bottomRight.y - topLeft.y)
|
||||||
|
),
|
||||||
|
lockRatio: lockRatio,
|
||||||
|
matrix: finalM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeMulti(
|
||||||
|
originals: ElementInitialSnapshot[],
|
||||||
|
elements: GfxModel[],
|
||||||
|
handle: ResizeHandle,
|
||||||
|
currPt: IVec,
|
||||||
|
startPt: IVec,
|
||||||
|
updateCallback: OptionResize['onResizeUpdate']
|
||||||
|
) {
|
||||||
|
const commonBound = getCommonBoundWithRotation(originals);
|
||||||
|
const { xSign, ySign } = this.getHandleSign(handle);
|
||||||
|
|
||||||
|
const pivot = new DOMPoint(
|
||||||
|
commonBound.x + ((-xSign + 1) / 2) * commonBound.w,
|
||||||
|
commonBound.y + ((-ySign + 1) / 2) * commonBound.h
|
||||||
|
);
|
||||||
|
const toLocalM = new DOMMatrix().translate(-pivot.x, -pivot.y);
|
||||||
|
|
||||||
|
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||||
|
|
||||||
|
const currPtLocal = toLocal(new DOMPoint(currPt[0], currPt[1]));
|
||||||
|
const handleLocal = toLocal(new DOMPoint(startPt[0], startPt[1]));
|
||||||
|
|
||||||
|
let scaleX = xSign
|
||||||
|
? (xSign * (currPtLocal.x - handleLocal.x) + commonBound.w) /
|
||||||
|
commonBound.w
|
||||||
|
: 1;
|
||||||
|
let scaleY = ySign
|
||||||
|
? (ySign * (currPtLocal.y - handleLocal.y) + commonBound.h) /
|
||||||
|
commonBound.h
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const min = Math.max(Math.abs(scaleX), Math.abs(scaleY));
|
||||||
|
scaleX = Math.sign(scaleX) * min;
|
||||||
|
scaleY = Math.sign(scaleY) * min;
|
||||||
|
|
||||||
|
const scaleM = new DOMMatrix().scale(scaleX, scaleY);
|
||||||
|
|
||||||
|
const data = elements.map((model, i) => {
|
||||||
|
const orig = originals[i];
|
||||||
|
const finalM = new DOMMatrix()
|
||||||
|
.multiply(toLocalM.inverse())
|
||||||
|
.multiply(scaleM)
|
||||||
|
.multiply(toLocalM);
|
||||||
|
const [topLeft, bottomRight] = [
|
||||||
|
new DOMPoint(orig.x, orig.y),
|
||||||
|
new DOMPoint(orig.x + orig.w, orig.y + orig.h),
|
||||||
|
].map(p => {
|
||||||
|
return p.matrixTransform(finalM);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newBound = new Bound(
|
||||||
|
Math.min(topLeft.x, bottomRight.x),
|
||||||
|
Math.min(bottomRight.y, topLeft.y),
|
||||||
|
Math.abs(bottomRight.x - topLeft.x),
|
||||||
|
Math.abs(bottomRight.y - topLeft.y)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
originalBound: new Bound(orig.x, orig.y, orig.w, orig.h),
|
||||||
|
newBound,
|
||||||
|
lockRatio: true,
|
||||||
|
matrix: finalM,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCallback({ lockRatio: true, scaleX, scaleY, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
startRotate(option: RotateOption) {
|
||||||
|
const { event, elements, onRotateUpdate } = option;
|
||||||
|
|
||||||
|
const originals: ElementInitialSnapshot[] = elements.map(el => ({
|
||||||
|
x: el.x,
|
||||||
|
y: el.y,
|
||||||
|
w: el.w,
|
||||||
|
h: el.h,
|
||||||
|
rotate: el.rotate,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const startPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
|
event.clientX,
|
||||||
|
event.clientY,
|
||||||
|
]);
|
||||||
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
|
const currentPt = this.gfx.viewport.toModelCoordFromClientCoord([
|
||||||
|
e.clientX,
|
||||||
|
e.clientY,
|
||||||
|
]);
|
||||||
|
const snap = e.shiftKey;
|
||||||
|
|
||||||
|
if (elements.length > 1) {
|
||||||
|
this.rotateMulti({
|
||||||
|
origs: originals,
|
||||||
|
models: elements,
|
||||||
|
startPt,
|
||||||
|
currentPt,
|
||||||
|
snap,
|
||||||
|
onRotateUpdate,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.rotateSingle({
|
||||||
|
orig: originals[0],
|
||||||
|
model: elements[0],
|
||||||
|
startPt,
|
||||||
|
currentPt,
|
||||||
|
snap,
|
||||||
|
onRotateUpdate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onPointerUp = () => {
|
||||||
|
this.host.removeEventListener('pointermove', onPointerMove);
|
||||||
|
this.host.removeEventListener('pointerup', onPointerUp);
|
||||||
|
this.host.removeEventListener('pointercancel', onPointerUp);
|
||||||
|
|
||||||
|
option.onRotateEnd?.({ data: elements.map(model => ({ model })) });
|
||||||
|
};
|
||||||
|
|
||||||
|
option.onRotateStart?.({ data: elements.map(model => ({ model })) });
|
||||||
|
|
||||||
|
this.host.addEventListener('pointermove', onPointerMove, false);
|
||||||
|
this.host.addEventListener('pointerup', onPointerUp, false);
|
||||||
|
this.host.addEventListener('pointercancel', onPointerUp, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNormalizedAngle(y: number, x: number) {
|
||||||
|
let angle = Math.atan2(y, x);
|
||||||
|
if (angle < 0) {
|
||||||
|
angle += 2 * Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (angle * 180) / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
private toNormalizedAngle(angle: number) {
|
||||||
|
if (angle < 0) {
|
||||||
|
angle += 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(angle) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rotateSingle(option: {
|
||||||
|
orig: ElementInitialSnapshot;
|
||||||
|
model: GfxModel;
|
||||||
|
startPt: IVec;
|
||||||
|
currentPt: IVec;
|
||||||
|
snap: boolean;
|
||||||
|
onRotateUpdate?: RotateOption['onRotateUpdate'];
|
||||||
|
}) {
|
||||||
|
const { orig, model, startPt, currentPt, snap, onRotateUpdate } = option;
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: orig.x + orig.w / 2,
|
||||||
|
y: orig.y + orig.h / 2,
|
||||||
|
};
|
||||||
|
const toLocalM = new DOMMatrix().translate(-center.x, -center.y);
|
||||||
|
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||||
|
|
||||||
|
const v0 = toLocal(new DOMPoint(startPt[0], startPt[1])),
|
||||||
|
v1 = toLocal(new DOMPoint(currentPt[0], currentPt[1]));
|
||||||
|
|
||||||
|
const startAngle = this.getNormalizedAngle(v0.y, v0.x),
|
||||||
|
endAngle = this.getNormalizedAngle(v1.y, v1.x);
|
||||||
|
const deltaDeg = endAngle - startAngle;
|
||||||
|
const rotatedAngle = orig.rotate + deltaDeg;
|
||||||
|
const targetRotate = this.toNormalizedAngle(
|
||||||
|
snap
|
||||||
|
? Math.round((rotatedAngle % 15) / 15) * 15 +
|
||||||
|
Math.floor(rotatedAngle / 15) * 15
|
||||||
|
: rotatedAngle
|
||||||
|
);
|
||||||
|
|
||||||
|
// only used to provide the matrix information
|
||||||
|
const rotateM = new DOMMatrix()
|
||||||
|
.translate(center.x, center.y)
|
||||||
|
.rotate(targetRotate - orig.rotate)
|
||||||
|
.translate(-center.x, -center.y);
|
||||||
|
|
||||||
|
onRotateUpdate?.({
|
||||||
|
delta: deltaDeg,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
model,
|
||||||
|
originalBound: new Bound(orig.x, orig.y, orig.w, orig.h),
|
||||||
|
newBound: new Bound(orig.x, orig.y, orig.w, orig.h),
|
||||||
|
originalRotate: orig.rotate,
|
||||||
|
newRotate: targetRotate,
|
||||||
|
matrix: rotateM,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private rotateMulti(option: {
|
||||||
|
origs: ElementInitialSnapshot[];
|
||||||
|
models: GfxModel[];
|
||||||
|
startPt: IVec;
|
||||||
|
currentPt: IVec;
|
||||||
|
snap: boolean;
|
||||||
|
onRotateUpdate?: RotateOption['onRotateUpdate'];
|
||||||
|
}) {
|
||||||
|
const { models, startPt, currentPt, onRotateUpdate } = option;
|
||||||
|
const commonBound = getCommonBoundWithRotation(option.origs);
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: commonBound.x + commonBound.w / 2,
|
||||||
|
y: commonBound.y + commonBound.h / 2,
|
||||||
|
};
|
||||||
|
const toLocalM = new DOMMatrix().translate(-center.x, -center.y);
|
||||||
|
const toLocal = (p: DOMPoint) => p.matrixTransform(toLocalM);
|
||||||
|
|
||||||
|
const v0 = toLocal(new DOMPoint(startPt[0], startPt[1])),
|
||||||
|
v1 = toLocal(new DOMPoint(currentPt[0], currentPt[1]));
|
||||||
|
const a0 = this.getNormalizedAngle(v0.y, v0.x),
|
||||||
|
a1 = this.getNormalizedAngle(v1.y, v1.x);
|
||||||
|
const deltaDeg = a1 - a0;
|
||||||
|
const rotateM = new DOMMatrix()
|
||||||
|
.translate(center.x, center.y)
|
||||||
|
.rotate(deltaDeg)
|
||||||
|
.translate(-center.x, -center.y);
|
||||||
|
const toRotatedPoint = (p: DOMPoint) => p.matrixTransform(rotateM);
|
||||||
|
|
||||||
|
onRotateUpdate?.({
|
||||||
|
delta: deltaDeg,
|
||||||
|
data: models.map((model, index) => {
|
||||||
|
const orig = option.origs[index];
|
||||||
|
const center = {
|
||||||
|
x: orig.x + orig.w / 2,
|
||||||
|
y: orig.y + orig.h / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const visualM = new DOMMatrix()
|
||||||
|
.translate(center.x, center.y)
|
||||||
|
.rotate(orig.rotate)
|
||||||
|
.translate(-center.x, -center.y);
|
||||||
|
const toVisual = (p: DOMPoint) => p.matrixTransform(visualM);
|
||||||
|
|
||||||
|
const [rotatedVisualLeftTop, rotatedVisualBottomRight] = [
|
||||||
|
new DOMPoint(orig.x, orig.y),
|
||||||
|
new DOMPoint(orig.x + orig.w, orig.y + orig.h),
|
||||||
|
].map(p => toRotatedPoint(toVisual(p)));
|
||||||
|
|
||||||
|
const newCenter = {
|
||||||
|
x:
|
||||||
|
Math.min(rotatedVisualLeftTop.x, rotatedVisualBottomRight.x) +
|
||||||
|
Math.abs(rotatedVisualBottomRight.x - rotatedVisualLeftTop.x) / 2,
|
||||||
|
y:
|
||||||
|
Math.min(rotatedVisualLeftTop.y, rotatedVisualBottomRight.y) +
|
||||||
|
Math.abs(rotatedVisualBottomRight.y - rotatedVisualLeftTop.y) / 2,
|
||||||
|
};
|
||||||
|
const newRotated = this.toNormalizedAngle(orig.rotate + deltaDeg);
|
||||||
|
const finalM = new DOMMatrix()
|
||||||
|
.translate(newCenter.x, newCenter.y)
|
||||||
|
.rotate(-newRotated)
|
||||||
|
.translate(-newCenter.x, -newCenter.y)
|
||||||
|
.multiply(rotateM)
|
||||||
|
.multiply(visualM);
|
||||||
|
|
||||||
|
const topLeft = rotatedVisualLeftTop.matrixTransform(
|
||||||
|
new DOMMatrix()
|
||||||
|
.translate(newCenter.x, newCenter.y)
|
||||||
|
.rotate(-newRotated)
|
||||||
|
.translate(-newCenter.x, -newCenter.y)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
originalBound: new Bound(orig.x, orig.y, orig.w, orig.h),
|
||||||
|
newBound: new Bound(topLeft.x, topLeft.y, orig.w, orig.h),
|
||||||
|
originalRotate: orig.rotate,
|
||||||
|
newRotate: newRotated,
|
||||||
|
matrix: finalM,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHandleSign(handle: ResizeHandle) {
|
||||||
|
switch (handle) {
|
||||||
|
case 'top-left':
|
||||||
|
return { xSign: -1, ySign: -1 };
|
||||||
|
case 'top':
|
||||||
|
return { xSign: 0, ySign: -1 };
|
||||||
|
case 'top-right':
|
||||||
|
return { xSign: 1, ySign: -1 };
|
||||||
|
case 'right':
|
||||||
|
return { xSign: 1, ySign: 0 };
|
||||||
|
case 'bottom-right':
|
||||||
|
return { xSign: 1, ySign: 1 };
|
||||||
|
case 'bottom':
|
||||||
|
return { xSign: 0, ySign: 1 };
|
||||||
|
case 'bottom-left':
|
||||||
|
return { xSign: -1, ySign: 1 };
|
||||||
|
case 'left':
|
||||||
|
return { xSign: -1, ySign: 0 };
|
||||||
|
default:
|
||||||
|
return { xSign: 0, ySign: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import type { Bound, IBound, IPoint } from '@blocksuite/global/gfx';
|
|||||||
import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
|
import type { GfxBlockComponent } from '../../../view/element/gfx-block-component.js';
|
||||||
import type { GfxModel } from '../../model/model.js';
|
import type { GfxModel } from '../../model/model.js';
|
||||||
import type { GfxElementModelView } from '../../view/view.js';
|
import type { GfxElementModelView } from '../../view/view.js';
|
||||||
|
import type { ResizeHandle } from '../resize/manager.js';
|
||||||
|
|
||||||
export type DragStartContext = {
|
export type DragStartContext = {
|
||||||
/**
|
/**
|
||||||
@@ -34,6 +35,97 @@ export type DragMoveContext = DragStartContext & {
|
|||||||
|
|
||||||
export type DragEndContext = DragMoveContext;
|
export type DragEndContext = DragMoveContext;
|
||||||
|
|
||||||
|
export type ResizeConstraint = {
|
||||||
|
minWidth?: number;
|
||||||
|
minHeight?: number;
|
||||||
|
|
||||||
|
maxWidth?: number;
|
||||||
|
maxHeight?: number;
|
||||||
|
allowedHandlers?: ResizeHandle[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to lock the aspect ratio of the element when resizing.
|
||||||
|
* If the value is an array, it will only lock the aspect ratio when resizing the specified handles.
|
||||||
|
*/
|
||||||
|
lockRatio?: boolean | ResizeHandle[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BeforeResizeContext = {
|
||||||
|
/**
|
||||||
|
* The elements that will be resized
|
||||||
|
*/
|
||||||
|
elements: (GfxBlockComponent | GfxElementModelView)[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the constraint before resize starts.
|
||||||
|
*/
|
||||||
|
set: (constraint: ResizeConstraint) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResizeStartContext = {
|
||||||
|
/**
|
||||||
|
* The handle that is used to resize the element
|
||||||
|
*/
|
||||||
|
handle: ResizeHandle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resize constraint.
|
||||||
|
*/
|
||||||
|
constraint: Readonly<Required<ResizeConstraint>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResizeMoveContext = ResizeStartContext & {
|
||||||
|
/**
|
||||||
|
* The element bound when resize starts
|
||||||
|
*/
|
||||||
|
originalBound: Bound;
|
||||||
|
|
||||||
|
newBound: Bound;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The matrix that used to transform the element.
|
||||||
|
*/
|
||||||
|
matrix: DOMMatrix;
|
||||||
|
|
||||||
|
lockRatio: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResizeEndContext = ResizeStartContext;
|
||||||
|
|
||||||
|
export type RotateConstraint = {
|
||||||
|
rotatable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BeforeRotateContext = {
|
||||||
|
/**
|
||||||
|
* The elements that will be rotated
|
||||||
|
*/
|
||||||
|
elements: (GfxBlockComponent | GfxElementModelView)[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the constraint before rotate starts.
|
||||||
|
*/
|
||||||
|
set: (constraint: RotateConstraint) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RotateStartContext = {
|
||||||
|
constraint: Readonly<Required<RotateConstraint>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RotateMoveContext = RotateStartContext & {
|
||||||
|
newBound: Bound;
|
||||||
|
|
||||||
|
originalBound: Bound;
|
||||||
|
|
||||||
|
newRotate: number;
|
||||||
|
|
||||||
|
originalRotate: number;
|
||||||
|
|
||||||
|
matrix: DOMMatrix;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RotateEndContext = RotateStartContext;
|
||||||
|
|
||||||
export type SelectedContext = {
|
export type SelectedContext = {
|
||||||
/**
|
/**
|
||||||
* The selected state of the element
|
* The selected state of the element
|
||||||
@@ -79,8 +171,6 @@ export type GfxViewTransformInterface = {
|
|||||||
onDragStart: (context: DragStartContext) => void;
|
onDragStart: (context: DragStartContext) => void;
|
||||||
onDragMove: (context: DragMoveContext) => void;
|
onDragMove: (context: DragMoveContext) => void;
|
||||||
onDragEnd: (context: DragEndContext) => void;
|
onDragEnd: (context: DragEndContext) => void;
|
||||||
onRotate: (context: {}) => void;
|
|
||||||
onResize: (context: {}) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the element is selected by the pointer
|
* When the element is selected by the pointer
|
||||||
|
|||||||
@@ -116,7 +116,19 @@ export class GfxBlockElementModel<
|
|||||||
*/
|
*/
|
||||||
responseExtension: [number, number] = [0, 0];
|
responseExtension: [number, number] = [0, 0];
|
||||||
|
|
||||||
rotate = 0;
|
get rotate() {
|
||||||
|
if ('rotate' in this.props) {
|
||||||
|
return this.props.rotate as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
set rotate(rotate: number) {
|
||||||
|
if ('rotate' in this.props) {
|
||||||
|
this.props.rotate = rotate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get deserializedXYWH() {
|
get deserializedXYWH() {
|
||||||
if (this._cacheDeserKey !== this.xywh || !this._cacheDeserXYWH) {
|
if (this._cacheDeserKey !== this.xywh || !this._cacheDeserXYWH) {
|
||||||
|
|||||||
@@ -224,10 +224,6 @@ export class GfxElementModelView<
|
|||||||
|
|
||||||
onBoxSelected(_: BoxSelectionContext): boolean | void {}
|
onBoxSelected(_: BoxSelectionContext): boolean | void {}
|
||||||
|
|
||||||
onResize = () => {};
|
|
||||||
|
|
||||||
onRotate = () => {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the view is destroyed.
|
* Called when the view is destroyed.
|
||||||
* Override this method requires calling `super.onDestroyed()`.
|
* Override this method requires calling `super.onDestroyed()`.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
GfxViewTransformInterface,
|
GfxViewTransformInterface,
|
||||||
SelectedContext,
|
SelectedContext,
|
||||||
} from '../../gfx/interactivity/index.js';
|
} from '../../gfx/interactivity/index.js';
|
||||||
import { type GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js';
|
import type { GfxBlockElementModel } from '../../gfx/model/gfx-block-model.js';
|
||||||
import { SurfaceSelection } from '../../selection/index.js';
|
import { SurfaceSelection } from '../../selection/index.js';
|
||||||
import { BlockComponent } from './block-component.js';
|
import { BlockComponent } from './block-component.js';
|
||||||
|
|
||||||
@@ -116,10 +116,6 @@ export abstract class GfxBlockComponent<
|
|||||||
|
|
||||||
onBoxSelected(_: BoxSelectionContext) {}
|
onBoxSelected(_: BoxSelectionContext) {}
|
||||||
|
|
||||||
onRotate() {}
|
|
||||||
|
|
||||||
onResize() {}
|
|
||||||
|
|
||||||
getCSSTransform() {
|
getCSSTransform() {
|
||||||
const viewport = this.gfx.viewport;
|
const viewport = this.gfx.viewport;
|
||||||
const { translateX, translateY, zoom } = viewport;
|
const { translateX, translateY, zoom } = viewport;
|
||||||
@@ -236,10 +232,6 @@ export function toGfxBlockComponent<
|
|||||||
|
|
||||||
onBoxSelected(_: BoxSelectionContext) {}
|
onBoxSelected(_: BoxSelectionContext) {}
|
||||||
|
|
||||||
onRotate() {}
|
|
||||||
|
|
||||||
onResize() {}
|
|
||||||
|
|
||||||
get gfx() {
|
get gfx() {
|
||||||
return this.std.get(GfxControllerIdentifier);
|
return this.std.get(GfxControllerIdentifier);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"@affine/track": "workspace:*",
|
"@affine/track": "workspace:*",
|
||||||
"@blocksuite/affine": "workspace:*",
|
"@blocksuite/affine": "workspace:*",
|
||||||
"@blocksuite/icons": "^2.2.13",
|
"@blocksuite/icons": "^2.2.13",
|
||||||
|
"@blocksuite/std": "workspace:*",
|
||||||
"@dotlottie/player-component": "^2.7.12",
|
"@dotlottie/player-component": "^2.7.12",
|
||||||
"@emotion/cache": "^11.14.0",
|
"@emotion/cache": "^11.14.0",
|
||||||
"@emotion/css": "^11.13.5",
|
"@emotion/css": "^11.13.5",
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Bound } from '@blocksuite/affine/global/gfx';
|
import { Bound, clamp } from '@blocksuite/affine/global/gfx';
|
||||||
import { toGfxBlockComponent } from '@blocksuite/affine/std';
|
import { toGfxBlockComponent } from '@blocksuite/affine/std';
|
||||||
|
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
|
|
||||||
import { AIChatBlockComponent } from './ai-chat-block';
|
import { AIChatBlockComponent } from './ai-chat-block';
|
||||||
|
import { AIChatBlockSchema } from './model';
|
||||||
|
|
||||||
export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
|
export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
|
||||||
AIChatBlockComponent
|
AIChatBlockComponent
|
||||||
@@ -31,6 +33,69 @@ export class EdgelessAIChatBlockComponent extends toGfxBlockComponent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const EdgelessAIChatBlockInteraction =
|
||||||
|
GfxViewInteractionExtension<EdgelessAIChatBlockComponent>(
|
||||||
|
AIChatBlockSchema.model.flavour,
|
||||||
|
{
|
||||||
|
resizeConstraint: {
|
||||||
|
minWidth: 260,
|
||||||
|
minHeight: 160,
|
||||||
|
maxWidth: 320,
|
||||||
|
maxHeight: 300,
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRotate() {
|
||||||
|
return {
|
||||||
|
beforeRotate(context) {
|
||||||
|
context.set({
|
||||||
|
rotatable: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleResize({ model }) {
|
||||||
|
const initialScale = model.props.scale$.peek();
|
||||||
|
|
||||||
|
return {
|
||||||
|
onResizeStart(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.stash('scale');
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeMove(context) {
|
||||||
|
const { newBound, originalBound, lockRatio, constraint } = context;
|
||||||
|
const { minWidth, maxWidth, minHeight, maxHeight } = constraint;
|
||||||
|
|
||||||
|
let scale = initialScale;
|
||||||
|
const originalRealWidth = originalBound.w / scale;
|
||||||
|
|
||||||
|
// update scale if resize is proportional
|
||||||
|
if (lockRatio) {
|
||||||
|
scale = newBound.w / originalRealWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newRealWidth = clamp(newBound.w / scale, minWidth, maxWidth);
|
||||||
|
let newRealHeight = clamp(newBound.h / scale, minHeight, maxHeight);
|
||||||
|
|
||||||
|
newBound.w = newRealWidth * scale;
|
||||||
|
newBound.h = newRealHeight * scale;
|
||||||
|
|
||||||
|
model.props.xywh = newBound.serialize();
|
||||||
|
if (scale !== initialScale) {
|
||||||
|
model.props.scale = scale;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResizeEnd(context) {
|
||||||
|
context.default(context);
|
||||||
|
model.pop('scale');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent;
|
'affine-edgeless-ai-chat': EdgelessAIChatBlockComponent;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
{ "path": "../../common/nbstore" },
|
{ "path": "../../common/nbstore" },
|
||||||
{ "path": "../track" },
|
{ "path": "../track" },
|
||||||
{ "path": "../../../blocksuite/affine/all" },
|
{ "path": "../../../blocksuite/affine/all" },
|
||||||
|
{ "path": "../../../blocksuite/framework/std" },
|
||||||
{ "path": "../../common/infra" }
|
{ "path": "../../common/infra" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,9 +441,11 @@ test.describe('edgeless text block', () => {
|
|||||||
);
|
);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
expect(await getPageSnapshot(page, true)).toMatchSnapshot(
|
const selectedRect2 = await getEdgelessSelectedRect(page);
|
||||||
`${testInfo.title}_drag.json`
|
expect(selectedRect2.width).toBeCloseTo(selectedRect1.width + 45);
|
||||||
);
|
expect(selectedRect2.height).toBeCloseTo(selectedRect1.height);
|
||||||
|
expect(selectedRect2.x).toBeCloseTo(selectedRect1.x);
|
||||||
|
expect(selectedRect2.y).toBeCloseTo(selectedRect1.y);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cut edgeless text', async ({ page }) => {
|
test('cut edgeless text', async ({ page }) => {
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ test.describe('note scale', () => {
|
|||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(
|
await page.mouse.move(
|
||||||
noteRect.x + noteRect.width * 2,
|
noteRect.x + noteRect.width * 2,
|
||||||
noteRect.y + noteRect.height * 2
|
noteRect.y + noteRect.height * 2,
|
||||||
|
{
|
||||||
|
steps: 10,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
|
|
||||||
|
|||||||
@@ -56,18 +56,17 @@ test('undo/redo should work correctly after resizing', async ({ page }) => {
|
|||||||
await switchEditorMode(page);
|
await switchEditorMode(page);
|
||||||
await zoomResetByKeyboard(page);
|
await zoomResetByKeyboard(page);
|
||||||
await activeNoteInEdgeless(page, noteId);
|
await activeNoteInEdgeless(page, noteId);
|
||||||
await waitNextFrame(page, 400);
|
await waitNextFrame(page, 600);
|
||||||
// current implementation may be a little inefficient
|
// current implementation may be a little inefficient
|
||||||
await fillLine(page, true);
|
await fillLine(page, true);
|
||||||
await page.mouse.click(0, 0);
|
await page.mouse.click(0, 0);
|
||||||
await waitNextFrame(page, 400);
|
await waitNextFrame(page, 600);
|
||||||
await selectNoteInEdgeless(page, noteId);
|
await selectNoteInEdgeless(page, noteId);
|
||||||
|
|
||||||
const initRect = await getNoteRect(page, noteId);
|
const initRect = await getNoteRect(page, noteId);
|
||||||
const rightHandle = page.locator('.handle[aria-label="right"] .resize');
|
const rightHandle = page.locator('.handle[aria-label="right"] .resize');
|
||||||
const box = await rightHandle.boundingBox();
|
const box = await rightHandle.boundingBox();
|
||||||
if (box === null) throw new Error();
|
if (box === null) throw new Error();
|
||||||
|
|
||||||
await dragBetweenCoords(
|
await dragBetweenCoords(
|
||||||
page,
|
page,
|
||||||
{ x: box.x + 5, y: box.y + 5 },
|
{ x: box.x + 5, y: box.y + 5 },
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ test.describe('rotation', () => {
|
|||||||
{ x: 100, y: 100 },
|
{ x: 100, y: 100 },
|
||||||
{ x: 200, y: 200 }
|
{ x: 200, y: 200 }
|
||||||
);
|
);
|
||||||
await rotateElementByHandle(page, 90, 'bottom-left');
|
await rotateElementByHandle(page, 90, 'bottom-left', 10);
|
||||||
await assertEdgelessSelectedRectRotation(page, 90);
|
await assertEdgelessSelectedRectRotation(page, 90);
|
||||||
|
|
||||||
await resizeElementByHandle(page, { x: 10, y: -10 }, 'bottom-right');
|
await resizeElementByHandle(page, { x: 10, y: -10 }, 'bottom-right');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
initEmptyEdgelessState,
|
initEmptyEdgelessState,
|
||||||
pressArrowLeft,
|
pressArrowLeft,
|
||||||
pressEnter,
|
pressEnter,
|
||||||
|
resizeElementByHandle,
|
||||||
setEdgelessTool,
|
setEdgelessTool,
|
||||||
SHORT_KEY,
|
SHORT_KEY,
|
||||||
switchEditorMode,
|
switchEditorMode,
|
||||||
@@ -208,12 +209,7 @@ test.describe('edgeless canvas text', () => {
|
|||||||
let lastHeight = selectedRect.height;
|
let lastHeight = selectedRect.height;
|
||||||
|
|
||||||
// move cursor to the right edge and drag it to resize the width of text element
|
// move cursor to the right edge and drag it to resize the width of text element
|
||||||
await page.mouse.move(130 + lastWidth, 160);
|
await resizeElementByHandle(page, { x: -20, y: 0 }, 'right', 10);
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(130 + lastWidth / 2, 160, {
|
|
||||||
steps: 10,
|
|
||||||
});
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
// the text should be wrapped, so check the width and height of text element
|
// the text should be wrapped, so check the width and height of text element
|
||||||
selectedRect = await getEdgelessSelectedRect(page);
|
selectedRect = await getEdgelessSelectedRect(page);
|
||||||
@@ -236,23 +232,13 @@ test.describe('edgeless canvas text', () => {
|
|||||||
selectedRect = await getEdgelessSelectedRect(page);
|
selectedRect = await getEdgelessSelectedRect(page);
|
||||||
lastWidth = selectedRect.width;
|
lastWidth = selectedRect.width;
|
||||||
lastHeight = selectedRect.height;
|
lastHeight = selectedRect.height;
|
||||||
// move cursor to the left edge and drag it to resize the width of text element
|
|
||||||
await page.mouse.move(130, 160);
|
await resizeElementByHandle(page, { x: 80, y: 0 }, 'right', 10);
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(60, 160, {
|
|
||||||
steps: 10,
|
|
||||||
});
|
|
||||||
await page.mouse.up();
|
|
||||||
|
|
||||||
// the text should be unwrapped, check the width and height of text element
|
// the text should be unwrapped, check the width and height of text element
|
||||||
selectedRect = await getEdgelessSelectedRect(page);
|
selectedRect = await getEdgelessSelectedRect(page);
|
||||||
expect(selectedRect.width).toBeGreaterThan(lastWidth);
|
expect(selectedRect.width).toBeGreaterThan(lastWidth);
|
||||||
expect(selectedRect.height).toBeLessThan(lastHeight);
|
expect(selectedRect.height).toBeLessThan(lastHeight);
|
||||||
|
|
||||||
await page.mouse.dblclick(100, 160);
|
|
||||||
await waitForInlineEditorStateUpdated(page);
|
|
||||||
await waitNextFrame(page);
|
|
||||||
await assertEdgelessCanvasText(page, 'hellohello');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('text element should have maxWidth after adjusting width by dragging left or right edge', async ({
|
test('text element should have maxWidth after adjusting width by dragging left or right edge', async ({
|
||||||
|
|||||||
@@ -594,6 +594,10 @@ export async function resizeElementByHandle(
|
|||||||
page: Page,
|
page: Page,
|
||||||
delta: Point,
|
delta: Point,
|
||||||
corner:
|
corner:
|
||||||
|
| 'top'
|
||||||
|
| 'bottom'
|
||||||
|
| 'left'
|
||||||
|
| 'right'
|
||||||
| 'top-left'
|
| 'top-left'
|
||||||
| 'top-right'
|
| 'top-right'
|
||||||
| 'bottom-right'
|
| 'bottom-right'
|
||||||
@@ -604,11 +608,13 @@ export async function resizeElementByHandle(
|
|||||||
const handle = page.locator(`.handle[aria-label="${corner}"] .resize`);
|
const handle = page.locator(`.handle[aria-label="${corner}"] .resize`);
|
||||||
const box = await handle.boundingBox();
|
const box = await handle.boundingBox();
|
||||||
if (box === null) throw new Error();
|
if (box === null) throw new Error();
|
||||||
const offset = 5;
|
const xOffset = box.width / 2;
|
||||||
|
const yOffset = box.height / 2;
|
||||||
|
|
||||||
await dragBetweenCoords(
|
await dragBetweenCoords(
|
||||||
page,
|
page,
|
||||||
{ x: box.x + offset, y: box.y + offset },
|
{ x: box.x + xOffset, y: box.y + yOffset },
|
||||||
{ x: box.x + delta.x + offset, y: box.y + delta.y + offset },
|
{ x: box.x + delta.x + xOffset, y: box.y + delta.y + yOffset },
|
||||||
{
|
{
|
||||||
steps,
|
steps,
|
||||||
beforeMouseUp,
|
beforeMouseUp,
|
||||||
|
|||||||
@@ -454,6 +454,10 @@ export async function resizeElementByHandle(
|
|||||||
page: Page,
|
page: Page,
|
||||||
delta: IVec,
|
delta: IVec,
|
||||||
corner:
|
corner:
|
||||||
|
| 'right'
|
||||||
|
| 'left'
|
||||||
|
| 'top'
|
||||||
|
| 'bottom'
|
||||||
| 'top-left'
|
| 'top-left'
|
||||||
| 'top-right'
|
| 'top-right'
|
||||||
| 'bottom-right'
|
| 'bottom-right'
|
||||||
|
|||||||
@@ -1260,6 +1260,7 @@ export const PackageList = [
|
|||||||
'packages/frontend/templates',
|
'packages/frontend/templates',
|
||||||
'packages/frontend/track',
|
'packages/frontend/track',
|
||||||
'blocksuite/affine/all',
|
'blocksuite/affine/all',
|
||||||
|
'blocksuite/framework/std',
|
||||||
'packages/common/infra',
|
'packages/common/infra',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -405,6 +405,7 @@ __metadata:
|
|||||||
"@affine/track": "workspace:*"
|
"@affine/track": "workspace:*"
|
||||||
"@blocksuite/affine": "workspace:*"
|
"@blocksuite/affine": "workspace:*"
|
||||||
"@blocksuite/icons": "npm:^2.2.13"
|
"@blocksuite/icons": "npm:^2.2.13"
|
||||||
|
"@blocksuite/std": "workspace:*"
|
||||||
"@dotlottie/player-component": "npm:^2.7.12"
|
"@dotlottie/player-component": "npm:^2.7.12"
|
||||||
"@emotion/cache": "npm:^11.14.0"
|
"@emotion/cache": "npm:^11.14.0"
|
||||||
"@emotion/css": "npm:^11.13.5"
|
"@emotion/css": "npm:^11.13.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user