@things-factory/figure-ui 10.1.40 → 10.1.42

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 +1 -0
  2. package/client/modeller/figure-v3-material.ts +248 -0
  3. package/client/modeller/figure-v3-parts.ts +16 -1
  4. package/client/modeller/modeller-route.ts +25 -0
  5. package/client/modeller/v3-align.ts +84 -0
  6. package/client/modeller/v3-canvas.ts +140 -2
  7. package/client/modeller/v3-editor.ts +170 -38
  8. package/client/modeller/v3-gizmo.ts +80 -0
  9. package/client/pages/figure-list-page.ts +5 -3
  10. package/client/pages/figure-modeller-page.ts +9 -0
  11. package/client/types.ts +2 -0
  12. package/dist-client/graphql/index.js +1 -0
  13. package/dist-client/graphql/index.js.map +1 -1
  14. package/dist-client/modeller/figure-v3-material.d.ts +36 -0
  15. package/dist-client/modeller/figure-v3-material.js +249 -0
  16. package/dist-client/modeller/figure-v3-material.js.map +1 -0
  17. package/dist-client/modeller/figure-v3-parts.js +16 -1
  18. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  19. package/dist-client/modeller/modeller-route.d.ts +16 -0
  20. package/dist-client/modeller/modeller-route.js +27 -0
  21. package/dist-client/modeller/modeller-route.js.map +1 -0
  22. package/dist-client/modeller/v3-align.d.ts +26 -0
  23. package/dist-client/modeller/v3-align.js +65 -0
  24. package/dist-client/modeller/v3-align.js.map +1 -0
  25. package/dist-client/modeller/v3-canvas.d.ts +18 -0
  26. package/dist-client/modeller/v3-canvas.js +154 -2
  27. package/dist-client/modeller/v3-canvas.js.map +1 -1
  28. package/dist-client/modeller/v3-editor.d.ts +24 -0
  29. package/dist-client/modeller/v3-editor.js +181 -35
  30. package/dist-client/modeller/v3-editor.js.map +1 -1
  31. package/dist-client/modeller/v3-gizmo.d.ts +48 -0
  32. package/dist-client/modeller/v3-gizmo.js +40 -0
  33. package/dist-client/modeller/v3-gizmo.js.map +1 -0
  34. package/dist-client/pages/figure-list-page.d.ts +1 -0
  35. package/dist-client/pages/figure-list-page.js +5 -3
  36. package/dist-client/pages/figure-list-page.js.map +1 -1
  37. package/dist-client/pages/figure-modeller-page.js +9 -0
  38. package/dist-client/pages/figure-modeller-page.js.map +1 -1
  39. package/dist-client/route.d.ts +1 -1
  40. package/dist-client/tsconfig.tsbuildinfo +1 -1
  41. package/dist-client/types.d.ts +2 -0
  42. package/dist-client/types.js.map +1 -1
  43. package/package.json +4 -4
  44. package/test/modeller-route.test.ts +42 -0
  45. package/test/v3-align.test.ts +64 -0
  46. package/test/v3-gizmo.test.ts +97 -0
  47. package/test/v3-material.test.ts +68 -0
  48. package/test/v3-parts-wiring.test.ts +56 -0
  49. package/translations/en.json +5 -0
  50. package/translations/ja.json +5 -0
  51. package/translations/ko.json +5 -0
  52. package/translations/ms.json +5 -0
  53. package/translations/zh.json +5 -0
@@ -1,10 +1,16 @@
1
1
  import { LitElement, html, css, type PropertyValues } from 'lit'
2
+ import { i18next } from '@operato/i18n'
2
3
  import { customElement, property, state } from 'lit/decorators.js'
3
4
  import type { V3InputChange, V3PrimitiveCreation } from '@hatiolab/figure-model'
4
5
  import type { V3ProductSession } from './v3-product-session.js'
5
6
  import { captureV3Thumbnail } from './v3-thumbnail.js'
6
7
  import { drawableV3Parts } from './v3-renderer.js'
7
8
  import './v3-canvas.js'
9
+ import './figure-v3-parts.js'
10
+ import './figure-v3-material.js'
11
+ import { gizmoChanges, gizmoReachOf, type GizmoReach, type GizmoResult } from './v3-gizmo.js'
12
+ import { alignChanges, canMirror, mirrorChanges, type AlignTool } from './v3-align.js'
13
+ import type { V3PartRow } from './figure-v3-parts.js'
8
14
  import type { FigureV3Canvas } from './v3-canvas.js'
9
15
 
10
16
  const names: Record<string, string> = {
@@ -70,8 +76,8 @@ export class FigureV3Editor extends LitElement {
70
76
  overflow: auto;
71
77
  background: var(--md-sys-color-surface, #fff);
72
78
  }
73
- aside:first-child {
74
- border-right: 1px solid var(--md-sys-color-outline-variant);
79
+ figure-v3-parts {
80
+ min-height: 0;
75
81
  }
76
82
  aside:last-child {
77
83
  border-left: 1px solid var(--md-sys-color-outline-variant);
@@ -108,6 +114,24 @@ export class FigureV3Editor extends LitElement {
108
114
  color: var(--md-sys-color-on-secondary-container);
109
115
  border-color: var(--md-sys-color-primary);
110
116
  }
117
+ div[tools] {
118
+ display: flex;
119
+ flex-wrap: wrap;
120
+ gap: 4px;
121
+ margin: 0 0 8px;
122
+ }
123
+ div[tools] button {
124
+ display: inline-flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ width: 34px;
128
+ padding: 0;
129
+ }
130
+ div[tools] md-icon {
131
+ font-size: 18px;
132
+ width: 18px;
133
+ height: 18px;
134
+ }
111
135
  .parts {
112
136
  display: grid;
113
137
  gap: 6px;
@@ -163,7 +187,7 @@ export class FigureV3Editor extends LitElement {
163
187
  figure-v3-canvas {
164
188
  order: -1;
165
189
  }
166
- aside:first-child {
190
+ figure-v3-parts {
167
191
  border-right: none;
168
192
  border-top: 1px solid var(--md-sys-color-outline-variant);
169
193
  }
@@ -176,6 +200,11 @@ export class FigureV3Editor extends LitElement {
176
200
  @property({ type: Number }) revision = 0
177
201
  @state() private selected = ''
178
202
  @state() private error = ''
203
+ /**
204
+ * Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved, as in
205
+ * the V2 modeller: a part hidden here is still in the figure, still checked, still drawn on a board.
206
+ */
207
+ @state() private hiddenParts = new Set<string>()
179
208
  private lastContext: string | null = null
180
209
  protected willUpdate(_changed: PropertyValues) {
181
210
  const view = this.session?.read()
@@ -183,6 +212,7 @@ export class FigureV3Editor extends LitElement {
183
212
  this.lastContext = view?.context ?? null
184
213
  this.selected = ''
185
214
  this.error = ''
215
+ this.hiddenParts = new Set()
186
216
  }
187
217
  if (view && !view.geometry.some(g => g.id === this.selected)) this.selected = ''
188
218
  }
@@ -252,6 +282,111 @@ export class FigureV3Editor extends LitElement {
252
282
  this.selected = part.id
253
283
  })
254
284
  }
285
+ /**
286
+ * The list's own operations, as V3 part edits — each one step of the session's history. The copy and the moved
287
+ * part stay selected, as they do in the V2 list.
288
+ */
289
+ private duplicatePart(id: string) {
290
+ this.run(() => {
291
+ const before = new Set(this.session!.read().geometry.map(g => g.id))
292
+ this.session!.edit({ kind: 'duplicate-part', target: id })
293
+ this.selected = this.session!.read().geometry.find(g => !before.has(g.id))?.id ?? id
294
+ })
295
+ }
296
+ private movePart(id: string, to: number) {
297
+ this.run(() => this.session!.edit({ kind: 'move-part', target: id, to }))
298
+ }
299
+ private removePart(id: string) {
300
+ try {
301
+ this.session!.edit({ kind: 'remove-part', target: id })
302
+ this.error = ''
303
+ if (this.selected === id) this.selected = ''
304
+ this.changed()
305
+ } catch (error) {
306
+ /* The refusal a person meets most: another part stands on this one or follows its size. Said in their words. */
307
+ this.error =
308
+ (error as { code?: string })?.code === 'PART_REFERENCED'
309
+ ? i18next.t('figure.text.part-is-referenced', { part: id })
310
+ : error instanceof Error
311
+ ? error.message
312
+ : String(error)
313
+ }
314
+ }
315
+ private toggleHiddenPart(id: string) {
316
+ const next = new Set(this.hiddenParts)
317
+ if (next.has(id)) next.delete(id)
318
+ else next.add(id)
319
+ this.hiddenParts = next
320
+ }
321
+ /** What the gizmo may write for the selected part — read off the asset being edited. */
322
+ private reachOf(id: string): GizmoReach {
323
+ const none = { translate: false, rotate: false, scale: false }
324
+ if (!id || !this.session) return none
325
+ try {
326
+ return gizmoReachOf(JSON.parse(this.session.source()), id)
327
+ } catch {
328
+ return none
329
+ }
330
+ }
331
+ /** A drag, written back as the part's own inputs — one step of the history, like typing the numbers. */
332
+ private transformed(id: string, result: GizmoResult) {
333
+ const view = this.session!.read()
334
+ const sizes = Object.fromEntries(view.fields.map(field => [field.id, field.value as number]))
335
+ const changes = gizmoChanges(id, this.reachOf(id), result, sizes)
336
+ if (changes.length) this.run(() => this.session!.apply(changes))
337
+ else this.requestUpdate()
338
+ }
339
+ /** 맞추기 — one tool, one step of the history, through the same inputs as the gizmo. */
340
+ private align(tool: AlignTool) {
341
+ this.run(() => {
342
+ const asset = JSON.parse(this.session!.source())
343
+ const changes = alignChanges(asset, this.selected, this.reachOf(this.selected), tool)
344
+ if (changes.length) this.session!.apply(changes)
345
+ })
346
+ }
347
+ /** A mirrored copy: duplicate, then place the copy on the other side — two steps, as the V2 list makes it. */
348
+ private mirror(axis: 'x' | 'z') {
349
+ this.run(() => {
350
+ const id = this.selected
351
+ const before = new Set(this.session!.read().geometry.map(g => g.id))
352
+ this.session!.edit({ kind: 'duplicate-part', target: id })
353
+ const copy = this.session!.read().geometry.find(g => !before.has(g.id))?.id
354
+ if (!copy) return
355
+ this.session!.apply(mirrorChanges(JSON.parse(this.session!.source()), id, copy, axis))
356
+ this.selected = copy
357
+ })
358
+ }
359
+ private renderTools(unavailable: boolean) {
360
+ const reach = this.reachOf(this.selected)
361
+ if (!this.selected || !reach.translate) return null
362
+ let asset: unknown
363
+ try {
364
+ asset = JSON.parse(this.session!.source())
365
+ } catch {
366
+ return null
367
+ }
368
+ const tool = (name: AlignTool, icon: string, label: string, title: string) =>
369
+ html`<button ?disabled=${unavailable} aria-label=${i18next.t(label)} title=${i18next.t(title)} @click=${() => this.align(name)}>
370
+ <md-icon>${icon}</md-icon>
371
+ </button>`
372
+ return html`<div tools title=${i18next.t('figure.text.tools-explained')}>
373
+ ${tool('centre-x', 'horizontal_distribute', 'figure.button.center-x', 'figure.text.center-on-x')}
374
+ ${tool('centre-z', 'vertical_align_center', 'figure.button.center-z', 'figure.text.center-on-z')}
375
+ ${tool('floor', 'vertical_align_bottom', 'figure.button.to-floor', 'figure.text.put-on-floor')}
376
+ ${(['x', 'z'] as const).map(axis => {
377
+ const able = canMirror(asset as never, this.selected, axis, reach)
378
+ return html`<button
379
+ ?disabled=${unavailable || !able}
380
+ aria-label=${i18next.t('figure.button.mirror-' + axis)}
381
+ title=${i18next.t(able ? 'figure.text.mirror-explained' : 'figure.text.mirror-would-overlap')}
382
+ @click=${() => this.mirror(axis)}
383
+ >
384
+ <md-icon>${axis === 'x' ? 'flip' : 'flip_camera_android'}</md-icon>
385
+ </button>`
386
+ })}
387
+ ${tool('snap', 'grid_on', 'figure.button.snap', 'figure.text.snap-explained')}
388
+ </div>`
389
+ }
255
390
  private changeField(field: { id: string; unit: V3InputChange['unit']; role?: string }, event: Event) {
256
391
  const input = event.target as HTMLInputElement,
257
392
  value = input.valueAsNumber
@@ -290,30 +425,34 @@ export class FigureV3Editor extends LitElement {
290
425
  </header>
291
426
  ${this.error || view.error ? html`<div role="alert" class="error">${this.error || view.error}</div>` : null}
292
427
  <main>
293
- <aside>
294
- <h3>도형 추가</h3>
295
- <div class="recipes">
296
- ${(['box', 'cylinder', 'rounded-box', 'portal'] as const).map((kind, i) => html`<button ?disabled=${unavailable} @click=${() => this.add(kind)}>${['상자', '원기둥', '둥근 상자', '문틀'][i]}</button>`)}
297
- </div>
298
- <h3>부품 ${view.geometry.length}</h3>
299
- <div class="parts">
300
- ${view.geometry.map(
301
- g =>
302
- html`<button
303
- aria-pressed=${g.id === this.selected}
304
- @click=${() => {
305
- this.selected = g.id
306
- }}
307
- >
308
- ${g.id}
309
- </button>`
310
- )}
311
- </div>
312
- </aside>
428
+ <figure-v3-parts
429
+ .parts=${view.geometry.map(
430
+ (g): V3PartRow => ({
431
+ id: g.id,
432
+ provider: (g as { shape?: { provider?: string } }).shape?.provider ?? '',
433
+ hidden: this.hiddenParts.has(g.id),
434
+ invisible: (g as { visible?: boolean }).visible === false
435
+ })
436
+ )}
437
+ .selected=${this.selected}
438
+ ?locked=${unavailable}
439
+ @add-part=${(e: CustomEvent) => this.add(e.detail.kind)}
440
+ @select-part=${(e: CustomEvent) => (this.selected = e.detail.id)}
441
+ @toggle-part-hidden=${(e: CustomEvent) => this.toggleHiddenPart(e.detail.id)}
442
+ @set-all-parts-hidden=${(e: CustomEvent) =>
443
+ (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
444
+ @move-part-up=${(e: CustomEvent) => this.movePart(e.detail.id, e.detail.to)}
445
+ @move-part-down=${(e: CustomEvent) => this.movePart(e.detail.id, e.detail.to)}
446
+ @duplicate-part=${(e: CustomEvent) => this.duplicatePart(e.detail.id)}
447
+ @remove-part=${(e: CustomEvent) => this.removePart(e.detail.id)}
448
+ ></figure-v3-parts>
313
449
  <figure-v3-canvas
314
- .parts=${drawableV3Parts(view.geometry ?? [])}
450
+ .parts=${drawableV3Parts(view.geometry ?? []).filter(body => !this.hiddenParts.has((body as { id: string }).id))}
315
451
  .palette=${view.palette ?? {}}
316
452
  .context=${view.context ?? ''}
453
+ .selected=${this.selected}
454
+ .reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
455
+ @part-transformed=${(e: CustomEvent) => this.transformed(e.detail.id, e.detail.result)}
317
456
  @part-selected=${(e: CustomEvent<string>) => {
318
457
  this.selected = e.detail
319
458
  }}
@@ -323,23 +462,16 @@ export class FigureV3Editor extends LitElement {
323
462
  ></figure-v3-canvas>
324
463
  <aside>
325
464
  <h3>${this.selected || '모델 입력'}</h3>
465
+ ${this.renderTools(unavailable)}
326
466
  ${fields.map(field => html`<label><span>${fieldLabel(field.id)} (${field.unit})${field.role === 'state' ? ' · 동작' : ''}</span><input aria-label=${field.id} type="number" step="any" .value=${String(field.value)} ?disabled=${unavailable} @change=${(e: Event) => this.changeField(field, e)} /></label>`)}
327
467
  ${
328
468
  part
329
- ? html`<h3>재질</h3>
330
- <label
331
- ><span>색 역할</span
332
- ><select
333
- .value=${part.material.token}
334
- ?disabled=${unavailable}
335
- @change=${(e: Event) => this.run(() => this.session!.edit({ kind: 'set-material', target: this.selected, material: { ...part.material, token: (e.target as HTMLSelectElement).value } }))}
336
- >
337
- ${Object.keys(view.palette ?? {}).map(token => html`<option value=${token}>${token === 'palette.primary' ? '기본 몸체' : token}</option>`)}
338
- </select></label
339
- >
340
- <p class="hint">
341
- 도형과 위치를 결정하는 입력을 편집합니다. 다른 부품과 공유하는 입력은 함께 영향을 줄 수 있습니다.
342
- </p>`
469
+ ? html`<figure-v3-material
470
+ .body=${part}
471
+ .palette=${view.palette ?? {}}
472
+ ?locked=${unavailable}
473
+ @v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
474
+ ></figure-v3-material>`
343
475
  : null
344
476
  }
345
477
  ${
@@ -0,0 +1,80 @@
1
+ import type { V3InputChange } from '@hatiolab/figure-model'
2
+
3
+ /**
4
+ * What the gizmo may write for a part — the V2 gizmo's move · turn · size, on a V3 asset (parity row ③).
5
+ *
6
+ * A drag can only be written back where the part is placed by its own numbers. A part the create commands made is:
7
+ * its pose is `rigid@1` into the asset frame from `<part>.position.*` and `<part>.rotation.*`, and a box's shape
8
+ * (`box-shape@1` or `rounded-box@1`) takes its width, height and depth from inputs of its own — `<part>.size.*` from
9
+ * `addV3Box`, `<part>.dimension.*` from the editor's shape palette. A part fastened to another, following an instance size or made by the
10
+ * converter from expressions has no such numbers; the gizmo does not offer what it could not write back, rather
11
+ * than move something the next evaluation would put back.
12
+ */
13
+ export interface GizmoReach {
14
+ translate: boolean
15
+ rotate: boolean
16
+ scale: boolean
17
+ /** The inputs sizing the part along X, Y and Z, when it can be sized. */
18
+ sizes?: [string, string, string]
19
+ }
20
+
21
+ /** Shapes whose first three arguments are the extent along X, Y and Z. */
22
+ const BOXES = new Set(['box-shape@1', 'rounded-box@1'])
23
+
24
+ interface Node {
25
+ id: string
26
+ op: string
27
+ args: string[]
28
+ params?: Record<string, unknown>
29
+ }
30
+ interface AssetLike {
31
+ document: { model: { nodes: Node[]; inputs: Array<{ id: string; role?: string }> } }
32
+ }
33
+
34
+ const AXES = ['x', 'y', 'z'] as const
35
+
36
+ export function gizmoReachOf(asset: AssetLike, part: string): GizmoReach {
37
+ const { nodes, inputs } = asset.document.model
38
+ const design = new Set(inputs.filter(input => input.role === 'design').map(input => input.id))
39
+ const own = (name: string) => AXES.map(axis => `${part}.${name}.${axis}`)
40
+ const allDesign = (ids: string[]) => ids.every(id => design.has(id))
41
+ const pose = nodes.find(node => node.id === `${part}.pose`)
42
+ const placed = nodes.find(node => node.id === part && node.op === 'place@1')
43
+ const posed = !!pose && pose.op === 'rigid@1' && pose.params?.to === 'asset' && placed?.args[1] === `${part}.poseValue`
44
+ const shape = nodes.find(node => node.id === `${part}.shape`)
45
+ const extent = shape && BOXES.has(shape.op) ? shape.args.slice(0, 3) : []
46
+ const sizes =
47
+ extent.length === 3 && extent.every(id => id.startsWith(`${part}.`) && design.has(id)) ? (extent as [string, string, string]) : undefined
48
+ return {
49
+ translate: posed && allDesign(own('position')) && own('position').every((id, i) => pose!.args[i] === id),
50
+ rotate: posed && allDesign(own('rotation')) && own('rotation').every((id, i) => pose!.args[3 + i] === id),
51
+ scale: !!sizes,
52
+ ...(sizes ? { sizes } : {})
53
+ }
54
+ }
55
+
56
+ /** Where a drag left the part, in the asset frame: millimetres, degrees and per-axis scale. */
57
+ export interface GizmoResult {
58
+ position?: [number, number, number]
59
+ rotation?: [number, number, number]
60
+ scale?: [number, number, number]
61
+ }
62
+
63
+ /**
64
+ * The input changes a drag makes — one list, applied as one step. Millimetres to the millimetre and degrees to a
65
+ * tenth, as the V2 gizmo rounds (a drag otherwise stores 722.4180046035418 mm). A size never goes below 1 mm.
66
+ */
67
+ export function gizmoChanges(part: string, reach: GizmoReach, result: GizmoResult, sizes: Record<string, number>): V3InputChange[] {
68
+ const changes: V3InputChange[] = []
69
+ const put = (input: string, unit: V3InputChange['unit'], value: number) =>
70
+ changes.push({ kind: 'set-design', input, unit, value } as V3InputChange)
71
+ if (result.position && reach.translate)
72
+ AXES.forEach((axis, i) => put(`${part}.position.${axis}`, 'mm', Math.round(result.position![i]!)))
73
+ if (result.rotation && reach.rotate)
74
+ AXES.forEach((axis, i) => put(`${part}.rotation.${axis}`, 'deg', Math.round(result.rotation![i]! * 10) / 10))
75
+ if (result.scale && reach.scale && reach.sizes)
76
+ reach.sizes.forEach((id, i) => {
77
+ if (sizes[id] !== undefined) put(id, 'mm', Math.max(1, Math.round(sizes[id]! * result.scale![i]!)))
78
+ })
79
+ return changes
80
+ }
@@ -15,6 +15,7 @@ import { CommonGristStyles, CommonHeaderStyles, ScrollbarStyles, WorkSurfacePage
15
15
  import { capacityFrom } from '../modeller/scene-budget.js'
16
16
  import { deleteFigure, fetchFigureList } from '../graphql/index.js'
17
17
  import type { Figure } from '../types.js'
18
+ import { modellerPathOf } from '../modeller/modeller-route.js'
18
19
 
19
20
  /**
20
21
  * 이 값 아래면 점수를 색으로 눈에 띄게 한다.
@@ -530,7 +531,7 @@ export class FigureListPage extends FigureListPageBase {
530
531
  handlers: {
531
532
  click: (_columns: any, _data: any, _column: any, record: Figure) => {
532
533
  if (record?.id) {
533
- this.openModeller(record.id)
534
+ this.openModeller(record)
534
535
  }
535
536
  }
536
537
  }
@@ -585,8 +586,9 @@ export class FigureListPage extends FigureListPageBase {
585
586
  return { total: total || 0, records: items || [] }
586
587
  }
587
588
 
588
- private openModeller(id?: string) {
589
- navigate(id ? `figure-modeller/${id}` : 'figure-modeller')
589
+ /** The modeller that can edit this figure, by its format; a new figure starts in V3 (`modeller-route.ts`). */
590
+ private openModeller(figure?: Figure) {
591
+ navigate(modellerPathOf(figure))
590
592
  }
591
593
 
592
594
  /** `remove` 는 HTMLElement 의 메서드다 — 가리면 조용한 결함이 된다. */
@@ -30,6 +30,7 @@ import { DEFAULT_VIEW } from '../modeller/figure-view.js'
30
30
  import type { ViewSettings } from '../modeller/figure-view.js'
31
31
  import { createFigure, fetchFigure, fetchFigureTypeNames, fetchReleasedSource, updateFigure } from '../graphql/index.js'
32
32
  import type { Figure, FigureFinding, FigureGateFix, FigureGateFixPlan, FigureProposal, ProposalFeedback } from '../types.js'
33
+ import { isV3Source, modellerPathOf } from '../modeller/modeller-route.js'
33
34
 
34
35
  const FigureModellerPageBase = localize(i18next)(PageView) as typeof PageView
35
36
 
@@ -1461,6 +1462,14 @@ export class FigureModellerPage extends FigureModellerPageBase {
1461
1462
  const generation = ++this.loadGeneration
1462
1463
  const figure = await fetchFigure(id)
1463
1464
  if (generation !== this.loadGeneration || !this.active) return
1465
+ /*
1466
+ A converted figure reached by an old address — a bookmark, the AI dock, a link someone sent — opens in the
1467
+ modeller that can edit it instead of failing here on a source this page cannot read.
1468
+ */
1469
+ if (isV3Source(figure.source)) {
1470
+ navigate(modellerPathOf({ id, format: 'v3' }), true)
1471
+ return
1472
+ }
1464
1473
 
1465
1474
  // Decode before assigning the target ID or generating a thumbnail. On failure the previous
1466
1475
  // editor snapshot remains internal, but is hidden and cannot be saved under the requested ID.
package/client/types.ts CHANGED
@@ -27,6 +27,8 @@ export interface Figure {
27
27
  thumbnailUpdatedAt?: string
28
28
  updatedAt?: string
29
29
  updater?: { id: string; name: string }
30
+ /** Which format the stored source is in — `'v2'`, `'v3'`, or null when the server cannot tell. */
31
+ format?: 'v2' | 'v3' | null
30
32
  /** `FigureSource` JSON 문자열. 목록 질의에는 실려 오지 않는다. */
31
33
  source?: string
32
34
  properties?: string
@@ -43,6 +43,7 @@ const FIGURE_LIST_FIELDS = `
43
43
  score
44
44
  triangles
45
45
  groups
46
+ format
46
47
  thumbnailUpdatedAt
47
48
  updatedAt
48
49
  updater { id name }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n ProposalFeedback,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n refine?: boolean\n feedback?: ProposalFeedback[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n quality {\n status\n summary\n findings {\n dimension\n code\n message\n }\n visualRegions\n visualCoverage\n visualReadability\n }\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request },\n // @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.\n // 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.\n context: { hasUpload: !!request.image }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n fixPlan {\n resolved\n safe\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n }\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 fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n fixPlan {\n resolved\n safe\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n }\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureProposal,\n ProposalFeedback,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n format\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 저작 보조에게 후보를 시킨다.\n *\n * 팔레트를 **클라이언트가 보낸다.** 서버는 팔레트를 모른다 — things-scene 이 갖고\n * 있는데 그 패키지의 node 조건은 팔레트가 없는 번들로 간다. 그리는 쪽이 어떤 토큰을\n * 풀 수 있는지 아는 유일한 자리이므로 그쪽이 보낸다.\n *\n * 돌아오는 것은 **후보**다. 저장되지 않았다 — 저작자가 받아야 정본이 된다.\n */\nexport async function proposeFigure(request: {\n prompt: string\n base?: string\n type?: string\n palette: string[]\n refine?: boolean\n feedback?: ProposalFeedback[]\n image?: File\n}): Promise<FigureProposal> {\n const response = await client.mutate({\n mutation: gql`\n mutation ProposeFigure($request: ProposeRequest!) {\n proposeFigure(request: $request) {\n source\n score\n grade\n triangles\n groups\n quality {\n status\n summary\n findings {\n dimension\n code\n message\n }\n visualRegions\n visualCoverage\n visualReadability\n }\n attempts\n violations {\n code\n message\n }\n }\n }\n `,\n variables: { request },\n // @operato/graphql 는 이 신호가 있을 때만 Upload가 포함된 변수를 multipart로 인코딩한다.\n // 없으면 브라우저 File은 JSON에서 {}가 되어 서버 Upload scalar가 요청 자체를 거절한다.\n context: { hasUpload: !!request.image }\n })\n\n return unwrap<FigureProposal>(response, 'proposeFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. */\nexport async function revertFigureVersion(id: string, version: number): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!) {\n revertFigureVersion(id: $id, version: $version) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, version }\n })\n\n return unwrap<SaveResult>(response, 'revertFigureVersion')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n fixPlan {\n resolved\n safe\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n }\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 fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n fixPlan {\n resolved\n safe\n fixes {\n findingWay\n part\n axis\n from\n to\n resolved\n safe\n }\n }\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ source?: string }>(response, 'figureReleased')?.source ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { LitElement, nothing } from 'lit';
2
+ import type { V3AssetGeometry } from '@hatiolab/figure-model';
3
+ export declare const purposeOf: (token?: string) => string;
4
+ declare const FigureV3Material_base: (new (...args: any[]) => LitElement) & typeof LitElement;
5
+ /**
6
+ * 재질 — a V3 part's material, drawn as the V2 inspector draws it (V3 parity row ③).
7
+ *
8
+ * The V3 editor showed the colour role as a raw token (`palette.dark`) in a select and nothing else. The V3 material
9
+ * holds the colour role, a preset, transparency and a glow; a round body also has how finely its rim is drawn. This
10
+ * shows those, with the V2 words: painted colours and lit colours as two groups of swatches, each named for what it
11
+ * is for.
12
+ *
13
+ * Not here, because a V3 material has no place for them: flat shading and casting shadows (V2 fields the converter
14
+ * does not carry). The glow's colour comes from the lit colours, as in V2.
15
+ *
16
+ * Every change leaves as one `v3-part-edit`; the editor runs it through the session, one step of the undo history.
17
+ */
18
+ export declare class FigureV3Material extends FigureV3Material_base {
19
+ static styles: import("lit").CSSResult;
20
+ /** The selected body as evaluated — its material, its segments and its shape. */
21
+ body?: V3AssetGeometry & {
22
+ id: string;
23
+ };
24
+ /** The asset's palette, token → colour. */
25
+ palette: Record<string, string>;
26
+ locked: boolean;
27
+ render(): import("lit-html").TemplateResult<1> | typeof nothing;
28
+ private renderSegments;
29
+ private swatches;
30
+ private offered;
31
+ /** Intensity 0 is no glow at all, as in V2; a new glow takes the part's own colour until another is picked. */
32
+ private glow;
33
+ private put;
34
+ private edit;
35
+ }
36
+ export {};