내용 대치가 끝난 컴포넌트를 넘침·겹침·유휴 여백이 없도록 다시 배치한다. 연산자가 여섯 개(method 1~6)이고, 축이 다른 것끼리 정해진 순서로 조립된다. 그 순서가 왜 그 순서인지, 각 연산자가 무엇을 고정하고 무엇을 푸는지를 코드에서 정리했다.
3 → 1 → 2 → 5 → 4 → 6. 크기를 먼저 정하고, 겹침을 안에서 밖으로 풀고, 글자 줄이기는 맨 마지막.3+1+2에서 끝나고 겹침은 +5+4가 푼다.
miricanvas-iui 브랜치 feat/post-replace-relayout 의
작업트리(2026-08-26)다. method6.ts 는 아직 커밋되지 않았고
나머지도 대부분 수정 중이라, GitHub 링크 대신 편집기 링크를 건다(§9).
숫자와 코드가 다르면 코드가 맞다.
후보 하나 = 파일 하나다. 서로 접근 방식이 다른 알고리즘을 나란히 놓고 고르려는 것이므로, 한 파일에 분기를 몰아넣지 않는다. 합본도 후보 하나로 친다 — 「따로 쓸 때와 합쳐 쓸 때 중 무엇이 나은가」에 답하려면 둘 다 같은 케이스에 태워 각자 채점해야 한다.
후보는 sheetJson → sheetJson 순수 함수다. 그래서 굽는 단계가
없다 — 케이스 폴더에 rendered.json 하나만 있으면 뷰어가 브라우저에서 직접
불러 전 후보를 화면에 띄우고, 후보 코드를 고치면 HMR로 즉시 반영된다.
cases/<id>/rendered.json ──▶ replaceLayout(doc, key, original) ──▶ After 장
▲ 뷰어가 브라우저에서 직접 부른다
// original 은 채점 기준으로만 넘긴다 — 답을 보고 푸는 것이 아니다.
// 좌표계를 맞춘 사본(alignDocFrame)을 받아 method 4·5·6 이 「원안에 없던 겹침」을 정의한다.
| key | 한 줄 | 줄 |
|---|---|---|
| method1 | 세로 공간 재분배 (폭 고정 · 예측 0) | 644 |
| method2 | 가로 여유로 줄 수 줄이기 (최소구성 + slack · 예측 포함) | 1,032 |
| method3 | 가로 넘침 해소 (도형을 좌우로 · 안 되면 텍스트를 좁혀 줄바꿈) | 684 |
| method4 | 대치가 만든 겹침 해소 (세로로 떼어 놓는다 · 크기 고정 · 예측 0) | 554 |
| method5 | 유닛 안의 겹침 해소 (가로로 열고, 안 되면 안에서 다시 쌓는다) | 572 |
| method6 | 가독성 하한을 지키는 폰트 축소 (원안 대비 · 페이지 대비 둘 중 더 엄한 쪽) | 377 |
| method1+method2 | 두 후보를 고정점까지 번갈아 — 세로로 풀고 남은 것만 가로로 | 259 |
| method3+1+2 | 가로 넘침을 먼저 없애고, 그때 생긴 세로를 기존 합본이 담는다 | 83 |
| method3+1+2+4 | 크기를 다 정한 뒤 대치가 만든 겹침을 세로로 떼어 놓는다 | 84 |
| method3+1+2+5+4 | 유닛 안의 겹침을 먼저 풀고, 남은 겹침을 유닛끼리 떼어 놓는다 | 93 |
| method3+1+2+5+4+6 전체 | 자리를 다 써 보고 그래도 안 담기면 가독성 하한까지 글자를 줄인다 | 78 |
합본이 짧은 것은 새 연산을 안 만들기 때문이다 — 앞 후보들을 순서대로 부르고 게이트만 지킨다.
relayout-core 에도 ALGORITHMS 라는 후보 목록이 있다.
계약이 다르다 — 그쪽은 run(doc, ctx): void 로 제자리 수정,
여기는 run(doc): Doc 순수 함수다. 그쪽은 후보가 아니라 대조군
(optimizeLayout)이고 CLI 에서만 돈다.
이름도 일부러 다르다 — 렌더러(엔진)도 iframe 안에서 재배치를 하고 대치 파이프라인에
tidyLayout 도 있어서, 이름이 같으면 「재배치가 됐다」가 무엇을 가리키는지
늘 되물어야 한다. 그래서 이 디렉터리가 만드는 것만 replaceLayout 이다.
여섯이 경쟁하는 관계가 아니다. 손대는 축이 서로 다르고, 무엇을 고정하는지가 곧 정체성이다. 아래 지도가 이 문서의 전부다 — 가로는 무엇을 건드리나, 세로는 줄바꿈 예측이 들어가나.
autoHeight).
아래는 폭을 바꿔서 「몇 px 넓히면 3줄이 2줄이 되나」를 렌더 전에 답해야 하는 쪽이다.
순서에 원칙이 셋 있다. ① 정확히 풀 수 있는 것을 먼저, ② 크기를 먼저 정하고 겹침을 나중에, ③ 되돌릴 수 없는 것을 맨 뒤에.
method 3 → 1 → 2 안에서도 같은 원칙이 반복된다 — 예측 0인 3①과 1이
앞이고, 줄바꿈 예측이 필요한 3②·2가 뒤다. 순서를 뒤집으면 세로로 풀렸을 케이스에까지
예측이 섞여 왜 그런지 가를 수 없게 된다.
한 줄짜리 텍스트 박스는 넘칠 때 줄바꿈하지 않고 옆으로 늘어난다.
그건 세로 문제가 아니라 method 1 이 아예 안 돈다(세로 넘침이 없으면 그대로 반환).
실측 6447600: 503px → 1399px, 높이는 57.8px 그대로. 카드 폭은 1131px — 268·375px 이 카드 밖으로 나간다. 세 후보 다 60,671px² 를 그대로 뒀다.
구멍은 순서가 아니라 연산자였다. 가로 넘침을 없애는 수가 하나도 없었다.
method 4 가 다루는 단위는 유닛(배경 도형 + 그 도형이 담은 텍스트)이다.
유닛 안에서 글자끼리 포개진 것은 유닛을 어디로 보내도 안 풀린다 — 옮길 대상이 하나다.
그래서 안을 먼저 풀어 유닛의 크기를 확정하고, 그 다음에 유닛끼리 떼어 놓는다. §4
앞의 다섯은 자리와 크기를 다시 나누는 후보다. 그것으로 풀리면 원안 글자 크기가 안 깎이고 판정이 SUCCESS 가 될 수 있다.
반면 method 6 이 손대는 순간 지표 ②(최소 폰트 배율)가 1 아래로 내려가고,
판정 규칙상 SUCCESS 는 영영 못 된다. 최선이 ACCEPTABLE 이다.
실측도 그 순서를 지지한다 — 넘침으로 FAIL 이던 5699884·6535388 은 0.95 한 칸이면 풀린다.
유닛 = 함께 움직이는 최소 단위다 — 배경 도형 하나 + 그 도형이 담은 텍스트 전부. 텍스트만 옮기면 「텍스트는 자기 배경 도형 밖으로 벗어나지 않는다」가 깨지고, 도형만 키우면 글자가 따라오지 않는다. 그래서 둘을 묶어 다닌다.
method 2 의 발동 조건을 한 칸 넓혔다).
그릇을 「그 칼럼이 지금 차지한 구간」으로 잡는 것이 핵심이다. 첫 글의 위 변과 끝 글의 아래 변이 제자리에 남으므로 정렬선(지표 ③)이 안 깎이고 도형 안쪽 여백도 그대로다. 쓸 재료는 글자 사이에 이미 있던 여백뿐이다.
칼럼을 「가로로 겹치는 것끼리」 묶는 것도 규칙이다 — 나란한 셋을 세로로 쌓으면 안 된다 (6535640 실측: 같은 y 에 놓인 텍스트 셋).
폭을 여는 순간 「폭 고정 → 줄바꿈 고정 → 높이 고정 → 예측 0」이 무너진다.
「폭을 몇 px 넓히면 3줄이 2줄이 되나」는 다시 렌더하기 전에 답해야 하므로 예측이다.
그래서 method 2 는 강력하지만 위험하고, 안전장치 넷을 전부 달고 있다.
폭을 WRAP_SAFETY(12%) 좁다고 보고 센다. 글자폭 계수의 오차와 letterSpacing 을
세지 않는 것을 덮는 값이다.
정답 빌더와 같은 상수를 본다 — 셈법이 다르면 후보가 「줄었다」한 폭을 채점이 「그대로」로 본다.
후보의 높이 h_k 가 문서에 박힌 지금 높이보다 작아야 받는다.
모델이 줄 수를 과대평가했으면 h_k 가 지금 높이와 같아져 저절로 거부된다 —
예측 오차가 「아무것도 안 함」으로 떨어진다.
노드 하나를 넓힐 때마다 재고, 새 쌍이 생기면 그 노드만 되돌린다. 체인을 통째로 되돌리지 않는다.
5698327 은 도형 8개를 키워 넘침을 다 없애 놓고 새 겹침 한 쌍 때문에 여덟 개가 전부 날아갔었고, 6134703 은 696px² 짜리 겹침이 5,509px² 짜리 수정을 날렸다.
견주는 기준은 자기 입력 문서다 — 정답은 생성 단계에서 읽지 않는다.
거부되면 다음(더 좁은) 후보로 물러난다. 게이트가 거부할 때는 렌더 없이 예측 안에서, 렌더가 거부할 때는 실측으로.
replaceLayoutVerified 는 라운드 경계마다 실제로 렌더한다 — 두 번째 라운드의
method 1 이 보는 높이가 예측값이 아니라 렌더러가 확정한 값이 된다.
헤드리스 크롬이 필요해 node 에서만 돈다.
hug 는 넘친 글자를 담는 일이라, 글자가 이미 이웃을 침범해 있었다면 그 침범을 도형이 물려받는다. 쌍의 이름만 바뀐 것을 「신규」로 세면 5628689 처럼 48.2px 짜리 넘침을 못 고친 채 7px 짜리 겹침을 피하게 된다. 승계에는 깊이 상한이 붙는다.
재배치가 되돌리려는 상태가 원안(original.json) 이기 때문이다 —
디자이너가 정한 글자 크기·정렬선·「의도된 겹침」이 거기 있고, 대치가 무너뜨린 그것을
다시 세우는 일이 재배치다.
답을 보고 푸는 것이 아니다 — 후보의 입력은 언제까지나 rendered.json
하나뿐이고, 원안은 채점 기준으로만 본다.
| 지표 | 방향 |
|---|---|
| ① 넘침 노드 수 / 넘침 면적(px²) | 낮을수록 좋음 |
| ② 최소 폰트 배율 원안 대비 | 높을수록 좋음 |
| ③ 정렬선(가이드) 보존율 원안 대비 | 높을수록 좋음 |
| ④ 유휴 여백 비율 | 낮을수록 좋음 |
| ⑤ 신규 겹침 쌍 / 면적 — 원안에 없던 겹침 | 0 이어야 함 (PASS/FAIL) |
| ⑥ 좁은 칼럼 노드 수 — 두 줄 이상 + 줄당 3자 미만 | 낮을수록 좋음 판정엔 미포함 |
| 라벨 | 조건 |
|---|---|
| FAIL | ① 넘침 노드 > 원안 · ⑤ 신규 겹침 있음 · ② 폰트 배율 < 0.7 — 셋 중 하나라도 |
| SUCCESS | FAIL 아님 + ② 폰트 배율 = 1.0 + ④ 유휴 여백 ≤ 원안 |
| ACCEPTABLE | FAIL 아님 + SUCCESS 아님 |
디자이너가 일부러 넣은 오버행을 벌주지 않기 위해서다. 119건에서 원안 넘침 노드가 45개 — 절대 0으로 재던 때는 그 케이스들이 손도 못 대고 FAIL 이었다.
같은 컴포넌트를 다른 자리·배율로 앉힌 문서다(배율 = fontSizeScale,
커밋된 케이스에서 0.97~1.91).
절대 좌표를 그냥 견주면 겹침 면적이 배율²로 커져 의도된 겹침이 전부 「신규」로 잡힌다.
alignDocFrame 이 원안을 렌더본과 같은 자리·배율로 옮긴다.
오토핏을 켜면 못 담을 글이 넘침이 아니라 축소로 박힌다 — 5628689 실측: 원안 38.7px → 27.1px(70%)에 넘침 노드 0.
그 문서를 넣으면 후보가 고칠 것을 못 찾는다. 끄는 레버는 렌더 메시지의
shrinkConfigs 하나다 — ThumbnailLoad.fontScaleRange 는
배포된 렌더러가 읽지 않는다(값만 바꿔 네 번 렌더해도 한 픽셀도 안 달라졌다).
커밋된 케이스 111건 합계, 채점 기준 = 원안, 2026-08-26 실측. 두 측정치는 다른 문제를 재므로 눈금을 따로 둔다 — 왼쪽은 넘침, 오른쪽은 겹침 게이트다.
+4·+5+4 세 칸의 넘침이
한 자리도 다르지 않은 것은 「넘침이 늘면 되돌린다」를 게이트로 걸었기 때문이다.
마지막 +6 만 성격이 달라 — 글자를 줄이므로 넘침까지 함께 지운다.
3+1+2 가 남긴 9건(534,454px² · 14노드)은 도형을 키울 자리가 없는
케이스다. 좌우로는 이웃이, 세로로는 겹침 게이트가 막아 min-content 까지 좁혀도
글이 그릇을 넘는다. 6780937 「VS」 배지는 지름 107px 원에 7줄이 들어갔다.
정답지는 폰트를 줄여 풀지만 그 길은 후보에 없다 — 「재배치로 못 고치는 것」으로
남겨 둔 자리이고, 케이스 id 는 method3Method1Method2.test.ts 가 못박는다.
⚠️ 그 0 들은 예측 위의 0 이다. method 3 의 축소 단계가 쓰는 줄 수·높이는
렌더가 아니라 모델값이다 — 실제 렌더에서 줄이 한 줄 더 흐르면 그만큼 다시 넘친다.
합본이 짧은 것처럼, 후보들도 대부분 순서를 정하는 일만 한다.
실제 계산은 relayout-core 의 순수 함수들이 하고, 축만 바꿔 재사용된다 —
method 1 이 세로에 쓰는 placeRow 를 method 2 가 가로에 그대로 쓴다.
| 모듈 | 무엇 | 줄 |
|---|---|---|
| topology.ts | 위상 동결 — 무엇이 함께 움직이고 무엇이 무엇을 담고 있나. 좌표를 건드리기 전에 한 번 정해 굳힌다(열면 조합 최적화가 되어 다항시간에 못 푼다) | 478 |
| measure.ts | 실측 — 문서에 이미 박힌 것을 읽는다. 예측이 아니라는 경계를 이름으로 못박았다 | 174 |
| pairing.ts | 텍스트 ↔ 배경 도형 짝짓기. 재배치의 최소 단위는 텍스트 하나가 아니라 「텍스트와 그것을 담은 도형」 한 쌍 | 51 |
| gapAlgebra.ts | 여백 대수 — 남은 공간을 갭에 어떻게 나눌지를 닫힌 형식으로. 축과 무관한 순수 숫자 함수 | 135 |
| placeRow.ts | Abacus PlaceRow — 한 축에서 「겹치지 않으면서 가장 덜 움직이는」 배치를 정확히 푼다 | 137 |
| neighbors.ts | 가로 slack — 「이 박스가 좌우로 어디까지 커질 수 있나」. 가로는 이웃을 밀어내지 않는다 | 148 |
| feasibility.ts | 실현가능성 — 배치를 시도하기 전에 「담을 수 있나」를 답한다. 못 담으면 담긴 척하는 결과 대신 신호가 나온다 | 49 |
| gate.ts | 신규 겹침 게이트의 정본. 후보가 무엇을 하든 이 판정을 통과해야 한다. 승계 규칙도 여기 | 182 |
| geometry.ts | 노드를 실제로 옮기고 키우는 곳. 재배치가 문서를 건드리는 지점은 여기뿐 — boundingBox 가 부모 상대 좌표라 헬퍼를 거쳐야 한다 | 411 |
| textOverflow.ts | 넘침 판정 — 이 feature 전용. comp-sign 의 예측 모델은 fontSizeScale 을 곱하지 않아 여기 쓸 수 없다 | 655 |
| visualBox.ts | visual bounding box — 글자 + 그 글자에 딸린 장식(글머리 기호 띠)까지. tight 만 쓰면 기호가 도형 밖에 나가도 넘침 0으로 적힌다 | 148 |
| docFrame.ts | 좌표계 맞추기 — 원안을 렌더본과 같은 자리·배율로 옮긴다. 배율의 정본은 text.fontSizeScale | 146 |
| renderProbe.ts | 렌더 재검증 — 예측으로 바꿔 놓은 줄바꿈을 실제로 렌더해 대본다 | 301 |
| metrics.ts | 정량 지표 일곱. absBoxMap 의 좌표계 함정 주석이 여기 있다 | 1,193 |
| verdict.ts | 케이스 하나를 한 낱말로 — SUCCESS · ACCEPTABLE · FAIL | 232 |
여백 대수는 「남은 공간을 갭에 어떻게 나눌까」를 묻고 목표 좌표를 낸다.
PlaceRow 는 「겹치지 않으면서 가장 덜 움직이는 배치는?」을 묻고 확정 좌표를 낸다.
둘 다 축과 무관한 순수 함수라, 세로 파스와 가로 파스가 같은 코드를 쓴다.
경로는 miricanvas-iui/src/post-replace-relayout/ 기준이다.
편집기 를 누르면 VS Code 로 열린다 — 작업트리 코드라 GitHub 링크는 걸지 않았다.
| 파일 | 무엇 | 편집기 |
|---|---|---|
| relayout-methods/index.ts | 레지스트리 11개 · replaceLayout · replaceLayoutVerified |
편집기 |
| relayout-methods/types.ts | RelayoutMethod 계약 — 후보와 레지스트리가 함께 보는 커널 |
편집기 |
| relayout-methods/method1.ts | 세로 재분배 · hugVertical · 다섯 단계 |
편집기 |
| relayout-methods/method2.ts | 가로 확장 · 최소구성 w_k · slack · 안전장치 넷 |
편집기 |
| relayout-methods/method3.ts | 가로 넘침 — ① hug(예측 0) · ② 축소(예측) | 편집기 |
| relayout-methods/method4.ts | 유닛 사이 겹침 · 띠 쪼개기 + pushApart |
편집기 |
| relayout-methods/method5.ts | 유닛 안 겹침 · 세 수(가로 → 다시 쌓기 → 넘기기) | 편집기 |
| relayout-methods/method6.ts 미커밋 | 폰트 축소 · 하한 둘 · 사다리 · 되부름 | 편집기 |
| …Method5Method4Method6.ts | 전체 파이프라인 — 78줄. 되부름 콜백을 여기서 꽂는다 | 편집기 |
| relayout-core/ | 공용 커널 15개 모듈 — §8 | 편집기 |
| README.md | 실행법 · 실측표 · 뷰어 설명. 표에 날짜가 박혀 있다 | 편집기 |
| docs/before-after-rules.md | 무엇을 만들어야 하는가 — §2 불변조건 · §4 알고리즘 · §8 Expected 만들기 | 편집기 |
| docs/evaluation-rules.md | 어떻게 그리고 채점하는가 — §2 지표 · §3 판정 라벨 | 편집기 |
| docs/relayout_logics.md | 후보별 정본 | 편집기 |
# 뷰어 — 케이스마다 네 장(원안 · 대치후 · 재배치 · 정답)을 겹쳐 본다 https://local.miricanvas.com:3000/post-replace-relayout # 지표 표를 터미널에 — 위 §7 의 표를 다시 재는 법 pnpm exec tsx scripts/relayout-metrics.ts # 한 후보만 전 케이스에 돌려 게이트를 본다 (--write 로 내보내기) pnpm exec tsx scripts/relayout-method-run.ts