post-replace-relayout · idle-area 설계

Method 7 — 세로 스프레드

겹침 클러스터를 유휴 세로 구간(코리도) 안에서 최소 변위 정확해로 다시 쌓는 재배치 후보

상세 설계안 A · 구현 대상 · 2026-08-27 · feat/post-replace-relayout-idle-area

§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
pushApart (method 4 · 2단계) 고정 이웃 −9px +9px 아래가 이웃과 충돌 → 되돌림 method 7 (코리도 안 placeRow) 고정 이웃 코리도 상한 코리도 하한 위쪽 빈 구간으로 비대칭 배분 → 겹침 0
같은 겹침이라도 남은 여유가 비대칭이면 절반씩 밀기는 실패한다. method 7은 클러스터가 쓸 수 있는 세로 코리도를 먼저 구하고, 그 안에서 최소 변위 배치를 정확히 푼다.

§3발동 조건 — 트리거의 일반화

발동 = base(원안) 존재
     ∧ 원안에 없던 겹침 유닛 쌍 ≥ 1        ← 오버플로우는 조건이 아니다
     ∧ idleMarginRatio ≥ τ                ← Method7Options.idleThreshold, 기본 0.5

고정점 규율 — 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))

설계 근거

§5파라미터 · 리포트

이름기본
idleThreshold (τ)0.5발동에 필요한 최소 유휴 여백 비율. 평가에서 스윕 후 조정
MIN_GAP0클러스터 안 유닛 사이 최소 갭 — 음수 갭이 곧 겹침이므로 0이 진짜 하한(method 1·4와 동일)
OVERLAP_EPS100px²겹침 판정 하한 — 지표(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일부러 안 하는 것

§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측정 계획

§10구현 노트 (as-built, 2026-08-27)

구현 과정에서 설계가 네 군데 진화했다. 코드가 정본이다(relayout-methods/method7.ts).