Method 7 — 세로 스프레드
겹침 클러스터를 유휴 세로 구간(코리도) 안에서 최소 변위 정확해로 다시 쌓는 재배치 후보
§1한 줄 요약
텍스트 오버플로우가 없어도, 원안에 없던 겹침이 남아 있고 유휴 여백 비율이 기준(τ, 기본 0.5) 이상이면 — 겹친 유닛들을 세로 빈 구간(코리도) 안에서 placeRow(Abacus) 정확해로 다시 쌓아 겹침을 없앤다. method 4의 2단계 pushApart(대칭 절반씩 한 쌍만, 첫 실패에서 중단)를 비대칭 · N유닛 · 최소 변위 정확해로 일반화한 것이다.
§2왜 필요한가 — pushApart가 손을 드는 자리
현행 최선 합본이 남기는 FAIL은 대부분 신규 겹침이고(18건 중 15건), 그 겹침이 남는 대표 원인 둘은 docs/relayout_logics.md §9의 ⓔ와 ⓒ1이다.
| 원인 | 무엇이 막나 | method 7이 하는 일 |
|---|---|---|
| ⓔ 다른 유닛 · 같은 띠 | pushApart가 두 유닛을 정확히 절반씩 밀고, 밀린 자리가 이웃과 부딪히면 통째로 포기한다 — 실제로는 위쪽에 200px, 아래쪽에 40px 남은 비대칭 상황이 흔하다 | 클러스터 전체를 코리도 안에서 다시 앉힌다 — 얼마나 비대칭이든 placeRow가 가중 최소 변위로 배분 |
| ⓒ1 한 열에 쌓임 | 「최소 침투축이 가로」로 읽혀 method 4가 세로 밀기를 거부 — 그런데 그 가로 침투는 상자의 전체 폭이다(한쪽이 다른 쪽 열 안에 통째로 들어 있음) | §9의 판별식을 넣는다: 가로 침투 ≥ min(두 상자 폭)이면 가로는 분리축이 아니다 → 세로 분리 대상으로 승격 |
| 연쇄 겹침(3개 이상) | pushApart는 가장 깊은 한 쌍씩 풀고, 한 번 게이트에 걸리면 나머지도 포기한다 | 연결 성분(클러스터) 단위로 한 번에 푼다 — 3개짜리 스택도 한 번의 placeRow |
§3발동 조건 — 트리거의 일반화
발동 = base(원안) 존재
∧ 원안에 없던 겹침 유닛 쌍 ≥ 1 ← 오버플로우는 조건이 아니다
∧ idleMarginRatio ≥ τ ← Method7Options.idleThreshold, 기본 0.5
- 「오버플로우가 없어도」: method 1·3은 넘침이 없으면 no-op이고, method 4·5는 겹침에만 반응하되 공간 지식이 국소적이다. method 7은 겹침 + 유휴 비율만 본다.
- τ의 뜻: 페이지가 이만큼 비어 있으면 「공간을 크게 옮겨 앉혀도 어색하지 않다」는 정책 스위치다. 잔존 FAIL 18건 기준 τ=0.5는 15건, τ=0.4는 17건을 발동권에 넣는다(빽빽한 케이스 6577997은 idle 0.356). τ는 옵션으로 두고 평가에서 {0.4, 0.5, 0.65} 스윕한다.
- τ 미만이면 아무것도 하지 않는다 — 빽빽한 페이지에서의 대이동은 게이트에 걸릴 확률만 높다.
고정점 규율 — method 2의 전례(§“발동시킨 결함을 실제로 풀 때만 확정”)를 그대로 따른다: 클러스터 하나를 옮긴 결과가 그 클러스터의 신규 겹침을 실제로 줄이지 않으면 확정하지 않는다. 유휴 트리거로 발동했다고 유휴를 줄일 의무는 없다 — 목적은 겹침 해소이고, 평행이동은 유휴 비율을 바꾸지 못한다(§7 한계).
§4알고리즘 — 여섯 단계
0. 동결 freezeRelations(doc) · 신규 겹침 노드 쌍 → 유닛 쌍으로 승격 (method 4와 동일)
1. 분리성 판별 쌍마다: iy ≤ ix 이고 ix < min(두 상자 폭) 이면 가로 침투가 진짜 — 건너뜀(→ method 8)
그 외(iy가 최소이거나, ix ≥ min(폭) = 열 포함)는 세로 분리 대상
2. 클러스터 세로 분리 대상 쌍으로 이어진 유닛들의 연결 성분
3. 코리도 클러스터 합집합 박스의 x 구간과 겹치는 장애물(다른 leaf)을 위·아래로 스캔
→ [lo, hi]. 장애물 규칙은 idleSpace 모듈(설계안 B와 공용) §B-3
4. placeRow cells = 클러스터 유닛들 (ideal=지금 top, size=유닛 높이, weight=pinWeightOf)
placeRow(cells, lo, hi) → dy 만큼 translateNode (유닛은 통째로)
5. 게이트 method 4와 같은 셋 — ① 입력에 없던 쌍 0 · ② 원안 대비 신규 겹침 실제 감소
· ③ 넘침 불증. 하나라도 걸리면 그 클러스터만 되돌림 (클러스터 단위 all-or-nothing)
마무리 refitGroupBoxes(groupsFittingChildren(input))
설계 근거
- placeRow 재사용: 「겹치지 않으면서 가장 덜 움직이는」 1차원 배치의 닫힌 정확해가 이미 저장소에 있다(
relayout-core/placeRow.ts, Abacus/VPSC). 새 탐색 알고리즘을 만들지 않는다. - 이동 단위는 유닛: 위상 전제(「유닛은 통째로 움직인다」)를 건드리지 않는다. 띠 소속도 바꾸지 않는다 — 띠는 배치의 묶음이지 위상이 아니다(method 4와 같은 논리).
- 클러스터 단위 되돌림: 되돌림은 최소 단위로(§4.2). 문서 전체를 포기하지 않는다.
- 실현불가 처리: Σ유닛 높이 > 코리도 길이면
checkFeasible식으로 그 클러스터를 건너뛰고 리포트에 「몇 px 모자란가」를 남긴다 — 폭을 열어야 풀리는 케이스라는 신호다(5647291이 이 모양, §6).
§5파라미터 · 리포트
| 이름 | 기본 | 뜻 |
|---|---|---|
idleThreshold (τ) | 0.5 | 발동에 필요한 최소 유휴 여백 비율. 평가에서 스윕 후 조정 |
MIN_GAP | 0 | 클러스터 안 유닛 사이 최소 갭 — 음수 갭이 곧 겹침이므로 0이 진짜 하한(method 1·4와 동일) |
OVERLAP_EPS | 100px² | 겹침 판정 하한 — 지표(metrics.ts)와 같은 값을 봐야 고쳐도 숫자가 움직인다 |
리포트(Method7Report): 발동 여부(τ·겹침 유무), 클러스터 수, 옮긴 유닛 수, 게이트로 되돌린 클러스터 수, 실현불가 클러스터(모자란 px), 가로 침투라 넘긴 쌍 수(→ method 8 몫).
§6사정권 — 무엇을 풀고 무엇을 못 푸나
| 대상 | 판단 |
|---|---|
| ⓔ 같은 띠 안 비대칭 겹침 · ⓒ1 열 포함 스택 · 3+연쇄 | 직접 사정권 — 이 후보가 겨눈 전부 |
| ⓒ2 가로로만 파고든 쌍 | 1단계 판별식이 걸러 method 8(설계안 B)에 넘긴다 |
| ⓑ 같은 유닛 안 | 옮길 대상이 하나 — method 5의 몫, 손대지 않음 |
| ⓕ 그릇(프레임)·배경판이 낀 쌍 | 그릇은 freezeRelations가 유닛에서 빼는 대상 — 사정권 밖으로 명시 |
| 코리도보다 큰 클러스터 (예: 5647291 — 스택 764px vs 코리도 ~590px) | 실현불가로 남기고 리포트 — 폭 열기(method 2·3)와의 결합이 다음 수 |
천장을 미리 적는다 — 잔존 FAIL 18건 중 13건은 정답(expected) 제작 과정도 게이트를 못 넘겨 UNSOLVABLE로 판정된 케이스다. method 7의 1차 목표는 라벨 뒤집기(최대 4~5건)보다 신규 겹침 쌍·면적의 감소다 — 라벨이 그대로여도 쌍이 줄면 눈에 보이는 품질은 좋아진다.
§7일부러 안 하는 것
- 가로 이동 — §4.1(이웃을 옆으로 밀지 않는다)을 건드리지 않는다. 가로는 설계안 B(method 8)가 「빈 공간으로만」 규칙으로 따로 연다.
- 크기 변경 — 텍스트·컨테이너 크기는 한 톨도 안 바꾼다(예측 0, tight bounding box가 그대로 유효). 크기는 method 1·2·3의 일.
- 유휴 비율 낮추기 — 평행이동은 커버리지(union)를 못 바꾼다. ACCEPTABLE 29건(유휴 > 원안)은 이 후보의 목표가 아니다 — 그건 글리프 면적이 준 것이라 이동으로는 물리적으로 회복 불가.
- 원안 없는 케이스 — 「신규」를 정의할 수 없다. method 4·5·6과 같이 그대로 통과.
§8합본에서의 자리
method 3 · 1 ↔ 2 크기를 정한다
method 5 유닛 안 겹침
method 4 유닛 사이 겹침 (띠 쪼개기 + 짝 밀기)
method 7 ← 여기. 4가 남긴 겹침을 코리도 스프레드로
(method 8) ← 설계안 B. 7이 넘긴 가로 침투 쌍
method 6 그래도 안 담기면 폰트 축소 (마지막)
method 4 뒤인 이유: 4의 띠 쪼개기(1단계)가 싸게 푸는 것을 먼저 소진시키고, 남은 어려운 쌍만 코리도 계산을 치른다. method 6 앞인 이유: 글자를 줄이기 전에 자리로 풀 수 있는 마지막 기회여야 한다.
§9측정 계획
- 등록:
method7단독 +method3+…+method4+method7+method6합본. 러너·뷰어 드롭박스는 저절로 따라온다. - 본 지표: ⑤-b 신규 겹침 쌍·면적(주 지표), 판정 분포, 정렬선 보존율(대가), 잔존 FAIL 케이스 목록의 변화.
- τ 스윕 {0.4, 0.5, 0.65} — 발동 커버리지와 게이트 되돌림 비율의 트레이드오프 확인.
§10구현 노트 (as-built, 2026-08-27)
구현 과정에서 설계가 네 군데 진화했다. 코드가 정본이다(relayout-methods/method7.ts).
- 분리축 판별은 유닛 박스로. 노드 박스로 재면 나란히 선 두 카드(6672017)가 글자 상자끼리 얕게 스치며 세로 몫으로 잘못 분류된다 — 움직이는 단위가 유닛이므로 판별도 유닛 박스가 정본.
- 스택 해법 + 짝 나누기의 2층 구조. 클러스터 전원이 x로 겹치는 「한 열」일 때만 placeRow 스택 해법을 쓰고(코리도는 합집합이 아니라 x 교집합 밴드로 — 합집합은 무관한 옆 콘텐츠를 장애물로 끌어들인다, 6377108 실측), 그 외·코리도 부족 클러스터는 비대칭 짝 나누기로 푼다: 위 유닛은 위 여유만큼, 아래 유닛은 아래 여유만큼, 여유 총합이 침투보다 작으면 되는 데까지만(부분 이동 — 쌍은 남아도 면적은 준다).
- 게이트가 넷이다. method 4의 셋에 「어떤 쌍도 자기 입력보다 커지면 안 된다」를 더했다 — 한 쌍을 떼느라 다른 쌍을 키우는 이동이 총량 게이트에 묻혀 통과하는 구멍을 막는다.
- 채점과 같은 자(judge). 케이스의 실측 tight(rendered + 배율 맞춘 원안)를
replaceLayout이 후보까지 배관해, 트리거·게이트가 성적표와 같은 판정 박스(carryTightBoxes 승계)를 본다. 프레임 대용만 쓰면 유령 겹침을 쫓다 실제 신규 겹침을 만든다(5629794 실측 — SUCCESS→FAIL 회귀). 합본에는 마지막 방어로betterOf가드가 있다: method 6의 나비효과까지 포함한 완성본끼리 견줘 나은 쪽만 남긴다(같으면 기존 경로).