diff --git a/mydocs/plans/task_m100_842.md b/mydocs/plans/task_m100_842.md new file mode 100644 index 000000000..d9b5a5b3d --- /dev/null +++ b/mydocs/plans/task_m100_842.md @@ -0,0 +1,41 @@ +# 수행계획서 — Task #842 (M100) + +## 대상 +shortcut.hwp(`samples/basic/shortcut.hwp`) ↔ 한컴 PDF(`pdf/basic/shortcut-2022.pdf`) 시각 정합성 잔여 결함 4건. + +GitHub Issue: edwardkim/rhwp#842 +브랜치: `local/task842` (← upstream/devel) + +## 결함 요약 + +| # | 증상 | 1차 원인 가설 | 영향 범위 | +|---|------|--------------|----------| +| 1 | 섹션 헤더 바(1×1 TAC 표) 위/아래 줄 간격 ~13~38px 압축 | TAC 1×1 표 앞뒤 단락 간격 보정 누락/부분적용 (구 #770/#773/#776, RFC #774 동일 본질) | 8페이지 전 헤더 | +| 2 | 일부 섹션(파일·편집·보기·입력·서식·기타) 본문에 없어야 할 좌측 여백 | ParaShape `margins.left`(예 4000) 적용 기준 또는 다단 zone 진입 첫 단락 들여쓰기 처리 | 해당 섹션 본문 | +| 3 | 두 단 사이 가운데 구분선이 실선 (점선이어야 함) | ColumnDef 구분선 종류(line type) 미보존 + 렌더러 실선 고정 | 다단 페이지 전체 | +| 4 | 단축키 우측정렬 항목 일부가 단 우측 끝 초과 (예 `Ctrl+(회색)5`, `Alt+P/Ctrl+P`) | cross-run right-tab 정렬이 탭 직후 1개 composed run 폭만 사용 → 스크립트 경계로 쪼개진 나머지 run 오버플로 (`src/renderer/layout/paragraph_layout.rs` 1419~1480행 부근) | 우측정렬 탭 + 혼합 스크립트 콘텐츠 | + +## 진행 방침 + +- 4건은 본질이 다르므로 **독립 단계**로 처리. 한 건 수정이 다른 건 회귀를 일으키지 않도록 매 단계 후 광범위 샘플 + shortcut.hwp 8페이지 SVG 비교. +- 1번(헤더 spacing)은 layout 본질 정정 위험군(메모리: `feedback_essential_fix_regression_risk`) — 다단/단일단/표분할 상호작용 회귀 점검 필수. 가장 위험하므로 마지막 단계 배치. +- 권위 자료: macOS 환경이므로 `pdf/basic/shortcut-2022.pdf` 1차. 한컴 2010 편집기 직접 출력이 가능한 환경이면 그게 최종. +- 코드 수정 전 IR 진단(`dump`, `dump-pages`, `--debug-overlay`) 으로 원인 확정. + +## 단계 구성 (예정 — 구현계획서에서 확정) + +1. **진단·재현 고정**: 4건 각각의 IR/레이아웃 근거 확정, 회귀 비교용 기준 SVG 캡처(`output/svg/`). +2. **결함 #4 (우측탭 오버플로)**: cross-run right-tab 폭 합산 수정. +3. **결함 #3 (단 구분선 점선)**: ColumnDef 구분선 종류 보존 + 렌더러 dasharray. +4. **결함 #2 (섹션 본문 좌측 여백)**: 원인 확정 후 들여쓰기 기준 수정. +5. **결함 #1 (헤더 표 spacing)**: TAC 1×1 표 앞뒤 단락 간격 보정 — 회귀 광역 검증 포함. +6. **종합 검증·보고서**: 전체 cargo test + clippy + shortcut.hwp 8페이지 + 회귀 샘플 비교, 최종 보고서. + +## 검증 기준 + +- `cargo test` 전건 통과, `cargo clippy` 경고 0(신규). +- shortcut.hwp 8페이지 SVG 가 PDF 와 4건 모두 정합. +- 회귀: 다단/표/목차 류 기존 샘플 SVG diff 무변화(의도된 변경 외). + +--- +승인 요청: 위 수행 방침으로 진행해도 되는지 확인 부탁드립니다. 승인 시 구현계획서(`task_m100_842_impl.md`)를 작성합니다. diff --git a/mydocs/plans/task_m100_842_impl.md b/mydocs/plans/task_m100_842_impl.md new file mode 100644 index 000000000..5a3c659e9 --- /dev/null +++ b/mydocs/plans/task_m100_842_impl.md @@ -0,0 +1,104 @@ +# 구현계획서 — Task #842 (M100) + +대상: shortcut.hwp PDF 정합성 잔여 결함 4건 (Issue edwardkim/rhwp#842) +브랜치: `local/task842` + +회귀 위험이 가장 큰 #1(헤더 표 spacing, layout 본질 정정)을 마지막에 배치. 작은 위험 → 큰 위험 순. + +--- + +## Stage 1 — 진단 및 회귀 기준 고정 + +목표: 4건 각각의 IR/레이아웃 근거를 확정하고, 수정 전 기준 산출물을 캡처한다. **소스 수정 없음.** + +작업: +- `rhwp export-svg samples/basic/shortcut.hwp -o output/svg/task842_before/` — 8페이지 기준 SVG. +- 결함 #4: `dump -s 0 -p {해당}` 으로 `Ctrl+(회색)5`, `Alt+P/Ctrl+P` 단락의 char-shape run·tab_def 확인. composer가 해당 run 을 스크립트 경계로 쪼개는지 로그/코드로 확정. +- 결함 #3: ColumnDef 파싱 결과(`src/parser/.../column*` 및 IR)에서 단 구분선 종류 필드가 존재/보존되는지 확인. 없으면 파싱 추가 필요 범위 식별. +- 결함 #2: 파일·편집·보기·입력·서식·기타 섹션 본문 첫 단락 `dump -s N -p M` → ParaShape margins, 소속 단/zone, 헤더 표와의 관계 확인. PDF 와 left x 차이를 수치로 기록. +- 결함 #1: `dump-pages -p {각 헤더 페이지}` 로 헤더 표 앞뒤 단락 간격 측정, PDF 대비 부족분 수치화. 구 #770/#773/#776/#774 문서 재확인. +- 회귀 비교 대상 샘플 목록 확정(다단/표분할/목차 류 — `samples/` 내). + +산출물: `mydocs/working/task_m100_842_stage1.md` (진단 결과 + 원인 확정 + 수정 범위 + 회귀 대상 목록). + +--- + +## Stage 2 — 결함 #4: cross-run 우측탭 폭 합산 수정 + +목표: 우측/가운데 탭 뒤 콘텐츠가 여러 composed run 으로 쪼개져도 단 우측 끝에 정확히 정렬되도록 한다. + +작업: +- `src/renderer/layout/paragraph_layout.rs` render 패스(1419~1480행 부근, est 패스 992~1069행도 동일 처리): + - 탭 뒤 첫 의미있는 run 부터 **다음 탭 또는 줄끝까지** 의 composed run 들의 폭을 합산하여 정렬 시작 x 산출. + - 빈 공백 run carry-over 로직과 일관되게 유지(공백 run 은 합산 단위 포함 여부 검토 — 한컴 동작 기준). + - leader end_x 보정 로직이 합산 폭 기준으로 동작하도록 조정. +- est 패스(높이 측정)와 render 패스가 동일 규칙을 쓰는지 확인. + +검증: shortcut.hwp 8페이지에서 `Ctrl+(회색)5`, `Alt+P/Ctrl+P`, `(회색)+/-`, `Shift+(회색)+/-`, `Ctrl+(회색)+`, `Ctrl+(회색)-` 등 모든 혼합 스크립트 우측탭 항목이 일반 항목과 같은 우측 끝(±1px). 회귀: 목차 류 우측탭(페이지번호) 샘플 SVG diff 무변화. `cargo test`. + +산출물: `mydocs/working/task_m100_842_stage2.md`. + +--- + +## Stage 3 — 결함 #3: 단 구분선 점선 반영 + +목표: 다단 구분선을 ColumnDef 에 지정된 선 종류(점선 등)로 렌더링한다. + +작업: +- (Stage 1 결과에 따라) ColumnDef 파싱에 구분선 종류 필드 보존 추가 — HWP5/HWPX 양쪽. +- 레이아웃→렌더 노드로 구분선 종류 전달. +- SVG/렌더러에서 선 종류 → `stroke-dasharray` 매핑(점선/파선/실선 등). SVG export 에 단 구분선이 누락돼 있다면 함께 추가. + +검증: shortcut.hwp 다단 페이지에 PDF 와 동일한 점선 세로 구분선. 회귀: 실선 구분선 사용 다단 샘플 무변화. + +산출물: `mydocs/working/task_m100_842_stage3.md`. + +--- + +## Stage 4 — 결함 #2: 섹션 본문 좌측 여백 정정 + +목표: 파일·편집·보기·입력·서식·기타 섹션 본문의 좌측 들여쓰기를 PDF 와 일치시킨다. + +작업: +- Stage 1 에서 확정한 원인에 따라 수정: + - (a) ParaShape `margins.left` 적용 기준이 단 안쪽이어야 하는데 본문 영역 기준이면 → 단(column) 기준으로 보정. + - (b) 다단 zone 진입 첫 단락 들여쓰기 처리 차이면 → 해당 경로 수정. +- 룰/휴리스틱 구분(메모리 `feedback_rule_not_heuristic`): HWP 명세상 기준이 명확하면 단일 룰로, 분기 도입 전 자문. + +검증: 해당 6개 섹션 본문 left x 가 PDF 와 일치. 회귀: 단일 단 문서 + 다른 다단 문서 본문 들여쓰기 무변화. + +산출물: `mydocs/working/task_m100_842_stage4.md`. + +--- + +## Stage 5 — 결함 #1: 헤더 1×1 TAC 표 앞뒤 단락 간격 보정 + +목표: 섹션 헤더 바 위/아래 간격을 한컴 PDF 와 일치시킨다. **layout 본질 정정 — 회귀 위험 최고, 광역 검증 필수.** + +작업: +- Stage 1 진단 + RFC #774 분석 기반으로 TAC 1×1 표 앞뒤 단락 간격(before/after spacing) 보정 규칙 구현. +- 메모리 `feedback_essential_fix_regression_risk`: 다단/단일단/표분할 상호작용 회귀 광범위 검증. + +검증: shortcut.hwp 8페이지 전 헤더 위아래 간격이 PDF 와 일치(±수 px). 회귀: TAC 표 포함 샘플 전수 + 표분할 샘플 + 한컴 2010/2020 정답지 대비 비교(가능 범위). `cargo test` 전건. + +산출물: `mydocs/working/task_m100_842_stage5.md`. + +--- + +## Stage 6 — 종합 검증 및 최종 보고서 + +작업: +- `cargo test` 전건 통과, `cargo clippy --all-targets` 신규 경고 0. +- shortcut.hwp 8페이지 SVG ↔ PDF 4건 모두 정합 최종 확인. `output/svg/task842_after/` 캡처 + before/after diff. +- 회귀 대상 샘플 SVG diff 최종 점검. +- 최종 보고서 `mydocs/report/task_m100_842_report.md` 작성. +- merge 전 `git status` 로 미커밋 파일 확인. + +--- + +## 커밋 규약 +- 각 Stage 소스 + `working/task_m100_842_stage{N}.md` 함께 커밋, 메시지 `Task #842: ...`. +- 최종 보고서 커밋 후 승인 → `local/task842` → `local/devel` merge (원격 push 금지). + +--- +승인 요청: 위 6단계 구현계획으로 진행해도 되는지 확인 부탁드립니다. 승인 시 Stage 1 진단부터 착수합니다. diff --git a/mydocs/report/task_m100_842_report.md b/mydocs/report/task_m100_842_report.md new file mode 100644 index 000000000..aa85fb471 --- /dev/null +++ b/mydocs/report/task_m100_842_report.md @@ -0,0 +1,38 @@ +# 최종 결과 보고서 — Task #842 (M100) + +대상: shortcut.hwp(`samples/basic/shortcut.hwp`) ↔ 한컴 PDF(`pdf/basic/shortcut-2022.pdf`) 시각 정합성 잔여 결함 4건. +GitHub Issue: edwardkim/rhwp#842 · 브랜치: `local/task842` (← upstream/devel) + +## 결과 요약 + +| # | 결함 | 결과 | +|---|------|------| +| 4 | 단축키 우측탭 정렬 일부가 단 우측 끝 초과 | ✅ 수정 (Stage 2 + 2b) | +| 3 | 두 단 가운데 구분선이 실선 (점선이어야 함) | ✅ 수정 (Stage 3) | +| 2 | 페이지 2~8 섹션 헤더 바 +28px 우측 편위 | ✅ 수정 (Stage 4) | +| 1 | 헤더 1×1 TAC 표 앞뒤 수직 spacing 압축 | ⏸ 미수정 — RFC #774 영역, 후속 이슈로 분리 (Stage 5 조사 완료) | + +`cargo test` 전건 통과 (svg_snapshot 8/8 포함). 회귀 없음. (`cargo clippy` 는 본 타스크 무관한 pre-existing `error: unwrap() will always panic` — `table_ops.rs:1007`, `object_ops.rs:304` — 으로 컴파일 실패하나 본 변경과 무관.) + +## 변경 내역 + +### 결함 #4 — `src/renderer/layout/paragraph_layout.rs`, `src/renderer/layout/text_measurement.rs` +- `right_tab_block_width()` 헬퍼: cross-run 우측·가운데 탭 정렬 시, 탭 직후 run 부터 `\t` 없는 연속 composed run 들의 폭을 합산해 정렬 시작 x 산출. composer 가 스크립트·char-shape 경계로 run 을 쪼개는 케이스(`"Ctrl+(회색)5"` → `["Ctrl+(", "회색)", "5"]`)에서 나머지 run 이 탭스톱 우측으로 흘러넘치던 ~32px 오버플로 해소. +- `compute_char_positions` 의 in-run RIGHT 인라인 탭 분기 `(2, _) if fill_low != 0` → `(2, _)` 로 확장: RIGHT 인라인 탭은 leader 유무 무관하게 `body_right - our_seg_w` 로 정렬(한컴 `ext[0]` 무시). char-shape 경계가 `\t` 앞에 놓여 run 이 `\t` 로 시작하는 케이스(`"끝"`(id7) + `"\tAlt+X"`(id8))의 ~28px 오버플로 해소. +- 결과: shortcut.hwp 8페이지 우측정렬 단축키 항목 전부 정렬 폭 ±6px 수렴. + +### 결함 #3 — `src/renderer/layout.rs::build_column_separators` +- `separator_type → StrokeDash` 매핑에 `6 => Dash`(LongDash 근사), `7 => Dot`(Circle/원형 점선) 추가. `doc_info.rs:294` line_type 의미와 정합. shortcut.hwp 2단 ColumnDef 의 `구분선 type=7` 이 실선 → 점선(`stroke-dasharray="2 2"`)으로 렌더. + +### 결함 #2 — `src/renderer/layout.rs::layout_table_item` (`is_tac` 분기) +- 다줄 문단(`composed.lines.len() > 1`)이고 line 0 에 `char::is_alphanumeric()` 글자(한글 음절/라틴/숫자/한자)가 있으면 → 표는 line 0 텍스트 *다음* 이 아니라 자체 줄 좌측에서 시작하므로 `leading = 0` (line 0 폭 미합산). +- line 0 이 HWP TAC 필러(`U+F081C`·`U+F012B` 등 PUA)/공백뿐인 경우(예 복학원서.hwp pi=16 — 한컴이 표 폭만큼 필러를 채워 줄바꿈시킨 케이스)는 종전대로 `compute_tac_leading_width`. `is_alphanumeric()` 판정으로 PUA 필러 자동 제외. +- 결과: shortcut.hwp 헤더 바 페이지 1~8 전부 rect x = body 좌측(94.5), `issue_677_bokhakwonseo_page1` snapshot 유지. + +## 미수정 — 결함 #1 (후속 이슈로 분리) +헤더 1×1 TAC 표 앞뒤 수직 여백(과 제목 위 여백)이 PDF 대비 ~15~25px 부족. 본문 행 pitch 자체는 정상. 명시 spacing(`spacing before/after`)에 해당 여백이 없어, 한컴이 zone 전환(1단↔2단)/TAC 표 문단 line-height 기반으로 넣는 암묵 간격으로 추정 — 닫힌 이슈 #770/#773/#776 + RFC #774 의 주제. 본질 정정 위험군이라 RFC 분석 + 광역 회귀 검증과 함께 별도 처리. 정밀 PDF 비교 데이터는 `mydocs/working/task_m100_842_stage5.md`. + +부수 발견(별개): (1) 제목 첫 글자 "흔" 누락("흔글 2010" → "글 2010"), (2) 페이지 3→4 column-break 행 밀림(`<편집 화면 분할에서>` "화면 이동" — 닫힌 #768 과 동일). 둘 다 후속 이슈 대상. + +## 커밋 +`f1665bff`(Stage 1) → `aac23bc7`(Stage 2) → `63a41829`(Stage 2b) → `6f0a0784`(Stage 3) → `2663eb32`/`5f2d85ab`(Stage 4 조사) → `bc2e8e54`(Stage 4 수정) → `3ed8da48`(Stage 5 조사) → 본 보고서. diff --git a/mydocs/working/task_m100_842_stage1.md b/mydocs/working/task_m100_842_stage1.md new file mode 100644 index 000000000..665fe890c --- /dev/null +++ b/mydocs/working/task_m100_842_stage1.md @@ -0,0 +1,72 @@ +# Stage 1 완료 보고서 — Task #842 (M100) + +목표: 4건 결함의 IR/레이아웃 근거 확정 + 수정 전 기준 산출물 캡처. **소스 수정 없음.** + +## 기준 산출물 +- `output/svg/task842_before/shortcut_00{1..8}.svg` — 수정 전 8페이지 SVG (회귀 비교 기준). + +--- + +## 결함 #4 — cross-run 우측탭 오버플로 (원인 확정) + +증상: `현재 낱말의 끝 글자로 ⟶ Ctrl+(회색)5` 의 `5` 우측 끝 ≈ x 1013px, 정상 우측탭(`Ctrl+Page Up` 등) ≈ x 973px → ~40px 초과. + +원인: +- 단축키 문단은 `tab_def_id=1 auto_right=true` (단 우측 끝 자동 우측탭) + 텍스트 `"…\tCtrl+(회색)5"`. +- `src/renderer/composer.rs::split_runs_by_lang` 가 char-shape run `"Ctrl+(회색)5"` 를 스크립트 경계로 분할 → `["Ctrl+(", "회색)", "5"]` (`회`/`색` 만 Hangul, `(`·`)` 는 중립이라 인접 run 에 흡수, `5` 는 ASCII digit 이라 비중립 → 별도 run). +- `src/renderer/layout/paragraph_layout.rs` cross-run right-tab 처리(render 패스 ~1419~1480, est 패스 ~992~1069): `pending_right_tab_render` 소비 시 **탭 직후 한 개 composed run** 의 폭만 `estimate_text_width(&run.text, …)` 로 빼서 시작 x 산출. 따라서 `"Ctrl+("` 만 우측 정렬되고 뒤따르는 `"회색)"`·`"5"` (~38px) 가 좌→우 정상 진행으로 탭스톱 오른쪽으로 밀려나옴. +- 기존 빈-공백-run carry-over 분기(`run.text.trim().is_empty()`)로는 못 잡음. + +수정 방향: 우측/가운데 탭의 정렬 단위 = **해당 탭부터 다음 탭(또는 줄끝)까지의 composed run 전체**. 그 합산 폭 기준으로 블록 시작 x 산출(leader end_x 보정도 합산 폭 기준). est/render 패스 동일 규칙. Task #279 목차(페이지번호) 케이스 회귀 점검. + +영향 항목 (혼합 스크립트 우측탭): `Ctrl+(회색)5`, `(회색)+/-`, `Shift+(회색)+/-`, `Ctrl+(회색)+`, `Ctrl+(회색)-` 등. `Alt+P/Ctrl+P` (`"인쇄\t Alt+P/Ctrl+P"` — 탭 뒤 선행 공백) 도 같은 계열(공백 run carry-over → 다음 단독 run 정렬). 합산-폭 방식이면 함께 해소될 가능성 큼 — Stage 2 에서 재확인. + +--- + +## 결함 #3 — 단 구분선 점선 (원인 확정) + +증상: 두 단 사이 세로 구분선이 실선. PDF 는 원형 점선(`⋮` 형태). + +원인: +- shortcut.hwp 의 2단 ColumnDef: `2단, 유형=배분, 구분선 type=7, width=7, color=0xaeaeae`. (`type=7` = HWP 선 종류 Circle/원형 점선 — `src/parser/doc_info.rs:303` 참조.) +- `src/renderer/layout.rs::build_column_separators` (~1029~1035): `separator_type` → `StrokeDash` 매핑이 `2=>Dash, 3=>Dot, 4=>DashDot, 5=>DashDotDot, _=>Solid` 만 처리. `6`(LongDash), `7`(Circle) 누락 → `7` 이 `_ => Solid` 로 떨어짐. +- 파서/IR(`ColumnDef.separator_type/width/color`)·SVG `` 출력 자체는 정상 동작 (`output/svg/task842_before/shortcut_002.svg` 에 `` 존재, dasharray 없음). + +수정 방향: `build_column_separators` 의 line-type→dash 매핑을 `doc_info.rs` 의 line_type 의미(1=Solid, 2=Dash, 3=Dot, 5=DashDotDot, 6=LongDash→Dash, 7=Circle→Dot, …)와 일치시킴. 가능하면 `border_line_type_to_dash` 류 공용 변환 재사용. `width=7` → `border_width_to_px` 값(현재 ~1.9px)이 HWP 0.5mm 와 큰 차이면 같이 검토(부차). + +--- + +## 결함 #2 — 섹션 헤더 바 좌측 위치 어긋남 (원인 미확정, Stage 4 에서 확정) + +증상(SVG 좌표 재측정으로 정정): +- 페이지 1 `커서 이동` 헤더 바 rect x ≈ 94.5px, 헤더 글자 x0 ≈ 98.3px. +- 페이지 2 `파일` 헤더 바 rect x ≈ 122.5px, 헤더 글자 x0 ≈ 126.3px → 페이지 1 대비 ~28px 우측 이동. +- **본문 텍스트 x0 는 두 페이지 모두 ≈ 121.2px 로 동일** — 즉 어긋난 것은 헤더 바(1×1 TAC 표)뿐. (사용자가 말한 "왼쪽에 여백" = 헤더 바가 오른쪽으로 밀려 본문보다 들어간 상태.) +- 또 페이지 1 body-clip width ≈ 933.5px, 페이지 2+ ≈ 954.0px 로 ~20px 차이. + +관찰: 페이지 1 `커서 이동` 헤더 문단(0.1)과 페이지 2 `파일` 헤더 문단(0.36) 의 ParaShape(`margins left=0 right=2000`), TAC 표 outer_margin(1mm), 표 size(69448 HU) 가 **동일**. 차이점: (a) 0.1 은 직전 `구역나누기` + 자체 `다단나누기`, 0.36 은 `쪽나누기`; (b) 직전 ColumnDef 가 0.1 은 `1단 간격=10mm`, 0.36 은 `1단 간격=0mm`. 다단 zone 안에서 쪽나누기로 새 페이지 진입 시 헤더 바 가로 위치/폭 계산이 어긋나는 것으로 추정. 정확한 원인은 Stage 4 에서 `--debug-overlay` + 레이아웃 로깅으로 확정. + +--- + +## 결함 #1 — 헤더 바 1×1 TAC 표 앞뒤 단락 간격 압축 (원인 확정, 회귀 위험 최고) + +증상: 각 회색 헤더 바 아래(와 위)에 PDF 가 두는 가시적 여백(~10~13px)이 rhwp 에서 0 에 가까움. + +근거 (`dump-pages -p 0`): +- 페이지 1: 단1(헤더 표) `used=31.1px`, 단2/단3(본문) `zone_y_offset=100.2` — 헤더 표 끝(69.1+31.1=100.2)과 본문 시작이 **gap=0**. +- 본문 단2 `used=186.7px` vs `hwp_used≈273.3px` (diff −86.7px), 단3 `used=173.3px` vs `≈253.3px` (diff −80.0px) — dump-pages `used` 가 line-spacing gap 미반영이라 과소 표시이긴 하나(실제 SVG row pitch 는 20px 로 정상), 헤더 표 직후 zone 전환 spacing 누락이 누적 압축의 핵심. +- 헤더 문단 0.1: `spacing before=0 after=0 line=100%`, 본문 문단 0.2: `spacing before=0 after=0` + `[다단나누기]` + `2단 ColumnDef` → 헤더(1단 zone)에서 본문(2단 zone)으로의 **zone 경계**. 명시 spacing 어디에도 없음 → 한컴이 암묵적으로 추가하는 간격(RFC #774 zone-level / TAC 표 후속 spacing). + +수정 방향: 구 #770/#773/#776 + RFC #774(`mydocs/...`) 분석 기반으로 TAC 1×1 표 문단 직후(또는 zone 전환 시) 한컴 동일 간격 보정. **layout 본질 정정 — 다단/단일단/표분할 상호작용 회귀 광범위 검증 필수**(메모리 `feedback_essential_fix_regression_risk`). 그래서 Stage 5(마지막)에 배치. + +--- + +## 회귀 비교 대상 (Stage 2~5 공통) +- 목차/페이지번호 우측탭 샘플 (#4 관련) +- 다단 + 단 구분선 사용 샘플 (#3 관련) +- TAC 표 포함 샘플 전수 + 표분할 샘플 + 다단/단일단 혼재 샘플 (#1, #2 관련) +- 한컴 2010/2020 정답지 접근 가능 시 우선 (#1) + +--- + +다음: Stage 2 — 결함 #4 (cross-run 우측탭 폭 합산) 수정. diff --git a/mydocs/working/task_m100_842_stage2.md b/mydocs/working/task_m100_842_stage2.md new file mode 100644 index 000000000..3b399ea9b --- /dev/null +++ b/mydocs/working/task_m100_842_stage2.md @@ -0,0 +1,43 @@ +# Stage 2 완료 보고서 — Task #842 (M100) + +목표: 결함 #4 — cross-run 우측탭 정렬에서 탭 다음 콘텐츠가 여러 composed run 으로 쪼개진 경우의 오버플로 수정. + +## 변경 + +`src/renderer/layout/paragraph_layout.rs`: +- `right_tab_block_width()` 헬퍼 추가 — 탭 직후 run 부터 `\t` 를 포함하지 않는 연속 run 들의 `estimate_text_width` 합산. +- est 패스 / render 패스의 cross-run 우측·가운데 탭 정렬 시작 x 계산을, 단일 run 폭 → 블록 전체 폭(`right_tab_block_width`) 기준으로 변경. +- est 패스 run 루프를 `enumerate()` 로 변경 (`run_idx_est` 필요). + +## 결과 + +shortcut.hwp 8페이지 우측탭 정렬: +- `Ctrl+(회색)5` 류(스크립트 경계로 `["Ctrl+(", "회색)", "5"]` 분할): `5` 우측 끝 1005px → 967px 로 교정. 페이지 1 우측열 16개 항목 정렬 폭 [966.0, 967.3] 으로 수렴. +- `(회색)+/-`, `(쉼표)` 등 한 run 으로 합쳐지는 항목: 기존대로 정상(변화 없음). +- 회귀: `cargo test` 전건 통과 (8/8 svg_snapshot 포함). + +## Stage 2b — `Alt+P/Ctrl+P` 계열 추가 수정 (완료) + +원인 확정: char-shape 경계가 `\t` **앞**에 놓이는 단축키 항목(`끝`(id7) + `\tAlt+X`(id8)) 은 cross-run 핸들러(직전 run 이 `\t` 로 끝나는 경우) 대상이 아니라 in-run 탭 경로(`compute_char_positions` 의 inline_tabs 처리)를 탄다. 그 경로의 RIGHT(no-leader) 탭 분기가 한컴이 저장한 `ext[0]`(한컴 metrics 기준 resolved 위치)를 사용 → fallback 폰트 환경에서 우변 폭 차이만큼(~28px) 우측 초과. + +수정 — `src/renderer/layout/text_measurement.rs` `compute_char_positions` 의 inline-tab RIGHT 분기를 `(2, _) if fill_low != 0` → `(2, _)` 로 확장. RIGHT 인라인 탭은 leader 유무 무관하게 `body_right - our_seg_w` 로 정렬 (한컴 `ext[0]` 무시 — cross-run RIGHT 핸들러와 동일 룰). 코멘트상 기존 의도("단일 룰")와 정합. + +결과: +- shortcut.hwp 8페이지 우측탭 항목 전부 정렬 폭 [961.7, 972.7] 으로 수렴 (`Alt+X`/`Alt+F9`/`Ctrl+W,H`/`Ctrl+K,M`/`Alt+Shift+*` 등 ~28px 초과 해소). +- `cargo test` 전건 통과 (svg_snapshot 8/8 포함). +- 잔여 미세 outlier: `흰색 글자색 Ctrl+M,W` (~10px 짧음) — 별개 원인, 수용 가능 범위. + +비고: `cargo clippy` 는 pre-existing `error: unwrap() will always panic` (`table_ops.rs:1007`, `object_ops.rs:304`) 로 컴파일 실패 — 본 타스크와 무관. + +## (구) 미해결 잔여 — 위 Stage 2b 에서 해소됨 + +증상: char-shape 경계가 `\t` **앞**에 놓여 run 이 `\t` 로 시작하는 항목(`끝`(id7) + `\tAlt+X`(id8) 등 — 단축키 우변이 별도 bold run 이고 그 run 이 `\t` 로 시작) 은 cross-run 핸들러("`\t` 로 끝나는 run 의 다음 run 정렬")가 트리거되지 않는다. 이 경우 in-run 탭 처리(`compute_char_positions`)가 `compute_char_positions`/`available_width` 기준으로 우측 정렬하나 실제 정렬 위치가 ~28~32px 우측으로 어긋남(`Alt+X`/`Alt+F9`/`Ctrl+W,H`/`Ctrl+K,M`/`Alt+Shift+*` 등 ASCII 로 끝나는 단축키). + +시도한 수정 — composer 단계에서 선행 `\t` 를 직전 run 끝으로 이동 정규화(`split_runs_by_lang` 후처리): shortcut.hwp 는 정합되나 **KTX.hwp 목차 / aift.hwp 3페이지 svg_snapshot 회귀**(목차 leader-dot run 이 `\t` 로 시작하는 케이스를 잘못 병합 → leader 추출/우측정렬 깨짐). 메모리 룰(`feedback_essential_fix_regression_risk`)에 따라 폐기. + +→ 별도 처리 필요. paragraph_layout 의 run 루프에서 "`\t` 로 시작하는 run" 을 "직전 run 이 `\t` 로 끝난 것"과 동등하게 다루되 leader run 케이스를 회피하는 핀포인트 수정이 필요. Stage 2b 또는 후속 이슈로 분리. + +## 산출물 +- `output/svg/task842_s2c/shortcut_00{1..8}.svg` — Stage 2 적용 후 SVG. + +다음: (논의 후) Stage 2b — `Alt+P/Ctrl+P` 계열 잔여, 또는 Stage 3 — 단 구분선 점선. diff --git a/mydocs/working/task_m100_842_stage3.md b/mydocs/working/task_m100_842_stage3.md new file mode 100644 index 000000000..a3cd376e5 --- /dev/null +++ b/mydocs/working/task_m100_842_stage3.md @@ -0,0 +1,18 @@ +# Stage 3 완료 보고서 — Task #842 (M100) + +목표: 결함 #3 — 두 단 사이 가운데 구분선이 실선으로 렌더되는 문제(점선이어야 함). + +## 원인 +shortcut.hwp 2단 ColumnDef 의 `구분선 type=7`(HWP 선 종류 Circle/원형 점선). `src/renderer/layout.rs::build_column_separators` 의 `separator_type → StrokeDash` 매핑이 `2..=5` 만 처리하고 `6`(LongDash)·`7`(Circle) 누락 → `7` 이 `_ => Solid` 로 떨어짐. (파서/IR `ColumnDef.separator_type` 및 SVG `` 출력은 정상.) + +## 변경 +`src/renderer/layout.rs::build_column_separators`: +- `separator_type` → `StrokeDash` 매핑에 `6 => Dash`(LongDash 근사), `7 => Dot`(Circle/원형 점선) 추가. `doc_info.rs:294` 의 `line_type` 의미와 정합. 8+ (이중선/물결 등) 은 종전대로 Solid 대체. + +## 결과 +- shortcut.hwp 2~8페이지 단 구분선 `` 로 점선 렌더 (이전: `stroke-dasharray` 없음 = 실선). +- `cargo test` 전건 통과. + +비고: HWP 의 "원형 점선"(type 7) 은 작은 둥근 점이나 `StrokeDash` enum 에 RoundDot 변형이 없어 `Dot`(사각 점선 `2 2`)로 근사. 시각상 충분히 점선으로 보이며 별도 enum 추가는 범위 외. + +다음: Stage 4 — 섹션 헤더 바 좌측 위치 어긋남(결함 #2). diff --git a/mydocs/working/task_m100_842_stage4.md b/mydocs/working/task_m100_842_stage4.md new file mode 100644 index 000000000..95ec0d1bc --- /dev/null +++ b/mydocs/working/task_m100_842_stage4.md @@ -0,0 +1,23 @@ +# Stage 4 완료 보고서 — Task #842 (M100) — 결함 #2 (헤더 바 좌측 위치) + +목표: 페이지 2~8 섹션 헤더 바(1×1 TAC 표)가 +28px 우측으로 밀리는 문제 수정. + +## 원인 +- 헤더 바 1×1 표는 `is_tac_table_inline()` 가 **false**(폭 ≈ 단 폭) → 블록 취급 → `PageItem::Table` → `layout_table_item` 의 `is_tac` 분기 → 표 x = `col_area.x + effective_margin + leading`, `leading = compute_tac_leading_width(...)`. +- 페이지 2 헤더 문단(0.36)은 LINE_SEG 가 2개: `ls[0]` = 텍스트 "파일", `ls[1]` = TAC 표(자체 줄). `compute_tac_leading_width` 는 `composed.lines.first()`(= line 0 = "파일")의 run 폭을 전부 leading 으로 합산 → 표가 width("파일") ≈ 28px 만큼 우측 이동. 페이지 1 헤더 문단(0.1)은 빈 문단이라 line 0 폭 = 0 → leading=0 → 정상. + +## 수정 +`src/renderer/layout.rs` `layout_table_item`, `is_tac` 분기: +- 문단이 여러 줄(`composed.lines.len() > 1`)이고 **line 0 에 alphanumeric 글자**(한글 음절/라틴/숫자/한자 등 — `char::is_alphanumeric()`)가 있으면 → 표는 line 0 텍스트 *다음* 이 아니라 자체 줄 좌측에서 시작하므로 `leading = 0`. +- 그 외(빈 문단, 또는 line 0 이 HWP TAC 필러 `U+F081C`/`U+F012B` 등 PUA·공백뿐인 경우 — 예 복학원서.hwp pi=16, 한컴이 표 폭만큼 필러를 채워 줄바꿈시킨 케이스)는 종전대로 `compute_tac_leading_width` 사용. +- `is_alphanumeric()` 판정으로 PUA 필러는 자동 제외(PUA = Letter/Number 아님). 이전 시도(특정 필러 코드포인트 blocklist)는 `U+F012B` 같은 추가 필러를 놓쳐 복학원서 회귀가 났음. + +## 결과 +- shortcut.hwp 헤더 바 페이지 1~8 전부 rect x = 94.5 (= body 좌측, 페이지 1과 동일). +- 복학원서.hwp pi=16 표는 종전대로 leading 유지 (`issue_677_bokhakwonseo_page1` snapshot 통과). +- `cargo test` 전건 통과 (8/8 svg_snapshot 포함, 34개 test result ok, exit 0). + +## 부수 관찰 (미해결, 별개) +- 페이지 2 헤더 문단(0.36)에는 표(ls[1]) 외에 line 0 = "파일" 텍스트가 black 으로 별도 렌더되는지(헤더 바의 white "파일"과 이중 표시) — PDF 는 "파일" 1회뿐. 확인 필요하나 본 결함과 별개. → 후속. + +다음: Stage 5 — 결함 #1 (헤더 1×1 TAC 표 앞뒤 spacing 압축). diff --git a/mydocs/working/task_m100_842_stage5.md b/mydocs/working/task_m100_842_stage5.md new file mode 100644 index 000000000..7c0085ceb --- /dev/null +++ b/mydocs/working/task_m100_842_stage5.md @@ -0,0 +1,32 @@ +# Stage 5 조사 보고 — Task #842 (M100) — 결함 #1 (헤더 1×1 TAC 표 앞뒤 spacing 압축) + +상태: **조사 진행 — 미수정**. RFC #774 영역, 본질 정정 위험 큼. 작업지시자 판단 요청. + +## PDF ↔ rhwp 정밀 비교 (shortcut.hwp 1페이지) + +`pdftotext -bbox` (PDF, pt) + `mutool draw -r 150` (PNG 시각 확인): + +| 요소 | PDF (pt → px ×1.335) | rhwp (px, SVG) | 차이 | +|------|---------------------|----------------|------| +| 본문영역 상단 | 15mm ≈ 56.7px | 56.7px | 0 | +| 제목 "흔글 2010 단축키 일람표" 텍스트 top | 62.65pt ≈ 83.6px | ~58px (baseline 79.4 − ascent) | rhwp ~25px 높음 | +| "커서 이동" 헤더 텍스트 (yMin~yMax) | 108.24~120.24pt ≈ 144~160px | bar rect 103.1~126.7px, 텍스트 baseline 121 | rhwp ~40px 높음 | +| "빈칸 삽입" 첫 본문행 텍스트 top | 145.93pt ≈ 194.8px | baseline 142 → top ~131px | rhwp ~64px 높음 | +| 헤더 바 아래 → 첫 본문행 간격 | ~34px (텍스트bottom→텍스트top) | ~15px (bar bottom→text baseline), 실질 ~4px (bar bottom→text top) | rhwp ~20~30px 부족 | +| 본문 행 pitch | 15pt ≈ 20px | ~20px | ≈ 동일 | + +→ rhwp 의 콘텐츠가 위로 갈수록 누적적으로 위쪽으로 압축됨: 제목 위 ~25px 부족, 제목↔헤더 ~15px 부족, 헤더↔본문 ~20px 부족. 본문 행 pitch 자체는 정상. `mutool` 렌더 시각 확인 — 제목이 헤더 바에 거의 붙어 있고, 헤더 바 위·아래 여백이 PDF 대비 1/3 수준. + +`dump-pages -p 0` 관찰: 단0(제목 zone) `used=69.1px hwp_used≈53.1px diff=+16px`(rhwp 16px 초과), 단2/3(본문) `used=186.7 hwp_used≈273.3 diff=−86.7px` — 단 `used` 값은 line-spacing gap 미포함으로 실제 렌더(SVG 본문 ≈ 280px ≈ hwp_used)와 불일치, 즉 본문은 정상. 제목 zone +16px / 헤더 바 주변 spacing 누락이 핵심. + +## 미규명 — spacing 출처 +헤더 문단(0.1): `spacing before=0 after=0 line=100%`, 표 outer_margin 1mm(≈1.9px), 셀 pad 0.5mm. 제목 문단(0.0): `spacing before=0 after=0 line=140%`. 본문 첫 문단(0.2): `spacing before=0` + `다단나누기` + 새 `2단 ColumnDef`. → 명시 spacing 어디에도 PDF 의 ~20px 가 없음. 한컴이 (a) zone 전환(1단↔2단) 시, 또는 (b) TAC 표 문단의 `line=100%`/표 높이 기반으로, 또는 (c) 제목/헤더 문단의 LINE_SEG vpos 해석으로 암묵 간격을 넣는 것으로 추정 — 정확한 규칙 미확정. 이게 닫힌 이슈 #770/#773/#776 + RFC #774("한컴 PDF paragraph spacing 알고리즘 정밀 분석")의 주제. + +## 부수 발견 (별개 결함) +1. **제목 첫 글자 누락**: rhwp 가 "흔글 2010..." 을 "글 2010..." 로 렌더 — 제목 첫 글자 "흔"(PUA? 옛한글?) 이 빠짐. char run 분할/PUA 처리 의심. +2. **페이지 3 → 4 column-break 행 밀림** (`<편집 화면 분할에서>` 의 "화면 이동" 행): 작업지시자 언급. 닫힌 이슈 #768 과 동일 — 다단 zone 분할 결함, 별개 영역. + +## 권고 +결함 #1 은 RFC #774 분석을 끼고 다뤄야 안전 (zone 전환/TAC 표 spacing 본질 정정 — 메모리 `feedback_essential_fix_regression_risk`: 다단/단일 단/표분할 상호작용 회귀 위험 큼, 광범위 샘플 + 한컴 2010/2020 정답지 검증 필요). 현 타스크에서 임의 수정은 회귀 위험이 큼. + +→ **본 타스크는 #4·#3·#2 (4건 중 3건, 완료·검증·커밋) 로 마무리** 하고, **#1(헤더 표 spacing/RFC #774) + 부수 발견 2건(제목 첫 글자 누락, 페이지 3→4 밀림) 을 별도 후속 이슈로 등록** 권고. #1 을 본 타스크에서 강행 시 회귀 위험을 감수해야 함. diff --git a/src/renderer/layout.rs b/src/renderer/layout.rs index dcb5847fe..2f7b9d2e7 100644 --- a/src/renderer/layout.rs +++ b/src/renderer/layout.rs @@ -1027,11 +1027,16 @@ impl LayoutEngine { ) { if layout.column_areas.len() >= 2 && layout.separator_type > 0 { let line_width = border_width_to_px(layout.separator_width).max(0.5); + // HWP 선 종류 코드 (doc_info.rs:294 line_type 매핑과 정합): + // 1=실선, 2=Dash, 3=Dot(점선), 5=DashDotDot, 6=LongDash, 7=Circle(원형 점선), + // 8+ (이중선/물결 등) 은 Solid 대체. let dash = match layout.separator_type { 2 => StrokeDash::Dash, 3 => StrokeDash::Dot, 4 => StrokeDash::DashDot, 5 => StrokeDash::DashDotDot, + 6 => StrokeDash::Dash, // LongDash → Dash 근사 + 7 => StrokeDash::Dot, // Circle(원형 점선) → Dot _ => StrokeDash::Solid, }; for i in 0..layout.column_areas.len() - 1 { @@ -2396,9 +2401,27 @@ impl LayoutEngine { // composed.lines[0] 의 runs 에서 TAC 이전 텍스트 폭을 직접 // 합산해 표 x 좌표에 반영한다. inline_shape_position 미세팅 상태에서 // 기본값 col_area.x(body_left) 으로 붕괴되는 현상 방지. - let leading = composed.get(para_index) - .map(|c| compute_tac_leading_width(c, control_index, styles)) - .unwrap_or(0.0); + // [Issue #842 #2] 문단이 여러 줄이고 line 0 에 *실제 텍스트*(필러/공백/ + // 오브젝트마커가 아닌 가시 글자)가 있으면 — 예: line 0 = "파일" 텍스트 + + // line 1 = 자체 줄의 헤더 바 표 — 표는 line 0 텍스트 *다음* 이 아니라 + // 자체 줄 좌측에서 시작하므로 leading = 0. line 0 이 HWP TAC 필러(U+F081C)/ + // 공백뿐인 경우(예: 복학원서.hwp pi=16, 한컴이 표 폭만큼 필러를 채워 + // 줄바꿈시킨 케이스)는 종전대로 compute_tac_leading_width 사용. + // "실제 텍스트" 판정은 alphanumeric(한글 음절·라틴·숫자·한자 등 Letter/Number) + // 만 인정 — HWP TAC 필러(U+F081C 등 PUA), 공백, 오브젝트마커는 PUA/공백이라 + // 자동 제외된다 (복학원서.hwp pi=16 line 0 = U+F081C/U+F012B 필러 99개 → 제외). + let line0_has_real_text = composed.get(para_index).map(|c| { + c.lines.len() > 1 && c.lines.first().map(|l0| { + l0.runs.iter().any(|r| r.text.chars().any(|ch| ch.is_alphanumeric())) + }).unwrap_or(false) + }).unwrap_or(false); + let leading = if line0_has_real_text { + 0.0 + } else { + composed.get(para_index) + .map(|c| compute_tac_leading_width(c, control_index, styles)) + .unwrap_or(0.0) + }; let base_x = col_area.x + effective_margin + leading; // [Issue #291] ParaShape align 반영: // TAC 표가 inline_shape_position 미설정 상태에서 단/문단 좌측에 diff --git a/src/renderer/layout/paragraph_layout.rs b/src/renderer/layout/paragraph_layout.rs index 1a7b8520b..b52f14a46 100644 --- a/src/renderer/layout/paragraph_layout.rs +++ b/src/renderer/layout/paragraph_layout.rs @@ -84,6 +84,50 @@ pub(crate) fn resolve_last_tab_pending( } } +/// 우측/가운데 탭 정렬 단위의 폭(px). +/// +/// 탭 직후 run(`start`)부터 `\t` 를 포함하지 않는 연속 run 들의 `estimate_text_width` 합산. +/// composer(`split_runs_by_lang` / `split_by_char_shapes`)가 char-shape·스크립트 경계로 run 을 +/// 쪼개므로(예: `"Ctrl+(회색)5"` → `["Ctrl+(", "회색)", "5"]`), 탭 직후 한 개 run 폭만 쓰면 +/// 나머지 run 이 탭스톱 우측으로 흘러넘친다 (Issue #842, 결함 #4). +#[allow(clippy::too_many_arguments)] +pub(crate) fn right_tab_block_width( + runs: &[crate::renderer::composer::ComposedTextRun], + start: usize, + styles: &ResolvedStyleSet, + default_tab_width: f64, + tab_stops: &[TabStop], + auto_tab_right: bool, + available_width: f64, +) -> f64 { + let mut w = 0.0; + for r in runs.iter().skip(start) { + if r.text.contains('\t') { + break; + } + if let Some(_ov) = &r.char_overlap { + let chars: Vec = r.text.chars().collect(); + let fs = { + let ts = resolved_to_text_style(styles, r.char_style_id, r.lang_index); + if ts.font_size > 0.0 { ts.font_size } else { 12.0 } + }; + w += if crate::renderer::composer::decode_pua_overlap_number(&chars).is_some() { + fs + } else { + fs * chars.len() as f64 + }; + continue; + } + let mut ts = resolved_to_text_style(styles, r.char_style_id, r.lang_index); + ts.default_tab_width = default_tab_width; + ts.tab_stops = tab_stops.to_vec(); + ts.auto_tab_right = auto_tab_right; + ts.available_width = available_width; + w += estimate_text_width(&r.text, &ts); + } + w +} + impl LayoutEngine { pub(crate) fn layout_inline_table_paragraph( &self, @@ -970,7 +1014,7 @@ impl LayoutEngine { let mut run_char_pos_est = comp_line.char_start; // cross-run 탭 감지용 inline_tabs(composed.tab_extended) 커서 — Task #290 let mut inline_tab_cursor_est: usize = 0; - for run in &comp_line.runs { + for (run_idx_est, run) in comp_line.runs.iter().enumerate() { let run_char_count_est = if run.char_overlap.is_some() { let chars: Vec = run.text.chars().collect(); if crate::renderer::composer::decode_pua_overlap_number(&chars).is_some() { @@ -1004,16 +1048,18 @@ impl LayoutEngine { } else { tab_pos }; + // [Issue #842 #4] 탭 다음 콘텐츠가 여러 composed run 으로 쪼개진 경우 + // (스크립트·char-shape 경계) 전체 블록 폭 기준으로 정렬해야 마지막 글자가 + // 탭스톱에 맞는다. (선행 공백 run "예 16" 케이스도 합산에 포함되어 동작 유지.) + let run_w = right_tab_block_width( + &comp_line.runs, run_idx_est, styles, + tab_width, &tab_stops, auto_tab_right, available_width, + ); match tab_type { 1 => { - // [Task #279] 전체 run 폭 기준 — 선행 공백이 있는 경우 (예: " 16") - // run 시작 x 가 좌측으로 가서 시각적으로 페이지번호 right edge 가 - // effective_pos 에 정렬되도록. - let run_w = estimate_text_width(&run.text, &ts); est_x = effective_pos - run_w; } 2 => { - let run_w = estimate_text_width(&run.text, &ts); est_x = effective_pos - run_w / 2.0; } _ => {} @@ -1442,16 +1488,17 @@ impl LayoutEngine { } else { tab_pos }; + // [Issue #842 #4] 탭 다음 콘텐츠가 여러 composed run 으로 쪼개진 경우 + // (스크립트·char-shape 경계, 예 "Ctrl+(회색)5") 전체 블록 폭 기준 정렬. + let next_w = right_tab_block_width( + &comp_line.runs, run_idx, styles, + tab_width, &tab_stops, auto_tab_right, available_width, + ); match tab_type { 1 => { - // [Task #279] 전체 run 폭 기준 — 선행 공백이 있는 경우 (예: " 16") - // run 시작 x 가 좌측으로 가서 시각적으로 페이지번호 right edge 가 - // effective_pos 에 정렬되도록. - let next_w = estimate_text_width(&run.text, &text_style); x = col_area.x + effective_pos - next_w; } 2 => { - let next_w = estimate_text_width(&run.text, &text_style); x = col_area.x + effective_pos - next_w / 2.0; } _ => {} diff --git a/src/renderer/layout/text_measurement.rs b/src/renderer/layout/text_measurement.rs index 5878b8995..8ce640672 100644 --- a/src/renderer/layout/text_measurement.rs +++ b/src/renderer/layout/text_measurement.rs @@ -403,7 +403,6 @@ impl TextMeasurer for EmbeddedTextMeasurer { f64::INFINITY }; let high_byte = (tab_type_raw >> 8) & 0xFF; - let fill_low = tab_type_raw & 0xFF; match (high_byte, tab_type_raw) { (_, 1) => { // 기존 raw 1 (LEFT 또는 잘못된 RIGHT 1) — 호환 유지 let seg_start = { let mut s = i + 1; while s < chars.len() && chars[s] == ' ' && cluster_len[s] != 0 { s += 1; } s }; @@ -414,11 +413,14 @@ impl TextMeasurer for EmbeddedTextMeasurer { let seg_w = measure_segment_from(&chars, &cluster_len, i + 1, &char_width); x = (tab_target - seg_w / 2.0).max(x); } - (2, _) if fill_low != 0 => { - // RIGHT + leader: ')' 끝이 본문 우측 끝까지 정렬되도록 - // x = body_right - our_seg_w. 한컴 ext[0] 는 무시 - // (한컴_seg_w 와 our_seg_w 미세 차이로 본문 우측 끝 미달). - let seg_start = { let mut s = i + 1; while s < chars.len() && chars[s] == ' ' && cluster_len[s] != 0 { s += 1; } s }; + (2, _) => { + // RIGHT 인라인 탭: 우변 콘텐츠 끝이 본문 우측 끝까지 정렬되도록 + // x = body_right - our_seg_w. 한컴 ext[0] 는 무시 — 한컴 metrics + // 기준 값이라 fallback 폰트 환경에서 우변 폭 차이만큼(~수십 px) 어긋남. + // leader 유무와 무관하게 동일 룰 (cross-run RIGHT 핸들러와 정합). + // [Issue #842 #4] `\t` 가 bold run 시작에 오는 단축키 항목(`끝`+`\tAlt+X`) + // 은 cross-run 핸들러 대상이 아니라 이 in-run 경로를 타므로 여기서 정렬. + let seg_start ={ let mut s = i + 1; while s < chars.len() && chars[s] == ' ' && cluster_len[s] != 0 { s += 1; } s }; let seg_w = measure_segment_from(&chars, &cluster_len, seg_start, &char_width); x = (body_right - seg_w).max(x); }