Task #321 최종 보고서 — 페이지네이션 LINE_SEG vpos-reset 강제 분할#323
Conversation
TypesetEngine::typeset_section의 문단 순회 루프에 다음 검사 추가: - 직전 문단이 같은 단에 있고 - 현재 문단의 first_vpos가 0이며 - 직전 문단의 last_vpos가 5000 HU 이상이면 강제로 다음 단/페이지로 이동한다. HWP LINE_SEG가 vpos=0으로 리셋한 위치는 HWP 원본이 해당 지점에서 페이지/단 분할을 의도한 신호이므로 존중. 21_언어 샘플 1페이지 우측단 하단에서 pi=29와 pi=30이 같은 y 좌표에 클램프되어 겹쳐 렌더되던 증상 해소. - 21_언어: 15쪽 유지, pi=30 → 페이지 2 col 0 (hwp_used 오차 -6.8px) - exam_math/kor/eng: 페이지 수 불변 - cargo test: 992 passed, 0 failed
cargo test --release: 992 passed, 0 failed 4 샘플 페이지 수 무변화 (21_언어 15쪽, exam_math 20쪽, exam_kor 24쪽, exam_eng 9쪽) samples/ 146개 export-svg 대량 실행 정상
…간에 반영 원인: 페이지 상단에 body 너비 80% 이상의 wrap=TopAndBottom 표/도형이 있을 때 layout은 body_wide_reserved로 col 1+ 시작 y를 그 아래로 밀어 정확히 처리하나, TypesetEngine은 이를 모르고 col 1+에 full available_body_height만큼 paragraph를 배치 → layout의 clamp 로직이 다수 paragraph를 col_bottom 근처에 클램프하여 시각적 텍스트 겹침 발생. 수정: TypesetState.pending_body_wide_top_reserve 필드 추가. col 0 paragraph 처리 중 body-wide TopAndBottom 도형/표 발견 시 그 점유 높이를 등록. advance_column_or_new_page 시 다음 column의 current_height 시작값으로 사용. zone_y_offset이 아닌 current_height를 통해 적용하여 layout과의 double-shift 회피 (layout은 body_wide_reserved로 별도 처리). 21_언어 1페이지 우측단 하단 pi=27~29 텍스트 겹침 시각적 해소. 대가: - 21_언어: 15→16쪽 (+1, 시각적 정확성 우선) - exam_eng: 9→10쪽 (+1, 동일 origin 의심) - exam_math/kor: 20/24쪽 무변화 - cargo test: 992 passed, 0 failed
vert_rel_to=Paper인 wrap=TopAndBottom 도형/표는 페이지 절대 위치(머리말 영역 포함)에 놓인 것이라 HWP는 col 1을 본문 상단부터 시작시키므로, 본 도형은 다단 reserve 계산에서 제외해야 함. 21_언어 1페이지의 4×5 표가 page-y=131px 부터 시작하여 body 상단 일부를 침범하지만 HWP의 col 1 hwp_used≈1213px (full body 사용) 이 이를 시사. 수정 (2개 파일): - src/renderer/typeset.rs::compute_body_wide_top_reserve_for_para — vert_rel_to=Paper 분기 제외 - src/renderer/layout/shape_layout.rs::calculate_body_wide_shape_reserved — 동일 분기 제외 (양쪽 동기화) 결과 (4 샘플): - 21_언어: 16 → **15쪽 (PDF 일치)** + 우측단 하단 텍스트 겹침 해소 - exam_math: 20 무변화 - exam_kor: 24 무변화 - exam_eng: 10 → **9쪽 (회복)** - cargo test: 992 passed, 0 failed
This reverts commit 3932b83.
|
@planet6897 님 검토 감사합니다. WASM 빌드 후 작업지시자가 시각 검증한 결과 회귀 1건이 발견되어 머지 전 수정 요청드립니다. 회귀 — 21_언어 1페이지 col 1 시작 위치PR이 해소한 pi=29/pi=30 겹침 자체는 정상입니다. 그러나 col 1 (오른쪽 단)이 col 0 상단 4×5 표의 reserve를 무시하고 본문 상단부터 시작하면서, 표 영역과 시각적으로 겹치는 위치에서 텍스트가 시작합니다. 좌표 비교 (
|
| 브랜치 | 단 1 used | hwp_used | diff | 단 1 시작 |
|---|---|---|---|---|
| devel (정상) | 1223.1 | 38.9 | +1184.3 | body 하단 시작 (≈1184px reserve) |
| PR #323 | 1174.7 | 1213.1 | -38.4 | body 상단 시작 (reserve=0) |
devel 은 col 1에 약 1184px reserve 를 두어 표 아래에서 단을 시작한다 — 한컴이 의도한 위치와 일치.
PR #323 은 v3 커밋 (3932b83) 의 VertRelTo::Paper 제외 분기가 본 케이스를 잘못 처리하여 reserve=0 으로 만든다.
원인 — v3 의 Paper 제외 가정이 본 케이스에 맞지 않음
21_언어 pi=0 4×5 표:
[common] treat_as_char=false, wrap=위아래, vert=용지(9872=34.8mm), horz=용지
표는 vert_rel_to=Paper, vertical_offset=34.8mm 이고 wrap=TopAndBottom. v3 커밋 메시지에서는 "Paper 기준 도형은 페이지 절대 위치이므로 col 1 시작에 영향 없음" 이라 가정했지만 — 본 표는 body_area (y=209.8 ~ 1436.2) 와 수직으로 겹치는 위치에 있어 col 0 뿐 아니라 col 1 도 reserve 가 필요한 정상 케이스입니다.
작성자 v3 분석 근거 ("col 1은 HWP 기준 1213px를 사용") 의 1213px 은 col 의 가용 폭 이지 시작 y 와 무관합니다. devel 의 단 1 hwp_used=38.9 가 보여주듯 한컴은 col 1 에도 ~1184px reserve 를 적용합니다.
첨부
작업지시자 시각 캡처: 1페이지 우측 단 첫 줄 ("기법을 적용하여 ...") 이 4×5 표 영역과 수직으로 겹쳐 시작 (정상은 표 아래 "이때 기존의 ..." 이 첫 줄).
수정 요청
Option A — v3 (Paper 제외) 가드 정밀화
vert_rel_to=Paper + wrap=TopAndBottom + body_area 와 수직 겹침 인 도형은 여전히 reserve 대상. 진짜 "페이지 머리말 영역에만 있어 본문에 영향 없음" 인 도형(예: y_offset + height < body_area.y) 만 제외하도록 조건 강화.
// 후보: body 와 겹치지 않는 Paper 도형만 제외
if matches!(common.vert_rel_to, VertRelTo::Paper) {
let shape_top = hwpunit_to_px(common.vertical_offset as i32, dpi);
let shape_bottom = shape_top + hwpunit_to_px(common.height as i32, dpi);
let body_top = layout.body_area.y;
if shape_bottom <= body_top {
continue; // 본문과 겹치지 않으면 제외
}
}Option B — v3 롤백 후 v1 (vpos-reset only) 으로 좁히기
v2 (pending_body_wide_top_reserve) + v3 (Paper 제외) 를 함께 롤백하고 보고서 핵심인 vpos-reset 강제 분할만 남기기. 21_언어 pi=29/pi=30 겹침 해소는 v1 만으로 달성됨 (작성자 stage2 보고서). aift.hwp +4 페이지 개선 효과는 보존됩니다 (이는 vpos-reset 신호에 의한 것 4건, 누적 시프트 2건).
다만 v2/v3 가 해결하던 다른 케이스 (Stage 보고서엔 명시 안 됨) 가 있다면 별도 후속 이슈 분리 필요.
Option 권고
Option A 권장 — v2/v3 의 핵심 의도(body 가용 공간 정합) 는 살리고 Paper 제외 가정만 정밀화. 검증 시 21_언어 1페이지 col 1 hwp_used 가 38.9 부근으로 회귀.
판단 부탁드립니다. 수정 push 후 재검토 진행하겠습니다.
v3 (3932b83 → 5ee912e revert) 가 Paper-anchored body-wide 도형을 일률 제외하여 21_언어 page 1 의 4×5 표(vert=Paper, body 와 겹침) 의 col 1 reserve 까지 제거 → col 1 첫 본문이 표 영역과 수직 겹침 회귀. Option A 적용: - src/renderer/typeset.rs::compute_body_wide_top_reserve_for_para - src/renderer/layout/shape_layout.rs::calculate_body_wide_shape_reserved 양쪽에 'shape_bottom <= body_top' 가드 추가. 본문과 겹치지 않는(머리말 영역만 점유) Paper 도형만 제외, body 와 겹치는 도형은 reserve 대상으로 포함. 검증: - 21_언어 page 1 col 1 첫 본문 SVG y=342.4 (4×5 표 end=314.8 아래) ✓ - compute_body_wide_top_reserve_for_para: pi=0 reserve=329.95 - calculate_body_wide_shape_reserved: pi=0 bottom_y=329.95 - 992 + 71 테스트 통과, clippy 클린 페이지 수: - 21_언어: 16 (v3 의 15쪽 효과는 reserve 부적절 제거의 부산물 — 시각 정확성 우선) - exam_math/kor: 20/24 무변화 - exam_eng: 10 (동일) ref edwardkim#326
…/inset
v5 (drift 보정):
- Paper-anchored TopAndBottom block-table 호스트 문단의 cur_h advance 를 표
effective_height 가 아닌 first_vpos jump 로 교정. 21_언어 p1 col 0 +85.8 → +9.5 px,
pi=9 가 col 0 에 통째로 fit, col 1 시작이 pi=10 ("적합성 검증이란...") 으로 PDF 일치.
v6 (border 시각 병합 + inset):
- 인접 문단 테두리 range 를 visible stroke signature (line_type/width/color) 로도 병합.
bf_id 가 달라도 동일 stroke 면 HWP/PDF 처럼 단일 사각형 렌더. invisible 끼리는 병합 금지.
- ParaShape::border_spacing[2]/[3] 를 push tuple 에 전달하여 그룹 첫/마지막 inset 에
반영. stroke 있을 때 default 최소 2 px. 인접 다른 border group 과의 충돌 회피.
검증:
- cargo test --lib: 992 passed
- cargo test --test svg_snapshot: 6 passed (form-002, issue-147, issue-157, issue-267,
table-text, deterministic)
- cargo clippy --release: clean
- 페이지 수: 21_언어 16, exam_math 20, exam_kor 24, exam_eng 10, math_8 1,
exam_science 5, exam_social 5 (모두 유지)
Task #321 — dump-pages -p 0 결과 (21_언어, task332 적용 후)
요약
task321 (직전) → task332 (현재) 비교
적용된 task332 커밋 (devel 기준 신규)핵심 흐름:
단 0 hwp_used 159.1 (diff +1043.6) 에 대한 메모단 0의 결론task332 변경으로 21_언어 1페이지 col 1 split 동작이 devel과 정합되며 페이지 수도 회복(16→15). col 1 다단 reserve 회귀 이슈는 해소된 것으로 판단. |
@planet6897 의 PR #323 (Task #321 vpos-reset) 은 메인테이너 회귀 통보 후 작성자가 자체 close 하고 PR #343 으로 후속 정리 통합. 본 PR 은 not merged 로 archive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task #321 최종 보고서 — 페이지네이션 LINE_SEG vpos-reset 강제 분할
상위 Epic: (해당 없음 — 독립 이슈)
관련 선행 작업: Task #310 (vpos 분석), Task #311 (Paginator에서 동일 접근 시도·부정), Task #313 (TypesetEngine 전환)
브랜치:
task321←task318이슈: #321
증상
samples/21_언어_기출_편집가능본.hwp1페이지 우측단 하단에서 문단 pi=29와 pi=30이 동일 y 좌표(y=1421.5)에 클램프되어 시각적 텍스트 겹침.LAYOUT_OVERFLOW경고 3건(pi=28/29/30).결과 요약
시각적 겹침 해소. pi=30이 HWP 원본 의도대로 페이지 2 col 0 상단에 배치되며 pi=29와의 중첩 제거.
cargo test --release: 992 passed, 0 failed.잔존 pi=28/pi=29 경고(9.5px)는 포맷터의 trailing line_spacing 포함 계산과 layout의 vpos 진행 사이 드리프트에 의한 것이며, 실제 body text는 col_bottom 이내에 렌더되어 시각 영향 없음. 별도 후속 이슈로 분리 가능.
구현
Stage 1 — 드리프트 origin 정량화 (커밋
348c617)src/renderer/typeset.rs::typeset_paragraph진입부에RHWP_TYPESET_DRIFT=1env 진단 훅 추가. 결과:fmt.total_height가 모든 문단에 trailing line_spacing 포함 (~9.5px/문단 과다).first_vpos=0이 HWP 원본의 "pi=30은 새 페이지/단 시작" 신호임을 확인.Stage 2 — 문단간 vpos-reset 강제 분할 (커밋
3cea672)src/renderer/typeset.rs::typeset_section문단 순회 루프에 다음 검사 추가:핵심 가드:
cv == 0: HWP가 vpos=0으로 리셋한 첫 seg만 대상 (일반 빈 문단의 우연 vpos 제외)pv > 5000 HU: 직전 문단이 실제 내용이 있어야 함 (5000 HU ≈ 1.76mm)!st.current_items.is_empty(): 단 최상단에서는 불필요한 분할 방지21_언어 상세 효과
Before
After
SVG 좌표 검증
Task #311과의 대비
Task #311은 Paginator 경로에서 유사한 vpos-reset 강제 분할을 시도하였으나 21_언어가 19→20쪽으로 회귀. 본 Task #321은 TypesetEngine 경로에서 적용하였고 페이지 수 불변 유지.
차이 원인:
detect_column_breaks_in_paragraph가 처리).pv > 5000 HU로 미미한 vpos만 있는 경우 제외.잔존 이슈 (별도 후속 후보)
pi=28/pi=29의 9.5px LAYOUT_OVERFLOW 경고는 다음 불일치에 기인:total_height = spacing_before + sum(line_height + line_spacing) + spacing_after→ trailing line_spacing 포함last.vpos + last.lh - first.vpos→ trailing line_spacing 미포함두 지표 차이가 문단당 ~9.5px. 누적되어 column 경계 근처에서 경고 발생. 실제 렌더는 layout의
is_column_top등 보정 로직으로 col_bottom 이내에 clamp되므로 시각 영향 없음. 포맷터와 vpos 정합이 필요하다면 별도 이슈로 다룬다.산출물
src/renderer/typeset.rs(+ 진단 훅 + vpos-reset 검출 로직)mydocs/plans/task_m100_321.md(수행계획서)mydocs/plans/task_m100_321_impl.md(구현 계획서)mydocs/working/task_m100_321_stage1.md(Stage 1 정량화)mydocs/working/task_m100_321_stage2.md(Stage 2 효과 분석)보존된 부속물
RHWP_TYPESET_DRIFTenv 진단 훅회귀 검증
cargo test --release: 992 passed, 0 failed.samples/146개 파일 export-svg 대량 실행 정상 완료 (배치 스크립트).본 Task 종료 절차
gh issue close 321학습
cv == 0 && pv > 5000조합으로 오탐 최소화.