Skip to content

Task #321 최종 보고서 — 페이지네이션 LINE_SEG vpos-reset 강제 분할#323

Closed
planet6897 wants to merge 16 commits into
edwardkim:develfrom
planet6897:task321
Closed

Task #321 최종 보고서 — 페이지네이션 LINE_SEG vpos-reset 강제 분할#323
planet6897 wants to merge 16 commits into
edwardkim:develfrom
planet6897:task321

Conversation

@planet6897

Copy link
Copy Markdown
Contributor

Task #321 최종 보고서 — 페이지네이션 LINE_SEG vpos-reset 강제 분할

상위 Epic: (해당 없음 — 독립 이슈)
관련 선행 작업: Task #310 (vpos 분석), Task #311 (Paginator에서 동일 접근 시도·부정), Task #313 (TypesetEngine 전환)
브랜치: task321task318
이슈: #321

증상

samples/21_언어_기출_편집가능본.hwp 1페이지 우측단 하단에서 문단 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.

항목 Before After
21_언어 페이지 수 15 15
21_언어 LAYOUT_OVERFLOW 건수 5 4
pi=29/pi=30 시각 겹침 ❌ 있음 ✅ 해소
exam_math/kor/eng 페이지 수 20/24/9 20/24/9

잔존 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=1 env 진단 훅 추가. 결과:

  • 포맷터 fmt.total_height가 모든 문단에 trailing line_spacing 포함 (~9.5px/문단 과다).
  • pi=30의 first_vpos=0이 HWP 원본의 "pi=30은 새 페이지/단 시작" 신호임을 확인.

Stage 2 — 문단간 vpos-reset 강제 분할 (커밋 3cea672)

src/renderer/typeset.rs::typeset_section 문단 순회 루프에 다음 검사 추가:

if para_idx > 0 && !st.current_items.is_empty() {
    let curr_first_vpos = para.line_segs.first().map(|s| s.vertical_pos);
    let prev_last_vpos = paragraphs[para_idx - 1].line_segs.last().map(|s| s.vertical_pos);
    if let (Some(cv), Some(pv)) = (curr_first_vpos, prev_last_vpos) {
        if cv == 0 && pv > 5000 {
            st.advance_column_or_new_page();
        }
    }
}

핵심 가드:

  • cv == 0: HWP가 vpos=0으로 리셋한 첫 seg만 대상 (일반 빈 문단의 우연 vpos 제외)
  • pv > 5000 HU: 직전 문단이 실제 내용이 있어야 함 (5000 HU ≈ 1.76mm)
  • !st.current_items.is_empty(): 단 최상단에서는 불필요한 분할 방지

21_언어 상세 효과

Before

  • 페이지 1 단 1: 22 items (pi=9 partial, pi=10..pi=30)
  • pi=30 LINE_SEG vpos=0 무시, col 1 하단에 배치 → pi=29와 동일 y로 클램프

After

  • 페이지 1 단 1: 21 items (pi=9 partial, pi=10..pi=29)
  • 페이지 2 단 0: pi=30 첫 항목 배치 (HWP 원본 의도 복원, hwp_used 오차 -6.8px)

SVG 좌표 검증

  • Before: pi=29 ③ y=1421.5, pi=30 ④ y=1421.5 (동일 위치 겹침)
  • After: pi=28 ② y=1420.28, pi=29 ③ y=1433.97 (13.7px 분리), pi=30 ④ → 페이지 2

Task #311과의 대비

Task #311은 Paginator 경로에서 유사한 vpos-reset 강제 분할을 시도하였으나 21_언어가 19→20쪽으로 회귀. 본 Task #321은 TypesetEngine 경로에서 적용하였고 페이지 수 불변 유지.

차이 원인:

  1. TypesetEngine이 column 가용 공간을 더 정확히 사용 (#313에서 검증됨).
  2. inter-paragraph reset만 대상으로 함 (intra-paragraph reset은 기존 detect_column_breaks_in_paragraph가 처리).
  3. 가드 조건 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 포함
  • vpos 실측: 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_DRIFT env 진단 훅 추후 fmt vs vpos 정합 디버깅용

회귀 검증

  • cargo test --release: 992 passed, 0 failed.
  • 4개 핵심 샘플: 페이지 수 불변.
  • samples/ 146개 파일 export-svg 대량 실행 정상 완료 (배치 스크립트).

본 Task 종료 절차

  1. Stage 4 커밋 (본 보고서 포함).
  2. 작업지시자 승인 후:
    • gh issue close 321
    • task321 → task318 (또는 local/devel) merge 결정.
  3. 병합 후 잔존 9.5px 드리프트는 별도 후속 이슈로 검토 (선택).

학습

  1. Task #311의 부정 결과를 교훈으로 보존한 덕분에 본 Task는 "같은 접근이 엔진만 바뀌면 성공할 수 있다"는 가설로 좁혀 즉시 검증 가능했다.
  2. 경고 ≠ 시각 버그: LAYOUT_OVERFLOW 경고가 남더라도 실제 렌더 결과가 허용 가능한지는 SVG를 직접 검증해야 한다.
  3. 좁은 가드가 광범위 회귀를 막았다: cv == 0 && pv > 5000 조합으로 오탐 최소화.

planet6897 and others added 10 commits April 25, 2026 12:20
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
@edwardkim

Copy link
Copy Markdown
Owner

@planet6897 님 검토 감사합니다. WASM 빌드 후 작업지시자가 시각 검증한 결과 회귀 1건이 발견되어 머지 전 수정 요청드립니다.

회귀 — 21_언어 1페이지 col 1 시작 위치

PR이 해소한 pi=29/pi=30 겹침 자체는 정상입니다. 그러나 col 1 (오른쪽 단)이 col 0 상단 4×5 표의 reserve를 무시하고 본문 상단부터 시작하면서, 표 영역과 시각적으로 겹치는 위치에서 텍스트가 시작합니다.

좌표 비교 (dump-pages -p 0 21_언어)

브랜치 단 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 (3932b835ee912e 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
@planet6897 planet6897 closed this Apr 25, 2026
…/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 (모두 유지)
@planet6897 planet6897 reopened this Apr 25, 2026
@planet6897 planet6897 closed this Apr 25, 2026
@planet6897 planet6897 reopened this Apr 25, 2026
@planet6897

Copy link
Copy Markdown
Contributor Author

Task #321 — dump-pages -p 0 결과 (21_언어, task332 적용 후)

  • 일시: 2026-04-25
  • 브랜치: task332 (HEAD = 01d75fe)
  • 명령: cargo build --release && ./target/release/rhwp dump-pages "samples/21_언어_기출_편집가능본.hwp" -p 0

요약

items used hwp_used diff
0 13 1202.7 159.1 +1043.6
1 20 1197.1 1213.1 -16.0
  • 총 15페이지 (이전 task321 16 → 15 회복, devel 일치)
  • col 0 마지막 item: PartialParagraph pi=10 lines 0..2 (pi=10 split 적용됨)
  • col 1 첫 item: PartialParagraph pi=10 lines 2..9 (vpos=12646 시작)
  • 단 1 diff = -16.0 (task321 기준 +218.9 대비 대폭 정합)

task321 (직전) → task332 (현재) 비교

항목 task321 (e7b1037) task332 (01d75fe)
페이지 수 16 15
단 0 items 12 13 (pi=10 lines 0..2 추가)
단 0 hwp_used 1220.3 159.1
단 0 diff +9.5 +1043.6
단 1 items 15 20
단 1 첫 item pi=10 (full) pi=10 lines 2..9 (partial)
단 1 used 1226.4 1197.1
단 1 hwp_used 1007.4 1213.1
단 1 diff +218.9 -16.0

적용된 task332 커밋 (devel 기준 신규)

01d75fe Task #332: 최종 결과 보고서
5376161 Task #332: golden SVG baseline 갱신 (issue-147, issue-157)
9e25f53 Task #332: stage5 vpos correction 가드 완화 + drift root cause 분석
0211e57 Task #332: stage4b clamp pile 제거 + typeset 측 마진 보강
d9713b8 Task #332: stage4a typeset fit 검사에 layout drift 안전 마진 도입
2d880bf Task #332: stage3b vpos correction 양방향 + collapse 가드
82f34a4 Task #332: stage3a vpos_end 에서 trail_ls 제외
08e477e Task #332: stage2 layout per-paragraph advance 를 height_for_fit 와 정합
bf42d61 Task #332: stage1 typeset advance 를 height_for_fit 기반으로 변경
5da5985 docs: typeset/layout drift 분석 — Task #331 revert 회고 (refs #332)
078717f Revert "Task #331: 문단 trailing line_spacing 누적 drift 해결"

핵심 흐름:

단 0 hwp_used 159.1 (diff +1043.6) 에 대한 메모

단 0의 hwp_used 가 159.1로 매우 작게 나오는 것은 단 0 마지막 item 이 PartialParagraph pi=10 lines 0..2 (vpos=9014..12646) 이고, compute_hwp_used_height 가 mid-paragraph vpos-reset 미발견 시 마지막 line bottom (=12646 hwpu ≈ 168.6px) 을 사용하는 데서 옴 — 단 1로 분할 이전 lines 0..2 의 vpos 가 col 0 시작 가까운 위치라 작은 값이 나오는 측정 특성. 회귀 판정 기준으로는 단 1 diff (-16.0) 가 의미 있는 지표.

결론

task332 변경으로 21_언어 1페이지 col 1 split 동작이 devel과 정합되며 페이지 수도 회복(16→15). col 1 다단 reserve 회귀 이슈는 해소된 것으로 판단.

@planet6897 planet6897 closed this Apr 26, 2026
edwardkim added a commit that referenced this pull request Apr 26, 2026
@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>
@planet6897 planet6897 deleted the task321 branch April 30, 2026 00:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants