mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor(editor): use model coord system in worker renderer (#9969)
This commit is contained in:
@@ -1,94 +0,0 @@
|
||||
import { type AffineEditorContainer } from '@blocksuite/presets';
|
||||
|
||||
import { CanvasRenderer } from './canvas-renderer.js';
|
||||
import { editor } from './editor.js';
|
||||
import type { SectionLayout } from './types.js';
|
||||
|
||||
async function wait(time: number = 100) {
|
||||
return new Promise(resolve => setTimeout(resolve, time));
|
||||
}
|
||||
|
||||
export class SwitchModeAnimator {
|
||||
constructor(private readonly editor: AffineEditorContainer) {
|
||||
this.renderer = new CanvasRenderer(this.editor, this.overlay);
|
||||
}
|
||||
|
||||
renderer: CanvasRenderer;
|
||||
|
||||
private readonly overlay = document.createElement('div');
|
||||
|
||||
get editorRect() {
|
||||
return this.editor.getBoundingClientRect();
|
||||
}
|
||||
|
||||
async switchMode() {
|
||||
this.initOverlay();
|
||||
const beginLayout = this.renderer.hostLayout;
|
||||
|
||||
await this.renderer.render(false);
|
||||
document.body.append(this.overlay);
|
||||
this.editor.mode = this.editor.mode === 'page' ? 'edgeless' : 'page';
|
||||
await wait();
|
||||
|
||||
const endLayout = this.renderer.hostLayout;
|
||||
|
||||
this.overlay.style.display = 'inherit';
|
||||
await this.animate(
|
||||
beginLayout.section,
|
||||
endLayout.section,
|
||||
beginLayout.hostRect,
|
||||
endLayout.hostRect
|
||||
);
|
||||
this.overlay.style.display = 'none';
|
||||
}
|
||||
|
||||
async animate(
|
||||
beginSection: SectionLayout,
|
||||
endSection: SectionLayout,
|
||||
beginHostRect: DOMRect,
|
||||
endHostRect: DOMRect
|
||||
): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
const duration = 600;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = () => {
|
||||
const currentTime = performance.now();
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
this.renderer.renderTransitionFrame(
|
||||
beginSection,
|
||||
endSection,
|
||||
beginHostRect,
|
||||
endHostRect,
|
||||
progress
|
||||
);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
}
|
||||
|
||||
initOverlay() {
|
||||
const { left, top, width, height } = this.editorRect;
|
||||
this.overlay.style.position = 'fixed';
|
||||
this.overlay.style.left = left + 'px';
|
||||
this.overlay.style.top = top + 'px';
|
||||
this.overlay.style.width = width + 'px';
|
||||
this.overlay.style.height = height + 'px';
|
||||
this.overlay.style.backgroundColor = 'white';
|
||||
this.overlay.style.pointerEvents = 'none';
|
||||
this.overlay.style.zIndex = '9999';
|
||||
this.overlay.style.display = 'flex';
|
||||
this.overlay.style.alignItems = 'flex-end';
|
||||
}
|
||||
}
|
||||
|
||||
export const animator = new SwitchModeAnimator(editor);
|
||||
@@ -2,7 +2,11 @@ import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
|
||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||
|
||||
import { getSentenceRects, segmentSentences } from './text-utils.js';
|
||||
import { type ParagraphLayout, type SectionLayout } from './types.js';
|
||||
import {
|
||||
type ParagraphLayout,
|
||||
type SectionLayout,
|
||||
type ViewportState,
|
||||
} from './types.js';
|
||||
|
||||
export class CanvasRenderer {
|
||||
private readonly worker: Worker;
|
||||
@@ -24,29 +28,40 @@ export class CanvasRenderer {
|
||||
|
||||
private initWorkerSize(width: number, height: number) {
|
||||
const dpr = window.devicePixelRatio;
|
||||
this.worker.postMessage({ type: 'init', data: { width, height, dpr } });
|
||||
const viewport = this.editorContainer.std.get(
|
||||
GfxControllerIdentifier
|
||||
).viewport;
|
||||
const viewportState: ViewportState = {
|
||||
zoom: viewport.zoom,
|
||||
viewScale: viewport.viewScale,
|
||||
viewportX: viewport.viewportX,
|
||||
viewportY: viewport.viewportY,
|
||||
};
|
||||
this.worker.postMessage({
|
||||
type: 'init',
|
||||
data: { width, height, dpr, viewport: viewportState },
|
||||
});
|
||||
}
|
||||
|
||||
get viewport() {
|
||||
return this.editorContainer.std.get(GfxControllerIdentifier).viewport;
|
||||
}
|
||||
|
||||
get hostRect() {
|
||||
return this.editorContainer.host!.getBoundingClientRect();
|
||||
}
|
||||
|
||||
get hostZoom() {
|
||||
return this.editorContainer.std.get(GfxControllerIdentifier).viewport.zoom;
|
||||
}
|
||||
|
||||
get hostLayout(): {
|
||||
section: SectionLayout;
|
||||
hostRect: DOMRect;
|
||||
editorContainerRect: DOMRect;
|
||||
} {
|
||||
const paragraphBlocks = this.editorContainer.host!.querySelectorAll(
|
||||
'.affine-paragraph-rich-text-wrapper [data-v-text="true"]'
|
||||
);
|
||||
|
||||
const zoom = this.hostZoom;
|
||||
const { viewport } = this;
|
||||
const zoom = this.viewport.zoom;
|
||||
const hostRect = this.hostRect;
|
||||
const editorContainerRect = this.editorContainer.getBoundingClientRect();
|
||||
|
||||
let sectionMinX = Infinity;
|
||||
let sectionMinY = Infinity;
|
||||
@@ -60,37 +75,55 @@ export class CanvasRenderer {
|
||||
rects.forEach(({ rect }) => {
|
||||
sectionMinX = Math.min(sectionMinX, rect.x);
|
||||
sectionMinY = Math.min(sectionMinY, rect.y);
|
||||
sectionMaxX = Math.max(sectionMaxX, rect.x + rect.width);
|
||||
sectionMaxY = Math.max(sectionMaxY, rect.y + rect.height);
|
||||
sectionMaxX = Math.max(sectionMaxX, rect.x + rect.w);
|
||||
sectionMaxY = Math.max(sectionMaxY, rect.y + rect.h);
|
||||
});
|
||||
return {
|
||||
text: sentence,
|
||||
rects,
|
||||
rects: rects.map(rect => {
|
||||
const [x, y] = viewport.toModelCoordFromClientCoord([
|
||||
rect.rect.x,
|
||||
rect.rect.y,
|
||||
]);
|
||||
return {
|
||||
...rect,
|
||||
rect: {
|
||||
x,
|
||||
y,
|
||||
w: rect.rect.w / zoom / viewport.viewScale,
|
||||
h: rect.rect.h / zoom / viewport.viewScale,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
sentences: sentenceLayouts,
|
||||
scale: zoom,
|
||||
zoom,
|
||||
};
|
||||
});
|
||||
|
||||
const sectionModelCoord = viewport.toModelCoordFromClientCoord([
|
||||
sectionMinX,
|
||||
sectionMinY,
|
||||
]);
|
||||
const section: SectionLayout = {
|
||||
paragraphs,
|
||||
rect: {
|
||||
x: sectionMinX,
|
||||
y: sectionMinY,
|
||||
width: sectionMaxX - sectionMinX,
|
||||
height: sectionMaxY - sectionMinY,
|
||||
x: sectionModelCoord[0],
|
||||
y: sectionModelCoord[1],
|
||||
w: (sectionMaxX - sectionMinX) / zoom / viewport.viewScale,
|
||||
h: (sectionMaxY - sectionMinY) / zoom / viewport.viewScale,
|
||||
},
|
||||
};
|
||||
|
||||
return { section, hostRect, editorContainerRect };
|
||||
return { section, hostRect };
|
||||
}
|
||||
|
||||
public async render(toScreen = true): Promise<void> {
|
||||
const { section, editorContainerRect } = this.hostLayout;
|
||||
this.initWorkerSize(section.rect.width, section.rect.height);
|
||||
const { section } = this.hostLayout;
|
||||
this.initWorkerSize(section.rect.w, section.rect.h);
|
||||
|
||||
return new Promise(resolve => {
|
||||
if (!this.worker) return;
|
||||
@@ -105,12 +138,10 @@ export class CanvasRenderer {
|
||||
this.worker.onmessage = (e: MessageEvent) => {
|
||||
const { type, bitmap } = e.data;
|
||||
if (type === 'render') {
|
||||
this.canvas.style.width = editorContainerRect.width + 'px';
|
||||
this.canvas.style.height = editorContainerRect.height + 'px';
|
||||
this.canvas.width =
|
||||
editorContainerRect.width * window.devicePixelRatio;
|
||||
this.canvas.height =
|
||||
editorContainerRect.height * window.devicePixelRatio;
|
||||
this.canvas.style.width = this.hostRect.width + 'px';
|
||||
this.canvas.style.height = this.hostRect.height + 'px';
|
||||
this.canvas.width = this.hostRect.width * window.devicePixelRatio;
|
||||
this.canvas.height = this.hostRect.height * window.devicePixelRatio;
|
||||
|
||||
if (!this.targetContainer.querySelector('canvas')) {
|
||||
this.targetContainer.append(this.canvas);
|
||||
@@ -118,8 +149,8 @@ export class CanvasRenderer {
|
||||
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const bitmapCanvas = new OffscreenCanvas(
|
||||
section.rect.width * window.devicePixelRatio,
|
||||
section.rect.height * window.devicePixelRatio
|
||||
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
||||
section.rect.h * window.devicePixelRatio * this.viewport.zoom
|
||||
);
|
||||
const bitmapCtx = bitmapCanvas.getContext('bitmaprenderer');
|
||||
bitmapCtx?.transferFromImageBitmap(bitmap);
|
||||
@@ -129,12 +160,17 @@ export class CanvasRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
const sectionViewCoord = this.viewport.toViewCoord(
|
||||
section.rect.x,
|
||||
section.rect.y
|
||||
);
|
||||
|
||||
ctx?.drawImage(
|
||||
bitmapCanvas,
|
||||
(section.rect.x - editorContainerRect.x) * window.devicePixelRatio,
|
||||
(section.rect.y - editorContainerRect.y) * window.devicePixelRatio,
|
||||
section.rect.width * window.devicePixelRatio,
|
||||
section.rect.height * window.devicePixelRatio
|
||||
sectionViewCoord[0] * window.devicePixelRatio,
|
||||
sectionViewCoord[1] * window.devicePixelRatio,
|
||||
section.rect.w * window.devicePixelRatio * this.viewport.zoom,
|
||||
section.rect.h * window.devicePixelRatio * this.viewport.zoom
|
||||
);
|
||||
|
||||
resolve();
|
||||
@@ -143,103 +179,6 @@ export class CanvasRenderer {
|
||||
});
|
||||
}
|
||||
|
||||
public renderTransitionFrame(
|
||||
beginSection: SectionLayout,
|
||||
endSection: SectionLayout,
|
||||
beginHostRect: DOMRect,
|
||||
endHostRect: DOMRect,
|
||||
progress: number
|
||||
) {
|
||||
const editorContainerRect = this.editorContainer.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio;
|
||||
|
||||
if (!this.targetContainer.querySelector('canvas')) {
|
||||
this.targetContainer.append(this.canvas);
|
||||
}
|
||||
|
||||
const ctx = this.canvas.getContext('2d')!;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, this.canvas.width / dpr, this.canvas.height / dpr);
|
||||
|
||||
const getParagraphRect = (paragraph: ParagraphLayout): DOMRect => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
paragraph.sentences.forEach(sentence => {
|
||||
sentence.rects.forEach(({ rect }) => {
|
||||
minX = Math.min(minX, rect.x);
|
||||
minY = Math.min(minY, rect.y);
|
||||
maxX = Math.max(maxX, rect.x + rect.width);
|
||||
maxY = Math.max(maxY, rect.y + rect.height);
|
||||
});
|
||||
});
|
||||
|
||||
return new DOMRect(minX, minY, maxX - minX, maxY - minY);
|
||||
};
|
||||
|
||||
// Helper function to interpolate between two rects
|
||||
const interpolateRect = (
|
||||
rect1: DOMRect,
|
||||
rect2: DOMRect,
|
||||
t: number
|
||||
): DOMRect => {
|
||||
return new DOMRect(
|
||||
rect1.x + (rect2.x - rect1.x) * t,
|
||||
rect1.y + (rect2.y - rect1.y) * t,
|
||||
rect1.width + (rect2.width - rect1.width) * t,
|
||||
rect1.height + (rect2.height - rect1.height) * t
|
||||
);
|
||||
};
|
||||
|
||||
// Draw host rect
|
||||
const currentHostRect = interpolateRect(
|
||||
beginHostRect,
|
||||
endHostRect,
|
||||
progress
|
||||
);
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(
|
||||
currentHostRect.x - editorContainerRect.x,
|
||||
currentHostRect.y - editorContainerRect.y,
|
||||
currentHostRect.width,
|
||||
currentHostRect.height
|
||||
);
|
||||
|
||||
// Draw paragraph rects
|
||||
const maxParagraphs = Math.max(
|
||||
beginSection.paragraphs.length,
|
||||
endSection.paragraphs.length
|
||||
);
|
||||
|
||||
for (let i = 0; i < maxParagraphs; i++) {
|
||||
const beginRect =
|
||||
i < beginSection.paragraphs.length
|
||||
? getParagraphRect(beginSection.paragraphs[i])
|
||||
: getParagraphRect(
|
||||
endSection.paragraphs[endSection.paragraphs.length - 1]
|
||||
);
|
||||
const endRect =
|
||||
i < endSection.paragraphs.length
|
||||
? getParagraphRect(endSection.paragraphs[i])
|
||||
: getParagraphRect(
|
||||
beginSection.paragraphs[beginSection.paragraphs.length - 1]
|
||||
);
|
||||
|
||||
const currentRect = interpolateRect(beginRect, endRect, progress);
|
||||
ctx.fillStyle = '#efefef';
|
||||
ctx.fillRect(
|
||||
currentRect.x - editorContainerRect.x,
|
||||
currentRect.y - editorContainerRect.y,
|
||||
currentRect.width,
|
||||
currentRect.height
|
||||
);
|
||||
}
|
||||
|
||||
ctx.scale(1 / dpr, 1 / dpr);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
import { type SectionLayout } from './types.js';
|
||||
import { type SectionLayout, type ViewportState } from './types.js';
|
||||
|
||||
type WorkerMessageInit = {
|
||||
type: 'init';
|
||||
data: {
|
||||
width: number;
|
||||
height: number;
|
||||
dpr: number;
|
||||
viewport: ViewportState;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkerMessageDraw = {
|
||||
type: 'draw';
|
||||
data: {
|
||||
section: SectionLayout;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkerMessage = WorkerMessageInit | WorkerMessageDraw;
|
||||
|
||||
const meta = {
|
||||
emSize: 2048,
|
||||
@@ -29,39 +48,50 @@ function getBaseline() {
|
||||
class CanvasWorkerManager {
|
||||
private canvas: OffscreenCanvas | null = null;
|
||||
private ctx: OffscreenCanvasRenderingContext2D | null = null;
|
||||
private viewport: ViewportState | null = null;
|
||||
|
||||
init(width: number, height: number, dpr: number) {
|
||||
this.canvas = new OffscreenCanvas(width * dpr, height * dpr);
|
||||
init(
|
||||
modelWidth: number,
|
||||
modelHeight: number,
|
||||
dpr: number,
|
||||
viewport: ViewportState
|
||||
) {
|
||||
const width = modelWidth * dpr * viewport.zoom;
|
||||
const height = modelHeight * dpr * viewport.zoom;
|
||||
this.canvas = new OffscreenCanvas(width, height);
|
||||
this.ctx = this.canvas.getContext('2d')!;
|
||||
this.ctx.scale(dpr, dpr);
|
||||
this.ctx.fillStyle = 'lightgrey';
|
||||
this.ctx.fillRect(0, 0, width, height);
|
||||
this.viewport = viewport;
|
||||
}
|
||||
|
||||
draw(section: SectionLayout) {
|
||||
const { canvas, ctx } = this;
|
||||
if (!canvas || !ctx) return;
|
||||
|
||||
const zoom = this.viewport!.zoom;
|
||||
ctx.scale(zoom, zoom);
|
||||
|
||||
// Track rendered positions to avoid duplicate rendering across all paragraphs and sentences
|
||||
const renderedPositions = new Set<string>();
|
||||
|
||||
section.paragraphs.forEach(paragraph => {
|
||||
const scale = paragraph.scale ?? 1;
|
||||
const fontSize = 15 * scale;
|
||||
ctx.font = `${fontSize}px Inter`;
|
||||
const baselineY = getBaseline() * scale;
|
||||
const fontSize = 15;
|
||||
ctx.font = `300 ${fontSize}px Inter`;
|
||||
const baselineY = getBaseline();
|
||||
|
||||
paragraph.sentences.forEach(sentence => {
|
||||
ctx.strokeStyle = 'yellow';
|
||||
sentence.rects.forEach(textRect => {
|
||||
const x = textRect.rect.left - section.rect.x;
|
||||
const y = textRect.rect.top - section.rect.y;
|
||||
const x = textRect.rect.x - section.rect.x;
|
||||
const y = textRect.rect.y - section.rect.y;
|
||||
|
||||
const posKey = `${x},${y}`;
|
||||
// Only render if we haven't rendered at this position before
|
||||
if (renderedPositions.has(posKey)) return;
|
||||
|
||||
ctx.strokeRect(x, y, textRect.rect.width, textRect.rect.height);
|
||||
ctx.strokeRect(x, y, textRect.rect.w, textRect.rect.h);
|
||||
ctx.fillStyle = 'black';
|
||||
ctx.fillText(textRect.text, x, y + baselineY);
|
||||
|
||||
@@ -77,12 +107,12 @@ class CanvasWorkerManager {
|
||||
|
||||
const manager = new CanvasWorkerManager();
|
||||
|
||||
self.onmessage = async (e: MessageEvent) => {
|
||||
self.onmessage = async (e: MessageEvent<WorkerMessage>) => {
|
||||
const { type, data } = e.data;
|
||||
switch (type) {
|
||||
case 'init': {
|
||||
const { width, height, dpr } = data;
|
||||
manager.init(width, height, dpr);
|
||||
const { width, height, dpr, viewport } = data;
|
||||
manager.init(width, height, dpr, viewport);
|
||||
break;
|
||||
}
|
||||
case 'draw': {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Text } from '@blocksuite/store';
|
||||
|
||||
import { animator } from './animator.js';
|
||||
import { CanvasRenderer } from './canvas-renderer.js';
|
||||
import { doc, editor } from './editor.js';
|
||||
|
||||
@@ -14,7 +13,7 @@ function initUI() {
|
||||
});
|
||||
const switchModeButton = document.querySelector('#switch-mode-button')!;
|
||||
switchModeButton.addEventListener('click', async () => {
|
||||
await animator.switchMode();
|
||||
editor.mode = editor.mode === 'page' ? 'edgeless' : 'page';
|
||||
});
|
||||
document.querySelector('#left-column')?.append(editor);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,15 @@ interface WordSegment {
|
||||
end: number;
|
||||
}
|
||||
|
||||
const CJK_REGEX = /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/u;
|
||||
|
||||
function hasCJK(text: string): boolean {
|
||||
return CJK_REGEX.test(text);
|
||||
}
|
||||
|
||||
function getWordSegments(text: string): WordSegment[] {
|
||||
const segmenter = new Intl.Segmenter(undefined, { granularity: 'word' });
|
||||
const granularity = hasCJK(text) ? 'grapheme' : 'word';
|
||||
const segmenter = new Intl.Segmenter(undefined, { granularity });
|
||||
return Array.from(segmenter.segment(text)).map(({ segment, index }) => ({
|
||||
text: segment,
|
||||
start: index,
|
||||
@@ -23,8 +30,14 @@ function getRangeRects(range: Range, fullText: string): TextRect[] {
|
||||
|
||||
// If there's only one rect, use the full text
|
||||
if (rects.length === 1) {
|
||||
const rect = rects[0];
|
||||
textRects.push({
|
||||
rect: rects[0],
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
text: fullText,
|
||||
});
|
||||
return textRects;
|
||||
@@ -33,7 +46,9 @@ function getRangeRects(range: Range, fullText: string): TextRect[] {
|
||||
const segments = getWordSegments(fullText);
|
||||
|
||||
// Calculate the total width and average width per character
|
||||
const totalWidth = rects.reduce((sum, rect) => sum + rect.width, 0);
|
||||
const totalWidth = Math.floor(
|
||||
rects.reduce((sum, rect) => sum + rect.width, 0)
|
||||
);
|
||||
const charWidthEstimate = totalWidth / fullText.length;
|
||||
|
||||
let currentRect = 0;
|
||||
@@ -42,26 +57,18 @@ function getRangeRects(range: Range, fullText: string): TextRect[] {
|
||||
|
||||
segments.forEach(segment => {
|
||||
const segmentWidth = segment.text.length * charWidthEstimate;
|
||||
const isPunctuation = /^[.,!?;:]$/.test(segment.text.trim());
|
||||
|
||||
// Handle punctuation: if the punctuation doesn't exceed the rect width, merge it with the previous segment
|
||||
if (isPunctuation && currentSegments.length > 0) {
|
||||
const withPunctuationWidth = currentWidth + segmentWidth;
|
||||
// Allow slight overflow (120%) since punctuation is usually very narrow
|
||||
if (withPunctuationWidth <= rects[currentRect]?.width * 1.2) {
|
||||
currentSegments.push(segment);
|
||||
currentWidth = withPunctuationWidth;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
currentWidth + segmentWidth > rects[currentRect]?.width &&
|
||||
currentSegments.length > 0 &&
|
||||
!isPunctuation // If it's punctuation, try merging with the previous word first
|
||||
currentSegments.length > 0
|
||||
) {
|
||||
const rect = rects[currentRect];
|
||||
textRects.push({
|
||||
rect: rects[currentRect],
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
text: currentSegments.map(seg => seg.text).join(''),
|
||||
});
|
||||
|
||||
@@ -75,9 +82,15 @@ function getRangeRects(range: Range, fullText: string): TextRect[] {
|
||||
});
|
||||
|
||||
// Handle remaining segments if any
|
||||
if (currentSegments.length > 0 && currentRect < rects.length) {
|
||||
if (currentSegments.length > 0) {
|
||||
const rect = rects[Math.min(currentRect, rects.length - 1)];
|
||||
textRects.push({
|
||||
rect: rects[currentRect],
|
||||
rect: {
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
w: rect.width,
|
||||
h: rect.height,
|
||||
},
|
||||
text: currentSegments.map(seg => seg.text).join(''),
|
||||
});
|
||||
}
|
||||
@@ -99,14 +112,18 @@ export function getSentenceRects(
|
||||
let rects: TextRect[] = [];
|
||||
let startIndex = 0;
|
||||
|
||||
// Find all occurrences of the sentence
|
||||
// Find all occurrences of the sentence and ensure we capture complete words
|
||||
while ((startIndex = text.indexOf(sentence, startIndex)) !== -1) {
|
||||
const range = document.createRange();
|
||||
range.setStart(textNode, startIndex);
|
||||
range.setEnd(textNode, startIndex + sentence.length);
|
||||
let endIndex = startIndex + sentence.length;
|
||||
|
||||
rects = rects.concat(getRangeRects(range, sentence));
|
||||
startIndex += sentence.length; // Move to next potential occurrence
|
||||
range.setStart(textNode, startIndex);
|
||||
range.setEnd(textNode, endIndex);
|
||||
|
||||
rects = rects.concat(
|
||||
getRangeRects(range, text.slice(startIndex, endIndex))
|
||||
);
|
||||
startIndex = endIndex;
|
||||
}
|
||||
|
||||
return rects;
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
// We can't use viewport instance here because it can't be reused in worker
|
||||
export interface ViewportState {
|
||||
zoom: number;
|
||||
viewScale: number;
|
||||
viewportX: number;
|
||||
viewportY: number;
|
||||
}
|
||||
|
||||
export interface SentenceLayout {
|
||||
text: string;
|
||||
rects: TextRect[];
|
||||
@@ -5,20 +20,15 @@ export interface SentenceLayout {
|
||||
|
||||
export interface ParagraphLayout {
|
||||
sentences: SentenceLayout[];
|
||||
scale: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export interface TextRect {
|
||||
rect: DOMRect;
|
||||
rect: Rect;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SectionLayout {
|
||||
paragraphs: ParagraphLayout[];
|
||||
rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
rect: Rect;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user