아카이브 · 당시 기준

2026-09-13 원본에서 평가 계약·구현·코퍼스가 달라진 것을 확인해 당시 기록으로 보관했습니다. 본문과 자동 동기화 표는 보관 당시 내용입니다.

현재 기준과 원본 보기 → · 보관 목록

← 미리디 아카이브 / 내용 대치 후 요소 재배치

내용시각화 post-replace-relayout v0.6 · 아카이브 작성 2026-08-28

재배치 알고리즘 파이프라인 v0.6

아카이브 — 2026-08-28의 구현과 측정 결과

작성 2026-08-28 · 브랜치 feat/post-replace-relayout-2 · 커밋 2aed14ada · 코퍼스가 1,177건이던 시점의 기록이다. 기술 내용과 수치는 당시 기준으로 보존하고, 문장은 2026-08-30에 다듬었다.

§5에서 다룬 중복 구현 중 일부는 이후 정리됐다. 짝 밀기를 method 7로 통합했으며(bad3810f2), 서로 다른 의미로 쓰이던 상수 이름을 구분했다(9a4f52fb2). 현재판은 재배치 알고리즘 파이프라인 v0.7 이다.

내용 대치가 끝난 컴포넌트의 넘침과 겹침을 줄이고, 남은 여백을 활용해 다시 배치한다. v0.6의 연산자는 8개이며, 새로 추가된 method 7·8은 페이지의 빈 공간을 활용해 요소를 이동한다. 이 문서는 실행 흐름과 각 연산자의 역할, 중복 구현, 실행 비용, 후보 비교 방식을 정리한다.

miricanvas-iui · src/post-replace-relayout 브랜치 feat/post-replace-relayout-2 후보 16개 (단독 8 · 합성 후보 8) 코퍼스 1,177건
§0

핵심 요약

  • 7과 8은?서로 다른 방향의 겹침을 처리한다. 둘은 penetrationOf 라는 같은 판별식을 사용한다. 동일한 좌표에서 판별하면 세로 처리 대상과 가로 처리 대상이 구분된다. 각 단계는 실행 시점의 좌표로 다시 판별한다. §4
  • 겹치는 책임있다. method 4 의 2단계(대칭 절반 밀기)와 method 7 의 2-b(비대칭 짝 밀기)는 같은 층·같은 축·같은 게이트다 — method 7은 이동 여유에 따라 위·아래 이동량을 조절한다. 두 구현을 통합할 때는 서로 다른 τ 발동 조건도 함께 고려해야 한다. §5
  • 단순화 여지penetrationOf 가 두 파일에 동일하게 중복 구현돼 있고 (두 구현이 일치해야 처리 대상을 같은 기준으로 구분할 수 있다), OVERLAP_EPS 는 같은 이름으로 1 과 100 두 뜻을 쓴다. §5
  • 복잡도당시 코퍼스의 leaf 노드는 중앙값 12 · 최대 60이었다. 겹침 비교 한 번의 규모보다 반복 횟수가 중요하다. method 6은 축소 배율마다 앞의 다섯 연산자를 재실행하며, 회귀 방지 비교가 필요하면 두 경로를 계산한다. §6.1
  • 공간문서 한 개의 JSON 크기는 중앙값 39.1KB이며, 합성 후보의 마지막 단계에서는 문서 사본 여러 개를 동시에 참조한다. 시도마다 사본을 생성하므로 반복 복제 비용도 고려해야 한다. §6.2
  • 호출 빈도와 실행 비용1,177건 전건 실행 실측 — 최악 복잡도가 가장 나쁜 method 5 는 합성 후보에서 16.0% 발동 · 8.9% 조치로 드물게 실행된다. method 1↔2 는 100% 발동, method 6 은 43.2%, betterOf 는 9.7%. §6.3
  • 선택기는?결정론적 후보 선택기는 아직 코드에 없다(설계만). 구현된 것은 betterOf 이고, 이는 여백 활용 단계를 포함한 합성 후보 3종에서 두 실행 경로를 비교한다. 설계안 C 는 후보 전부가 대상이라 범위가 다르다. §7

이 글의 근거

브랜치 feat/post-replace-relayout-2 · 커밋 2aed14ada 의 당시 작업트리를 기준으로 작성했다(작성 시점에는 인용 파일의 커밋과 작업트리가 일치했다). 실측은 aee07d0b 에서 돌렸고, 이후 커밋이 추가됐지만 인용한 후보 파일은 문서 작성 시점까지 변경되지 않았다 — 바뀐 것은 metrics.ts(+59줄) · relayout-core/index.ts · 문서, 그리고 새 파일 separateOverlaps.ts 다(§5 에 반영). 인용한 15개 줄 번호는 당시 HEAD에서 다시 확인했다.

인용 커밋은 당시 원격에 없었다. 따라서 기존 코드 링크는 당시 작업트리의 vscode://file/… 주소를 보존했다. 해당 작업트리가 없거나 파일이 바뀌면 링크와 줄 번호가 맞지 않을 수 있다. 최신 코드 링크는 v0.7을 참고한다.

이전판은 v0.5 (2026-08-26 · 연산자 여섯 · 실측 111건) 이다.

§1

파이프라인 상세 흐름도

대표 합성 후보 method 3 + 1 + 2 + 5 + 4 + 7 + 8 + 6 이 한 문서를 어떻게 통과시키는지, 분기, 되돌림, 재호출 과정까지 보여 준다. 왼쪽에는 단계별 검사 조건과 되돌림 단위를, 오른쪽에는 반복 상한을 표시했다.

게이트 · 되돌림 단위 파이프라인 반복 상한 rendered.json  — 후보의 유일한 입력 freezeRelations — 위상 동결 유닛 · 띠 · 체인 · 프레임의 관계를 정한다 이후 재계산 없음 method 3 · 가로 넘침 해소 도형 확장 → 불가능하면 텍스트 폭 조정 ① 없던 쌍 되돌림 = 노드 method 1 세로 재분배 method 2 가로 확장 폭이 바뀌면 높이가 바뀐다 — 변화가 멈출 때까지 반복 ① 없던 쌍 되돌림 = 노드 MAX_ROUNDS = 3 method 5 · 유닛 안의 겹침 폭 확장 또는 도형 내부 재배치 ①②③ 되돌림 = 유닛 method 4 · 유닛 사이의 겹침 1단계 띠 쪼개기 → 2단계 pushApart 겹침 깊이의 절반씩 이동 — 여유 확인은 이동 후 ①②③ 되돌림 = 체인·쌍 MAX_PUSHES = 20 base 있음 ∧ 신규 겹침 ≥ 1쌍 ∧ idleMarginRatio ≥ τ (0.5) 넘침은 조건이 아니다 — v0.5 와의 핵심 차이 아니오 7·8 을 건너뛴다 (빽빽한 페이지) 예 쌍 분류  penetrationOf(a, b) iy · ix · columnContained로 방향을 분류한다 iy ≤ ix 또는 열 포함 iy > ix 이고 열 포함 아님 method 7 세로 여백 활용 세로 빈 구간 placeRow / 비대칭 짝 밀기 method 8 가로 여백 활용 방향별 이동 여유로 빈 자리 찾기 ①②③ (공통 검사) 되돌림 = 클러스터 / 쌍 MAX_NUDGES = 20 MAX_MOVES = 20 7을 먼저 실행해 가로 위치를 유지한다 7·8 이 하나라도 옮겼나 아니오 예 — 두 갈래를 만든다 갈래 A — 스프레드 있음 method 6 (폰트 축소) on 7·8 결과 배율 한 칸마다 3+1+2+5+4 재실행 갈래 B — 기존 경로 method 6 on 4 까지의 결과 7·8을 생략한 결과 ③ 넘침 증가 되돌림 = 문서 전체 betterOf — 완성본끼리 사전순 비교 ⑤-b 쌍 ↓ → 면적 ↓ → ① 노드 ↓ → 면적 ↓ → ② ↑ → ③ ↑ 전부 같으면 기존 경로(B)를 남긴다 After 문서 — 재배치된 sheetJson 채점은 여기서 시작한다
점선 두 갈래가 우회로다 — 유휴 여백이 τ 미만이면 7·8 을 건너뛰고, 7·8 이 아무것도 안 옮겼으면 betterOf 비교 자체를 건너뛴다. 회귀 방지 비교는 7·8이 실제로 이동한 경우에만 추가 실행한다(§6).
§2

공통 모듈과 배치 단위

좌표를 바꾸기 전에 정하는 네 가지 배치 단위

freezeRelations 가 배치 단위와 관계를 정한다. 옮기는 도중에 다시 재면 소속이 바뀌어 결과가 처리 순서에 의존한다. 이 층이 겹침 후보 넷을 가른다 — 움직이는 단위가 무엇이냐가 곧 무엇을 풀 수 있냐다.

① 유닛 — 함께 움직이는 최소 단위 배경 도형 하나 + 그 도형이 담은 텍스트 전부 → 같은 유닛 안에서 겹치면 옮길 대상이 하나라 함께 옮겨도 겹침은 남는다 — method 5가 처리 ② 띠(가로) · ③ 체인(세로) 띠 — 세로 구간이 겹치는 유닛들 = 「같은 줄」 띠는 통째로 움직인다. 체인은 세로 자리를 다투는 열이다 체인 — 가로 구간이 겹치는 유닛들 = 「같은 열」 ④ 프레임 — 배치 경계 안에 다른 유닛을 담은 도형(카드·패널). 고정된 경계로 내부 배치를 제한한다
밴드 구성에서 주의할 점이 있다 — 2D 로 포개진 둘은 세로 구간도 당연히 겹치므로 자동으로 한 띠가 된다. 함께 이동하면 두 요소의 겹침이 유지되므로, method 4의 1단계에서 밴드를 분리한다.

배치 관계를 고정한 뒤 좌표를 계산한다

배치 관계와 좌표를 동시에 탐색하면 고려할 조합이 많아진다. 이 구현은 관계를 먼저 정한 뒤, 순서와 경계가 고정된 1차원 배치 문제를 푼다. 음수 갭이 곧 겹침이라 「겹친 것을 떼어 놓기」는 「갭 하한을 0 으로 두고 다시 나누기」와 같은 말이다.

부품무엇비용파일
여백 대수갭을 델타 균등 + 하한 water-filling 으로 다시 나눈다O(g²) 최악gapAlgebra.ts
PlaceRow (Abacus)겹치지 않으면서 가장 덜 움직이는 배치 — 정렬 후 한 차례 클러스터 스윕O(k log k)placeRow.ts
실현가능성공간이 부족하면 부족한 길이(px)를 기록하고 예외를 발생시킨다O(1)feasibility.ts
유휴 여백 지도 v0.6 신규장애물 · 4방향 여유 · 세로 빈 구간 — 격자가 아니라 상자 스윕O(L)idleSpace.ts
겹침 지도같은 그룹 안의 leaf 쌍을 비교한다 — 지표와 검사 조건의 공통 기준O(L²)metrics.ts

게이트 — 단계별 결과를 채택하는 조건

게이트무엇을 묻나누가 쓰나
① 없던 쌍자기 입력에 없던 겹침 쌍을 만들었나1·2·3·4·5·7·8
② 목적 달성원안 대비 신규 겹침(지표 ⑤-b)이 실제로 줄었나4·5·7·8
③ 넘침 증가넘침이 늘지 않았나4·5·6·7·8

②가 겹침 후보에만 있는 이유 — 자리를 다시 나누는 연산은 한 쌍을 떼어놓느라 원래 있던 쌍을 키울 수 있는데 ①은 그것을 못 잡는다(쌍 자체는 있던 것이라서).

판정 — 무엇이 SUCCESS 인가

라벨조건
FAIL① 넘침이 원안보다 나쁘다(노드 수 ↑ 또는 면적이 OVERFLOW_AREA_TOL = 10% 넘게 ↑) · ⑤ 신규 겹침이 있다 · ② 폰트가 EXCESSIVE_FONT_SCALE = 0.7 미만 — 셋 중 하나라도
SUCCESSFAIL 아님 + 폰트 배율이 원안 그대로(=1) + ④ 유휴 여백이 원안 이하
ACCEPTABLE그 사이

「폰트 배율 = 1」이 method 6 의 자리를 정한다

당시 판정 규칙에서는 폰트를 줄인 결과가 SUCCESS가 될 수 없었다. 그래서 method 6 은 맨 뒤이고, 앞 단계의 결과가 판정 기준을 통과한 경우에는 실행하지 않는다.

§3

8개 연산자의 처리 대상과 이동 방향

#역할처리 단위축크기트리거예측
1세로 유휴 여백 재분배노드·체인세로세로만 키움넘침0
2가로 확장노드가로넓힘넘침·좁은 칼럼줄바꿈
3가로 넘침 해소노드가로키우거나 좁힘가로 넘침줄 수
4겹침 — 띠 쪼개기 + 대칭 절반 밀기유닛 사이세로고정신규 겹침0
5겹침 — 도형 안의 폭 확장·세로 재배치유닛 안가로·세로바꿈신규 겹침일부
6가독성 하한 폰트 축소노드—글자판정 미달0
7 신규겹침 — 세로 빈 구간 재배치 + 비대칭 짝 밀기유닛 사이세로고정신규 겹침 ∧ 유휴 ≥ τ0
8 신규겹침 — 빈 공간으로 가로 이동유닛 사이가로고정신규 겹침 ∧ 유휴 ≥ τ0

겹침을 푸는 후보가 넷이고, 넷을 가르는 것은 두 축이다 — 층(유닛 안 / 유닛 사이)과 이동축(세로 / 가로). 4·7의 처리 범위가 겹치는 부분이 §5 에서 따질 중복이다.

§4

method 7·8의 처리 대상을 구분하는 기준

동일한 좌표에서는 세로 처리와 가로 처리의 조건이 겹치지 않는다

두 후보는 penetrationOf(a, b) 로 겹친 쌍의 세로 침투 iy · 가로 침투 ix · 열 포함 여부를 재고, 그 결과로 서로 배타적인 조건을 건다. 두 조건은 동일한 좌표의 겹침 쌍을 서로 다르게 분류한다. 다만 각 후보의 발동 조건과 안전 검사를 통과해야 실제로 이동한다.

원안에 없던 겹침 쌍 유닛 박스로 잰다 — 움직이는 단위가 유닛이라서 penetrationOf(a, b) iy = 세로 겹친 길이 · ix = 가로 겹친 길이 columnContained = ix ≥ min(두 폭) − 1px iy ≤ ix  또는  columnContained → method 7 (세로 여백 활용) iy 얕음 세로로 조금만 겹쳤다 — 떼어 놓을 축이 세로다 ix = 상자의 전체 폭 한 열에 통째로 — 가로는 분리축이 아니다 iy > ix  그리고  NOT columnContained → method 8 (가로 여백 활용) ix 얕음 옆에서 살짝 파고들었다 — 가로로 분리하는 편이 유리하다
두 조건은 서로의 부정이다 — P = (iy > ix ∧ ¬columnContained) 라 할 때 method 8 은 P, method 7 은 ¬P 를 맡는다. 다만 실행 중 좌표가 바뀌므로, 전체 실행에서 같은 쌍을 두 번 다루지 않는다는 보장은 아니다. 다른 방향으로 분류한 쌍은 보고서에 집계한다 — method 7 의 horizontalPairs, method 8 의 verticalPairs 가 해당 집계값이다.

분류는 각 단계의 실행 시점에 다시 계산한다

method 8 은 method 7 이 이미 옮긴 뒤의 문서에서 쌍을 다시 분류한다. method 7 이 다른 유닛을 세로로 옮기면 그 주변 쌍의 iy·ix 가 바뀌므로, 7 이전에 「가로」였던 쌍이 8 시점에는 아예 안 겹칠 수도, 반대로 새 쌍이 생길 수도 있다. 분업은 매 시점 다시 계산되는 분업이지 한 번 정해 둔 명단이 아니다. (새로 생긴 겹침 쌍은 게이트 ①에서 검사한다.)

§5

겹치는 책임과 단순화 여지

① 역할이 겹치는 구현 — method 4 의 2단계와 method 7 의 2-b

겹침 후보 넷 중 4와 7은 처리 단위와 이동 방향이 같다(§3) — 같은 층(유닛 사이), 같은 축(세로), 같은 게이트 셋(①②③), 같은 MOVE_EPS·OVERLAP_EPS. 각각의 「짝 밀기」를 나란히 놓으면 관계가 분명해진다.

method 4 — 2단계 pushApartmethod 7 — 2-b 짝 나누기
배분최소 침투 깊이의 절반씩 서로 반대로비대칭 — 위 유닛은 위 여유만큼, 아래 유닛은 아래 여유만큼
이동 여유 확인확인하지 않는다 — 이동 후 검사해 채택 여부를 정한다directionalSlack(box).up / .down 을 먼저 잰다
부분 이동없다 — 되면 하고 아니면 되돌린다있다 — 여유가 침투보다 작으면 가능한 거리만큼
대상 수가장 깊은 한 쌍씩클러스터 안의 쌍을 깊은 것부터
상한MAX_PUSHES = 20MAX_NUDGES = 20
트리거신규 겹침만신규 겹침 ∧ 유휴 ≥ τ

7 의 짝 밀기는 4 의 짝 밀기를 포함한다

위·아래 여유가 같을 때 비대칭 배분은 정확히 절반씩으로 떨어진다 — 즉 method 4 의 2단계는 method 7 2-b 의 특수한 경우다. 둘을 갈라 두는 유일한 이유는 τ 트리거다: method 7 은 유휴 여백이 0.5 미만인 빽빽한 페이지에서 아예 안 돈다.

단순화 제안 — 짝 밀기를 하나로 통합하고 τ를 단계별 파라미터로 내리면 (빽빽한 페이지에서는 여유가 작아 자연히 부분 이동조차 못 하므로) method 4 는 1단계 띠 쪼개기만 남고 세로 분리는 전부 method 7 이 맡는다. 반복 상한, 허용 오차, 검사 조건을 한곳에서 관리할 수 있다.

② 중복 구현과 상수 이름 정리

penetrationOf의 중복 구현

method7.ts 와 method8.ts 에 동일한 내용으로 복제돼 있고, COLUMN_EPS = 1 도 각자 선언한다.

위험 — §4 의 분업은 두 판별식이 같다는 전제 위에 서 있다. 한쪽만 고치면 아무도 안 맡는 쌍이나 둘 다 손대는 쌍이 조용히 생긴다.

고칠 자리 — relayout-core 로 올린다. 쌍을 다루는 모듈(pairing.ts)이 이미 있다.

OVERLAP_EPS 가 두 뜻

metrics.ts 의 것은 export 된 100 — 「이보다 작은 접촉은 겹침으로 안 센다」. method 4·7·8 의 것은 로컬 1 — 「원안보다 늘었나」를 물을 때의 잡음 하한.

같은 이름 · 다른 뜻 · 100배 차이다. 현재는 서로 다른 파일에서 사용하지만, 공통 모듈로 옮길 때 의미를 혼동하기 쉽다.

고칠 자리 — 뜻이 이름에 드러나게 OVERLAP_MIN_AREA / OVERLAP_GROWTH_EPS 처럼 가른다.

겹침 분리 로직이 세 곳에 있다

후보 쪽 둘(method 4·7) 말고 정답 제작 경로용이 하나 더 있다 — relayout-core/separateOverlaps.ts(2026-08-28 신규). 「열을 아래로 흘리기」만 하는 최소판이고, 합치지 않은 이유를 스스로 적어 두었다: 후보는 자기 입력에 견주고 정답은 원안에 견주므로 기준이 다르다.

이 파일은 원래 스크립트 내부의 함수였다. 같은 로직을 복제하면 처리 기준이 달라질 수 있어 공통 코어로 옮겼다. 주석이 든 전례가 「내접 판정이 세 군데로 갈려 원형 컨테이너 위반을 놓친 사고」다. penetrationOf도 같은 관점에서 공통화할 수 있다.

그 밖의 중복

MOVE_EPS = 0.01 이 method 1·4·5·7 네 곳에 선언돼 있다. 값이 같다는 사실 자체가 계약인데 그 계약을 적어 둔 자리가 없다.

unitComponents(연결 성분)는 topology.ts 에 같은 구조의 구현이 있지만 module-private 이라 method 7 이 별도로 구현했다 — 주석이 그 사정을 적어 두고 있다. 공통 함수로 제공하면 중복을 줄일 수 있다.

③ 축소 후 재실행에는 method 7·8이 포함되지 않는다

method 6 은 배율을 한 칸 줄일 때마다 앞의 다섯을 재실행한다(options.relayout). 그런데 대표 합성 후보가 넘기는 재호출 함수는 3+1+2+5+4 뿐이다 — 7·8 이 빠져 있다.

method3Method1Method2Method5Method4Method7Method8Method6.ts재호출의 범위
const relayout = (doc) => runMethod3Method1Method2Method5Method4(doc, base, outlines).doc; // ↑ 7·8 이 없다 — 축소 뒤 기하에서 스프레드를 다시 잡지 않는다 const shrunk = runMethod6(escaped.doc, { base, relayout, outlines });

의도된 것일 수 있다 — 재호출을 한 겹으로 묶어 「배율 위에 배율」을 막는 규율이 있고, 7·8 을 넣으면 재호출에 필요한 계산량이 늘어난다. 다만 축소 뒤 바뀐 공간에서 여백 활용 단계를 다시 검토하지 않는다. betterOf는 두 경로를 사후에 비교한다. 비교 지표가 나은 결과를 고르지만 전체 최적을 보장하지는 않는다. 계산 비용과 탐색 범위 사이의 선택이다.

§6

복잡도 — 시간 · 공간 · 실제 빈도

기호 — N = 전체 노드 수, L = leaf 노드 수, U = 유닛 수, P = 겹친 쌍 수, k = 한 행/클러스터의 셀 수, g = 한 체인의 갭 수, M = method 6 의 배율 칸 수(≤ 10).

당시 코퍼스의 노드 수와 비교 규모

코퍼스 1,177건을 직접 세면 leaf 노드 중앙값 12 · p90 26 · 최대 60이다. 그래서 O(L²) 는 최대 3,600 쌍 비교, 중앙값 144 — 당시 데이터에서는 비교 규모가 크지 않았다. 단, 점근 복잡도가 상수가 되는 것은 아니다. 같은 비교를 반복하는 횟수도 함께 살펴봐야 한다.

6.1 시간

단계한 번의 비용반복합계
freezeRelationsO(L²)1O(L²)
method 3O(L)1O(L)
method 1 ↔ 2O(L + g²)≤ 3 라운드O(L + g²)
method 5O(L²)유닛당O(U·L²) 최악
method 4O(L²)≤ 2020·O(L²)
method 7O(L²) + O(k log k)≤ 2020·O(L²)
method 8O(L²) + O(P·L)≤ 2020·O(L²)
method 6앞의 다섯 전체 + O(L²)≤ M ≈ 10M · (전체 체인)
betterOf2 × O(L²)1O(L²)

6.2 공간

지배 요인은 문서 복제다. 모든 후보가 structuredClone(input) 으로 시작하고 (run(doc): Doc 계약이 「입력을 건드리지 않는다」이므로), 시도마다 trial 사본을 추가로 만든다. 실측으로 문서 한 개의 크기는 JSON 기준 중앙값 39.1KB · p90 83.2KB · 최대 174.1KB 다 (객체 그래프는 그보다 몇 배).

무엇공간실측 크기수명
문서 사본 structuredCloneO(N)39.1KB (중앙값)단계마다 1개 + 시도마다 trial 1개
absBoxMapO(N)노드당 좌표·크기 값 4개계산 1회분
overlapAreaMapO(P)쌍 중앙값 2 · 최대 28겹친 쌍만 담는다 — L² 가 아니다
freezeRelations 그래프O(N)유닛·띠·체인·프레임후보 1회 실행 내내
tight box mapO(텍스트 수)1.1KB (중앙값)케이스 적재 시부터
outline (도형 실루엣)O(도형 수)1.2KB · 최대 115KB모듈 적재 때 한 번 읽어 둔다
placeRow 정렬 사본O(k)셀 수십한 클러스터 처리 중
idleSpace 장애물 배열O(L)≤ 60질의 1회분
마지막 단계에서 동시에 참조하는 문서
대표 합성 후보은 laid.doc · spread.doc · escaped.doc · shrunk.doc · plain.doc 을 동시에 들고 betterOf 로 들어간다 — 문서 사본 5~6개와 두 결과의 지표이 한 시점에 살아 있다. 중앙값이면 ~0.2MB, 최대 케이스면 ~1MB. 절대량은 작다.
반복되는 문서 복제 비용
최대 생존량이 아니라 만들고 버리는 횟수다. method 4·7·8 이 시도마다 trial 을 뜨고(각 ≤ 20), method 6 은 배율 칸마다 체인을 통째로 다시 돌린다 — 한 케이스에 문서 사본 수십 개가 생성됐다가 해제된다. 여러 케이스를 연속 실행할 때의 화면 지연은 복제와 렌더링 등을 함께 측정해 확인해야 한다.

6.3 발동 빈도와 문서 변경 비율 1,177건 전건 실행 · 2026-08-28

복잡도의 상한과 실제 비용은 다르다. 코퍼스 전체에 후보를 실행해 각 후보의 리포트를 집계했다 — 발동(그 후보가 할 일을 찾음)과 실제 조치(문서를 실제로 바꿈)를 나눠 셌다.

후보단독 발동단독 조치합성 후보 발동합성 후보 조치최악 복잡도
method 321.2%17.6%19.8%17.6%O(L)
method 1 ↔ 266.7% / 73.7%53.3% / 46.4%100%51.6%O(L + g²)
method 520.4%13.2%16.0%8.9%O(U·L²)
method 473.9%39.2%44.5%32.5%20·O(L²)
method 764.3%34.2%21.7%7.7%20·O(L²)
method 864.3%2.8%18.3%2.3%20·O(L²)
method 696.9%38.1%43.2%22.3%M · 전체 체인
betterOf——9.7%9.7%+1 × method 6

단독 = 후보 하나를 rendered.json 에 바로 실행했을 때. 합성 후보 = 본판 3+1+2+5+4+7+8+6 안에서 그 패스가 한 일. 원안이 있는 1,177건 전건, 예외 0건. ⚠️ 단독 method 2 는 세 번째 트리거(collided)를 못 받는다 — 그 값은 합성 후보가 만들어 넘기므로 단독 수치가 그만큼 낮게 잡힌다.

복잡도만으로 실제 실행 비용을 판단할 수 없다

method 5 는 최악 복잡도가 O(U·L²) 로 가장 나쁘지만, 합성 후보에서 16.0%만 발동하고 8.9%만 조치한다 — 여덟 중 가장 드물게 일하는 축이다. 게다가 U ≤ L ≤ 60 이라 최악값도 21만 연산 남짓이다.

반복 비용을 검토할 때는 method 6을 우선 살펴볼 필요가 있다 — 43.2%에서 발동하고, 발동하면 배율 칸마다 앞의 다섯 연산자를 모두 재실행한다. 여기에 betterOf 가 9.7%에서 그 method 6 을 추가로 실행한다. 발동 빈도는 비용 분석의 단서이며, 실행 시간 자체를 측정한 값은 아니다.

6.4 분업이 실측으로 확인된다

§4 에서 코드로 보인 배타적 분업이 숫자로도 나온다. 각 후보는 처리 대상에서 제외한 쌍을 보고서에 기록한다. 아래는 그 집계값이다.

넘기는 쪽넘기는 것케이스비율
method 4유닛 안 겹침 → method 5 의 몫25321.5%
method 4가로 침투 → method 8 의 몫15713.3%
method 7가로 침투 → method 8 의 몫937.9%
method 7유닛 안 겹침 → method 5 의 몫17414.8%
method 8세로로 풀 쌍 → method 4·7 의 몫57248.6%

단독 실행에서 7·8 이 실제로 옮긴 케이스를 갈라 보면 — 7만 옮김 394건 · 8만 옮김 25건 · 둘 다 옮김 8건 · 아무도 안 옮김 750건. 「둘 다」 8건은 모순이 아니다 — 한 문서 안의 서로 다른 쌍을 각자 맡은 것이다. 분업은 케이스 단위가 아니라 쌍 단위다.

method 8 이 실제로 맡는 쌍은 중앙값 1쌍 · 최대 10쌍(75건에서만 대상이 생긴다). 가로 침투가 진짜인 겹침은 드물고, 그래서 method 8 의 조치율이 2.3%로 가장 낮다 — §5 에서 「7 이 4 를 포함한다」고 본 것과 달리, 8 은 대체 불가능하되 제한된 유형의 겹침을 처리하는 연산자다.

§7

두 경로 비교와 전체 후보 선택

답 — 결정론적 후보 선택기는 아직 코드에 없다

저장소를 훑어도 선택기 구현이 없다(candidateSelector·selectBest 류 0건). 설계안 C 는 설계 문서(2026-08-27)로만 있고, 거기 적힌 채택 게이트가 「A·B 실측 이후」다.

구현된 것은 betterOf 뿐이고, 그것은 선택기가 아니라 회귀 가드다 — method 7·8 전용도 아니고, 전 후보 대상도 아니다.

betterOf — 구현됨결정론적 후보 선택기 — 설계만
무엇을 고르나두 갈래 — 스프레드 있음 / 없음후보 전부 — pool = METHODS (단독 8 + 합성 후보 8)
어디서 도나스프레드 합성 후보 3종에서만 — …+7+6 · …+8+6 · …+7+8+6후보 하나(key: 'select')로 등록해 선택 목록에 표시하는 구조
비교 항목6개 — ⑤-b 쌍·면적 → ① 노드·면적 → ② 폰트 → ③ 정렬선7개 — 판정 라벨 랭크로 시작, ④ 유휴 차이와 총 변위·등록 순서로 종결
동률 처리기존 경로(스프레드 없는 쪽)를 남긴다총 변위 → 후보 등록 순서로 동률을 없앤다
비용method 6 을 한 번 더 (≈ 2배)후보 N배 실행
목적안 나빠지게 — 후속 단계로 인한 결과 악화 방지더 좋아지게 — 케이스마다 최선 후보
그래서 지금 보장되는 것
스프레드 합성 후보에서 두 경로의 지표를 비교해 결과를 선택한다. 비교에 쓰지 않는 지표나 전체 디자인 품질까지 보장하지는 않는다. 단계 게이트는 자기 단계만 지키므로, 합성 후보 끝에서 완성본끼리 견주는 것이 후속 단계의 영향까지 확인하는 장치다 (5629794 가 그 필요를 증명했다 — 7·8 자신은 게이트를 통과했는데 뒤의 method 6 이 다른 경로를 타면서 SUCCESS 가 FAIL 로 떨어졌다).
아직 안 되는 것
「케이스 X 에는 4→7 이, 케이스 Y 에는 7→4 가 낫다」는 고정 순서 합성 후보으로 담을 수 없다. 그 조합 한계를 푸는 것이 선택기의 몫이고, 설계안은 구현 전에 베이스라인 JSON 으로 상한을 먼저 재는 후보를 새로 실행하지 않고 저장 결과로 수행하는 분석을 채택 게이트로 걸어 두었다.

당시 구현은 여백 활용 단계를 포함한 합성 후보의 두 경로를 비교하는 범위였다. 전 후보를 대상으로 하는 층은 설계로만 존재하고, 그 설계에서 method 7·8 은 선택 대상의 일부일 뿐이다.

§8

유휴 여백 지도 — 이동 가능한 공간 찾기

지표 ④ idleMarginRatio 는 「여백이 얼마나 남았나」만 답하는 페이지 전역 스칼라였고, 후보들이 쓰는 여유는 전부 국소적이었다 — neighbors.ts 는 좌·우 이웃의 한계선, gapAlgebra.ts 는 한 체인의 1차원 갭. 「어디에 남았나」를 답하는 자리가 없어서 겹친 요소를 이동시킬 공간을 찾기 어려웠다.

obstaclesFor — 이동을 막는 것 나 장애물 장식 — 제외 페이지 배경판 · 자기 유닛 — 제외 leaf 만 본다. 겹침 판정(⑤)이 안 세는 것을 장애물로 세면 풀 수 있는 것을 못 푼다 directionalSlack — 4방향 여유 left right up down 그 방향으로 이만큼 옮겨도 장애물·경계에 안 닿는다 — method 8 이 쓴다 verticalCorridor — 세로 빈 구간 세로 빈 구간 — 공통 x 구간의 위·아래 여유 클러스터를 placeRow로 함께 배치한다 — 겹치지 않으면서 가장 덜 움직이는 배치
왜 격자(비트맵)가 아니라 상자 스윕인가 — ④의 4px 격자를 재활용하는 안도 있었지만, 격자는 「전체 점유율」에는 맞아도 방향 질의에는 O(격자) 스캔과 셀 경계 오차가 붙는다. 문서의 장애물은 수십 개 수준이라 상자 스윕이 더 단순하고 정확하다.
relayout-core/idleSpace.tsIDLE_SPREAD_TRIGGER
base(원안) 존재 ∧ 신규 겹침 ≥ 1쌍 ∧ idleMarginRatio ≥ τ // τ 기본 0.5

넘침은 조건이 아니다. 이것이 v0.5 와의 가장 큰 차이다 — 기존 후보들은 「텍스트가 넘쳤을 때」 발동했고, 그래서 넘침 없이 겹침만 남은 케이스에는 아무도 손대지 않았다. 발동에 「신규 겹침 존재」가 결합돼 있어 원안부터 여백이 큰 미니멀 디자인을 불필요하게 이동시키지 않는다 — τ 는 예산의 하한이지 목표가 아니다.

§9

측정 기준 — 프레임과 실제 글자 영역의 차이

v0.6 에서 확인한 주요 문제 중 하나는 후보 내부와 최종 채점에서 사용하는 영역이 다르다는 점이었다. 후보 안쪽에서는 엔진이 잰 값을 못 얻으므로 frameBoxesAsTight 로 노드 박스를 그 자리에 끼워 쓴다. 그런데 성적표는 실측 tight 로 잰다 — 측정 영역이 다르면 실제 글자는 겹치지 않는데 후보가 겹침으로 판단할 수 있다.

측정 기준무엇누가 보나
frameBoxesAsTight노드 박스를 tight 자리에 끼운 프레임 대체값. 오토핏 여백만큼 과대 보고하지만 모든 후보가 같은 기준을 본다후보 내부(기본)
carryTightBoxes케이스의 rendered.json + 그 tight 를 승계해 시행 문서에 옮긴다 — 채점과 같은 측정 기준7·8 (tight 를 받았을 때) · betterOf
visualBox.ts엔진이 잰 tight + 글머리 기호 몫 = visual bounding box성적표(채점)

실측값 전달로 판정 기준을 맞추고 처리 대상을 넓혔다

replaceLayout 이 케이스의 실측 tight 와 원안 tight 를 후보까지 넘기도록 (RelayoutRunContext.tight / baseTight) 전달하자, method 7·8 의 트리거·게이트가 성적표와 같은 자를 보게 됐다. 이 변경은 결과 악화를 줄이는 데 그치지 않았다 — 기준 쪽 겹침 부풀림에 가려 안 보이던 신규 쌍들이 드러나 새로 풀린 케이스가 생겼다.

아직 남은 불일치

method 6(·4·5)은 여전히 프레임 기반 대체값로 발동을 판정한다. §5 의 중복 정리와 함께 묶어 보면, 「측정 기준을 어디서 정할 것인가」가 아직 한 곳에 모여 있지 않다 — tight 를 받는 후보와 안 받는 후보가 섞여 있고, 그 차이가 §5 ③ 의 재호출 비대칭과 같은 뿌리다.

§10

측정 결과 — 날짜와 평가 범위

측정 시점과 대상 건수가 다른 결과는 직접 비교하지 않는다

아래 두 성적표는 각각 다른 시점·다른 건수이고, 문서 작성 당시 코퍼스는 두 평가 범위보다 컸다 — 그대로 견주면 안 된다.

작성 당시 코퍼스 2026-08-28 직접 셈

케이스 (rendered.json)
1,177
tight·outline을 생성한 케이스
1,177 전건
정답(expected) 있는 케이스
484
before 판정 (라벨 2026-08-27)
F 1,166 / A 11 · S 0

eval-out/cases/*/case.json 을 직접 세어 얻은 값이다. before(대치 직후)의 FAIL 사유는 신규 겹침 843 · 넘침 709 · 유휴 11 로 갈린다(한 케이스가 여러 사유를 가진다).

후보 성적표 111건 · 2026-08-26 · 낡음

열넘침 노드넘침 면적게이트 PASS폰트 배율정렬선
original (기준)451,086,451기준1.0001.000
before1394,107,47034/1111.0000.932
합성 후보 3+1+214534,45443/1111.0000.895
합성 후보 3+1+2+414534,45471/1111.0000.885
합성 후보 3+1+2+5+414534,45483/1111.0000.864
합성 후보 3+1+2+5+4+611368,34597/1110.9820.861
expected (목표선)39699,72960/900.9930.849

읽는 법 — 넘침은 3+1+2 에서 87% 가 사라진다. 거기서 게이트가 43/111 밖에 안 되는 것은 넘침과 겹침이 다른 문제이기 때문이다. +4 가 유닛 사이를, +5+4 가 유닛 안까지 푼다 — 그 셋은 넘침 측정값이 같다(「넘침이 늘면 되돌린다」가 게이트라서). 마지막 +6 만 글자를 줄여 넘침도 줄이지만, 대가로 SUCCESS 는 한 건도 안 는다.

method 7·8 의 기여 96건 · 2026-08-27 · 낡음

후보SAF⑤-b 쌍정렬선
당시 비교 기준 3+1+2+5+4+6502917590.8540
+ method 7 (A)542913460.8486
+ method 8 (B)512916550.8498
+ 7 + 8 (A+B)552912420.8444

FAIL 17 → 12(−29%) · 신규 겹침 59 → 42쌍(−29%) · 나빠진 케이스 0건. §4 의 분업이 실측으로 확인된다 — 5628689 는 B 만이, 5611182·5694141·5698092·6377108 은 A 가 풀었고, A+B 는 정확히 둘의 합집합이다. 다만 이 결과만으로 모든 케이스에서 역할이 중복되지 않는다고 일반화할 수는 없다. 자세한 설계·평가는 유휴 여백 재배치 설계 에 있다.

§11

코드 지도

아래 링크는 vscode://file/… 편집기 링크다 — 당시 실행 환경의 주소를 보존한 것으로, 최신 파일의 줄 번호를 가리키지는 않는다.

무엇파일여기서 확인한 것
레지스트리 · 진입점relayout-methods/index.ts:69후보 16개 · replaceLayout 의 6개 인자
method 4relayout-methods/method4.ts:152MAX_PUSHES = 20 · 2단계 대칭 절반 밀기
method 5relayout-methods/method5.ts:150유닛 안 · MOVE_EPS 사본
method 6relayout-methods/method6.ts:317SHRINK_STEP = 0.05 · relayout(trial) 재호출
method 7relayout-methods/method7.ts:227penetrationOf 원본 · MAX_NUDGES = 20 · 비대칭 짝 밀기
method 8relayout-methods/method8.ts:202penetrationOf 사본 · MAX_MOVES = 20
회귀 가드relayout-methods/betterOf.ts:436항목 사전순 · 동률 → 기존 경로
대표 합성 후보relayout-methods/method3Method1Method2Method5Method4Method7Method8Method6.ts:55재호출에 7·8 이 빠진 자리(§5 ③)
위상relayout-core/topology.ts:57유닛 · 띠 · 체인 · 프레임 · PIN_WEIGHT = 100
유휴 여백 지도relayout-core/idleSpace.ts:57IDLE_SPREAD_TRIGGER = 0.5 · 세 질의
겹침·지표relayout-core/metrics.ts:95OVERLAP_EPS = 100 — 후보 로컬의 1 과 같은 이름
1차원 배치 계산relayout-core/placeRow.ts:112정렬 후 한 차례 — O(k log k)
여백 대수relayout-core/gapAlgebra.ts:85water-filling guard ≤ orig.length
판정relayout-core/verdict.ts:108EXCESSIVE_FONT_SCALE = 0.7 · OVERFLOW_AREA_TOL = 0.1
쌍짓기relayout-core/pairing.ts:33penetrationOf 를 옮길 위치 후보(§5 ②)

다시 재는 법

miricanvas-iui 루트에서파일을 만들지 않는다
# 규칙 검사 전건 pnpm exec vitest run src/post-replace-relayout # 지표 표를 터미널에 — 후보 전부 + tidy pnpm exec tsx scripts/relayout-metrics.ts # 후보 하나만 전 케이스에 — 케이스별 판정과 게이트 pnpm exec tsx scripts/relayout-method-run.ts --method method3+method1+method2+method5+method4+method7+method8+method6 # 케이스를 새로 넣었을 때 굽는 둘 pnpm exec tsx scripts/tight-box-cases.ts --only <id> pnpm exec tsx scripts/outline-cases.ts --only <id>

코퍼스 건수와 알고리즘 검증을 구분한다

코퍼스가 늘고 줄 때마다 깨질 뿐 후보에 대해 아무것도 말하지 않는다. 검증해야 할 것은 앞 합성 후보과의 차이(방향과 크기)와 남는 케이스의 목록이다.