# 간소 재배치(`simple-relayout`) — 계산 중 재측정하지 않는 재배치 2026-10-17 작성. 코드는 `relayout-methods/simpleRelayout.ts`(후보)와 `relayout-core/syntheticTight.ts`(합성 판정 박스)다. comp-frame 의 「대치 후 재배치(간소)」 체크박스가 이 후보를 탄다(`src/comp-frame/thumbnailRelayout.ts` 의 `simpleRelayoutDoc`). ## 왜 따로 두나 Adaptive Relayout 은 후보를 고칠 때마다 렌더해서 tight bounding box 를 다시 잰다. 프로덕션 썸네일에서는 그 왕복이 너무 느리다. 이 후보는 재배치 중에 **렌더를 한 번도 안 한다.** 입력은 셋뿐이다. - 대치된 sheetJson(렌더러가 한 번 돌려준 pageJson). comp-frame 에서는 **오토핏을 켠 판**이다 (2026-10-17 사용자 결정) — 렌더러가 먼저 글자를 줄여 담고, 남은 결함만 이 후보가 고친다. - 원안 sheetJson - 원안 외곽선과 원안 텍스트 tight. A는 `iui.text_tight_boxes`의 내용 해시·버전이 맞는 원안 값을 읽고, 누락분은 프레임으로 예측한다. A는 글자 측정 iframe을 띄우지 않는다. B·Adaptive는 원안을 한 번 측정하고 같은 원안의 값을 메모리에 보관한다. 대치 직후 한 번 잰 tight 가 있으면(`ctx.tight`) 크기와 글이 그대로인 노드는 그 값을 물려받는다. 없어도 돈다. **화면에 그리는 비용은 별개다.** 최초 대치 결과와 재배치 결과를 각각 그리므로 A는 **문서 렌더 2회·추가 측정 0회**다. B는 **문서 렌더 2회·추가 측정 1회**이며, 원안 측정 메모리 캐시가 없으면 측정이 1회 더 필요하다(전체 호출 3회 또는 4회). 측정도 내부에서 문서를 그리지만 화면 표시용 문서 렌더와 구분해 센다. 기존 c4 렌더 결과를 입력으로 시작하면 A의 추가 호출은 재배치 결과를 표시하는 문서 렌더 1회다. DB 조회·외곽선 캔버스 계산은 문서 렌더·측정 호출과 따로 센다. 표가 커져 뒤 요소를 다시 배치하는 경로는 대치 렌더를 1회 더 할 수 있고, 실패·재시도는 이 정상 경로 횟수에서 제외한다. 리소스 외곽선은 `iui.resource_outlines`의 호환 v1 행을 먼저 읽는다. 칠해진 영역의 구멍은 `paintedPolygons`에 보존하고 기존 `outerEnvelopePolygons`로 재배치용 외곽을 만든다. 열린 선을 임의로 닫거나 볼록 껍질로 채우지 않는다. 누락은 기존 브라우저 계산으로 보완한다. 이 리소스 캐시는 확장형 도형·배경색 제거 등 인스턴스 외곽선 전체를 대체하지 않는다. 그 기능의 모르도르 통합·캐시 표준화는 [MOR-2639](https://miridih.atlassian.net/browse/MOR-2639)의 별도 범위다. ## 하는 것과 안 하는 것 | 결함 | 고치는 법 | | --- | --- | | 글이 컨테이너 밖으로 넘친다 | 컨테이너 안에서 옮겨 담기면 옮긴다. 안 되면 컨테이너를 **제자리에서** 키운다 | | 글이 장식·다른 글과 새로 겹친다 | 글을 컨테이너 안에서 비킨다. 모자라면 그쪽으로 컨테이너를 키운다 | | 글이 페이지 밖으로 나간다 | 유효한 원안 컨테이너가 있는 글만 컨테이너와 페이지의 교집합 안으로 옮긴다 | | 그래도 깊이 남는다 | 가독성 하한까지 5%씩 줄이되, 원안에서 대응하는 반복 역할은 같은 배율을 적용한다 | | 짧은 번호 배지가 줄바꿈된다 | 컨테이너 형상이 허락하는 범위에서 글상자 폭만 넓힌다. 번호 축소·이동·배지 도형 확대는 하지 않는다 | - **컨테이너 없는 글은 옮기지 않는다(2026-10-17 사용자 결정).** 원안의 유효한 컨테이너를 확정한 뒤 없는 글의 입력 절대 좌표를 고정한다. 겹침·페이지 이탈을 이유로 이동시키지 않으며, 아래/가운데 정렬 글자를 줄일 때도 x/y를 바꾸지 않는다. 해결하지 못한 결함은 그대로 남긴다. - 원안 글을 거의 다 담던 작은 배경 도형을 지원 영역으로 보존한다. 그 도형에서 글을 밀어내거나, 원래 글을 담지 않았던 밑줄 장식을 컨테이너처럼 키우는 변경을 거부한다. 외곽선이 있으면 bbox만으로 담김을 판단하지 않는다. - 이동 가능한 글도 입력 기준 누적 이동 상한·원안 읽기 순서·붙어 있는 장식 관계를 지켜야 한다. 같은 검사를 개별 후보, 축소 후 배치, 최종 결과에 적용한다. - **컨테이너는 옮기지 않는다.** 키운 상자는 언제나 옛 상자를 포함한다(`containersStayPut` 이 끝에서 확인하고, 어기면 입력을 돌려준다). 이웃 앞에서 멈추므로 키우다가 새 겹침을 만들지 않는다. 그 대신 카드끼리 이미 겹친 것은 못 푼다. - **장식**: 변에 붙은 장식이 안쪽으로 파고든 깊이는 영역에서 빼고(`fitRectIn`), 바깥으로 내민 몫만큼 성장 상한을 페이지에서 떼어 놓는다(`decorationOverhangOf`). 카드 안 아이콘처럼 코너·중앙에 앉은 장식은 새 겹침으로 잡혀 글이 비킨다. 끝에서 `carryDecorations` 가 장식을 키운 변과 옮긴 글에 다시 붙인다. - 정렬선·크기 묶음·중앙 복원·구분선은 고치지 않는다. 정렬선은 판정 바닥(0.6) 밑으로 떨어뜨리는 고치기만 거절한다. ## 판정 박스 — 합성 자 렌더가 없으니 지표(`computeLayoutMetrics`)에 넣을 tight 를 만들어야 한다. 프레임을 그대로 쓰면 (`frameBoxesAsTight`) 넘침과 겹침을 크게 부풀린다. 그래서 노드마다 원안 tight 에서 위·아래 리딩, 베어링, 폭 보정을 배워 지금 프레임에 얹는다(`createSyntheticRuler`). 원안 tight 가 없으면 코퍼스 중앙값 리딩을 쓴다. 코퍼스 298건의 대치 직후 입력에서 판정 입력(①넘침·⑤신규 겹침)이 실측과 같은 케이스 수: | 자 | 넘침 노드 | 신규 겹침 쌍 | 둘 다 실측과 같은 케이스 | | --- | ---: | ---: | ---: | | 실측 tight | 338 | 408 | 298 | | 프레임 | 591 | 588 | 135 | | 합성 자(원안 tight 보정) | 351 | 405 | 238 | ⚠️ **가로 정렬이 안 적힌 글은 가운데다.** `textOverflow.ts` 의 `alignOf` 는 값이 없으면 `left` 로 보지만, `horizontalAlign` 이 트리 어디에도 없는 텍스트 891개를 실측하면 렌더러는 789개를 가운데에 앉혔다(왼쪽 20 · 오른쪽 12 · 폭을 다 채운 것 70). 합성 자는 원안 잉크의 좌우 여백으로 정렬을 읽는다. `alignOf` 자체는 고치지 않았다 — 다른 후보도 쓰는 함수다. ## 여유 합성 자의 아래 변 오차는 p90 7px 안팎이다. 그래서 세 군데에 여유를 둔다. - 옮기거나 키울 때 글자 크기의 0.3배를 남긴다(`SAFETY_EM`). - 글자 크기의 0.15배보다 얕은 새 겹침은 건드리지 않는다(`OVERLAP_DEPTH_EM`). - 되돌릴 수 없는 축소는 0.5배보다 깊은 결함에만 건다(`SHRINK_DEPTH_EM`). ## 실측 — 코퍼스 298건, 결과를 한 번씩 렌더해 채점 ⚠️ **아래 숫자는 오토핏을 끈 입력(`rendered.json`)으로 잰 것이다.** comp-frame 은 오토핏을 켠 판을 넣기로 바꿨다(2026-10-17). 코퍼스에는 오토핏을 켠 렌더본이 없어(`batch/renderShrink.ts` — 지금은 굽지 않는다) 그 판으로는 아직 못 쟀다. 굽는 데 제품 미리보기 렌더러가 필요하다. `pnpm exec tsx scripts/post-replace-relayout-simple-eval.ts`. 평가 가능 293건이다 (`case_sol.json` 의 UNSOLVABLE 5건 제외). 채점은 `post-replace-relayout-method-run.ts` 와 같다. | 비교군 | SUCCESS | ACCEPTABLE | FAIL | UNKNOWN | 재배치 시간(평균) | | --- | ---: | ---: | ---: | ---: | ---: | | 재배치 없음 | 33 | 17 | 229 | 14 | — | | 간소 A(원안 tight 만) | 44 | 38 | 197 | 14 | 57ms | | 간소 B(+ 대치 직후 1회 tight) | 46 | 38 | 195 | 14 | 55ms | | Adaptive 예측 경로 | 69 | 29 | 181 | 14 | 271ms | 요청한 네 결함만 세면 이렇다(결함이 남은 케이스 수). | 비교군 | 넘침 | 신규 겹침 | 페이지 밖 잘림 | 폰트 0.9 미만 | | --- | ---: | ---: | ---: | ---: | | 재배치 없음 | 141 | 154 | 0 | 0 | | 간소 A | 48 | 72 | 0 | 25 | | 간소 B | 54 | 75 | 1 | 20 | | Adaptive 예측 경로 | 47 | 93 | 3 | 14 | - 판정이 재배치 없음보다 나아진 케이스는 간소 A 가 32건이고, 나빠진 케이스는 0건이다. Adaptive 예측 경로는 54건이 나아지고 2건이 나빠졌다. - FAIL 이 많이 남는 주원인은 범위 밖의 **단어 내부 분절**이다(간소 A 의 FAIL 197건 중 158건). - **예측 자의 결정이 맞았나**(바뀐 케이스, 간소 A): 「나아졌다」고 보고 받은 206건 중 실제로 나아진 것 195건, 같은 것 8건, **나빠진 것 3건**(1.5%). - 코퍼스에는 페이지 밖으로 나간 텍스트가 없다. 페이지 끌어오기는 단위 테스트로만 확인했다. - B 가 A 보다 낫지 않다. 실측을 물려받은 노드가 늘면서 결정이 바뀐 케이스 중 일부가 실측으로는 나빠졌다(9건). 원인은 아직 안 봤다. ## c4 오토핏 입력 3,114장 재평가 — 2026-10-17 `post-replace-relayout-c4-eval.ts --label repaired-reviewed`로 전체 3,114장을 다시 평가했다. 오류는 0건이며 컨테이너 없는 글의 입력 절대 좌표가 바뀐 장은 A·B 모두 0장이다. 원안에 없는 렌더러 생성 빈 TextItem도 검사한다. 산출물은 `eval-out/runs/c4-regression/repaired-reviewed/`의 `summary.json`, `regression-comparison.json`, `gallery.html`에 보존했다. | 비교군 | 바꾼 장 | 넘침 | 신규 겹침 | 페이지 밖 잘림 | 폰트 0.9 미만 | | --- | ---: | ---: | ---: | ---: | ---: | | 입력 | 0 | 150 | 159 | 27 | 293 | | 수정 A | 221 | 102 | 133 | 26 | 315 | | 수정 B | 230 | 81 | 122 | 24 | 309 | 입력 대비 넘침·겹침 개선은 남지만, 이전 간소판(A 112/110, B 69/81)의 겹침 개선 폭은 유지하지 못했다. 글의 자리·순서를 깨는 이동을 막으면서 결함을 남긴 경우가 있다. 글꼴 0.9 미만인 장도 입력보다 늘었다. 기존 13개 회귀 비교군의 이미지를 다시 확인했고, 번호 배지는 글상자 폭을 넓혀 한 줄로 만들었으며 잘못된 이동·장식 성장·일부 번호만 더 축소하는 변경을 막았다. 원래부터 있던 넘침·겹침은 일부 남는다. **새 LLM 판정은 실행하지 않았으므로 등급 개선을 뜻하지 않는다.** 이 결과는 이전 c4 실험과 같은 **CLI 측정·정렬 보정·외곽선**을 사용한 알고리즘 비교다. 화면의 새 DB 캐시 A 경로를 그대로 재생한 결과가 아니다. 화면 `DocRenderHost`는 원시 글자 경계를 반환하고 CLI `TightBoxRuntime`은 정렬 보정을 적용하므로 두 경로를 구분해야 한다. 캐시 어댑터는 화면 측정값과 같은 좌표 규약을 따른다. 별도 캐시 전수 조사에서는 컴포넌트 1,533개의 텍스트 11,975개 중 11,901개가 적중했고, 리소스 외곽선은 2,426개 키 중 2,290개가 호환됐다. CLI 정렬 보정을 맞춰 비교한 11,884개 텍스트 중 11,854개는 1px 이내였고 30개는 차이가 남았다. 이 값을 완전한 캐시·렌더 일치로 해석하지 않는다. 외곽선의 새 브라우저 계산과 캐시 변환을 비교한 6개 리소스에서는 외곽 면적 IoU가 0.99569~1.000이었다. 캐시 일부 누락의 보완 API가 실패해도 이미 얻은 외곽선은 유지한다.