mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 09:04:56 +00:00
Compare commits
20 Commits
v0.21.2-ca
...
fix-toc-se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5526696357 | ||
|
|
5be0292536 | ||
|
|
823bf40a57 | ||
|
|
588659ef67 | ||
|
|
d5aebc1421 | ||
|
|
25418b402a | ||
|
|
f0fb1447a4 | ||
|
|
0f39ab4ea4 | ||
|
|
ffad5d0a2e | ||
|
|
a166760041 | ||
|
|
e79e4c9e9b | ||
|
|
a6ddfdd85e | ||
|
|
dba8e00fb6 | ||
|
|
69d4620753 | ||
|
|
dbf09ea055 | ||
|
|
2822146a4d | ||
|
|
c36dc9318c | ||
|
|
f85b35227b | ||
|
|
b8e93ed714 | ||
|
|
cc257f4fbe |
2
.github/workflows/build-test.yml
vendored
2
.github/workflows/build-test.yml
vendored
@@ -791,6 +791,8 @@ jobs:
|
||||
with:
|
||||
filters: |
|
||||
changed:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||
- 'tests/affine-cloud-copilot/**'
|
||||
|
||||
|
||||
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -601,9 +601,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.17"
|
||||
version = "1.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
|
||||
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -1997,9 +1997,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.41"
|
||||
version = "0.1.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b20daca3a4ac14dbdc753c5e90fc7b490a48a9131daed3c9a9ced7b2defd37b"
|
||||
checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -2153,9 +2153,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.45"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03cb1f88093fe50061ca1195d336ffec131347c7b833db31f9ab62a2d1b7925f"
|
||||
checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
|
||||
@@ -3769,6 +3769,48 @@ bbb
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('escapes dollar signs followed by a digit or space and digit', async () => {
|
||||
const markdown =
|
||||
'The price of the T-shirt is $9.15 and the price of the hat is $ 8\n';
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[0]',
|
||||
flavour: 'affine:note',
|
||||
props: {
|
||||
xywh: '[0,0,800,95]',
|
||||
background: DefaultTheme.noteBackgrounColor,
|
||||
index: 'a0',
|
||||
hidden: false,
|
||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:paragraph',
|
||||
props: {
|
||||
type: 'text',
|
||||
text: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [
|
||||
{
|
||||
insert:
|
||||
'The price of the T-shirt is $9.15 and the price of the hat is $ 8',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
|
||||
file: markdown,
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
});
|
||||
|
||||
test('reference', async () => {
|
||||
|
||||
@@ -3,21 +3,84 @@ import {
|
||||
MarkdownPreprocessorExtension,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\S\s]*?```|`.*?`)|\\\[([\S\s]*?[^\\])\\]|\\\((.*?)\\\)/g;
|
||||
return text.replaceAll(
|
||||
pattern,
|
||||
(match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock;
|
||||
} else if (squareBracket) {
|
||||
return `$$${squareBracket}$$`;
|
||||
} else if (roundBracket) {
|
||||
return `$${roundBracket}$`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function escapeMhchem(text: string) {
|
||||
return text.replaceAll('$\\ce{', '$\\\\ce{').replaceAll('$\\pu{', '$\\\\pu{');
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocess the content to protect code blocks and LaTeX expressions
|
||||
* reference issue: https://github.com/remarkjs/react-markdown/issues/785
|
||||
* reference comment: https://github.com/remarkjs/react-markdown/issues/785#issuecomment-2307567823
|
||||
* @param content - The content to preprocess
|
||||
* @returns The preprocessed content
|
||||
*/
|
||||
function preprocessLatex(content: string) {
|
||||
// Protect code blocks
|
||||
const codeBlocks: string[] = [];
|
||||
let preprocessedContent = content;
|
||||
preprocessedContent = preprocessedContent.replace(
|
||||
/(```[\s\S]*?```|`[^`\n]+`)/g,
|
||||
(_, code) => {
|
||||
codeBlocks.push(code);
|
||||
return `<<CODE_BLOCK_${codeBlocks.length - 1}>>`;
|
||||
}
|
||||
);
|
||||
|
||||
// Protect existing LaTeX expressions
|
||||
const latexExpressions: string[] = [];
|
||||
preprocessedContent = preprocessedContent.replace(
|
||||
/(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\(.*?\\\))/g,
|
||||
match => {
|
||||
latexExpressions.push(match);
|
||||
return `<<LATEX_${latexExpressions.length - 1}>>`;
|
||||
}
|
||||
);
|
||||
|
||||
// Escape dollar signs that are likely currency indicators
|
||||
preprocessedContent = preprocessedContent.replace(/\$(?=\d)/g, '\\$');
|
||||
|
||||
// Restore LaTeX expressions
|
||||
preprocessedContent = preprocessedContent.replace(
|
||||
/<<LATEX_(\d+)>>/g,
|
||||
(_, index) => latexExpressions[parseInt(index)]
|
||||
);
|
||||
|
||||
// Restore code blocks
|
||||
preprocessedContent = preprocessedContent.replace(
|
||||
/<<CODE_BLOCK_(\d+)>>/g,
|
||||
(_, index) => codeBlocks[parseInt(index)]
|
||||
);
|
||||
|
||||
// Apply additional escaping functions
|
||||
preprocessedContent = escapeBrackets(preprocessedContent);
|
||||
preprocessedContent = escapeMhchem(preprocessedContent);
|
||||
|
||||
return preprocessedContent;
|
||||
}
|
||||
|
||||
const latexPreprocessor: MarkdownAdapterPreprocessor = {
|
||||
name: 'latex',
|
||||
levels: ['block', 'slice', 'doc'],
|
||||
preprocess: content => {
|
||||
// Replace block-level LaTeX delimiters \[ \] with $$ $$
|
||||
const blockProcessedContent = content.replace(
|
||||
/\\\[(.*?)\\\]/gs,
|
||||
(_, equation) => `$$${equation}$$`
|
||||
);
|
||||
// Replace inline LaTeX delimiters \( \) with $ $
|
||||
const inlineProcessedContent = blockProcessedContent.replace(
|
||||
/\\\((.*?)\\\)/gs,
|
||||
(_, equation) => `$${equation}$`
|
||||
);
|
||||
return inlineProcessedContent;
|
||||
return preprocessLatex(content);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
segmentSentences,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord } from '@blocksuite/std/gfx';
|
||||
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { ListLayout } from './list-painter.worker';
|
||||
|
||||
@@ -21,24 +22,27 @@ export class ListLayoutHandlerExtension extends BlockLayoutHandlerExtension<List
|
||||
);
|
||||
}
|
||||
|
||||
queryLayout(component: GfxBlockComponent): ListLayout | null {
|
||||
// Select all list items within this list block
|
||||
override queryLayout(
|
||||
model: BlockModel,
|
||||
host: EditorHost,
|
||||
viewportRecord: ViewportRecord
|
||||
): ListLayout | null {
|
||||
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
|
||||
if (!component) return null;
|
||||
|
||||
// Find the list items within this specific list component
|
||||
const listItemSelector =
|
||||
'.affine-list-block-container .affine-list-rich-text-wrapper [data-v-text="true"]';
|
||||
const listItemNodes = component.querySelectorAll(listItemSelector);
|
||||
|
||||
if (listItemNodes.length === 0) return null;
|
||||
|
||||
const viewportRecord = component.gfx.viewport.deserializeRecord(
|
||||
component.dataset.viewportState
|
||||
);
|
||||
|
||||
if (!viewportRecord) return null;
|
||||
|
||||
const { zoom, viewScale } = viewportRecord;
|
||||
const list: ListLayout = {
|
||||
type: 'affine:list',
|
||||
items: [],
|
||||
blockId: model.id,
|
||||
rect: { x: 0, y: 0, w: 0, h: 0 },
|
||||
};
|
||||
|
||||
listItemNodes.forEach(listItemNode => {
|
||||
|
||||
@@ -77,10 +77,15 @@ class ListLayoutPainter implements BlockLayoutPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isListLayout(layout)) return;
|
||||
if (!isListLayout(layout)) {
|
||||
console.warn(
|
||||
'Expected list layout but received different format:',
|
||||
layout
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const renderedPositions = new Set<string>();
|
||||
|
||||
layout.items.forEach(item => {
|
||||
const fontSize = item.fontSize;
|
||||
const baselineY = getBaseline(fontSize);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@blocksuite/affine-block-surface": "workspace:*",
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*",
|
||||
"@blocksuite/affine-gfx-turbo-renderer": "workspace:*",
|
||||
"@blocksuite/affine-inline-preset": "workspace:*",
|
||||
"@blocksuite/affine-model": "workspace:*",
|
||||
"@blocksuite/affine-rich-text": "workspace:*",
|
||||
@@ -37,7 +38,8 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts"
|
||||
"./effects": "./src/effects.ts",
|
||||
"./turbo-painter": "./src/turbo/note-painter.worker.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
|
||||
@@ -6,3 +6,5 @@ export * from './edgeless-clipboard-config';
|
||||
export * from './note-block';
|
||||
export * from './note-edgeless-block';
|
||||
export * from './note-spec';
|
||||
export * from './turbo/note-layout-handler';
|
||||
export * from './turbo/note-painter.worker';
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Rect } from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import {
|
||||
BlockLayoutHandlerExtension,
|
||||
BlockLayoutHandlersIdentifier,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import {
|
||||
ColorScheme,
|
||||
type NoteBlockModel,
|
||||
resolveColor,
|
||||
} from '@blocksuite/affine-model';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { NoteLayout } from './note-painter.worker';
|
||||
|
||||
export class NoteLayoutHandlerExtension extends BlockLayoutHandlerExtension<NoteLayout> {
|
||||
readonly blockType = 'affine:note';
|
||||
|
||||
static override setup(di: Container) {
|
||||
di.addImpl(
|
||||
BlockLayoutHandlersIdentifier('note'),
|
||||
NoteLayoutHandlerExtension
|
||||
);
|
||||
}
|
||||
|
||||
override queryLayout(
|
||||
model: BlockModel,
|
||||
host: EditorHost,
|
||||
viewportRecord: ViewportRecord
|
||||
): NoteLayout | null {
|
||||
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
|
||||
if (!component) return null;
|
||||
|
||||
// Get the note container element
|
||||
const noteContainer = component.querySelector('.affine-note-mask');
|
||||
if (!noteContainer) return null;
|
||||
|
||||
// Get the bounding client rect of the note container
|
||||
const clientRect = noteContainer.getBoundingClientRect();
|
||||
|
||||
// Convert client coordinates to model coordinates
|
||||
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
|
||||
clientRect.x,
|
||||
clientRect.y,
|
||||
]);
|
||||
|
||||
const { zoom, viewScale } = viewportRecord;
|
||||
|
||||
// Cast model to NoteBlockModel to access background property from props
|
||||
const noteModel = model as NoteBlockModel;
|
||||
const background = noteModel.props.background;
|
||||
// Resolve the color to a string
|
||||
const backgroundString = resolveColor(background, ColorScheme.Light);
|
||||
|
||||
// Create the note layout object
|
||||
const noteLayout: NoteLayout = {
|
||||
type: 'affine:note',
|
||||
blockId: model.id,
|
||||
rect: {
|
||||
x: modelX,
|
||||
y: modelY,
|
||||
w: clientRect.width / zoom / viewScale,
|
||||
h: clientRect.height / zoom / viewScale,
|
||||
},
|
||||
background: backgroundString,
|
||||
};
|
||||
|
||||
return noteLayout;
|
||||
}
|
||||
|
||||
calculateBound(layout: NoteLayout) {
|
||||
const rect: Rect = layout.rect;
|
||||
|
||||
return {
|
||||
rect,
|
||||
subRects: [rect], // The note is represented by a single rectangle
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type {
|
||||
BlockLayout,
|
||||
BlockLayoutPainter,
|
||||
WorkerToHostMessage,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import { BlockLayoutPainterExtension } from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
|
||||
export interface NoteLayout extends BlockLayout {
|
||||
type: 'affine:note';
|
||||
background?: string;
|
||||
}
|
||||
|
||||
function isNoteLayout(layout: BlockLayout): layout is NoteLayout {
|
||||
return layout.type === 'affine:note';
|
||||
}
|
||||
|
||||
class NoteLayoutPainter implements BlockLayoutPainter {
|
||||
paint(
|
||||
ctx: OffscreenCanvasRenderingContext2D,
|
||||
layout: BlockLayout,
|
||||
layoutBaseX: number,
|
||||
layoutBaseY: number
|
||||
): void {
|
||||
if (!isNoteLayout(layout)) {
|
||||
const message: WorkerToHostMessage = {
|
||||
type: 'paintError',
|
||||
error: 'Invalid layout format',
|
||||
blockType: 'affine:note',
|
||||
};
|
||||
self.postMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the layout rectangle
|
||||
const x = layout.rect.x - layoutBaseX;
|
||||
const y = layout.rect.y - layoutBaseY;
|
||||
const width = layout.rect.w;
|
||||
const height = layout.rect.h;
|
||||
|
||||
ctx.fillStyle = layout.background || 'rgb(255, 255, 255)';
|
||||
ctx.fillRect(x, y, width, height);
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
export const NoteLayoutPainterExtension = BlockLayoutPainterExtension(
|
||||
'affine:note',
|
||||
NoteLayoutPainter
|
||||
);
|
||||
@@ -11,6 +11,7 @@
|
||||
{ "path": "../surface" },
|
||||
{ "path": "../../components" },
|
||||
{ "path": "../../fragments/doc-title" },
|
||||
{ "path": "../../gfx/turbo-renderer" },
|
||||
{ "path": "../../inlines/preset" },
|
||||
{ "path": "../../model" },
|
||||
{ "path": "../../rich-text" },
|
||||
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
segmentSentences,
|
||||
} from '@blocksuite/affine-gfx-turbo-renderer';
|
||||
import type { Container } from '@blocksuite/global/di';
|
||||
import type { GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord } from '@blocksuite/std/gfx';
|
||||
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
|
||||
import { clientToModelCoord, type ViewportRecord } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import type { ParagraphLayout } from './paragraph-painter.worker';
|
||||
|
||||
@@ -21,58 +22,54 @@ export class ParagraphLayoutHandlerExtension extends BlockLayoutHandlerExtension
|
||||
);
|
||||
}
|
||||
|
||||
queryLayout(component: GfxBlockComponent): ParagraphLayout | null {
|
||||
override queryLayout(
|
||||
model: BlockModel,
|
||||
host: EditorHost,
|
||||
viewportRecord: ViewportRecord
|
||||
): ParagraphLayout | null {
|
||||
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
|
||||
const paragraphSelector =
|
||||
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]';
|
||||
const paragraphNodes = component.querySelectorAll(paragraphSelector);
|
||||
|
||||
if (paragraphNodes.length === 0) return null;
|
||||
|
||||
const viewportRecord = component.gfx.viewport.deserializeRecord(
|
||||
component.dataset.viewportState
|
||||
);
|
||||
|
||||
if (!viewportRecord) return null;
|
||||
const paragraphNode = component.querySelector(paragraphSelector);
|
||||
if (!paragraphNode) return null;
|
||||
|
||||
const { zoom, viewScale } = viewportRecord;
|
||||
const paragraph: ParagraphLayout = {
|
||||
type: 'affine:paragraph',
|
||||
sentences: [],
|
||||
blockId: model.id,
|
||||
rect: { x: 0, y: 0, w: 0, h: 0 },
|
||||
};
|
||||
|
||||
paragraphNodes.forEach(paragraphNode => {
|
||||
const computedStyle = window.getComputedStyle(paragraphNode);
|
||||
const fontSizeStr = computedStyle.fontSize;
|
||||
const fontSize = parseInt(fontSizeStr);
|
||||
const computedStyle = window.getComputedStyle(paragraphNode);
|
||||
const fontSizeStr = computedStyle.fontSize;
|
||||
const fontSize = parseInt(fontSizeStr);
|
||||
|
||||
const sentences = segmentSentences(paragraphNode.textContent || '');
|
||||
const sentenceLayouts = sentences.map(sentence => {
|
||||
const sentenceRects = getSentenceRects(paragraphNode, sentence);
|
||||
const rects = sentenceRects.map(({ text, rect }) => {
|
||||
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
|
||||
rect.x,
|
||||
rect.y,
|
||||
]);
|
||||
return {
|
||||
text,
|
||||
rect: {
|
||||
x: modelX,
|
||||
y: modelY,
|
||||
w: rect.w / zoom / viewScale,
|
||||
h: rect.h / zoom / viewScale,
|
||||
},
|
||||
};
|
||||
});
|
||||
const sentences = segmentSentences(paragraphNode.textContent || '');
|
||||
const sentenceLayouts = sentences.map(sentence => {
|
||||
const sentenceRects = getSentenceRects(paragraphNode, sentence);
|
||||
const rects = sentenceRects.map(({ text, rect }) => {
|
||||
const [modelX, modelY] = clientToModelCoord(viewportRecord, [
|
||||
rect.x,
|
||||
rect.y,
|
||||
]);
|
||||
return {
|
||||
text: sentence,
|
||||
rects,
|
||||
fontSize,
|
||||
text,
|
||||
rect: {
|
||||
x: modelX,
|
||||
y: modelY,
|
||||
w: rect.w / zoom / viewScale,
|
||||
h: rect.h / zoom / viewScale,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
paragraph.sentences.push(...sentenceLayouts);
|
||||
return {
|
||||
text: sentence,
|
||||
rects,
|
||||
fontSize,
|
||||
};
|
||||
});
|
||||
|
||||
paragraph.sentences.push(...sentenceLayouts);
|
||||
return paragraph;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,10 +73,15 @@ class ParagraphLayoutPainter implements BlockLayoutPainter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isParagraphLayout(layout)) return; // cast to ParagraphLayout
|
||||
if (!isParagraphLayout(layout)) {
|
||||
console.warn(
|
||||
'Expected paragraph layout but received different format:',
|
||||
layout
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const renderedPositions = new Set<string>();
|
||||
|
||||
layout.sentences.forEach(sentence => {
|
||||
const fontSize = sentence.fontSize;
|
||||
const baselineY = getBaseline(fontSize);
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
import type { BaseSelection, Store } from '@blocksuite/store';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { css, html, nothing } from 'lit';
|
||||
import { query, state } from 'lit/decorators.js';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
@@ -103,17 +103,12 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ref-viewport-event-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
inset: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -139,11 +134,11 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
return this._referencedModel;
|
||||
}
|
||||
|
||||
private _focusBlock() {
|
||||
private readonly _handleClick = () => {
|
||||
this.selection.update(() => {
|
||||
return [this.selection.create(BlockSelection, { blockId: this.blockId })];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _initHotkey() {
|
||||
const selection = this.host.selection;
|
||||
@@ -178,7 +173,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
|
||||
this.bindHotKey({
|
||||
Enter: () => {
|
||||
if (!this._focused) return;
|
||||
if (!this.selected$.value) return;
|
||||
addParagraph();
|
||||
return true;
|
||||
},
|
||||
@@ -260,17 +255,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
}
|
||||
}
|
||||
|
||||
private _initSelection() {
|
||||
const selection = this.host.selection;
|
||||
this._disposables.add(
|
||||
selection.slots.changed.subscribe(selList => {
|
||||
this._focused = selList.some(
|
||||
sel => sel.blockId === this.blockId && sel.is(BlockSelection)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private _initViewport() {
|
||||
const refreshViewport = () => {
|
||||
if (!this._referenceXYWH$.value) return;
|
||||
@@ -436,7 +420,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
this._initHotkey();
|
||||
this._initViewport();
|
||||
this._initReferencedModel();
|
||||
this._initSelection();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
@@ -462,10 +445,10 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
<div
|
||||
class=${classMap({
|
||||
'affine-surface-ref': true,
|
||||
focused: this._focused,
|
||||
focused: this.selected$.value,
|
||||
})}
|
||||
data-theme=${edgelessTheme}
|
||||
@click=${this._focusBlock}
|
||||
@click=${this._handleClick}
|
||||
>
|
||||
${content}
|
||||
</div>
|
||||
@@ -488,9 +471,6 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
|
||||
this.std.get(DocModeProvider).setEditorMode('edgeless');
|
||||
}
|
||||
|
||||
@state()
|
||||
private accessor _focused: boolean = false;
|
||||
|
||||
@query('.affine-surface-ref')
|
||||
accessor hoverableContainer!: HTMLDivElement;
|
||||
|
||||
|
||||
@@ -17,8 +17,12 @@ export interface IModelCoord {
|
||||
y: number;
|
||||
}
|
||||
|
||||
// TODO(@L-Sun): we should remove this list when refactor the pointerOut event to pointerLeave,
|
||||
// since the previous will be triggered when the pointer move to the area of the its children elements
|
||||
// see: https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerout_event
|
||||
export const EXCLUDING_MOUSE_OUT_CLASS_LIST = [
|
||||
'affine-note-mask',
|
||||
'edgeless-block-portal-note',
|
||||
'affine-block-children-container',
|
||||
'edgeless-container',
|
||||
];
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { IBound, IVec, IVec3 } from '@blocksuite/global/gfx';
|
||||
import {
|
||||
almostEqual,
|
||||
Bound,
|
||||
clamp,
|
||||
getBezierCurveBoundingBox,
|
||||
getBezierParameters,
|
||||
getBoundFromPoints,
|
||||
@@ -85,7 +84,7 @@ export function calculateNearestLocation(
|
||||
) {
|
||||
const { x, y, w, h } = bounds;
|
||||
return locations
|
||||
.map(offset => [x + offset[0] * w, y + offset[1] * h] as IVec)
|
||||
.map<IVec>(offset => [x + offset[0] * w, y + offset[1] * h])
|
||||
.map(point => getPointFromBoundsWithRotation(bounds, point))
|
||||
.reduce(
|
||||
(prev, curr, index) => {
|
||||
@@ -99,7 +98,7 @@ export function calculateNearestLocation(
|
||||
return prev;
|
||||
},
|
||||
[...locations[0]]
|
||||
) as IVec;
|
||||
);
|
||||
}
|
||||
|
||||
function rBound(ele: GfxModel, anti = false): IBound {
|
||||
@@ -139,21 +138,19 @@ export function getAnchors(ele: GfxModel) {
|
||||
const anchors: { point: PointLocation; coord: IVec }[] = [];
|
||||
const rotate = ele.rotate;
|
||||
|
||||
[
|
||||
[bound.center[0], bound.y - offset],
|
||||
[bound.center[0], bound.maxY + offset],
|
||||
[bound.x - offset, bound.center[1]],
|
||||
[bound.maxX + offset, bound.center[1]],
|
||||
]
|
||||
.map(vec =>
|
||||
getPointFromBoundsWithRotation({ ...bound, rotate }, vec as IVec)
|
||||
)
|
||||
(
|
||||
[
|
||||
[bound.center[0], bound.y - offset],
|
||||
[bound.center[0], bound.maxY + offset],
|
||||
[bound.x - offset, bound.center[1]],
|
||||
[bound.maxX + offset, bound.center[1]],
|
||||
] satisfies IVec[]
|
||||
)
|
||||
.map(vec => getPointFromBoundsWithRotation({ ...bound, rotate }, vec))
|
||||
.forEach(vec => {
|
||||
const rst = ele.getLineIntersections(bound.center as IVec, vec as IVec);
|
||||
if (!rst) {
|
||||
console.error(`Failed to get line intersections for ${ele.id}`);
|
||||
return;
|
||||
}
|
||||
const rst = ele.getLineIntersections(bound.center, vec);
|
||||
if (!rst) return;
|
||||
|
||||
const originPoint = getPointFromBoundsWithRotation(
|
||||
{ ...bound, rotate: -rotate },
|
||||
rst[0]
|
||||
@@ -164,7 +161,7 @@ export function getAnchors(ele: GfxModel) {
|
||||
}
|
||||
|
||||
function getConnectableRelativePosition(connectable: GfxModel, position: IVec) {
|
||||
const location = connectable.getRelativePointLocation(position as IVec);
|
||||
const location = connectable.getRelativePointLocation(position);
|
||||
if (isVecZero(Vec.sub(position, [0, 0.5])))
|
||||
location.tangent = Vec.rot([0, -1], toRadian(connectable.rotate));
|
||||
else if (isVecZero(Vec.sub(position, [1, 0.5])))
|
||||
@@ -184,7 +181,11 @@ export function getNearestConnectableAnchor(ele: Connectable, point: IVec) {
|
||||
);
|
||||
}
|
||||
|
||||
function closestPoint(points: PointLocation[], point: IVec) {
|
||||
function closestPoint(
|
||||
points: PointLocation[],
|
||||
point: IVec
|
||||
): PointLocation | null {
|
||||
if (points.length === 0) return null;
|
||||
const rst = points.map(p => ({ p, d: Vec.dist(p, point) }));
|
||||
rst.sort((a, b) => a.d - b.d);
|
||||
return rst[0].p;
|
||||
@@ -245,7 +246,7 @@ function filterConnectablePoints<T extends IVec3 | IVec>(
|
||||
): T[] {
|
||||
return points.filter(point => {
|
||||
if (!bound) return true;
|
||||
return !bound.isPointInBound(point as IVec);
|
||||
return !bound.isPointInBound([point[0], point[1]]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -368,15 +369,17 @@ function pushGapMidPoint(
|
||||
bound.lowerLine,
|
||||
bound2.upperLine,
|
||||
bound2.lowerLine,
|
||||
].map(line => {
|
||||
return lineIntersects(
|
||||
point as unknown as IVec,
|
||||
[point[0], point[1] + 1],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
) as IVec;
|
||||
});
|
||||
]
|
||||
.map(line => {
|
||||
return lineIntersects(
|
||||
[point[0], point[1]],
|
||||
[point[0], point[1] + 1],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
);
|
||||
})
|
||||
.filter(p => p !== null);
|
||||
rst.sort((a, b) => a[1] - b[1]);
|
||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||
pushWithPriority(points, [midPoint], 6);
|
||||
@@ -399,15 +402,17 @@ function pushGapMidPoint(
|
||||
bound.rightLine,
|
||||
bound2.leftLine,
|
||||
bound2.rightLine,
|
||||
].map(line => {
|
||||
return lineIntersects(
|
||||
point as unknown as IVec,
|
||||
[point[0] + 1, point[1]],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
) as IVec;
|
||||
});
|
||||
]
|
||||
.map(line => {
|
||||
return lineIntersects(
|
||||
[point[0], point[1]],
|
||||
[point[0] + 1, point[1]],
|
||||
line[0],
|
||||
line[1],
|
||||
true
|
||||
);
|
||||
})
|
||||
.filter(p => p !== null);
|
||||
rst.sort((a, b) => a[0] - b[0]);
|
||||
const midPoint = Vec.lrp(rst[1], rst[2], 0.5);
|
||||
pushWithPriority(points, [midPoint], 6);
|
||||
@@ -480,14 +485,14 @@ function getConnectablePoints(
|
||||
expandEndBound: Bound | null
|
||||
) {
|
||||
const lineBound = Bound.fromPoints([
|
||||
startPoint,
|
||||
endPoint,
|
||||
] as unknown[] as IVec[]);
|
||||
[startPoint[0], startPoint[1]],
|
||||
[endPoint[0], endPoint[1]],
|
||||
]);
|
||||
const outerBound =
|
||||
expandStartBound &&
|
||||
expandEndBound &&
|
||||
expandStartBound.unite(expandEndBound);
|
||||
let points = [nextStartPoint, lastEndPoint] as IVec3[];
|
||||
let points = [nextStartPoint, lastEndPoint];
|
||||
pushWithPriority(points, lineBound.getVerticesAndMidpoints());
|
||||
|
||||
if (!startBound || !endBound) {
|
||||
@@ -534,7 +539,7 @@ function getConnectablePoints(
|
||||
pushWithPriority(points, expandStartBound.getVerticesAndMidpoints());
|
||||
pushWithPriority(
|
||||
points,
|
||||
expandStartBound.include(lastEndPoint as unknown as IVec).points
|
||||
expandStartBound.include([lastEndPoint[0], lastEndPoint[1]]).points
|
||||
);
|
||||
}
|
||||
|
||||
@@ -542,7 +547,7 @@ function getConnectablePoints(
|
||||
pushWithPriority(points, expandEndBound.getVerticesAndMidpoints());
|
||||
pushWithPriority(
|
||||
points,
|
||||
expandEndBound.include(nextStartPoint as unknown as IVec).points
|
||||
expandEndBound.include([nextStartPoint[0], nextStartPoint[1]]).points
|
||||
);
|
||||
}
|
||||
|
||||
@@ -561,7 +566,7 @@ function getConnectablePoints(
|
||||
almostEqual(item[0], point[0], 0.02) &&
|
||||
almostEqual(item[1], point[1], 0.02)
|
||||
);
|
||||
}) as IVec3[];
|
||||
});
|
||||
if (!startEnds[0] || !startEnds[1]) {
|
||||
throw new BlockSuiteError(
|
||||
BlockSuiteError.ErrorCode.ValueNotExists,
|
||||
@@ -603,7 +608,9 @@ function mergePath(points: IVec[] | IVec3[]) {
|
||||
continue;
|
||||
result.push([cur[0], cur[1]]);
|
||||
}
|
||||
result.push(last(points as IVec[]) as IVec);
|
||||
if (points.length !== 0) {
|
||||
result.push([points[points.length - 1][0], points[points.length - 1][1]]);
|
||||
}
|
||||
for (let i = 0; i < result.length - 1; i++) {
|
||||
const cur = result[i];
|
||||
const next = result[i + 1];
|
||||
@@ -687,7 +694,7 @@ function getNextPoint(
|
||||
offsetW = 10,
|
||||
offsetH = 10
|
||||
) {
|
||||
const result: IVec = Array.from(point) as IVec;
|
||||
const result: IVec = [point[0], point[1]];
|
||||
if (almostEqual(bound.x, result[0])) result[0] -= offsetX;
|
||||
else if (almostEqual(bound.y, result[1])) result[1] -= offsetY;
|
||||
else if (almostEqual(bound.maxX, result[0])) result[0] += offsetW;
|
||||
@@ -993,7 +1000,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
this.highlightPoint = anchor.point;
|
||||
result = {
|
||||
id: connectable.id,
|
||||
position: anchor.coord as IVec,
|
||||
position: anchor.coord,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1001,7 +1008,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
if (shortestDistance < 8 && result) break;
|
||||
|
||||
// if not, check if closes to bound
|
||||
const nearestPoint = connectable.getNearestPoint(point as IVec) as IVec;
|
||||
const nearestPoint = connectable.getNearestPoint(point);
|
||||
|
||||
if (Vec.dist(nearestPoint, point) < 8) {
|
||||
this.highlightPoint = nearestPoint;
|
||||
@@ -1013,9 +1020,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
target.push(connectable);
|
||||
result = {
|
||||
id: connectable.id,
|
||||
position: bound
|
||||
.toRelative(originPoint)
|
||||
.map(n => clamp(n, 0, 1)) as IVec,
|
||||
position: Vec.clampV(bound.toRelative(originPoint), 0, 1),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1048,7 +1053,7 @@ export class ConnectionOverlay extends Overlay {
|
||||
// at last, if not, just return the point
|
||||
if (!result) {
|
||||
result = {
|
||||
position: point as IVec,
|
||||
position: point,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1383,7 +1388,7 @@ export class ConnectorPathGenerator extends PathGenerator {
|
||||
const eb = Bound.deserialize(end.xywh);
|
||||
const startPoint = getNearestConnectableAnchor(start, eb.center);
|
||||
const endPoint = getNearestConnectableAnchor(end, sb.center);
|
||||
return [startPoint, endPoint];
|
||||
return (startPoint && endPoint && [startPoint, endPoint]) ?? [];
|
||||
} else {
|
||||
const endPoint = this._getConnectionPoint(connector, 'target');
|
||||
const startPoint = this._getConnectionPoint(connector, 'source');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DisposableGroup } from '@blocksuite/global/disposable';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
import type { GfxController } from '@blocksuite/std/gfx';
|
||||
import { startWith } from 'rxjs';
|
||||
|
||||
import type { RoughCanvas } from '../utils/rough/canvas';
|
||||
import { Overlay } from './overlay';
|
||||
@@ -18,10 +19,11 @@ export class ToolOverlay extends Overlay {
|
||||
super(gfx);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.globalAlpha = 0;
|
||||
this.globalAlpha = 1;
|
||||
this.gfx = gfx;
|
||||
|
||||
this.disposables.add(
|
||||
this.gfx.viewport.viewportUpdated.subscribe(() => {
|
||||
this.gfx.viewport.viewportUpdated.pipe(startWith(null)).subscribe(() => {
|
||||
// when viewport is updated, we should keep the overlay in the same position
|
||||
// to get last mouse position and convert it to model coordinates
|
||||
const pos = this.gfx.tool.lastMousePos$.value;
|
||||
|
||||
@@ -38,11 +38,11 @@ const styles = css`
|
||||
transition: 0.1s;
|
||||
}
|
||||
|
||||
label.subscribe {
|
||||
label.on {
|
||||
background: var(--affine-primary-color);
|
||||
}
|
||||
|
||||
label.subscribe:after {
|
||||
label.on:after {
|
||||
left: calc(100% - 1px);
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
@@ -326,6 +326,16 @@ export class EdgelessMindmapToolButton extends EdgelessToolbarToolMixin(
|
||||
},
|
||||
{ global: true }
|
||||
);
|
||||
|
||||
// since there is not a tool called mindmap, we need to cancel the drag when the tool is changed
|
||||
this.disposables.add(
|
||||
this.gfx.tool.currentToolName$.subscribe(toolName => {
|
||||
// FIXME: remove the assertion after gfx tool refactor
|
||||
if ((toolName as string) !== 'empty' && this.readyToDrop) {
|
||||
this.draggableController.cancel();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
|
||||
@@ -26,7 +26,6 @@ export class NoteOverlay extends ToolOverlay {
|
||||
|
||||
constructor(gfx: GfxController, background: Color) {
|
||||
super(gfx);
|
||||
this.globalAlpha = 0;
|
||||
this.backgroundColor = gfx.std
|
||||
.get(ThemeProvider)
|
||||
.getColorValue(background, DefaultTheme.noteBackgrounColor, true);
|
||||
|
||||
@@ -236,30 +236,39 @@ export class EdgelessToolbarShapeDraggable extends EdgelessToolbarToolMixin(
|
||||
const locked = this.gfx.viewport.locked;
|
||||
const selection = this.gfx.selection;
|
||||
if (locked || selection.editing) return;
|
||||
|
||||
if (this.readyToDrop) {
|
||||
const activeIndex = shapes.findIndex(
|
||||
s => s.name === this.draggingShape
|
||||
);
|
||||
const nextIndex = (activeIndex + 1) % shapes.length;
|
||||
const next = shapes[nextIndex];
|
||||
this.draggingShape = next.name;
|
||||
|
||||
this.draggableController.cancelWithoutAnimation();
|
||||
}
|
||||
|
||||
const el = this.shapeContainer.querySelector(
|
||||
`.shape.${this.draggingShape}`
|
||||
) as HTMLElement;
|
||||
if (!el) {
|
||||
console.error('Edgeless toolbar Shape element not found');
|
||||
if (
|
||||
this.gfx.tool.dragging$.peek() &&
|
||||
this.gfx.tool.currentToolName$.peek() === 'shape'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { x, y } = this.gfx.tool.lastMousePos$.peek();
|
||||
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
|
||||
const { left, top } = viewport;
|
||||
const clientPos = { x: x + left, y: y + top };
|
||||
this.draggableController.dragAndMoveTo(el, clientPos);
|
||||
|
||||
const activeIndex = shapes.findIndex(
|
||||
s => s.name === this.draggingShape
|
||||
);
|
||||
const nextIndex = (activeIndex + 1) % shapes.length;
|
||||
const next = shapes[nextIndex];
|
||||
this.draggingShape = next.name;
|
||||
|
||||
if (this.readyToDrop) {
|
||||
this.draggableController.cancelWithoutAnimation();
|
||||
const el = this.shapeContainer.querySelector(
|
||||
`.shape.${this.draggingShape}`
|
||||
) as HTMLElement;
|
||||
if (!el) {
|
||||
console.error('Edgeless toolbar Shape element not found');
|
||||
return;
|
||||
}
|
||||
const { x, y } = this.gfx.tool.lastMousePos$.peek();
|
||||
const { viewport } = this.edgeless.std.get(ViewportElementProvider);
|
||||
const { left, top } = viewport;
|
||||
const clientPos = { x: x + left, y: y + top };
|
||||
this.draggableController.dragAndMoveTo(el, clientPos);
|
||||
} else {
|
||||
this.setEdgelessTool('shape', {
|
||||
shapeName: this.draggingShape,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{ global: true }
|
||||
|
||||
@@ -89,7 +89,6 @@ export class ShapeTool extends BaseTool<ShapeToolOption> {
|
||||
|
||||
private _hideOverlay() {
|
||||
if (!this._shapeOverlay) return;
|
||||
|
||||
this._shapeOverlay.globalAlpha = 0;
|
||||
(this.gfx.surfaceComponent as SurfaceBlockComponent)?.refresh();
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ export class ShapeElementView extends GfxElementModelView<ShapeElementModel> {
|
||||
}
|
||||
|
||||
private _initDblClickToEdit(): void {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
|
||||
this.on('dblclick', () => {
|
||||
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
||||
|
||||
if (edgeless && !this.model.isLocked()) {
|
||||
mountShapeTextEditor(this.model, edgeless);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createIdentifier } from '@blocksuite/global/di';
|
||||
import type { GfxBlockComponent } from '@blocksuite/std';
|
||||
import type { EditorHost } from '@blocksuite/std';
|
||||
import type { ViewportRecord } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
import { Extension } from '@blocksuite/store';
|
||||
|
||||
import type { BlockLayout, Rect } from '../types';
|
||||
@@ -8,7 +10,13 @@ export abstract class BlockLayoutHandlerExtension<
|
||||
T extends BlockLayout = BlockLayout,
|
||||
> extends Extension {
|
||||
abstract readonly blockType: string;
|
||||
abstract queryLayout(component: GfxBlockComponent): T | null;
|
||||
|
||||
abstract queryLayout(
|
||||
model: BlockModel,
|
||||
host: EditorHost,
|
||||
viewportRecord: ViewportRecord
|
||||
): T | null;
|
||||
|
||||
abstract calculateBound(layout: T): {
|
||||
rect: Rect;
|
||||
subRects: Rect[];
|
||||
|
||||
@@ -7,8 +7,9 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import type {
|
||||
BlockLayoutPainter,
|
||||
BlockLayoutTreeNode,
|
||||
HostToWorkerMessage,
|
||||
ViewportLayout,
|
||||
ViewportLayoutTree,
|
||||
WorkerToHostMessage,
|
||||
} from '../types';
|
||||
|
||||
@@ -33,8 +34,8 @@ export class ViewportLayoutPainter {
|
||||
private zoom = 1;
|
||||
public provider: ServiceProvider;
|
||||
|
||||
getPainter(type: string): BlockLayoutPainter | undefined {
|
||||
return this.provider.get(BlockPainterProvider(type));
|
||||
getPainter(type: string): BlockLayoutPainter | null {
|
||||
return this.provider.getOptional(BlockPainterProvider(type));
|
||||
}
|
||||
|
||||
constructor(extensions: ExtensionType[]) {
|
||||
@@ -66,24 +67,28 @@ export class ViewportLayoutPainter {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
}
|
||||
|
||||
paint(layout: ViewportLayout, version: number) {
|
||||
paint(layout: ViewportLayoutTree, version: number) {
|
||||
const { canvas, ctx } = this;
|
||||
if (!canvas || !ctx) return;
|
||||
if (layout.rect.w === 0 || layout.rect.h === 0) {
|
||||
console.warn('empty layout rect');
|
||||
return;
|
||||
}
|
||||
|
||||
this.paintTree(layout, version);
|
||||
}
|
||||
|
||||
paintTree(layout: ViewportLayoutTree, version: number) {
|
||||
const { canvas, ctx } = this;
|
||||
const { overallRect } = layout;
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
this.clearBackground();
|
||||
|
||||
ctx.scale(this.zoom, this.zoom);
|
||||
|
||||
layout.blocks.forEach(blockLayout => {
|
||||
const painter = this.getPainter(blockLayout.type);
|
||||
if (!painter) return;
|
||||
painter.paint(ctx, blockLayout, layout.rect.x, layout.rect.y);
|
||||
});
|
||||
const paintNode = (node: BlockLayoutTreeNode) => {
|
||||
const painter = this.getPainter(node.type);
|
||||
painter?.paint(ctx, node.layout, overallRect.x, overallRect.y);
|
||||
node.children.forEach(paintNode);
|
||||
};
|
||||
|
||||
layout.roots.forEach(root => paintNode(root));
|
||||
const bitmap = canvas.transferToImageBitmap();
|
||||
const message: WorkerToHostMessage = {
|
||||
type: 'bitmapPainted',
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { EditorHost, GfxBlockComponent } from '@blocksuite/std';
|
||||
import {
|
||||
GfxBlockElementModel,
|
||||
GfxControllerIdentifier,
|
||||
type Viewport,
|
||||
} from '@blocksuite/std/gfx';
|
||||
import { type Viewport } from '@blocksuite/std/gfx';
|
||||
import type { BlockModel } from '@blocksuite/store';
|
||||
|
||||
import { BlockLayoutHandlersIdentifier } from './layout/block-layout-provider';
|
||||
import type { BlockLayout, RenderingState, ViewportLayout } from './types';
|
||||
import type {
|
||||
BlockLayout,
|
||||
BlockLayoutTreeNode,
|
||||
RenderingState,
|
||||
ViewportLayoutTree,
|
||||
} from './types';
|
||||
|
||||
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
||||
const hostRect = host.getBoundingClientRect();
|
||||
@@ -21,33 +23,10 @@ export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
|
||||
canvas.style.pointerEvents = 'none';
|
||||
}
|
||||
|
||||
function getBlockLayouts(host: EditorHost): BlockLayout[] {
|
||||
const gfx = host.std.get(GfxControllerIdentifier);
|
||||
const models = gfx.gfxElements.filter(e => e instanceof GfxBlockElementModel);
|
||||
const components = models
|
||||
.map(model => gfx.view.get(model.id))
|
||||
.filter(Boolean) as GfxBlockComponent[];
|
||||
|
||||
const layouts: BlockLayout[] = [];
|
||||
components.forEach(component => {
|
||||
const layoutHandlers = host.std.provider.getAll(
|
||||
BlockLayoutHandlersIdentifier
|
||||
);
|
||||
const handlersArray = Array.from(layoutHandlers.values());
|
||||
for (const handler of handlersArray) {
|
||||
const layout = handler.queryLayout(component);
|
||||
if (layout) {
|
||||
layouts.push(layout);
|
||||
}
|
||||
}
|
||||
});
|
||||
return layouts;
|
||||
}
|
||||
|
||||
export function getViewportLayout(
|
||||
export function getViewportLayoutTree(
|
||||
host: EditorHost,
|
||||
viewport: Viewport
|
||||
): ViewportLayout {
|
||||
): ViewportLayoutTree {
|
||||
const zoom = viewport.zoom;
|
||||
|
||||
let layoutMinX = Infinity;
|
||||
@@ -55,36 +34,106 @@ export function getViewportLayout(
|
||||
let layoutMaxX = -Infinity;
|
||||
let layoutMaxY = -Infinity;
|
||||
|
||||
const blockLayouts = getBlockLayouts(host);
|
||||
const store = host.std.store;
|
||||
const rootModel = store.root;
|
||||
|
||||
if (!rootModel) {
|
||||
return { roots: [], overallRect: { x: 0, y: 0, w: 0, h: 0 } };
|
||||
}
|
||||
|
||||
const providers = host.std.provider.getAll(BlockLayoutHandlersIdentifier);
|
||||
const providersArray = Array.from(providers.values());
|
||||
|
||||
blockLayouts.forEach(blockLayout => {
|
||||
const provider = providersArray.find(p => p.blockType === blockLayout.type);
|
||||
if (!provider) return;
|
||||
// Recursive function to build the tree structure
|
||||
const buildLayoutTreeNode = (
|
||||
model: BlockModel,
|
||||
ancestorViewportState?: string | null
|
||||
): BlockLayoutTreeNode | null => {
|
||||
const baseLayout: BlockLayout = {
|
||||
blockId: model.id,
|
||||
type: model.flavour,
|
||||
rect: { x: 0, y: 0, w: 0, h: 0 },
|
||||
};
|
||||
|
||||
const { rect } = provider.calculateBound(blockLayout);
|
||||
const handler = providersArray.find(p => p.blockType === model.flavour);
|
||||
|
||||
layoutMinX = Math.min(layoutMinX, rect.x);
|
||||
layoutMinY = Math.min(layoutMinY, rect.y);
|
||||
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
|
||||
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
|
||||
});
|
||||
// Determine the correct viewport state to use
|
||||
const component = host.std.view.getBlock(model.id) as GfxBlockComponent;
|
||||
const currentViewportState = component?.dataset.viewportState;
|
||||
const effectiveViewportState =
|
||||
currentViewportState ?? ancestorViewportState;
|
||||
const defaultViewportState = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
viewportX: 0,
|
||||
viewportY: 0,
|
||||
zoom: 1,
|
||||
viewScale: 1,
|
||||
};
|
||||
|
||||
const layoutModelCoord = [layoutMinX, layoutMinY];
|
||||
const viewportRecord = effectiveViewportState
|
||||
? viewport.deserializeRecord(effectiveViewportState) ||
|
||||
defaultViewportState
|
||||
: defaultViewportState;
|
||||
|
||||
const layoutData = handler?.queryLayout(model, host, viewportRecord);
|
||||
|
||||
if (handler && layoutData) {
|
||||
const { rect } = handler.calculateBound(layoutData);
|
||||
baseLayout.rect = rect;
|
||||
layoutMinX = Math.min(layoutMinX, rect.x);
|
||||
layoutMinY = Math.min(layoutMinY, rect.y);
|
||||
layoutMaxX = Math.max(layoutMaxX, rect.x + rect.w);
|
||||
layoutMaxY = Math.max(layoutMaxY, rect.y + rect.h);
|
||||
}
|
||||
|
||||
const children: BlockLayoutTreeNode[] = [];
|
||||
for (const childModel of model.children) {
|
||||
const childNode = buildLayoutTreeNode(childModel, effectiveViewportState);
|
||||
if (childNode) {
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Create node for this block - ALWAYS return a node
|
||||
// Return the node structure including the layout (either real or fallback)
|
||||
return {
|
||||
blockId: model.id,
|
||||
type: model.flavour,
|
||||
layout: layoutData ? { ...baseLayout, ...layoutData } : baseLayout,
|
||||
children,
|
||||
};
|
||||
};
|
||||
|
||||
const roots: BlockLayoutTreeNode[] = [];
|
||||
const rootNode = buildLayoutTreeNode(rootModel);
|
||||
if (rootNode) {
|
||||
roots.push(rootNode);
|
||||
}
|
||||
|
||||
// If no valid layouts were found, use default values
|
||||
if (layoutMinX === Infinity) {
|
||||
layoutMinX = 0;
|
||||
layoutMinY = 0;
|
||||
layoutMaxX = 0;
|
||||
layoutMaxY = 0;
|
||||
}
|
||||
|
||||
// Calculate overall rectangle
|
||||
const w = (layoutMaxX - layoutMinX) / zoom / viewport.viewScale;
|
||||
const h = (layoutMaxY - layoutMinY) / zoom / viewport.viewScale;
|
||||
const layout: ViewportLayout = {
|
||||
blocks: blockLayouts,
|
||||
rect: {
|
||||
x: layoutModelCoord[0],
|
||||
y: layoutModelCoord[1],
|
||||
|
||||
const result = {
|
||||
roots,
|
||||
overallRect: {
|
||||
x: layoutMinX,
|
||||
y: layoutMinY,
|
||||
w: Math.max(w, 0),
|
||||
h: Math.max(h, 0),
|
||||
},
|
||||
};
|
||||
return layout;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function debugLog(message: string, state: RenderingState) {
|
||||
@@ -98,14 +147,15 @@ export function debugLog(message: string, state: RenderingState) {
|
||||
export function paintPlaceholder(
|
||||
host: EditorHost,
|
||||
canvas: HTMLCanvasElement,
|
||||
layout: ViewportLayout | null,
|
||||
layout: ViewportLayoutTree | null,
|
||||
viewport: Viewport
|
||||
) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (!layout) return;
|
||||
if (!ctx || !layout) return;
|
||||
|
||||
const dpr = window.devicePixelRatio;
|
||||
const layoutViewCoord = viewport.toViewCoord(layout.rect.x, layout.rect.y);
|
||||
const { overallRect } = layout;
|
||||
const layoutViewCoord = viewport.toViewCoord(overallRect.x, overallRect.y);
|
||||
|
||||
const offsetX = layoutViewCoord[0];
|
||||
const offsetY = layoutViewCoord[1];
|
||||
@@ -120,30 +170,28 @@ export function paintPlaceholder(
|
||||
);
|
||||
const handlersArray = Array.from(layoutHandlers.values());
|
||||
|
||||
layout.blocks.forEach((blockLayout, blockIndex) => {
|
||||
ctx.fillStyle = colors[blockIndex % colors.length];
|
||||
const renderedPositions = new Set<string>();
|
||||
|
||||
const handler = handlersArray.find(h => h.blockType === blockLayout.type);
|
||||
if (!handler) return;
|
||||
const { subRects } = handler.calculateBound(blockLayout);
|
||||
|
||||
subRects.forEach(rect => {
|
||||
const x = ((rect.x - layout.rect.x) * viewport.zoom + offsetX) * dpr;
|
||||
const y = ((rect.y - layout.rect.y) * viewport.zoom + offsetY) * dpr;
|
||||
|
||||
const paintNode = (node: BlockLayoutTreeNode, depth: number = 0) => {
|
||||
const { layout: nodeLayout, type } = node;
|
||||
const handler = handlersArray.find(h => h.blockType === type);
|
||||
if (handler) {
|
||||
ctx.fillStyle = colors[depth % colors.length];
|
||||
const rect = nodeLayout.rect;
|
||||
const x = ((rect.x - overallRect.x) * viewport.zoom + offsetX) * dpr;
|
||||
const y = ((rect.y - overallRect.y) * viewport.zoom + offsetY) * dpr;
|
||||
const width = rect.w * viewport.zoom * dpr;
|
||||
const height = rect.h * viewport.zoom * dpr;
|
||||
|
||||
const posKey = `${x},${y}`;
|
||||
if (renderedPositions.has(posKey)) return;
|
||||
ctx.fillRect(x, y, width, height);
|
||||
if (width > 10 && height > 5) {
|
||||
ctx.strokeStyle = 'rgba(150, 150, 150, 0.3)';
|
||||
ctx.strokeRect(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
renderedPositions.add(posKey);
|
||||
});
|
||||
});
|
||||
if (node.children.length > 0) {
|
||||
node.children.forEach(childNode => paintNode(childNode, depth + 1));
|
||||
}
|
||||
};
|
||||
|
||||
layout.roots.forEach(rootNode => paintNode(rootNode));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import { debounceTime } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
debugLog,
|
||||
getViewportLayout,
|
||||
getViewportLayoutTree,
|
||||
paintPlaceholder,
|
||||
syncCanvasSize,
|
||||
} from './renderer-utils';
|
||||
@@ -28,7 +28,7 @@ import type {
|
||||
RendererOptions,
|
||||
RenderingState,
|
||||
TurboRendererConfig,
|
||||
ViewportLayout,
|
||||
ViewportLayoutTree,
|
||||
WorkerToHostMessage,
|
||||
} from './types';
|
||||
|
||||
@@ -42,6 +42,34 @@ const defaultOptions: RendererOptions = {
|
||||
export const TurboRendererConfigFactory =
|
||||
ConfigExtensionFactory<TurboRendererConfig>('viewport-turbo-renderer');
|
||||
|
||||
/**
|
||||
* Manages the Turbo Rendering process for the viewport, coordinating between the main thread and a painter worker.
|
||||
* Turbo Rendering optimizes performance by rendering block content onto a canvas bitmap,
|
||||
* falling back to standard DOM rendering during interactions.
|
||||
*
|
||||
* To add Turbo Rendering support for a new block type (e.g., 'affine:my-block'):
|
||||
*
|
||||
* 1. **In the block's package (e.g., `blocksuite/affine/blocks/my-block`):**
|
||||
* a. Add `@blocksuite/affine/gfx/turbo-renderer` as a dependency in `package.json` and create a `src/turbo` directory.
|
||||
* b. Implement the Layout Handler (e.g., `MyBlockLayoutHandlerExtension`) and Painter Worker (e.g., `MyBlockLayoutPainterExtension`). Refer to `ParagraphLayoutHandlerExtension` and `ParagraphLayoutPainterExtension` in `blocksuite/affine/blocks/block-paragraph` for implementation examples.
|
||||
* c. Export the Layout Handler and Painter Worker extensions from the block package's main `src/index.ts` by adding these two explicit export statements:
|
||||
* ```typescript
|
||||
* export * from './turbo/my-block-layout-handler';
|
||||
* export * from './turbo/my-block-painter.worker';
|
||||
* ```
|
||||
* d. Add an export mapping for the painter worker in `package.json` under the `exports` field (e.g., `"./turbo-painter": "./src/turbo/my-block-painter.worker.ts"`).
|
||||
* e. Add a TypeScript project reference to `blocksuite/affine/gfx/turbo-renderer` in `tsconfig.json`.
|
||||
*
|
||||
* 2. **In the application integration point (e.g., `packages/frontend/core/src/blocksuite/extensions` and `blocksuite/integration-test/src/__tests__/utils/renderer-entry.ts`):**
|
||||
* a. In `turbo-renderer.ts` (or the file setting up `TurboRendererConfigFactory`):
|
||||
* - Import and add the new Layout Handler extension to the `patchTurboRendererExtension` array (or equivalent DI setup). See how `ParagraphLayoutHandlerExtension` is added as a reference.
|
||||
* b. In `turbo-painter.worker.ts` (the painter worker entry point):
|
||||
* - Import and add the new Painter Worker extension to the `ViewportLayoutPainter` constructor's extension array. See how `ParagraphLayoutPainterExtension` is added as a reference.
|
||||
*
|
||||
* 3. **Run `yarn affine init`** from the workspace root to update generated configuration files (`workspace.gen.ts`) and the lockfile (`yarn.lock`).
|
||||
*
|
||||
* **Note:** Always ensure the directory structure and export patterns match the `paragraph` block (`blocksuite/affine/blocks/block-paragraph`) for consistency.
|
||||
*/
|
||||
export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
static override key = 'viewportTurboRenderer';
|
||||
|
||||
@@ -49,7 +77,7 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
public readonly canvas: HTMLCanvasElement = document.createElement('canvas');
|
||||
private readonly worker: Worker;
|
||||
private readonly disposables = new DisposableGroup();
|
||||
private layoutCacheData: ViewportLayout | null = null;
|
||||
private layoutCacheData: ViewportLayoutTree | null = null;
|
||||
private layoutVersion = 0;
|
||||
private bitmap: ImageBitmap | null = null;
|
||||
private viewportElement: GfxViewportElement | null = null;
|
||||
@@ -172,9 +200,9 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
|
||||
get layoutCache() {
|
||||
if (this.layoutCacheData) return this.layoutCacheData;
|
||||
const layout = getViewportLayout(this.std.host, this.viewport);
|
||||
const layoutTree = getViewportLayoutTree(this.std.host, this.viewport);
|
||||
this.debugLog('Layout cache updated');
|
||||
return (this.layoutCacheData = layout);
|
||||
return (this.layoutCacheData = layoutTree);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
@@ -248,8 +276,8 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
type: 'paintLayout',
|
||||
data: {
|
||||
layout,
|
||||
width: layout.rect.w,
|
||||
height: layout.rect.h,
|
||||
width: layout.overallRect.w,
|
||||
height: layout.overallRect.h,
|
||||
dpr,
|
||||
zoom: this.viewport.zoom,
|
||||
version: currentVersion,
|
||||
@@ -316,17 +344,18 @@ export class ViewportTurboRendererExtension extends GfxExtension {
|
||||
if (!ctx) return;
|
||||
|
||||
this.clearCanvas();
|
||||
|
||||
const layoutViewCoord = this.viewport.toViewCoord(
|
||||
layout.rect.x,
|
||||
layout.rect.y
|
||||
layout.overallRect.x,
|
||||
layout.overallRect.y
|
||||
);
|
||||
|
||||
ctx.drawImage(
|
||||
bitmap,
|
||||
layoutViewCoord[0] * window.devicePixelRatio,
|
||||
layoutViewCoord[1] * window.devicePixelRatio,
|
||||
layout.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
||||
layout.rect.h * window.devicePixelRatio * this.viewport.zoom
|
||||
layout.overallRect.w * window.devicePixelRatio * this.viewport.zoom,
|
||||
layout.overallRect.h * window.devicePixelRatio * this.viewport.zoom
|
||||
);
|
||||
|
||||
this.debugLog('Bitmap drawn to canvas');
|
||||
|
||||
@@ -14,13 +14,14 @@ export interface ViewportState {
|
||||
}
|
||||
|
||||
export interface BlockLayout extends Record<string, unknown> {
|
||||
blockId: string;
|
||||
type: string;
|
||||
rect?: Rect;
|
||||
}
|
||||
|
||||
export interface ViewportLayout {
|
||||
blocks: BlockLayout[];
|
||||
rect: Rect;
|
||||
rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TextRect {
|
||||
@@ -60,7 +61,7 @@ export type WorkerToHostMessage = MessageBitmapPainted | MessagePaintError;
|
||||
export type MessagePaint = {
|
||||
type: 'paintLayout';
|
||||
data: {
|
||||
layout: ViewportLayout;
|
||||
layout: ViewportLayoutTree;
|
||||
width: number;
|
||||
height: number;
|
||||
dpr: number;
|
||||
@@ -89,3 +90,15 @@ export interface TurboRendererConfig {
|
||||
}
|
||||
|
||||
export type HostToWorkerMessage = MessagePaint;
|
||||
|
||||
export interface BlockLayoutTreeNode {
|
||||
blockId: string;
|
||||
type: string;
|
||||
layout: BlockLayout;
|
||||
children: BlockLayoutTreeNode[];
|
||||
}
|
||||
|
||||
export interface ViewportLayoutTree {
|
||||
roots: BlockLayoutTreeNode[];
|
||||
overallRect: BlockLayout['rect'];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,24 @@ export function randomSeed(): number {
|
||||
return Math.floor(Math.random() * 2 ** 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the intersection point of two line segments.
|
||||
*
|
||||
* @param sp - Start point of the first line segment [x, y]
|
||||
* @param ep - End point of the first line segment [x, y]
|
||||
* @param sp2 - Start point of the second line segment [x, y]
|
||||
* @param ep2 - End point of the second line segment [x, y]
|
||||
* @param infinite - If true, treats the lines as infinite lines rather than line segments
|
||||
* @returns The intersection point [x, y] if the lines intersect, null if they are parallel or coincident
|
||||
*
|
||||
* @example
|
||||
* const intersection = lineIntersects([0, 0], [2, 2], [0, 2], [2, 0]);
|
||||
* // Returns [1, 1] - the intersection point of the two line segments
|
||||
*
|
||||
* @example
|
||||
* const parallel = lineIntersects([0, 0], [2, 2], [0, 1], [2, 3], true);
|
||||
* // Returns null - the lines are parallel
|
||||
*/
|
||||
export function lineIntersects(
|
||||
sp: IVec,
|
||||
ep: IVec,
|
||||
@@ -45,10 +63,23 @@ export function lineIntersects(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the nearest point on a polygon to a given point.
|
||||
*
|
||||
* @param points - Array of points defining the polygon vertices [x, y][]
|
||||
* @param point - The point to find the nearest point to [x, y]
|
||||
* @returns The nearest point on the polygon to the given point
|
||||
* @throws Error if points array is empty or has less than 2 points
|
||||
*/
|
||||
export function polygonNearestPoint(points: IVec[], point: IVec) {
|
||||
const len = points.length;
|
||||
let rst: IVec;
|
||||
let dis = Infinity;
|
||||
if (len < 2) {
|
||||
throw new Error('Polygon must have at least 2 points');
|
||||
}
|
||||
|
||||
let rst: IVec = points[0]; // Initialize with first point as fallback
|
||||
let dis = Vec.dist(points[0], point);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const p = points[i];
|
||||
const p2 = points[(i + 1) % len];
|
||||
@@ -59,7 +90,7 @@ export function polygonNearestPoint(points: IVec[], point: IVec) {
|
||||
rst = temp;
|
||||
}
|
||||
}
|
||||
return rst!;
|
||||
return rst;
|
||||
}
|
||||
|
||||
export function polygonPointDistance(points: IVec[], point: IVec) {
|
||||
|
||||
@@ -341,8 +341,15 @@ export class Bound implements IBound {
|
||||
return serializeXYWH(this.x, this.y, this.w, this.h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a point to relative coordinates.
|
||||
* @param point - The point to convert.
|
||||
* @returns The normalized relative coordinates of the point.
|
||||
*/
|
||||
toRelative([x, y]: IVec): IVec {
|
||||
return [(x - this.x) / this.w, (y - this.y) / this.h];
|
||||
const normalizedX = this.w === 0 ? 0 : (x - this.x) / this.w;
|
||||
const normalizedY = this.h === 0 ? 0 : (y - this.y) / this.h;
|
||||
return [normalizedX, normalizedY];
|
||||
}
|
||||
|
||||
toXYWH(): XYWH {
|
||||
|
||||
@@ -565,6 +565,8 @@ export class Vec {
|
||||
* @param n
|
||||
* @param min
|
||||
*/
|
||||
static clampV(A: IVec, min: number, max?: number): IVec;
|
||||
|
||||
static clampV(A: number[], min: number): number[];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
|
||||
@@ -214,7 +214,12 @@ export class Viewport {
|
||||
* This property is used to calculate the scale of the editor.
|
||||
*/
|
||||
get viewScale() {
|
||||
if (!this._shell || this._cachedOffsetWidth === null) return 1;
|
||||
if (
|
||||
!this._shell ||
|
||||
this._cachedOffsetWidth === null ||
|
||||
this._cachedOffsetWidth === 0
|
||||
)
|
||||
return 1;
|
||||
return this.boundingClientRect.width / this._cachedOffsetWidth;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
|
||||
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import {
|
||||
TurboRendererConfigFactory,
|
||||
@@ -11,6 +12,7 @@ import { createPainterWorker, setupEditor } from './setup.js';
|
||||
async function init() {
|
||||
setupEditor('edgeless', [
|
||||
ParagraphLayoutHandlerExtension,
|
||||
ListLayoutHandlerExtension,
|
||||
TurboRendererConfigFactory({
|
||||
painterWorkerEntry: createPainterWorker,
|
||||
}),
|
||||
|
||||
@@ -103,7 +103,7 @@ async function createEditor(
|
||||
|
||||
export function createPainterWorker() {
|
||||
const worker = new Worker(
|
||||
new URL('./turbo-painter-entry.worker.ts', import.meta.url),
|
||||
new URL('./turbo-painter.worker.ts', import.meta.url),
|
||||
{
|
||||
type: 'module',
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine-block-paragraph/turbo-painter';
|
||||
import { ViewportLayoutPainter } from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
|
||||
new ViewportLayoutPainter([ParagraphLayoutPainterExtension]);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ListLayoutPainterExtension } from '@blocksuite/affine-block-list/turbo-painter';
|
||||
import { NoteLayoutPainterExtension } from '@blocksuite/affine-block-note/turbo-painter';
|
||||
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine-block-paragraph/turbo-painter';
|
||||
import { ViewportLayoutPainter } from '@blocksuite/affine-gfx-turbo-renderer/painter';
|
||||
|
||||
new ViewportLayoutPainter([
|
||||
ParagraphLayoutPainterExtension,
|
||||
ListLayoutPainterExtension,
|
||||
NoteLayoutPainterExtension,
|
||||
]);
|
||||
@@ -82,7 +82,7 @@
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.2.11",
|
||||
"msw": "^2.6.8",
|
||||
"oxlint": "0.16.4",
|
||||
"oxlint": "0.16.5",
|
||||
"prettier": "^3.4.2",
|
||||
"semver": "^7.6.3",
|
||||
"serve": "^14.2.4",
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.28.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@prisma/instrumentation": "^5.22.0",
|
||||
"@react-email/components": "0.0.35",
|
||||
"@react-email/components": "0.0.36",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"ai": "^4.1.51",
|
||||
"bullmq": "^5.40.2",
|
||||
@@ -140,7 +140,7 @@
|
||||
"c8": "^10.1.3",
|
||||
"cross-env": "^7.0.3",
|
||||
"nodemon": "^3.1.7",
|
||||
"react-email": "4.0.3",
|
||||
"react-email": "4.0.7",
|
||||
"sinon": "^20.0.0",
|
||||
"supertest": "^7.0.0",
|
||||
"why-is-node-running": "^3.2.2"
|
||||
|
||||
@@ -162,7 +162,7 @@ export class TeamWorkspaceResolver {
|
||||
if (Number.isSafeInteger(expireTime)) {
|
||||
return {
|
||||
link: this.url.link(`/invite/${id.inviteId}`),
|
||||
expireTime: new Date(Date.now() + expireTime),
|
||||
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ export class TeamWorkspaceResolver {
|
||||
if (Number.isSafeInteger(expireTime)) {
|
||||
return {
|
||||
link: this.url.link(`/invite/${invite.inviteId}`),
|
||||
expireTime: new Date(Date.now() + expireTime),
|
||||
expireTime: new Date(Date.now() + expireTime * 1000), // Convert seconds to milliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ export class Env implements AppEnv {
|
||||
);
|
||||
DEPLOYMENT_TYPE = readEnv(
|
||||
'DEPLOYMENT_TYPE',
|
||||
DeploymentType.Selfhosted,
|
||||
this.dev ? DeploymentType.Affine : DeploymentType.Selfhosted,
|
||||
Object.values(DeploymentType)
|
||||
);
|
||||
FLAVOR = readEnv('SERVER_FLAVOR', Flavor.AllInOne, Object.values(Flavor));
|
||||
|
||||
8
packages/common/env/src/worker.ts
vendored
8
packages/common/env/src/worker.ts
vendored
@@ -1,7 +1,5 @@
|
||||
export function getWorkerUrl(name: string) {
|
||||
if (BUILD_CONFIG.debug && !name.endsWith('.worker.js')) {
|
||||
throw new Error(`worker should be named with '.worker.js', get ${name}`);
|
||||
}
|
||||
|
||||
return environment.workerPath + name + '?v=' + BUILD_CONFIG.appVersion;
|
||||
return (
|
||||
environment.workerPath + `${name}-${BUILD_CONFIG.appVersion}.worker.js`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.7.2'
|
||||
classpath 'com.android.tools.build:gradle:8.9.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,15 @@ apollo = "4.1.1"
|
||||
appcompat = "1.7.0"
|
||||
browser = "1.8.0"
|
||||
coordinatorLayout = "1.3.0"
|
||||
coreKtx = "1.15.0"
|
||||
coreKtx = "1.16.0"
|
||||
coreSplashScreen = "1.0.1"
|
||||
googleServices = "4.4.2"
|
||||
jna = "5.17.0"
|
||||
junitVersion = "4.13.2"
|
||||
kotlin = "2.1.20"
|
||||
kotlinxCoroutines = "1.10.1"
|
||||
kotlinxCoroutines = "1.10.2"
|
||||
material = "1.12.0"
|
||||
material3 = "1.3.1"
|
||||
material3 = "1.3.2"
|
||||
rustAndroid = "0.9.6"
|
||||
|
||||
[libraries]
|
||||
|
||||
@@ -48,7 +48,7 @@ import { AffineTheme } from './plugins/affine-theme';
|
||||
import { AIButton } from './plugins/ai-button';
|
||||
|
||||
const storeManagerClient = new StoreManagerClient(
|
||||
new OpClient(new Worker(getWorkerUrl('nbstore.worker.js')))
|
||||
new OpClient(new Worker(getWorkerUrl('nbstore')))
|
||||
);
|
||||
window.addEventListener('beforeunload', () => {
|
||||
storeManagerClient.dispose();
|
||||
|
||||
@@ -407,7 +407,7 @@ export function App() {
|
||||
}
|
||||
|
||||
function createStoreManagerClient() {
|
||||
const worker = new Worker(getWorkerUrl('nbstore.worker.js'));
|
||||
const worker = new Worker(getWorkerUrl('nbstore'));
|
||||
const { port1: nativeDBApiChannelServer, port2: nativeDBApiChannelClient } =
|
||||
new MessageChannel();
|
||||
AsyncCall<typeof NbStoreNativeDBApis>(NbStoreNativeDBApis, {
|
||||
|
||||
@@ -23,7 +23,7 @@ import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
let storeManagerClient: StoreManagerClient;
|
||||
|
||||
const workerUrl = getWorkerUrl('nbstore.worker.js');
|
||||
const workerUrl = getWorkerUrl('nbstore');
|
||||
if (window.SharedWorker) {
|
||||
const worker = new SharedWorker(workerUrl, { name: 'affine-shared-worker' });
|
||||
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));
|
||||
|
||||
@@ -24,13 +24,15 @@ const cache = createEmotionCache();
|
||||
|
||||
let storeManagerClient: StoreManagerClient;
|
||||
|
||||
const workerUrl = getWorkerUrl('nbstore.worker.js');
|
||||
const workerUrl = getWorkerUrl('nbstore');
|
||||
|
||||
if (
|
||||
window.SharedWorker &&
|
||||
localStorage.getItem('disableSharedWorker') !== 'true'
|
||||
) {
|
||||
const worker = new SharedWorker(workerUrl);
|
||||
const worker = new SharedWorker(workerUrl, {
|
||||
name: 'affine-shared-worker',
|
||||
});
|
||||
storeManagerClient = new StoreManagerClient(new OpClient(worker.port));
|
||||
} else {
|
||||
const worker = new Worker(workerUrl);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ListLayoutPainterExtension } from '@blocksuite/affine/blocks/list';
|
||||
import { NoteLayoutPainterExtension } from '@blocksuite/affine/blocks/note';
|
||||
import { ParagraphLayoutPainterExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import { ViewportLayoutPainter } from '@blocksuite/affine/gfx/turbo-renderer';
|
||||
|
||||
new ViewportLayoutPainter([
|
||||
ParagraphLayoutPainterExtension,
|
||||
ListLayoutPainterExtension,
|
||||
NoteLayoutPainterExtension,
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getWorkerUrl } from '@affine/env/worker';
|
||||
import { ListLayoutHandlerExtension } from '@blocksuite/affine/blocks/list';
|
||||
import { NoteLayoutHandlerExtension } from '@blocksuite/affine/blocks/note';
|
||||
import { ParagraphLayoutHandlerExtension } from '@blocksuite/affine/blocks/paragraph';
|
||||
import {
|
||||
TurboRendererConfigFactory,
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
} from '@blocksuite/affine/gfx/turbo-renderer';
|
||||
|
||||
function createPainterWorker() {
|
||||
const worker = new Worker(getWorkerUrl('turbo-painter-entry.worker.js'));
|
||||
const worker = new Worker(getWorkerUrl('turbo-painter'));
|
||||
return worker;
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ export function patchTurboRendererExtension() {
|
||||
return [
|
||||
ParagraphLayoutHandlerExtension,
|
||||
ListLayoutHandlerExtension,
|
||||
NoteLayoutHandlerExtension,
|
||||
TurboRendererConfigFactory({
|
||||
options: {
|
||||
zoomThreshold: 1,
|
||||
|
||||
@@ -276,7 +276,7 @@ const ActionModal = ({
|
||||
),
|
||||
2: (
|
||||
<a
|
||||
href="https://affine.pro/pricing"
|
||||
href="https://affine.pro/pricing/?type=selfhost#table"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: 'var(--affine-link-color)' }}
|
||||
|
||||
@@ -376,6 +376,10 @@ export class AtMenuConfigService extends Service {
|
||||
|
||||
close();
|
||||
|
||||
track.doc.editor.atMenu.mentionMember({
|
||||
type: 'member',
|
||||
});
|
||||
|
||||
const inlineRange = inlineEditor.getInlineRange();
|
||||
if (!inlineRange || inlineRange.length !== 0) return;
|
||||
|
||||
@@ -440,6 +444,8 @@ export class AtMenuConfigService extends Service {
|
||||
const err = UserFriendlyError.fromAny(error);
|
||||
|
||||
if (err.is(ErrorNames.MENTION_USER_DOC_ACCESS_DENIED)) {
|
||||
track.doc.editor.atMenu.noAccessPrompted();
|
||||
|
||||
const canUserManage = this.guardService.can$(
|
||||
'Doc_Users_Manage',
|
||||
docId
|
||||
@@ -456,6 +462,11 @@ export class AtMenuConfigService extends Service {
|
||||
action: {
|
||||
label: 'Invite',
|
||||
onClick: async () => {
|
||||
track.$.sharePanel.$.inviteUserDocRole({
|
||||
control: 'member list',
|
||||
role: 'reader',
|
||||
});
|
||||
|
||||
try {
|
||||
await this.docGrantedUsersService.updateUserRole(
|
||||
id,
|
||||
@@ -517,6 +528,11 @@ export class AtMenuConfigService extends Service {
|
||||
icon: UserIcon(),
|
||||
action: () => {
|
||||
close();
|
||||
|
||||
track.doc.editor.atMenu.mentionMember({
|
||||
type: 'invite',
|
||||
});
|
||||
|
||||
this.dialogService.open('setting', {
|
||||
activeTab: 'workspace:members',
|
||||
});
|
||||
|
||||
@@ -6,7 +6,12 @@ export { NotificationType } from './stores/notification';
|
||||
|
||||
import type { Framework } from '@toeverything/infra';
|
||||
|
||||
import { GraphQLService, ServerScope, ServerService } from '../cloud';
|
||||
import {
|
||||
AuthService,
|
||||
GraphQLService,
|
||||
ServerScope,
|
||||
ServerService,
|
||||
} from '../cloud';
|
||||
import { GlobalSessionState } from '../storage';
|
||||
import { NotificationCountService } from './services/count';
|
||||
import { NotificationListService } from './services/list';
|
||||
@@ -17,7 +22,7 @@ export function configureNotificationModule(framework: Framework) {
|
||||
framework
|
||||
.scope(ServerScope)
|
||||
.service(NotificationService, [NotificationStore])
|
||||
.service(NotificationCountService, [NotificationStore])
|
||||
.service(NotificationCountService, [NotificationStore, AuthService])
|
||||
.service(NotificationListService, [
|
||||
NotificationStore,
|
||||
NotificationCountService,
|
||||
|
||||
@@ -12,17 +12,24 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, mergeMap, switchMap, timer } from 'rxjs';
|
||||
|
||||
import { AccountChanged, type AuthService } from '../../cloud';
|
||||
import { ServerStarted } from '../../cloud/events/server-started';
|
||||
import { ApplicationFocused } from '../../lifecycle';
|
||||
import type { NotificationStore } from '../stores/notification';
|
||||
|
||||
@OnEvent(ApplicationFocused, s => s.handleApplicationFocused)
|
||||
@OnEvent(ServerStarted, s => s.handleServerStarted)
|
||||
@OnEvent(AccountChanged, s => s.handleAccountChanged)
|
||||
export class NotificationCountService extends Service {
|
||||
constructor(private readonly store: NotificationStore) {
|
||||
constructor(
|
||||
private readonly store: NotificationStore,
|
||||
private readonly authService: AuthService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
loggedIn$ = this.authService.session.status$.map(v => v === 'authenticated');
|
||||
|
||||
readonly count$ = LiveData.from(this.store.watchNotificationCountCache(), 0);
|
||||
readonly isLoading$ = new LiveData(false);
|
||||
readonly error$ = new LiveData<any>(null);
|
||||
@@ -32,9 +39,12 @@ export class NotificationCountService extends Service {
|
||||
return timer(0, 30000); // revalidate every 30 seconds
|
||||
}),
|
||||
exhaustMapWithTrailing(() => {
|
||||
return fromPromise(signal =>
|
||||
this.store.getNotificationCount(signal)
|
||||
).pipe(
|
||||
return fromPromise(signal => {
|
||||
if (!this.loggedIn$.value) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
return this.store.getNotificationCount(signal);
|
||||
}).pipe(
|
||||
mergeMap(result => {
|
||||
this.setCount(result ?? 0);
|
||||
return EMPTY;
|
||||
@@ -57,6 +67,10 @@ export class NotificationCountService extends Service {
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
handleAccountChanged() {
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
setCount(count: number) {
|
||||
this.store.setNotificationCountCache(count);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export class PDFRenderer extends OpClient<ClientOps> {
|
||||
private readonly worker: Worker;
|
||||
|
||||
constructor() {
|
||||
const worker = new Worker(getWorkerUrl('pdf.worker.js'));
|
||||
const worker = new Worker(getWorkerUrl('pdf'));
|
||||
super(worker);
|
||||
|
||||
this.worker = worker;
|
||||
|
||||
@@ -10,7 +10,7 @@ export function getWorkspaceProfileWorker() {
|
||||
return worker;
|
||||
}
|
||||
|
||||
const rawWorker = new Worker(getWorkerUrl('workspace-profile.worker.js'));
|
||||
const rawWorker = new Worker(getWorkerUrl('workspace-profile'));
|
||||
|
||||
worker = new OpClient<WorkerOps>(rawWorker);
|
||||
return worker;
|
||||
|
||||
@@ -177,6 +177,10 @@ type MeetingEvents =
|
||||
| 'activeMenubarAppItem';
|
||||
// END SECTION
|
||||
|
||||
// SECTION: mention
|
||||
type MentionEvents = 'mentionMember' | 'noAccessPrompted';
|
||||
// END SECTION
|
||||
|
||||
type UserEvents =
|
||||
| GeneralEvents
|
||||
| AppEvents
|
||||
@@ -198,7 +202,9 @@ type UserEvents =
|
||||
| TemplateEvents
|
||||
| NotificationEvents
|
||||
| IntegrationEvents
|
||||
| MeetingEvents;
|
||||
| MeetingEvents
|
||||
| MentionEvents;
|
||||
|
||||
interface PageDivision {
|
||||
[page: string]: {
|
||||
[segment: string]: {
|
||||
@@ -405,7 +411,13 @@ const PageEvents = {
|
||||
doc: {
|
||||
editor: {
|
||||
slashMenu: ['linkDoc', 'createDoc', 'bookmark'],
|
||||
atMenu: ['linkDoc', 'import', 'createDoc'],
|
||||
atMenu: [
|
||||
'linkDoc',
|
||||
'import',
|
||||
'createDoc',
|
||||
'mentionMember',
|
||||
'noAccessPrompted',
|
||||
],
|
||||
quickSearch: ['createDoc'],
|
||||
formatToolbar: ['bold'],
|
||||
pageRef: ['navigate'],
|
||||
@@ -698,6 +710,10 @@ export type EventArgs = {
|
||||
| 'Meeting Settings'
|
||||
| 'Quit AFFiNE Completely';
|
||||
};
|
||||
mentionMember: {
|
||||
type: 'member' | 'invite' | 'more';
|
||||
};
|
||||
noAccessPrompted: {};
|
||||
};
|
||||
|
||||
// for type checking
|
||||
|
||||
@@ -28,7 +28,7 @@ export class EditorUtils {
|
||||
const lines = await page.$$('page-editor .inline-editor');
|
||||
const contents = await Promise.all(lines.map(el => el.innerText()));
|
||||
content = contents
|
||||
.map(c => c.replace(/\u200B/g, '').trim())
|
||||
.map(c => c.replace(/[\u200B-\u200D\uFEFF]/g, '').trim())
|
||||
.filter(c => !!c)
|
||||
.join('\n');
|
||||
if (!content) {
|
||||
@@ -43,7 +43,9 @@ export class EditorUtils {
|
||||
const edgelessNode = await page.waitForSelector(
|
||||
'affine-edgeless-note .edgeless-note-page-content'
|
||||
);
|
||||
return (await edgelessNode.innerText()).replace(/\u200B/g, '').trim();
|
||||
return (await edgelessNode.innerText())
|
||||
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
public static async switchToEdgelessMode(page: Page) {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
|
||||
import {
|
||||
addCodeBlock,
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
type,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
@@ -178,9 +177,6 @@ test.describe('paste in multiple blocks text selection', () => {
|
||||
test('paste surface-ref block to another doc as embed-linked-doc block', async ({
|
||||
page,
|
||||
}) => {
|
||||
await openHomePage(page);
|
||||
await clickNewPageButton(page, 'Clipboard Test');
|
||||
await waitForEditorLoad(page);
|
||||
await clickEdgelessModeButton(page);
|
||||
const container = locateEditorContainer(page);
|
||||
await container.click();
|
||||
@@ -205,21 +201,18 @@ test('paste surface-ref block to another doc as embed-linked-doc block', async (
|
||||
await insertIntoPageButton.click();
|
||||
|
||||
await clickPageModeButton(page);
|
||||
await page.waitForTimeout(50);
|
||||
await waitForEditorLoad(page);
|
||||
await container.click();
|
||||
|
||||
// copy surface-ref block
|
||||
const surfaceRefBlock = page.locator('.affine-surface-ref');
|
||||
const surfaceRefBlock = page.locator('affine-surface-ref');
|
||||
await surfaceRefBlock.click();
|
||||
await page.waitForTimeout(50);
|
||||
await page.waitForSelector('affine-surface-ref .focused');
|
||||
await copyByKeyboard(page);
|
||||
|
||||
// paste to another doc
|
||||
await clickNewPageButton(page);
|
||||
await waitForEditorLoad(page);
|
||||
const title2 = getBlockSuiteEditorTitle(page);
|
||||
await title2.pressSequentially('page2');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(50);
|
||||
await clickNewPageButton(page, 'page2');
|
||||
await pressEnter(page);
|
||||
|
||||
// paste the surface-ref block
|
||||
await pasteByKeyboard(page);
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { clickView } from '../utils/actions/click.js';
|
||||
import {
|
||||
addBasicRectShapeElement,
|
||||
getSelectedBoundCount,
|
||||
locatorComponentToolbar,
|
||||
resizeElementByHandle,
|
||||
selectNoteInEdgeless,
|
||||
switchEditorMode,
|
||||
zoomResetByKeyboard,
|
||||
} from '../utils/actions/edgeless.js';
|
||||
import {
|
||||
pressBackspace,
|
||||
selectAllBlocksByKeyboard,
|
||||
} from '../utils/actions/keyboard.js';
|
||||
import {
|
||||
enterPlaygroundRoom,
|
||||
initEmptyEdgelessState,
|
||||
@@ -87,3 +93,26 @@ test('should be hidden when resizing element', async ({ page }) => {
|
||||
|
||||
await expect(toolbar).toBeVisible();
|
||||
});
|
||||
|
||||
test('should only one tool active at the same time when using shortcut to switch tool', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await clickView(page, [0, 0]);
|
||||
await selectAllBlocksByKeyboard(page);
|
||||
await pressBackspace(page);
|
||||
|
||||
await page.keyboard.press('s');
|
||||
await page.keyboard.press('m');
|
||||
await page.keyboard.press('n');
|
||||
|
||||
await clickView(page, [100, 100]);
|
||||
await clickView(page, [0, 0]); // click on empty space to deselect the note
|
||||
await selectAllBlocksByKeyboard(page);
|
||||
expect(
|
||||
await getSelectedBoundCount(page),
|
||||
'only a note should be created'
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
changeShapeStrokeStyle,
|
||||
changeShapeStrokeWidth,
|
||||
clickComponentToolbarMoreMenuButton,
|
||||
dragBetweenViewCoords,
|
||||
getEdgelessSelectedRect,
|
||||
getSelectedBoundCount,
|
||||
locatorComponentToolbar,
|
||||
locatorEdgelessToolButton,
|
||||
locatorShapeStrokeStyleButton,
|
||||
@@ -24,13 +26,17 @@ import {
|
||||
import {
|
||||
addBasicBrushElement,
|
||||
addBasicRectShapeElement,
|
||||
clickView,
|
||||
copyByKeyboard,
|
||||
dblclickView,
|
||||
dragBetweenCoords,
|
||||
enterPlaygroundRoom,
|
||||
focusRichText,
|
||||
initEmptyEdgelessState,
|
||||
pasteByKeyboard,
|
||||
pressBackspace,
|
||||
pressEscape,
|
||||
selectAllBlocksByKeyboard,
|
||||
type,
|
||||
waitNextFrame,
|
||||
} from '../utils/actions/index.js';
|
||||
@@ -40,6 +46,7 @@ import {
|
||||
assertEdgelessNonSelectedRect,
|
||||
assertEdgelessSelectedRect,
|
||||
assertRichTexts,
|
||||
assertSelectedBound,
|
||||
} from '../utils/asserts.js';
|
||||
import { test } from '../utils/playwright.js';
|
||||
|
||||
@@ -739,3 +746,45 @@ test.describe('shape hit test', () => {
|
||||
await assertEdgelessCanvasText(page, 'hello world');
|
||||
});
|
||||
});
|
||||
|
||||
test('should create a shape when press s and click on canvas', async ({
|
||||
page,
|
||||
}) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await clickView(page, [0, 0]);
|
||||
await zoomResetByKeyboard(page);
|
||||
await selectAllBlocksByKeyboard(page);
|
||||
await pressBackspace(page);
|
||||
|
||||
await page.keyboard.press('s');
|
||||
await assertEdgelessTool(page, 'shape');
|
||||
await clickView(page, [100, 100]);
|
||||
await selectAllBlocksByKeyboard(page);
|
||||
expect(await getSelectedBoundCount(page)).toBe(1);
|
||||
await assertSelectedBound(page, [100, 100, 100, 100]);
|
||||
});
|
||||
|
||||
test('shape should be editable when re-enter canvas', async ({ page }) => {
|
||||
await enterPlaygroundRoom(page);
|
||||
await initEmptyEdgelessState(page);
|
||||
await switchEditorMode(page);
|
||||
await clickView(page, [0, 0]);
|
||||
await zoomResetByKeyboard(page);
|
||||
await selectAllBlocksByKeyboard(page);
|
||||
await pressBackspace(page);
|
||||
|
||||
await page.keyboard.press('s');
|
||||
await dragBetweenViewCoords(page, [0, 0], [100, 100]);
|
||||
await dblclickView(page, [50, 50]);
|
||||
await type(page, 'hello');
|
||||
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
|
||||
await assertEdgelessCanvasText(page, 'hello');
|
||||
|
||||
await switchEditorMode(page);
|
||||
await switchEditorMode(page);
|
||||
|
||||
await dblclickView(page, [50, 50]);
|
||||
await expect(page.locator('edgeless-shape-text-editor')).toBeAttached();
|
||||
});
|
||||
|
||||
@@ -405,10 +405,7 @@ export function createWorkerTargetConfig(
|
||||
pkg: Package,
|
||||
entry: string
|
||||
): Omit<webpack.Configuration, 'name'> & { name: string } {
|
||||
const workerName = path.basename(entry).replace(/\.([^.]+)$/, '');
|
||||
if (!workerName.endsWith('.worker')) {
|
||||
throw new Error('Worker name must end with `.worker.[ext]`');
|
||||
}
|
||||
const workerName = path.basename(entry).replace(/\.worker\.ts$/, '');
|
||||
const buildConfig = getBuildConfigFromEnv(pkg);
|
||||
|
||||
return {
|
||||
@@ -423,7 +420,7 @@ export function createWorkerTargetConfig(
|
||||
[workerName]: entry,
|
||||
},
|
||||
output: {
|
||||
filename: 'js/[name].js',
|
||||
filename: `js/${workerName}-${buildConfig.appVersion}.worker.js`,
|
||||
path: pkg.distPath.value,
|
||||
clean: false,
|
||||
globalObject: 'globalThis',
|
||||
|
||||
@@ -270,6 +270,7 @@ export const PackageList = [
|
||||
'blocksuite/affine/blocks/surface',
|
||||
'blocksuite/affine/components',
|
||||
'blocksuite/affine/fragments/doc-title',
|
||||
'blocksuite/affine/gfx/turbo-renderer',
|
||||
'blocksuite/affine/inlines/preset',
|
||||
'blocksuite/affine/model',
|
||||
'blocksuite/affine/rich-text',
|
||||
|
||||
Reference in New Issue
Block a user