@things-factory/figure-ui 10.1.24 → 10.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/graphql/index.ts +4 -1
- package/client/modeller/figure-animations.ts +57 -5
- package/client/modeller/figure-history-panel.ts +2 -2
- package/client/modeller/figure-inspector.ts +8 -2
- package/client/modeller/figure-preview.ts +3 -3
- package/client/modeller/figure-source.ts +17 -41
- package/client/modeller/proposal-session.ts +8 -2
- package/client/pages/figure-modeller-page.ts +50 -15
- package/dist-client/graphql/index.js +4 -1
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/modeller/figure-animations.d.ts +5 -0
- package/dist-client/modeller/figure-animations.js +55 -5
- package/dist-client/modeller/figure-animations.js.map +1 -1
- package/dist-client/modeller/figure-history-panel.js +2 -2
- package/dist-client/modeller/figure-history-panel.js.map +1 -1
- package/dist-client/modeller/figure-inspector.js +8 -2
- package/dist-client/modeller/figure-inspector.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +3 -3
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-source.d.ts +4 -1
- package/dist-client/modeller/figure-source.js +5 -36
- package/dist-client/modeller/figure-source.js.map +1 -1
- package/dist-client/modeller/proposal-session.d.ts +5 -0
- package/dist-client/modeller/proposal-session.js +3 -2
- package/dist-client/modeller/proposal-session.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +2 -22
- package/dist-client/pages/figure-modeller-page.js +46 -15
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/test/ai-proposal-contract.test.ts +3 -2
- package/test/figure-source.test.ts +11 -57
- package/test/i18n-prefix-guard.test.ts +2 -3
- package/translations/en.json +95 -80
- package/translations/ja.json +184 -58
- package/translations/ko.json +115 -100
- package/translations/ms.json +182 -56
- package/translations/zh.json +183 -57
package/client/graphql/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import gql from 'graphql-tag'
|
|
2
|
+
import { i18next } from '@operato/i18n'
|
|
2
3
|
import { client } from '@operato/graphql'
|
|
3
4
|
|
|
4
5
|
import type {
|
|
@@ -29,7 +30,9 @@ function unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly
|
|
|
29
30
|
if (said) throw new Error(said)
|
|
30
31
|
|
|
31
32
|
if (!response.data) {
|
|
32
|
-
|
|
33
|
+
/* The field name stays off the screen because the user cannot act on it; it goes to the console. */
|
|
34
|
+
console.error(`[figure-ui] the server returned no data for ${field}`)
|
|
35
|
+
throw new Error(i18next.t('figure.text.server-returned-no-data'))
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
return response.data[field] as T
|
|
@@ -380,7 +380,6 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
380
380
|
return html`
|
|
381
381
|
<section>
|
|
382
382
|
<h3>${i18next.t('figure.label.animation')}</h3>
|
|
383
|
-
<p quiet>${i18next.t('figure.text.animation-is-driven-by-a-fact')}</p>
|
|
384
383
|
</section>
|
|
385
384
|
|
|
386
385
|
${this.renderRecipes()}
|
|
@@ -488,6 +487,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
488
487
|
const nameless = !parameter.name?.trim()
|
|
489
488
|
const range = parameter.range ?? NEW_RANGE
|
|
490
489
|
const backwards = !(range.max > range.min)
|
|
490
|
+
const slowness = parameter.clip?.duration ?? 0
|
|
491
491
|
|
|
492
492
|
return html`
|
|
493
493
|
<div clip ?on=${on}>
|
|
@@ -552,6 +552,21 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
552
552
|
/>
|
|
553
553
|
</div>
|
|
554
554
|
|
|
555
|
+
<div row>
|
|
556
|
+
<label>${i18next.t('figure.label.parameter-duration')}</label>
|
|
557
|
+
<input
|
|
558
|
+
type="number"
|
|
559
|
+
step="0.1"
|
|
560
|
+
min="0"
|
|
561
|
+
?bad=${slowness < 0}
|
|
562
|
+
.value=${live(parameter.clip?.duration === undefined ? '' : String(parameter.clip.duration))}
|
|
563
|
+
placeholder="0"
|
|
564
|
+
aria-label="duration"
|
|
565
|
+
@change=${(e: Event) => this.setDuration(at, (e.target as HTMLInputElement).value)}
|
|
566
|
+
/>
|
|
567
|
+
</div>
|
|
568
|
+
<p quiet>${i18next.t('figure.text.parameter-duration-empty-is-instant')}</p>
|
|
569
|
+
|
|
555
570
|
${on ? this.renderChannels('parameter', at) : nothing}
|
|
556
571
|
</div>
|
|
557
572
|
`
|
|
@@ -678,11 +693,18 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
678
693
|
*/
|
|
679
694
|
private renderKeys(kind: MotionKind, channel: Channel, at: number, j: number): TemplateResult {
|
|
680
695
|
const keys = channel.keys ?? []
|
|
696
|
+
/*
|
|
697
|
+
Keys on a parameter curve sit on the 0..1 value span (ADR-0051). A key above 1 is unreachable and
|
|
698
|
+
the format refuses it. The screen marks it and leaves the number alone: rewriting what the author
|
|
699
|
+
typed would hide what they wrote.
|
|
700
|
+
*/
|
|
701
|
+
const onValues = kind === 'parameter'
|
|
702
|
+
const outside = (key: Keyframe) => onValues && key.at > 1
|
|
681
703
|
|
|
682
704
|
return html`
|
|
683
705
|
<table keys>
|
|
684
706
|
<tr>
|
|
685
|
-
<th>${i18next.t('figure.label.key-at')}</th>
|
|
707
|
+
<th>${i18next.t(onValues ? 'figure.label.key-at-value' : 'figure.label.key-at')}</th>
|
|
686
708
|
${AXES.map(axis => html`<th>${axis}</th>`)}
|
|
687
709
|
<th></th>
|
|
688
710
|
</tr>
|
|
@@ -693,11 +715,16 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
693
715
|
<td>
|
|
694
716
|
<input
|
|
695
717
|
type="number"
|
|
696
|
-
step
|
|
718
|
+
step=${onValues ? '0.05' : '0.1'}
|
|
697
719
|
min="0"
|
|
698
|
-
|
|
720
|
+
max=${onValues ? '1' : nothing}
|
|
721
|
+
?bad=${backwards || key.at < 0 || outside(key)}
|
|
699
722
|
.value=${live(String(key.at))}
|
|
700
|
-
title=${backwards
|
|
723
|
+
title=${backwards
|
|
724
|
+
? i18next.t('figure.text.keys-must-rise')
|
|
725
|
+
: outside(key)
|
|
726
|
+
? i18next.t('figure.text.parameter-key-within-span')
|
|
727
|
+
: ''}
|
|
701
728
|
@change=${(e: Event) =>
|
|
702
729
|
this.setKey(kind, at, j, k, { at: Math.max(0, Number((e.target as HTMLInputElement).value) || 0) })}
|
|
703
730
|
/>
|
|
@@ -736,6 +763,7 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
736
763
|
${keys.some((key, k) => k > 0 && key.at <= (keys[k - 1]!.at ?? 0))
|
|
737
764
|
? html`<p warn>${i18next.t('figure.text.keys-must-rise')}</p>`
|
|
738
765
|
: nothing}
|
|
766
|
+
${keys.some(outside) ? html`<p warn>${i18next.t('figure.text.parameter-key-within-span')}</p>` : nothing}
|
|
739
767
|
|
|
740
768
|
<button @click=${() => this.addKey(kind, at, j)}>
|
|
741
769
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-key')}
|
|
@@ -883,6 +911,19 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
883
911
|
this.patchParameter(at, { range: { ...(parameter.range ?? NEW_RANGE), ...patch } })
|
|
884
912
|
}
|
|
885
913
|
|
|
914
|
+
/**
|
|
915
|
+
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
916
|
+
* a stored 0 and a missing field are different sources, so an empty input is never saved as 0.
|
|
917
|
+
*/
|
|
918
|
+
private setDuration(at: number, raw: string): void {
|
|
919
|
+
const parameter = this.parameters[at]
|
|
920
|
+
if (!parameter) return
|
|
921
|
+
const { duration: _dropped, ...rest } = parameter.clip
|
|
922
|
+
const text = raw.trim()
|
|
923
|
+
const clip = text === '' || !Number.isFinite(Number(text)) ? rest : { ...rest, duration: Number(text) }
|
|
924
|
+
this.patchParameter(at, { clip })
|
|
925
|
+
}
|
|
926
|
+
|
|
886
927
|
private setDefault(at: number, value: number): void {
|
|
887
928
|
this.patchParameter(at, { default: value })
|
|
888
929
|
}
|
|
@@ -954,6 +995,17 @@ export class FigureAnimations extends localize(i18next)(LitElement) {
|
|
|
954
995
|
if (!channel) return
|
|
955
996
|
|
|
956
997
|
const last = channel.keys[channel.keys.length - 1]
|
|
998
|
+
|
|
999
|
+
/*
|
|
1000
|
+
A parameter curve ends at 1, so appending a key would make it unreachable. Insert it between the last two keys.
|
|
1001
|
+
*/
|
|
1002
|
+
if (kind === 'parameter' && channel.keys.length >= 2) {
|
|
1003
|
+
const before = channel.keys[channel.keys.length - 2]!
|
|
1004
|
+
const middle: Keyframe = { at: (before.at + last!.at) / 2, value: { ...before.value } }
|
|
1005
|
+
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys.slice(0, -1), middle, last!] })
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
1008
|
+
|
|
957
1009
|
const key: Keyframe = { at: (last?.at ?? 0) + 1, value: { ...(last?.value ?? startValue(channel.path)) } }
|
|
958
1010
|
this.patchChannel(kind, at, j, { ...channel, keys: [...channel.keys, key] })
|
|
959
1011
|
}
|
|
@@ -438,11 +438,11 @@ export class FigureHistoryPanel extends localize(i18next)(LitElement) {
|
|
|
438
438
|
${(finding.fixes ?? []).map(fix => html`
|
|
439
439
|
${fix.safe ? html`
|
|
440
440
|
<button apply-fix @click=${() => this.applyFix(fix)}>
|
|
441
|
-
|
|
441
|
+
${i18next.t('figure.button.apply-fix', { part: fix.part, axis: fix.axis.toUpperCase(), from: fix.from ?? 'auto', to: fix.to })}
|
|
442
442
|
</button>
|
|
443
443
|
` : nothing}
|
|
444
444
|
<button preview-fix @click=${() => this.previewFix(fix)}>
|
|
445
|
-
|
|
445
|
+
${i18next.t('figure.button.preview-fix', { part: fix.part, axis: fix.axis.toUpperCase(), from: fix.from ?? 'auto', to: fix.to })}
|
|
446
446
|
</button>
|
|
447
447
|
`)}
|
|
448
448
|
</li>
|
|
@@ -1704,8 +1704,14 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
1706
|
private renderAnchors(part: FigurePart) {
|
|
1707
|
+
/*
|
|
1708
|
+
The scene draft stores the box height as depth, so format y is draft.depth and z is draft.height.
|
|
1709
|
+
This used to pass them swapped, and every automatic anchor still came out the same: the
|
|
1710
|
+
nearest-face rule reads the sign of a centred position, which does not depend on the box size.
|
|
1711
|
+
It is fixed so the next rule that does read the box does not inherit the mistake.
|
|
1712
|
+
*/
|
|
1707
1713
|
const base = this.draft
|
|
1708
|
-
? { x: this.draft.width, y: this.draft.
|
|
1714
|
+
? { x: this.draft.width, y: this.draft.depth, z: this.draft.height }
|
|
1709
1715
|
: { x: 0, y: 0, z: 0 }
|
|
1710
1716
|
const nameOf = (rule: string, axis: Axis) =>
|
|
1711
1717
|
`${i18next.t(rule === 'min' || rule === 'max' ? `figure.label.anchor-${rule}-${axis}` : `figure.label.anchor-${rule}`)} (${rule})`
|
|
@@ -1748,7 +1754,7 @@ export class FigureInspector extends localize(i18next)(LitElement) {
|
|
|
1748
1754
|
*/
|
|
1749
1755
|
return html`
|
|
1750
1756
|
<div anchor-row>
|
|
1751
|
-
<label>${axis.toUpperCase()}
|
|
1757
|
+
<label>${i18next.t('figure.label.axis-name', { axis: axis.toUpperCase() })}</label>
|
|
1752
1758
|
<div anchor-options>
|
|
1753
1759
|
${(['auto', 'scale', 'min', 'center', 'max', 'span'] as const).map(rule => {
|
|
1754
1760
|
const selected = rule === 'auto' ? !said : rule === said
|
|
@@ -47,7 +47,7 @@ export const PREVIEW_FACTOR = 2
|
|
|
47
47
|
* 안 담겨서 **네 칸씩 두 줄**로 놓는다.
|
|
48
48
|
*/
|
|
49
49
|
export const PREVIEW_WAYS: readonly { name: string; axes: readonly ('x' | 'y' | 'z')[] }[] = [
|
|
50
|
-
{ name: '
|
|
50
|
+
{ name: 'base', axes: [] },
|
|
51
51
|
{ name: 'x', axes: ['x'] },
|
|
52
52
|
{ name: 'y', axes: ['y'] },
|
|
53
53
|
{ name: 'z', axes: ['z'] },
|
|
@@ -208,7 +208,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
208
208
|
${this.cannot
|
|
209
209
|
? html`<div cannot>${this.cannot}</div>`
|
|
210
210
|
: html`<div legend>
|
|
211
|
-
${PREVIEW_WAYS.map(way => html`<span>${way.name}</span>`)}
|
|
211
|
+
${PREVIEW_WAYS.map(way => html`<span>${way.axes.length === 0 ? i18next.t('figure.label.preview-base') : way.name}</span>`)}
|
|
212
212
|
</div>
|
|
213
213
|
${clips.length + parameters.length > 0 ? this.renderDrivers(clips, parameters) : ''}`}
|
|
214
214
|
`
|
|
@@ -253,7 +253,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
253
253
|
|
|
254
254
|
return html`
|
|
255
255
|
<label>
|
|
256
|
-
<span>${clip.name} <span drive
|
|
256
|
+
<span>${clip.name} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
257
257
|
<input
|
|
258
258
|
type="range"
|
|
259
259
|
min="0"
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FIGURE_SOURCE_VERSION,
|
|
3
|
+
type DetailLevel,
|
|
4
|
+
type FigurePart,
|
|
5
|
+
type FigurePlacement,
|
|
6
|
+
type FigureSource,
|
|
7
|
+
type Vec3
|
|
8
|
+
} from '@hatiolab/figure-model'
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* Scene model <-> `FigureSource`.
|
|
@@ -40,9 +47,12 @@ import { FIGURE_SOURCE_VERSION, type DetailLevel, type FigurePart, type FigurePl
|
|
|
40
47
|
* size.z height
|
|
41
48
|
*
|
|
42
49
|
* position.x left + width/2 - draft width/2
|
|
43
|
-
* position.y zPos + depth/2
|
|
50
|
+
* position.y zPos + depth/2 <- both measured from the box bottom
|
|
44
51
|
* position.z top + height/2 - draft height/2
|
|
45
52
|
*
|
|
53
|
+
* (Source edition 2 measures y from the bottom face of the base box, as the scene's zPos does, so
|
|
54
|
+
* that row needs no half-box shift. x and z are still measured from the box centre. ADR-0065.)
|
|
55
|
+
*
|
|
46
56
|
* rotation.x rotationX (scene radians, format degrees)
|
|
47
57
|
* rotation.y -rotation
|
|
48
58
|
* rotation.z rotationY
|
|
@@ -317,7 +327,7 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
317
327
|
transform: withoutEmpty({
|
|
318
328
|
position: {
|
|
319
329
|
x: round4(part.left + part.width / 2 - half.x),
|
|
320
|
-
y: round4(part.zPos + part.depth / 2
|
|
330
|
+
y: round4(part.zPos + part.depth / 2),
|
|
321
331
|
z: round4(part.top + part.height / 2 - half.z)
|
|
322
332
|
},
|
|
323
333
|
size: { x: part.width, y: part.depth, z: part.height },
|
|
@@ -336,44 +346,9 @@ function partTo(part: PartModel, half: Vec3): FigurePart {
|
|
|
336
346
|
}) as FigurePart
|
|
337
347
|
}
|
|
338
348
|
|
|
339
|
-
/**
|
|
340
|
-
* 옛 `drive: 'hold'` clip 을 파라미터로 옮긴다 — **여는 순간에.**
|
|
341
|
-
*
|
|
342
|
-
* 형식은 값이 만드는 자세를 `parameters` 로 옮겼고(ADR-0051 ①), `compile()` 이 옛 원본을
|
|
343
|
-
* 청사진으로 만들 때 같은 이전을 한다. **거기서만 하면 저작 화면이 어긋난다** — 씬은
|
|
344
|
-
* 파라미터로 세우는데 저작 화면은 원본을 그대로 읽어 애니메이션 칸에 놓는다.
|
|
345
|
-
*
|
|
346
|
-
* 실제로 그랬다. 카탈로그의 리프터를 열면 `lift-height` 가 애니메이션 목록에 앉고,
|
|
347
|
-
* 미리보기는 그것에 **배속 손잡이**를 붙인다. 손잡이를 끝까지 밀어도 아무 일도 안 난다 —
|
|
348
|
-
* 청사진에는 그 이름의 clip 이 없고 파라미터로 가 있기 때문이다. 저작자가 보는 것은
|
|
349
|
-
* 「움직이지 않는 리프터」다.
|
|
350
|
-
*
|
|
351
|
-
* 범위는 `%` 0~100 이다. 옛 clip 은 제 수가 무엇인지 말한 적이 없으므로 **지어내지
|
|
352
|
-
* 않는다** — 저작자가 mm 로 고쳐 쓸 자리를 열어 줄 뿐이다.
|
|
353
|
-
*/
|
|
354
|
-
function partedMotion(source: FigureSource): Pick<FigureDraft, 'animations' | 'parameters'> {
|
|
355
|
-
const animations: NonNullable<FigureDraft['animations']> = []
|
|
356
|
-
const parameters: NonNullable<FigureDraft['parameters']> = [...(source.parameters ?? [])]
|
|
357
|
-
|
|
358
|
-
for (const clip of source.animations ?? []) {
|
|
359
|
-
const { drive, ...rest } = clip as typeof clip & { drive?: string }
|
|
360
|
-
if (drive !== 'hold') {
|
|
361
|
-
animations.push(rest)
|
|
362
|
-
continue
|
|
363
|
-
}
|
|
364
|
-
parameters.push({ name: clip.name, range: { unit: '%', min: 0, max: 100 }, clip: { channels: clip.channels } })
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
animations: animations.length > 0 ? animations : undefined,
|
|
369
|
-
parameters: parameters.length > 0 ? parameters : undefined
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
349
|
/** 형식을 씬 모델로 편다. */
|
|
374
350
|
export function fromFigureSource(source: FigureSource): { draft: FigureDraft; parts: PartModel[] } {
|
|
375
351
|
const half: Vec3 = { x: source.base.x / 2, y: source.base.y / 2, z: source.base.z / 2 }
|
|
376
|
-
const motion = partedMotion(source)
|
|
377
352
|
|
|
378
353
|
const draft = withoutEmpty({
|
|
379
354
|
version: source.version,
|
|
@@ -384,8 +359,9 @@ export function fromFigureSource(source: FigureSource): { draft: FigureDraft; pa
|
|
|
384
359
|
placement: source.placement,
|
|
385
360
|
detailLevel: source.detailLevel,
|
|
386
361
|
styleKit: source.styleKit,
|
|
387
|
-
|
|
388
|
-
|
|
362
|
+
/* Opened as stored. A legacy `drive: 'hold'` clip is refused by the format now; stored ones were migrated. */
|
|
363
|
+
animations: source.animations?.length ? source.animations : undefined,
|
|
364
|
+
parameters: source.parameters?.length ? source.parameters : undefined,
|
|
389
365
|
capabilities: source.capabilities
|
|
390
366
|
}) as FigureDraft
|
|
391
367
|
|
|
@@ -405,7 +381,7 @@ function partFrom(part: FigurePart, half: Vec3): PartModel {
|
|
|
405
381
|
top: round4(half.z + position.z - size.z / 2),
|
|
406
382
|
width: size.x,
|
|
407
383
|
height: size.z,
|
|
408
|
-
zPos: round4(
|
|
384
|
+
zPos: round4(position.y - size.y / 2),
|
|
409
385
|
depth: size.y,
|
|
410
386
|
|
|
411
387
|
/*
|
|
@@ -15,6 +15,11 @@ export interface ProposalSession {
|
|
|
15
15
|
picked: Set<string>
|
|
16
16
|
note: string
|
|
17
17
|
metrics?: Pick<FigureProposal, 'grade' | 'triangles' | 'groups' | 'attempts' | 'quality'>
|
|
18
|
+
/**
|
|
19
|
+
* The release-check fix this session was opened from, if any. The page translates the title from it;
|
|
20
|
+
* this module stays free of i18n because node tests import it directly.
|
|
21
|
+
*/
|
|
22
|
+
fix?: FigureGateFix
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
function selectedKeys(current: FigureSource | undefined, source: FigureSource): Set<string> {
|
|
@@ -25,7 +30,7 @@ function selectedKeys(current: FigureSource | undefined, source: FigureSource):
|
|
|
25
30
|
export function openAiProposal(current: FigureSource | undefined, proposal: FigureProposal): ProposalSession {
|
|
26
31
|
const source = JSON.parse(proposal.source) as FigureSource
|
|
27
32
|
if (!source || !Array.isArray(source.parts)) {
|
|
28
|
-
throw new Error('AI
|
|
33
|
+
throw new Error('the AI candidate is not a figure source: parts is missing or not an array')
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
return {
|
|
@@ -56,7 +61,8 @@ export function openSizingFix(current: FigureSource, fix: FigureGateFix): Propos
|
|
|
56
61
|
|
|
57
62
|
return {
|
|
58
63
|
source,
|
|
59
|
-
title:
|
|
64
|
+
title: '',
|
|
65
|
+
fix,
|
|
60
66
|
picked: selectedKeys(current, source),
|
|
61
67
|
note: ''
|
|
62
68
|
}
|
|
@@ -14,7 +14,7 @@ import { i18next, localize } from '@operato/i18n'
|
|
|
14
14
|
import { navigate, PageView } from '@operato/shell'
|
|
15
15
|
import { openOverlay } from '@operato/layout'
|
|
16
16
|
import { requestFigureAI, consumePendingFigureProposal } from '../modeller/figure-ai-target.js'
|
|
17
|
-
import { validate } from '@hatiolab/figure-model'
|
|
17
|
+
import { isPlaceholderType, placeholderType, TYPE_LENGTH, TYPE_PATTERN, validate } from '@hatiolab/figure-model'
|
|
18
18
|
import type { DetailLevel, FigureSource, PrimitiveKind } from '@hatiolab/figure-model'
|
|
19
19
|
|
|
20
20
|
import * as edits from '../modeller/part-edits.js'
|
|
@@ -114,6 +114,20 @@ function startingModel(type: string): { draft: FigureDraft; parts: PartModel[] }
|
|
|
114
114
|
* 바꾸는 것은 이 페이지다. 두 곳에서 고치면 실행 취소도 저장도 어느 쪽이 맞는지 알 수
|
|
115
115
|
* 없게 된다.
|
|
116
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Whether this name could be released, by the format's own rules (`type-not-identifier`,
|
|
119
|
+
* `type-is-placeholder`). Saving is not blocked, but the type cannot change after creation, so the
|
|
120
|
+
* author has to hear it now to avoid a draft that can never be released.
|
|
121
|
+
*/
|
|
122
|
+
function releasableType(type: string): boolean {
|
|
123
|
+
return (
|
|
124
|
+
!isPlaceholderType(type) &&
|
|
125
|
+
TYPE_PATTERN.test(type) &&
|
|
126
|
+
type.length >= TYPE_LENGTH.min &&
|
|
127
|
+
type.length <= TYPE_LENGTH.max
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
117
131
|
@customElement('figure-modeller-page')
|
|
118
132
|
export class FigureModellerPage extends FigureModellerPageBase {
|
|
119
133
|
static styles = css`
|
|
@@ -1112,10 +1126,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1112
1126
|
|
|
1113
1127
|
return html`
|
|
1114
1128
|
<div decide>
|
|
1115
|
-
<span>${this.
|
|
1129
|
+
<span>${this.proposalTitle(this.proposalSession)}</span>
|
|
1116
1130
|
<div spacer></div>
|
|
1117
1131
|
<button drop @click=${() => this.discard()}>${i18next.t('figure.button.discard')}</button>
|
|
1118
|
-
${stale ? html`<span role="alert"
|
|
1132
|
+
${stale ? html`<span role="alert">${i18next.t('figure.text.source-changed-ask-again')}</span>` : ''}
|
|
1119
1133
|
<button take ?disabled=${stale || errors.length > 0 || this.proposalSession.picked.size === 0} @click=${() => this.take()}>
|
|
1120
1134
|
${i18next.t('figure.button.take-n-changes', { n: this.proposalSession.picked.size })}
|
|
1121
1135
|
</button>
|
|
@@ -1434,7 +1448,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1434
1448
|
this.proposalFeedback = []
|
|
1435
1449
|
this.mode = 'edit'
|
|
1436
1450
|
// 타입 이름을 사람이 정하기 전까지는 임시 이름을 쓴다. 저장할 때 확정한다.
|
|
1437
|
-
const draftType =
|
|
1451
|
+
const draftType = placeholderType()
|
|
1438
1452
|
const { draft, parts } = startingModel(draftType)
|
|
1439
1453
|
|
|
1440
1454
|
this.figure = { id: '', type: draftType, name: '' } as Figure
|
|
@@ -1719,8 +1733,12 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1719
1733
|
if (!this.figure.id) {
|
|
1720
1734
|
// 신규 저장: 타입 코드와 표시 이름을 명확히 팝업으로 확인/입력받음
|
|
1721
1735
|
let defaultType = this.draft?.figureType || this.figure.type || ''
|
|
1722
|
-
|
|
1723
|
-
|
|
1736
|
+
/*
|
|
1737
|
+
A placeholder is not prefilled. This used to prefill 'FIGURE', inviting the author to confirm a
|
|
1738
|
+
meaningless name that cannot change later and that the release gate refuses.
|
|
1739
|
+
*/
|
|
1740
|
+
if (!defaultType || isPlaceholderType(defaultType)) {
|
|
1741
|
+
defaultType = ''
|
|
1724
1742
|
}
|
|
1725
1743
|
this.saveModalType = defaultType
|
|
1726
1744
|
this.saveModalName = this.figure.name || ''
|
|
@@ -1745,7 +1763,7 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1745
1763
|
const nameToSave = (this.saveModalName || this.figure.name || typeToSave).trim()
|
|
1746
1764
|
|
|
1747
1765
|
if (!typeToSave) {
|
|
1748
|
-
throw new Error('
|
|
1766
|
+
throw new Error(i18next.t('figure.text.enter-a-type-name'))
|
|
1749
1767
|
}
|
|
1750
1768
|
|
|
1751
1769
|
const taken = await fetchFigureTypeNames()
|
|
@@ -1803,29 +1821,46 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1803
1821
|
}
|
|
1804
1822
|
}
|
|
1805
1823
|
|
|
1824
|
+
/** Title of the candidate bar. A candidate opened from a release-check fix is described by translating that fix here. */
|
|
1825
|
+
private proposalTitle(session: proposalSessions.ProposalSession): string {
|
|
1826
|
+
const fix = session.fix
|
|
1827
|
+
if (fix) {
|
|
1828
|
+
return i18next.t('figure.text.release-check-fix', {
|
|
1829
|
+
part: fix.part,
|
|
1830
|
+
axis: fix.axis.toUpperCase(),
|
|
1831
|
+
from: fix.from ?? 'auto',
|
|
1832
|
+
to: fix.to
|
|
1833
|
+
})
|
|
1834
|
+
}
|
|
1835
|
+
return session.title || i18next.t('figure.text.assistant-proposed-a-figure')
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1806
1838
|
private renderSaveModal() {
|
|
1807
1839
|
if (!this.showSaveModal) return nothing
|
|
1808
1840
|
|
|
1809
1841
|
return html`
|
|
1810
1842
|
<div class="save-modal-backdrop" @click=${(e: Event) => { if (e.target === e.currentTarget) this.showSaveModal = false }}>
|
|
1811
1843
|
<div class="save-modal-card" role="dialog" aria-modal="true">
|
|
1812
|
-
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure'
|
|
1813
|
-
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc'
|
|
1844
|
+
<h3 class="save-modal-title">${i18next.t('figure.title.save-new-figure')}</h3>
|
|
1845
|
+
<p class="save-modal-desc">${i18next.t('figure.text.save-new-figure-desc')}</p>
|
|
1814
1846
|
|
|
1815
1847
|
<div class="save-modal-field">
|
|
1816
|
-
<label>${i18next.t('figure.label.figure-type'
|
|
1848
|
+
<label>${i18next.t('figure.label.figure-type')}</label>
|
|
1817
1849
|
<input
|
|
1818
1850
|
.value=${this.saveModalType}
|
|
1819
|
-
placeholder
|
|
1851
|
+
placeholder=${i18next.t('figure.text.figure-type-example')}
|
|
1820
1852
|
@input=${(e: Event) => (this.saveModalType = (e.target as HTMLInputElement).value.trim().toUpperCase())}
|
|
1821
1853
|
/>
|
|
1854
|
+
${this.saveModalType && !releasableType(this.saveModalType)
|
|
1855
|
+
? html`<div class="save-modal-error">${i18next.t('figure.text.type-name-will-not-release')}</div>`
|
|
1856
|
+
: nothing}
|
|
1822
1857
|
</div>
|
|
1823
1858
|
|
|
1824
1859
|
<div class="save-modal-field">
|
|
1825
|
-
<label>${i18next.t('figure.label.name'
|
|
1860
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
1826
1861
|
<input
|
|
1827
1862
|
.value=${this.saveModalName}
|
|
1828
|
-
placeholder
|
|
1863
|
+
placeholder=${i18next.t('figure.text.figure-name-example')}
|
|
1829
1864
|
@input=${(e: Event) => (this.saveModalName = (e.target as HTMLInputElement).value)}
|
|
1830
1865
|
/>
|
|
1831
1866
|
</div>
|
|
@@ -1834,10 +1869,10 @@ export class FigureModellerPage extends FigureModellerPageBase {
|
|
|
1834
1869
|
|
|
1835
1870
|
<div class="save-modal-actions">
|
|
1836
1871
|
<button class="btn-cancel" @click=${() => (this.showSaveModal = false)}>
|
|
1837
|
-
${i18next.t('figure.button.cancel'
|
|
1872
|
+
${i18next.t('figure.button.cancel')}
|
|
1838
1873
|
</button>
|
|
1839
1874
|
<button class="btn-confirm" ?disabled=${this.saving || !this.saveModalType} @click=${() => this.doSave()}>
|
|
1840
|
-
${this.saving ? i18next.t('figure.text.saving-figure'
|
|
1875
|
+
${this.saving ? i18next.t('figure.text.saving-figure') : i18next.t('figure.button.save')}
|
|
1841
1876
|
</button>
|
|
1842
1877
|
</div>
|
|
1843
1878
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import gql from 'graphql-tag';
|
|
2
|
+
import { i18next } from '@operato/i18n';
|
|
2
3
|
import { client } from '@operato/graphql';
|
|
3
4
|
/**
|
|
4
5
|
* 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**
|
|
@@ -16,7 +17,9 @@ function unwrap(response, field) {
|
|
|
16
17
|
if (said)
|
|
17
18
|
throw new Error(said);
|
|
18
19
|
if (!response.data) {
|
|
19
|
-
|
|
20
|
+
/* The field name stays off the screen because the user cannot act on it; it goes to the console. */
|
|
21
|
+
console.error(`[figure-ui] the server returned no data for ${field}`);
|
|
22
|
+
throw new Error(i18next.t('figure.text.server-returned-no-data'));
|
|
20
23
|
}
|
|
21
24
|
return response.data[field];
|
|
22
25
|
}
|
|
@@ -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 { 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 throw new Error(`서버가 ${field} 를 돌려주지 않았습니다.`)\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 blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\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 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 blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\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"]}
|
|
@@ -102,6 +102,11 @@ export declare class FigureAnimations extends FigureAnimations_base {
|
|
|
102
102
|
private renameClip;
|
|
103
103
|
private renameParameter;
|
|
104
104
|
private setRange;
|
|
105
|
+
/**
|
|
106
|
+
* Transition time. Clearing the input removes the field, which means instant. 0 is instant too, but
|
|
107
|
+
* a stored 0 and a missing field are different sources, so an empty input is never saved as 0.
|
|
108
|
+
*/
|
|
109
|
+
private setDuration;
|
|
105
110
|
private setDefault;
|
|
106
111
|
private removeClip;
|
|
107
112
|
private removeParameter;
|