Skip to content

rhwp-studio: 마우스 드래그 텍스트 선택 하이라이트가 페이지 밖으로 튀고 버벅임 #658

@postmelee

Description

@postmelee

현상

첨부 영상(화면 기록 2026-05-07 12.57.50.mov) 기준으로, rhwp-studio web 편집 화면에서 텍스트를 마우스로 드래그 선택할 때 선택 영역이 정상적으로 유지되지 않고 화면이 버벅인다.

2026-05-07.13.28.24.mov

영상 관찰 환경:

  • 실행 경로: Firefox 확장 viewer (moz-extension://.../viewer.html)
  • 문서: exam_social1.hwp
  • 관찰 위치: 오른쪽 박스/자료 영역 내부 텍스트
  • 영상: 6.16초, 3248x2122, 약 58fps

관찰된 동작:

  1. 텍스트 드래그 시 파란 선택 하이라이트가 생성되기는 한다.
  2. 그러나 일부 줄의 선택 하이라이트 폭이 실제 텍스트/박스 영역을 벗어나 오른쪽 페이지 바깥 회색 영역까지 길게 튄다.
  3. 드래그 중 하이라이트가 줄 단위로 안정적으로 유지되지 않고, 넓게 튀었다가 사라지는 동작이 반복된다.
  4. 드래그 중 화면이 버벅이며, 선택 종료 후 사용자가 기대한 텍스트 선택 상태가 정상적으로 남지 않는 것처럼 보인다.

즉 본질은 "선택 기능 미동작"이라기보다 선택 rect 계산이 잘못되어 페이지/박스 밖으로 튀고, 그 잘못된 rect를 매 프레임 DOM으로 재생성하면서 드래그 UX가 무너지는 문제로 보인다.

업스트림/기존 이슈 확인

2026-05-07 기준 upstream/devel 최신화 후 확인:

  • 기준 커밋: e7ae428
  • local/devel...upstream/devel: 0 / 0

관련 검색 결과:

  • 텍스트 선택 드래그
  • selection drag rhwp-studio
  • 버벅 / 느림 / 성능 rhwp-studio
  • 마우스 선택 rhwp-studio

정확히 같은 열린 이슈/PR은 확인되지 않았다.

인접 이슈:

의심 지점

1. 드래그 루프가 매 rAF마다 무거운 선택 갱신을 수행

rhwp-studio/src/engine/input-handler-mouse.ts

// 드래그 중: requestAnimationFrame으로 throttle하여 성능 확보
if (this.isDragging) {
  if (this.dragRafId) return;
  this.dragRafId = requestAnimationFrame(() => {
    this.dragRafId = 0;
    if (!this.isDragging) return;
    const hit = this.hitTestFromEvent(e);
    if (hit && hit.paragraphIndex < 0xFFFFFF00) {
      this.cursor.moveTo(hit);
      this.updateCaret();
    }
  });
  return;
}

드래그 중 매 프레임:

  • hitTestFromEvent(e)
  • cursor.moveTo(hit)CursorState.updateRect()
  • updateCaret()
  • updateSelection()

이 전체 경로가 실행된다. 선택 rect 계산이 무겁거나 잘못된 rect를 반환하면 화면이 즉시 버벅일 수 있다.

2. updateCaret()가 드래그 중에도 항상 updateSelection() 호출

rhwp-studio/src/engine/input-handler.ts

private updateCaret(): void {
  ...
  this.updateSelection();
  this.emitCursorFormatState();
  this.updateFieldMarkers();
  ...
}

드래그 중에는 커서 시각 갱신과 선택 rect 갱신이 강하게 결합되어 있다. 선택 rect만 갱신하면 되는 상황에서도 캐럿 스크롤/필드 마커/서식 상태 emit까지 동반될 수 있다.

3. SelectionRenderer가 매번 div를 전부 삭제하고 재생성

rhwp-studio/src/engine/selection-renderer.ts

render(rects: SelectionRect[], zoom: number): void {
  this.clear();
  this.ensureAttached();
  ...
  for (const rect of rects) {
    const div = document.createElement('div');
    ...
    this.layer.appendChild(div);
    this.highlights.push(div);
  }
}

긴 다중 줄 선택에서는 드래그 프레임마다:

  • 기존 highlight DOM 전부 remove
  • 새 div 다수 생성
  • style 문자열 전체 재할당

이 반복된다. 영상의 버벅임은 이 DOM churn의 영향일 가능성이 높다.

4. native selection rect 계산이 박스/셀/단 영역으로 클리핑되지 않는 것으로 보임

src/document_core/queries/cursor_nav.rs::get_selection_rects_native

의심 포인트:

  • get_selection_rects_native()가 반환하는 rect의 width가 실제 텍스트 컨테이너 폭을 넘어간다.
  • 영상에서 오른쪽 자료 박스 내부 텍스트를 선택할 때 하이라이트가 박스 경계뿐 아니라 페이지 오른쪽 바깥 회색 영역까지 확장된다.
  • 이는 SelectionRenderer가 단순히 left = pageLeft + rect.x * zoom, width = rect.width * zoom을 신뢰하기 때문에 native rect 오류가 그대로 화면에 드러난 것으로 보인다.

특히 아래 분기 점검 필요:

let width = if selection_continues {
    (area_right - rect_x).max(0.0)
} else if !partial_start && cell_ctx.is_none() {
    (rh.x - rect_x).max(0.0)
} else {
    (rh.x - lh.x).abs()
};

점검해야 할 사항:

  • lhrh가 같은 줄/같은 컨테이너의 커서 hit인지
  • range_end가 줄 끝/비렌더링 문자일 때 right_hit가 다음 줄 또는 다른 컨테이너 hit로 잡히지 않는지
  • cell_ctx.is_none() 경로에서 find_column_area()만 기준으로 삼아야 하는지, 글상자/박스/표 내부 텍스트에서 별도 컨테이너 bbox가 필요한지
  • cell_ctx=Some 경로도 셀 bbox로 rect_x..rect_x+width 클램프가 필요한지

예상 해결 방향

Stage 1. 재현/계측

  1. 영상 시나리오를 기준으로 exam_social1.hwp의 오른쪽 박스 텍스트 드래그를 재현한다.
  2. 드래그 중 다음 값을 임시 로그 또는 e2e 계측으로 수집한다.
    • hitTestFromEvent 결과: sectionIndex, paragraphIndex, charOffset, parentParaIndex, controlIndex, cellIndex, isTextBox, cursorRect
    • getSelectionRects*() 반환 rect 배열: pageIndex, x, y, width, height
    • 선택 rect가 페이지 폭/컬럼/셀/글상자 bbox를 초과하는 순간
  3. exam_math.hwp 2페이지부터 수식 더블클릭 hitTest 오동작 — 1페이지만 정상 #595 / PR #645의 hitTestHeaderFooter 정정과 무관하게 본 증상이 재현되는지 확인한다.

Stage 2. native selection rect 정합화

src/document_core/queries/cursor_nav.rs::get_selection_rects_native를 우선 점검/수정한다.

권장 방향:

  • 선택 rect는 실제 TextRun bbox와 해당 줄의 컨테이너 bbox를 기준으로 계산한다.
  • lhrh가 다른 줄/다른 컨테이너에서 온 경우, 줄 끝 rect 계산을 별도로 처리한다.
  • 본문/다단은 column area로, 셀 내부는 cell bbox로, 글상자 내부는 textbox/cell-context bbox로 클램프한다.
  • 최종 rect는 최소한 페이지 영역 밖으로 나가지 않도록 0..page.width, 0..page.height로 방어 클램프한다.
  • selection_continues일 때도 무조건 column 끝까지 확장하기보다 현재 줄의 실제 컨테이너 끝을 사용한다.

완료 조건:

  • 선택 하이라이트가 페이지 바깥 회색 영역으로 확장되지 않는다.
  • 오른쪽 자료 박스 내부 드래그 선택이 박스/텍스트 줄 범위 안에서 안정적으로 표시된다.
  • 기존 본문 다단 선택 회귀가 없어야 한다.

Stage 3. frontend 드래그 성능 개선

정확도 수정 후에도 버벅임이 남으면 rhwp-studio 쪽을 최적화한다.

권장 방향:

  • SelectionRenderer.render()에서 div 전량 삭제/재생성 대신 기존 div pool 재사용.
  • rect 배열이 이전 프레임과 동일하면 DOM 업데이트 생략.
  • 드래그 중 updateCaret() 전체를 호출하지 않고, 선택 갱신 전용 경량 경로를 분리하는 방안 검토.
  • 드래그 중 scrollCaretIntoView, emitCursorFormatState, updateFieldMarkers 호출 빈도 제한.

완료 조건:

  • 긴 다중 줄 드래그 선택에서도 프레임 드랍이 눈에 띄지 않는다.
  • 하이라이트가 깜빡이거나 선택 종료 시 사라지는 현상이 없어야 한다.

회귀 테스트 제안

Native/Rust

get_selection_rects_native()에 대해:

  • 같은 문단 다중 줄 선택
  • 표/셀 내부 다중 줄 선택
  • 글상자/박스 내부 다중 줄 선택
  • 다단 문서 선택

각 rect에 대해 다음 조건을 검증한다.

  • x >= 0
  • width > 0
  • x + width <= page_width + epsilon
  • 컨테이너 bbox가 있는 경우 x..x+width가 컨테이너 bbox를 초과하지 않음

rhwp-studio e2e

rhwp-studio/e2e/text-selection-drag.test.mjs 후보:

  1. 샘플 문서 로드
  2. 오른쪽 박스 텍스트 영역에서 mouse down → move → up
  3. .selection-layer 하이라이트 div들의 getBoundingClientRect() 수집
  4. 모든 하이라이트가 페이지 흰 영역 또는 대상 박스 영역 안에 있는지 검증
  5. 드래그 종료 후 선택 하이라이트가 유지되는지 검증

비목표

  • 네이티브 브라우저 텍스트 선택으로 전환
  • 클립보드 포맷 개선
  • #595의 머리말/꼬리말 hit-test 정정 자체

본 이슈는 마우스 드래그 텍스트 선택의 rect 정확도와 드래그 중 렌더링 성능에 한정한다.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions