@things-factory/figure-ui 10.1.81 → 10.1.82

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.
Files changed (145) hide show
  1. package/client/figure-assistant.ts +2 -2
  2. package/client/graphql/index.ts +99 -10
  3. package/client/graphql/reached.ts +25 -0
  4. package/client/graphql/store.ts +27 -4
  5. package/client/modeller/ai-proposal.ts +74 -9
  6. package/client/modeller/align.ts +2 -3
  7. package/client/modeller/canvas.ts +18 -5
  8. package/client/modeller/command-preview.ts +113 -0
  9. package/client/modeller/editor.ts +826 -54
  10. package/client/modeller/fields.ts +12 -4
  11. package/client/modeller/figure-facts.ts +2 -2
  12. package/client/modeller/figure-first-save.ts +36 -14
  13. package/client/modeller/figure-history-panel.ts +133 -14
  14. package/client/modeller/figure-links.ts +385 -68
  15. package/client/modeller/figure-parts.ts +56 -2
  16. package/client/modeller/figure-preview.ts +19 -1
  17. package/client/modeller/figure-properties.ts +3 -3
  18. package/client/modeller/figure-revert-choice.ts +49 -0
  19. package/client/modeller/first-save.ts +16 -0
  20. package/client/modeller/gizmo.ts +0 -16
  21. package/client/modeller/links.ts +142 -15
  22. package/client/modeller/motion-ends.ts +83 -0
  23. package/client/modeller/occupancy.ts +28 -2
  24. package/client/modeller/product-session.ts +154 -18
  25. package/client/modeller/refusals.ts +15 -0
  26. package/client/modeller/revert-request.ts +26 -0
  27. package/client/modeller/side-panel-styles.ts +12 -0
  28. package/client/modeller/stage-model.ts +25 -3
  29. package/client/pages/figure-modeller-page.ts +68 -29
  30. package/dist-client/figure-assistant.js +2 -2
  31. package/dist-client/figure-assistant.js.map +1 -1
  32. package/dist-client/graphql/index.d.ts +39 -2
  33. package/dist-client/graphql/index.js +63 -9
  34. package/dist-client/graphql/index.js.map +1 -1
  35. package/dist-client/graphql/reached.d.ts +10 -0
  36. package/dist-client/graphql/reached.js +28 -0
  37. package/dist-client/graphql/reached.js.map +1 -0
  38. package/dist-client/graphql/store.js +27 -4
  39. package/dist-client/graphql/store.js.map +1 -1
  40. package/dist-client/modeller/ai-proposal.d.ts +31 -2
  41. package/dist-client/modeller/ai-proposal.js +65 -6
  42. package/dist-client/modeller/ai-proposal.js.map +1 -1
  43. package/dist-client/modeller/align.d.ts +1 -1
  44. package/dist-client/modeller/align.js +0 -2
  45. package/dist-client/modeller/align.js.map +1 -1
  46. package/dist-client/modeller/canvas.d.ts +11 -0
  47. package/dist-client/modeller/canvas.js +33 -4
  48. package/dist-client/modeller/canvas.js.map +1 -1
  49. package/dist-client/modeller/clips.d.ts +1 -1
  50. package/dist-client/modeller/command-preview.d.ts +35 -0
  51. package/dist-client/modeller/command-preview.js +95 -0
  52. package/dist-client/modeller/command-preview.js.map +1 -0
  53. package/dist-client/modeller/editor.d.ts +113 -1
  54. package/dist-client/modeller/editor.js +850 -50
  55. package/dist-client/modeller/editor.js.map +1 -1
  56. package/dist-client/modeller/fields.d.ts +3 -2
  57. package/dist-client/modeller/fields.js +10 -3
  58. package/dist-client/modeller/fields.js.map +1 -1
  59. package/dist-client/modeller/figure-facts.js +2 -2
  60. package/dist-client/modeller/figure-facts.js.map +1 -1
  61. package/dist-client/modeller/figure-first-save.d.ts +3 -4
  62. package/dist-client/modeller/figure-first-save.js +41 -14
  63. package/dist-client/modeller/figure-first-save.js.map +1 -1
  64. package/dist-client/modeller/figure-history-panel.d.ts +31 -0
  65. package/dist-client/modeller/figure-history-panel.js +130 -14
  66. package/dist-client/modeller/figure-history-panel.js.map +1 -1
  67. package/dist-client/modeller/figure-links.d.ts +51 -1
  68. package/dist-client/modeller/figure-links.js +392 -72
  69. package/dist-client/modeller/figure-links.js.map +1 -1
  70. package/dist-client/modeller/figure-parts.d.ts +7 -0
  71. package/dist-client/modeller/figure-parts.js +55 -2
  72. package/dist-client/modeller/figure-parts.js.map +1 -1
  73. package/dist-client/modeller/figure-preview.d.ts +6 -0
  74. package/dist-client/modeller/figure-preview.js +22 -1
  75. package/dist-client/modeller/figure-preview.js.map +1 -1
  76. package/dist-client/modeller/figure-properties.js +3 -3
  77. package/dist-client/modeller/figure-properties.js.map +1 -1
  78. package/dist-client/modeller/figure-revert-choice.d.ts +17 -0
  79. package/dist-client/modeller/figure-revert-choice.js +59 -0
  80. package/dist-client/modeller/figure-revert-choice.js.map +1 -0
  81. package/dist-client/modeller/first-save.d.ts +7 -0
  82. package/dist-client/modeller/first-save.js +17 -0
  83. package/dist-client/modeller/first-save.js.map +1 -1
  84. package/dist-client/modeller/gizmo.d.ts +0 -6
  85. package/dist-client/modeller/gizmo.js +0 -18
  86. package/dist-client/modeller/gizmo.js.map +1 -1
  87. package/dist-client/modeller/links.d.ts +49 -4
  88. package/dist-client/modeller/links.js +141 -15
  89. package/dist-client/modeller/links.js.map +1 -1
  90. package/dist-client/modeller/motion-ends.d.ts +12 -0
  91. package/dist-client/modeller/motion-ends.js +65 -0
  92. package/dist-client/modeller/motion-ends.js.map +1 -0
  93. package/dist-client/modeller/occupancy.d.ts +14 -1
  94. package/dist-client/modeller/occupancy.js +14 -1
  95. package/dist-client/modeller/occupancy.js.map +1 -1
  96. package/dist-client/modeller/product-session.d.ts +43 -2
  97. package/dist-client/modeller/product-session.js +146 -12
  98. package/dist-client/modeller/product-session.js.map +1 -1
  99. package/dist-client/modeller/refusals.d.ts +13 -0
  100. package/dist-client/modeller/refusals.js +16 -0
  101. package/dist-client/modeller/refusals.js.map +1 -1
  102. package/dist-client/modeller/revert-request.d.ts +16 -0
  103. package/dist-client/modeller/revert-request.js +11 -0
  104. package/dist-client/modeller/revert-request.js.map +1 -0
  105. package/dist-client/modeller/side-panel-styles.js +12 -0
  106. package/dist-client/modeller/side-panel-styles.js.map +1 -1
  107. package/dist-client/modeller/stage-model.d.ts +18 -2
  108. package/dist-client/modeller/stage-model.js +21 -3
  109. package/dist-client/modeller/stage-model.js.map +1 -1
  110. package/dist-client/pages/figure-modeller-page.d.ts +8 -1
  111. package/dist-client/pages/figure-modeller-page.js +72 -34
  112. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  113. package/dist-client/route.d.ts +1 -1
  114. package/dist-client/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +5 -5
  116. package/test/ai-proposal.test.ts +2 -2
  117. package/test/align.test.ts +50 -5
  118. package/test/browser/acceptance-page.js +187 -0
  119. package/test/browser/build.mjs +5 -1
  120. package/test/browser/revert-page.js +47 -0
  121. package/test/browser/serve-acceptance.mjs +85 -0
  122. package/test/browser/serve-revert.mjs +119 -0
  123. package/test/clip-ai-proposal.test.ts +104 -0
  124. package/test/clip-consent.test.ts +88 -0
  125. package/test/command-preview.test.ts +288 -0
  126. package/test/fields.test.ts +4 -3
  127. package/test/figure-assistant-surface.test.ts +4 -1
  128. package/test/first-minute.test.ts +96 -0
  129. package/test/first-save-suggest.test.ts +42 -0
  130. package/test/gizmo.test.ts +20 -16
  131. package/test/joint-fan.test.ts +2 -1
  132. package/test/layout.test.ts +9 -2
  133. package/test/links.test.ts +7 -4
  134. package/test/proposal-record.test.ts +126 -0
  135. package/test/release-entry.test.ts +28 -0
  136. package/test/release-panel.test.ts +3 -2
  137. package/test/resize-with-placement.test.ts +225 -0
  138. package/test/revert-choice.test.ts +39 -0
  139. package/test/revert-keeping.test.ts +102 -0
  140. package/test/save-landed.test.ts +198 -0
  141. package/translations/en.json +222 -10
  142. package/translations/ja.json +223 -11
  143. package/translations/ko.json +223 -11
  144. package/translations/ms.json +222 -10
  145. package/translations/zh.json +223 -11
@@ -1,6 +1,7 @@
1
1
  import gql from 'graphql-tag';
2
2
  import { i18next } from '@operato/i18n';
3
3
  import { client } from '@operato/graphql';
4
+ import { reached, refusedIn } from './reached.js';
4
5
  /**
5
6
  * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**
6
7
  *
@@ -168,20 +169,73 @@ export async function releaseFigure(id, comment) {
168
169
  });
169
170
  return unwrap(response, 'releaseFigure');
170
171
  }
171
- /** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */
172
- export async function revertFigureVersion(id, version) {
173
- const response = await client.mutate({
172
+ const KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`;
173
+ /**
174
+ * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가
175
+ * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면
176
+ * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.
177
+ */
178
+ export async function revertFigureVersion(id, version, requestId, expectedSource) {
179
+ const response = await reached(client.mutate({
174
180
  mutation: gql `
175
- mutation RevertFigureVersion($id: String!, $version: Float!) {
176
- revertFigureVersion(id: $id, version: $version) {
177
- figure { ${FIGURE_LIST_FIELDS} source }
178
- violations { code message at }
181
+ mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {
182
+ revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {
183
+ figure { ${FIGURE_LIST_FIELDS} source }
184
+ violations { code message at }
185
+ kept { ${KEPT_FIELDS} }
186
+ }
179
187
  }
188
+ `,
189
+ variables: { id, version, requestId, expectedSource }
190
+ }), 'REVERT_UNREACHABLE');
191
+ refusedIn(response);
192
+ return unwrap(response, 'revertFigureVersion');
193
+ }
194
+ /** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */
195
+ export async function undoFigureRevert(keptId, requestId) {
196
+ const response = await reached(client.mutate({
197
+ mutation: gql `
198
+ mutation UndoFigureRevert($keptId: String!, $requestId: String!) {
199
+ undoFigureRevert(keptId: $keptId, requestId: $requestId) {
200
+ figure { ${FIGURE_LIST_FIELDS} source }
201
+ violations { code message at }
202
+ kept { ${KEPT_FIELDS} }
203
+ }
204
+ }
205
+ `,
206
+ variables: { keptId, requestId }
207
+ }), 'REVERT_UNREACHABLE');
208
+ refusedIn(response);
209
+ return unwrap(response, 'undoFigureRevert');
210
+ }
211
+ /**
212
+ * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,
213
+ * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.
214
+ */
215
+ export async function fetchProposalOutcome(proposalKey) {
216
+ const response = await client.query({
217
+ query: gql `
218
+ query FigureProposalOutcome($proposalKey: String!) {
219
+ figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }
180
220
  }
181
221
  `,
182
- variables: { id, version }
222
+ variables: { proposalKey },
223
+ fetchPolicy: 'network-only'
183
224
  });
184
- return unwrap(response, 'revertFigureVersion');
225
+ return unwrap(response, 'figureProposalOutcome') ?? [];
226
+ }
227
+ /** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */
228
+ export async function fetchReplacedDrafts(id) {
229
+ const response = await client.query({
230
+ query: gql `
231
+ query FigureReplacedDrafts($id: String!) {
232
+ figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }
233
+ }
234
+ `,
235
+ variables: { id },
236
+ fetchPolicy: 'network-only'
237
+ });
238
+ return unwrap(response, 'figureReplacedDrafts');
185
239
  }
186
240
  /**
187
241
  * 발행해도 되나 — **누르기 전에** 묻는다.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n"]}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A request that never reached the server, or whose answer never came back, is told apart from a refusal: what
3
+ * happened to the write is not known (`unreachable`, SAVE_UNREACHABLE for a save). The caller finds out before saying
4
+ * more -- a save reads the server's copy back, a revert asks again with the same request id.
5
+ */
6
+ export declare function reached<T>(request: Promise<T>, unreachable?: string): Promise<T>;
7
+ /** Errors carried in the answer (the client's error policy lets them through), thrown with the server's code. */
8
+ export declare function refusedIn(response: {
9
+ errors?: readonly unknown[];
10
+ }): void;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A request that never reached the server, or whose answer never came back, is told apart from a refusal: what
3
+ * happened to the write is not known (`unreachable`, SAVE_UNREACHABLE for a save). The caller finds out before saying
4
+ * more -- a save reads the server's copy back, a revert asks again with the same request id.
5
+ */
6
+ export async function reached(request, unreachable = 'SAVE_UNREACHABLE') {
7
+ try {
8
+ return await request;
9
+ }
10
+ catch (error) {
11
+ const lost = error?.networkError || error instanceof TypeError;
12
+ if (lost)
13
+ throw Object.assign(Error('서버에 연결하지 못했습니다.'), { code: unreachable, cause: error });
14
+ /* A refusal keeps the server's code, so a conflict is told from a refusal on grounds. */
15
+ const refused = error?.graphQLErrors;
16
+ if (refused?.length)
17
+ throw Object.assign(Error(refused.map(e => e.message).join('\n')), { code: refused[0].extensions?.code, cause: error });
18
+ throw error;
19
+ }
20
+ }
21
+ /** Errors carried in the answer (the client's error policy lets them through), thrown with the server's code. */
22
+ export function refusedIn(response) {
23
+ if (response.errors?.length)
24
+ throw Object.assign(Error(response.errors.map(e => e.message).join('\n')), {
25
+ code: response.errors[0].extensions?.code
26
+ });
27
+ }
28
+ //# sourceMappingURL=reached.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reached.js","sourceRoot":"","sources":["../../client/graphql/reached.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,OAAmB,EAAE,WAAW,GAAG,kBAAkB;IACpF,IAAI,CAAC;QACH,OAAO,MAAM,OAAO,CAAA;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,GAAI,KAAoC,EAAE,YAAY,IAAI,KAAK,YAAY,SAAS,CAAA;QAC9F,IAAI,IAAI;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QAC5F,yFAAyF;QACzF,MAAM,OAAO,GAAI,KAAwF,EAAE,aAAa,CAAA;QACxH,IAAI,OAAO,EAAE,MAAM;YAAE,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,CAAE,CAAC,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;QAC7I,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,SAAS,CAAC,QAAyC;IACjE,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;QACzB,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,CAAyB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE;YAClG,IAAI,EAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAwC,CAAC,UAAU,EAAE,IAAI;SAClF,CAAC,CAAA;AACN,CAAC","sourcesContent":["/**\n * A request that never reached the server, or whose answer never came back, is told apart from a refusal: what\n * happened to the write is not known (`unreachable`, SAVE_UNREACHABLE for a save). The caller finds out before saying\n * more -- a save reads the server's copy back, a revert asks again with the same request id.\n */\nexport async function reached<T>(request: Promise<T>, unreachable = 'SAVE_UNREACHABLE'): Promise<T> {\n try {\n return await request\n } catch (error) {\n const lost = (error as { networkError?: unknown })?.networkError || error instanceof TypeError\n if (lost) throw Object.assign(Error('서버에 연결하지 못했습니다.'), { code: unreachable, cause: error })\n /* A refusal keeps the server's code, so a conflict is told from a refusal on grounds. */\n const refused = (error as { graphQLErrors?: Array<{ message: string; extensions?: { code?: string } }> })?.graphQLErrors\n if (refused?.length) throw Object.assign(Error(refused.map(e => e.message).join('\\n')), { code: refused[0]!.extensions?.code, cause: error })\n throw error\n }\n}\n\n/** Errors carried in the answer (the client's error policy lets them through), thrown with the server's code. */\nexport function refusedIn(response: { errors?: readonly unknown[] }) {\n if (response.errors?.length)\n throw Object.assign(Error(response.errors.map(e => (e as { message: string }).message).join('\\n')), {\n code: (response.errors[0] as { extensions?: { code?: string } }).extensions?.code\n })\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import gql from 'graphql-tag';
2
2
  import { client } from '@operato/graphql';
3
3
  import { fetchFigure } from './index.js';
4
+ import { reached } from './reached.js';
4
5
  /** source is sent intact. No conversion into the old parts/base authoring representation. */
5
6
  export const figureProductStore = {
6
7
  async load(id) {
@@ -9,10 +10,31 @@ export const figureProductStore = {
9
10
  throw Error('Figure 정본이 없습니다.');
10
11
  return { id: record.id, source: record.source };
11
12
  },
13
+ /* What the server holds of every field a save writes, read back after a save whose outcome is not known. */
14
+ async readBack(id) {
15
+ const response = await client.query({
16
+ query: gql `
17
+ query ($id: String!) {
18
+ figure(id: $id) {
19
+ id
20
+ source
21
+ thumbnail
22
+ type
23
+ }
24
+ }
25
+ `,
26
+ variables: { id },
27
+ fetchPolicy: 'network-only'
28
+ });
29
+ const figure = response.data?.figure;
30
+ if (!figure?.source)
31
+ throw Error('Figure 정본이 없습니다.');
32
+ return { id: figure.id, source: figure.source, thumbnail: figure.thumbnail ?? undefined, type: figure.type };
33
+ },
12
34
  async save(request) {
13
- const response = await client.mutate({
35
+ const response = await reached(client.mutate({
14
36
  mutation: gql `
15
- mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String) {
37
+ mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String, $proposalKeys: [String!]) {
16
38
  saveFigure(
17
39
  source: $source
18
40
  id: $id
@@ -20,6 +42,7 @@ export const figureProductStore = {
20
42
  creationId: $creationId
21
43
  thumbnail: $thumbnail
22
44
  type: $type
45
+ proposalKeys: $proposalKeys
23
46
  ) {
24
47
  id
25
48
  source
@@ -27,9 +50,9 @@ export const figureProductStore = {
27
50
  }
28
51
  `,
29
52
  variables: request
30
- });
53
+ }));
31
54
  if (response.errors?.length)
32
- throw Error(response.errors.map(e => e.message).join('\n'));
55
+ throw Object.assign(Error(response.errors.map(e => e.message).join('\n')), { code: response.errors[0].extensions?.code });
33
56
  const result = response.data?.saveFigure;
34
57
  if (!result?.id || !result.source)
35
58
  throw Error('서버가 저장 결과를 돌려주지 않았습니다.');
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../client/graphql/store.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAGxC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,kBAAkB,GAAuB;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;IACjD,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,OAAO;QAChB,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YACnC,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;;;;;;OAcZ;YACD,SAAS,EAAE,OAAO;SACnB,CAAC,CAAA;QACF,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACxF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,UAA6C,CAAA;QAC3E,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,CAAA;QACxE,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\nimport { fetchFigure } from './index.js'\nimport type { FigureProductStore, FigureProductRecord } from '../modeller/product-session.js'\n\n/** source is sent intact. No conversion into the old parts/base authoring representation. */\nexport const figureProductStore: FigureProductStore = {\n async load(id) {\n const record = await fetchFigure(id)\n if (!record?.source) throw Error('Figure 정본이 없습니다.')\n return { id: record.id, source: record.source }\n },\n async save(request) {\n const response = await client.mutate({\n mutation: gql`\n mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String) {\n saveFigure(\n source: $source\n id: $id\n expectedSource: $expectedSource\n creationId: $creationId\n thumbnail: $thumbnail\n type: $type\n ) {\n id\n source\n }\n }\n `,\n variables: request\n })\n if (response.errors?.length) throw Error(response.errors.map(e => e.message).join('\\n'))\n const result = response.data?.saveFigure as FigureProductRecord | undefined\n if (!result?.id || !result.source) throw Error('서버가 저장 결과를 돌려주지 않았습니다.')\n return result\n }\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../client/graphql/store.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,kBAAkB,GAAuB;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;IACjD,CAAC;IACD,4GAA4G;IAC5G,KAAK,CAAC,QAAQ,CAAC,EAAE;QACf,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;OAST;YACD,SAAS,EAAE,EAAE,EAAE,EAAE;YACjB,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,MAA8F,CAAA;QAC5H,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,IAAI,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;IAC9G,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,OAAO;QAChB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YAC3C,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;OAeZ;YACD,SAAS,EAAE,OAAO;SACnB,CAAC,CAAC,CAAA;QACH,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;YACzB,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAwC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;QACnK,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,UAA6C,CAAA;QAC3E,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,CAAA;QACxE,OAAO,MAAM,CAAA;IACf,CAAC;CACF,CAAA","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\nimport { fetchFigure } from './index.js'\nimport { reached } from './reached.js'\nimport type { FigureProductStore, FigureProductRecord } from '../modeller/product-session.js'\n\n/** source is sent intact. No conversion into the old parts/base authoring representation. */\nexport const figureProductStore: FigureProductStore = {\n async load(id) {\n const record = await fetchFigure(id)\n if (!record?.source) throw Error('Figure 정본이 없습니다.')\n return { id: record.id, source: record.source }\n },\n /* What the server holds of every field a save writes, read back after a save whose outcome is not known. */\n async readBack(id) {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n id\n source\n thumbnail\n type\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n const figure = response.data?.figure as { id: string; source: string; thumbnail?: string | null; type?: string } | undefined\n if (!figure?.source) throw Error('Figure 정본이 없습니다.')\n return { id: figure.id, source: figure.source, thumbnail: figure.thumbnail ?? undefined, type: figure.type }\n },\n async save(request) {\n const response = await reached(client.mutate({\n mutation: gql`\n mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String, $proposalKeys: [String!]) {\n saveFigure(\n source: $source\n id: $id\n expectedSource: $expectedSource\n creationId: $creationId\n thumbnail: $thumbnail\n type: $type\n proposalKeys: $proposalKeys\n ) {\n id\n source\n }\n }\n `,\n variables: request\n }))\n if (response.errors?.length)\n throw Object.assign(Error(response.errors.map(e => e.message).join('\\n')), { code: (response.errors[0] as { extensions?: { code?: string } }).extensions?.code })\n const result = response.data?.saveFigure as FigureProductRecord | undefined\n if (!result?.id || !result.source) throw Error('서버가 저장 결과를 돌려주지 않았습니다.')\n return result\n }\n}\n"]}
@@ -1,6 +1,9 @@
1
1
  import type { FigureProposalStep } from '@hatiolab/figure-model';
2
+ import type { FigureProductSession } from './product-session.js';
2
3
  /** What the card carries, as `proposeFigureCandidate` returned it. */
3
4
  export interface FigureProposalCard {
5
+ /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */
6
+ key?: string;
4
7
  steps?: FigureProposalStep[];
5
8
  create?: boolean;
6
9
  figureId?: string;
@@ -9,6 +12,7 @@ export interface FigureProposalCard {
9
12
  }
10
13
  /** The `figure-apply` event's detail. The page that takes it sets `applied`, and `error` when refused. */
11
14
  export interface FigureApplyDetail {
15
+ key?: string;
12
16
  steps: FigureProposalStep[];
13
17
  create: boolean;
14
18
  context?: string;
@@ -16,13 +20,15 @@ export interface FigureApplyDetail {
16
20
  applied?: boolean;
17
21
  error?: string;
18
22
  code?: string;
23
+ /** The clips a refused proposal would have stopped (CLIPS_DROPPED), by name. */
24
+ clips?: string[];
19
25
  }
20
26
  export type Say = (key: string, params?: Record<string, string>) => string;
21
27
  export interface FigureProposalTarget {
22
28
  /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */
23
29
  offer(detail: FigureApplyDetail): FigureApplyDetail;
24
30
  /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */
25
- openNew(steps: FigureProposalStep[]): Promise<{
31
+ openNew(steps: FigureProposalStep[], key?: string): Promise<{
26
32
  applied: boolean;
27
33
  error?: string;
28
34
  code?: string;
@@ -34,8 +40,30 @@ export interface FigureProposalOutcome {
34
40
  applied?: number;
35
41
  note?: string;
36
42
  }
43
+ /**
44
+ * The dock's "check the result" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:
45
+ * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,
46
+ * which the dock keeps as "not known" and never turns into a refusal. Nothing is run again.
47
+ */
48
+ export declare function proposalOutcomeFrom(saved: ReadonlyArray<{
49
+ figureId: string;
50
+ type?: string;
51
+ name?: string;
52
+ }>, say: Say): FigureProposalOutcome | undefined;
53
+ /**
54
+ * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands
55
+ * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new
56
+ * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a
57
+ * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.
58
+ */
59
+ export declare function takeFigureOffer(session: FigureProductSession, detail: FigureApplyDetail & {
60
+ changes?: unknown;
61
+ }, { onSavedFigure, say }: {
62
+ onSavedFigure: boolean;
63
+ say: Say;
64
+ }): boolean;
37
65
  export declare function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome>;
38
- export declare function pendNewFigure(steps: FigureProposalStep[], ms?: number): Promise<{
66
+ export declare function pendNewFigure(steps: FigureProposalStep[], key?: string, ms?: number): Promise<{
39
67
  applied: boolean;
40
68
  error?: string;
41
69
  code?: string;
@@ -43,6 +71,7 @@ export declare function pendNewFigure(steps: FigureProposalStep[], ms?: number):
43
71
  /** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */
44
72
  export declare function takePendingNewFigure(): {
45
73
  steps: FigureProposalStep[];
74
+ key?: string;
46
75
  settle: (result: {
47
76
  applied: boolean;
48
77
  error?: string;
@@ -1,13 +1,71 @@
1
+ /**
2
+ * The dock's "check the result" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:
3
+ * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,
4
+ * which the dock keeps as "not known" and never turns into a refusal. Nothing is run again.
5
+ */
6
+ export function proposalOutcomeFrom(saved, say) {
7
+ if (!saved.length)
8
+ return undefined;
9
+ const names = saved.map(f => f.name || f.type || f.figureId).join(', ');
10
+ return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) };
11
+ }
1
12
  /** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */
2
13
  const STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT']);
3
- function refused(code, error, say) {
14
+ function refused(code, error, say, clips) {
15
+ /*
16
+ A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not
17
+ asked about -- the person edits by hand if they mean it (designer 2026-09-27).
18
+ */
4
19
  const note = code && STALE.has(code)
5
20
  ? say('figure.text.proposal-stale')
6
21
  : code === 'NEW_FIGURE_UNSAVED'
7
22
  ? (error ?? '')
8
- : say('figure.text.proposal-refused', { reason: error ?? '' });
23
+ : code === 'CLIPS_DROPPED'
24
+ ? say('figure.text.proposal-would-drop-clips', { clips: (clips ?? []).join(', ') })
25
+ : say('figure.text.proposal-refused', { reason: error ?? '' });
9
26
  return { verdict: 'refused', errorCode: code ?? 'refused', note };
10
27
  }
28
+ /**
29
+ * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands
30
+ * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new
31
+ * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a
32
+ * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.
33
+ */
34
+ export function takeFigureOffer(session, detail, { onSavedFigure, say }) {
35
+ const view = session.read();
36
+ if (detail.create && onSavedFigure)
37
+ return false;
38
+ if (detail.create && view.dirty) {
39
+ /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */
40
+ detail.applied = false;
41
+ detail.code = 'NEW_FIGURE_UNSAVED';
42
+ detail.error = say('figure.text.proposal-new-figure-unsaved');
43
+ return false;
44
+ }
45
+ try {
46
+ if (detail.create) {
47
+ /* Created under the proposal's key: taken twice, it meets the first figure, never a second (ADR-0097 §5.6 item 5). */
48
+ if (detail.key)
49
+ session.createUnder(detail.key);
50
+ session.applyProposal(detail.steps, view.revision, view.context, detail.key);
51
+ }
52
+ else if (detail.steps)
53
+ session.applyProposal(detail.steps, detail.revision, detail.context, detail.key);
54
+ else
55
+ session.apply(detail.changes, 'ai', detail.revision, detail.context);
56
+ detail.applied = true;
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ detail.applied = false;
61
+ detail.code = error?.code;
62
+ detail.error = error instanceof Error ? error.message : String(error);
63
+ const clips = error?.clips;
64
+ if (clips)
65
+ detail.clips = clips.map(c => c.clip);
66
+ return false;
67
+ }
68
+ }
11
69
  export async function runFigureProposal(card, target, say) {
12
70
  const steps = card?.steps;
13
71
  if (!Array.isArray(steps) || !steps.length)
@@ -18,15 +76,15 @@ export async function runFigureProposal(card, target, say) {
18
76
  applied: steps.length,
19
77
  note: say('figure.text.proposal-applied', { n: String(steps.length) })
20
78
  });
21
- const detail = target.offer({ steps, create, context: card.context, revision: card.revision });
79
+ const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision });
22
80
  if (detail.applied)
23
81
  return accepted();
24
82
  if (detail.applied === false)
25
- return refused(detail.code, detail.error, say);
83
+ return refused(detail.code, detail.error, say, detail.clips);
26
84
  /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */
27
85
  if (!create)
28
86
  return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') };
29
- const opened = await target.openNew(steps);
87
+ const opened = await target.openNew(steps, card.key);
30
88
  if (opened.applied)
31
89
  return accepted();
32
90
  return opened.code === 'not-opened'
@@ -39,11 +97,12 @@ export async function runFigureProposal(card, target, say) {
39
97
  figure must not receive steps nobody is waiting for.
40
98
  */
41
99
  let pending;
42
- export function pendNewFigure(steps, ms = 10_000) {
100
+ export function pendNewFigure(steps, key, ms = 10_000) {
43
101
  pending?.settle({ applied: false, code: 'not-opened' });
44
102
  return new Promise(resolve => {
45
103
  const mine = {
46
104
  steps,
105
+ key,
47
106
  settle: (result) => {
48
107
  clearTimeout(timer);
49
108
  if (pending === mine)
@@ -1 +1 @@
1
- {"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/** The `figure-apply` event's detail. The page that takes it sets `applied`, and `error` when refused. */\nexport interface FigureApplyDetail {\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n code?: string\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[]): Promise<{ applied: boolean; error?: string; code?: string }>\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(code: string | undefined, error: string | undefined, say: Say): FigureProposalOutcome {\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail.code, detail.error, say)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened.code, opened.error, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure(): { steps: FigureProposalStep[]; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
1
+ {"version":3,"file":"ai-proposal.js","sourceRoot":"","sources":["../../client/modeller/ai-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * An AI proposal reaching the modeller (ADR-0094). The dock's card carries the modeller's own commands; running it\n * applies them as one step of the modeller's history, undone whole, on the version the model saw. Nothing half made\n * is applied, and every outcome is said: applied, refused with the reason, or not possible here.\n *\n * Kept free of Lit, i18n and the shell so node tests read it directly; the handler and the page pass in what they own.\n */\nimport type { FigureProposalStep } from '@hatiolab/figure-model'\nimport type { FigureProductSession } from './product-session.js'\n\n/** What the card carries, as `proposeFigureCandidate` returned it. */\nexport interface FigureProposalCard {\n /** The proposal's own name, given by the tool: a save records it, a new figure made from it is created under it. */\n key?: string\n steps?: FigureProposalStep[]\n create?: boolean\n figureId?: string\n context?: string\n revision?: number\n}\n\n/** The `figure-apply` event's detail. The page that takes it sets `applied`, and `error` when refused. */\nexport interface FigureApplyDetail {\n key?: string\n steps: FigureProposalStep[]\n create: boolean\n context?: string\n revision?: number\n applied?: boolean\n error?: string\n code?: string\n /** The clips a refused proposal would have stopped (CLIPS_DROPPED), by name. */\n clips?: string[]\n}\n\nexport type Say = (key: string, params?: Record<string, string>) => string\n\nexport interface FigureProposalTarget {\n /** Offers the proposal to a modeller on screen. It comes back untouched when none took it. */\n offer(detail: FigureApplyDetail): FigureApplyDetail\n /** Opens a new figure in the modeller and applies the steps there; resolves once the page has, or has not. */\n openNew(steps: FigureProposalStep[], key?: string): Promise<{ applied: boolean; error?: string; code?: string }>\n}\n\nexport interface FigureProposalOutcome {\n verdict: 'accepted' | 'refused' | 'unavailable'\n errorCode?: string\n applied?: number\n note?: string\n}\n\n/**\n * The dock's \"check the result\" (ADR-0097 §5.6 item 5): what the server recorded of a proposal. Saved into figures:\n * accepted, named. Nothing recorded: not known -- applied and not saved yet, undone, or never taken -- so `undefined`,\n * which the dock keeps as \"not known\" and never turns into a refusal. Nothing is run again.\n */\nexport function proposalOutcomeFrom(saved: ReadonlyArray<{ figureId: string; type?: string; name?: string }>, say: Say): FigureProposalOutcome | undefined {\n if (!saved.length) return undefined\n const names = saved.map(f => f.name || f.type || f.figureId).join(', ')\n return { verdict: 'accepted', note: say('figure.text.proposal-saved-into', { names }) }\n}\n\n/** The workspace refuses a request made on another version; said as what happened, not as the kernel's code. */\nconst STALE = new Set(['REVISION_CONFLICT', 'CONTEXT_CONFLICT'])\n\nfunction refused(code: string | undefined, error: string | undefined, say: Say, clips?: string[]): FigureProposalOutcome {\n /*\n A proposal that would stop clips is refused whole; nobody but the person may agree to that, and a proposal is not\n asked about -- the person edits by hand if they mean it (designer 2026-09-27).\n */\n const note =\n code && STALE.has(code)\n ? say('figure.text.proposal-stale')\n : code === 'NEW_FIGURE_UNSAVED'\n ? (error ?? '')\n : code === 'CLIPS_DROPPED'\n ? say('figure.text.proposal-would-drop-clips', { clips: (clips ?? []).join(', ') })\n : say('figure.text.proposal-refused', { reason: error ?? '' })\n return { verdict: 'refused', errorCode: code ?? 'refused', note }\n}\n\n/**\n * The modeller page's way of taking an offer from the dock (`figure-apply`): input changes, or a proposal's commands\n * as one step (ADR-0094). A revision is applied on the version the model saw; a new figure's proposal only on a new\n * figure not yet touched, and a page on a saved figure leaves it untaken. Never with a consent to let clips go: a\n * proposal that would stop one is refused whole, the clips named (designer 2026-09-27). True when it was applied.\n */\nexport function takeFigureOffer(\n session: FigureProductSession,\n detail: FigureApplyDetail & { changes?: unknown },\n { onSavedFigure, say }: { onSavedFigure: boolean; say: Say }\n): boolean {\n const view = session.read()\n if (detail.create && onSavedFigure) return false\n if (detail.create && view.dirty) {\n /* A new figure already being made is the person's; a proposal does not go on top of it or replace it. */\n detail.applied = false\n detail.code = 'NEW_FIGURE_UNSAVED'\n detail.error = say('figure.text.proposal-new-figure-unsaved')\n return false\n }\n try {\n if (detail.create) {\n /* Created under the proposal's key: taken twice, it meets the first figure, never a second (ADR-0097 §5.6 item 5). */\n if (detail.key) session.createUnder(detail.key)\n session.applyProposal(detail.steps, view.revision!, view.context!, detail.key)\n } else if (detail.steps) session.applyProposal(detail.steps, detail.revision!, detail.context!, detail.key)\n else session.apply(detail.changes as never, 'ai', detail.revision, detail.context)\n detail.applied = true\n return true\n } catch (error) {\n detail.applied = false\n detail.code = (error as { code?: string })?.code\n detail.error = error instanceof Error ? error.message : String(error)\n const clips = (error as { clips?: Array<{ clip: string }> })?.clips\n if (clips) detail.clips = clips.map(c => c.clip)\n return false\n }\n}\n\nexport async function runFigureProposal(card: FigureProposalCard, target: FigureProposalTarget, say: Say): Promise<FigureProposalOutcome> {\n const steps = card?.steps\n if (!Array.isArray(steps) || !steps.length) return { verdict: 'unavailable', errorCode: 'no-steps' }\n const create = card.create === true\n const accepted = (): FigureProposalOutcome => ({\n verdict: 'accepted',\n applied: steps.length,\n note: say('figure.text.proposal-applied', { n: String(steps.length) })\n })\n\n const detail = target.offer({ key: card.key, steps, create, context: card.context, revision: card.revision })\n if (detail.applied) return accepted()\n if (detail.applied === false) return refused(detail.code, detail.error, say, detail.clips)\n\n /* A revision belongs to the figure the model saw; with that modeller gone there is nowhere true to apply it. */\n if (!create) return { verdict: 'unavailable', errorCode: 'modeller-not-open', note: say('figure.text.proposal-needs-modeller') }\n\n const opened = await target.openNew(steps, card.key)\n if (opened.applied) return accepted()\n return opened.code === 'not-opened'\n ? { verdict: 'unavailable', errorCode: 'not-opened', note: say('figure.text.proposal-not-opened') }\n : refused(opened.code, opened.error, say)\n}\n\n/*\n A new figure's proposal handed to the modeller page across a navigation: the handler leaves it here and opens the\n modeller on a new figure; the page takes it once it has started that figure. Left untaken, it lapses -- a later new\n figure must not receive steps nobody is waiting for.\n*/\nlet pending: { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void } | undefined\n\nexport function pendNewFigure(steps: FigureProposalStep[], key?: string, ms = 10_000): Promise<{ applied: boolean; error?: string; code?: string }> {\n pending?.settle({ applied: false, code: 'not-opened' })\n return new Promise(resolve => {\n const mine = {\n steps,\n key,\n settle: (result: { applied: boolean; error?: string; code?: string }) => {\n clearTimeout(timer)\n if (pending === mine) pending = undefined\n resolve(result)\n }\n }\n const timer = setTimeout(() => mine.settle({ applied: false, code: 'not-opened' }), ms)\n pending = mine\n })\n}\n\n/** For the page, on a new figure it has just started: the steps waiting for it, and where to say how it went. */\nexport function takePendingNewFigure():\n | { steps: FigureProposalStep[]; key?: string; settle: (result: { applied: boolean; error?: string; code?: string }) => void }\n | undefined {\n const taken = pending\n pending = undefined\n return taken\n}\n"]}
@@ -11,7 +11,7 @@ import type { GizmoReach } from './gizmo.js';
11
11
  * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only
12
12
  * where the gizmo could (`GizmoReach`).
13
13
  */
14
- export type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap';
14
+ export type AlignTool = 'centre-x' | 'centre-z' | 'snap';
15
15
  export declare function boxOf(asset: FigureAsset, part: string): FigureWorldBox | undefined;
16
16
  /** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */
17
17
  export declare function gridStepOf(asset: FigureAsset): number;
@@ -31,8 +31,6 @@ export function alignChanges(asset, part, reach, tool) {
31
31
  return [set(at('x'), value(at('x')) - box.centre.x)];
32
32
  case 'centre-z':
33
33
  return [set(at('z'), value(at('z')) - box.centre.z)];
34
- case 'floor':
35
- return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))];
36
34
  case 'snap': {
37
35
  const step = gridStepOf(asset);
38
36
  const round = (n) => Math.round(n / step) * step;
@@ -1 +1 @@
1
- {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAkBnG,MAAM,IAAI,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAU,CAAA;AAErC,MAAM,UAAU,KAAK,CAAC,KAAkB,EAAE,IAAY;IACpD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAA;IAC9D,OAAO,qBAAqB,CAAC,QAAQ,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;AAC/F,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,UAAU,CAAC,KAAkB;IAC3C,MAAM,QAAQ,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAA;IAC9D,MAAM,KAAK,GAAG,qBAAqB,CAAC,QAAQ,EAAE,eAAe,CAAC,KAAK,CAAC,CAAC,CAAA;IACrE,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,OAAO,CAAC,CAAA;IAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACnB,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACjB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3E,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3E,OAAO,EAAE,GAAG,EAAE,CAAA;IAChB,CAAC,CAAC,CACH,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAkB,EAAE,IAAY,EAAE,KAAiB,EAAE,IAAe;IAC/F,IAAI,CAAC,KAAK,CAAC,SAAS;QAAE,OAAO,EAAE,CAAA;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAA;IACzD,MAAM,GAAG,GAAG,CAAC,KAAa,EAAE,IAAY,EAAqB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpI,MAAM,EAAE,GAAG,CAAC,IAAqB,EAAE,EAAE,CAAC,GAAG,IAAI,aAAa,IAAI,EAAE,CAAA;IAChE,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU;YACb,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,KAAK,UAAU;YACb,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;QACtD,KAAK,OAAO;YACV,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3E,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;YAC9B,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI,CAAA;YACxD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACvE,yDAAyD;YACzD,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK;gBAC5B,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,KAAK;oBAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YACvF,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAY,EAAE,IAAe;IAC3F,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,GAAG,GAAG,IAAI,aAAa,IAAI,EAAE,CAAA;IACrC,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,IAAI,aAAa,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAA;AACzJ,CAAC;AAED,6GAA6G;AAC7G,MAAM,UAAU,SAAS,CAAC,KAAkB,EAAE,IAAY,EAAE,IAAe,EAAE,KAAiB;IAC5F,MAAM,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5D,OAAO,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAA;AACnD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IAC3D,MAAM,CAAC,GAAG,GAAG,GAAG,SAAS,CAAA;IACzB,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC7D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAA;AAC1C,CAAC","sourcesContent":["import { compileFigureAsset, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'floor' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\nexport function boxOf(asset: FigureAsset, part: string): FigureWorldBox | undefined {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n return figureOccupiedBoxesOf(geometry, figureWritersOf(asset)).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: FigureAsset): number {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n const boxes = figureOccupiedBoxesOf(geometry, figureWritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: FigureAsset, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'floor':\n return [set(at('y'), value(at('y')) - (box.centre.y - box.extent.y / 2))]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: FigureAsset, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */\nexport function canMirror(asset: FigureAsset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
1
+ {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { compileFigureAsset, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * mirror on X or Z, snap to the grid.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\nexport function boxOf(asset: FigureAsset, part: string): FigureWorldBox | undefined {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n return figureOccupiedBoxesOf(geometry, figureWritersOf(asset)).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(asset: FigureAsset): number {\n const geometry = compileFigureAsset(asset).evaluate().geometry\n const boxes = figureOccupiedBoxesOf(geometry, figureWritersOf(asset))\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(asset: FigureAsset, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(asset, part)\n if (!box) return []\n const value = (id: string) => asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(asset)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by\n * `duplicate-part` first; this is the position the copy is then given.\n */\nexport function mirrorChanges(asset: FigureAsset, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(asset, part)\n if (!box) return []\n const id = `${part}.position.${axis}`\n return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }]\n}\n\n/** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */\nexport function canMirror(asset: FigureAsset, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(asset, part) : undefined\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}