@tienne/gestalt 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/package.json +1 -1
- package/dist/plugin/skills/architecture/SKILL.md +23 -5
- package/dist/schemas/architecture-ir.schema.json +54 -1
- package/dist/src/architecture/flow-layout.d.ts +16 -1
- package/dist/src/architecture/flow-layout.d.ts.map +1 -1
- package/dist/src/architecture/flow-layout.js +108 -28
- package/dist/src/architecture/flow-layout.js.map +1 -1
- package/dist/src/architecture/html-client.d.ts +5 -0
- package/dist/src/architecture/html-client.d.ts.map +1 -1
- package/dist/src/architecture/html-client.js +217 -15
- package/dist/src/architecture/html-client.js.map +1 -1
- package/dist/src/architecture/html-renderer.d.ts.map +1 -1
- package/dist/src/architecture/html-renderer.js +279 -25
- package/dist/src/architecture/html-renderer.js.map +1 -1
- package/dist/src/architecture/html-theme.d.ts +7 -0
- package/dist/src/architecture/html-theme.d.ts.map +1 -1
- package/dist/src/architecture/html-theme.js +96 -0
- package/dist/src/architecture/html-theme.js.map +1 -1
- package/dist/src/architecture/ir-schema.d.ts +124 -27
- package/dist/src/architecture/ir-schema.d.ts.map +1 -1
- package/dist/src/architecture/ir-schema.js +24 -2
- package/dist/src/architecture/ir-schema.js.map +1 -1
- package/dist/src/architecture/merge.d.ts.map +1 -1
- package/dist/src/architecture/merge.js +9 -0
- package/dist/src/architecture/merge.js.map +1 -1
- package/dist/src/architecture/sequence-views.d.ts +155 -0
- package/dist/src/architecture/sequence-views.d.ts.map +1 -0
- package/dist/src/architecture/sequence-views.js +407 -0
- package/dist/src/architecture/sequence-views.js.map +1 -0
- package/dist/src/architecture/types.d.ts +23 -1
- package/dist/src/architecture/types.d.ts.map +1 -1
- package/dist/src/architecture/types.js +2 -0
- package/dist/src/architecture/types.js.map +1 -1
- package/dist/src/architecture/validator.d.ts +2 -1
- package/dist/src/architecture/validator.d.ts.map +1 -1
- package/dist/src/architecture/validator.js +119 -0
- package/dist/src/architecture/validator.js.map +1 -1
- package/dist/src/mcp/schemas.d.ts +6 -6
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/skills/architecture/SKILL.md +23 -5
- package/schemas/architecture-ir.schema.json +54 -1
package/README.md
CHANGED
|
@@ -962,6 +962,8 @@ Claude Code (you)
|
|
|
962
962
|
└──────────────────────────────────┘
|
|
963
963
|
```
|
|
964
964
|
|
|
965
|
+
**Interactive map:** [`docs/architecture/screen-chain.html`](./docs/architecture/screen-chain.html) shows how this repo fits together, from clients and skills through MCP tools and handlers down to engines and storage. Gestalt drew it with its own `architecture` skill, and every solid line points to the source line it came from. GitHub shows HTML files as source, so download the file and open it in a browser.
|
|
966
|
+
|
|
965
967
|
**Further reading:**
|
|
966
968
|
|
|
967
969
|
- [MCP Reference](./docs/mcp-reference.md) — all tools, parameters, and action schemas
|
package/dist/package.json
CHANGED
|
@@ -337,7 +337,7 @@ AI 클라이언트가 플러그인으로 읽어 들이는 스킬과 에이전트
|
|
|
337
337
|
],
|
|
338
338
|
"stateLabels": { "WAITING": "대기", "CALLED": "호출" },
|
|
339
339
|
"transitions": [
|
|
340
|
-
{ "id": "t-1", "from": "st-register", "to": "st-call", "path": "main", "
|
|
340
|
+
{ "id": "t-1", "from": "st-register", "to": "st-call", "path": "main", "condition": "차례가 오면",
|
|
341
341
|
"evidence": [ ... ], "lineStyle": "solid" }
|
|
342
342
|
]
|
|
343
343
|
}
|
|
@@ -346,9 +346,13 @@ AI 클라이언트가 플러그인으로 읽어 들이는 스킬과 에이전트
|
|
|
346
346
|
- **행위자**는 가로줄 하나씩이다. 사람은 `person`, 사람 손 없이 도는 배치나 자동 발송은 `system`, 지시를 읽고 스스로 판단하는 서브에이전트는 `agent`다. 위에서 아래로 적은 순서대로 쌓인다.
|
|
347
347
|
- **단계**는 행위자가 하는 일 하나다. 상태 값이 있으면 `state`에 코드의 enum 이름 그대로 적는다. 그림 위쪽 구간이 이 값으로 나뉜다. 그림에는 enum 이름 대신 흐름의 `stateLabels`에 단 이름이 찍힌다. 상태 값마다 사용자 언어로 이름을 단다 (`WAITING` → 대기). 안 달면 영어 enum이 구간 머리와 단계 칩에 그대로 나온다. 상태가 없는 단계는 앞 단계 구간에 붙고 옆 흐름 단계는 갈라져 나온 단계 옆에 서니 구간을 따로 적지 않는다. `refs`에는 그 단계에서 쓰는 화면, API, 기능영역, 앱 노드 id를 단다. 범용 `component` 노드도 단다. 단계 서랍에서 그 카드로 건너가고 기술 카드 서랍에는 거꾸로 "이 항목이 나오는 흐름 단계"가 뜬다.
|
|
348
348
|
- **전이**는 단계 사이 상태 변화다. 정상 흐름은 `main`, 취소나 노쇼처럼 정상 흐름을 벗어나는 전이는 `side`다.
|
|
349
|
-
-
|
|
349
|
+
- **선 위 글자는 `trigger`와 `condition`에 나눠 적는다.** 사용자가 누른 버튼 이름은 `trigger`(`주문 변경하기`), 그 길로 가는 조건은 `condition`(`차례가 오면`, `30분 이내`)이다. 그림에는 조건이 굵은 강조색으로 앞에 서고 누른 것이 뒤따른다. `label`은 예전 칸이다. 둘 중 하나라도 있으면 `label`은 선에 안 나오고 전이 서랍의 **메모** 줄로 간다. 새로 쓰는 IR에서는 `label`을 비우고 짧은 메모가 필요할 때만 쓴다.
|
|
350
|
+
- **화면 흐름을 그릴 때도 같다.** 단계는 화면이나 시트 하나이고 전이의 `trigger`에 그 화면에서 누른 것을, `condition`에 길이 나뉘는 조건을 적는다. `trigger`는 버튼 이름 그대로 짧게 쓴다. 열 간격이 선 글자 폭을 따라 넓어지지만 한글 열두 자쯤에서 멈춰서 더 긴 글자는 이웃 카드 위로 넘친다. 화면에서 하는 일의 설명은 단계 `description`에 둔다.
|
|
351
|
+
- **조건에 따라 길이 나뉘는 자리는 갈림길 단계로 적는다.** `"kind": "decision"`을 달면 카드 대신 마름모로 그려진다. 단계 이름은 `품절 상품 포함?`처럼 묻는 꼴로 쓰고 나가는 전이마다 `condition`을 단다 (`예`, `아니오`). 같은 높이의 카드로 가는 길은 오른쪽 꼭짓점에서, 위아래로 다른 높이의 카드로 가는 길은 위나 아래 꼭짓점에서 나간다. 갈림길은 흐름이 끝나는 자리가 아니라서 `terminal: true`를 함께 달면 스키마 위반이다. validate는 나가는 길이 하나뿐인 갈림길에 `auto:branch`를, 근거는 있는데 `condition`도 `label`도 없는 나가는 전이에 `auto:condition` 질문을 남긴다.
|
|
352
|
+
- **되돌리기와 정정은 단계가 아니라 전이로 적는다.** 새 상태가 생기지 않고 앞 상태로 돌아가기만 해서다. "되돌리기" 카드를 따로 만들지 말고 `매장 취소 → 호출`처럼 돌아가는 화살표 하나로 쓴다. 누른 것과 조건은 나눠 적는다 (`"trigger": "되돌리기", "condition": "30분 이내"`). 그림에서는 두 카드 가까이로 지나가는 둥근 선에 ↩ 표시가 붙는다.
|
|
350
353
|
- **흐름이 끝나는 단계에는 `"terminal": true`를 단다.** 착석, 취소, 만료처럼 더 갈 곳이 없는 단계다. 나가는 전이가 없는데 이 표시도 없으면 validate가 `auto:dead-end` 질문을 남긴다. 미루기처럼 다시 줄로 돌아가는 단계가 선을 빠뜨렸을 때 그림이 거기서 끝난 것처럼 읽히는 걸 막으려는 표시다. 그 질문이 뜨면 끝 단계로 표시하기 전에 이어지는 전이를 코드에서 먼저 찾는다. 되돌릴 수 있는 취소처럼 끝 단계에서 나가는 전이가 있어도 된다.
|
|
351
354
|
- **정정도 같다.** 자동 노쇼를 착석으로 고치는 "노쇼 정정"은 `자동 노쇼 → 착석` 전이에 `actors: ["ops"]`다. 설명에 갈 수 있는 곳이 여럿 적혀 있으면 (착석이나 고객 취소) 전이도 그만큼 긋는다. validate가 이런 단계를 찾아 `auto:as-transition` 질문을 남긴다. 결제처럼 화면이 따로 있어 `refs`로 건너갈 일이 있는 단계는 카드로 둬도 된다.
|
|
355
|
+
- **행위자가 한 명이어도 된다.** 손님 혼자 화면을 넘기는 흐름이면 `actors`에 하나만 적는다. 그때는 줄을 가를 일이 없어 왼쪽 행위자 머리 칸을 걷고 그림 맨 위 띠에 행위자 칩 하나만 세운다.
|
|
352
356
|
- **여러 행위자가 할 수 있는 전이에는 `actors`를 단다.** 되돌리기를 손님도 매장도 할 수 있으면 `"actors": ["guest", "staff"]`다. 선 글자 옆에 행위자 이름이 붙는다. 행위자 줄은 그 사람이 하는 일을 놓는 자리라, 여럿이 하는 동작을 한 줄에 단계로 넣으면 틀린 그림이 된다.
|
|
353
357
|
- 근거 규칙은 엣지와 같다. 상태를 바꾸는 코드 줄을 봤으면 `code` 근거로 실선이다. 기획 문서나 KB로만 확인했으면 `doc` 근거로 점선이다. 그 단계 카드도 점선 테두리가 된다. 근거 없는 단계나 전이는 그리지 않고 질문이 된다.
|
|
354
358
|
- **기획 문서에만 있는 단계도 넣는다.** 아직 안 만든 기능이나 만들다 만 기능이 흐름 그림에서 같이 보여야 기술 그림과의 차이가 드러난다. 대신 근거는 `doc`뿐이라 점선이다.
|
|
@@ -357,6 +361,7 @@ AI 클라이언트가 플러그인으로 읽어 들이는 스킬과 에이전트
|
|
|
357
361
|
- `participants`에는 그 경로에 나오는 스킬, 에이전트, MCP 서버만 10개 안팎으로 고른다. MCP 서버 자리에는 도구 처리기 `app_module`이나 도구 `endpoint`를 쓴다.
|
|
358
362
|
- 메시지는 지도 엣지를 `edge`로 가리킨다. 문서 읽기처럼 지도에 선이 없는 단계는 자기 호출 메시지로 쓰고 그 SKILL.md 줄을 근거로 단다. 응답은 `reply: true`다. 조건이 맞을 때만 도는 단계는 `blocks`의 `opt`로 묶고 경우에 따라 하나만 도는 단계는 `alt`로 묶는다.
|
|
359
363
|
- 세션이 도구를 두 번 불러 결과를 넘기는 2-Call Passthrough도 순서도에 그린다. 세션 모델과 MCP 서버를 오가는 메시지로 쓴다.
|
|
364
|
+
- 구간은 [`phases`](#질문별-그림을-얹는다)의 sequence 구간 규칙대로 직접 적는다.
|
|
360
365
|
- 지도는 그대로 그린다. 무엇이 어느 플러그인에 있는지는 지도가 보여준다. 전체보기 지도 위에 질문별 그림 카드 줄이 뜨니 순서도가 몇 개인지는 위쪽 바를 안 눌러도 보인다.
|
|
361
366
|
- IR의 `repos`가 둘 이상이면 순서도 머리 카드 둘째 줄에 노드의 레포 이름이 나온다. MCP 도구는 레포 대신 `<mcpServer> MCP`로 나온다. 카드에 그대로 찍히니 `participants`에 넣은 노드의 `repo`가 실제 레포와 맞는지 한 번 더 본다. MCP 도구에는 `mcpServer`를 빠뜨리지 않는다. 빠지면 도구를 기록한 레포 이름이 대신 나와 어느 서버의 도구인지 헷갈린다.
|
|
362
367
|
- `flows`는 사람 쪽 업무 절차가 따로 있을 때만 쓴다. 그때는 사용자가 `person`, MCP 서버가 `system`, 세션 모델과 서브에이전트가 `agent`다. `refs`에는 스킬과 에이전트 노드 id도 단다.
|
|
@@ -513,7 +518,7 @@ FE가 부르는 BE 레포나 배포 매니페스트 레포처럼 지금 레포
|
|
|
513
518
|
|
|
514
519
|
### 페이지 글
|
|
515
520
|
|
|
516
|
-
페이지에 그대로 보이는 글은 **사용자가 이 분석을 시킨 프롬프트의 언어로** 쓴다. 코드와 주석, 기획 문서가 영어여도 옮겨 쓴다. 미해결 질문(`question`), 노드 `description`, 기능영역 이름, `displayName`, 구간 `label`, 흐름의 `title`과 `description`, 행위자와 단계와 전이의 `label`, `stateLabels` 값이 그 자리다. 코드 식별자인 노드 `label`과 단계 `state`만 코드에 있는 그대로 둔다. 한국어라면 읽는 사람에게 말하는 해요체로 쓴다. 번역투나 AI 말투는 피하고 [`ai-tell-quick-rules.md`](../../role-agents/_shared/references/ai-tell-quick-rules.md)를 따른다.
|
|
521
|
+
페이지에 그대로 보이는 글은 **사용자가 이 분석을 시킨 프롬프트의 언어로** 쓴다. 코드와 주석, 기획 문서가 영어여도 옮겨 쓴다. 미해결 질문(`question`), 노드 `description`, 기능영역 이름, `displayName`, 구간 `label`, 흐름의 `title`과 `description`, 행위자와 단계와 전이의 `label`, 전이의 `trigger`와 `condition`, `stateLabels` 값이 그 자리다. 코드 식별자인 노드 `label`과 단계 `state`만 코드에 있는 그대로 둔다. 한국어라면 읽는 사람에게 말하는 해요체로 쓴다. 번역투나 AI 말투는 피하고 [`ai-tell-quick-rules.md`](../../role-agents/_shared/references/ai-tell-quick-rules.md)를 따른다.
|
|
517
522
|
|
|
518
523
|
### 근거 규칙
|
|
519
524
|
|
|
@@ -602,6 +607,10 @@ FE가 부르는 BE 레포나 배포 매니페스트 레포처럼 지금 레포
|
|
|
602
607
|
"messages": [
|
|
603
608
|
{ "id": "m1", "from": "app", "to": "api", "label": "POST /orders/pay", "edge": "e-app-api", "evidence": [], "lineStyle": "solid" },
|
|
604
609
|
{ "id": "m2", "from": "pg", "to": "api", "label": "승인됨", "edge": "e-api-pg", "evidence": [], "lineStyle": "solid", "reply": true, "block": "b-result", "branch": "승인" }
|
|
610
|
+
],
|
|
611
|
+
"phases": [
|
|
612
|
+
{ "id": "p-pay", "label": "결제 요청", "from": "m1", "to": "m1" },
|
|
613
|
+
{ "id": "p-result", "label": "승인 결과 받기", "from": "m2", "to": "m2" }
|
|
605
614
|
]
|
|
606
615
|
}
|
|
607
616
|
```
|
|
@@ -612,6 +621,10 @@ FE가 부르는 BE 레포나 배포 매니페스트 레포처럼 지금 레포
|
|
|
612
621
|
- 엣지로 안 잡히는 일(같은 노드 안의 검증, 문서에만 적힌 단계)은 `edge` 없이 자기 `evidence`를 단다. 자기 호출은 `from`과 `to`를 같게 쓴다.
|
|
613
622
|
- 근거가 하나도 없는 메시지는 그려지지 않고 `auto:message:<id>` 질문이 된다. 메시지를 지어내 채우지 말고 Step 7로 넘긴다.
|
|
614
623
|
- **sequence**: `participants`로 세로줄 순서를 정한다. 적으면 메시지 끝이 전부 여기 있어야 한다. 묶음은 `blocks`에 두고 메시지 `block`으로 건다. `alt`는 경우에 따라 하나만 도는 묶음, `opt`는 조건이 맞을 때만 도는 묶음, `loop`는 되풀이, `par`는 동시에 도는 묶음이다. `alt` 안의 경우 이름은 `branch`에 적는다. 한 묶음의 메시지는 붙여서 적는다.
|
|
624
|
+
- **sequence 구간**: `phases`는 순서도를 단계로 나눈다. 그림 위 보기 전환의 **단계별 카드**와 **따라가기**가 이 구간대로 카드를 나누고 띠를 깐다. 구간 하나는 `{ id, label, from, to }`이고 `from`과 `to`는 이 그림의 메시지 id다. 두 메시지 다 그 구간에 든다.
|
|
625
|
+
- `label`은 사람이 읽는 단계 이름이다. 노드 id나 도구 이름(`ges_interview`) 말고 "요구사항 인터뷰", "스펙 만들기"처럼 그 구간에서 무슨 일이 벌어지는지를 쓴다. [페이지 글](#페이지-글) 규칙을 따른다.
|
|
626
|
+
- 첫 메시지부터 마지막 메시지까지 빈틈도 겹침도 없이 메시지 순서대로 적는다. 배열 순서도 메시지 순서와 같아야 하고 한 메시지는 한 구간에만 든다. 구간 경계가 묶음 한가운데를 지나는 건 괜찮다.
|
|
627
|
+
- 안 적으면 render가 알아서 자른다. 맨 위 참여자가 지금 구간에서 처음 만나는 대상을 부르는 자리에서 새 구간이 열리고 구간 이름은 그 대상의 표시 이름이다. 그래서 모든 호출이 한 진입점을 거치는 흐름은 구간 하나로 뭉친다. 세션 모델이나 스킬 하나가 모든 호출을 내보내는 하네스 순서도가 그렇다. 같은 대상을 여러 번 오가는 흐름은 같은 이름 구간이 되풀이된다. 이런 흐름과 참여자가 많은 순서도에는 `phases`를 적는다.
|
|
615
628
|
- **dataflow**: `messages`만 쓴다. 데이터가 한 노드에서 다른 노드로 옮겨 가는 것 하나가 메시지 하나다. `blocks`와 메시지의 `reply`, `block`, `branch`는 쓰지 않는다.
|
|
616
629
|
- **compare**: `messages`는 빈 배열로 두고 `sides`에 견줄 두 묶음을 적는다. `{ id, label, nodes[] }` 둘이고 id는 달라야 한다. 그림은 "첫 묶음에만", "둘 다", "둘째 묶음에만" 세 열로 선다.
|
|
617
630
|
- 질문 하나에 그림 하나다. 질문이 여럿이면 그림도 여럿 단다.
|
|
@@ -651,7 +664,7 @@ FE가 부르는 BE 레포나 배포 매니페스트 레포처럼 지금 레포
|
|
|
651
664
|
|
|
652
665
|
- 성공하면 `{ ok: true, autoUnresolved, drawable }`이다. `drawable`에 빠진 노드와 엣지가 있으면 왜 빠졌는지 `autoUnresolved`에서 확인한다.
|
|
653
666
|
- 성공해도 `warnings`가 올 수 있다. 지금은 `NODE_DESCRIPTION_MISSING` 하나이고 그리는 노드 중 `description`이 빈 노드를 `nodeIds`에 모아 준다. render는 그대로 되지만 그 카드에는 설명 줄이 없다. [노드 설명](#노드-설명)대로 채운다. render 응답에도 같은 `warnings`가 실린다.
|
|
654
|
-
- 실패하면 `errors[]`에 `code`와 `nodeId`나 `edgeId`가 온다. 질문별 그림 에러면 `projectionId`와 `messageId`가 온다.
|
|
667
|
+
- 실패하면 `errors[]`에 `code`와 `nodeId`나 `edgeId`가 온다. 질문별 그림 에러면 `projectionId`와 `messageId`가 오고 구간 에러면 `phaseId`가 함께 온다.
|
|
655
668
|
|
|
656
669
|
| 에러 코드 | 고치는 법 |
|
|
657
670
|
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -684,6 +697,11 @@ FE가 부르는 BE 레포나 배포 매니페스트 레포처럼 지금 레포
|
|
|
684
697
|
| `SOLID_MESSAGE_WITHOUT_EVIDENCE` | 가리킨 엣지나 자기 근거에 code나 spec이 없다. 근거를 찾아 달거나 점선으로 바꾼다 |
|
|
685
698
|
| `PROJECTION_EMPTY` | sequence와 dataflow에 메시지를 넣는다. 넣을 메시지가 없으면 그 그림을 뺀다 |
|
|
686
699
|
| `PROJECTION_SHAPE_FIELD` | 모양에 안 맞는 필드를 뺀다. [질문별 그림을 얹는다](#질문별-그림을-얹는다)의 모양별 설명을 본다 |
|
|
700
|
+
| `DUPLICATE_PHASE_ID` | 한 그림 안에서 구간 id가 겹친다. 다시 짓는다 |
|
|
701
|
+
| `PROJECTION_PHASE_MESSAGE_NOT_FOUND` | 구간 `from`이나 `to`를 그 그림의 메시지 id로 고친다. 다른 그림의 메시지는 못 가리킨다 |
|
|
702
|
+
| `PROJECTION_PHASE_REVERSED` | `from`이 `to`보다 뒤 메시지다. 메시지 순서대로 앞쪽을 `from`에 둔다 |
|
|
703
|
+
| `PROJECTION_PHASE_OVERLAP` | 구간이 겹치면 다음 구간을 앞 구간의 `to` 바로 다음 메시지에서 시작한다. 구간 배열이 메시지 순서를 안 따르면 배열을 메시지 순서대로 다시 적는다 |
|
|
704
|
+
| `PROJECTION_PHASE_GAP` | 첫 구간은 첫 메시지에서 시작하고 마지막 구간은 마지막 메시지에서 끝낸다. 어느 구간에도 안 든 메시지는 앞뒤 구간에 넣는다. 배열이 메시지 순서대로인지도 함께 본다 |
|
|
687
705
|
|
|
688
706
|
**최대 3회까지 고쳐 다시 validate한다.** 세 번째에도 같은 노드나 엣지에서 실패하면 더 붙잡지 않는다. 그 노드나 엣지를 IR에서 빼고 `unresolved`에 무엇을 왜 확인 못 했는지 질문으로 남긴다.
|
|
689
707
|
|
|
@@ -733,7 +751,7 @@ render는 validate를 다시 하고 이전 실행 IR과 병합해 노드 id를
|
|
|
733
751
|
| 서비스 (`service:<id>`) | 그 서비스의 기능영역과 서비스에 바로 단 화면, 거기서 닿는 `gateway`와 서버. 서비스를 서빙하는 버킷이 있으면 왼쪽에 도메인, CDN, 버킷 레인이 붙고 서비스 카드가 기능영역 레인 맨 위에 선다 |
|
|
734
752
|
| 기능영역 (`feature:<id>`) | 그 기능영역의 화면, 화면이 부르는 엔드포인트, 거쳐 가는 `gateway`와 받는 모듈 |
|
|
735
753
|
| 서버 (`server:<id>`) | 그 모듈이나 `gateway`에 걸린 엔드포인트, 읽고 쓰는 테이블, 쓰는 클라이언트 |
|
|
736
|
-
| 흐름 (`flow:<id>`) | Step 3.5의 흐름 하나. 행위자 가로줄 위에 단계 카드가 왼쪽에서 오른쪽으로 선다. 옆 흐름은 정상 흐름과 다른 색 선이다. 서비스 레벨의 **흐름** 버튼으로 들어간다. `service` 없는 흐름은 전체 바로 아래 서고 전체 레벨의 **흐름** 버튼으로 들어간다 |
|
|
754
|
+
| 흐름 (`flow:<id>`) | Step 3.5의 흐름 하나. 행위자 가로줄 위에 단계 카드가 왼쪽에서 오른쪽으로 선다. 옆 흐름은 정상 흐름과 다른 색 선이다. 갈림길은 마름모로 서고 행위자가 한 명이면 가로줄 대신 맨 위에 칩 하나가 선다. 서비스 레벨의 **흐름** 버튼으로 들어간다. `service` 없는 흐름은 전체 바로 아래 서고 전체 레벨의 **흐름** 버튼으로 들어간다 |
|
|
737
755
|
| 묶음 (`group:<id>`) | `infra`, `data`, `process` 팩에서 자식을 가진 노드 하나. 바로 아래 자식만 보이고 손자 사이 선은 자식 카드 사이 선으로 묶인다. 바깥과 잇는 카드는 보내기만 하면 왼쪽, 받으면 오른쪽에 서고 한 열에 네 장이 넘으면 옆 열로 넘어간다 |
|
|
738
756
|
| 질문 (`view:<id>`) | 질문별 그림 하나. 전체 바로 아래 서고 전체 레벨 지도 위의 카드 줄이나 위쪽 바의 **질문별 그림** 버튼으로 들어간다. 메시지를 누르면 보내는 쪽, 받는 쪽, 근거가 나오고 지도의 카드로 건너간다 |
|
|
739
757
|
|
|
@@ -473,14 +473,27 @@
|
|
|
473
473
|
"id": { "type": "string", "minLength": 1 },
|
|
474
474
|
"actor": { "type": "string", "minLength": 1 },
|
|
475
475
|
"label": { "type": "string", "minLength": 1 },
|
|
476
|
+
"kind": {
|
|
477
|
+
"type": "string",
|
|
478
|
+
"enum": ["step", "decision"],
|
|
479
|
+
"description": "decision is a branch drawn as a diamond. Omit for a plain step"
|
|
480
|
+
},
|
|
476
481
|
"description": { "type": "string" },
|
|
477
482
|
"state": { "type": "string", "minLength": 1 },
|
|
483
|
+
"terminal": {
|
|
484
|
+
"type": "boolean",
|
|
485
|
+
"description": "the flow may end here. Not allowed on a decision step"
|
|
486
|
+
},
|
|
478
487
|
"refs": {
|
|
479
488
|
"type": "array",
|
|
480
489
|
"description": "ids of screen, endpoint, feature or micro_app nodes this step touches",
|
|
481
490
|
"items": { "type": "string", "minLength": 1 }
|
|
482
491
|
},
|
|
483
492
|
"evidence": { "type": "array", "items": { "$ref": "#/definitions/evidence" } }
|
|
493
|
+
},
|
|
494
|
+
"if": { "properties": { "kind": { "const": "decision" } }, "required": ["kind"] },
|
|
495
|
+
"then": {
|
|
496
|
+
"not": { "properties": { "terminal": { "const": true } }, "required": ["terminal"] }
|
|
484
497
|
}
|
|
485
498
|
},
|
|
486
499
|
"flowTransition": {
|
|
@@ -491,7 +504,27 @@
|
|
|
491
504
|
"from": { "type": "string", "minLength": 1 },
|
|
492
505
|
"to": { "type": "string", "minLength": 1 },
|
|
493
506
|
"path": { "type": "string", "enum": ["main", "side"] },
|
|
494
|
-
"
|
|
507
|
+
"trigger": {
|
|
508
|
+
"type": "string",
|
|
509
|
+
"minLength": 1,
|
|
510
|
+
"description": "what the user pressed, such as a button name. Shown on the line"
|
|
511
|
+
},
|
|
512
|
+
"condition": {
|
|
513
|
+
"type": "string",
|
|
514
|
+
"minLength": 1,
|
|
515
|
+
"description": "when this branch is taken. Put it on lines leaving a decision step"
|
|
516
|
+
},
|
|
517
|
+
"label": {
|
|
518
|
+
"type": "string",
|
|
519
|
+
"minLength": 1,
|
|
520
|
+
"description": "legacy text. Shown on the line only when trigger and condition are both absent"
|
|
521
|
+
},
|
|
522
|
+
"actors": {
|
|
523
|
+
"type": "array",
|
|
524
|
+
"minItems": 1,
|
|
525
|
+
"description": "ids of flow actors who can make this transition",
|
|
526
|
+
"items": { "type": "string", "minLength": 1 }
|
|
527
|
+
},
|
|
495
528
|
"evidence": { "type": "array", "items": { "$ref": "#/definitions/evidence" } },
|
|
496
529
|
"lineStyle": { "type": "string", "enum": ["solid", "dashed"] }
|
|
497
530
|
}
|
|
@@ -580,9 +613,29 @@
|
|
|
580
613
|
"type": "array",
|
|
581
614
|
"description": "the two groups compare puts side by side, such as before and after",
|
|
582
615
|
"items": { "$ref": "#/definitions/projectionSide" }
|
|
616
|
+
},
|
|
617
|
+
"phases": {
|
|
618
|
+
"type": "array",
|
|
619
|
+
"minItems": 1,
|
|
620
|
+
"description": "sequence only. Phases in message order, from and to inclusive, no overlap or gap. Leave out to split automatically",
|
|
621
|
+
"items": { "$ref": "#/definitions/projectionPhase" }
|
|
583
622
|
}
|
|
584
623
|
}
|
|
585
624
|
},
|
|
625
|
+
"projectionPhase": {
|
|
626
|
+
"type": "object",
|
|
627
|
+
"required": ["id", "label", "from", "to"],
|
|
628
|
+
"properties": {
|
|
629
|
+
"id": { "type": "string", "minLength": 1 },
|
|
630
|
+
"label": {
|
|
631
|
+
"type": "string",
|
|
632
|
+
"minLength": 1,
|
|
633
|
+
"description": "step name a person reads"
|
|
634
|
+
},
|
|
635
|
+
"from": { "type": "string", "minLength": 1, "description": "first message id" },
|
|
636
|
+
"to": { "type": "string", "minLength": 1, "description": "last message id" }
|
|
637
|
+
}
|
|
638
|
+
},
|
|
586
639
|
"projectionSide": {
|
|
587
640
|
"type": "object",
|
|
588
641
|
"required": ["id", "label", "nodes"],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type LayoutPoint } from './layout.js';
|
|
2
2
|
import type { ArchitectureFlow, FlowActor, FlowPath } from './types.js';
|
|
3
3
|
import type { ValidatedIr } from './validator.js';
|
|
4
4
|
export declare const FLOW_LEVEL_PREFIX = "flow:";
|
|
@@ -19,6 +19,8 @@ export interface FlowStepBox {
|
|
|
19
19
|
row: number;
|
|
20
20
|
/** 정상 흐름 전이에 한 번도 안 닿는 단계면 side다 */
|
|
21
21
|
path: FlowPath;
|
|
22
|
+
/** 갈림길이면 diamond. 상자는 마름모를 감싸는 bbox다 */
|
|
23
|
+
shape?: 'diamond';
|
|
22
24
|
}
|
|
23
25
|
export interface FlowTransitionRoute {
|
|
24
26
|
id: string;
|
|
@@ -30,6 +32,8 @@ export interface FlowTransitionRoute {
|
|
|
30
32
|
tip: LayoutPoint[];
|
|
31
33
|
/** 조건 글자와 행위자 이름을 놓을 자리. 둘 다 없으면 없다 */
|
|
32
34
|
labelAt?: LayoutPoint;
|
|
35
|
+
/** 갈림길에서 나가는 선은 글자를 꼭짓점 옆에 붙여 오른쪽으로 쓴다. 가운데 정렬이면 마름모를 덮는다 */
|
|
36
|
+
labelAnchor?: 'start';
|
|
33
37
|
/** 왼쪽 열로 돌아가는 전이. 되돌리기처럼 상태가 앞 단계로 돌아가는 자리다 */
|
|
34
38
|
back: boolean;
|
|
35
39
|
}
|
|
@@ -43,6 +47,10 @@ export interface FlowStage {
|
|
|
43
47
|
export interface FlowLayout {
|
|
44
48
|
width: number;
|
|
45
49
|
height: number;
|
|
50
|
+
/** 행위자 머리 칸 폭. 행위자가 한 명이면 머리 칸 없이 맨 위 띠에 칩으로 세워서 0이다 */
|
|
51
|
+
headWidth: number;
|
|
52
|
+
/** 구간 머리가 서는 y. 행위자 띠가 있으면 그 아래다 */
|
|
53
|
+
stageTop: number;
|
|
46
54
|
lanes: FlowLane[];
|
|
47
55
|
/** 정상 흐름 단계에 상태 값이 하나도 없으면 빈 배열이다 */
|
|
48
56
|
stages: FlowStage[];
|
|
@@ -64,6 +72,13 @@ export interface FlowLevel {
|
|
|
64
72
|
export declare const FLOW_HEAD_WIDTH = 132;
|
|
65
73
|
export declare const FLOW_STEP_WIDTH = 176;
|
|
66
74
|
export declare const FLOW_STEP_HEIGHT = 64;
|
|
75
|
+
/**
|
|
76
|
+
* 마름모는 카드보다 8px 높고 4px 위에 서서 가운데 y가 카드와 같다. 그래야 좌우 꼭짓점이 카드 중간 앵커와 맞는다.
|
|
77
|
+
* 되돌아가는 선은 카드 위아래 11px에 서는데 4px가 넘쳐도 15px라 줄 여백 18px 안에 든다. 80이면 넘친다
|
|
78
|
+
*/
|
|
79
|
+
export declare const FLOW_DECISION_HEIGHT = 72;
|
|
80
|
+
/** 행위자가 한 명일 때 그림 맨 위에 두는 띠 높이. 행위자 칩이 선다 */
|
|
81
|
+
export declare const FLOW_ACTOR_BAND = 28;
|
|
67
82
|
export declare const FLOW_STAGE_HEAD = 34;
|
|
68
83
|
export declare const FLOW_SIDE_STAGE_LABEL = "\uC606 \uD750\uB984";
|
|
69
84
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-layout.d.ts","sourceRoot":"","sources":["../../../src/architecture/flow-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"flow-layout.d.ts","sourceRoot":"","sources":["../../../src/architecture/flow-layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
+
import { textUnits } from './layout.js';
|
|
1
2
|
export const FLOW_LEVEL_PREFIX = 'flow:';
|
|
2
3
|
export const FLOW_HEAD_WIDTH = 132;
|
|
3
4
|
export const FLOW_STEP_WIDTH = 176;
|
|
4
5
|
export const FLOW_STEP_HEIGHT = 64;
|
|
6
|
+
/**
|
|
7
|
+
* 마름모는 카드보다 8px 높고 4px 위에 서서 가운데 y가 카드와 같다. 그래야 좌우 꼭짓점이 카드 중간 앵커와 맞는다.
|
|
8
|
+
* 되돌아가는 선은 카드 위아래 11px에 서는데 4px가 넘쳐도 15px라 줄 여백 18px 안에 든다. 80이면 넘친다
|
|
9
|
+
*/
|
|
10
|
+
export const FLOW_DECISION_HEIGHT = 72;
|
|
11
|
+
/** 행위자가 한 명일 때 그림 맨 위에 두는 띠 높이. 행위자 칩이 선다 */
|
|
12
|
+
export const FLOW_ACTOR_BAND = 28;
|
|
5
13
|
const COLUMN_GAP = 56;
|
|
14
|
+
const MAX_LABEL_GAP = 168;
|
|
15
|
+
const LABEL_UNIT = 6;
|
|
16
|
+
const LABEL_GAP_PAD = 20;
|
|
6
17
|
const ROW_GAP = 22;
|
|
7
18
|
const LANE_PAD_Y = 18;
|
|
8
19
|
const PAD_RIGHT = 32;
|
|
9
20
|
const BACK_LANE_GAP = 10;
|
|
21
|
+
const VERTEX_LABEL_STEP = 14;
|
|
10
22
|
const TIP_LENGTH = 8;
|
|
11
23
|
const TIP_HALF = 4.5;
|
|
12
24
|
export const FLOW_STAGE_HEAD = 34;
|
|
@@ -14,6 +26,26 @@ export const FLOW_SIDE_STAGE_LABEL = '옆 흐름';
|
|
|
14
26
|
function round2(v) {
|
|
15
27
|
return Math.round(v * 100) / 100;
|
|
16
28
|
}
|
|
29
|
+
function centerY(box) {
|
|
30
|
+
return box.y + box.height / 2;
|
|
31
|
+
}
|
|
32
|
+
/** 상자 위나 아래 변에서 가로 비율 r 자리의 y. 마름모는 꼭짓점에서 멀어질수록 가운데로 들어온다 */
|
|
33
|
+
function edgeY(box, r, side) {
|
|
34
|
+
if (box.shape !== 'diamond')
|
|
35
|
+
return side === 'top' ? box.y : box.y + box.height;
|
|
36
|
+
const inset = (box.height / 2) * (1 - Math.abs(2 * r - 1));
|
|
37
|
+
return side === 'top' ? centerY(box) - inset : centerY(box) + inset;
|
|
38
|
+
}
|
|
39
|
+
/** 선 위에 찍힐 글자의 폭 단위. 렌더러가 조건, 누른 것, 행위자 이름 순으로 찍는 것과 맞춘다 */
|
|
40
|
+
function lineTextUnits(t, actorLabel) {
|
|
41
|
+
const main = t.trigger !== undefined || t.condition !== undefined
|
|
42
|
+
? [t.condition, t.trigger].filter((x) => x !== undefined).join(' ')
|
|
43
|
+
: (t.label ?? '');
|
|
44
|
+
const who = (t.actors ?? [])
|
|
45
|
+
.map((id) => (actorLabel.get(id) ?? id).replace(/\s*\([^)]*\)\s*$/, ''))
|
|
46
|
+
.join(', ');
|
|
47
|
+
return textUnits(main) + (who !== '' ? textUnits(main !== '' ? ` · ${who}` : who) : 0);
|
|
48
|
+
}
|
|
17
49
|
/**
|
|
18
50
|
* 열은 전이를 따라 가장 긴 경로로 정한다. 되돌아가는 전이는 DFS에서 스택 위의 단계로 가는 선으로 보고 열 계산에서 뺀다.
|
|
19
51
|
* DFS 시작 순서는 IR에 적힌 단계 순서다. 작성자가 이야기 순서로 적은 걸 그대로 따라야 정상 흐름이 왼쪽에서부터 읽힌다.
|
|
@@ -95,7 +127,13 @@ function stagedColumns(steps, edges, isMain, base) {
|
|
|
95
127
|
if (!firstCol.has(state) || col < firstCol.get(state))
|
|
96
128
|
firstCol.set(state, col);
|
|
97
129
|
}
|
|
98
|
-
|
|
130
|
+
// 열이 같으면 steps에 먼저 적힌 상태가 앞이다. 들어오는 선 없는 단계도 열 0을 받아 정상 흐름 첫 단계와 비기는데 topo 순서로 풀면 그 단계가 앞을 차지한다
|
|
131
|
+
const declared = new Map();
|
|
132
|
+
steps.forEach((s, i) => {
|
|
133
|
+
if (s.state !== undefined && !declared.has(s.state))
|
|
134
|
+
declared.set(s.state, i);
|
|
135
|
+
});
|
|
136
|
+
const keys = [...firstCol.keys()].sort((a, b) => firstCol.get(a) - firstCol.get(b) || declared.get(a) - declared.get(b));
|
|
99
137
|
const rank = new Map(keys.map((k, i) => [k, i]));
|
|
100
138
|
const stageOf = new Map();
|
|
101
139
|
for (const id of mainTopo) {
|
|
@@ -266,26 +304,30 @@ function collides(points, from, skip, boxes, placed) {
|
|
|
266
304
|
* 줄이 다른 옆 흐름 전이의 경로. 먼저 카드 위나 아래에서 바로 꺾어 나가는 길을 본다. 오른쪽 변으로 나가면 정상 흐름 선과 같은 꺾임 자리를 겹쳐 쓴다.
|
|
267
305
|
* 그 길이 다른 카드를 뚫거나 다른 단계의 선과 겹치면 카드 사이 빈 줄로 나가 도착 열 앞의 빈 칸에서 내려가는 길을 고른다
|
|
268
306
|
*/
|
|
269
|
-
function sideRoute(a, b, from, boxes, placed) {
|
|
307
|
+
function sideRoute(a, b, from, boxes, placed, ratios) {
|
|
270
308
|
const down = b.y > a.y;
|
|
271
309
|
const ty = b.y + b.height / 2;
|
|
272
|
-
const sy = down ? a.y + a.height : a.y;
|
|
273
310
|
const candidates = [];
|
|
274
311
|
// 내려가는 선과 올라가는 선이 같은 세로줄을 쓰지 않게 나가는 자리를 나눈다
|
|
275
|
-
|
|
312
|
+
const starts = ratios ?? (down ? [0.72, 0.86] : [0.28, 0.14]);
|
|
313
|
+
for (const ratio of starts) {
|
|
276
314
|
const sx = a.x + a.width * ratio;
|
|
315
|
+
const sy = edgeY(a, ratio, down ? 'bottom' : 'top');
|
|
277
316
|
candidates.push([
|
|
278
317
|
{ x: sx, y: sy },
|
|
279
318
|
{ x: sx, y: ty },
|
|
280
319
|
{ x: b.x, y: ty },
|
|
281
320
|
]);
|
|
282
321
|
}
|
|
283
|
-
const
|
|
322
|
+
const edge = down ? a.y + a.height : a.y;
|
|
323
|
+
const gapY = down ? edge + ROW_GAP / 2 : edge - ROW_GAP / 2;
|
|
324
|
+
const sr = starts[0];
|
|
325
|
+
const sx = a.x + a.width * sr;
|
|
326
|
+
const sy = edgeY(a, sr, down ? 'bottom' : 'top');
|
|
284
327
|
for (let k = 0; k < 4; k += 1) {
|
|
285
328
|
const cx = b.x - 10 - k * 6;
|
|
286
329
|
if (cx <= a.x + a.width)
|
|
287
330
|
break;
|
|
288
|
-
const sx = a.x + a.width * (down ? 0.72 : 0.28);
|
|
289
331
|
candidates.push([
|
|
290
332
|
{ x: sx, y: sy },
|
|
291
333
|
{ x: sx, y: gapY },
|
|
@@ -311,17 +353,15 @@ function nearBackRoute(a, b, from, boxes, placed) {
|
|
|
311
353
|
[over - shift, 'top'],
|
|
312
354
|
[under + shift, 'bottom'],
|
|
313
355
|
]) {
|
|
314
|
-
const fromEdge = side === 'top' ? a.y : a.y + a.height;
|
|
315
|
-
const toEdge = side === 'top' ? b.y : b.y + b.height;
|
|
316
356
|
for (const rs of ratios) {
|
|
317
357
|
for (const rt of ratios) {
|
|
318
358
|
const sx = a.x + a.width * rs;
|
|
319
359
|
const tx = b.x + b.width * rt;
|
|
320
360
|
const points = [
|
|
321
|
-
{ x: sx, y:
|
|
361
|
+
{ x: sx, y: edgeY(a, rs, side) },
|
|
322
362
|
{ x: sx, y: gy },
|
|
323
363
|
{ x: tx, y: gy },
|
|
324
|
-
{ x: tx, y:
|
|
364
|
+
{ x: tx, y: edgeY(b, rt, side) },
|
|
325
365
|
];
|
|
326
366
|
if (!collides(points, from, [a, b], boxes, placed))
|
|
327
367
|
return points;
|
|
@@ -364,7 +404,11 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
364
404
|
const { back } = base;
|
|
365
405
|
const staged = stagedColumns(steps, transitions, (id) => pathOf(id) === 'main', base);
|
|
366
406
|
const column = staged?.column ?? base.column;
|
|
367
|
-
|
|
407
|
+
// 행위자가 한 명이면 줄을 가를 일이 없어 머리 칸을 걷고 맨 위 띠에 칩으로 세운다
|
|
408
|
+
const solo = flow.actors.length === 1;
|
|
409
|
+
const headWidth = solo ? 0 : FLOW_HEAD_WIDTH;
|
|
410
|
+
const stageTop = solo ? FLOW_ACTOR_BAND : 0;
|
|
411
|
+
const top = stageTop + (staged !== undefined ? FLOW_STAGE_HEAD : 0);
|
|
368
412
|
// 행위자 줄 안에서 칸이 겹치면 아래 줄로 내린다. 정상 흐름 단계를 먼저 놓아야 옆 흐름이 그 아래로 간다
|
|
369
413
|
const taken = new Set();
|
|
370
414
|
const rowOf = new Map();
|
|
@@ -394,18 +438,30 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
394
438
|
y += height;
|
|
395
439
|
}
|
|
396
440
|
const columns = Math.max(0, ...[...column.values()]) + 1;
|
|
441
|
+
// 누른 것이나 조건을 적은 흐름만 선 글자 폭에 맞춰 열 사이를 넓힌다. 예전 흐름은 간격이 그대로다
|
|
442
|
+
const actorLabel = new Map(flow.actors.map((a) => [a.id, a.label]));
|
|
443
|
+
const rich = transitions.some((t) => t.trigger !== undefined || t.condition !== undefined);
|
|
444
|
+
const forwardUnits = transitions
|
|
445
|
+
.filter((t) => !back.has(t.id) && column.get(t.to) > column.get(t.from))
|
|
446
|
+
.map((t) => lineTextUnits(t, actorLabel));
|
|
447
|
+
const gap = rich
|
|
448
|
+
? Math.min(MAX_LABEL_GAP, Math.max(COLUMN_GAP, Math.max(0, ...forwardUnits) * LABEL_UNIT + LABEL_GAP_PAD))
|
|
449
|
+
: COLUMN_GAP;
|
|
397
450
|
const boxes = steps.map((s) => {
|
|
398
451
|
const col = column.get(s.id);
|
|
399
452
|
const row = rowOf.get(s.id);
|
|
453
|
+
const y = laneY.get(s.actor) + LANE_PAD_Y + row * (FLOW_STEP_HEIGHT + ROW_GAP);
|
|
454
|
+
const decision = s.kind === 'decision';
|
|
400
455
|
return {
|
|
401
456
|
id: s.id,
|
|
402
|
-
x:
|
|
403
|
-
y:
|
|
457
|
+
x: headWidth + gap / 2 + col * (FLOW_STEP_WIDTH + gap),
|
|
458
|
+
y: decision ? y - (FLOW_DECISION_HEIGHT - FLOW_STEP_HEIGHT) / 2 : y,
|
|
404
459
|
width: FLOW_STEP_WIDTH,
|
|
405
|
-
height: FLOW_STEP_HEIGHT,
|
|
460
|
+
height: decision ? FLOW_DECISION_HEIGHT : FLOW_STEP_HEIGHT,
|
|
406
461
|
column: col,
|
|
407
462
|
row,
|
|
408
463
|
path: pathOf(s.id),
|
|
464
|
+
...(decision ? { shape: 'diamond' } : {}),
|
|
409
465
|
};
|
|
410
466
|
});
|
|
411
467
|
const boxOf = new Map(boxes.map((b) => [b.id, b]));
|
|
@@ -413,10 +469,12 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
413
469
|
// 되돌아가는 선은 두 카드 바로 위나 아래 빈 줄로 건넌다. 그 줄이 막히면 그림 맨 아래 여백으로 돌린다
|
|
414
470
|
let backCount = 0;
|
|
415
471
|
const placed = [];
|
|
472
|
+
const vertexLabels = new Map();
|
|
416
473
|
const routes = transitions.map((t) => {
|
|
417
474
|
const a = boxOf.get(t.from);
|
|
418
475
|
const b = boxOf.get(t.to);
|
|
419
476
|
const isBack = back.has(t.id) || b.column <= a.column;
|
|
477
|
+
const diamond = a.shape === 'diamond';
|
|
420
478
|
let points;
|
|
421
479
|
if (isBack) {
|
|
422
480
|
const near = nearBackRoute(a, b, t.from, boxes, placed);
|
|
@@ -426,25 +484,28 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
426
484
|
else {
|
|
427
485
|
backCount += 1;
|
|
428
486
|
const floor = height + BACK_LANE_GAP * backCount;
|
|
487
|
+
const sr = 0.5 + 10 / a.width;
|
|
488
|
+
const tr = 0.5 - 10 / b.width;
|
|
429
489
|
const sx = a.x + a.width / 2 + 10;
|
|
430
490
|
const tx = b.x + b.width / 2 - 10;
|
|
431
491
|
points = [
|
|
432
|
-
{ x: sx, y: a
|
|
492
|
+
{ x: sx, y: edgeY(a, sr, 'bottom') },
|
|
433
493
|
{ x: sx, y: floor },
|
|
434
494
|
{ x: tx, y: floor },
|
|
435
|
-
{ x: tx, y: b
|
|
495
|
+
{ x: tx, y: edgeY(b, tr, 'bottom') },
|
|
436
496
|
];
|
|
437
497
|
}
|
|
438
498
|
}
|
|
439
|
-
else if (t.path === 'side' && b
|
|
440
|
-
|
|
499
|
+
else if ((t.path === 'side' || diamond) && centerY(b) !== centerY(a)) {
|
|
500
|
+
// 갈림길에서 다른 줄로 가는 길은 위아래 꼭짓점에서 세로로 나간다. 오른쪽 꼭짓점은 같은 줄로 가는 길 몫이다
|
|
501
|
+
points = sideRoute(a, b, t.from, boxes, placed, diamond ? [0.5] : undefined);
|
|
441
502
|
}
|
|
442
503
|
else {
|
|
443
504
|
const sx = a.x + a.width;
|
|
444
505
|
const sy = a.y + a.height / 2;
|
|
445
506
|
const tx = b.x;
|
|
446
507
|
const ty = b.y + b.height / 2;
|
|
447
|
-
const mx = tx -
|
|
508
|
+
const mx = tx - gap / 2;
|
|
448
509
|
points =
|
|
449
510
|
sy === ty
|
|
450
511
|
? [
|
|
@@ -462,11 +523,27 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
462
523
|
placed.push(...segmentsOf(points, t.from));
|
|
463
524
|
const last = points[points.length - 1];
|
|
464
525
|
const prev = points[points.length - 2];
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
526
|
+
const first = points[0];
|
|
527
|
+
const atVertex = diamond && !isBack;
|
|
528
|
+
const hasText = t.label !== undefined ||
|
|
529
|
+
t.actors !== undefined ||
|
|
530
|
+
t.trigger !== undefined ||
|
|
531
|
+
t.condition !== undefined;
|
|
532
|
+
// 렌더러가 글자 y를 5px 올려 찍는다. 꼭짓점 옆 글자는 오른쪽이면 선 위, 아래로 나가면 꼭짓점 밑, 위로 나가면 꼭짓점 위에 선다
|
|
533
|
+
const vertexDy = first.y > centerY(a) ? 19 : first.y < centerY(a) ? -1 : 0;
|
|
534
|
+
// 한 꼭짓점에서 길이 여럿 나가면 글자가 모두 같은 자리를 받아 포개진다. 뒤에 오는 글자를 한 줄씩 비켜 쌓는다
|
|
535
|
+
const vertexKey = `${t.from}\u0000${first.x}\u0000${first.y}`;
|
|
536
|
+
const stacked = atVertex && hasText ? (vertexLabels.get(vertexKey) ?? 0) : 0;
|
|
537
|
+
if (atVertex && hasText)
|
|
538
|
+
vertexLabels.set(vertexKey, stacked + 1);
|
|
539
|
+
const stackDy = stacked * (first.y > centerY(a) ? VERTEX_LABEL_STEP : -VERTEX_LABEL_STEP);
|
|
540
|
+
const mid = atVertex
|
|
541
|
+
? { x: round2(first.x + 6), y: round2(first.y + vertexDy + stackDy) }
|
|
542
|
+
: isBack
|
|
543
|
+
? { x: round2((points[1].x + points[2].x) / 2), y: round2(points[1].y) }
|
|
544
|
+
: points.length === 2
|
|
545
|
+
? { x: round2((prev.x + last.x) / 2), y: round2(last.y) }
|
|
546
|
+
: longestVerticalMid(points);
|
|
470
547
|
return {
|
|
471
548
|
id: t.id,
|
|
472
549
|
from: t.from,
|
|
@@ -474,20 +551,23 @@ export function computeFlowLayout(flow, drawableSteps, drawableTransitions) {
|
|
|
474
551
|
path: t.path,
|
|
475
552
|
points,
|
|
476
553
|
tip: tipAt(last, prev),
|
|
477
|
-
...(
|
|
554
|
+
...(hasText ? { labelAt: mid } : {}),
|
|
555
|
+
...(hasText && atVertex ? { labelAnchor: 'start' } : {}),
|
|
478
556
|
back: isBack,
|
|
479
557
|
};
|
|
480
558
|
});
|
|
481
|
-
const width =
|
|
559
|
+
const width = headWidth + columns * (FLOW_STEP_WIDTH + gap) + PAD_RIGHT;
|
|
482
560
|
const stages = (staged?.stages ?? []).map((st) => ({
|
|
483
561
|
label: st.label,
|
|
484
|
-
x: round2(
|
|
485
|
-
width: round2((st.last - st.first + 1) * (FLOW_STEP_WIDTH +
|
|
562
|
+
x: round2(headWidth + st.first * (FLOW_STEP_WIDTH + gap)),
|
|
563
|
+
width: round2((st.last - st.first + 1) * (FLOW_STEP_WIDTH + gap)),
|
|
486
564
|
side: st.side,
|
|
487
565
|
}));
|
|
488
566
|
return {
|
|
489
567
|
width: round2(width),
|
|
490
568
|
height: round2(height + (backCount > 0 ? BACK_LANE_GAP * (backCount + 1) : 0)),
|
|
569
|
+
headWidth,
|
|
570
|
+
stageTop,
|
|
491
571
|
lanes,
|
|
492
572
|
stages,
|
|
493
573
|
steps: boxes,
|