Repository: toeverything/AFFiNE
File: blocksuite/affine/shared/src/viewport-renderer/renderer-utils.ts (TypeScript)

Code before:
export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
  const hostRect = host.getBoundingClientRect();
  const dpr = window.devicePixelRatio;
  canvas.style.position = 'absolute';
  canvas.style.left = '0px';
  canvas.style.top = '0px';
  canvas.style.width = '100%';
  canvas.style.height = '100%';
  canvas.width = hostRect.width * dpr;
  canvas.height = hostRect.height * dpr;
  canvas.style.pointerEvents = 'none';
}

function clientToModelCoord(
  viewport: ViewportRecord,
  clientCoord: [number, number]
) {
  const { left, top, viewportX, viewportY, zoom, viewScale } = viewport;

  const [clientX, clientY] = clientCoord;
  const viewportInternalX = clientX - left;
  const viewportInternalY = clientY - top;
  const modelX = viewportX + viewportInternalX / zoom / viewScale;
  const modelY = viewportY + viewportInternalY / zoom / viewScale;
  return [modelX, modelY];
}

function getParagraphs(host: EditorHost) {
  const gfx = host.std.get(GfxControllerIdentifier);
  const doc = host.doc;
  const surface = gfx.surface;

  const surfaceChildren = surface?.children || [];
  const rootChildren = doc.root?.children || [];
  const nonSurfaceChildren = rootChildren.filter(c => c.id !== surface?.id);
  const blocks = [...surfaceChildren, ...nonSurfaceChildren];
  const components = blocks
    .map(block => gfx.view.get(block.id))
    .filter(Boolean) as GfxBlockComponent[];

  const paragraphs: ParagraphLayout[] = [];
  const selector = '.affine-paragraph-rich-text-wrapper [data-v-text="true"]';

  components.forEach(component => {
    const paragraphNodes = component.querySelectorAll(selector);
    const viewportRecord = component.gfx.viewport.deserializeRecord(
      component.dataset.viewportState
    );
    if (!viewportRecord) return;
    const { zoom, viewScale } = viewportRecord;


Review comment (refactor):
I think we should move this function to `gfx/viewport.ts` and modify `Viewport.toModelCoordFromClientCoord` to ensure they have the same implementation.🤔

Code after:

export function syncCanvasSize(canvas: HTMLCanvasElement, host: HTMLElement) {
  const hostRect = host.getBoundingClientRect();
  const dpr = window.devicePixelRatio;
  canvas.style.position = 'absolute';
  canvas.style.left = '0px';
  canvas.style.top = '0px';
  canvas.style.width = '100%';
  canvas.style.height = '100%';
  canvas.width = hostRect.width * dpr;
  canvas.height = hostRect.height * dpr;
  canvas.style.pointerEvents = 'none';
}

function getParagraphs(host: EditorHost) {
  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 paragraphs: ParagraphLayout[] = [];
  const selector = '.affine-paragraph-rich-text-wrapper [data-v-text="true"]';

  components.forEach(component => {
    const paragraphNodes = component.querySelectorAll(selector);
    const viewportRecord = component.gfx.viewport.deserializeRecord(
      component.dataset.viewportState
    );
    if (!viewportRecord) return;
    const { zoom, viewScale } = viewportRecord;

    paragraphNodes.forEach(paragraphNode => {
      const paragraph: ParagraphLayout = {
        sentences: [],
      };
      const sentences = segmentSentences(paragraphNode.textContent || '');
      paragraph.sentences = sentences.map(sentence => {
        const sentenceRects = getSentenceRects(paragraphNode, sentence);
        const rects = sentenceRects.map(({ rect }) => {
          const [modelX, modelY] = clientToModelCoord(viewportRecord, [
            rect.x,
            rect.y,
          ]);
          return {
            text: sentence,
            ...rect,
            rect: {
              x: modelX,
              y: modelY,
              w: rect.w / zoom / viewScale,