@things-factory/figure-ui 10.1.270 โ†’ 10.1.271

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 (53) hide show
  1. package/client/graphql/index.ts +2 -0
  2. package/client/modeller/data-interface.ts +14 -0
  3. package/client/modeller/editor-styles.ts +11 -0
  4. package/client/modeller/editor.ts +39 -11
  5. package/client/modeller/fasten-form.ts +6 -1
  6. package/client/modeller/fasten-geometry.ts +11 -1
  7. package/client/modeller/figure-fasten-drawing.ts +2 -3
  8. package/client/modeller/figure-preview.ts +9 -1
  9. package/client/modeller/palette.ts +27 -0
  10. package/client/modeller/product-session.ts +5 -1
  11. package/client/types.ts +2 -0
  12. package/dist-client/graphql/index.js +2 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/clips.d.ts +4 -4
  15. package/dist-client/modeller/data-interface.d.ts +5 -0
  16. package/dist-client/modeller/data-interface.js +15 -0
  17. package/dist-client/modeller/data-interface.js.map +1 -1
  18. package/dist-client/modeller/editor-styles.js +11 -0
  19. package/dist-client/modeller/editor-styles.js.map +1 -1
  20. package/dist-client/modeller/editor.d.ts +6 -0
  21. package/dist-client/modeller/editor.js +35 -11
  22. package/dist-client/modeller/editor.js.map +1 -1
  23. package/dist-client/modeller/fasten-form.js +6 -2
  24. package/dist-client/modeller/fasten-form.js.map +1 -1
  25. package/dist-client/modeller/fasten-geometry.d.ts +6 -0
  26. package/dist-client/modeller/fasten-geometry.js +10 -1
  27. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  28. package/dist-client/modeller/figure-fasten-drawing.js +2 -3
  29. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  30. package/dist-client/modeller/figure-preview.js +9 -1
  31. package/dist-client/modeller/figure-preview.js.map +1 -1
  32. package/dist-client/modeller/links.d.ts +1 -1
  33. package/dist-client/modeller/palette.d.ts +14 -0
  34. package/dist-client/modeller/palette.js +23 -0
  35. package/dist-client/modeller/palette.js.map +1 -1
  36. package/dist-client/modeller/product-session.js +6 -2
  37. package/dist-client/modeller/product-session.js.map +1 -1
  38. package/dist-client/route.d.ts +1 -1
  39. package/dist-client/tsconfig.tsbuildinfo +1 -1
  40. package/dist-client/types.d.ts +2 -0
  41. package/dist-client/types.js.map +1 -1
  42. package/package.json +5 -5
  43. package/test/data-motion.test.ts +8 -0
  44. package/test/fasten-form.test.ts +13 -0
  45. package/test/fasten-geometry.test.ts +25 -1
  46. package/test/first-part-on-plane.test.ts +52 -0
  47. package/test/save-landed.test.ts +20 -1
  48. package/translations/en.json +1 -0
  49. package/translations/es.json +1 -0
  50. package/translations/ja.json +1 -0
  51. package/translations/ko.json +1 -0
  52. package/translations/ms.json +1 -0
  53. package/translations/zh.json +1 -0
@@ -473,6 +473,7 @@ export async function inspectFigure(id: string): Promise<FigureInspection> {
473
473
  how
474
474
  at
475
475
  blocking
476
+ asNewType
476
477
  }
477
478
  }
478
479
  }
@@ -504,6 +505,7 @@ export async function inspectFigureDraft(source: string, id?: string): Promise<F
504
505
  how
505
506
  at
506
507
  blocking
508
+ asNewType
507
509
  }
508
510
  }
509
511
  }
@@ -387,6 +387,20 @@ export function sampleOf(
387
387
  }
388
388
  }
389
389
 
390
+ /**
391
+ * The state inputs a sample of data sets on this figure, by id: what the preview shows for them instead of a slider. None
392
+ * when there is no sample, or the figure refuses it.
393
+ */
394
+ export function sampledStatesOf(asset: FigureAsset, sample: unknown): Record<string, number> {
395
+ if (sample === null || sample === undefined || !asset.dataInterface) return {}
396
+ try {
397
+ const read = sampleOf(asset, sample)
398
+ return read.issues.length ? {} : Object.fromEntries(Object.entries(read.result.states).map(([id, s]) => [id, s.value]))
399
+ } catch {
400
+ return {}
401
+ }
402
+ }
403
+
390
404
  /** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
391
405
  export const valueRowsOf = (asset: FigureAsset): FigureDataRow[] => schemaRowsOf(asset).filter(r => isValue(r.node) && r.bindings.length > 0 && !r.path.includes('[]'))
392
406
 
@@ -94,6 +94,17 @@ export const EditorStyles = css`
94
94
  a promise to whoever draws the figure, is outlined.
95
95
  */
96
96
  ox-canvas-bar button[save],
97
+ div[release-blockers] button[as-new-type] {
98
+ justify-self: start;
99
+ margin-top: 4px;
100
+ padding: 4px 10px;
101
+ border: 1px solid var(--md-sys-color-outline);
102
+ border-radius: 999px;
103
+ color: var(--md-sys-color-primary);
104
+ background: var(--md-sys-color-surface);
105
+ font: inherit;
106
+ cursor: pointer;
107
+ }
97
108
  ox-canvas-bar button[publish] {
98
109
  display: inline-flex;
99
110
  align-items: center;
@@ -50,7 +50,7 @@ import { EditorStyles } from './editor-styles.js'
50
50
  import { ScrollbarStyles } from '@operato/styles'
51
51
  import { alignChanges, canMirror, mirrorEditOf, mirroredClipsOf, mirrorPlanOf, type AlignTool, type MirrorForm } from './align.js'
52
52
  import type { FigurePaletteKind, FigurePartRow } from './figure-parts.js'
53
- import { paletteRecipe } from './palette.js'
53
+ import { firstPartFastensToPlane, paletteRecipe } from './palette.js'
54
54
  import { meshChoicesOf, meshPartOf } from './mesh-import.js'
55
55
  import './figure-mesh-choice.js'
56
56
  import { uploadFigureMesh } from '../graphql/index.js'
@@ -544,13 +544,8 @@ export class FigureEditor extends LitElement {
544
544
  }).result as SaveConflictChoice | undefined
545
545
  if (this.session !== session || !session.read().conflict) return
546
546
  if (choice === 'latest') window.open(location.href, '_blank', 'noopener')
547
- else if (choice === 'copy') {
548
- session.detachAsCopy()
549
- /* The page forgets the row of the figure left: the copy's identifier is asked, not the other's offered. */
550
- this.dispatchEvent(new CustomEvent('figure-detached', { bubbles: true, composed: true }))
551
- this.changed()
552
- await this.save()
553
- } else if (choice === 'overwrite') {
547
+ else if (choice === 'copy') await this.saveAsNewFigure()
548
+ else if (choice === 'overwrite') {
554
549
  const agreed = await OxPrompt.open({
555
550
  type: 'warning',
556
551
  text: i18next.t('figure.text.overwrite-latest-warning'),
@@ -562,12 +557,35 @@ export class FigureEditor extends LitElement {
562
557
  await this.save()
563
558
  }
564
559
  }
560
+ /**
561
+ * The edits kept apart as a new figure, the one opened left as the server has it: a copy after a save conflict, or a
562
+ * new figure type for edits a released figure cannot take (the release gate's way out, tutorial 2026-11-11). The first
563
+ * save asks its identifier.
564
+ */
565
+ private async saveAsNewFigure() {
566
+ this.session!.detachAsCopy()
567
+ this.blockersOpen = false
568
+ /* The page forgets the row of the figure left: the copy's identifier is asked, not the other's offered. */
569
+ this.dispatchEvent(new CustomEvent('figure-detached', { bubbles: true, composed: true }))
570
+ this.changed()
571
+ await this.save()
572
+ }
565
573
  private add(kind: FigurePaletteKind) {
566
574
  if (!this.session) return
567
575
  this.run(() => {
568
576
  /* The id is figure-model's to choose: what is in use is its rule, not a list the screen keeps. */
569
- const part = paletteRecipe(kind, figurePartIdFor(this.session!.revisionNow()!.asset, kind))
570
- this.session!.add(part)
577
+ const asset = this.session!.revisionNow()!.asset
578
+ const part = paletteRecipe(kind, figurePartIdFor(asset, kind))
579
+ /*
580
+ * The first part of a figure that stands on a plane (floor, ceiling, wall) is its base: it is added fastened to
581
+ * that plane, one step of the history, so a size typed next keeps it on the plane instead of sinking through it.
582
+ */
583
+ if (firstPartFastensToPlane(this.session!.read().geometry ?? [], asset?.placement)) {
584
+ this.session!.applyRecipe([
585
+ { kind: 'add-part', part },
586
+ { kind: 'attach', part: part.id, to: { plane: 'mounting-plane' } }
587
+ ])
588
+ } else this.session!.add(part)
571
589
  this.selected = part.id
572
590
  })
573
591
  }
@@ -1615,7 +1633,17 @@ export class FigureEditor extends LitElement {
1615
1633
  return html`<div release-blockers role="dialog" aria-label=${i18next.t('figure.text.must-fix-to-release')} @keydown=${(e: KeyboardEvent) => e.key === 'Escape' && (this.blockersOpen = false)}>
1616
1634
  <ul gate>
1617
1635
  <li head>${i18next.t('figure.text.must-fix-to-release')}</li>
1618
- ${blockers.map(finding => renderGateFinding(finding, true, nothing, parts))}
1636
+ ${blockers.map(finding =>
1637
+ renderGateFinding(
1638
+ finding,
1639
+ true,
1640
+ /* A break of the released contract: the edits go out as a new figure type, the placed ones keep the old one. */
1641
+ finding.asNewType
1642
+ ? html`<button as-new-type @click=${() => this.saveAsNewFigure()}>${i18next.t('figure.button.save-as-new-type')}</button>`
1643
+ : nothing,
1644
+ parts
1645
+ )
1646
+ )}
1619
1647
  </ul>
1620
1648
  </div>`
1621
1649
  }
@@ -19,6 +19,7 @@ import {
19
19
  type Axis
20
20
  } from './links.js'
21
21
  import type { FigureRevision } from './revision.js'
22
+ import { standsWhereAdded } from './palette.js'
22
23
  import { ownerOf } from './fields.js'
23
24
 
24
25
  /*
@@ -67,7 +68,11 @@ export const freshFasten = (): FastenForm => ({
67
68
  /** The part's own fastening as a form, or a fresh one: a window opens on what the part has (walk 1, 2026-09-26). */
68
69
  export function fastenFormOf(revision: FigureRevision, part: string): FastenForm {
69
70
  const own = fasteningOf(revision, part)
70
- if (!own || own.atOrigin) return freshFasten()
71
+ if (!own || own.atOrigin) {
72
+ /* A part still where it was added was put nowhere by the author: fastened, it goes on the face, no gap to keep. */
73
+ const pose = (revision.geometry.find(g => (g as { id: string }).id === part) as { pose?: { r: number[][]; t: number[] } } | undefined)?.pose
74
+ return !own && standsWhereAdded(pose) ? { ...freshFasten(), gapFrom: 'pressed' } : freshFasten()
75
+ }
71
76
  /* A gap the record works out is the author's; a number is where the part stands, so aiming again keeps it there. */
72
77
  return { ...freshFasten(), ...own, gapFrom: own.gapKept !== undefined ? 'typed' : 'kept' } as FastenForm
73
78
  }
@@ -100,7 +100,8 @@ export function alignmentAt(d: number, face: number | undefined, part: number, e
100
100
  [-inside, 'min'],
101
101
  [inside, 'max']
102
102
  ] as const)
103
- if (Math.abs(d - at) <= catchMm && inside > 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }
103
+ /* Level with an edge whatever the sizes: a part bigger than the face (a mast on a low body) lines up with it too. */
104
+ if (Math.abs(d - at) <= catchMm && inside !== 0) return edges ? { how, caught: 'inside' } : { how: 'mm', mm: Math.round(at), caught: 'inside' }
104
105
  for (const at of [-outside, outside]) if (Math.abs(d - at) <= catchMm) return { how: 'mm', mm: Math.round(at), caught: 'outside' }
105
106
  }
106
107
  return { how: 'mm', mm: Math.round(d) }
@@ -183,3 +184,12 @@ export function fastenLabelsOf(
183
184
  if (below - 11 < edge + 2 && below + 3 > edge - 2 && edge + 14 <= height - 4) return { face: facePlace, part: { x: part.x + part.w / 2, y: edge + 14, inside: false } }
184
185
  return { face: facePlace, part: { x: part.x + part.w / 2, y: below <= height - 20 ? below : part.y - 5, inside: false } }
185
186
  }
187
+
188
+ /**
189
+ * How much of an axis the face drawing shows, mm: room around the face for the part to go past its edges by about half
190
+ * itself, and for a part bigger than the face lined up with one of its edges (a mast flush with a low body's bottom) to
191
+ * show whole; a plane with no size, three parts wide.
192
+ */
193
+ export function drawingSpanOf(face: number | undefined, part: number): number {
194
+ return face !== undefined ? Math.max(face + part, 2 * part - face) : part * 3
195
+ }
@@ -6,7 +6,7 @@ import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-
6
6
  import { figureAroundNormalOf, figureAroundOf, figureCaughtAngleOf, figureSurfaceAnglesOf, figureSurfacePlaceOf } from '@hatiolab/figure-model'
7
7
  import type { FigureFace, FigureSurfacePlace } from '@hatiolab/figure-model'
8
8
 
9
- import { alignmentAt, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
9
+ import { alignmentAt, drawingSpanOf, fastenLabelsOf, offsetOf, overhangOf, screenOf, type FaceView, type FastenMode } from './fasten-geometry.js'
10
10
  import type { Alignment } from './links.js'
11
11
 
12
12
  type Axis = 'x' | 'y' | 'z'
@@ -332,8 +332,7 @@ export class FigureFastenDrawing extends localize(i18next)(LitElement) {
332
332
  const { h, v, sign } = screenOf(view.axes)
333
333
  const W = WHERE_W,
334
334
  H = WHERE_H
335
- /* Room around the face for the part to go past its edges by about half itself; a plane with no size, three parts wide. */
336
- const span = (a: Axis) => (view.face[a] !== undefined ? view.face[a]! + view.part[a] : view.part[a] * 3)
335
+ const span = (a: Axis) => drawingSpanOf(view.face[a], view.part[a])
337
336
  const k = Math.min((W - 40) / span(h), (H - 40) / span(v))
338
337
  this.scale = k
339
338
  const cx = W / 2,
@@ -12,7 +12,7 @@ import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSetting
12
12
  import { parametersForClip } from '@things-factory/figure-viewer/dist-client/detail-motion.js'
13
13
  import { controlsOf } from '@things-factory/figure-viewer/dist-client/controls.js'
14
14
  import { dialShape } from './angle-dial.js'
15
- import { dataValueAt, domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
15
+ import { dataValueAt, domainOf, motionWrittenByData, sampledStatesOf, typedValueOf, valueRowsOf } from './data-interface.js'
16
16
  import { derivedOf, type FigureRevision } from './revision.js'
17
17
  import { authoredSizeOf, bodyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js'
18
18
  import { previewSizeWordsOf } from './preview-size.js'
@@ -550,6 +550,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
550
550
 
551
551
  private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[]) {
552
552
  const steps = new Map(controlsOf(asset).map(c => [c.id, c.step]))
553
+ const sampledStates = sampledStatesOf(asset, this.sample)
553
554
  const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
554
555
  const fold = html`<button fold-controls aria-expanded=${String(!this.controlsFolded)} @click=${() => (this.controlsFolded = !this.controlsFolded)}>
555
556
  ${i18next.t(this.controlsFolded ? 'figure.button.controls-unfold' : 'figure.button.controls-fold')}
@@ -568,6 +569,13 @@ export class FigurePreview extends localize(i18next)(LitElement) {
568
569
  </button>`
569
570
  : nothing}
570
571
  ${controls.map(control => {
572
+ /* A control the data tab's sample sets is what the figure shows, as a value row the sample gives: said, not offered. */
573
+ const fromSample = sampledStates[control.id]
574
+ if (fromSample !== undefined)
575
+ return html`<label sampled title=${i18next.t('figure.text.data-value-from-sample')}>
576
+ <span>${asset.stateInputs?.[control.id]?.label ?? control.id} <span drive>${i18next.t('figure.label.data-value-from-sample')}</span></span>
577
+ <output>${Math.round(fromSample * 100) / 100} ${unitWord(control.unit)}</output>
578
+ </label>`
571
579
  const value = this.giving[control.id] ?? asset.stateDefaults[control.id] ?? control.min
572
580
  /* The step the editor's controls take (\`controlsOf\`): a degree for an angle, a hundredth of the range otherwise. */
573
581
  const step = steps.get(control.id) ?? (control.max - control.min) / 100
@@ -69,3 +69,30 @@ export function paletteRecipe(kind: FigurePaletteKind, id: string): FigurePrimit
69
69
  }
70
70
  }
71
71
  }
72
+
73
+ /**
74
+ * Whether a part added now is the figure's base, added fastened to its mounting plane: the figure has no part yet and
75
+ * stands on a plane -- the floor (also when not said), the ceiling or a wall. A centred figure has no plane to stand on.
76
+ */
77
+ export function firstPartFastensToPlane(drawn: readonly unknown[], placement: string | undefined): boolean {
78
+ return drawn.length === 0 && placement !== 'center'
79
+ }
80
+
81
+ const PALETTE_KINDS: FigurePaletteKind[] = ['box', 'cylinder', 'rounded-box', 'sphere', 'frustum', 'polygon', 'pyramid', 'pipe', 'plate', 'wall', 'hollow-box', 'portal']
82
+
83
+ /**
84
+ * Whether a drawn part still stands where the palette put it -- unturned, at one of the places a part is added at. Such a
85
+ * part was not placed by the author: fastening it has no place of theirs to keep (tutorial 2026-11-11, a mast added
86
+ * inside the body was fastened 550 mm deep in it).
87
+ */
88
+ export function standsWhereAdded(pose: { r: number[][]; t: number[] } | undefined): boolean {
89
+ if (!pose) return false
90
+ const unturned = pose.r.every((row, i) => row.every((v, j) => Math.abs(v - (i === j ? 1 : 0)) < 1e-9))
91
+ return (
92
+ unturned &&
93
+ PALETTE_KINDS.some(kind => {
94
+ const { x, y, z } = paletteRecipe(kind, 'p').position
95
+ return [x, y, z].every((v, i) => Math.abs(v - pose.t[i]!) < 1e-6)
96
+ })
97
+ )
98
+ }
@@ -265,7 +265,11 @@ export function createFigureProductSession(store: FigureProductStore) {
265
265
  */
266
266
  detachAsCopy() {
267
267
  if (saving) throw refusal('SESSION_SAVING', '์ €์žฅ์ด ๋๋‚œ ๋’ค ํŽธ์ง‘ํ•ด ์ฃผ์„ธ์š”.')
268
- if (!conflict) throw refusal('SESSION_NO_CONFLICT', '์ถฉ๋Œํ•œ ์ €์žฅ์ด ์—†์Šต๋‹ˆ๋‹ค.')
268
+ /*
269
+ After a conflict, or to release edits that break the released contract as a new figure type (the release gate's
270
+ way out, tutorial 2026-11-11): either way the figure it came from stays as the server has it.
271
+ */
272
+ if (!conflict && !id) throw refusal('SESSION_NOT_SAVED', '์ €์žฅ๋œ ํ”ผ๊ทœ์–ด๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค.')
269
273
  id = undefined
270
274
  baseline = undefined
271
275
  creationId = crypto.randomUUID()
package/client/types.ts CHANGED
@@ -122,6 +122,8 @@ export interface FigureGateFinding {
122
122
  /** ๊ฐˆ๋ผ์ง„ ๋ถ€ํ’ˆ ์ด๋ฆ„๋“ค, ๋˜๋Š” ์ •๋ณธ ์•ˆ์˜ ๊ฒฝ๋กœ. */
123
123
  at?: string
124
124
  blocking: boolean
125
+ /** ๋ฐœํ–‰ ๊ณ„์•ฝ์ด ๊นจ์ง โ€” ์ƒˆ ํ”ผ๊ทœ์–ด ํƒ€์ž…์œผ๋กœ ์ €์žฅํ•˜๋ฉด ๋ฐœํ–‰๋˜๊ณ , ๋†“์ธ ํ”ผ๊ทœ์–ด๋Š” ์˜› ์ •์˜๋ฅผ ์œ ์ง€ํ•œ๋‹ค. */
126
+ asNewType?: boolean
125
127
  }
126
128
 
127
129
  /** ๋ฐœํ–‰ํ•ด๋„ ๋˜๋‚˜. `blocked` ๋ฉด ์„œ๋ฒ„๊ฐ€ ๊ฑฐ์ ˆํ•œ๋‹ค. */
@@ -368,6 +368,7 @@ export async function inspectFigure(id) {
368
368
  how
369
369
  at
370
370
  blocking
371
+ asNewType
371
372
  }
372
373
  }
373
374
  }
@@ -397,6 +398,7 @@ export async function inspectFigureDraft(source, id) {
397
398
  how
398
399
  at
399
400
  blocking
401
+ asNewType
400
402
  }
401
403
  }
402
404
  }
@@ -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 /* The server's code and details go with its words: a conflict is told apart from a refusal (FIGURE_CATALOG_CONFLICT). */\n const extensions = (response.errors?.[0] as { extensions?: { code?: string } } | undefined)?.extensions\n if (said) throw Object.assign(new Error(said), extensions?.code ? { code: extensions.code, extensions } : {})\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[], signal?: AbortSignal): Promise<Record<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 context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw new Error(response.errors.map(one => one.message).join(' ยท '))\n return Object.fromEntries((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\n/** The type, category and tags as the editor saw them; null is \"was empty\". The update goes on only while the row holds them. */\nexport interface FigureCatalogSeen {\n type?: string | null\n category?: string | null\n tags?: string[] | null\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch, seen?: FigureCatalogSeen): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!, $seen: FigureCatalogSeen) {\n updateFigure(id: $id, patch: $patch, seen: $seen) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch, ...(seen ? { seen } : {}) }\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 /* The server's code and details go with its words: a conflict is told apart from a refusal (FIGURE_CATALOG_CONFLICT). */\n const extensions = (response.errors?.[0] as { extensions?: { code?: string } } | undefined)?.extensions\n if (said) throw Object.assign(new Error(said), extensions?.code ? { code: extensions.code, extensions } : {})\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[], signal?: AbortSignal): Promise<Record<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 context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw new Error(response.errors.map(one => one.message).join(' ยท '))\n return Object.fromEntries((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\n/** The type, category and tags as the editor saw them; null is \"was empty\". The update goes on only while the row holds them. */\nexport interface FigureCatalogSeen {\n type?: string | null\n category?: string | null\n tags?: string[] | null\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch, seen?: FigureCatalogSeen): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!, $seen: FigureCatalogSeen) {\n updateFigure(id: $id, patch: $patch, seen: $seen) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch, ...(seen ? { seen } : {}) }\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 asNewType\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 asNewType\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"]}
@@ -83,8 +83,8 @@ export declare function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: C
83
83
  state: string;
84
84
  clip: string;
85
85
  keys: ClipKey[];
86
- loop: "wrap" | "hold";
87
- interpolation: "step" | "linear" | "smooth";
86
+ loop: "hold" | "wrap";
87
+ interpolation: "smooth" | "linear" | "step";
88
88
  accumulates: boolean;
89
89
  };
90
90
  /** The edit that writes a track back. */
@@ -93,8 +93,8 @@ export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<P
93
93
  state: string;
94
94
  clip: string;
95
95
  keys: ClipKey[];
96
- loop: "wrap" | "hold";
97
- interpolation: "step" | "linear" | "smooth";
96
+ loop: "hold" | "wrap";
97
+ interpolation: "smooth" | "linear" | "step";
98
98
  };
99
99
  /**
100
100
  * A new track's keys. By default it holds the pose the figure stands in now, from start to end: a track added to a pose
@@ -122,6 +122,11 @@ export declare function sampleOf(asset: FigureAsset, sample: unknown, legends?:
122
122
  uses: Readonly<Record<string, FigureTableUse>>;
123
123
  notes: FigureRuntimeIssue[];
124
124
  };
125
+ /**
126
+ * The state inputs a sample of data sets on this figure, by id: what the preview shows for them instead of a slider. None
127
+ * when there is no sample, or the figure refuses it.
128
+ */
129
+ export declare function sampledStatesOf(asset: FigureAsset, sample: unknown): Record<string, number>;
125
130
  /** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
126
131
  export declare const valueRowsOf: (asset: FigureAsset) => FigureDataRow[];
127
132
  /**
@@ -340,6 +340,21 @@ export function sampleOf(asset, sample, legends = {}) {
340
340
  notes: inputs.diagnostics().filter(i => i.severity === 'note')
341
341
  };
342
342
  }
343
+ /**
344
+ * The state inputs a sample of data sets on this figure, by id: what the preview shows for them instead of a slider. None
345
+ * when there is no sample, or the figure refuses it.
346
+ */
347
+ export function sampledStatesOf(asset, sample) {
348
+ if (sample === null || sample === undefined || !asset.dataInterface)
349
+ return {};
350
+ try {
351
+ const read = sampleOf(asset, sample);
352
+ return read.issues.length ? {} : Object.fromEntries(Object.entries(read.result.states).map(([id, s]) => [id, s.value]));
353
+ }
354
+ catch {
355
+ return {};
356
+ }
357
+ }
343
358
  /** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
344
359
  export const valueRowsOf = (asset) => schemaRowsOf(asset).filter(r => isValue(r.node) && r.bindings.length > 0 && !r.path.includes('[]'));
345
360
  /**