본문으로 이동

배치

요약

배치는 원본에서 읽은 도형, 그룹, 선에 좌표와 선 경로를 정하는 기능이다. 도형 크기와 연결점은 그리는 코드와 같은 함수로 먼저 정하고, elkjs에는 그 값을 그대로 넘긴다. elkjs가 돌려준 좌표와 경로를 고치지 않고 그린다.

동기

첫 구현은 D2가 자기 크기로 배치한 좌표 위에 다른 크기의 도형을 그렸다. 그 차이를 메우려고 선 끝 붙이기, 선 끝 다시 나누기, 계단 펴기, 곧은 선 가운데 맞추기를 차례로 더했고, 고칠 때마다 다른 그림이 깨졌다. 배치 계산이 그리는 모양을 모르면 이런 후처리는 끝나지 않는다. 글자 폭도 보는 쪽 시스템 글꼴에 따라 달라서, 잰 폭과 그려진 폭이 어긋났다.

예시

저장소에서 나가는 선

  1. 원본에 store db "주문 DB"와 db -> api "조회"가 있다.
  2. 배치가 원통의 몸통에 위아래 뚜껑 높이를 더한 사각형을 elkjs 도형으로 넘기고, 선 끝 연결점은 뚜껑 윤곽 자리에 고정한다.
  3. elkjs가 그 연결점에서 경로를 잡는다.
  4. 배치가 결과 사각형에서 바깥 여백(뚜껑 높이)을 빼 몸통 사각형으로 되돌린다.
  5. 그리기가 몸통 위아래에 뚜껑을 그린다. 선은 뚜껑 윤곽에 붙어 있다.

세로로 쌓은 그룹

  1. 원본에 group system "Saturn" direction=down {이 있고 보기는 적지 않아, 기본 그래프(graph right)가 카드를 담는다.
  2. 배치가 system을 따로 배치해 안쪽 도형을 위에서 아래로 쌓고, system을 그 크기의 도형 하나로 바깥 배치에 넘긴다.
  3. 바깥 도형에서 system 안 도형으로 가는 선은 system 왼쪽 면의 연결점을 거친다.
  4. 그림이 가로로 길어지지 않는다.

상세 설계

단계

  1. 글 재기: 도형 이름, 부제, 카드 줄, 선 라벨의 글 폭을 잰다.
  2. 크기 정하기: 도형마다 크기와 연결점 제약을 정한다. 카드는 그 도형이 시간 흐름에서 보일 모든 카드 내용 가운데 가장 큰 것으로 정한다.
  3. 그룹 배치: 가장 안쪽 그룹부터 따로 배치해 그룹 크기를 정한다.
  4. 바깥 배치: 그룹을 정한 크기의 도형으로 넣고 배치한다.
  5. 경로 잇기: 그룹 경계 연결점에서 끊긴 경로 조각을 이어 선 하나의 경로로 만든다.
  6. 그림 넓히기: 가장 넓은 이동 글 상자가 그림 폭보다 넓으면 그림을 넓힌다.
  7. 이동 글 간격: 흐름 글 상자가 도형 이름을 가려 보이는 시간의 25% 넘게 숨거나, 박자 이동의 글 상자가 보이는 시간의 6할(토큰 scale.chip-visible-share)을 못 채운다고 그림 검사 7번이 알린 그림만 다시 배치한다. 그 이동이 지나는 선(경고가 난 구간의 선)마다 층 간격이 글 상자 폭(세로 층은 높이)에 spacing.3 두 쪽을 더한 값이 되도록 보이지 않는 라벨을 elkjs에 넘기고, 그래도 경고가 남으면 토큰 scale.chip-room-step(16px)씩 늘려 최대 scale.chip-room-tries(4)번까지 다시 본다. 재시도에서는 앞서 확보한 다른 구간의 간격도 유지하여 가림이 구간 사이를 오가지 않게 한다. 경고가 가장 적은 배치를 쓰고 배치 검사 오류가 늘지 않아야 한다. 풀리지 않으면 처음 배치와 경고를 그대로 두며 글은 지우지 않는다. 경고가 없는 그림과 이동이 지나지 않는 선은 건드리지 않으므로 그 출력은 그대로다. 자동 간격 재배치는 이동 글 검사에서 반환한 숨김 경고 수를 읽는다. 경고 문장의 언어와 표현은 배치 선택에 영향을 주지 않는다. 구현은 src/chip-room.js와 src/build-scene.js가 호출하는 widenForHiddenChips다.
    • 박자 이동은 층 간격에 더해 선 옆 폭(sweep)도 요구한다. 글 상자가 선 옆을 쓸고 지나므로, 세로 구간은 글 상자 반 폭에 spacing.1(CHIP_CLEAR), 가로 구간은 글 상자 높이에 spacing.3을 폭으로 잡는다. 이 폭은 글 상자 크기에서 한 번 정하고 되풀이해 키우지 않는다. 층 간격을 늘려도 풀리지 않는 원인은 이동이 끝나는 쪽의 간격이 아니라, 배치 뒤 선 옆에 자리를 고르는 글자가 그 폭 안에 놓이는 것이기 때문이다.
    • 배치 뒤 선 옆에 놓는 두 가지가 이 폭을 비킨다. 그룹 제목은 그 선의 구간에서 spacing.2 대신 옆 폭만큼 떨어져 선다(옆 폭이 그룹 안에 들지 않으면 기본 거리를 두고 경고는 그대로 남는다). 끝 라벨(다중성)이 다른 선의 옆 폭 안에 있으면 자기 선의 반대쪽으로 옮기고, 옮긴 자리가 옆 폭, 도형, 다른 선, 다른 라벨에 걸리면 그대로 둔다. 그래서 같은 그림에서 일부 끝 라벨만 반대쪽에 설 수 있다.
    • 자기 알약 위를 지나는 세로 구간처럼 규칙상 피할 수 없는 가림(알약을 덮지 않는다는 규칙)은 고치지 않는다.
  8. 그림 검사: 그림 검사를 돌린다.
  • 2단계가 끝난 뒤에는 어떤 크기도 바꾸지 않는다. 배치에 넘긴 크기와 그리는 크기가 같아야 하기 때문이다.
  • 크기를 고쳐 다시 배치하지 않는다. 원통이 있는 그림만 같은 크기로 elkjs를 두 번 돌린다(연결점 순서 절).

글 재기

  • 시간 보기에서 구간 이름은 막대 오른쪽에 놓고, 실제 시간축의 오른쪽 경계를 넘을 때만 왼쪽에 놓는다. 판의 폭을 바꾸면 이 경계도 함께 바뀐다.

  • 글꼴은 토큰이 정한 글꼴 파일이다. 본문은 한글, 라틴, 기호, 숫자 모두 Pretendard 하나이고(참고 문서 페이지와 같은 글꼴), 고정폭은 JetBrains Mono다. 모두 SIL Open Font License라 그림 안에 넣을 수 있다. Pretendard 정적 파일(regular 400, semibold 600, OTF)은 pretendard npm 패키지(OFL 1.1, 예약 글꼴 이름 Pretendard)로 받는다. 그림에 넣는 조각은 FigSans라는 다른 이름으로 쓴다. 굵기마다 정적 파일이라 측정과 자르기가 같은 파일을 쓴다. JetBrains Mono 파일은 jetbrains-mono npm 패키지(패키지 껍데기는 MIT, 글꼴은 JetBrains의 OFL 1.1)로 받는다.

  • 글자 사이 간격은 토큰 tracking.text(-0.3px)다. 글자마다 더해지므로 글 폭은 글꼴 폭의 합에 글자 수 곱하기 -0.3px를 더해 잰다. 고정폭 글(백틱 구간, 테이블 타입)에는 간격을 주지 않는다. CSS letter-spacing도 같은 토큰이고 고정폭 글에는 normal이라 잰 폭과 그린 폭이 같다. 측정이 모르는 자간을 역할에 따로 주지 않는다(그룹 제목에는 자간이 없다).

  • 본문 글은 글자마다 Pretendard에 있으면 Pretendard, 없으면(수학 기호) 뒤 글꼴로 정하고, 글꼴이 바뀌는 구간마다 그 글꼴로 폭을 재서 더한다. 띄어쓰기는 Pretendard 것이다.

  • JetBrains Mono에는 한글이 없다. 고정폭 글(테이블 타입, 백틱 구간) 안의 한글은 Pretendard로 그린다. 글자마다 고정폭 글꼴, Pretendard 순서로 처음 있는 글꼴로 정하고 위와 같이 구간별로 폭을 더한다.

  • 그림에는 쓴 글꼴마다 조각이 들어간다. Pretendard 조각은 FigSans, 고정폭 조각은 FigMono이고 font-family는 본문 FigSans, FigSansSym, FigSansMath, ...(토큰 font.figure-sans), 고정폭 FigMono, FigSans, FigSansSym, FigSansMath, ...(토큰 font.figure-mono) 대체 사슬이다. 사슬의 뒷부분(Pretendard, 시스템 대체)은 design-tokens의 font.sans, font.mono와 같고(고정폭은 FigMono가 대신하는 JetBrains Mono 이름만 뺀다). 이 일치를 직접 확인하는 자동 시험은 지금 없다(검증 요구사항, 미완료). 브라우저가 글자마다 고르는 글꼴과 잰 글꼴이 같아야 한다.

  • 차트 숫자 폭은 Pretendard 파일의 tnum 기능을 적용해 잰다(num, numSemibold 글꼴). 글꼴 조각은 tnum 대체 글리프를 포함해 자른다. 그래서 보는 쪽이 tabular-nums로 그린 폭과 잰 폭이 같다.

  • 글을 읽는 방식은 그 글을 그리는 글꼴이 정하고(text.js의 isLiteralFace), 고정폭 글꼴(mono)과 값 글꼴(semiboldLiteral)이 글 그대로 읽는 글꼴이다. 고정폭 글꼴 칸(클래스 멤버, 테이블과 API 타입, mono 줄)과 값 글은 백틱도 글자로 읽어 잰 폭, 줄 나눔, 글꼴 조각에 모으는 글자, 그린 글, 원본 검사, 보이는 글 비교가 모두 같다. 값 글꼴은 semibold와 같은 글꼴 파일이고 글자 사이 간격도 같다. 고정폭만 간격이 없다. 그 밖의 칸에서는 글 안의 백틱 구간을 이름이 무엇이든 고정폭으로 잰다. 줄을 나눌 때 구간이 줄 사이에 걸치면 줄마다 구간을 닫고 다시 열어 각 줄이 짝이 맞는 글이 된다. 넘침 검사와 글꼴 없는 글자 검사도 구간별 글꼴을 따른다.

  • 글 폭은 글꼴 파일의 글자 너비 표로 잰다. 어림 비율을 쓰지 않는다.

  • 글자 크기는 역할별 정본 토큰을 따른다. 페이지·조작부는 text.heading, subheading, body, control, caption을 쓰고, 그림 안 글은 역할 셋이다. 제목은 text.small-text(15: 카드 제목 regular, 그룹 제목 semibold), 문장과 이름표는 text.tiny-text(13), 작은 표시는 text.figure-meta(11)다. 차트 눈금·범례·값도 같은 11과 13 역할 토큰을 읽는다(chart/metrics.js의 TEXT, chart.css). 페이지와 재생기 UI 글은 공통 text·font-weight 역할을 따로 쓴다. 역할의 크기와 굵기는 measure/texts.js의 STYLE이 재고 @woonyong-choi/design-tokens/styles/diagram/figure.css, chart.css, status.css가 그리며, 차트 제목과 부제도 같은 label, sub 역할이다. 그림 안 글의 굵기는 regular와 semibold 둘뿐이다. 아래는 현재 구현값이며 Things와의 시각적 일치 판정은 아니다. 도형 이름의 측정값과 CSS 크기와 굵기는 반드시 함께 변경한다.

자리 정본 토큰 현재 크기·굵기 색
목록·문서 페이지 h1 text.heading, font-weight.bold 36px·700 ink
문서 미리보기 h2 text.subtitle, font-weight.semibold 27px·600 ink
목록의 그림 제목 text.figure.22, font-weight.semibold 22px·600 ink
문서 미리보기 문단 text.body-text 18px·400 ink
도형 이름·차트 제목 text.small-text 15px·400 ink
단계 탭 text.body-text를 기준으로 text.platform-text, font-weight.semibold 홈페이지 문서 탭과 같은 버튼 변형 color.platform-color, 선택 color.control-paper
테마 선택 text.body-text를 기준으로 text.feature-tab-font, font-weight.semibold 공통 분절 변형, 선택과 비선택의 굵기 동일 color.platform-color, 선택 color.control-paper
차트 항목 이름 text.tiny-text 13px·400, 워터폴 합계 행 600 ink
테이블 열 이름·격자 항목 text.figure.13 13px·400 ink
단계 설명·현재 단계 text.figure.13, text.tiny-text 13px·400 ui-muted
파일 이름·목록 링크 text.figure.13 13px·400 파일 이름 ui-muted, 링크 ui.link
도형·차트 부제, 선 라벨, 메모, 이동 글 text.tiny-text 13px·400 부제 ui-muted, 선 라벨은 알약 글자색, 나머지 ink
카드 줄·코드·형식·제약·값 text.tiny-text 13px·400, 값 600 ink, 형식과 제약 ui-muted
그룹 제목·보기 이름 text.small-text, font-weight.semibold 15px·600, 자간 없음 group-title
카드 태그·표시·열 키(PK, FK) text.figure-meta 11px·600 태그와 열 키 ink, 표시 state.active-text
차트 범례·눈금·단위·값·끝 이름 text.figure-meta 11px·400, 강조 값 600 값 ink, 끝 이름 계열 글자색, 나머지 ui-muted
상태 알약 글자 text.figure-meta 11px·600 ink

페이지 제목과 그림 내부 제목은 서로 다른 역할이다. 같은 요소를 맞출 때는 숫자뿐 아니라 글꼴, 줄 높이, 주변 간격과 실제 표시 배율도 비교한다. 목록과 문서의 큰 제목은 모두 leading.heading을 사용한다.

  • 이름 한 줄에 아이콘 하나뿐인 단순 상자(카드, 부제, 배지, 개수, 타일, 원통 없음)는 아이콘과 이름을 한 줄에 나란히 놓는다(sizeInline). 너비는 아이콘, 간격, 이름 폭이고 높이는 spacing.figure.node.min-height 이상이라 아이콘을 위에 쌓아 생기던 빈 높이가 없다. 이름 자리는 labelRows가 같은 값(decor.textCx)을 쓴다. 타일, 카드가 있는 도형, 여러 줄 이름은 이전 배치 그대로다.
  • 줄 높이는 글자 크기에 비율 토큰을 곱해 반올림한다. 도형 안의 여러 줄 글(이름, 부제, 카드 줄, 격자 칸 글)은 leading.normal(1.5)이고 카드 줄 사이 간격은 spacing.1-5이다. 크기를 정하는 쪽(measure/texts.js의 STYLE)과 그리는 쪽이 같은 값을 쓰고, 카드 높이와 도형 높이가 이 값으로 늘어나 글이 카드 밖으로 잘리지 않는다. 이동 글 상자는 leading.snug(1.35), 설명 줄과 문단은 leading.normal(1.5)이다.
  • 차트 값 글자(11)는 눈금과 같은 크기라 굵기(강조 값 600)와 색(ink 대 ui-muted)으로 앞선다. 데이터 그림에서 가장 중요한 글이지만 크기 단계를 하나 더 늘리지 않았다. 카드 값 줄의 값은 카드 줄과 같은 13이고 굵기만 다르다.
  • 이동 글 상자는 문장과 이름표 역할(13)이다.
  • 글 한 줄의 자리와 폭은 measure/texts.js가 textAt으로 정한 text 하나이고, 기준 점(anchor)도 text가 정한다. 카드 내용 줄(measure/content.js의 sizeContent)도 태그, 오른쪽 표시, 본문, 관계 그래프 이름, 값을 모두 text로 재어 돌려주고, 그리는 쪽(draw/content.js, draw/values.js)과 그림 검사 1번(check/fit.js)은 그 text를 그대로 읽어 따로 폭을 다시 세지 않는다. 메모와 순서 구획 제목은 textBlock, 그룹 제목은 placeGroupHead가 같은 방식으로 text를 만든다.
  • 구획 카드(표, API, 클래스)와 순서 보기 참여자의 머리는 measure/card.js의 headerOf 하나다. 제목은 아이콘과 배지를 뺀 폭(room)에 한 줄이 들면 그대로 두고, 넘으면 낱말 자리, 이름 안의 /, -, _, . 뒤, 글자 단위 순서로 줄을 나눈다. room은 본문 폭과 INNER_MAX 중 큰 쪽이라 본문이 이미 넓으면 제목은 필요 없이 좁게 나뉘지 않는다. 머리 높이는 줄 수만큼 늘어나고 아이콘은 첫 줄과 가운데가 같다.
  • 차트 머리와 시간 보기 머리는 chart/labels.js의 drawHeader 하나다. 제목은 label, 부제와 덧붙임 글, 좁은 배치의 기준선 이름은 sub 역할과 줄 높이로 위에서 아래로 쌓는다. 차트는 플롯 배치라 왼쪽에 맞추고, 긴 글은 좁은 배치의 폭(chart.layout.width)이나 없으면 차트 기본 폭(WIDTH)에서 줄을 나눈다.
  • 결과 SVG에는 그 그림에 쓰인 글자만 잘라 낸 글꼴을 넣는다. 보는 쪽에 글꼴이 없어도 잰 폭과 그려진 폭이 같게 하기 위해서다.
  • 수학 기호는 Pretendard에 없는 것만 뒤 글꼴이 그린다. 글자 순서는 Pretendard, Noto Sans(FigSansSym, 위 첨자 ᵀ), Noto Sans Math(FigSansMath, ∈, ∇, ⊙, ∘ 같은 연산자)다. ⁻¹, √, ×, ∑, ≤, ∞는 Pretendard에 있다. 두 글꼴 모두 SIL OFL 1.1이고 @expo-google-fonts/noto-sans, @expo-google-fonts/noto-sans-math 패키지(껍데기 MIT, 글꼴 OFL)로 받는다. Noto Sans Math는 굵기별 파일이 없어 모든 굵기에 한 파일을 쓰고(font-weight: 400 900) 브라우저가 글자를 덧칠하지 않게 한다. font-family 사슬은 FigSans, FigSansSym, FigSansMath, ...다.
  • 글꼴 파일과 대체 글꼴 어디에도 없는 글자(그림 문자 등)는 오류다. 대신 그릴 글꼴의 폭을 알 수 없기 때문이다.

글꼴 자리

자리 글꼴
SVG 내부 글: 도형·카드·차트·선 라벨·이동 글 상자 font.figure-sans의 내장 Pretendard 조각(FigSans)과 기호 대체 글꼴
목록·문서 페이지·HTML 재생기 조작부 font.sans의 시스템 글꼴 사슬
차트 숫자: 눈금·값·바뀐 비율·히트맵 칸 숫자 내장 Pretendard에 font-variant-numeric: tabular-nums
그림 안 코드: 테이블 타입·카드 mono 줄·백틱 구간 font.figure-mono의 내장 JetBrains Mono 조각(FigMono). 한글은 Pretendard
HTML 코드·파일 이름 font.mono의 사용 가능한 글꼴 사슬. 별도 웹 글꼴을 내장하지 않음
  • SVG 안의 글은 내장 글꼴 조각으로 측정값과 표시 폭을 맞춘다. 목록·문서는 별도 웹 글꼴을 내장하지 않고 본문에 font.sans를 적용한다. HTML 조작부와 페이지는 시스템 글꼴을 사용하므로 플랫폼별 글꼴·줄바꿈 차이가 생긴다. SVG의 내장 글꼴이 모든 HTML 글자에 적용된다고 가정하지 않는다.

  • 목록 쪽과 문서 미리보기의 카드 머리는 원본 파일 이름(bar.thinkflow, 코드 글꼴)과 종류 꼬리표(bar, architecture)다. 그림이 제목을 직접 그리는 차트는 그림 제목을 머리에서 되풀이하지 않는다. 제목을 그리지 않는 그림(흐름, 순서, 상태, 데이터)만 title 값을 파일 이름 앞에 제목으로 붙인다. 제목과 종류는 그림을 빌드한 파서 결과를 그대로 쓰며, 제목이 없을 때만 첫 주석을 설명으로 쓴다. 제목은 별도 줄을 차지하고 파일 이름과 종류 꼬리표는 다음 줄에서 기준선을 맞춘다. 사이는 spacing.1-5이다. 머리가 좁으면 칸 단위로 줄바꿈한다(말줄임 없음). 코드 글꼴은 파일 이름과 백틱 글에만 쓴다.

도형 크기와 연결점

도형 elkjs에 넘기는 사각형 바깥 여백 연결점
box, external 이름, 부제, 카드를 담는 사각형 없음 네 면 어디나
store 몸통(위아래 원통 뚜껑 높이를 뺀 사각형)에 위아래 뚜껑 높이를 더한 사각형 위아래 뚜껑 높이 세로 그림은 나가는 선이 아랫면, 들어오는 선이 윗면이고, 가운데 1/3 안 뚜껑 윤곽(뚜껑까지 포함한 배치 사각형의 위아래 변, 곧 뚜껑 윤곽)에 둔다. 가로 그림은 나가는 선이 오른쪽 면, 들어오는 선이 왼쪽 면이고, 몸통 높이 안에 둔다
queue 이름과 칸 묶음을 담는 둥근 사각형. spacing.figure.node.max-width 안에 들어가는 칸 수로 행을 나누며, 이름 폭과 칸 묶음 폭으로 너비를 정한다. 칸 크기와 개수를 유지하고 행 수에 따라 높이를 늘린다(여러 줄 대기 큐) 없음 네 면 어디나(box와 같다)
person box와 같은 표준 카드. 사용자 의미 아이콘(icon=user가 기본)이 윗줄에 붙어 이름 줄과 함께 사각형 하나를 이룬다 없음 네 면 어디나(box와 같다)
decision 마름모를 감싼 사각형 없음 나가는 선은 오른쪽 꼭짓점, 들어오는 선은 왼쪽 꼭짓점
group 안쪽 배치 결과와 제목 줄 없음 경계 연결점(그룹 배치 절)
grid 제목 줄, 칸 묶음, 빈 자리를 담는 사각형. 크기는 칸 격자 없음 격자 전체를 이은 선은 네 면 어디나. 칸을 이은 선은 그 칸에서 나가는 연결점(칸 연결점과 통로 절)
box shape=circle 지름이 이름 폭에 맞는 정사각형 없음 면의 가운데 네 점. 나가는 선은 오른쪽, 아래, 위, 들어오는 선은 왼쪽, 위, 아래 순으로 쓰고 선이 늘면 다음 면의 가운데를 쓴다
table 머리 칸, 열 칸, 카드 칸 없음 열을 이은 선은 그 열 줄 가운데 높이에서, 나가는 선은 오른쪽 면, 들어오는 선은 왼쪽 면. 묶음 배치(아래 그림 크기)에서는 들어오는 선도 오른쪽 면이다. 열 없이 이은 선은 네 면 어디나
상태 그림 처음 점, 끝 겹원 원을 감싼 사각형 없음 네 면 어디나
  • 도형 너비는 spacing.figure.node.min-width과 spacing.figure.node.max-width 사이다. 넘는 글은 줄을 나눈다. 예외는 둘이다. 마름모는 글 사각형이 안에 들어가도록 글 폭의 두 배쯤까지 넓어지고, 테이블은 가장 긴 열 이름과 타입 폭만큼 넓어진다. 구조를 보이는 내용도 넓힌다. 카드에 보이는 관계 그래프 줄은 깊이별 열마다 가장 넓은 이름 알약에 열 사이 간격을 더한 폭을 measure/content.js의 contentMinWidth 하나가 카드 하한으로 정하고(measure/minigraph.js가 같은 열 폭으로 그린다), 상자, 머리 있는 카드, 표, API가 모두 이 하한을 쓴다. 이름 알약은 줄이거나 자르지 않고 깊이나 이름 길이에 새 한도도 없다. spacing.figure.node.max-width는 글 줄바꿈 폭이라 그래프를 자르지 않는다. 넓어진 카드도 같은 바깥 폭 맞춤(그림 폭 초과)을 받는다.
  • 바깥 여백은 elkjs에 넘기는 사각형에 포함한다. 다른 도형과 선이 그 자리를 비켜 가고, 결과 사각형에서 여백을 빼면 몸통 사각형이 된다.
  • 원통 위아래 면의 연결점을 가운데 1/3로 묶는 이유는, 그 밖에서는 뚜껑 곡선이 사각형 위쪽 변보다 내려와 선 끝이 허공에 닿기 때문이다. 몸통 변이 아니라 뚜껑 윤곽에 두는 이유는, 몸통 위 변은 뚜껑 가운데 줄이라 화살표가 윗면을 파고들기 때문이다.
  • 사람은 별도 도형 모양이 없다. 표준 카드의 크기 규칙을 그대로 따르므로 선이 많아도 카드가 선 수에 맞춰 자라지 않고, 선 끝 간격은 그림 검사 5번이 지킨다.
  • 원통의 모든 선 끝에 연결점을 두는 이유는, 연결점이 없으면 elkjs가 선을 여백을 포함한 사각형 변에 붙여 뚜껑 자리 허공에 닿기 때문이다.
  • 마름모 연결점을 꼭짓점으로 묶는 이유는 같다. 나가는 선과 들어오는 선을 반대 꼭짓점에 두는 이유는, 갈림길을 지나는 흐름이 한 방향으로 읽히게 하기 위해서다.
  • 연결점이 정해진 도형에서 나가는 선을 오른쪽(세로 원통은 아래), 들어오는 선을 왼쪽(세로 원통은 위)에 두는 이유는, 모든 면에 연결점을 열면 elkjs 경로가 도형을 감아 돌기 때문이다.

연결점 순서

  • 원통이 있으면 elkjs를 두 번 돌린다. 연결점 순서를 미리 정하면 선이 엇갈리고, elkjs에 맡기기만 하면 연결점이 뚜껑 자리에 놓이기 때문이다.
  • 첫 배치는 면만 고정하고(FIXED_SIDE) 면마다 연결점 순서를 elkjs에 맡긴다.
  • 둘째 배치는 그 순서를 지켜 연결점을 몸통 범위에 고정한다(FIXED_POS). 위아래 면은 가운데 1/3, 옆면은 몸통 높이다.
  • 원통이 없으면 한 번만 돈다.

칸 연결점과 통로

  • 칸을 이은 선은 elkjs에 격자 테두리의 연결점 하나만 넘긴다. 칸 안쪽 구간(lead)은 격자 크기와 함께 측정 단계에서 정하고(measure/grid-links.js), 선 경로의 앞이나 뒤에 이어 붙인다. 두 구간의 경계는 같은 점이다. elkjs 경로를 옮겨 칸에 맞추지 않는다.
  • 면 고르기는 그림 방향이 정한다. 가로 흐름은 나가는 선이 칸의 동쪽 면, 들어오는 선이 서쪽 면이고, 세로 흐름은 나가는 선이 남쪽 면, 들어오는 선이 북쪽 면이다. 칸이 그 면에 걸쳐(격자 테두리 쪽에 있어) 있으면 칸 면에서 격자 테두리까지 바로 나간다. 같은 칸의 같은 방향 선 끝은 면을 고르게 나눈다. 북쪽 면은 제목 글을 비켜 제목 양옆 빈 구간(제목 폭 절반에 spacing.3)에만 둔다.
  • 걸치지 않은 안쪽 칸은 칸 아래 통로를 거친다. 칸 아래 면에서 내려와 행 사이 빈 줄(통로)을 따라 가서 격자 옆면(동쪽, 들어오는 선은 서쪽)으로 나간다. 통로는 칸 사이가 아니라 행 사이 빈 줄이라 이웃 칸 글을 가리지 않고, 칸 폭 비율은 그대로다. 합친 칸은 걸친 마지막 행 아래 통로를 쓴다.
  • 통로가 필요한 격자는 모든 행 사이를 같은 높이로 벌린다. 한 줄을 지나는 선이 n개이면 높이는 (n + 1) × spacing.2-5이고(선 하나당 한 줄, 줄 간격은 elk.spacing.edgeEdge와 같은 값), 맨 아래 여백은 max(grid.pad, (n + 1) × spacing.2-5)다. 통로가 없는 격자의 크기는 그대로다. 통로는 두 그림 방향 가운데 하나라도 직접 연결이 없는 선 끝마다 잡으므로, 방향을 돌려 다시 배치해도 크기가 같다.
  • 한 통로 안 선의 줄은 옆면까지 가는 거리가 긴 선이 아래 줄을 쓴다. 짧은 선의 세로 조각이 긴 선의 가로 조각을 가로지르지 않게 하기 위해서다. 서쪽으로 나가는 선이 위 줄, 동쪽이 아래 줄이다. 칸 아래 면을 쓰는 선 끝(바로 나가는 선과 통로 선)은 칸 폭을 고르게 나눠 세로 조각이 붙지 않게 한다.
  • 한 격자의 두 칸을 잇는 선은 elkjs를 거치지 않는다. 두 칸의 통로를 격자 오른쪽 여백의 세로 줄로 이은 경로가 전부이고, 이런 선이 있으면 오른쪽 여백이 (n + 1) × spacing.2-5로 넓어진다. 이 선은 라벨을 받지 않는다.
  • 칸 연결점은 면과 위치를 고정하므로 칸 선이 여럿인 격자(스택 예제)에서 선이 교차할 수 있다. 받아들인 한계이고 경로를 옮겨 푸는 후처리는 두지 않는다.
  • 테이블 열 연결점과 칸 연결점은 같은 장치다. 도형이 선 끝마다 { side, position, lead }를 정하고(layout/ports.js의 portSpec), 연결점 position이 elkjs에, lead가 선 경로에 간다. 테이블 열은 lead가 없다.

장식: 아이콘, 배지, 개수, 번호

  • 도형 윗줄(아이콘 spacing.figure.icon.node, 글자 알약 높이 spacing.figure.pill.height)과 그룹 제목 줄의 장식 크기는 배치 전에 정하고 도형과 제목 너비에 넣는다. 윗줄은 이름 위에 서고 높이만큼 도형이 커진다. 복제 개수 상자는 뒤 윤곽 두 겹(spacing.1씩)만큼 크고 이름과 카드는 앞 상자 안에 놓인다.
  • 여러 자리 선 번호도 높이는 일정하고 숫자 폭만 가로로 늘어난다. 선 번호 원은 라벨 알약의 일부라 알약 크기에 들어가 배치와 검사가 같은 사각형을 쓴다. 번호만 있고 라벨이 없는 알약은 elkjs에 자리를 요구하지 않고, 선을 다 그린 뒤 가장 긴 구간 가운데에 얹는다(이 알약이 층 사이 간격을 늘려 가로 구성도가 캔버스에 들지 않던 문제를 푼다).
  • 타일(shape=tile)은 윗줄 아이콘을 spacing.figure.icon.tile로 키우고 최소 너비를 spacing.figure.node.tile-width로 줄인 같은 상자다. 타일 아이콘(spacing.figure.icon.tile)과 이름 사이는 spacing.figure.icon.tile-gap이다. 그룹 제목 줄 아이콘은 왼쪽 위 모서리 정사각 탭에 놓인다. 크기는 탭 너비(spacing.figure.group.title)에서 spacing.2를 뺀 값이고, 제목은 탭 오른쪽(titleDx가 탭 너비만큼 큼)에서 시작한다. 탭은 선이 가리면 제목과 함께 선 오른쪽으로 비킨다. 구성도는 층 하나가 spacing.15 간격과 그룹 안쪽 여백만큼 폭을 쓰므로, 캔버스(960)에 들려면 그룹 방향으로 열을 줄여 짜야 한다(examples/architecture.thinkflow은 그룹마다 방향을 달리해 표준 폭에 맞춘다).

아이콘

  • 기본 세트 builtin은 공통 ui/build/icons.mjs가 읽는 배포 카탈로그(@woonyong-choi/design-tokens/assets/icons/catalog.json)의 이름과 별칭을 사용한다. 각 이름은 assets/icons/<의미 범주>/<이름>.svg 하나를 가리킨다. src/icons/index.js가 ICON_NAMES를 만들며 접두사 없이 사용한다. 없는 이름은 가까운 이름과 카탈로그 위치를 안내하며 전체 등록명을 오류에 나열하지 않는다.
  • 개념 아이콘과 브랜드는 홈페이지와 같은 SVG 원본과 ui/svg.mjs 렌더러를 사용한다. 공통 iconBody가 icon.size-small 격자에 맞추고, ThinkFlow는 측정한 정사각 칸에 배치한다. 같은 도형이 반복되어도 내부 마스크 ID는 삽입마다 구분하며, 재생·정지 층도 서로 다른 ID를 쓴다. 내장 SVG의 색과 모양은 보존하며 별도 도형이나 팔레트를 두지 않는다. 출처와 라이선스는 카탈로그의 libraries, brands와 assets/icons/licenses/, 저장소의 NOTICE에 있다. 사용자 세트만 아래의 단색 SVG 제약을 따른다.
  • 재생기 조작 아이콘은 같은 카탈로그의 controls 역할과 공통 ui/icons.mjs의 ControlIcon을 사용한다. 단일 HTML도 공통 렌더러와 조작부에 필요한 SVG만 포함한다. 색은 단추의 글자색을 따르고 크기는 공통 조작 토큰이 정한다. 문법 복사, HTML 내려받기, 전체화면과 확대·맞춤을 제공하며 재생·일시정지·배속·반복 단추는 없다(재생).
  • 사용자 세트: icons 이름 "폴더"로 등록하고 icon=이름:파일이름으로 쓴다. 저장소에는 넣지 않는다(라이선스가 불명확한 세트를 사용자가 직접 쓰는 경우). 렌더 때 파일을 읽어 g, path, circle, ellipse, rect, line, polyline, polygon과 좌표, 변환, 칠하기 속성만 다시 쓰고(색은 모두 currentColor나 none) 나머지(script, image, style, use, 그라디언트 등)는 오류다. 크기는 루트 viewBox(유한한 숫자 넷, 폭과 높이는 양수)가 정하고, viewBox를 적었는데 틀리면 width와 height로 넘어가지 않고 오류다. viewBox가 없을 때만 width와 height(둘 다 유한한 양수, px 허용)를 쓴다. 숫자 속성은 무한대로 넘치는 값(1e309)도 오류다. 읽을 때 틀을 24 격자 한가운데에 맞추는 변환을 아이콘 안에 한 번 적어 두므로(반올림 없이), 그릴 때는 격자만 본다. 격자에 맞추는 배율이나 이동이 유한한 0 아닌 수로 나타나지 않는 크기(1e-320)는 icon= 줄의 오류다. 64KB, 요소 600개 상한이다.

연결점 방향

  • 그룹 경계를 넘는 선의 끝은 도형이 든 그룹의 방향이 아니라 선이 놓이는 가장 가까운 공통 그룹의 흐름 방향 면에 닿는다. 오른쪽 흐름 그림에서 세로 그룹 안 도형으로 가는 선은 위로 돌아 들어가지 않고 왼쪽 면에 닿고, 세로 그룹 안 도형에서 나가는 선은 아래가 아니라 오른쪽 면에서 나간다(그림 검사 4번도 같은 면을 본다). 그 면의 연결점만 고정하고(FIXED_SIDE) 위치는 elkjs가 정한다.
  • 그룹 경계 연결점은 면 가운데에 모으고(portAlignment CENTER), 그룹 안은 네트워크 심플렉스 배치로 놓아 연결점에서 도형까지 선이 곧게 간다(선이 붙는 검사에 걸리면 안전 배치가 기본 배치로 다시 놓는다).

그룹 제목 줄

  • 선은 그룹 제목 줄(아이콘, 제목, 배지)을 지나지 못한다. 선이 제목 자리를 지나면 제목 덩어리가 기본 자리, 선분 오른쪽 바로 너머, 선분 왼쪽 바로 앞 가운데 가장 왼쪽으로 맞는 자리로 비킨다. 아이콘 탭이 있으면 탭과 제목 글이 한 덩어리(headBox)라 함께 비키고, 선과의 간격은 spacing.2다. 선이 그룹 경계를 지나가는 것은 의도한 연결이라 결함이 아니고, 아이콘 탭이나 제목을 지나는 것과 그룹 변과 나란히 붙어 달리는 것만 결함이다. 위 면으로 선이 들어오는 그룹은 제목이 선 한쪽에 들어가도록 최소 너비가 제목 덩어리의 두 배다. 그래도 맞지 않으면 그림 검사 13번이 오류로 알린다.

되돌아가는 선

  • 순환이 있는 그룹(바깥 층 포함)에서만 되돌아가는 선을 따로 정한다. 원본에 적은 줄 순서대로 깊이 우선으로 선을 따라가다, 지금 따라가는 길 위의 도형으로 되돌아오는 선이 되돌아가는 선이다. 도형과 그룹은 그룹을 앞에 모으지 않고 원본 줄 순서로 섞어 둔다.
  • 되돌아가는 선만 거스르는 순서로 자식을 놓고, 그 그룹의 순환 처리를 elk.layered.cycleBreaking.strategy: MODEL_ORDER로 한다. elkjs는 모델 순서를 거스르는 선만 뒤집으므로, 먼저 적은 도형이 앞(왼쪽, 위)에 오고 마지막에 적은 닫는 선만 되돌아간다. 이전의 GREEDY_MODEL_ORDER는 나가는 선이 많은 도형을 먼저 놓아 호출 코드가 맨 아래로 내려가고 첫 선이 그림을 크게 돌았다.
  • 순환이 없는 그룹은 처음 설정(GREEDY_MODEL_ORDER)을 지킨다.
  • 선 번호는 원본에 적은 선의 번호이고, 상태 그림의 처음 점과 끝 겹원 선은 그 뒤에 놓인다. 시간표와 그리기가 같은 번호로 선을 찾기 때문이다. 모델 순서에서 처음 점의 선이 맨 앞에 놓여 번호를 한 칸씩 밀던 오류를 고쳤다.
  • 내부 이름은 __root처럼 _가 든 이름이다. 원본 이름은 소문자, 숫자, -뿐이라 root를 포함한 어떤 이름도 부딪히지 않는다.
  • 그룹 방향이 바깥과 다른 그룹의 경계 연결점은 안쪽 배치가 도형과 상관없이 놓아, 그룹 가로 폭만큼 도는 선이 남을 수 있다. 연결점 자리를 도형에 맞춘 고정(FIXED_POS)은 그룹 안 배치를 무너뜨렸고, 연결점 면을 안쪽 방향으로 바꾸면 그룹 안 도형이 쌓이는 방향이 달라졌다. 그래서 그룹 방향이 바깥과 같을 때 선이 가장 짧다.

선 라벨

  • 선 라벨은 elkjs의 선 라벨(labels)로 알약 크기를 넘긴다. elkjs가 라벨 자리를 비워 두고 배치해서, 라벨끼리와 라벨과 도형이 겹치지 않는다.
  • 라벨 자리는 선 가운데이고 선 위에 얹는다(라벨마다 elk.edgeLabels.placement: CENTER, elk.edgeLabels.inline: true).
  • 열 선 묶음 배치는 방향 돌리기를 쓸 수 있게 하려는 것이다. 열을 이은 테이블 그림이 한 줄에 안 들어가면 먼저 방향을 돌린다. 그래도 나가는 선이 오른쪽, 들어오는 선이 왼쪽이면 아래 도형이 오른쪽으로 계단처럼 밀려 캔버스에 들지 않아 줄 바꿈으로 접히고, 줄 사이로 되돌아오는 선이 그림 폭만큼 가로지른다. 그때 열 선을 모두 오른쪽 면에 모은 묶음 배치를 돌린 방향과 원래 방향에서 비교한다. 선이 도형 오른쪽을 돌아 아래 도형으로 들어가 세로로 쌓이고 선 길이 합이 준다. 돌린 배치가 이미 폭에 드는 그림은 묶음 배치를 보지 않는다. 묶음이 선 길이를 늘리고 이동 글 상자가 들어갈 옆 자리가 없어 그림 검사 7번 경고가 날 수 있기 때문이다. 데이터 그림도 구조 그림과 같은 맞춤 함수(fitCanvas)를 쓰므로 후보가 달라지는 것은 연결점 규칙 때문이다.
  • 세로로 쌓는 층(down)의 quiet 선은 라벨을 elkjs에 넘기지 않고 선 옆에 둔다. 라벨을 넘기면 elkjs가 라벨을 한 층으로 세워 층 간격이 nodeNodeBetweenLayers의 두 배에 알약 높이를 더한 값(138px)이 되는데, 선을 처음 지나기 전 박자에는 그 간격이 비어 보여 "주 메모리"가 큰 빈 간격 아래로 떨어져 보였다(vm-translation, vm-walk-i7). 라벨 없이 두면 간격이 60px이다. 알약은 가장 긴 곧은 구간 가운데에서 선 간격(spacing.1-5) 띄운 옆에 놓는데, 그림 가장자리가 가까운 쪽에 놓는다. 넓은 이동 글 상자가 알약을 비켜 넓은 쪽에 들어갈 자리를 남기기 위해서다. 알약과 이동 글 상자는 재생의 자리 찾기가 서로 비켜 놓는다. 가로 층(right)의 quiet 선은 알약이 선 위에서 층 사이 폭을 차지해야 이동 글 상자와 겹치지 않으므로 보이는 선과 같게 라벨을 넘긴다. 이전의 “보이는 선과 같은 간격” 방식은 이동 글 상자가 자리를 순간 이동으로 바꿔 알약을 비켜 가던 때의 것이다.
  • 세로 사슬이 길어 보이는 원인은 비율 규칙이 아니라 층 간격과 카드 높이다. 한 줄이 폭에 들지 않으면 방향을 돌린 배치가 가장 먼저 쓰이고, 그 비율(vm-translation 960/1377 = 0.70, vm-walk-i7 0.48)은 알맞은 범위(1/3 ~ 1.6) 안이다. 접어 보면 선 길이가 4~6배(vm-translation 1142에서 4800, vm-walk-i7 888에서 5701)로 늘고 그림 검사 경고가 나서, 범위(scale.aspect-fit-max)와 방향 돌리기 우선 순서는 그대로 둔다.

그룹 배치

  • 그룹은 따로 배치한다(elk.hierarchyHandling: SEPARATE_CHILDREN). 그룹마다 direction을 지키기 위해서다. 한 번에 배치하면 안쪽 방향이 무시된다(2026-10-01 elkjs 0.9로 확인).
  • 그룹 경계를 넘는 선은 넘는 경계마다 연결점 하나를 거친다. 안쪽 배치는 선 안쪽 끝과 연결점을 잇고, 바깥 배치는 연결점과 바깥 끝을 잇는다.
  • 연결점이 놓이는 면: 그룹에서 나가는 선은 바깥 방향의 앞쪽 면(right면 오른쪽, down이면 아래쪽), 들어오는 선은 뒤쪽 면이다.
  • direction이 없는 그룹은 바로 바깥의 방향을 따른다.
  • 그룹 제목은 그룹 왼쪽 안쪽(spacing.4-5)에 둔다. 선이 그 자리(아이콘 탭과 제목 글을 합친 사각형에 spacing.2 여백. 박자 이동이 지나는 선은 단계 7의 옆 폭)를 지나면 탭과 제목을 그 선 오른쪽으로 옮긴다(그룹 titleDx, 탭은 titleDx - spacing.4-5 - spacing.figure.group.title만큼 같이 움직인다). 선이 제목을 가로지르면 글이 읽히지 않기 때문이다. 옮길 자리가 그룹 안에 없으면 기본 자리에 둔다. 제목 자리는 선 경로가 정해진 뒤 한 번 정한다.

그림 비율

  • aspect가 있으면 가장 바깥 층(root)만 elkjs 줄 바꿈 배치(elk.layered.wrapping.strategy: MULTI_EDGE, elk.aspectRatio)로 목표 비율에 맞춘다. 그룹은 따로 배치하는 한 덩어리 도형이라 줄 바꿈이 그룹과 함께 동작한다(2026-10-01 elkjs 0.9로 확인). 그룹 안은 줄 바꿈하지 않고 그룹의 direction을 따른다.
  • 줄 바꿈 선택 사항은 multiEdge.improveWrappedEdges: false, additionalEdgeSpacing: 0이다. 근거는 무작위 40개 원본(그룹 있음 80%, 되돌아오는 선과 라벨 포함) 실험이다. 기본값(improveWrappedEdges: true)은 선이 엉뚱한 도형으로 가는 elkjs 오류를 냈고, 끄면 오류가 없었다. additionalEdgeSpacing: 0은 줄 사이 되돌아오는 선의 간격을 줄여 그림 넓이를 약 10% 줄였다. SINGLE_EDGE는 열이 맞지만 검사 실패가 많았고, correctionFactor 2와 cutting.strategy: ARD는 비율 3을 넘는 그림을 냈고, improveCuts: false는 선이 더 길어졌다.
  • 줄 바꿈한 사슬은 줄이 바뀔 때마다 되돌아오는 선이 그림 폭만큼 가로지른다. 이 선은 elkjs 경로 그대로이며 더 줄일 수 없다. 갈래가 많은 그림에 aspect를 넣으면 선이 엉킬 수 있어, 긴 사슬에만 권한다.
  • 그룹이 있는 그림을 줄 바꿈하면 elkjs는 열(층) 너비를 그 열의 가장 넓은 도형에 맞춘다. 그룹은 박스보다 넓어서 그룹이 놓인 열만 넓어지고, 그 열의 박스는 기본값(가운데)으로 놓여 첫 줄이 들여쓴 것처럼 보이고 줄마다 칸 간격이 달랐다(n0은 오른쪽, n4는 가운데). 그래서 그룹이 있는 줄 바꿈 그림은 바깥 층 도형(박스와 그룹)마다 elk.alignment: RIGHT를 준다. 열 안에서 도형의 오른쪽 끝이 같아져 모든 줄의 첫 상자가 같은 열에서 시작하고 칸 간격이 줄마다 같다. 이 선택 사항은 층이 아니라 도형마다 줘야 적용된다.
  • 같은 열 정렬은 오른쪽 끝 기준이다. 한 열에 폭이 다른 도형이 있으면 오른쪽 끝만 맞고, 넓은 그룹 열의 왼쪽은 빈 채로 남아 되돌아오는 선이 그 빈 곳을 가로질러 들어온다. 열 너비를 도형 크기와 무관하게 같게 하는 elkjs 선택 사항은 없고, 크기를 늘리면 그리는 도형과 달라져 규칙에 어긋난다. 시도한 nodePlacement.strategy(SIMPLE, LINEAR_SEGMENTS, NETWORK_SIMPLEX), bk.fixedAlignment, 층 전체 elk.alignment는 열 위치를 바꾸지 못했거나(SIMPLE은 줄마다 칸이 더 벌어짐) 효과가 없었다.
  • aspect를 적으면 자동 맞춤을 하지 않고 적은 방향과 비율을 지킨다. 줄 바꿈이 선을 도형 위로 지나가게 하거나 선 끝을 연결점 밖에 놓아 그림 검사 3번, 4번, 5번이 실패하면, 줄 바꿈과 모델 순서 없이 한 번 더 배치하고 실패가 줄면 그 배치를 쓴다. 이때 aspect를 적었다면 무시했다고 경고한다.
  • 가로세로 비율은 보이는 모양으로 잰다. 내용이 캔버스보다 좁으면 판만 캔버스 폭으로 넓어지므로 가로는 내용 폭과 캔버스 폭 중 큰 쪽이다(451x986은 내용 비율 0.46이지만 960x986으로 보여 0.97이다). 이 비율이 3을 넘거나 1/3보다 작으면 그림 검사가 경고한다. 캔버스 폭으로 줄일 때 글자가 읽기 어렵게 작아지기 때문이다.
  • 알맞은 비율은 데스크톱 화면과 모바일을 함께 본다. 데스크톱은 폭 960으로 보여 세로가 길면 페이지가 길어지고, HTML은 판이 구역보다 넓어도 자연 크기를 유지하고 그림 영역에서 가로로 스크롤한다(재생). 문서 전체는 가로로 넘치지 않는다. 그래서 세로로 긴 쪽은 두 화면 모두 페이지 스크롤로 읽혀 1/scale.aspect-max(1/3)까지, 가로로 넓은 쪽은 데스크톱 가로 화면 비율(1400x900)인 scale.aspect-fit-max(1.6)까지를 알맞다고 한다. 그림 검사 9번의 한도(3과 1/3)는 이 범위 밖에서도 경고하지 않는 바깥 한도다. 자동 맞춤은 후보 가운데 캔버스에 들고 이 범위 안인 첫 배치를 쓰고, 범위 안인 후보가 없으면 캔버스에 드는 후보 중 범위에 가장 가까운 것을 쓴다.
  • 순서 그림과 차트는 배치가 격자로 정해져 aspect를 쓰지 않는다.
  • 가장 넓은 이동 글 상자에 양쪽 간격(토큰 spacing.3)을 더한 폭이 그림 폭보다 크면 그림을 그 폭으로 넓히고 내용을 가운데로 옮긴다. 좁은 세로 그림에서 글 상자가 그림 밖으로 나가지 않게 하기 위해서다.
  • 그림 머리 width wide를 쓰면 캔버스가 넓은 폭(spacing.figure.figure-canvas-wide, 1440 안팎의 데스크톱 문서 본문 폭)이 된다. 배치의 접기와 방향 돌리기, 그림 검사 9번과 10번이 모두 이 폭을 기준으로 재므로 최소 글자 크기 규칙은 그대로다. 문서의 SVG 이미지가 가용 폭에 맞춰 줄어드는 규칙은 유지한다. HTML 재생기와 목록 카드의 틀은 가용 폭을 따르지만, 안쪽 그림은 자연 크기를 유지한다(--figure-canvas는 틀의 최대 폭이다). 생략하면 아래 표준 폭이다.
  • SVG 파일은 width wide가 아니면 표준 캔버스 폭(spacing.figure.figure-canvas, 960)을 쓴다. 차트의 논리 너비(spacing.figure.chart.width)는 720이고 캔버스 가운데에 놓인다(차트). SVG의 height는 비율대로 정한다. HTML 재생기의 틀은 이 캔버스 폭을 최대 폭으로 쓰되, 삽입된 그림과 좁은 화면에서는 아래 내용 맞춤 규칙을 적용한다(재생).
  • 단독 HTML 재생기는 가용 폭이 표준 표시 폭보다 좁으면 내용 둘레로 보기 영역을 맞춘다. 빈 캔버스 때문에 작은 그림의 글자가 더 줄어들지 않으며 원래 내용 크기보다 확대하지 않는다. 화면 회전과 전체 화면에서 복귀할 때 다시 맞춘다. SVG 파일의 고정 캔버스 규칙은 유지한다. 브라우저에서 보이는 동작이라 자동 시험이 없고 실제 Chrome 검수의 몫이다(검증 요구사항, 미완료).
  • 판 폭은 내용 폭과 판 제목 폭(제목 글 폭에 FIGURE_PAD 둘을 더한 값) 가운데 큰 쪽이라 제목이 잘리지 않는다. 내용이 캔버스나 제목보다 좁으면 판만 넓히고 내용은 크기와 글자 크기를 그대로 둔 채 가운데에 둔다. 이 폭 예약은 판을 합치는 공통 한 곳(layout/panels.js의 sizeOf)이 하고, 보기 종류(그래프, 순서, 차트, 시간)마다 따로 계산하지 않는다. 이동 글 상자 밀어 넣기와 검사(7번 등)는 내용 좌표 기준이라 가운데로 옮겨도 판 안에 든다. 단계 이름 줄은 판 너비에 맞춰 줄을 나눈다.
  • 내용이 캔버스보다 넓으면 순서를 둔다. 먼저 구조, 상태, 데이터 그림은 aspect를 적지 않았을 때 글자 크기를 지키는 맞춤을 한다. 1) 바깥 방향을 돌려(right는 down, down은 right) 한 줄로 다시 배치한다. 그룹이 직접 적은 direction은 그대로다. 2) 비율 scale.fold-aspect(1.6)로 접어 다시 배치하고, 그래도 넓으면 scale.fold-step을 곱해 낮춰 가며 최대 4번 본다. 후보는 캔버스 폭 안에 들고 보이는 가로세로 비율이 토큰 scale.aspect-max(3) 안인 것만 인정하고, 그 가운데 알맞은 비율 범위 안인 첫 배치를 쓴다(위 알맞은 비율). 범위 안인 후보가 없으면 범위에 가장 가까운 후보를 쓰고, 캔버스에 드는 후보가 아예 없으면 가장 좁은 배치를 쓴다. 방향을 돌리는 쪽을 먼저 보는 것은 접으면 줄 사이를 되돌아오는 선이 그림을 가로질러 선이 2~3배 길어지기 때문이다. elkjs 줄 바꿈은 그룹이 큰 도형 하나라 줄 바꿈 단위가 커서, 한 줄이 넓은 그림은 비율을 바꿔도 줄지 않는다. 이 방법은 글자 크기를 지키므로 가장 먼저 쓴다. 접어도 넓은 그림(참여자가 많은 순서 그림, 접을 수 없는 그림)은 viewBox를 내용 폭 그대로 두고 SVG width만 캔버스 폭으로 줄인다. 이 방식은 글자도 같은 비율로 작아진다. 같은 폭을 지키려면 줄이는 수밖에 없고, 그림 문법으로 폭을 따로 정하는 선택 사항은 두지 않는다. 글자가 작아지는 정도는 그림 검사 10번이 경고한다. 이 축소는 SVG 파일의 표시 폭 규칙이며 HTML 재생기는 자연 크기와 내부 가로 스크롤을 쓴다.
  • 차트는 내부 배치가 차트 너비(데스크톱 spacing.figure.chart.width, 좁은 배치는 컨테이너 폭)를 채운다. 항목 이름 칸은 이름 길이에 맞추고(spacing.figure.chart.label, spacing.figure.chart.label-max 사이), 남는 폭은 모두 그림 영역이다. 오른쪽 여백은 왼쪽 여백(28)과 같게 값 축 길이를 글자 폭으로 계산해 정한다(차트).

간격과 결정성

  • 도형 사이, 층 사이, 선 사이 간격은 토큰 space 값을 elkjs 설정으로 넘긴다.
  • 이동 글의 숨김을 줄이는 첫 간격 요구는 ELK의 기본 층·라벨 간격에 글 상자와 양쪽 여백만큼을 추가한다. 이미 확보된 기본 간격을 글 상자의 빈 공간으로 중복 계산하지 않는다.
  • 같은 원본과 같은 버전은 같은 좌표를 낸다. 다시 그려도 git 차이가 없어야 문서 스킬의 검사를 통과하기 때문이다. elkjs의 무작위 씨앗은 고정한다.
  • 선이 순환을 이룰 때만 elkjs는 선언 순서를 거스르는 선을 거꾸로 놓는다(elk.layered.cycleBreaking.strategy: GREEDY_MODEL_ORDER). 순환이 없는 선은 선언 순서와 상관없이 뒤집지 않는다. 뒤집으면 그룹보다 뒤에 선언한 사람이 오른쪽으로 밀리기 때문이다.
  • 상태 그림의 처음 점은 모델 순서의 맨 앞이다. 순환이 있을 때 먼저 적은 도형과 처음 점이 앞(왼쪽, 위)에 오게 하기 위해서다.
  • 출력 숫자는 src/format.js 하나가 글로 쓰기 직전에 줄인다. 좌표, 크기, 경로는 소수 첫째 자리, keyTimes, keyPoints, 퍼센트, 재생기 데이터는 소수 다섯째 자리다. 계산 중간 값은 줄이지 않는다. 줄이기 전에 1e-9보다 작은 계산 오차를 걷어 내서, Node 버전마다 Math 함수의 끝자리가 달라도 같은 바이트가 나온다. 생성물 일치 검사(md --check, 갤러리 SVG 시험)가 실행 환경에 따라 깨지지 않게 하기 위해서다.

순서 그림 배치

제어 구획이 있으면 첫 메시지 전에 구획 제목과 대안 이름의 실제 줄 높이를 예약하고, 마지막 메시지 뒤에 닫는 여백을 둔다. 중첩 경계는 spacing.2, 제목 여백은 spacing.3, 글자는 공통 13px 문장 역할(meta)을 쓰며 구획 제목만 semibold다. 참여자·메모·자기 호출 선이 가장 깊은 경계 안에 들어가도록 양옆을 넓힌다. 경계는 color.ui-line와 border-width.thin, 모서리는 카드와 그룹 틀과 같은 바깥 모서리(radius.lg), 대안 구분선은 border-width.hair와 dash.card로 그린다. 제목 아래에 공통 카드색과 작은 반지름의 면을 두어 생명선이 글자를 가로지르지 않는다. 구획이 없는 그림은 기존 배치를 유지한다.

  • elkjs를 쓰지 않는다. 참여자 열과 메시지 행이 정해진 격자이기 때문이다.
  • 메시지 행은 장면마다 따로 첫 행부터 쌓인다. 메시지, 메모, 구획, 활성 구간, 소멸 표식은 자기 장면 번호(si)를 달고, 고른 장면 하나의 것만 보인다. 판 크기와 생명선 길이는 가장 긴 장면이 정해 모든 장면에서 같고, 장면이 바뀌어도 자리가 움직이지 않는다.
  • 순서 그림이 다른 보기와 한 그림에 놓여 판이 옮겨지면(판 원점이 바뀌면) 메시지, 메모뿐 아니라 구획 제목과 대안 경계의 y도 같은 만큼 옮겨진다. 서로 다른 장면에서만 보이는 선 둘은 같은 행을 써도 충돌이 아니고, 장면 번호가 없는 선(그래프)과는 늘 견준다(그림 검사 5번).
  • 생성과 소멸은 장면마다 따로 검사한다. 같은 참여자를 둘 이상의 장면이 만들 수 있고, 한 장면의 소멸이 다른 장면의 생명선을 자르지 않으며, 한 장면의 생성이 다른 장면의 머리를 내리지 않는다. 참여자 머리는 그 참여자가 나오는 모든 장면이 같은 행에서 만들 때만 생성 행으로 내려오고(한 장면 그림은 늘 그렇다), 소멸이 있는 참여자는 장면마다 자기 생명선을 갖는다. 장면 안의 생성·소멸 오류는 그대로 오류다.
  • 메시지가 건너는 열 간격의 합은 그 메시지 라벨 폭 이상이다. 메모와 자기 자신에게 보내는 고리는 그 참여자 오른쪽 열 간격이 메모 폭, 고리 라벨 폭 이상이다. 이웃한 열 간격은 두 참여자 너비 절반의 합 이상이다. 이 두 조건을 만족하는 가장 작은 간격을 왼쪽 열부터 정한다.
  • 행 높이는 토큰 값 하나이고, 메모가 있는 행은 메모 높이만큼 늘어난다. 메모가 같은 행 화살표나 그 라벨과 가로로 겹치면(메모가 보내는 쪽 참여자에 있어 화살표 위로 뻗는 경우) 메모를 위에, 화살표와 라벨을 그 아래에 쌓는다. 행 높이는 메모 위 여백, 메모, 최소 간격(토큰 spacing.1, 이동 글 상자 간격과 같다), 라벨, 화살표 아래 여백의 합이다. 겹치지 않으면 한 높이에 나란히 둔다.
  • 자기 자신에게 보내는 고리와 같은 행의 그 참여자 메모는 왼쪽에 놓는다. 왼쪽 메모는 이웃 왼쪽 생명선 사이 간격(첫 참여자는 그림 왼쪽 여백)이 메모 폭 이상이게 열을 정한다.
  • 자기 자신에게 보내는 메시지는 오른쪽으로 나갔다 돌아오는 고리이고, 그 행 높이를 두 배로 쓴다.

선 그리기

  • 경로는 elkjs의 직교 경로(elk.edgeRouting: ORTHOGONAL)다. 꺾이는 점의 모서리만 토큰 radius.route로 둥글게 한다.
  • 경로 점은 옮기지 않는다. 한 가지 예외만 둔다. 그룹 경계 연결점의 높이는 그룹 안쪽 배치가, 선 끝은 바깥 배치가 정해서 몇 px 어긋나는 계단이 남는다. 연결점 제약이 없는 도형의 선 끝에서, 가로지르는 흔들림이 토큰 spacing.2 미만인 구간만 그 면 위에서 선 끝을 옮겨 편다. 옮긴 선 끝이 면 밖으로 나가거나 같은 면의 다른 선 끝(들어오는 선과 되돌아 나가는 선 포함)에 토큰 spacing.1-25보다 가까워지면 펴지 않는다. 펴서 선 끝이 붙으면 그림 검사 5번이 바르게 그린 선을 오류로 알리기 때문이다. 펴고 남은, 곧게 이어지는 가운데 점은 뺀다. 한 선분이 둘로 쪼개지면 검사 5번이 쪼개진 쪽을 도형에서 나가는 첫 선분으로 보지 못한다.
  • 연결점이 없는 도형(상태, 클래스)의 자기 선은 도형에서 spacing.6(화살촉 길이의 세 배 이상) 떨어져 돌아, 화살촉만 한 고리로 보이지 않는다. 도형별 간격(individualSpacings의 nodeSelfLoop)이라 다른 도형과 표, 격자의 선 간격은 그대로이고, 라벨은 여전히 ELK의 edgeLabel 간격(spacing.1)으로 고리 옆에 놓인다.
  • 같은 자리의 점과 한 줄 위 가운데 점은 그릴 때 지운다. 그린 모양은 같다.

배치 실패

  • elkjs가 던지거나 선 조각의 경로를 주지 않으면 줄 바꿈과 모델 순서 없이 한 번 더 배치한다. 그래도 실패하면 내부 오류로 끝내지 않고 layout 진단(error)을 낸다. 경로가 없는 선이 있으면 그 선의 줄, 아니면 그림 종류 문장의 줄이다.

요구사항

시험은 공개 진입점(buildFigure, toSvg, toHtml, 명령)과 실제 공통 부품의 진입점으로 계약을 확인하고, 시험만을 위한 내보내기나 훅을 만들지 않는다. 공통 도형(카드, 필드, 선, 화살촉, 라벨)의 모양은 부품 시험이 한 번 보고, 배치 시험은 크기, 연결점, 좌표 같은 배치 계산만 본다. 부품 시험은 test/components.test.js(U)다(시험 번호). 시험 칸이 "검증 요구사항, 미완료"인 항목은 이 계약을 직접 확인하는 자동 시험이 아직 없다는 뜻이다. 화면에서 눈으로 보는 배치는 시험이 대신하지 않는다.

요구사항 검증 계획
선은 그룹 제목 줄과 아이콘 탭을 지나지 않고, 그룹 변과 나란히 붙어 달리지 않으며, 그룹 제목 글을 가로지르지 않는다. 그림 검사의 항목이 strict 빌드에서 지킨다. 항목별 판정을 직접 확인하는 자동 시험은 확인되지 않았다(검증 요구사항, 미완료)
선 번호는 라벨 알약의 일부이고 라벨이 없으면 번호 원만 있다. test/kinds.test.js(K-arch)가 번호 선을 읽는다. 알약 구조는 검증 요구사항, 미완료
기본 아이콘 세트는 공통 카탈로그의 개념·제어·브랜드 아이콘을 사용한다. 사용자 세트는 안전한 SVG만 쓴다. 사용자 세트의 안전은 test/escaping.test.js. 기본 세트의 표와 파일 일치는 검증 요구사항, 미완료
배치에 넘긴 도형 크기와 그린 도형 크기가 같다. 선 양 끝이 도형의 연결점 규칙 자리에 있다. 검증 요구사항, 미완료
그룹의 direction이 안쪽 배치에 지켜진다. test/kinds.test.js(K-arch)는 그룹 안 도형을 읽는다. 방향의 기하는 검증 요구사항, 미완료
그룹 경계를 넘는 선이 끊김 없는 경로 하나가 된다. 자동 시험 없음. 눈으로 확인한다(검증 요구사항, 미완료)
같은 원본을 두 번 그리면 결과가 같다. test/exports.test.js(결정성)
순환에서 되돌아가는 선만 위로 가고, 한 줄이 넓으면 방향을 돌려 폭 안에 든다. 검증 요구사항, 미완료
올바른 무작위 구조 그림이 배치 오류나 그림 검사 오류가 되지 않는다. node scripts/fuzz-layout.mjs(씨앗, 그림 종류 flow, state, data, --no-aspect. 구조 그림에는 칸 선 끝, 한 격자의 두 칸을 잇는 선, head, 원 도형이 섞인다)로 그림 수천 개를 만들어 실패 수 0 확인. 그림 하나가 5초를 넘으면 멈춘 것으로 세고 원본을 --hang-dir에 남긴다. 실행 결과는 이슈 기록이 맡는다
칸에서 칸으로 가는 선이 칸 테두리에서 나가고 들어오며, 안쪽 칸으로 가는 선과 한 격자의 두 칸을 잇는 선이 이웃 칸을 지나지 않는다. 그림 검사 3번(칸), 4번(칸 테두리)이 strict 빌드에서 지킨다. 선분을 직접 재는 자동 시험은 확인되지 않았다(검증 요구사항, 미완료)
양끝 표식(head)이 선택한 끝에 그려지고, 원 도형이 정사각형이다. 검증 요구사항, 미완료
같은 도형의 같은 면에 닿는 선 끝이 spacing.1-25보다 붙지 않고, 곧은 구간 가운데 점이 없다. 검증 요구사항, 미완료. 선 끝이 붙으면 그림 검사 5번이 strict 빌드에서 알린다
이름 root가 배치 내부 이름과 부딪히지 않는다. 검증 요구사항, 미완료
박자 이동 글 상자의 선 옆 폭을 요구하고, 그룹 제목과 끝 라벨이 그 폭 밖에 선다. 예제가 strict로 만들어지고 두 번 만들어도 SVG와 HTML이 같다. 예제의 strict 빌드와 결정성은 test/examples.test.js, test/exports.test.js. 글 상자 배치 자체는 검증 요구사항, 미완료
배치 실패가 줄 번호 있는 오류가 되고 안전 배치로 다시 그린다. 검증 요구사항, 미완료
메모가 같은 행 화살표와 라벨을 가리지 않는다. test/kinds.test.js(K-sequence)는 메모가 그려지는지 읽는다. 겹침 판정은 그림 검사 12번이 strict 빌드에서 알린다
상태 그림이 밝히는 선이 이동 줄과 같다. test/kinds.test.js(K-state)
폭을 넘는 열 선 사슬이 세로로 쌓이고, 세로 그림의 열 선이 오른쪽 면 열 높이에 닿는다. test/cards.test.js(S1)가 열 선이 자기 행에서 끝나는지 읽는다. 사슬 쌓기는 검증 요구사항, 미완료
세로 층의 quiet 선은 층 간격을 늘리지 않고 라벨이 선 옆에 있으며, 가로 층의 quiet 선은 보이는 선과 같은 폭이다. 검증 요구사항, 미완료
그린 글 폭이 잰 글 폭과 같다. 차트 숫자는 tabular-nums, 고정폭 글꼴은 코드(백틱 구간)에만 쓴다. 브라우저에서 계산된 글꼴을 보는 일이라 자동 시험이 없다. 실제 화면 검수로 확인한다(검증 요구사항, 미완료)

단점

  • 그림마다 글꼴 조각을 넣어 SVG 크기가 커진다. 한글 글자 수에 비례한다.
  • 그림이 곡선 선을 쓸 수 없다. 직교 경로만 그린다.

대안

  • 배치 뒤 경로 후처리: 첫 구현이 이 방식이었고 후처리끼리 충돌해 버렸다. 결정 기록
  • 보는 쪽 시스템 글꼴과 어림 폭: 글꼴에 따라 글이 상자를 넘쳐 버렸다.
  • 곡선 선(edges curve): 직교 경로와 두 벌을 유지해야 하고, 곡선끼리 겹침을 검사하기 어려워 버렸다.

미해결 질문

  • 그룹 안 도형이 그룹 바깥의 앞쪽 층이 아니라 뒤쪽 층 도형과 이어질 때(되돌아가는 선) 연결점 면 규칙으로 선이 도형을 돌아가는가. 예제로 측정해 면 규칙을 정한다.
  • 글꼴 조각을 넣은 SVG 크기가 문서 스킬의 그림 크기 기준 안에 드는가. 예제 그림으로 측정한다.

표면 위계

HTML은 흰 문서 면 위에 제목과 조작을 놓는다. 설명 그림 판만 color.prose-pre-background과 radius.card-radius 모서리를 가지며 바깥 그림자는 없다. SVG 판도 같은 면과 모서리를 쓴다. 내부 상자·큐·표·격자는 radius.lg 모서리의 그림자 없는 평면이고 윤곽 굵기는 border-width.thin 하나다. 평소 윤곽 색은 color.help-border(--fx-edge-rest)이고, light가 켠 도형만 윤곽 색이 강조 색(--fx-edge)으로 바뀐다. 굵기와 모서리는 켜져도 같다. 글자·선·점·의미 기호에도 그림자를 붙이지 않는다. 표 내부 구분선은 color.ui-line로 낮추고 의미를 구분하는 바깥 윤곽과 격자 경계는 기존 대비를 유지한다. 표와 격자는 전체 바깥 모서리만 둥글다. 테이블 열 줄의 밝힘 면은 틀 모양(clipPath)으로 잘려 마지막 줄이 둥근 틀을 뚫지 않고, 격자 칸은 칸 묶음의 바깥 모서리와 맞닿은 모서리만 반지름(radius.md)을 가지며 안쪽 칸의 교차는 직각이다(cellShape). 사람은 표준 카드이고 윗줄의 사용자 의미 아이콘(color.figure.icon-person-ink로 채운 머리와 어깨 실루엣)만 사람임을 알린다. 켜짐(light)은 같은 윤곽의 색과 도형 면만 바꾸고 굵기, 모서리, 크기를 바꾸지 않는다. 값 변경 강조는 같은 윤곽 안의 면만 바꾼다. 어느 쪽도 윤곽을 겹으로 덧씌우지 않는다. 흐름이나 이동이 닿은 도형은 도착 후광(펄스)만 쓰고 면과 윤곽을 켜지 않는다. 마우스를 올려도 도형이나 카드는 켜지지 않는다(장식용 호버 활성 없음). 시각 위계는 글과 의미 아이콘이 먼저, 현재 흐름이 둘째, 도형 면과 관계선이 셋째, 그룹과 틀이 가장 뒤다. 경계 문법은 기본 = 중립 경계(color.help-border), 켜짐(light) = 강조 윤곽(color.state-active)과 옅은 켜진 면(color.help-hover), 값 변화 = 짧은 배경 면이다. 호버 상태는 없다. 켜짐과 평소의 값은 figure.css의 효과 한 벌(--fx-*)이 한 번 정하고, 정지 그림, HTML 재생기의 .on 규칙, 움직이는 SVG의 keyframes가 같은 속성 이름을 읽는다(keyframes는 토큰 값을 다시 적지 않는다). 표 줄과 격자 칸이 켜지면 두 출력 모두 --fx-row(color.figure.selection-fill) 면이다. 고른 색(tone, appearance)은 평소 값(--fx-face, --fx-edge-rest)만 바꾼다(draw/paint.js). 켜진 동안 윤곽은 강조 색에 자리를 내주고, 옅은 면(appearance=filled)으로 고른 도형은 켜져도 면이 같다. 내용이 찬 카드는 윤곽 없이 중립 면 하나(color.ui-card)만 두고 안쪽 값 행에도 윤곽을 두지 않는다(반지름 도형 8, 카드 6, 값 면 4). 그룹은 가장 뒤의 조직 정보다. 틀은 카드와 같은 바깥 모서리(radius.lg)와 가는 경계(border-width.thin)이고 면이 없어(옅은 면은 appearance=filled일 때만) 깊어질수록 짙어지지 않으며, 굵기, 면, 모서리는 단계에 따라 바뀌지 않는다. light가 명시한 그룹만 경계선 색이 도형 윤곽과 같은 강조 색(--fx-edge)이 된다. 그룹의 평소 경계는 틀 사각형 자신의 --fx-edge-rest라 같은 그룹 안의 배지가 읽지 않는다. 이름 묶음(아이콘, 이름, 부제)의 자리는 카드가 비어 있어도 채워져도 같다. 머리가 내려오거나 올라가지 않고, 카드 내용이 들어올 자리는 가장 큰 내용에 맞춰 늘 예약되어 비어 있는 동안에도 남아 있으며 단계마다 도형 크기는 변하지 않는다. 행 사이 통로(gutter)가 있는 격자는 칸 묶음 전체가 바깥 모서리만 둥근 한 판(grid-tray, 테두리 없는 color.ui-card 면) 위에 놓여 통로가 판의 면으로 보이고, 바깥 모서리는 칸 묶음 전체의 네 모서리에 닿은 칸만 둥글다. 카드 안 관계 그래프는 밝힌 이름을 선택 행의 옅은 면(color.figure.selection-fill)으로, 이름 윤곽은 중립 color.outline으로 그리고 열 사이에 spacing.2를 비운다. 선 번호 원은 검정 면(color.figure.number-fill)과 흰 숫자(color.figure.number-ink)를 쓰며 강조 상태에서도 유지한다. 선은 border-width.edge 굵기의 둥근 끝이고 화살촉은 선마다 같은 함수로 만든 채운 삼각형 표식(fl-arrow-{번호}, 상자 한 변이 몸통 굵기의 spacing.figure.arrow.head배, 모양과 계산은 src/draw/arrow.js)다. 몸통 색으로 채우고 같은 실제 굵기의 윤곽선(currentColor, 둥근 끝과 이음)으로 그리므로 평소, 켜짐, 직접 고른 색, 흐림, 다크 어느 상태에서도 색이 실제 선 stroke와 같고 크기도 같다. 표식은 markerUnits=strokeWidth라 몸통 굵기에 비례해 커지므로 마커 안 선 굵기를 viewBox / size로 정규화해 이중으로 굵어지지 않게 한다(어떤 몸통 굵기에서도 머리 굵기 = 몸통 굵기). 꼭지는 선 끝 점에 놓여 몸통의 둥근 끝과 매끈히 이어지고 보이는 끝은 선 끝 점에서 몸통 굵기의 절반만 나온다. 관계선 1.75px에서 머리는 진행 방향 길이 약 6.8px, 가로 폭 약 9.6px의 채운 삼각형이다(ARM_X 1.1, ARM_HALF 2.25, 마커 좌표). 시작 끝 머리(both)는 같은 표식을 auto-start-reverse로 쓴다. 키보드 초점(:focus-visible)은 도형 자신의 윤곽을 --color-ui-focus 색과 outline-width.focus 굵기로 그리는 접근성용 굵기 예외다. 켜짐(light)의 강조 윤곽이나 도착 펄스와 별개의 표시이고, 같은 윤곽 모양(원통, 둥근 표)과 모서리를 쓰므로 도형의 크기와 배치는 그대로다. 브라우저 기본 사각 포커스 상자(outline)는 어떤 초점에서도 g 요소에서 끄고, :focus-visible이 아닌 초점은 아무 표시도 그리지 않는다. 값 변경 강조는 값 행의 정확한 행 모양(첫 행과 마지막 행은 카드 모서리에 맞춘 둥근 끝)으로만 그리고, 이 행 강조가 켜진 도형에는 도형 전체의 지금 면을 칠하지 않는다.

도형의 글자는 내장 Pretendard로 측정하고 같은 파일의 부분 글꼴로 출력한다. Things 웹의 시스템 글꼴 자체를 재배포하지 않는다. 제목 15px(카드 제목 regular, 그룹 제목 semibold), 문장과 이름표 13px, 작은 표시 11px semibold의 위계를 유지하며 조작부는 15px semibold다.

테이블과 클래스는 제목 영역을 color.figure.selection-fill 면으로 구분한다. 제목 면은 바깥 윤곽 안쪽에만 채워 모서리와 선 굵기를 바꾸지 않는다. PK·FK·UNQ는 강조 글자색으로 구분하고 자료형은 고정폭 보조 글자다.

user와 admin 아이콘은 같은 머리·어깨 비율과 둥근 아래 모서리를 공유한다. 사용자도 기본 의미 아이콘의 파랑 계열을 쓰며 별도 분홍 역할색을 두지 않는다. 소멸 X는 관계선과 같은 색·굵기, 둥근 끝을 쓰고 메시지 화살촉은 표식 앞에서 끝난다.

모바일 재생기와 목록 안 그림은 자연 크기를 유지하고 남는 가로 부분을 그림 영역 안에서 스크롤한다. 페이지와 조작부는 화면 폭을 유지한다. 화살촉 정의는 각 관계선 묶음 안에 두고 몸통과 같은 color를 상속한다. 평소와 켜진 상태에서 면과 윤곽이 몸통 색을 따라야 하고, 브라우저 엔진별 확인은 자동 시험이 없다(검증 요구사항, 미완료). 차트의 방향선은 선 묶음이 아니라 색 역할 class(.arrow-main, .arrow-muted)가 소유자이고, 그 class로 감싼 표식 정의가 역할마다 하나다(차트).

좁은 화면을 위한 배치 목표 폭

buildFigure(source, { layoutWidth })는 구조 그림의 배치 목표 폭을 받는다. 생략하면 종전 표준 캔버스 폭을 그대로 사용한다. 목표 폭은 CSS 축소 배율이 아니다. 같은 크기의 도형과 원본 연결을 ELK에 넘기고, 기존 방향 전환·접기 후보를 요청 폭 기준으로 평가한다. 선의 점을 사후 보정하지 않는다. 제목 회피와 안전 배치 재시도에도 같은 목표 폭을 전달한다.

양수인 유한수만 받으며 그래프 보기가 있는 문서에만 적용한다. 차트의 좁은 폭은 별도 값 chartWidth가 정한다(아래 HTML의 좁은 구조 배치). 들어가지 못하면 가장 좁은 유효 배치를 반환하고 layout-width 경고에 실제 폭과 목표 폭을 기록한다. strict에서는 출력 대신 실패한다. 표준 캔버스에서는 명시한 aspect를 유지한다. 그보다 좁은 목표 폭에서 원래 배치가 넘치면 폭을 우선하여 자동 맞춤을 허용한다. 안전 배치 규칙은 유지하며 폭에 맞추려고 글자나 도형을 줄이거나 도형을 생략하지 않는다.

CLI 옵션은 없다. HTML 생성이 이 재배치를 호출하고(아래 HTML의 좁은 구조 배치), 플레이어는 전달받은 시간표만 읽는다. 원본의 이동 시간이 생략되면 선 길이에 따른 자동 시간이 배치마다 달라질 수 있어, 서로 다른 배치의 시간표를 같은 시계에 무조건 교체하면 안 된다.

reflowFigure(result, { layoutWidth?, chartWidth?, strict?, budget? })는 이미 빌드한 그림의 사건 시간표를 보존하는 재배치 API다. layoutWidth는 그래프 보기가 있을 때만 쓰고, chartWidth는 차트와 시간축을 그리는 폭이다. 최초 빌드와 재배치는 build-scene.js의 크기·ELK 배치·글 상자·그림 검사 경로를 공유한다. 재배치에서는 이벤트 처리기를 다시 실행하지 않는다. 단계, 출발, 중간 도착, 대기 해제, 시간 초과, 값 기간, 카드 변경, 사라짐 시각을 원본에서 복사한다.

여러 선을 지나는 흐름은 track-geometry.js가 새 경로를 만들고 reflow-timeline.js가 도형 진입·진출 경계를 기존 거리 비율과 대응한다. 기존 속도 꺾은선의 모든 지점과 경계 시각을 함께 보존하는 꺾은선을 합성하므로 경로 길이가 달라져도 같은 시각에 같은 도형에 닿는다. 글 상자 위치와 충돌 숨김은 새 경로에서 다시 구한다. 연결 순서·끝점이 달라지거나 길이 0인 이음이 이동 구간으로 바뀌어 시각 보존이 불가능하면 layout-timing 오류로 거절한다. 원본 결과를 수정하지 않는다.

긴 이동 글 때문에 캔버스를 넓힐 때는 도형·선뿐 아니라 흐름의 경로 점도 같은 거리만큼 옮긴다. 그렇지 않으면 점이 실제 선 옆으로 지나간다. 이 정렬, 원본 불변, 사건 전체와 경계 시각의 보존을 직접 확인하는 자동 시험은 지금 없다(검증 요구사항, 미완료). 실제 Chrome HTML에서 원본 시각에 새 경계에 닿는 점과 값 변경도 따로 확인해야 한다. HTML의 자동 전환 계약은 아래에 따로 적는다.

HTML의 좁은 구조 배치

HTML 생성 때 좁은 배치의 목표 폭을 컨테이너 폭(spacing.figure.figure-compact-width에서 좌우 바깥 여백 spacing.6 둘을 뺀 값, 현재 272px, src/canvas.js의 COMPACT_WIDTH)으로 잡고 같은 그림을 다시 만든다. 그래프 보기와 차트 보기(plot), 시간 보기(time)가 다시 만드는 대상이다. 시간 보기는 레인 이름과 구간 이름을 막대 위로 줄바꿈하며 실제 시작 시각과 길이 비례를 유지한다. 그래프는 layoutWidth로 재배치하고, 차트는 chartWidth로 차트 그림 폭을 그 값 이하로 다시 그린다. 차트 보기 안의 차트와 그래프 카드 안의 차트 모두 이 폭 이하이고, 그래프 카드 안의 차트는 넓은 배치에서도 spacing.figure.chart.compact-width 이하다. 순서 보기는 다시 만들지 않고 자연 크기를 유지한다.

대체 SVG와 시간표는 어느 판이 컨테이너보다 넓고 다시 만든 결과가 그 판을 실제로 좁힐 때만 함께 넣는다. 좁은 배치는 후보일 뿐이며, 이동 글이 가려지거나 겹친다는 검사 7번 경고 수가 원본 배치보다 늘지 않을 때만 싣는다(원본과 비교하므로 원본에 이미 있는 경고는 벌점이 아니다). 그래프 판이 목표 폭을 여전히 넘으면서 원본보다 높아지는 후보도 싣지 않는다. 가로 탐색이 필요한 상태에서 세로 길이까지 늘어나는 것을 막는다. 그래프 재배치가 사건 시각을 보존하지 못해 layout-timing으로 거절되거나 이동 글 경고를 늘리거나 이 높이 조건을 어기면 그래프는 두고 차트만 좁힌 후보를 같은 기준으로 본다. 그 후보도 안 되면 좁은 배치 없이 기존 배치를 자연 크기로 유지한다. layout-timing이 아닌 오류는 숨기지 않고 그대로 낸다. 폭 초과나 비율 같은 다른 경고는 이 기준에 넣지 않는다. 사건, 시각, 값 기간, 차트 프레임의 순서와 시각은 원본 시간표와 같고 판마다의 기하만 다르다. 구조 그림의 검사 오류를 숨기고 대체 그림을 넣지 않는다. 좁혀지지 않으면 기존 배치를 그대로 두고 남는 가로 부분을 그림 영역에서 스크롤한다. 다시 만드는 쪽은 글자 크기를 줄이거나 차트의 이름·점을 생략하지 않는다.

전환 기준은 브라우저 창 자체가 아니라 그림 틀의 실제 폭이다. 그 폭이 공통 정본의 breakpoint.mobile보다 작고 원래 가장 넓은 판의 폭에도 못 미칠 때 좁은 배치를 선택한다. 도형과 글자를 강제로 줄이거나 예제별 좌표를 보정하지 않는다. 판은 자연 크기를 유지하고 남는 가로 부분을 그림 영역에서 스크롤한다. 화면 폭뿐 아니라 iframe이나 부모 요소의 폭이 바뀌어도 ResizeObserver가 같은 기준으로 다시 판정한다.

대체 SVG는 비활성 template에 보관한다. 표시 SVG는 항상 하나이며 자식 원소만 교체한다. 같은 시계와 조작부를 유지하고, 현재 구간·경과 시간으로 카드·상태·값·점 위치를 복원한다. 선택한 장면 탭과 키보드 초점을 바꾸지 않는다. 도착 강조도 현재 시각에서 이어가며 정지 중에 새로 재생하지 않는다. 전체 화면에서는 현재 배치를 고정하고 돌아왔을 때 다시 폭을 판정한다.

좁은 배치로의 전환, 초점, 중복 ID, 전체 화면 복귀는 브라우저에서 보이는 동작이라 npm test가 확인하지 않는다. 좁은 화면의 모양이 읽기 좋은지, 좁은 배치를 얻지 못하는 큰 그림과 차트 종류별 좁은 모양이 320·390·430px에서 읽히는지는 실제 Chrome에서 눈으로 보는 검수의 몫이다. 폭 390 라이트의 일부 예제와 폭 390 다크의 통합 예제를 본 범위는 표현 범위가 정하고, 흐름 예제에 좁은 배치가 실리지 않고 지표, 도넛, 통합 예제에는 실리는 것도 실제 HTML에서 확인했다. 그 밖의 폭과 종류의 읽힘은 확인하지 않았다(검증 요구사항, 미완료).

좁은 화면에서 나란한 도형의 층 배치

방향 전환과 자동 접기를 모두 시도해도 별도의 좁은 목표 폭에 들어가지 않으면, ELK의 COFFMAN_GRAHAM과 층당 도형 한 개 제한으로 세로 후보를 비교한다. 표준 캔버스 배치에는 이 후보를 추가하지 않는다. 중첩 그룹 내부의 방향·연결점은 유지하고 루트의 층만 바꾼다. 기존 후보보다 좁아지지 않으면 선택하지 않는다. 같은 조건의 형제는 COFFMAN_GRAHAM이 순서를 정하지 못해 선언 순서가 뒤집히므로, 이 후보에만 ELK 분할(elk.partitioning)로 층 순서를 자식 순서(src/layout/order.js)에 맞춘다. 분할은 층 나누기 단계의 제약이고 모형의 연결이 아니며 그린 선에 남지 않는다. 선 경로를 사후 보정하거나 생산자 사이에 가짜 연결을 추가하지 않는다. 세로 위치는 실행 순서를 뜻하지 않으며 동시 요청·대기·예약은 기존 시간표를 따른다.

제약이 있는 스키마 행

measure/table.js가 열 이름·키·타입의 첫 줄과 NULL·삭제 정책의 보조 줄을 측정해 tableRows를 만든다. 행마다 필요한 제약 줄만큼 높이가 늘어난다. SVG, 이동 글상자의 장애물, 글자 넘침 검사, ELK 연결점, 선 끝 검사와 카드 시작 위치가 이 배치를 공유한다. 열 이름과 모든 제약을 한 줄에 붙이던 표시를 제거했다. 접근성 설명에는 전체 열 정보와 키의 열 묶음·참조 대상이 남는다.

테이블 키는 source/table-check.js에서 keys와 foreignKeys로 정규화한다. 열별 약식 속성은 정규화 뒤 제거해 두 모형을 유지하지 않는다. 연결선의 fromColumns와 toColumns는 단일 열도 배열로 표현한다. 복합 외래 키 하나는 공통 선 하나이며 연결점은 포함된 첫 행과 마지막 행 중심 사이에 놓는다. 이 높이는 table.js의 columnAnchorY를 배치와 선 끝 검사가 함께 사용한다. 키 표식과 연결선 라벨도 기존 글 부품으로 측정하고 그리며 별도 테이블 렌더러를 두지 않는다.

이동을 포함한 HTML 보기 영역

고정 여백을 잘라내는 tightView만 구조 그림에 적용하면 여백 안으로 움직이는 글상자도 잘렸다. html/view-bounds.js는 기본 보기 영역에 모든 이동 점의 경로와 후광, 글상자의 전체 계획 영역, 상태 알약의 돌출 범위를 합친다. 직선 경로 구간과 글상자 계획 구간을 함께 나누고 점 좌표 범위와 상대 이동 범위를 더해 구간 중간의 돌출도 포함한다. 재생 중 DOM을 재측정하거나 보기 크기를 바꾸지 않는다. 차트는 기존 보기 영역을 쓴다.

병합된 비트 필드의 모바일 폭

한 행을 두 열 이상인 병합 필드들이 빈틈없이 채우는 격자는 비율형 필드 띠로 측정한다. 기존의 논리 단위당 최소 폭 적용을 이 경우에만 보이는 필드의 최소 폭으로 바꾼다. 제목과 필드 비율은 유지하며, 긴 필드 글을 줄 나누어 선호 폭 안에 담는다. 소수 단위 폭을 허용하므로 논리 비트 수를 백만으로 늘려도 같은 두 필드를 물리적으로 백만 칸 너비로 펼치지 않는다. 개별 셀·빈 칸·다중 행의 기존 배치 규칙은 유지한다.

다중 행 격자의 긴 설명

각 칸의 글 폭만 제한하면 여러 열을 차지하는 긴 글 하나가 격자의 모든 열을 넓혔다. pte-fields에서는 권한 (읽기, 쓰기, 실행)이 단위 폭을 약 42.23px로 요구해 올림 후 모든 열이 43px가 됐다. 일반 격자에도 전체 선호 폭을 비교하되 최소 단위 칸 크기와 제목 폭은 지킨다. 선호 폭에 맞추기 위해 칸 수·행 수·빈 칸·병합 비율을 바꾸지 않고, 글을 줄 나누어 행 높이를 계산한다. 선 경로는 바뀐 실제 칸 크기에서 다시 배치한다.

좁은 목표 폭에서 이 격자의 한 칸 최소 폭은 32px이고, 열 비율과 빈 칸은 유지되며 설명 글은 생략하지 않는다.

좁은 층 배치의 좌우 밀림

한 층에 도형 하나를 두어도 기본 위치 계산은 선을 곧게 만들려고 도형을 좌우로 옮겨 장면 폭이 넓어졌다. 좁은 폭 전용 층 후보의 루트에만 ELK의 nodePlacement.strategy=SIMPLE을 적용한다. 그 밖의 배치(안전 배치는 제외)는 NETWORK_SIMPLEX를 쓴다(src/layout/elk.js). 지원하는 위치 전략은 ELK 공식 문서를 따른다.

여러 줄 대기 큐

큐는 spacing.figure.node.max-width 안에 들어가는 칸 수로 행을 나눈다. 이름과 칸 묶음은 같은 크기 계산을 사용하며, 빈 칸과 값별 찬 칸도 queueSlots가 만든 좌표를 공유한다. 한 줄을 강제하던 폭 계산은 제거했다. 칸 크기·개수·논리 용량은 줄이지 않고 각 행의 왼쪽부터, 위에서 아래로 채운다. 마지막 행은 앞 행의 왼쪽에 맞춘다.

명시한 비율과 모바일 목표 폭

aspect가 있으면 모든 자동 맞춤을 막던 조건을 좁은 목표 폭에서는 적용하지 않는다. 원래 배치가 목표 폭에 들어가면 그대로 쓰고, 표준 캔버스보다 좁은 목표를 넘었을 때만 기존 방향 전환·접기 후보를 비교한다. 표준 SVG와 데스크톱 기본 배치는 원래 비율을 유지한다. 원본의 aspect 값과 도형·색·연결·시간표를 수정하지 않는다.

세로 그룹의 좁은 층 후보

루트만 한 층씩 배치해도 그룹 안의 형제 도형은 가로로 남았다. 좁은 목표 폭에 들어오지 않을 때는 기존 세로 그룹 내부에도 한 층씩 놓는 후보를 비교한다. 그 후보가 목표 폭을 넘더라도 기존 배치보다 좁으면 폭 우선 비교에 포함한다. 루트와 그룹이 같은 ELK 옵션 정의를 공유한다. 그룹의 포함 관계와 원본 방향, 도형 크기, 연결 관계는 바꾸지 않으며 가로 방향 그룹에는 적용하지 않는다. 그룹 안의 형제도 같은 분할로 자식 순서를 지키며, ELK가 연결되지 않은 덩어리로 나눈 그룹은 덩어리 안에서만 순서가 적용된다. 경로 점을 사후 보정하지 않는다.

그룹까지 세로로 풀었는데도 목표 폭을 넘으면 새 후보를 버리고 최종 장면은 기존 크기를 유지한다. 후보 선택 뒤에는 기존 장면·시간표 검사를 거치며, 폭에 들어온다는 이유로 겹침 오류를 허용하지 않는다.

카드 태그와 모바일 읽기 폭

카드 태그, 번호, 표식, 열 키는 작은 표시 역할(text.figure-meta 11px)이고 그려지는 가장 작은 글이 11px이다. HTML의 일반 보기에서는 이 자연 크기를 유지한다. SVG 이미지의 폭 맞춤과 전체화면 전체 보기에서는 그림과 함께 작아질 수 있다.

표의 PK·FK는 카드 태그와 별도의 키 역할(STYLE.key)을 쓴다. 표 크기·충돌 검사·글자 장애물이 모두 이 역할을 사용하므로 카드 태그를 바꿔도 표의 폭이나 경로가 바뀌지 않는다.

예제 구성의 화면 검수

예제(examples/)의 모든 장면은 폭 320·390·430·1440에서 기하(가로 넘침, 판 경계)를 쟀고, 눈으로 본 배치는 일부뿐이다. 확인한 범위는 표현 범위의 표가 정하고 나머지는 검증 요구사항, 미완료다. HTML의 판과 글자는 자연 크기를 유지하며 남는 가로 부분은 그림 영역에서 스크롤한다. 페이지 전체는 가로로 넘치지 않는다. 이 규칙의 자동 회귀 범위는 test/browser/narrow-view.test.js가 확인하는 architecture와 sequence이며, 다른 예제의 현재 규칙 검수는 별도다.

삽입된 HTML은 본문 크기가 바뀔 때 높이를 전달하며, 부모의 테마 메시지에도 현재 높이를 다시 전달한다. 부모가 늦게 연결되어 최초 크기 알림을 놓쳐도 프레임 높이를 복구한다.

정보 역할별 내부 구획

스키마·격자는 필드 및 셀 경계가 의미이므로 구분선을 항상 표시한다. 표 머리는 옅은 중립 면으로 본문과 구분한다. 이름–값 지표 카드는 평소 배경과 내부 선을 추가하지 않고 여백과 정렬로 구분하며, 값 갱신 때만 공통 강조 면이 나타난다. 차트 면과 이동 라벨은 같은 범주 solid/on-solid 쌍을 쓴다. 진한 면은 흰 글자, 밝은 노랑은 어두운 글자이며 별도 흐름 팔레트는 두지 않는다. 스키마 내부 선·신뢰구간·목표는 축과 같은 color.data.grid의 옅은 회색을 쓴다.

그룹의 명시한 아이콘은 제목·배지와 같은 장식 줄에 놓고 spacing.1만큼 띄운다. 별도 아이콘 탭 공간은 예약하지 않으며, 제목 묶음 전체를 재고 선과 충돌하지 않게 함께 이동한다. 제목만으로 구분되는 프런트·서비스·데이터 예제는 그룹 아이콘을 생략한다.