post-replace-relayout · idle-area 설계

Method 8 — 유휴 여백 지도와 가로 탈출

「어디가 비었나」를 답하는 공용 모듈(idleSpace)과, 그 지도를 근거로 겹침을 빈 공간으로 옮겨 푸는 재배치 후보

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

§1한 줄 요약

지표 ④ idleMarginRatio는 「여백이 얼마나 남았나」만 답하는 페이지 전역 스칼라다 — 「어디에 남았나」를 답하는 코드는 저장소에 없다(docs/architecture.md §9.5). 설계안 B는 그 공백을 메우는 두 조각이다: relayout-core/idleSpace.ts — 상자 하나의 4방향 여유(slack)와 세로 코리도를 재는 순수 조회 모듈(method 7과 공용), method8 — 그 지도를 근거로, 세로로는 못 푸는 겹침을 가로 빈 공간으로 옮겨 푸는 후보.

§2왜 필요한가 — 지표와 액추에이터의 해상도가 다르다

지금 있는 「여유」 지식범위못 답하는 것
metrics.ts idleMarginRatio페이지 전역 스칼라 1개어디가 비었나
neighbors.ts limitsOf좌·우 이웃이 정하는 한계선 (가로 · 체인 안)위·아래, 이웃 너머, 체인 밖
gapAlgebra.ts chainBudget한 체인 [from,to]의 1차원 갭 합체인에 안 묶인 방향의 여유

그 결과가 §4.1의 전면 금지다 — 「이웃을 옆으로 밀지 않는다」. 옳은 규칙이지만, 현행 구현은 「이웃을 민다」와 「빈 공간으로 간다」를 구분하지 못해 후자까지 막고 있다. 잔존 FAIL의 ⓒ2(가로로만 파고든 겹침 14쌍, §9 기준)가 정확히 그 자리다.

directionalSlack(box) — 4방향 여유 질의 페이지 (또는 담는 프레임) 이웃 leaf 이웃 leaf 장식(LineItem) — 장애물로 안 센다 유닛 left 144 right 84 up 80 down 68
각 방향의 slack은 「교차 축 구간이 겹치는 장애물 중 가장 가까운 변까지의 거리」다. 위쪽 이웃(x 구간이 안 겹침)은 up을 막지 못하고, 장식은 장애물에서 빠진다.

§3relayout-core/idleSpace.ts — 공용 조회 모듈

type Slack = { left: number; right: number; up: number; down: number };

obstaclesFor(doc, abs, opts?: { excludeIds?: Set<string> }): AbsBox[]
directionalSlack(box: AbsBox, obstacles: AbsBox[], bounds: AbsBox): Slack
verticalCorridor(box: AbsBox, obstacles: AbsBox[], bounds: AbsBox): { lo: number; hi: number }

장애물 규칙 — 지표·위상과 같은 것을 본다

왜 비트맵이 아니라 스윕인가 — ④의 4px 격자 비트맵을 재활용하는 안도 검토했다. 격자는 「전체 점유율」에는 맞지만 방향 질의에는 O(격자) 스캔 + 셀 경계 오차가 붙는다. 장애물이 수십 개 수준인 문서에서는 상자 스윕이 더 단순하고 정확하다(limitsOf가 이미 같은 방식이고, 이 모듈은 그 4방향 일반화다).

§4method8 — 가로 탈출

0. 동결 · 쌍 검출   method 4·7과 동일 (원안에 없던 겹침 → 유닛 쌍)
1. 대상 고르기      가로 침투가 진짜인 쌍만: ix < iy 이고 ix < min(두 상자 폭)
                   (세로로 풀리는 쌍은 method 4·7의 몫 — 축으로 분업한다)
2. 이동 후보 열거    쌍 {A,B}에 대해 4가지: A←, A→, B←, B→ (필요량 δ = ix + ε)
                   + 양쪽 분담(A← δa, B→ δb, δa+δb = δ) — slack 비례 배분
3. 지도 필터        directionalSlack ≥ 필요량 인 후보만 남긴다 → 총 변위가 작은 순으로 정렬
4. 시도 · 게이트    후보를 앞에서부터 시도 — 게이트는 method 4와 같은 셋
                   (입력에 없던 쌍 0 · 원안 대비 신규 겹침 실제 감소 · 넘침 불증)
                   통과한 첫 후보로 확정, 전부 실패하면 그 쌍은 그대로 (쌍 단위 all-or-nothing)
5. 반복             남은 쌍에 대해 반복 (MAX_MOVES 안전핀, method 4의 MAX_PUSHES와 같은 급)
   마무리           refitGroupBoxes
before — 가로 침투 ix ix=30 빈 공간 after — 빈 공간으로 δ=ix+ε 이동 δ 이웃을 민 것이 아니라 빈 자리로 갔다 — §4.1 위반이 아니다
method 8이 여는 것은 「가로 이동」이지 「가로로 밀기」가 아니다. slack이 보장하는 빈 공간으로만 가고, 진실은 게이트가 재확인한다.

§4.1(이웃을 옆으로 밀지 않는다)과의 관계 — 규칙을 고치지 않고 지킨다

§4.1이 금지한 것은 이웃을 밀어내는 것이다. method 8은 이웃을 한 픽셀도 건드리지 않는다 — 겹침 당사자 유닛만, slack이 보장하는 빈 자리로 옮긴다. 이동 후보가 이웃과 부딪히면 지도 필터(3단계)가 먼저 거르고, 혹시 지도가 놓쳐도 게이트(4단계)가 되돌린다. 이중 방어다.

§5파라미터 · 리포트

이름기본
idleThreshold (τ)0.5method 7과 같은 정책 스위치 — 빽빽한 페이지에서의 가로 대이동을 막는다
MAX_MOVES20한 문서에서 옮겨 보는 최대 횟수(method 4의 MAX_PUSHES와 같은 급의 안전핀)
EDGE_MARGIN0페이지·프레임 경계에 붙이는 여유 — 0이면 경계까지 허용(⑦ 잘림은 게이트·경계가 이미 막는다)

리포트(Method8Report): 대상 쌍 수, 옮긴 쌍 수(방향별), slack 부족으로 못 옮긴 쌍, 게이트로 되돌린 쌍, 남은 쌍.

§6대가와 한계 — 미리 적는다

천장 — method 7과 같은 사정: 잔존 FAIL 18건 중 13건은 정답 제작도 못 넘긴 UNSOLVABLE이다. method 8의 주 지표도 라벨보다 ⑤-b 쌍·면적 감소다.

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

§7합본에서의 자리와 측정 계획

… method 5 → method 4 → method 7(세로 스프레드) → method 8(가로 탈출) → method 6