@things-factory/figure-ui 10.1.158 → 10.1.160
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 +38 -0
- package/client/harness-theme/brand.css +21 -25
- package/client/index.ts +1 -0
- package/client/modeller/figure-clips.ts +17 -8
- package/client/modeller/figure-links.ts +21 -6
- package/client/modeller/figure-preview.ts +21 -7
- package/client/modeller/links.ts +13 -0
- package/client/setting-let/figure-namespace-setting-let.ts +84 -0
- package/dist-client/graphql/index.d.ts +13 -0
- package/dist-client/graphql/index.js +24 -0
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/harness-theme/brand.css +21 -25
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +1 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/modeller/figure-clips.js +26 -18
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-links.js +21 -7
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.js +21 -7
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/links.d.ts +13 -0
- package/dist-client/modeller/links.js +11 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/setting-let/figure-namespace-setting-let.d.ts +18 -0
- package/dist-client/setting-let/figure-namespace-setting-let.js +93 -0
- package/dist-client/setting-let/figure-namespace-setting-let.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/test/brand-palette.test.ts +41 -6
- package/test/data-motion.test.ts +3 -0
- package/test/harness-serve.test.ts +49 -0
- package/test/links.test.ts +29 -0
- package/translations/en.json +8 -1
- package/translations/es.json +8 -1
- package/translations/ja.json +8 -1
- package/translations/ko.json +8 -1
- package/translations/ms.json +8 -1
- package/translations/zh.json +8 -1
package/client/graphql/index.ts
CHANGED
|
@@ -211,6 +211,44 @@ export async function releaseFigure(id: string, comment?: string): Promise<Figur
|
|
|
211
211
|
return unwrap<Figure>(response, 'releaseFigure')
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/** A domain's figure namespace as the setting screen shows it (ADR-0114). */
|
|
215
|
+
export interface FigureNamespaceState {
|
|
216
|
+
/** What new figures get; null when neither the setting nor the subdomain is a namespace. */
|
|
217
|
+
namespace: string | null
|
|
218
|
+
/** The setting as written; empty when the subdomain is used. */
|
|
219
|
+
written: string
|
|
220
|
+
subdomain: string
|
|
221
|
+
/** The domain's figures carry it: it cannot be set any more. */
|
|
222
|
+
fixed: boolean
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed'
|
|
226
|
+
|
|
227
|
+
export async function fetchFigureNamespace(): Promise<FigureNamespaceState> {
|
|
228
|
+
const response = await client.query({
|
|
229
|
+
query: gql`
|
|
230
|
+
query {
|
|
231
|
+
figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }
|
|
232
|
+
}
|
|
233
|
+
`,
|
|
234
|
+
fetchPolicy: 'network-only'
|
|
235
|
+
})
|
|
236
|
+
return unwrap<FigureNamespaceState>(response, 'figureNamespace')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
|
|
240
|
+
export async function setFigureNamespace(namespace: string): Promise<FigureNamespaceState> {
|
|
241
|
+
const response = await client.mutate({
|
|
242
|
+
mutation: gql`
|
|
243
|
+
mutation ($namespace: String!) {
|
|
244
|
+
setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }
|
|
245
|
+
}
|
|
246
|
+
`,
|
|
247
|
+
variables: { namespace }
|
|
248
|
+
})
|
|
249
|
+
return unwrap<FigureNamespaceState>(response, 'setFigureNamespace')
|
|
250
|
+
}
|
|
251
|
+
|
|
214
252
|
/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
|
|
215
253
|
export interface KeptDraft {
|
|
216
254
|
id: string
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* figure
|
|
2
|
+
* figure 하네스의 **브랜드 색 이름 — 값은 operato-suite 의 teal 틀이 갖는다.**
|
|
3
3
|
*
|
|
4
4
|
* ── 왜 이 파일이 생겼나 ────────────────────────────────────────────────────
|
|
5
5
|
* figure-ui 에는 팔레트가 없었다. 호스트 앱이 주는 `--md-sys-color-*` 를 그대로 받아 썼고,
|
|
6
|
-
* 하네스
|
|
6
|
+
* 그때의 하네스 픽스처는 자기 `:root` 에 값을 직접 적고 있었다. 그래서
|
|
7
7
|
* 하네스에서 본 색과 앱에서 볼 색이 **아무 오류 없이 달라질 수 있는** 상태였다.
|
|
8
8
|
*
|
|
9
9
|
* operato-plant 가 같은 문제를 겪고 같은 방식으로 풀었다(`operato-plant/assets/brand.css`).
|
|
10
|
-
* 그 구조를
|
|
10
|
+
* 그 구조를 따른다 — 하네스 팔레트는 이 파일의 이름만 읽고, 이 파일은 값을 suite 에서 들여온다.
|
|
11
11
|
*
|
|
12
12
|
* ── 왜 `themes/` 가 아닌가 ─────────────────────────────────────────────────
|
|
13
13
|
* `@things-factory/builder` 의 `ThemeOverridePlugin` 이 모든 모듈의 `client/themes/*.css` 를
|
|
@@ -17,16 +17,22 @@
|
|
|
17
17
|
* 모듈이 선언하지 않는다 — shell 이 열고 앱이 채운다(ADR-0060 결정 2).
|
|
18
18
|
*
|
|
19
19
|
* 이 폴더의 셋은 **하네스가 쓰는 것**이고, `themes` 밖에 있으므로 플러그인이 아예 보지 않는다.
|
|
20
|
-
* 앱 화면의 색은 앱의 테마 파일이 정한다.
|
|
21
|
-
*
|
|
20
|
+
* 앱 화면의 색은 앱의 테마 파일이 정한다.
|
|
21
|
+
*
|
|
22
|
+
* ── 값은 여기 없다 ─────────────────────────────────────────────────────────
|
|
23
|
+
* teal 두 값은 `@things-factory/operato-suite` 의 `frames/teal.css` 가 `:root` 에 한 번 적는다
|
|
24
|
+
* (`--teal-on-light` · `--teal-on-dark`). operato-figure 와 operato-twin 의 앱 화면이 읽는 바로 그
|
|
25
|
+
* 값이다. 이 파일은 그것을 들여와 하네스의 이름으로 잇기만 한다 — 하네스는 시험 도구이고, 패키지
|
|
26
|
+
* 의존이 아니라 저장소 안의 경로로 읽는다(`/pkg/` 가 packages 폴더다). 설치본에서도 두 패키지는
|
|
27
|
+
* 형제 폴더라 같은 상대 경로가 맞는다.
|
|
22
28
|
*
|
|
23
29
|
* ── 브랜드 색만 여기 둔다 ──────────────────────────────────────────────────
|
|
24
30
|
* 중성색은 여기 없다. 그것은 `client/harness-theme/light.css` 가 갖는다. 여기 있는 것은 **제품을
|
|
25
31
|
* 가리키는 색**뿐이고, 그것만 절대 갈라지면 안 된다.
|
|
26
32
|
*
|
|
27
33
|
* ── 밝은 것과 어두운 것을 함께 든다 ────────────────────────────────────────
|
|
28
|
-
* 어두운 바탕에서
|
|
29
|
-
* (대비 2.15). 그래서 같은 이름이 밝기마다 다른 값을 갖는다 — 이름이 뜻이고 값은 자리에 따라
|
|
34
|
+
* 어두운 바탕에서 밝은 면용 teal 은 가라앉고(대비 3.21), 밝은 바탕에서 어두운 면용 teal 은 글자에
|
|
35
|
+
* 못 쓴다(대비 2.15). 그래서 같은 이름이 밝기마다 다른 값을 갖는다 — 이름이 뜻이고 값은 자리에 따라
|
|
30
36
|
* 다르다. plant · twin 과 같은 규약이다.
|
|
31
37
|
*
|
|
32
38
|
* ── 색은 하나뿐이다 ────────────────────────────────────────────────────────
|
|
@@ -38,31 +44,21 @@
|
|
|
38
44
|
* 팔레트 파일에 있다.
|
|
39
45
|
*/
|
|
40
46
|
|
|
47
|
+
@import url('../../../operato-suite/client/frames/teal.css');
|
|
48
|
+
|
|
41
49
|
:root {
|
|
42
50
|
/*
|
|
43
|
-
* figure — 딥 틸 186°.
|
|
44
|
-
*
|
|
45
|
-
* 흰 글자 대비 5.66 으로 작은 글자 기준(4.5)을 넘는다. [측정 2026-09-19]
|
|
46
|
-
*
|
|
47
|
-
* **이 값은 앱이 화면에 쓰는 것과 같다**(`operato-figure` 의 팔레트 · `assets/brand.css` 의
|
|
48
|
-
* `--brand-figure`). 한동안 이 파일은 네이비 `#154473` 이었고 앱은 틸이었다 — 이 파일이 테마
|
|
49
|
-
* 묶음에 실리지 않고 하네스만 읽었으므로 아무 오류 없이 두 얼굴이었다. 그것을 막으려고 생긴
|
|
50
|
-
* 파일이 그것을 못 막고 있었다. 2026-09-19 사용자 결정으로 앱 쪽 값에 맞췄다.
|
|
51
|
+
* figure — 딥 틸 186°. 흰 글자 대비 5.66 으로 작은 글자 기준(4.5)을 넘는다.
|
|
51
52
|
*
|
|
52
53
|
* 채도를 낮게 잡은 것이 이 색의 요점이다. 이 도구의 화면에서 색이 말해야 하는 것은 **저작자가
|
|
53
54
|
* 고른 부품 색**이다. 틀이 진한 색으로 소리를 내면 캔버스 안의 색을 판단할 수 없다. 그래서
|
|
54
55
|
* 브랜드 색은 깊고 조용하게 두고, 고른 것의 밑줄 · 기본 단추 · 링크에만 좁게 쓴다.
|
|
55
56
|
*/
|
|
56
|
-
--figure-brand:
|
|
57
|
+
--figure-brand: var(--teal-on-light);
|
|
57
58
|
|
|
58
|
-
/*
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
* 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. 앱 팔레트의 어두운 쪽 브랜드 색과
|
|
62
|
-
* 같다.
|
|
63
|
-
*/
|
|
64
|
-
--figure-brand-2: #22c3cf;
|
|
59
|
+
/* 어두운 면 위에서 쓰는 같은 색. 3D 캔버스의 선택 테두리와 어두운 배경의 강조가 이 값이다. */
|
|
60
|
+
--figure-brand-2: var(--teal-on-dark);
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
--figure-brand-wash:
|
|
62
|
+
/* 아주 옅게 깔 때 — 고른 줄의 바탕. 위 색을 흰 바탕에 8% 로 깐 값이다. */
|
|
63
|
+
--figure-brand-wash: color-mix(in srgb, var(--teal-on-light) 8%, white);
|
|
68
64
|
}
|
package/client/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { clipGroupsOf, clipsOf, freeGroupIdOf, groupEdit, poseAt, trackEdit, und
|
|
|
10
10
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
11
11
|
import { HelpToggles } from './help.js'
|
|
12
12
|
|
|
13
|
+
/** A value as a person reads it: at most three decimals, the float's last digits (14.399999999999999) left out. */
|
|
14
|
+
const shown = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 3 })
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
17
|
* 클립 — controls played by keys over time. A clip is one clock; each track plays one
|
|
15
18
|
* control. Scrubbing a clip shows its pose at that moment (one batch of input changes, not saved as the default); a key
|
|
@@ -108,13 +111,16 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
108
111
|
const asset = this.asset
|
|
109
112
|
if (!asset) return nothing
|
|
110
113
|
const clips = clipsOf(asset)
|
|
114
|
+
/* Nothing to play yet: said inside the section it belongs to, with the section's own margins. */
|
|
115
|
+
const empty = !clips.length && !undrivenOf(asset).length && !asset.motionGroups?.length
|
|
111
116
|
return html`
|
|
112
117
|
<section>
|
|
113
118
|
<h3>${i18next.t('figure.label.clips')}${this.help.button('figure.text.clips-explained')}</h3>
|
|
114
119
|
${this.help.hint('figure.text.clips-explained')}
|
|
115
120
|
${clips.map(clip => this.renderClip(clip))}
|
|
121
|
+
${empty ? html`<p hint>${i18next.t('figure.text.no-controls')}</p>` : nothing}
|
|
116
122
|
</section>
|
|
117
|
-
${this.renderAdd(asset, clips)} ${this.renderGroups(asset)}
|
|
123
|
+
${empty ? nothing : this.renderAdd(asset, clips)} ${this.renderGroups(asset)}
|
|
118
124
|
`
|
|
119
125
|
}
|
|
120
126
|
|
|
@@ -166,7 +172,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
166
172
|
<div keys>
|
|
167
173
|
${track.keys.map(
|
|
168
174
|
(key, i) => html`<span key
|
|
169
|
-
>${key.seconds}s → ${key.value}
|
|
175
|
+
>${key.seconds}s → ${shown(key.value)}
|
|
170
176
|
<button
|
|
171
177
|
?disabled=${this.locked || !withoutKey(track.keys, i)}
|
|
172
178
|
title=${i18next.t('figure.button.remove-key')}
|
|
@@ -183,7 +189,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
183
189
|
?disabled=${this.locked || current === undefined}
|
|
184
190
|
@click=${() => this.edit(trackEdit(clip, track, { keys: withKey(track.keys, { seconds: Math.round(at * 1000) / 1000, value: current! }) }))}
|
|
185
191
|
>
|
|
186
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-key-here', { seconds: at.toFixed(2), value: current
|
|
192
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-key-here', { seconds: at.toFixed(2), value: current === undefined ? '' : shown(current) })}
|
|
187
193
|
</button>
|
|
188
194
|
</div>`
|
|
189
195
|
}
|
|
@@ -195,12 +201,13 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
195
201
|
const group = joining ? undefined : groups.find(g => g.id === a.group)
|
|
196
202
|
/* The controls this clip may take: not played by it yet, and by no clip outside its group. */
|
|
197
203
|
const free = undrivenOf(asset, joining?.name ?? '', group?.id)
|
|
198
|
-
if (!free.length && !clips.length && !groups.length) return html`<p hint>${i18next.t('figure.text.no-controls')}</p>`
|
|
199
204
|
const stateId = free.includes(a.state) ? a.state : free[0]
|
|
200
205
|
const name = joining ? joining.name : a.name.trim()
|
|
201
206
|
const input = asset.document.model.inputs.find(i => i.id === stateId)
|
|
202
207
|
const duration = joining ? joining.duration : a.duration
|
|
203
|
-
|
|
208
|
+
/* A new clip takes a name no clip has: the same name would rewrite that clip's track, not add one. */
|
|
209
|
+
const taken = !joining && clips.some(c => c.name === name)
|
|
210
|
+
const ready = !!name && !taken && duration > 0 && !!input
|
|
204
211
|
return html`<section><h3>${i18next.t('figure.label.add-track')}</h3>
|
|
205
212
|
<div form>
|
|
206
213
|
<span>${i18next.t('figure.label.controls')}</span>
|
|
@@ -229,7 +236,7 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
229
236
|
<button
|
|
230
237
|
act
|
|
231
238
|
?disabled=${this.locked || !ready}
|
|
232
|
-
@click=${() =>
|
|
239
|
+
@click=${() => {
|
|
233
240
|
this.edit({
|
|
234
241
|
kind: 'set-driver',
|
|
235
242
|
state: stateId,
|
|
@@ -240,7 +247,10 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
240
247
|
{ seconds: 0, value: input!.min },
|
|
241
248
|
{ seconds: duration, value: input!.max }
|
|
242
249
|
]
|
|
243
|
-
})
|
|
250
|
+
})
|
|
251
|
+
/* The name was this clip's; the next new clip is named again. */
|
|
252
|
+
this.adding = { ...a, name: '', state: '' }
|
|
253
|
+
}}
|
|
244
254
|
>
|
|
245
255
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-track')}
|
|
246
256
|
</button></section>`
|
|
@@ -287,7 +297,6 @@ export class FigureClips extends localize(i18next)(LitElement) {
|
|
|
287
297
|
return html`<section clip>
|
|
288
298
|
<div track-head>
|
|
289
299
|
<strong>${group.label}</strong>
|
|
290
|
-
<span></span>
|
|
291
300
|
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-motion-group')} @click=${() => this.edit({ kind: 'remove-motion-group', id: group.id })}>
|
|
292
301
|
<md-icon>close</md-icon>
|
|
293
302
|
</button>
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
visibilityControlsOf,
|
|
29
29
|
placeKeptOf,
|
|
30
30
|
fasteningOf,
|
|
31
|
+
fastenGapOf,
|
|
31
32
|
hasInstanceSize,
|
|
32
33
|
holdsThings,
|
|
33
34
|
leadersFor,
|
|
@@ -73,6 +74,7 @@ const freshFasten = () => ({
|
|
|
73
74
|
targetFace: 'top' as FigureFace,
|
|
74
75
|
facing: 'meet' as 'meet' | 'flush',
|
|
75
76
|
gap: 0,
|
|
77
|
+
gapKept: undefined as unknown,
|
|
76
78
|
twist: 0,
|
|
77
79
|
align: {} as Partial<Record<Axis, Alignment>>,
|
|
78
80
|
alignMm: {} as Partial<Record<Axis, number>>,
|
|
@@ -482,11 +484,14 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
482
484
|
private heldWords(asset: FigureAsset) {
|
|
483
485
|
const joint = (asset.joints ?? []).find(j => j.body1.part === this.partId)
|
|
484
486
|
const face = joint ? i18next.t(endPlaceKeyOf(joint.body1)) : ''
|
|
485
|
-
const gap =
|
|
487
|
+
const gap = joint?.origin?.gap ?? 0
|
|
488
|
+
const place = i18next.t(`figure.label.resize-place-${asset.placement ?? 'floor'}`)
|
|
486
489
|
const where = !joint
|
|
487
490
|
? i18next.t('figure.text.held-by-rule')
|
|
488
491
|
: 'plane' in joint.body0
|
|
489
|
-
?
|
|
492
|
+
? typeof gap === 'number'
|
|
493
|
+
? i18next.t('figure.text.held-on-plane', { face, place, gap: String(gap) })
|
|
494
|
+
: i18next.t('figure.text.held-on-plane-worked-out', { face, place })
|
|
490
495
|
: i18next.t('figure.text.held-on-part', { face, other: joint.body0.part, otherFace: i18next.t(endPlaceKeyOf(joint.body0)) })
|
|
491
496
|
return where
|
|
492
497
|
}
|
|
@@ -573,7 +578,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
573
578
|
face: myFace,
|
|
574
579
|
to: f.toPlane ? { plane: 'mounting-plane' } : { part: f.target, face: targetFace },
|
|
575
580
|
...(f.toPlane ? {} : { facing: f.facing }),
|
|
576
|
-
...(f
|
|
581
|
+
...fastenGapOf(f),
|
|
577
582
|
twist: f.twist,
|
|
578
583
|
...(align ? { align } : {}),
|
|
579
584
|
replace: !!held
|
|
@@ -586,7 +591,11 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
586
591
|
for the default.
|
|
587
592
|
*/
|
|
588
593
|
const detail = [
|
|
589
|
-
...(f.
|
|
594
|
+
...(f.gapKept !== undefined
|
|
595
|
+
? [i18next.t('figure.text.fasten-summary-gap-worked-out')]
|
|
596
|
+
: f.gap
|
|
597
|
+
? [i18next.t('figure.text.fasten-summary-gap', { mm: String(f.gap) })]
|
|
598
|
+
: []),
|
|
590
599
|
...(f.twist ? [i18next.t('figure.text.fasten-summary-twist', { degrees: String(f.twist) })] : [])
|
|
591
600
|
]
|
|
592
601
|
/* Across the face, a row only where the drawing cannot set it: no drawing of the face, or a share or a field to follow. */
|
|
@@ -672,7 +681,13 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
672
681
|
${alignRows.length ? this.renderAlignment(alignRows, settle) : nothing}
|
|
673
682
|
<div form>
|
|
674
683
|
<span>${i18next.t('figure.label.gap')}</span>
|
|
675
|
-
<input
|
|
684
|
+
<input
|
|
685
|
+
type="number"
|
|
686
|
+
step="any"
|
|
687
|
+
.value=${f.gapKept !== undefined ? '' : String(f.gap)}
|
|
688
|
+
placeholder=${f.gapKept !== undefined ? i18next.t('figure.label.follows-expression') : ''}
|
|
689
|
+
@change=${(e: Event) => ((this.fasten = { ...f, gap: Number((e.target as HTMLInputElement).value) || 0, gapKept: undefined }), settle())}
|
|
690
|
+
/>
|
|
676
691
|
<span>${i18next.t('figure.label.twist')}</span>
|
|
677
692
|
<input type="number" step="any" .value=${String(f.twist)} @change=${(e: Event) => ((this.fasten = { ...f, twist: Number((e.target as HTMLInputElement).value) || 0 }), settle())} />
|
|
678
693
|
</div>
|
|
@@ -696,7 +711,7 @@ export class FigureLinks extends localize(i18next)(LitElement) {
|
|
|
696
711
|
const f = this.fasten
|
|
697
712
|
if ('targetFace' in c) this.aimAt(asset, { ...f, targetFace: c.targetFace as FigureFace })
|
|
698
713
|
else if ('mode' in c) this.fasten = { ...f, face: c.mode === 'centre' ? 'centre' : f.face === 'centre' ? 'bottom' : f.face, facing: c.mode === 'flush' ? 'flush' : 'meet' }
|
|
699
|
-
else if ('gap' in c) this.fasten = { ...f, gap: c.gap }
|
|
714
|
+
else if ('gap' in c) this.fasten = { ...f, gap: c.gap, gapKept: undefined }
|
|
700
715
|
else
|
|
701
716
|
this.fasten = {
|
|
702
717
|
...f,
|
|
@@ -113,6 +113,16 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
113
113
|
align-items: center;
|
|
114
114
|
gap: 8px;
|
|
115
115
|
}
|
|
116
|
+
/* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
|
|
117
|
+
div[clips] select {
|
|
118
|
+
grid-column: 2 / -1;
|
|
119
|
+
min-width: 0;
|
|
120
|
+
font: inherit;
|
|
121
|
+
color: inherit;
|
|
122
|
+
background-color: rgba(255, 255, 255, 0.14);
|
|
123
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
124
|
+
border-radius: 5px;
|
|
125
|
+
}
|
|
116
126
|
div[clips] svg[dial] {
|
|
117
127
|
width: 20px;
|
|
118
128
|
height: 20px;
|
|
@@ -296,13 +306,17 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
296
306
|
${(asset.motionGroups ?? []).map(group => {
|
|
297
307
|
const playing = this.moving[group.id] ?? { clip: group.defaultClip, speed: 0 }
|
|
298
308
|
return html`<label>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
<
|
|
305
|
-
|
|
309
|
+
<span>${group.label} <span drive>${i18next.t('figure.label.clip')}</span></span>
|
|
310
|
+
<select aria-label=${group.label} @change=${(e: Event) => this.move(group.id, { ...playing, clip: (e.target as HTMLSelectElement).value })}>
|
|
311
|
+
${group.clips.map(c => html`<option value=${c} ?selected=${c === playing.clip}>${c}</option>`)}
|
|
312
|
+
</select>
|
|
313
|
+
</label>
|
|
314
|
+
<label>
|
|
315
|
+
<span>${group.label} <span drive>${i18next.t('figure.label.playback-speed')}</span></span>
|
|
316
|
+
<span></span>
|
|
317
|
+
<input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e: Event) => this.move(group.id, { ...playing, speed: Number((e.target as HTMLInputElement).value) })} />
|
|
318
|
+
<output>${playing.speed.toFixed(2)}</output>
|
|
319
|
+
</label>`
|
|
306
320
|
})}
|
|
307
321
|
</div>`
|
|
308
322
|
}
|
package/client/modeller/links.ts
CHANGED
|
@@ -387,6 +387,8 @@ export function fasteningOf(
|
|
|
387
387
|
targetFace: string
|
|
388
388
|
facing: 'meet' | 'flush'
|
|
389
389
|
gap: number
|
|
390
|
+
/** The record's gap when it is worked out (a length that follows something): kept as it is until a number is typed. */
|
|
391
|
+
gapKept?: unknown
|
|
390
392
|
twist: number
|
|
391
393
|
align: Partial<Record<Axis, Alignment>>
|
|
392
394
|
alignMm: Partial<Record<Axis, number>>
|
|
@@ -424,12 +426,23 @@ export function fasteningOf(
|
|
|
424
426
|
targetFace,
|
|
425
427
|
facing: record.origin?.facing ?? 'meet',
|
|
426
428
|
gap: typeof gap === 'number' ? gap : 0,
|
|
429
|
+
...(gap !== undefined && typeof gap !== 'number' ? { gapKept: gap } : {}),
|
|
427
430
|
twist,
|
|
428
431
|
...kept,
|
|
429
432
|
...(atOrigin ? { atOrigin: true as const } : {})
|
|
430
433
|
}
|
|
431
434
|
}
|
|
432
435
|
|
|
436
|
+
/**
|
|
437
|
+
* The `gap` an attach command writes from the form: a worked-out gap as it was read, else the number, else none. Read as
|
|
438
|
+
* a number, a gap that follows the legs or the instance height became 0, and opening the form on the part dropped it
|
|
439
|
+
* to the plane (2026-10-13, the robot dog and the forklift).
|
|
440
|
+
*/
|
|
441
|
+
export function fastenGapOf(form: { gap: number; gapKept?: unknown }): { gap?: unknown } {
|
|
442
|
+
if (form.gapKept !== undefined) return { gap: form.gapKept }
|
|
443
|
+
return form.gap ? { gap: form.gap } : {}
|
|
444
|
+
}
|
|
445
|
+
|
|
433
446
|
/**
|
|
434
447
|
* The alignment that keeps the part where it stands when it is fastened: on each axis across the face, its distance
|
|
435
448
|
* from the target's centre (the plane's is the origin). Fastening used to centre it -- a door set 100 mm in front of
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import '@material/web/textfield/filled-text-field.js'
|
|
2
|
+
import '@things-factory/setting-base'
|
|
3
|
+
|
|
4
|
+
import { css, html, LitElement } from 'lit'
|
|
5
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
6
|
+
|
|
7
|
+
import { i18next, localize } from '@operato/i18n'
|
|
8
|
+
|
|
9
|
+
import { fetchFigureNamespace, setFigureNamespace, type FigureNamespaceState } from '../graphql/index.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The domain's figure namespace (ADR-0114): the `{namespace}` of every full name `{namespace}/{type}` a board stores.
|
|
13
|
+
* Empty takes the subdomain. It is set once: when the domain's figures carry it, it is shown and no longer edited --
|
|
14
|
+
* the server refuses a change either way, and says why.
|
|
15
|
+
*/
|
|
16
|
+
@customElement('figure-namespace-setting-let')
|
|
17
|
+
export class FigureNamespaceSettingLet extends localize(i18next)(LitElement) {
|
|
18
|
+
static styles = [
|
|
19
|
+
css`
|
|
20
|
+
div[slot='content'] {
|
|
21
|
+
display: grid;
|
|
22
|
+
gap: var(--spacing-small, 4px);
|
|
23
|
+
}
|
|
24
|
+
md-filled-text-field {
|
|
25
|
+
width: 100%;
|
|
26
|
+
}
|
|
27
|
+
strong {
|
|
28
|
+
font: var(--label-font);
|
|
29
|
+
color: var(--md-sys-color-on-surface);
|
|
30
|
+
}
|
|
31
|
+
p {
|
|
32
|
+
margin: 0;
|
|
33
|
+
font-size: 0.8rem;
|
|
34
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
35
|
+
text-transform: none;
|
|
36
|
+
}
|
|
37
|
+
`
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
@state() private now?: FigureNamespaceState
|
|
41
|
+
@state() private refused = ''
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
const now = this.now
|
|
45
|
+
return html`
|
|
46
|
+
<setting-let>
|
|
47
|
+
<span slot="title">${i18next.t('figure.title.namespace-setting')}</span>
|
|
48
|
+
<div slot="content">
|
|
49
|
+
${!now
|
|
50
|
+
? html``
|
|
51
|
+
: now.fixed
|
|
52
|
+
? html`<strong>${now.namespace}</strong>
|
|
53
|
+
<p>${i18next.t('figure.text.namespace-fixed')}</p>`
|
|
54
|
+
: html`<md-filled-text-field
|
|
55
|
+
.label=${i18next.t('figure.label.namespace')}
|
|
56
|
+
.value=${now.written}
|
|
57
|
+
placeholder=${now.subdomain}
|
|
58
|
+
?error=${!!this.refused || !now.namespace}
|
|
59
|
+
error-text=${this.refused || i18next.t('figure.text.namespace-subdomain-unusable', { subdomain: now.subdomain })}
|
|
60
|
+
supporting-text=${i18next.t('figure.text.namespace-empty-takes-subdomain', { subdomain: now.subdomain })}
|
|
61
|
+
@change=${(e: Event) => this.save((e.target as HTMLInputElement).value)}
|
|
62
|
+
></md-filled-text-field>`}
|
|
63
|
+
</div>
|
|
64
|
+
</setting-let>
|
|
65
|
+
`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async firstUpdated() {
|
|
69
|
+
try {
|
|
70
|
+
this.now = await fetchFigureNamespace()
|
|
71
|
+
} catch (e) {
|
|
72
|
+
this.refused = (e as Error).message
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async save(value: string) {
|
|
77
|
+
try {
|
|
78
|
+
this.now = await setFigureNamespace(value)
|
|
79
|
+
this.refused = ''
|
|
80
|
+
} catch (e) {
|
|
81
|
+
this.refused = (e as Error).message
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -39,6 +39,19 @@ export declare function deleteFigure(id: string): Promise<boolean>;
|
|
|
39
39
|
* 발행을 푼다(초안으로 돌아온다).
|
|
40
40
|
*/
|
|
41
41
|
export declare function releaseFigure(id: string, comment?: string): Promise<Figure>;
|
|
42
|
+
/** A domain's figure namespace as the setting screen shows it (ADR-0114). */
|
|
43
|
+
export interface FigureNamespaceState {
|
|
44
|
+
/** What new figures get; null when neither the setting nor the subdomain is a namespace. */
|
|
45
|
+
namespace: string | null;
|
|
46
|
+
/** The setting as written; empty when the subdomain is used. */
|
|
47
|
+
written: string;
|
|
48
|
+
subdomain: string;
|
|
49
|
+
/** The domain's figures carry it: it cannot be set any more. */
|
|
50
|
+
fixed: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function fetchFigureNamespace(): Promise<FigureNamespaceState>;
|
|
53
|
+
/** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
|
|
54
|
+
export declare function setFigureNamespace(namespace: string): Promise<FigureNamespaceState>;
|
|
42
55
|
/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
|
|
43
56
|
export interface KeptDraft {
|
|
44
57
|
id: string;
|
|
@@ -170,6 +170,30 @@ export async function releaseFigure(id, comment) {
|
|
|
170
170
|
});
|
|
171
171
|
return unwrap(response, 'releaseFigure');
|
|
172
172
|
}
|
|
173
|
+
const FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed';
|
|
174
|
+
export async function fetchFigureNamespace() {
|
|
175
|
+
const response = await client.query({
|
|
176
|
+
query: gql `
|
|
177
|
+
query {
|
|
178
|
+
figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }
|
|
179
|
+
}
|
|
180
|
+
`,
|
|
181
|
+
fetchPolicy: 'network-only'
|
|
182
|
+
});
|
|
183
|
+
return unwrap(response, 'figureNamespace');
|
|
184
|
+
}
|
|
185
|
+
/** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */
|
|
186
|
+
export async function setFigureNamespace(namespace) {
|
|
187
|
+
const response = await client.mutate({
|
|
188
|
+
mutation: gql `
|
|
189
|
+
mutation ($namespace: String!) {
|
|
190
|
+
setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
variables: { namespace }
|
|
194
|
+
});
|
|
195
|
+
return unwrap(response, 'setFigureNamespace');
|
|
196
|
+
}
|
|
173
197
|
const KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`;
|
|
174
198
|
/**
|
|
175
199
|
* 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가
|
|
@@ -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 { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 판을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 도형들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행판을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 판들 — 최근 것부터 열 개. */\n/**\n * 직전 발행판의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n 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): 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: { hasUpload: true }\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 } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 판을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n}): Promise<FigureListResult> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n figures(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, 'figures')\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 판 번호가 오르고, 그 순간이 판본으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A domain's figure namespace as the setting screen shows it (ADR-0114). */\nexport interface FigureNamespaceState {\n /** What new figures get; null when neither the setting nor the subdomain is a namespace. */\n namespace: string | null\n /** The setting as written; empty when the subdomain is used. */\n written: string\n subdomain: string\n /** The domain's figures carry it: it cannot be set any more. */\n fixed: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'namespace written subdomain fixed'\n\nexport async function fetchFigureNamespace(): Promise<FigureNamespaceState> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespace { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespaceState>(response, 'figureNamespace')\n}\n\n/** Writes the namespace; empty takes the subdomain. The server says why when it refuses. */\nexport async function setFigureNamespace(namespace: string): Promise<FigureNamespaceState> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($namespace: String!) {\n setFigureNamespace(namespace: $namespace) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { namespace }\n })\n return unwrap<FigureNamespaceState>(response, 'setFigureNamespace')\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): 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: { hasUpload: true }\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}
|