@things-factory/figure-ui 10.1.244 → 10.1.246

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 (135) hide show
  1. package/client/graphql/index.ts +15 -0
  2. package/client/modeller/align.ts +51 -13
  3. package/client/modeller/clips.ts +51 -1
  4. package/client/modeller/data-interface.ts +16 -0
  5. package/client/modeller/editor-styles.ts +6 -0
  6. package/client/modeller/editor.ts +112 -25
  7. package/client/modeller/fasten-form.ts +8 -1
  8. package/client/modeller/fasten-geometry.ts +24 -2
  9. package/client/modeller/figure-clips.ts +137 -4
  10. package/client/modeller/figure-fasten-dialog.ts +1 -0
  11. package/client/modeller/figure-fasten-drawing.ts +11 -5
  12. package/client/modeller/figure-links.ts +25 -0
  13. package/client/modeller/figure-material-panel.ts +54 -4
  14. package/client/modeller/figure-parts.ts +7 -2
  15. package/client/modeller/figure-pipe-panel.ts +215 -0
  16. package/client/modeller/figure-preview.ts +125 -4
  17. package/client/modeller/figure-section-editor.ts +36 -3
  18. package/client/modeller/links.ts +24 -2
  19. package/client/modeller/material-edit.ts +9 -0
  20. package/client/modeller/maturity.ts +15 -4
  21. package/client/modeller/palette.ts +9 -0
  22. package/client/modeller/pipe-path.ts +21 -0
  23. package/client/modeller/refusals.ts +66 -1
  24. package/client/modeller/section-controls.ts +2 -1
  25. package/client/modeller/section-drawing.ts +20 -1
  26. package/client/modeller/starting-shape.ts +8 -5
  27. package/client/pages/figure-list-page.ts +15 -5
  28. package/dist-client/graphql/index.d.ts +2 -0
  29. package/dist-client/graphql/index.js +15 -0
  30. package/dist-client/graphql/index.js.map +1 -1
  31. package/dist-client/modeller/align.d.ts +28 -7
  32. package/dist-client/modeller/align.js +37 -12
  33. package/dist-client/modeller/align.js.map +1 -1
  34. package/dist-client/modeller/clips.d.ts +24 -0
  35. package/dist-client/modeller/clips.js +53 -1
  36. package/dist-client/modeller/clips.js.map +1 -1
  37. package/dist-client/modeller/data-interface.d.ts +6 -0
  38. package/dist-client/modeller/data-interface.js +17 -0
  39. package/dist-client/modeller/data-interface.js.map +1 -1
  40. package/dist-client/modeller/editor-styles.js +6 -0
  41. package/dist-client/modeller/editor-styles.js.map +1 -1
  42. package/dist-client/modeller/editor.d.ts +10 -1
  43. package/dist-client/modeller/editor.js +117 -26
  44. package/dist-client/modeller/editor.js.map +1 -1
  45. package/dist-client/modeller/fasten-form.d.ts +2 -0
  46. package/dist-client/modeller/fasten-form.js +5 -1
  47. package/dist-client/modeller/fasten-form.js.map +1 -1
  48. package/dist-client/modeller/fasten-geometry.d.ts +8 -1
  49. package/dist-client/modeller/fasten-geometry.js +22 -1
  50. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  51. package/dist-client/modeller/figure-clips.d.ts +9 -0
  52. package/dist-client/modeller/figure-clips.js +137 -4
  53. package/dist-client/modeller/figure-clips.js.map +1 -1
  54. package/dist-client/modeller/figure-fasten-dialog.js +1 -0
  55. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  56. package/dist-client/modeller/figure-fasten-drawing.d.ts +3 -1
  57. package/dist-client/modeller/figure-fasten-drawing.js +13 -5
  58. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  59. package/dist-client/modeller/figure-links.d.ts +5 -0
  60. package/dist-client/modeller/figure-links.js +22 -0
  61. package/dist-client/modeller/figure-links.js.map +1 -1
  62. package/dist-client/modeller/figure-material-panel.d.ts +2 -0
  63. package/dist-client/modeller/figure-material-panel.js +53 -4
  64. package/dist-client/modeller/figure-material-panel.js.map +1 -1
  65. package/dist-client/modeller/figure-parts.d.ts +1 -1
  66. package/dist-client/modeller/figure-parts.js +5 -1
  67. package/dist-client/modeller/figure-parts.js.map +1 -1
  68. package/dist-client/modeller/figure-pipe-panel.d.ts +29 -0
  69. package/dist-client/modeller/figure-pipe-panel.js +221 -0
  70. package/dist-client/modeller/figure-pipe-panel.js.map +1 -0
  71. package/dist-client/modeller/figure-preview.d.ts +8 -0
  72. package/dist-client/modeller/figure-preview.js +128 -4
  73. package/dist-client/modeller/figure-preview.js.map +1 -1
  74. package/dist-client/modeller/figure-section-editor.d.ts +5 -0
  75. package/dist-client/modeller/figure-section-editor.js +33 -3
  76. package/dist-client/modeller/figure-section-editor.js.map +1 -1
  77. package/dist-client/modeller/links.d.ts +6 -0
  78. package/dist-client/modeller/links.js +17 -2
  79. package/dist-client/modeller/links.js.map +1 -1
  80. package/dist-client/modeller/material-edit.d.ts +8 -0
  81. package/dist-client/modeller/material-edit.js +8 -0
  82. package/dist-client/modeller/material-edit.js.map +1 -1
  83. package/dist-client/modeller/maturity.js +16 -5
  84. package/dist-client/modeller/maturity.js.map +1 -1
  85. package/dist-client/modeller/palette.js +9 -0
  86. package/dist-client/modeller/palette.js.map +1 -1
  87. package/dist-client/modeller/pipe-path.d.ts +19 -0
  88. package/dist-client/modeller/pipe-path.js +14 -0
  89. package/dist-client/modeller/pipe-path.js.map +1 -0
  90. package/dist-client/modeller/refusals.d.ts +12 -1
  91. package/dist-client/modeller/refusals.js +67 -0
  92. package/dist-client/modeller/refusals.js.map +1 -1
  93. package/dist-client/modeller/section-controls.js +2 -1
  94. package/dist-client/modeller/section-controls.js.map +1 -1
  95. package/dist-client/modeller/section-drawing.d.ts +5 -0
  96. package/dist-client/modeller/section-drawing.js +19 -1
  97. package/dist-client/modeller/section-drawing.js.map +1 -1
  98. package/dist-client/modeller/starting-shape.d.ts +9 -2
  99. package/dist-client/modeller/starting-shape.js +8 -3
  100. package/dist-client/modeller/starting-shape.js.map +1 -1
  101. package/dist-client/pages/figure-list-page.d.ts +1 -0
  102. package/dist-client/pages/figure-list-page.js +12 -3
  103. package/dist-client/pages/figure-list-page.js.map +1 -1
  104. package/dist-client/tsconfig.tsbuildinfo +1 -1
  105. package/package.json +8 -8
  106. package/test/align.test.ts +28 -6
  107. package/test/browser/editing-page.js +21 -1
  108. package/test/browser/editing.check.mjs +124 -0
  109. package/test/browser/links.check.mjs +49 -8
  110. package/test/browser/modeller.check.mjs +27 -1
  111. package/test/browser/part-ids.check.mjs +60 -3
  112. package/test/browser/parts.check.mjs +2 -2
  113. package/test/browser/places-mesh.check.mjs +14 -8
  114. package/test/browser/screen.check.mjs +54 -1
  115. package/test/browser/section-page.js +7 -1
  116. package/test/browser/section.check.mjs +81 -0
  117. package/test/browser/side-panel-sweep.check.mjs +13 -0
  118. package/test/clips.test.ts +30 -1
  119. package/test/data-motion.test.ts +14 -1
  120. package/test/fasten-geometry.test.ts +13 -2
  121. package/test/joint-frames.test.ts +12 -2
  122. package/test/links.test.ts +24 -0
  123. package/test/material.test.ts +15 -1
  124. package/test/maturity.test.ts +19 -1
  125. package/test/palette-icons.test.ts +2 -1
  126. package/test/pipe-panel.test.ts +33 -0
  127. package/test/refusals.test.ts +29 -0
  128. package/test/revision.test.ts +1 -1
  129. package/test/section-editor.test.ts +17 -1
  130. package/translations/en.json +86 -3
  131. package/translations/es.json +86 -3
  132. package/translations/ja.json +86 -3
  133. package/translations/ko.json +86 -3
  134. package/translations/ms.json +86 -3
  135. package/translations/zh.json +86 -3
@@ -3,12 +3,14 @@
3
3
  * 원기둥, 문틀, 구, 원뿔, 그린 단면, 뿔. A word for the screen, read from the shape node's own params; it is not a kind of
4
4
  * stored shape, and nothing is decided by it.
5
5
  */
6
- export type StartingShapeKind = 'cube' | 'rounded-box' | 'hollow-box' | 'cylinder' | 'portal' | 'sphere' | 'frustum' | 'polygon' | 'pyramid'
6
+ export type StartingShapeKind = 'cube' | 'rounded-box' | 'hollow-box' | 'cylinder' | 'portal' | 'sphere' | 'frustum' | 'polygon' | 'pyramid' | 'tapered-box' | 'pipe' | 'duct'
7
7
 
8
8
  export function startingShapeKindOf(shape: { op: string; params?: Record<string, unknown> } | undefined): StartingShapeKind | undefined {
9
9
  const p = shape?.params ?? {}
10
10
  if (shape?.op === 'revolve@1') return p.profile === 'taper' ? 'frustum' : 'sphere'
11
- if (shape?.op === 'loft@1') return 'pyramid'
11
+ if (shape?.op === 'loft@1') return p.section === 'rectangle' ? 'tapered-box' : 'pyramid'
12
+ /* A section along a path (ADR-0100 §16.2): a round one is a pipe, a bent bar without a wall; a rectangle's a duct. */
13
+ if (shape?.op === 'sweep@1') return p.section === 'ellipse' ? 'pipe' : 'duct'
12
14
  if (shape?.op !== 'extrude@1') return undefined
13
15
  if (p.section === 'ellipse') return 'cylinder'
14
16
  if (p.section === 'polygon') return 'polygon'
@@ -21,8 +23,9 @@ export function startingShapeKindOf(shape: { op: string; params?: Record<string,
21
23
  * The same word for an evaluated body, when only the shape the kernel evaluated is in hand (a row of the parts list):
22
24
  * a door frame is the section with jambs, a drawn section the rest of the drawn ones.
23
25
  */
24
- export function startingShapeKindOfBody(shape: { provider?: string; dimensions?: Record<string, number>; extrusion?: { section: { kind: string; corner?: unknown } }; cavity?: unknown; revolution?: { profile: { kind: string } }; loft?: unknown } | undefined): StartingShapeKind | undefined {
25
- if (shape?.loft) return 'pyramid'
26
+ export function startingShapeKindOfBody(shape: { provider?: string; dimensions?: Record<string, number>; extrusion?: { section: { kind: string; corner?: unknown } }; cavity?: unknown; revolution?: { profile: { kind: string } }; loft?: { to?: unknown }; sweep?: { section: { kind: string } } } | undefined): StartingShapeKind | undefined {
27
+ if (shape?.loft) return shape.loft.to ? 'tapered-box' : 'pyramid'
28
+ if (shape?.sweep) return shape.sweep.section.kind === 'ellipse' ? 'pipe' : 'duct'
26
29
  if (shape?.revolution) return shape.revolution.profile.kind === 'taper' ? 'frustum' : 'sphere'
27
30
  const s = shape?.extrusion?.section
28
31
  if (!s) return undefined
@@ -33,4 +36,4 @@ export function startingShapeKindOfBody(shape: { provider?: string; dimensions?:
33
36
  }
34
37
 
35
38
  /** Whether a body is round across -- a circle or an ellipse pushed, anything turned: it takes a segment count. */
36
- export const isRoundBody = (shape: Parameters<typeof startingShapeKindOfBody>[0]) => ['cylinder', 'sphere', 'frustum'].includes(startingShapeKindOfBody(shape) ?? '')
39
+ export const isRoundBody = (shape: Parameters<typeof startingShapeKindOfBody>[0]) => ['cylinder', 'sphere', 'frustum', 'pipe'].includes(startingShapeKindOfBody(shape) ?? '')
@@ -14,7 +14,7 @@ import { notify } from '@operato/layout'
14
14
  import { CardCatalogStyles, CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePageStyles } from '@operato/styles'
15
15
 
16
16
  import { capacityFrom } from '../modeller/scene-budget.js'
17
- import { deleteFigure, fetchFigureList } from '../graphql/index.js'
17
+ import { deleteFigure, fetchFigureList, fetchFigureStateCounts } from '../graphql/index.js'
18
18
  import type { Figure } from '../types.js'
19
19
  import { hasBeenReleased } from '../figure-released.js'
20
20
  import { modellerPathOf } from '../modeller/modeller-route.js'
@@ -116,6 +116,8 @@ export class FigureListPage extends FigureListPageBase {
116
116
  * only looked at, so a switch inside one list mixed two jobs. `figure-public-list-page` sets it.
117
117
  */
118
118
  protected readonly scope: 'own' | 'public' = 'own'
119
+ /* How many figures each state chip would show, under the list's other conditions - counted by the server. */
120
+ private stateCounts = new Map<string, number>()
119
121
 
120
122
  private listen?: () => void
121
123
 
@@ -335,10 +337,11 @@ export class FigureListPage extends FigureListPageBase {
335
337
  type: 'select-buttons',
336
338
  operator: 'in',
337
339
  label: '',
338
- options: [
339
- { display: i18next.t('figure.label.draft'), value: 'draft' },
340
- { display: i18next.t('figure.label.figure-released'), value: 'released' }
341
- ]
340
+ options: () =>
341
+ [
342
+ { display: i18next.t('figure.label.draft'), value: 'draft' },
343
+ { display: i18next.t('figure.label.figure-released'), value: 'released' }
344
+ ].map(option => ({ ...option, count: this.stateCounts.get(option.value) ?? 0 }))
342
345
  }
343
346
  : undefined,
344
347
  width: 120
@@ -541,6 +544,13 @@ export class FigureListPage extends FigureListPageBase {
541
544
  * 받아 화면에 낸다 — 목록이 비어 있는 것과 못 불러온 것은 다른 사실이다.
542
545
  */
543
546
  async fetchHandler({ page = 1, limit = 24, sortings = [], filters = [] }: FetchOption) {
547
+ if (this.scope === 'own')
548
+ fetchFigureStateCounts(filters)
549
+ .then(counts => {
550
+ this.stateCounts = counts
551
+ ;(this.renderRoot.querySelector('ox-filters-form') as any)?.requestUpdate()
552
+ })
553
+ .catch(e => console.error(e))
544
554
  const { items, total } = await fetchFigureList({ page, limit, filters, sortings, scope: this.scope })
545
555
 
546
556
  return { total: total || 0, records: items || [] }
@@ -7,6 +7,8 @@ import type { Figure, FigureFinding, FigureInspection, FigureListResult, FigureP
7
7
  * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
8
8
  * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
9
9
  */
10
+ /** How many of the domain's figures each state chip would show (draft, released), under the list's other conditions. */
11
+ export declare function fetchFigureStateCounts(filters: unknown[]): Promise<Map<string, number>>;
10
12
  export declare function fetchFigureList(params: {
11
13
  page?: number;
12
14
  limit?: number;
@@ -59,6 +59,21 @@ const FIGURE_LIST_FIELDS = `
59
59
  * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,
60
60
  * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.
61
61
  */
62
+ /** How many of the domain's figures each state chip would show (draft, released), under the list's other conditions. */
63
+ export async function fetchFigureStateCounts(filters) {
64
+ const response = await client.query({
65
+ query: gql `
66
+ query ($filters: [Filter!]) {
67
+ figureStateCounts(filters: $filters) { value count }
68
+ }
69
+ `,
70
+ variables: { filters },
71
+ fetchPolicy: 'no-cache'
72
+ });
73
+ if (response.errors?.length)
74
+ throw new Error(response.errors.map(one => one.message).join(' · '));
75
+ return new Map((response.data?.figureStateCounts ?? []).map((row) => [row.value, row.count]));
76
+ }
62
77
  export async function fetchFigureList(params) {
63
78
  const field = params.scope === 'public' ? 'publicFigures' : 'figures';
64
79
  const response = await client.query({
@@ -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, uploadContext, type UploadProgress } 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 namespace\n type\n name\n description\n category\n tags\n state\n version\n deprecated\n supersededBy\n public\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 /** 'public': other domains' public figures, with their domain (LV figure-server design G2). The domain's own by default. */\n scope?: 'own' | 'public'\n}): Promise<FigureListResult> {\n const field = params.scope === 'public' ? 'publicFigures' : 'figures'\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n ${field}(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} ${params.scope === 'public' ? 'domain { id name subdomain }' : ''} }\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, field)\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\n/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */\nexport async function setFigurePublic(id: string, isPublic: boolean): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $public: Boolean!) {\n setFigurePublic(id: $id, public: $public) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, public: isPublic }\n })\n\n return unwrap<Figure>(response, 'setFigurePublic')\n}\n\n/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */\nexport async function setFigureDeprecation(id: string, deprecated: boolean, supersededBy?: string | null): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $deprecated: Boolean!, $supersededBy: String) {\n setFigureDeprecation(id: $id, deprecated: $deprecated, supersededBy: $supersededBy) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, deprecated, supersededBy: supersededBy || null }\n })\n\n return unwrap<Figure>(response, 'setFigureDeprecation')\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 namespace the domain owns (LV figure-server design G1): the `{namespace}` of the full name `{namespace}/{type}`. */\nexport interface FigureNamespace {\n id: string\n name: string\n description?: string | null\n state: 'active' | 'suspended'\n isDefault: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'id name description state isDefault'\n\n/** The categories the server allows, or null when a category is free text (LV figure-server design G5). */\nexport async function fetchFigureCategories(): Promise<string[] | null> {\n const response = await client.query({\n query: gql`\n query {\n figureCategories\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<string[] | null>(response, 'figureCategories') ?? null\n}\n\n/** The domain's namespaces, its default first. */\nexport async function fetchFigureNamespaces(): Promise<FigureNamespace[]> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespaces { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespace[]>(response, 'figureNamespaces') ?? []\n}\n\n/** Adds a namespace to the domain; the server says why when it refuses (not a namespace, another domain's). */\nexport async function createFigureNamespace(name: string, isDefault = false): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $isDefault: Boolean) {\n createFigureNamespace(name: $name, isDefault: $isDefault) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, isDefault }\n })\n return unwrap<FigureNamespace>(response, 'createFigureNamespace')\n}\n\n/** Makes one of the domain's active namespaces its default. */\nexport async function setDefaultFigureNamespace(name: string): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!) {\n setDefaultFigureNamespace(name: $name) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name }\n })\n return unwrap<FigureNamespace>(response, 'setDefaultFigureNamespace')\n}\n\n/** Suspends a namespace or makes it active again. */\nexport async function setFigureNamespaceState(name: string, state: 'active' | 'suspended'): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $state: String!) {\n setFigureNamespaceState(name: $name, state: $state) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, state }\n })\n return unwrap<FigureNamespace>(response, 'setFigureNamespaceState')\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 return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: 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<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? 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\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File, onProgress: UploadProgress): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: uploadContext(onProgress)\n })\n return unwrap(response, 'uploadFigureMesh')\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, uploadContext, type UploadProgress } 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 namespace\n type\n name\n description\n category\n tags\n state\n version\n deprecated\n supersededBy\n public\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 */\n/** How many of the domain's figures each state chip would show (draft, released), under the list's other conditions. */\nexport async function fetchFigureStateCounts(filters: unknown[]): Promise<Map<string, number>> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!]) {\n figureStateCounts(filters: $filters) { value count }\n }\n `,\n variables: { filters },\n fetchPolicy: 'no-cache'\n })\n if (response.errors?.length) throw new Error(response.errors.map(one => one.message).join(' · '))\n return new Map((response.data?.figureStateCounts ?? []).map((row: { value: string; count: number }) => [row.value, row.count]))\n}\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n /** 'public': other domains' public figures, with their domain (LV figure-server design G2). The domain's own by default. */\n scope?: 'own' | 'public'\n}): Promise<FigureListResult> {\n const field = params.scope === 'public' ? 'publicFigures' : 'figures'\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n ${field}(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} ${params.scope === 'public' ? 'domain { id name subdomain }' : ''} }\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, field)\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\n/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */\nexport async function setFigurePublic(id: string, isPublic: boolean): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $public: Boolean!) {\n setFigurePublic(id: $id, public: $public) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, public: isPublic }\n })\n\n return unwrap<Figure>(response, 'setFigurePublic')\n}\n\n/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */\nexport async function setFigureDeprecation(id: string, deprecated: boolean, supersededBy?: string | null): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $deprecated: Boolean!, $supersededBy: String) {\n setFigureDeprecation(id: $id, deprecated: $deprecated, supersededBy: $supersededBy) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, deprecated, supersededBy: supersededBy || null }\n })\n\n return unwrap<Figure>(response, 'setFigureDeprecation')\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 namespace the domain owns (LV figure-server design G1): the `{namespace}` of the full name `{namespace}/{type}`. */\nexport interface FigureNamespace {\n id: string\n name: string\n description?: string | null\n state: 'active' | 'suspended'\n isDefault: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'id name description state isDefault'\n\n/** The categories the server allows, or null when a category is free text (LV figure-server design G5). */\nexport async function fetchFigureCategories(): Promise<string[] | null> {\n const response = await client.query({\n query: gql`\n query {\n figureCategories\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<string[] | null>(response, 'figureCategories') ?? null\n}\n\n/** The domain's namespaces, its default first. */\nexport async function fetchFigureNamespaces(): Promise<FigureNamespace[]> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespaces { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespace[]>(response, 'figureNamespaces') ?? []\n}\n\n/** Adds a namespace to the domain; the server says why when it refuses (not a namespace, another domain's). */\nexport async function createFigureNamespace(name: string, isDefault = false): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $isDefault: Boolean) {\n createFigureNamespace(name: $name, isDefault: $isDefault) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, isDefault }\n })\n return unwrap<FigureNamespace>(response, 'createFigureNamespace')\n}\n\n/** Makes one of the domain's active namespaces its default. */\nexport async function setDefaultFigureNamespace(name: string): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!) {\n setDefaultFigureNamespace(name: $name) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name }\n })\n return unwrap<FigureNamespace>(response, 'setDefaultFigureNamespace')\n}\n\n/** Suspends a namespace or makes it active again. */\nexport async function setFigureNamespaceState(name: string, state: 'active' | 'suspended'): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $state: String!) {\n setFigureNamespaceState(name: $name, state: $state) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, state }\n })\n return unwrap<FigureNamespace>(response, 'setFigureNamespaceState')\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 return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: 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<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? 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\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File, onProgress: UploadProgress): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: uploadContext(onProgress)\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}
@@ -1,9 +1,9 @@
1
- import type { FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model';
1
+ import type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model';
2
2
  import { type FigureRevision } from './revision.js';
3
3
  import type { GizmoReach } from './gizmo.js';
4
4
  /**
5
5
  * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,
6
- * mirror on X or Z, snap to the grid.
6
+ * snap to the grid; and whether a mirrored copy is offered.
7
7
  *
8
8
  * 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
9
9
  * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it
@@ -23,12 +23,33 @@ export declare function boxOf(revision: FigureRevision, part: string): FigureWor
23
23
  export declare function gridStepOf(revision: FigureRevision): number;
24
24
  export declare function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[];
25
25
  /**
26
- * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
27
- * `duplicate-part` first; this is the position the copy is then given.
26
+ * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy
27
+ * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.
28
28
  */
29
- export declare function mirrorChanges(revision: FigureRevision, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[];
30
- /** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */
31
- export declare function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean;
29
+ export declare function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean;
30
+ /** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */
31
+ export interface MirrorForm {
32
+ axis: 'x' | 'z';
33
+ chain: boolean;
34
+ /** The beginning swapped in the copies' names (\`fl\` → \`fr\`); empty, the copies take \`-copy\`. */
35
+ from: string;
36
+ to: string;
37
+ keepMotion: boolean;
38
+ }
39
+ /**
40
+ * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them
41
+ * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\`fl-hip\` across Z
42
+ * is \`bl-\`, across X \`fr-\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing
43
+ * moving asks nothing.
44
+ */
45
+ export declare function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): {
46
+ hanging: string[];
47
+ moves: boolean;
48
+ asks: boolean;
49
+ form: MirrorForm;
50
+ };
51
+ /** The one edit the form comes to. */
52
+ export declare function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit;
32
53
  /**
33
54
  * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.
34
55
  * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.
@@ -1,4 +1,4 @@
1
- import { figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model';
1
+ import { figureHangingChainOf, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model';
2
2
  import { derivedOf } from './revision.js';
3
3
  const AXES = ['x', 'y', 'z'];
4
4
  /**
@@ -50,21 +50,46 @@ export function alignChanges(revision, part, reach, tool) {
50
50
  }
51
51
  }
52
52
  /**
53
- * Where a mirrored copy goes: the same distance on the other side of the centre plane. The copy is made by
54
- * `duplicate-part` first; this is the position the copy is then given.
53
+ * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy
54
+ * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.
55
55
  */
56
- export function mirrorChanges(revision, part, copy, axis) {
56
+ export function canMirror(revision, part, axis) {
57
57
  const box = boxOf(revision, part);
58
- if (!box)
59
- return [];
60
- const id = `${part}.position.${axis}`;
61
- return [{ kind: 'set-design', input: `${copy}.position.${axis}`, unit: 'mm', value: Math.round((revision.asset.designInputs[id] ?? 0) - 2 * box.centre[axis]) }];
62
- }
63
- /** Mirroring a part that already sits on the centre plane would put the copy inside it, so it is refused. */
64
- export function canMirror(revision, part, axis, reach) {
65
- const box = reach.translate ? boxOf(revision, part) : undefined;
66
58
  return !!box && Math.abs(box.centre[axis]) >= 0.5;
67
59
  }
60
+ /* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */
61
+ const SWAPS = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } };
62
+ /**
63
+ * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them
64
+ * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\`fl-hip\` across Z
65
+ * is \`bl-\`, across X \`fr-\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing
66
+ * moving asks nothing.
67
+ */
68
+ export function mirrorPlanOf(asset, part, axis) {
69
+ const chain = figureHangingChainOf(asset, part);
70
+ const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length);
71
+ const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? '';
72
+ const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('');
73
+ const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`));
74
+ const hanging = chain.slice(1);
75
+ return {
76
+ hanging,
77
+ moves,
78
+ asks: hanging.length > 0 || moves,
79
+ form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }
80
+ };
81
+ }
82
+ /** The one edit the form comes to. */
83
+ export function mirrorEditOf(part, form) {
84
+ return {
85
+ kind: 'mirror-part',
86
+ target: part,
87
+ axis: form.axis,
88
+ ...(form.chain ? { chain: true } : {}),
89
+ ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),
90
+ ...(form.keepMotion ? { keepMotion: true } : {})
91
+ };
92
+ }
68
93
  /**
69
94
  * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.
70
95
  * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.
@@ -1 +1 @@
1
- {"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureInputChange, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\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\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).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(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\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(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.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(revision)\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(revision: FigureRevision, part: string, copy: string, axis: 'x' | 'z'): FigureInputChange[] {\n const box = boxOf(revision, 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((revision.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(revision: FigureRevision, part: string, axis: 'x' | 'z', reach: GizmoReach): boolean {\n const box = reach.translate ? boxOf(revision, 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 { figureHangingChainOf, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\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 * snap to the grid; and whether a mirrored copy is offered.\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\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).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(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\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(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.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(revision)\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 * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy\n * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.\n */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean {\n const box = boxOf(revision, part)\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */\nexport interface MirrorForm {\n axis: 'x' | 'z'\n chain: boolean\n /** The beginning swapped in the copies' names (\\`fl\\` → \\`fr\\`); empty, the copies take \\`-copy\\`. */\n from: string\n to: string\n keepMotion: boolean\n}\n\n/* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */\nconst SWAPS: Record<'x' | 'z', Record<string, string>> = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } }\n\n/**\n * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them\n * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\\`fl-hip\\` across Z\n * is \\`bl-\\`, across X \\`fr-\\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing\n * moving asks nothing.\n */\nexport function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): { hanging: string[]; moves: boolean; asks: boolean; form: MirrorForm } {\n const chain = figureHangingChainOf(asset, part)\n const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length)\n const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? ''\n const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('')\n const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`))\n const hanging = chain.slice(1)\n return {\n hanging,\n moves,\n asks: hanging.length > 0 || moves,\n form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }\n }\n}\n\n/** The one edit the form comes to. */\nexport function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit {\n return {\n kind: 'mirror-part',\n target: part,\n axis: form.axis,\n ...(form.chain ? { chain: true } : {}),\n ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),\n ...(form.keepMotion ? { keepMotion: true } : {})\n }\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"]}
@@ -56,8 +56,32 @@ export declare const groupEdit: (group: ClipGroup, patch?: Partial<Omit<ClipGrou
56
56
  export declare function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[];
57
57
  /** A key added at a moment, replacing one already there; the keys stay in time order. */
58
58
  export declare function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[];
59
+ /**
60
+ * A key typed where it stands: its moment, its value, or both. The first key stays at 0 (a track starts there); a moment
61
+ * outside the clip, or on another key's, is refused (null) rather than taking that key's place unseen.
62
+ */
63
+ export declare function withKeyChanged(keys: readonly ClipKey[], index: number, patch: Partial<ClipKey>, duration: number): ClipKey[] | null;
59
64
  /** A track's keys without one; a track keeps at least its two ends, as the format asks. */
60
65
  export declare function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null;
66
+ /** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */
67
+ export declare function freeClipNameOf(asset: FigureAsset, clip: string): string;
68
+ /** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */
69
+ export declare const copyTargetsOf: (asset: FigureAsset, clip: string) => string[];
70
+ /** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */
71
+ export declare const shiftable: (track: ClipTrack) => boolean;
72
+ /**
73
+ * A track copied onto `target` -- itself, or another control -- played `seconds` later round its loop (robot dog
74
+ * 2026-11-03: a leg's swing half a period after another's). One edit; a track the clip plays on `target` is replaced.
75
+ */
76
+ export declare function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: ClipTrack, target: string, seconds: number): {
77
+ kind: "set-driver";
78
+ state: string;
79
+ clip: string;
80
+ keys: ClipKey[];
81
+ loop: "wrap" | "hold";
82
+ interpolation: "step" | "linear";
83
+ accumulates: boolean;
84
+ };
61
85
  /** The edit that writes a track back. */
62
86
  export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<Pick<ClipTrack, "keys" | "loop" | "interpolation">>) => {
63
87
  kind: "set-driver";
@@ -1,4 +1,4 @@
1
- import { driverValue } from '@hatiolab/figure-model';
1
+ import { driverValue, figureShiftedDriverKeys } from '@hatiolab/figure-model';
2
2
  const round = (n) => Math.round(n * 1000) / 1000;
3
3
  export function clipsOf(asset) {
4
4
  const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]));
@@ -68,12 +68,64 @@ export function poseAt(asset, clip, seconds) {
68
68
  export function withKey(keys, key) {
69
69
  return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds);
70
70
  }
71
+ /**
72
+ * A key typed where it stands: its moment, its value, or both. The first key stays at 0 (a track starts there); a moment
73
+ * outside the clip, or on another key's, is refused (null) rather than taking that key's place unseen.
74
+ */
75
+ export function withKeyChanged(keys, index, patch, duration) {
76
+ const was = keys[index];
77
+ if (!was)
78
+ return null;
79
+ const next = { seconds: patch.seconds ?? was.seconds, value: patch.value ?? was.value };
80
+ if (!Number.isFinite(next.seconds) || !Number.isFinite(next.value))
81
+ return null;
82
+ if (index === 0 && next.seconds !== 0)
83
+ return null;
84
+ if (next.seconds < 0 || next.seconds > duration)
85
+ return null;
86
+ if (keys.some((k, i) => i !== index && k.seconds === next.seconds))
87
+ return null;
88
+ return keys.map((k, i) => (i === index ? next : k)).sort((a, b) => a.seconds - b.seconds);
89
+ }
71
90
  /** A track's keys without one; a track keeps at least its two ends, as the format asks. */
72
91
  export function withoutKey(keys, index) {
73
92
  if (keys.length <= 2 || index === 0)
74
93
  return null;
75
94
  return keys.filter((_, i) => i !== index);
76
95
  }
96
+ /** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */
97
+ export function freeClipNameOf(asset, clip) {
98
+ const taken = new Set((asset.drivers ?? []).map(d => d.clip));
99
+ const base = `${clip}-copy`;
100
+ if (!taken.has(base))
101
+ return base;
102
+ let n = 2;
103
+ while (taken.has(`${base}-${n}`))
104
+ n++;
105
+ return `${base}-${n}`;
106
+ }
107
+ /** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */
108
+ export const copyTargetsOf = (asset, clip) => [
109
+ ...new Set([...(asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state), ...undrivenOf(asset, clip)])
110
+ ];
111
+ /** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */
112
+ export const shiftable = (track) => track.loop === 'wrap' && !track.accumulates;
113
+ /**
114
+ * A track copied onto `target` -- itself, or another control -- played `seconds` later round its loop (robot dog
115
+ * 2026-11-03: a leg's swing half a period after another's). One edit; a track the clip plays on `target` is replaced.
116
+ */
117
+ export function copiedTrackEdit(asset, clip, track, target, seconds) {
118
+ const driver = (asset.drivers ?? []).find(d => d.clip === clip.name && d.state === track.state);
119
+ return {
120
+ kind: 'set-driver',
121
+ state: target,
122
+ clip: clip.name,
123
+ keys: seconds ? figureShiftedDriverKeys(driver, seconds) : track.keys,
124
+ loop: track.loop,
125
+ interpolation: track.interpolation,
126
+ accumulates: track.accumulates
127
+ };
128
+ }
77
129
  /** The edit that writes a track back. */
78
130
  export const trackEdit = (clip, track, patch = {}) => ({
79
131
  kind: 'set-driver',
@@ -1 +1 @@
1
- {"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"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","sourcesContent":["import { driverValue } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/**\n * The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays\n * outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip\n * plays. Without a clip, the controls no clip plays at all.\n */\nexport function undrivenOf(asset: FigureAsset, clip = '', group?: string): string[] {\n const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id] as const)))\n const mine = group ?? (clip ? groupOf.get(clip) : undefined)\n const taken = (state: string) =>\n (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id)\n}\n\n/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */\nexport interface ClipGroup {\n id: string\n label: string\n clips: string[]\n defaultClip: string\n transitionSeconds: number\n}\n\n/** The figure's motion groups, and the clips in none -- the ones a group can take. */\nexport function clipGroupsOf(asset: FigureAsset): { groups: ClipGroup[]; free: string[] } {\n const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }))\n const grouped = new Set(groups.flatMap(g => g.clips))\n const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c))\n return { groups, free }\n}\n\n/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */\nexport function freeGroupIdOf(asset: FigureAsset): string {\n const taken = new Set((asset.motionGroups ?? []).map(g => g.id))\n if (!taken.has('motion')) return 'motion'\n let n = 2\n while (taken.has(`motion-${n}`)) n++\n return `motion-${n}`\n}\n\n/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */\nexport const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>> = {}) => ({\n kind: 'set-motion-group' as const,\n group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }\n})\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n"]}
1
+ {"version":3,"file":"clips.js","sourceRoot":"","sources":["../../client/modeller/clips.ts"],"names":[],"mappings":"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","sourcesContent":["import { driverValue, figureShiftedDriverKeys } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureDriver, FigureInputChange } from '@hatiolab/figure-model'\n\n/*\n * The figure's clips: controls played by keys over time, grouped into clips that each run on one\n * clock. Kept apart from the element so the reading is tested without a browser.\n */\nexport interface ClipKey {\n seconds: number\n value: number\n}\nexport interface ClipTrack {\n state: string\n label: string\n unit: string\n min: number\n max: number\n keys: ClipKey[]\n interpolation: 'linear' | 'step'\n loop: 'wrap' | 'hold'\n accumulates: boolean\n}\nexport interface Clip {\n name: string\n duration: number\n tracks: ClipTrack[]\n}\n\nconst round = (n: number) => Math.round(n * 1000) / 1000\n\nexport function clipsOf(asset: FigureAsset): Clip[] {\n const inputs = new Map(asset.document.model.inputs.map(i => [i.id, i]))\n const clips = new Map<string, Clip>()\n for (const d of asset.drivers ?? []) {\n const input = inputs.get(d.state)\n const clip = clips.get(d.clip) ?? { name: d.clip, duration: d.time.duration, tracks: [] }\n clip.tracks.push({\n state: d.state,\n label: asset.stateInputs?.[d.state]?.label ?? d.state,\n unit: input?.unit ?? '',\n min: input?.min ?? 0,\n max: input?.max ?? 0,\n keys: d.keys.map(k => ({ seconds: round(k.at * d.time.duration), value: k.value })),\n interpolation: d.interpolation,\n loop: d.loop,\n accumulates: d.accumulates\n })\n clips.set(d.clip, clip)\n }\n return [...clips.values()]\n}\n\n/**\n * The controls a new track of `clip` can take (ADR-0051 §3.2): one the clip does not play yet, and that no clip plays\n * outside the clip's motion group -- `group` for a clip not made yet. A clip in no group takes only a control no clip\n * plays. Without a clip, the controls no clip plays at all.\n */\nexport function undrivenOf(asset: FigureAsset, clip = '', group?: string): string[] {\n const groupOf = new Map((asset.motionGroups ?? []).flatMap(g => g.clips.map(c => [c, g.id] as const)))\n const mine = group ?? (clip ? groupOf.get(clip) : undefined)\n const taken = (state: string) =>\n (asset.drivers ?? []).some(d => d.state === state && (d.clip === clip || mine === undefined || groupOf.get(d.clip) !== mine))\n return asset.document.model.inputs.filter(i => i.role === 'state' && !taken(i.id)).map(i => i.id)\n}\n\n/** A motion group as the panel shows it: its setting, and the clips it holds in the clips' order. */\nexport interface ClipGroup {\n id: string\n label: string\n clips: string[]\n defaultClip: string\n transitionSeconds: number\n}\n\n/** The figure's motion groups, and the clips in none -- the ones a group can take. */\nexport function clipGroupsOf(asset: FigureAsset): { groups: ClipGroup[]; free: string[] } {\n const groups = (asset.motionGroups ?? []).map(g => ({ ...g, clips: [...g.clips] }))\n const grouped = new Set(groups.flatMap(g => g.clips))\n const free = [...new Set((asset.drivers ?? []).map(d => d.clip))].filter(c => !grouped.has(c))\n return { groups, free }\n}\n\n/** A group id not yet taken: `motion`, then `motion-2`, `motion-3`, … An id is not made from a label a person types. */\nexport function freeGroupIdOf(asset: FigureAsset): string {\n const taken = new Set((asset.motionGroups ?? []).map(g => g.id))\n if (!taken.has('motion')) return 'motion'\n let n = 2\n while (taken.has(`motion-${n}`)) n++\n return `motion-${n}`\n}\n\n/** The edit that writes a group back with a change: one whole group, as the kernel takes it. */\nexport const groupEdit = (group: ClipGroup, patch: Partial<Omit<ClipGroup, 'id'>> = {}) => ({\n kind: 'set-motion-group' as const,\n group: { id: group.id, label: patch.label ?? group.label, clips: patch.clips ?? group.clips, defaultClip: patch.defaultClip ?? group.defaultClip, transitionSeconds: patch.transitionSeconds ?? group.transitionSeconds }\n})\n\n/** The pose a clip gives at a moment, as one batch of state changes -- one step of the history. */\nexport function poseAt(asset: FigureAsset, clip: string, seconds: number): FigureInputChange[] {\n const units = new Map(asset.document.model.inputs.map(i => [i.id, i.unit]))\n return (asset.drivers ?? [])\n .filter((d: FigureDriver) => d.clip === clip)\n .map(d => {\n const unit = units.get(d.state)!\n return { kind: 'set-state' as const, input: d.state, unit, value: driverValue(d, seconds, unit === 'deg' || unit === 'rad' ? unit : 'other') }\n })\n}\n\n/** A key added at a moment, replacing one already there; the keys stay in time order. */\nexport function withKey(keys: readonly ClipKey[], key: ClipKey): ClipKey[] {\n return [...keys.filter(k => k.seconds !== key.seconds), key].sort((a, b) => a.seconds - b.seconds)\n}\n\n/**\n * A key typed where it stands: its moment, its value, or both. The first key stays at 0 (a track starts there); a moment\n * outside the clip, or on another key's, is refused (null) rather than taking that key's place unseen.\n */\nexport function withKeyChanged(keys: readonly ClipKey[], index: number, patch: Partial<ClipKey>, duration: number): ClipKey[] | null {\n const was = keys[index]\n if (!was) return null\n const next = { seconds: patch.seconds ?? was.seconds, value: patch.value ?? was.value }\n if (!Number.isFinite(next.seconds) || !Number.isFinite(next.value)) return null\n if (index === 0 && next.seconds !== 0) return null\n if (next.seconds < 0 || next.seconds > duration) return null\n if (keys.some((k, i) => i !== index && k.seconds === next.seconds)) return null\n return keys.map((k, i) => (i === index ? next : k)).sort((a, b) => a.seconds - b.seconds)\n}\n\n/** A track's keys without one; a track keeps at least its two ends, as the format asks. */\nexport function withoutKey(keys: readonly ClipKey[], index: number): ClipKey[] | null {\n if (keys.length <= 2 || index === 0) return null\n return keys.filter((_, i) => i !== index)\n}\n\n/** A name no clip has, for a copy: `walk-copy`, then `walk-copy-2`, … */\nexport function freeClipNameOf(asset: FigureAsset, clip: string): string {\n const taken = new Set((asset.drivers ?? []).map(d => d.clip))\n const base = `${clip}-copy`\n if (!taken.has(base)) return base\n let n = 2\n while (taken.has(`${base}-${n}`)) n++\n return `${base}-${n}`\n}\n\n/** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */\nexport const copyTargetsOf = (asset: FigureAsset, clip: string): string[] => [\n ...new Set([...(asset.drivers ?? []).filter(d => d.clip === clip).map(d => d.state), ...undrivenOf(asset, clip)])\n]\n\n/** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */\nexport const shiftable = (track: ClipTrack) => track.loop === 'wrap' && !track.accumulates\n\n/**\n * A track copied onto `target` -- itself, or another control -- played `seconds` later round its loop (robot dog\n * 2026-11-03: a leg's swing half a period after another's). One edit; a track the clip plays on `target` is replaced.\n */\nexport function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: ClipTrack, target: string, seconds: number) {\n const driver = (asset.drivers ?? []).find(d => d.clip === clip.name && d.state === track.state)!\n return {\n kind: 'set-driver' as const,\n state: target,\n clip: clip.name,\n keys: seconds ? figureShiftedDriverKeys(driver, seconds) : track.keys,\n loop: track.loop,\n interpolation: track.interpolation,\n accumulates: track.accumulates\n }\n}\n\n/** The edit that writes a track back. */\nexport const trackEdit = (clip: Clip, track: ClipTrack, patch: Partial<Pick<ClipTrack, 'keys' | 'loop' | 'interpolation'>> = {}) => ({\n kind: 'set-driver' as const,\n state: track.state,\n clip: clip.name,\n keys: patch.keys ?? track.keys,\n loop: patch.loop ?? track.loop,\n interpolation: patch.interpolation ?? track.interpolation\n})\n"]}