@things-factory/figure-ui 10.1.263 → 10.1.266
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 +14 -5
- package/client/graphql/store.ts +1 -1
- package/client/modeller/align.ts +13 -1
- package/client/modeller/clips.ts +90 -1
- package/client/modeller/editor-styles.ts +36 -0
- package/client/modeller/editor.ts +53 -6
- package/client/modeller/figure-clips.ts +82 -34
- package/client/modeller/joint-drag.ts +32 -2
- package/client/modeller/product-session.ts +53 -5
- package/client/modeller/refusals.ts +23 -0
- package/dist-client/graphql/index.d.ts +7 -1
- package/dist-client/graphql/index.js +7 -5
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/store.js +1 -1
- package/dist-client/graphql/store.js.map +1 -1
- package/dist-client/modeller/align.d.ts +5 -0
- package/dist-client/modeller/align.js +10 -1
- package/dist-client/modeller/align.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +58 -6
- package/dist-client/modeller/clips.js +89 -0
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +36 -0
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +7 -0
- package/dist-client/modeller/editor.js +58 -5
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +13 -1
- package/dist-client/modeller/figure-clips.js +87 -34
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/joint-drag.d.ts +21 -2
- package/dist-client/modeller/joint-drag.js +20 -1
- package/dist-client/modeller/joint-drag.js.map +1 -1
- package/dist-client/modeller/links.d.ts +1 -1
- package/dist-client/modeller/product-session.d.ts +14 -2
- package/dist-client/modeller/product-session.js +43 -3
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +5 -0
- package/dist-client/modeller/refusals.js +22 -1
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/align.test.ts +18 -0
- package/test/clips.test.ts +83 -0
- package/test/data-motion.test.ts +13 -4
- package/test/joint-drag.test.ts +49 -0
- package/test/joint-frames.test.ts +3 -2
- package/test/product-session.test.ts +98 -0
- package/translations/en.json +24 -2
- package/translations/es.json +24 -2
- package/translations/ja.json +24 -2
- package/translations/ko.json +24 -2
- package/translations/ms.json +24 -2
- package/translations/zh.json +24 -2
|
@@ -46,6 +46,15 @@ export interface FigureCatalogPatch {
|
|
|
46
46
|
tags?: string[]
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** A catalogue field as the editor saw it before changing it; null when it was empty. */
|
|
50
|
+
export type FigureCatalogSeen = { [K in keyof FigureCatalogPatch]?: FigureCatalogPatch[K] | null }
|
|
51
|
+
|
|
52
|
+
/** Another editor changed a catalogue field first: which, and what the row holds now. */
|
|
53
|
+
export interface FigureCatalogConflict {
|
|
54
|
+
field: keyof FigureCatalogPatch
|
|
55
|
+
now: string | string[] | null
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
/** Persistence is injected; neither the model engine nor the renderer owns the product ID. */
|
|
50
59
|
export interface FigureProductStore {
|
|
51
60
|
load(id: string): Promise<FigureProductRecord>
|
|
@@ -65,12 +74,22 @@ export interface FigureProductStore {
|
|
|
65
74
|
/** Keys of the AI proposals whose steps the source holds: the server records them with the save. */
|
|
66
75
|
proposalKeys?: string[]
|
|
67
76
|
}): Promise<FigureProductRecord>
|
|
68
|
-
/**
|
|
69
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Writes the row's catalogue fields of a saved figure, only while the row still holds them as `seen` says -- else it
|
|
79
|
+
* is refused with FIGURE_CATALOG_CONFLICT and the fields the row holds now.
|
|
80
|
+
*/
|
|
81
|
+
updateCatalog(id: string, patch: FigureCatalogPatch, seen: FigureCatalogSeen): Promise<unknown>
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
const sameValue = (a: unknown, b: unknown) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null)
|
|
73
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A catalogue value as the server compares it (figure-service catalogConflictsOf): an empty category or type, or an
|
|
88
|
+
* empty tag list, is no value -- null; tags keep their order, which is part of the value.
|
|
89
|
+
*/
|
|
90
|
+
const catalogValue = (value: unknown) => (value === '' || (Array.isArray(value) && !value.length) ? null : (value ?? null))
|
|
91
|
+
const sameCatalogValue = (a: unknown, b: unknown) => sameValue(catalogValue(a), catalogValue(b))
|
|
92
|
+
|
|
74
93
|
/** Product lifecycle boundary. A late response can never change another editor's asset or ID. */
|
|
75
94
|
export function createFigureProductSession(store: FigureProductStore) {
|
|
76
95
|
const workspace = createFigureEditorWorkspace()
|
|
@@ -103,6 +122,12 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
103
122
|
with them before it has read the row again (2026-11-01 third re-check). Empty from the start of each save.
|
|
104
123
|
*/
|
|
105
124
|
let catalogWritten: FigureCatalogPatch = {}
|
|
125
|
+
/*
|
|
126
|
+
What the row held for each field held in `catalog`, as the editor saw it when it first changed it: sent with the
|
|
127
|
+
save, which goes on only while the row still holds it (outside review 2026-11-09: a later tab wrote over another's
|
|
128
|
+
category without a word). After a conflict it is what the row holds then, so saving again writes over it knowingly.
|
|
129
|
+
*/
|
|
130
|
+
let catalogSeen: FigureCatalogSeen = {}
|
|
106
131
|
const catalogHeld = () => Object.keys(catalog).length > 0
|
|
107
132
|
/* The author's size ends the last step moved without being asked (ADR-0087 decision 6), told once. */
|
|
108
133
|
let fitted: FigureSizeRangeFit[] = []
|
|
@@ -180,6 +205,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
180
205
|
creationId = undefined
|
|
181
206
|
catalog = {}
|
|
182
207
|
catalogWritten = {}
|
|
208
|
+
catalogSeen = {}
|
|
183
209
|
conflict = undefined
|
|
184
210
|
done = []
|
|
185
211
|
undone = []
|
|
@@ -207,11 +233,18 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
207
233
|
editCatalog(patch: FigureCatalogPatch, saved: FigureCatalogPatch) {
|
|
208
234
|
if (!id) throw refusal('SESSION_NOT_SAVED', '먼저 저장해 주세요.')
|
|
209
235
|
const next = { ...catalog }
|
|
236
|
+
const seen = { ...catalogSeen }
|
|
210
237
|
for (const key of Object.keys(patch) as (keyof FigureCatalogPatch)[]) {
|
|
211
|
-
if (!(catalogSending && key in catalogSending) &&
|
|
212
|
-
|
|
238
|
+
if (!(catalogSending && key in catalogSending) && sameCatalogValue(patch[key], saved[key])) {
|
|
239
|
+
delete next[key]
|
|
240
|
+
delete seen[key]
|
|
241
|
+
} else {
|
|
242
|
+
(next as Record<string, unknown>)[key] = patch[key]
|
|
243
|
+
if (!(key in seen)) (seen as Record<string, unknown>)[key] = catalogValue(saved[key])
|
|
244
|
+
}
|
|
213
245
|
}
|
|
214
246
|
catalog = next
|
|
247
|
+
catalogSeen = seen
|
|
215
248
|
return read()
|
|
216
249
|
},
|
|
217
250
|
/**
|
|
@@ -240,6 +273,8 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
240
273
|
/* The category and tags changed go with the copy, written once its first save gives it a row; its type is asked anew. */
|
|
241
274
|
const { type: _type, ...kept } = catalog
|
|
242
275
|
catalog = kept
|
|
276
|
+
/* A copy has no row yet: nothing it could have been changed on. */
|
|
277
|
+
catalogSeen = {}
|
|
243
278
|
unconfirmed.clear()
|
|
244
279
|
return read()
|
|
245
280
|
},
|
|
@@ -391,9 +426,22 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
391
426
|
if (id === undefined || workspace.read().dirty) await saveSource()
|
|
392
427
|
if (requestEpoch !== epoch) return read()
|
|
393
428
|
if (id !== undefined && Object.keys(sentCatalog).length) {
|
|
394
|
-
|
|
429
|
+
const seen = Object.fromEntries(Object.keys(sentCatalog).map(key => [key, catalogSeen[key as keyof FigureCatalogPatch] ?? null])) as FigureCatalogSeen
|
|
430
|
+
try {
|
|
431
|
+
await store.updateCatalog(id, sentCatalog, seen)
|
|
432
|
+
} catch (failure) {
|
|
433
|
+
/* The fields stay held, as typed; what the row holds now is what the next save expects -- it writes over it. */
|
|
434
|
+
const fields = (failure as { extensions?: { fields?: FigureCatalogConflict[] } })?.extensions?.fields
|
|
435
|
+
if ((failure as { code?: string })?.code === 'FIGURE_CATALOG_CONFLICT' && requestEpoch === epoch && fields)
|
|
436
|
+
for (const { field, now } of fields) if (field in catalog) (catalogSeen as Record<string, unknown>)[field] = now
|
|
437
|
+
throw failure
|
|
438
|
+
}
|
|
395
439
|
if (requestEpoch !== epoch) return read()
|
|
396
440
|
catalogWritten = sentCatalog
|
|
441
|
+
/* The row holds what was sent: a field changed again on the way is next compared with that. */
|
|
442
|
+
for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[])
|
|
443
|
+
if (sameValue(catalog[key], sentCatalog[key])) delete catalogSeen[key]
|
|
444
|
+
else (catalogSeen as Record<string, unknown>)[key] = catalogValue(sentCatalog[key])
|
|
397
445
|
/* What was sent is saved; a field changed again while it was on its way stays held. */
|
|
398
446
|
const next = { ...catalog }
|
|
399
447
|
for (const key of Object.keys(sentCatalog) as (keyof FigureCatalogPatch)[]) if (sameValue(next[key], sentCatalog[key])) delete next[key]
|
|
@@ -340,6 +340,7 @@ export function fieldRefusalOf(error: unknown, say: Say): string | null {
|
|
|
340
340
|
/** A refusal said in the author's words, or nothing when the screen has none for it. */
|
|
341
341
|
function knownRefusalOf(error: unknown, say: Say): string | null {
|
|
342
342
|
return (
|
|
343
|
+
catalogConflictOf(error, say) ??
|
|
343
344
|
sessionRefusalOf(error, say) ??
|
|
344
345
|
sizeRangeRefusalOf(error, say) ??
|
|
345
346
|
inputRangeRefusalOf(error, say) ??
|
|
@@ -360,6 +361,28 @@ function knownRefusalOf(error: unknown, say: Say): string | null {
|
|
|
360
361
|
)
|
|
361
362
|
}
|
|
362
363
|
|
|
364
|
+
/** The words for each catalogue field, as the properties tab labels it. */
|
|
365
|
+
const CATALOG_FIELD_LABEL: Record<string, string> = {
|
|
366
|
+
type: 'figure.label.figure-identifier',
|
|
367
|
+
category: 'figure.label.category',
|
|
368
|
+
tags: 'figure.label.tags'
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Another editor changed a catalogue field first (FIGURE_CATALOG_CONFLICT): each field by its label, with what the row
|
|
373
|
+
* holds now. The values typed are still held; saving again writes them over. Nothing for any other error.
|
|
374
|
+
*/
|
|
375
|
+
export function catalogConflictOf(error: unknown, say: Say): string | null {
|
|
376
|
+
const failure = error as { code?: string; extensions?: { fields?: { field: string; now: string | string[] | null }[] } }
|
|
377
|
+
if (failure?.code !== 'FIGURE_CATALOG_CONFLICT') return null
|
|
378
|
+
const valueOf = (now: string | string[] | null) =>
|
|
379
|
+
now === null || (Array.isArray(now) && !now.length) ? say('figure.text.catalog-value-empty', {}) : Array.isArray(now) ? now.join(', ') : now
|
|
380
|
+
const changes = (failure.extensions?.fields ?? [])
|
|
381
|
+
.map(({ field, now }) => `${CATALOG_FIELD_LABEL[field] ? say(CATALOG_FIELD_LABEL[field], {}) : field}: ${valueOf(now)}`)
|
|
382
|
+
.join(' · ')
|
|
383
|
+
return say('figure.text.catalog-conflict', { changes })
|
|
384
|
+
}
|
|
385
|
+
|
|
363
386
|
export function editRefusalOf(error: unknown, say: Say): string {
|
|
364
387
|
return knownRefusalOf(error, say) ?? say('figure.text.edit-refused', { code: codeOf(error) ?? '' })
|
|
365
388
|
}
|
|
@@ -35,7 +35,13 @@ export interface SaveResult {
|
|
|
35
35
|
violations: FigureFinding[];
|
|
36
36
|
}
|
|
37
37
|
export declare function createFigure(figure: NewFigure): Promise<SaveResult>;
|
|
38
|
-
|
|
38
|
+
/** The type, category and tags as the editor saw them; null is "was empty". The update goes on only while the row holds them. */
|
|
39
|
+
export interface FigureCatalogSeen {
|
|
40
|
+
type?: string | null;
|
|
41
|
+
category?: string | null;
|
|
42
|
+
tags?: string[] | null;
|
|
43
|
+
}
|
|
44
|
+
export declare function updateFigure(id: string, patch: FigurePatch, seen?: FigureCatalogSeen): Promise<SaveResult>;
|
|
39
45
|
/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */
|
|
40
46
|
export declare function setFigurePublic(id: string, isPublic: boolean): Promise<Figure>;
|
|
41
47
|
/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */
|
|
@@ -15,8 +15,10 @@ import { reached, refusedIn } from './reached.js';
|
|
|
15
15
|
*/
|
|
16
16
|
function unwrap(response, field) {
|
|
17
17
|
const said = response.errors?.[0]?.message;
|
|
18
|
+
/* The server's code and details go with its words: a conflict is told apart from a refusal (FIGURE_CATALOG_CONFLICT). */
|
|
19
|
+
const extensions = response.errors?.[0]?.extensions;
|
|
18
20
|
if (said)
|
|
19
|
-
throw new Error(said);
|
|
21
|
+
throw Object.assign(new Error(said), extensions?.code ? { code: extensions.code, extensions } : {});
|
|
20
22
|
if (!response.data) {
|
|
21
23
|
/* The field name stays off the screen because the user cannot act on it; it goes to the console. */
|
|
22
24
|
console.error(`[figure-ui] the server returned no data for ${field}`);
|
|
@@ -143,17 +145,17 @@ export async function createFigure(figure) {
|
|
|
143
145
|
});
|
|
144
146
|
return unwrap(response, 'createFigure');
|
|
145
147
|
}
|
|
146
|
-
export async function updateFigure(id, patch) {
|
|
148
|
+
export async function updateFigure(id, patch, seen) {
|
|
147
149
|
const response = await client.mutate({
|
|
148
150
|
mutation: gql `
|
|
149
|
-
mutation ($id: String!, $patch: FigurePatch
|
|
150
|
-
updateFigure(id: $id, patch: $patch) {
|
|
151
|
+
mutation ($id: String!, $patch: FigurePatch!, $seen: FigureCatalogSeen) {
|
|
152
|
+
updateFigure(id: $id, patch: $patch, seen: $seen) {
|
|
151
153
|
figure { ${FIGURE_LIST_FIELDS} source }
|
|
152
154
|
violations { code message at }
|
|
153
155
|
}
|
|
154
156
|
}
|
|
155
157
|
`,
|
|
156
|
-
variables: { id, patch }
|
|
158
|
+
variables: { id, patch, ...(seen ? { seen } : {}) }
|
|
157
159
|
});
|
|
158
160
|
return unwrap(response, 'updateFigure');
|
|
159
161
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client, uploadContext, type UploadProgress } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n if (said) throw new Error(said)\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 버전을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n deprecated\n supersededBy\n public\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\n/** How many of the domain's figures each state chip would show (draft, released), under the list's other conditions. */\nexport async function fetchFigureStateCounts(filters: unknown[], signal?: AbortSignal): Promise<Record<string, number>> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!]) {\n figureStateCounts(filters: $filters) { value count }\n }\n `,\n variables: { filters },\n fetchPolicy: 'no-cache',\n context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw new Error(response.errors.map(one => one.message).join(' · '))\n return Object.fromEntries((response.data?.figureStateCounts ?? []).map((row: { value: string; count: number }) => [row.value, row.count]))\n}\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n /** 'public': other domains' public figures, with their domain (LV figure-server design G2). The domain's own by default. */\n scope?: 'own' | 'public'\n}): Promise<FigureListResult> {\n const field = params.scope === 'public' ? 'publicFigures' : 'figures'\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n ${field}(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} ${params.scope === 'public' ? 'domain { id name subdomain }' : ''} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, field)\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!) {\n updateFigure(id: $id, patch: $patch) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\n/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */\nexport async function setFigurePublic(id: string, isPublic: boolean): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $public: Boolean!) {\n setFigurePublic(id: $id, public: $public) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, public: isPublic }\n })\n\n return unwrap<Figure>(response, 'setFigurePublic')\n}\n\n/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */\nexport async function setFigureDeprecation(id: string, deprecated: boolean, supersededBy?: string | null): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $deprecated: Boolean!, $supersededBy: String) {\n setFigureDeprecation(id: $id, deprecated: $deprecated, supersededBy: $supersededBy) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, deprecated, supersededBy: supersededBy || null }\n })\n\n return unwrap<Figure>(response, 'setFigureDeprecation')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 버전 번호가 오르고, 그 순간이 버전으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A namespace the domain owns (LV figure-server design G1): the `{namespace}` of the full name `{namespace}/{type}`. */\nexport interface FigureNamespace {\n id: string\n name: string\n description?: string | null\n state: 'active' | 'suspended'\n isDefault: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'id name description state isDefault'\n\n/** The categories the server allows, or null when a category is free text (LV figure-server design G5). */\nexport async function fetchFigureCategories(): Promise<string[] | null> {\n const response = await client.query({\n query: gql`\n query {\n figureCategories\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<string[] | null>(response, 'figureCategories') ?? null\n}\n\n/** The domain's namespaces, its default first. */\nexport async function fetchFigureNamespaces(): Promise<FigureNamespace[]> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespaces { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespace[]>(response, 'figureNamespaces') ?? []\n}\n\n/** Adds a namespace to the domain; the server says why when it refuses (not a namespace, another domain's). */\nexport async function createFigureNamespace(name: string, isDefault = false): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $isDefault: Boolean) {\n createFigureNamespace(name: $name, isDefault: $isDefault) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, isDefault }\n })\n return unwrap<FigureNamespace>(response, 'createFigureNamespace')\n}\n\n/** Makes one of the domain's active namespaces its default. */\nexport async function setDefaultFigureNamespace(name: string): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!) {\n setDefaultFigureNamespace(name: $name) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name }\n })\n return unwrap<FigureNamespace>(response, 'setDefaultFigureNamespace')\n}\n\n/** Suspends a namespace or makes it active again. */\nexport async function setFigureNamespaceState(name: string, state: 'active' | 'suspended'): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $state: String!) {\n setFigureNamespaceState(name: $name, state: $state) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, state }\n })\n return unwrap<FigureNamespace>(response, 'setFigureNamespaceState')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 버전을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 피규어들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행 버전을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 버전들 — 최근 것부터 열 개. */\n/**\n * 직전 발행 버전의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: string } | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File, onProgress: UploadProgress): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: uploadContext(onProgress)\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../client/graphql/index.ts"],"names":[],"mappings":"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","sourcesContent":["import gql from 'graphql-tag'\nimport { i18next } from '@operato/i18n'\nimport { client, uploadContext, type UploadProgress } from '@operato/graphql'\n\nimport { reached, refusedIn } from './reached.js'\n\nimport type {\n Figure,\n FigureFinding,\n FigureInspection,\n FigureListResult,\n FigurePatch,\n FigureVersion,\n NewFigure\n} from '../types.js'\n\n/**\n * 응답에서 값을 꺼낸다. **서버가 말한 사유를 그대로 올린다.**\n *\n * 전에는 `response.data.figures` 처럼 바로 꺼냈다. 서버가 오류를 돌려주면 `data` 가\n * 없으므로 `Cannot read properties of undefined (reading 'figures')` 가 났고, 화면에는\n * 그 문장이 그대로 떴다. 사용자는 자기가 무엇을 잘못했는지 알 수 없다 — 실제로 AI\n * 요청에서 그렇게 났다.\n *\n * 값이 `null` 인 것은 오류가 아니다. 지운 것을 물었을 때처럼 없는 것이 답인 경우가 있다.\n * 여기서 막는 것은 **`data` 자체가 없는 것**이다.\n */\nfunction unwrap<T>(response: { data?: Record<string, unknown>; errors?: readonly { message: string }[] }, field: string): T {\n const said = response.errors?.[0]?.message\n /* The server's code and details go with its words: a conflict is told apart from a refusal (FIGURE_CATALOG_CONFLICT). */\n const extensions = (response.errors?.[0] as { extensions?: { code?: string } } | undefined)?.extensions\n if (said) throw Object.assign(new Error(said), extensions?.code ? { code: extensions.code, extensions } : {})\n\n if (!response.data) {\n /* The field name stays off the screen because the user cannot act on it; it goes to the console. */\n console.error(`[figure-ui] the server returned no data for ${field}`)\n throw new Error(i18next.t('figure.text.server-returned-no-data'))\n }\n\n return response.data[field] as T\n}\n\n\n/**\n * 목록에 필요한 필드만 가져온다 — `source` 는 무겁고 목록에서 쓰지 않는다.\n *\n * **`thumbnail` 도 여기 없다.** base64 문자열이라 목록 응답에 실으면 카탈로그를 열 때마다 그림\n * 전부가 다시 오고(표본 14개 227KB), data URL 은 브라우저가 캐시하지 못한다. 그림은 주소로\n * 부른다(`figure-thumbnail` 라우트). 대신 `thumbnailUpdatedAt` 을 받는다 — 그림이 있나 없나를\n * 그것으로 알고, 버전을 가리는 값으로도 쓴다.\n */\nconst FIGURE_LIST_FIELDS = `\n id\n namespace\n type\n name\n description\n category\n tags\n state\n version\n deprecated\n supersededBy\n public\n score\n triangles\n groups\n thumbnailUpdatedAt\n updatedAt\n updater { id name }\n`\n\n/**\n * 목록을 가져온다.\n *\n * 걸러 보기·정렬·쪽 나누기를 **그대로 넘긴다.** 화면이 `search`·`state` 같은 이름을 따로 만들어\n * filter 로 옮기던 것을 그만두었다 — 그 이름들은 격자(ox-grist)가 이미 컬럼 설정에서 만들어 주고,\n * 중간에 한 벌 더 두면 격자가 아는 조건과 서버에 가는 조건이 어긋난다.\n */\n/** How many of the domain's figures each state chip would show (draft, released), under the list's other conditions. */\nexport async function fetchFigureStateCounts(filters: unknown[], signal?: AbortSignal): Promise<Record<string, number>> {\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!]) {\n figureStateCounts(filters: $filters) { value count }\n }\n `,\n variables: { filters },\n fetchPolicy: 'no-cache',\n context: { fetchOptions: { signal }, queryDeduplication: false }\n })\n if (response.errors?.length) throw new Error(response.errors.map(one => one.message).join(' · '))\n return Object.fromEntries((response.data?.figureStateCounts ?? []).map((row: { value: string; count: number }) => [row.value, row.count]))\n}\n\nexport async function fetchFigureList(params: {\n page?: number\n limit?: number\n filters?: unknown[]\n sortings?: { name: string; desc?: boolean }[]\n /** 'public': other domains' public figures, with their domain (LV figure-server design G2). The domain's own by default. */\n scope?: 'own' | 'public'\n}): Promise<FigureListResult> {\n const field = params.scope === 'public' ? 'publicFigures' : 'figures'\n const response = await client.query({\n query: gql`\n query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {\n ${field}(filters: $filters, pagination: $pagination, sortings: $sortings) {\n items { ${FIGURE_LIST_FIELDS} ${params.scope === 'public' ? 'domain { id name subdomain }' : ''} }\n total\n }\n }\n `,\n variables: {\n filters: params.filters ?? [],\n pagination: { page: params.page ?? 1, limit: params.limit ?? 30 },\n sortings: params.sortings?.length ? params.sortings : [{ name: 'updatedAt', desc: true }]\n },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureListResult>(response, field)\n}\n\n/** 저작면에 필요한 전부 — `source` 를 포함한다. */\nexport async function fetchFigure(id: string): Promise<Figure> {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n ${FIGURE_LIST_FIELDS}\n source\n properties\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<Figure>(response, 'figure')\n}\n\n/**\n * 이미 쓰이고 있는 타입 이름.\n *\n * `type` 은 만든 뒤에 고칠 수 없으므로 **만들기 전에** 알려 줘야 한다. 저장을 눌러\n * 거절당하고 나서 아는 것은 늦다.\n */\nexport async function fetchFigureTypeNames(): Promise<string[]> {\n const response = await client.query({\n query: gql`\n query {\n figureTypeNames\n }\n `,\n fetchPolicy: 'network-only'\n })\n\n return unwrap<string[] | null>(response, 'figureTypeNames') ?? []\n}\n\nexport interface SaveResult {\n figure: Figure\n /** 막지 않는 발견. 저장은 됐지만 화면이 보여 줘야 한다. */\n violations: FigureFinding[]\n}\n\nexport async function createFigure(figure: NewFigure): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($figure: NewFigure!) {\n createFigure(figure: $figure) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { figure }\n })\n\n return unwrap<SaveResult>(response, 'createFigure')\n}\n\n/** The type, category and tags as the editor saw them; null is \"was empty\". The update goes on only while the row holds them. */\nexport interface FigureCatalogSeen {\n type?: string | null\n category?: string | null\n tags?: string[] | null\n}\n\nexport async function updateFigure(id: string, patch: FigurePatch, seen?: FigureCatalogSeen): Promise<SaveResult> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $patch: FigurePatch!, $seen: FigureCatalogSeen) {\n updateFigure(id: $id, patch: $patch, seen: $seen) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n }\n }\n `,\n variables: { id, patch, ...(seen ? { seen } : {}) }\n })\n\n return unwrap<SaveResult>(response, 'updateFigure')\n}\n\n/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */\nexport async function setFigurePublic(id: string, isPublic: boolean): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $public: Boolean!) {\n setFigurePublic(id: $id, public: $public) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, public: isPublic }\n })\n\n return unwrap<Figure>(response, 'setFigurePublic')\n}\n\n/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */\nexport async function setFigureDeprecation(id: string, deprecated: boolean, supersededBy?: string | null): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $deprecated: Boolean!, $supersededBy: String) {\n setFigureDeprecation(id: $id, deprecated: $deprecated, supersededBy: $supersededBy) {\n ${FIGURE_LIST_FIELDS}\n }\n }\n `,\n variables: { id, deprecated, supersededBy: supersededBy || null }\n })\n\n return unwrap<Figure>(response, 'setFigureDeprecation')\n}\n\nexport async function deleteFigure(id: string): Promise<boolean> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($id: String!) {\n deleteFigure(id: $id)\n }\n `,\n variables: { id }\n })\n\n return unwrap<boolean>(response, 'deleteFigure')\n}\n\n/**\n * 발행한다 — 버전 번호가 오르고, 그 순간이 버전으로 남는다.\n *\n * 이미 발행된 것을 다시 발행하면 서버가 거절한다. 고쳐서 다시 내려면 저장이 먼저이고, 저장은\n * 발행을 푼다(초안으로 돌아온다).\n */\nexport async function releaseFigure(id: string, comment?: string): Promise<Figure> {\n const response = await client.mutate({\n mutation: gql`\n mutation ReleaseFigure($id: String!, $comment: String) {\n releaseFigure(id: $id, comment: $comment) {\n id\n version\n state\n updatedAt\n }\n }\n `,\n variables: { id, comment }\n })\n\n return unwrap<Figure>(response, 'releaseFigure')\n}\n\n/** A namespace the domain owns (LV figure-server design G1): the `{namespace}` of the full name `{namespace}/{type}`. */\nexport interface FigureNamespace {\n id: string\n name: string\n description?: string | null\n state: 'active' | 'suspended'\n isDefault: boolean\n}\n\nconst FIGURE_NAMESPACE_FIELDS = 'id name description state isDefault'\n\n/** The categories the server allows, or null when a category is free text (LV figure-server design G5). */\nexport async function fetchFigureCategories(): Promise<string[] | null> {\n const response = await client.query({\n query: gql`\n query {\n figureCategories\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<string[] | null>(response, 'figureCategories') ?? null\n}\n\n/** The domain's namespaces, its default first. */\nexport async function fetchFigureNamespaces(): Promise<FigureNamespace[]> {\n const response = await client.query({\n query: gql`\n query {\n figureNamespaces { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n fetchPolicy: 'network-only'\n })\n return unwrap<FigureNamespace[]>(response, 'figureNamespaces') ?? []\n}\n\n/** Adds a namespace to the domain; the server says why when it refuses (not a namespace, another domain's). */\nexport async function createFigureNamespace(name: string, isDefault = false): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $isDefault: Boolean) {\n createFigureNamespace(name: $name, isDefault: $isDefault) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, isDefault }\n })\n return unwrap<FigureNamespace>(response, 'createFigureNamespace')\n}\n\n/** Makes one of the domain's active namespaces its default. */\nexport async function setDefaultFigureNamespace(name: string): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!) {\n setDefaultFigureNamespace(name: $name) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name }\n })\n return unwrap<FigureNamespace>(response, 'setDefaultFigureNamespace')\n}\n\n/** Suspends a namespace or makes it active again. */\nexport async function setFigureNamespaceState(name: string, state: 'active' | 'suspended'): Promise<FigureNamespace> {\n const response = await client.mutate({\n mutation: gql`\n mutation ($name: String!, $state: String!) {\n setFigureNamespaceState(name: $name, state: $state) { ${FIGURE_NAMESPACE_FIELDS} }\n }\n `,\n variables: { name, state }\n })\n return unwrap<FigureNamespace>(response, 'setFigureNamespaceState')\n}\n\n/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */\nexport interface KeptDraft {\n id: string\n kind: 'revert' | 'undo'\n state?: string\n version?: number\n revertedToVersion?: number\n restoredAt?: string\n createdAt?: string\n creator?: { name?: string }\n}\n\nconst KEPT_FIELDS = `id kind state version revertedToVersion restoredAt createdAt creator { name }`\n\n/**\n * 옛 버전을 초안으로 되살린다. 정본·속성·그림이 돌아오고 이름·설명은 그대로다. 덮이는 초안은 같은 트랜잭션에서 서버가\n * 보관한다. `expectedSource` 는 서버가 들고 있다고 확인된 정본 -- 그 사이 바뀌었으면 거절된다. 답을 못 받았으면\n * (REVERT_UNREACHABLE) 같은 `requestId` 로 다시 묻는다: 서버는 같은 되돌리기로 답하고 두 번 하지 않는다.\n */\nexport async function revertFigureVersion(\n id: string,\n version: number,\n requestId: string,\n expectedSource: string\n): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation RevertFigureVersion($id: String!, $version: Float!, $requestId: String!, $expectedSource: String!) {\n revertFigureVersion(id: $id, version: $version, requestId: $requestId, expectedSource: $expectedSource) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { id, version, requestId, expectedSource }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'revertFigureVersion')\n}\n\n/** 되돌리기 취소 -- 보관한 초안을 되살린다. 되돌린 뒤 모델이 바뀌었으면 서버가 거절하고 보관본은 남는다. */\nexport async function undoFigureRevert(keptId: string, requestId: string): Promise<SaveResult & { kept: KeptDraft }> {\n const response = await reached(\n client.mutate({\n mutation: gql`\n mutation UndoFigureRevert($keptId: String!, $requestId: String!) {\n undoFigureRevert(keptId: $keptId, requestId: $requestId) {\n figure { ${FIGURE_LIST_FIELDS} source }\n violations { code message at }\n kept { ${KEPT_FIELDS} }\n }\n }\n `,\n variables: { keptId, requestId }\n }),\n 'REVERT_UNREACHABLE'\n )\n refusedIn(response)\n return unwrap<SaveResult & { kept: KeptDraft }>(response, 'undoFigureRevert')\n}\n\n/** A figure an AI proposal was saved into (ADR-0097 §5.6 item 5). */\nexport interface ProposalOutcome {\n figureId: string\n type?: string\n name?: string\n recordedAt?: string\n}\n\n/**\n * 제안이 무엇이 됐나 -- 그 제안을 담은 저장이 들어간 피규어들. 비었으면 저장되지 않았다(적용만 하고 저장 전이거나, 되돌렸거나,\n * 적용하지 않았다). 도크의 「결과 확인」이 이것을 묻는다: 도크의 기억이 아니라 서버의 기록이다.\n */\nexport async function fetchProposalOutcome(proposalKey: string): Promise<ProposalOutcome[]> {\n const response = await client.query({\n query: gql`\n query FigureProposalOutcome($proposalKey: String!) {\n figureProposalOutcome(proposalKey: $proposalKey) { figureId type name recordedAt }\n }\n `,\n variables: { proposalKey },\n fetchPolicy: 'network-only'\n })\n return unwrap<ProposalOutcome[]>(response, 'figureProposalOutcome') ?? []\n}\n\n/** 되돌리기가 보관한 초안들, 새것부터 -- 다시 접속해도 여기서 찾는다. */\nexport async function fetchReplacedDrafts(id: string): Promise<KeptDraft[]> {\n const response = await client.query({\n query: gql`\n query FigureReplacedDrafts($id: String!) {\n figureReplacedDrafts(id: $id) { ${KEPT_FIELDS} }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n return unwrap<KeptDraft[]>(response, 'figureReplacedDrafts')\n}\n\n/**\n * 발행해도 되나 — **누르기 전에** 묻는다.\n *\n * 막는 것은 서버의 `releaseFigure` 이고 이것은 이유를 미리 보여 주기 위한 것이다. 허가증이\n * 아니므로 이 답이 통과라 해도 발행은 다시 판정을 받는다.\n */\nexport async function inspectFigure(id: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigure($id: String!) {\n inspectFigure(id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigure')\n}\n\n/**\n * 편집 중인 원본의 발행 판정 — 저장하지 않은 것을 잰다.\n *\n * 발행 패널의 `inspectFigure` 는 저장된 원본을 재므로 고친 결과를 저장한 뒤에야 보여 준다. 편집하면서 막히는\n * 것을 보려면 편집 중인 원본을 보내야 한다. 판정은 서버의 같은 문(`inspectAnySource`)이 하고 문장도 같다 —\n * 화면이 제 판정을 따로 가지면 두 자리가 다른 말을 한다. `id` 가 있으면 서버가 그 행의 타입과 직전 발행 버전을 읽는다.\n */\nexport async function inspectFigureDraft(source: string, id?: string): Promise<FigureInspection> {\n const response = await client.query({\n query: gql`\n query InspectFigureDraft($source: String!, $id: String) {\n inspectFigureDraft(source: $source, id: $id) {\n blocked\n findings {\n code\n message\n why\n how\n at\n blocking\n }\n }\n }\n `,\n variables: { source, id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureInspection>(response, 'inspectFigureDraft')\n}\n\n/** 발행된 버전들 — 최근 것부터 열 개. */\n/**\n * 직전 발행 버전의 정본. 한 번도 발행 안 했으면 `undefined`.\n *\n * 저작 중에 「이 고침이 이미 놓인 것을 깨나」를 그 자리에서 보여 주려면 견줄 것이 필요하다. 저장\n * 전 초안은 서버가 모르므로 판정도 화면에서 한다 — 형식의 `contractChanges` 를 같이 쓴다.\n */\nexport async function fetchReleasedSource(id: string): Promise<string | undefined> {\n return (await fetchReleasedFigure(id))?.source\n}\n\n/** Read-only catalogue preview: source and version come from the same released snapshot. */\nexport async function fetchReleasedFigure(id: string): Promise<{ version: number; source: string } | undefined> {\n const response = await client.query({\n query: gql`\n query FigureReleased($id: String!) {\n figureReleased(id: $id) {\n version\n source\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<{ version: number; source: string } | undefined>(response, 'figureReleased') ?? undefined\n}\n\nexport async function fetchFigureVersions(id: string): Promise<FigureVersion[]> {\n const response = await client.query({\n query: gql`\n query FigureVersions($id: String!) {\n figureVersions(id: $id) {\n version\n comment\n state\n updatedAt\n updater { id name }\n score\n triangles\n groups\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n\n return unwrap<FigureVersion[]>(response, 'figureVersions') ?? []\n}\n\n/** A recipe template as the server gives it (ADR-0095, amended): words in the language asked, the recipe as JSON. */\nexport interface RecipeTemplateRow {\n name: string\n category: string\n tags: string[]\n target: 'part' | 'figure'\n title: string\n description: string\n source: string\n hasThumbnail: boolean\n}\n\nexport async function fetchRecipeTemplates(lang: string): Promise<RecipeTemplateRow[]> {\n const response = await client.query({\n query: gql`\n query ($lang: String) {\n figureRecipeTemplates(lang: $lang) {\n name\n category\n tags\n target\n title\n description\n source\n hasThumbnail\n }\n }\n `,\n variables: { lang }\n })\n return unwrap<RecipeTemplateRow[] | null>(response, 'figureRecipeTemplates') ?? []\n}\n\n/** One template's guide in the language asked, or '' while none is written. */\nexport async function fetchRecipeGuide(name: string, lang: string): Promise<string> {\n const response = await client.query({\n query: gql`\n query ($name: String!, $lang: String) {\n figureRecipeTemplate(name: $name, lang: $lang) {\n guide\n }\n }\n `,\n variables: { name, lang }\n })\n return unwrap<{ guide?: string } | null>(response, 'figureRecipeTemplate')?.guide ?? ''\n}\n\n/**\n * A GLB for a mesh part (ADR-0129): the server reads it, keeps it by its hash and answers what it holds. A file a board\n * cannot draw is refused with the reason in the screen's words.\n */\nexport async function uploadFigureMesh(file: File, onProgress: UploadProgress): Promise<import('../modeller/mesh-import.js').MeshUpload> {\n const response = await client.mutate({\n mutation: gql`\n mutation UploadFigureMesh($file: Upload!) {\n uploadFigureMesh(file: $file) {\n hash\n summary\n }\n }\n `,\n variables: { file },\n context: uploadContext(onProgress)\n })\n return unwrap(response, 'uploadFigureMesh')\n}\n"]}
|
|
@@ -59,6 +59,6 @@ export const figureProductStore = {
|
|
|
59
59
|
throw Error('서버가 저장 결과를 돌려주지 않았습니다.');
|
|
60
60
|
return result;
|
|
61
61
|
},
|
|
62
|
-
updateCatalog: (id, patch) => updateFigure(id, patch)
|
|
62
|
+
updateCatalog: (id, patch, seen) => updateFigure(id, patch, seen)
|
|
63
63
|
};
|
|
64
64
|
//# sourceMappingURL=store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../client/graphql/store.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,kBAAkB,GAAuB;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;IACjD,CAAC;IACD,4GAA4G;IAC5G,KAAK,CAAC,QAAQ,CAAC,EAAE;QACf,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;OAST;YACD,SAAS,EAAE,EAAE,EAAE,EAAE;YACjB,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAkG,CAAA;QAChI,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;IACtH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,OAAO;QAChB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YAC3C,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;OAgBZ;YACD,SAAS,EAAE,OAAO;SACnB,CAAC,CAAC,CAAA;QACH,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;YACzB,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAwC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;QACnK,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,UAA6C,CAAA;QAC3E,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,CAAA;QACxE,OAAO,MAAM,CAAA;IACf,CAAC;IACD,aAAa,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../client/graphql/store.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,aAAa,CAAA;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,kBAAkB,GAAuB;IACpD,KAAK,CAAC,IAAI,CAAC,EAAE;QACX,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,CAAC,CAAA;QACpC,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAA;IACjD,CAAC;IACD,4GAA4G;IAC5G,KAAK,CAAC,QAAQ,CAAC,EAAE;QACf,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAClC,KAAK,EAAE,GAAG,CAAA;;;;;;;;;OAST;YACD,SAAS,EAAE,EAAE,EAAE,EAAE;YACjB,WAAW,EAAE,cAAc;SAC5B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,MAAkG,CAAA;QAChI,IAAI,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,KAAK,CAAC,kBAAkB,CAAC,CAAA;QACpD,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,SAAS,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAA;IACtH,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,OAAO;QAChB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YAC3C,QAAQ,EAAE,GAAG,CAAA;;;;;;;;;;;;;;;;OAgBZ;YACD,SAAS,EAAE,OAAO;SACnB,CAAC,CAAC,CAAA;QACH,IAAI,QAAQ,CAAC,MAAM,EAAE,MAAM;YACzB,MAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAwC,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;QACnK,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,UAA6C,CAAA;QAC3E,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,MAAM,KAAK,CAAC,wBAAwB,CAAC,CAAA;QACxE,OAAO,MAAM,CAAA;IACf,CAAC;IACD,aAAa,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC;CAClE,CAAA","sourcesContent":["import gql from 'graphql-tag'\nimport { client } from '@operato/graphql'\nimport { fetchFigure, updateFigure } from './index.js'\nimport { reached } from './reached.js'\nimport type { FigureProductStore, FigureProductRecord } from '../modeller/product-session.js'\n\n/** source is sent intact. No conversion into the old parts/base authoring representation. */\nexport const figureProductStore: FigureProductStore = {\n async load(id) {\n const record = await fetchFigure(id)\n if (!record?.source) throw Error('Figure 정본이 없습니다.')\n return { id: record.id, source: record.source }\n },\n /* What the server holds of every field a save writes, read back after a save whose outcome is not known. */\n async readBack(id) {\n const response = await client.query({\n query: gql`\n query ($id: String!) {\n figure(id: $id) {\n id\n source\n thumbnailHash\n type\n }\n }\n `,\n variables: { id },\n fetchPolicy: 'network-only'\n })\n const figure = response.data?.figure as { id: string; source: string; thumbnailHash?: string | null; type?: string } | undefined\n if (!figure?.source) throw Error('Figure 정본이 없습니다.')\n return { id: figure.id, source: figure.source, thumbnailHash: figure.thumbnailHash ?? undefined, type: figure.type }\n },\n async save(request) {\n const response = await reached(client.mutate({\n mutation: gql`\n mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String, $proposalKeys: [String!], $namespace: String) {\n saveFigure(\n source: $source\n id: $id\n expectedSource: $expectedSource\n creationId: $creationId\n thumbnail: $thumbnail\n type: $type\n proposalKeys: $proposalKeys\n namespace: $namespace\n ) {\n id\n source\n }\n }\n `,\n variables: request\n }))\n if (response.errors?.length)\n throw Object.assign(Error(response.errors.map(e => e.message).join('\\n')), { code: (response.errors[0] as { extensions?: { code?: string } }).extensions?.code })\n const result = response.data?.saveFigure as FigureProductRecord | undefined\n if (!result?.id || !result.source) throw Error('서버가 저장 결과를 돌려주지 않았습니다.')\n return result\n },\n updateCatalog: (id, patch, seen) => updateFigure(id, patch, seen)\n}\n"]}
|
|
@@ -48,6 +48,11 @@ export declare function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x'
|
|
|
48
48
|
asks: boolean;
|
|
49
49
|
form: MirrorForm;
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* The clips whose tracks come with a mirrored copy (figure-model's mirror-part, review 2026-11-09): those that play a
|
|
53
|
+
* control of the part, or of what hangs from it when the chain is taken. Said in the form before the copy is made.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mirroredClipsOf(asset: FigureAsset, part: string, chain: boolean): string[];
|
|
51
56
|
/** The one edit the form comes to. */
|
|
52
57
|
export declare function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit;
|
|
53
58
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { figureHangingChainOf, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model';
|
|
1
|
+
import { figureHangingChainOf, figureMotionsOf, figureOccupiedBoxesOf, figureWritersOf, motionStateIdOf } from '@hatiolab/figure-model';
|
|
2
2
|
import { derivedOf } from './revision.js';
|
|
3
3
|
const AXES = ['x', 'y', 'z'];
|
|
4
4
|
/**
|
|
@@ -79,6 +79,15 @@ export function mirrorPlanOf(asset, part, axis) {
|
|
|
79
79
|
form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The clips whose tracks come with a mirrored copy (figure-model's mirror-part, review 2026-11-09): those that play a
|
|
84
|
+
* control of the part, or of what hangs from it when the chain is taken. Said in the form before the copy is made.
|
|
85
|
+
*/
|
|
86
|
+
export function mirroredClipsOf(asset, part, chain) {
|
|
87
|
+
const parts = chain ? figureHangingChainOf(asset, part) : [part];
|
|
88
|
+
const controls = new Set((asset.joints ?? []).filter(j => parts.includes(j.body1.part)).flatMap(j => figureMotionsOf(j).filter(m => !m.mimic).map(m => motionStateIdOf(m))));
|
|
89
|
+
return [...new Set((asset.drivers ?? []).filter(d => controls.has(d.state)).map(d => d.clip))];
|
|
90
|
+
}
|
|
82
91
|
/** The one edit the form comes to. */
|
|
83
92
|
export function mirrorEditOf(part, form) {
|
|
84
93
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureHangingChainOf, figureOccupiedBoxesOf, figureWritersOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * snap to the grid; and whether a mirrored copy is offered.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(revision)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy\n * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.\n */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean {\n const box = boxOf(revision, part)\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */\nexport interface MirrorForm {\n axis: 'x' | 'z'\n chain: boolean\n /** The beginning swapped in the copies' names (\\`fl\\` → \\`fr\\`); empty, the copies take \\`-copy\\`. */\n from: string\n to: string\n keepMotion: boolean\n}\n\n/* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */\nconst SWAPS: Record<'x' | 'z', Record<string, string>> = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } }\n\n/**\n * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them\n * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\\`fl-hip\\` across Z\n * is \\`bl-\\`, across X \\`fr-\\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing\n * moving asks nothing.\n */\nexport function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): { hanging: string[]; moves: boolean; asks: boolean; form: MirrorForm } {\n const chain = figureHangingChainOf(asset, part)\n const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length)\n const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? ''\n const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('')\n const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`))\n const hanging = chain.slice(1)\n return {\n hanging,\n moves,\n asks: hanging.length > 0 || moves,\n form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }\n }\n}\n\n/** The one edit the form comes to. */\nexport function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit {\n return {\n kind: 'mirror-part',\n target: part,\n axis: form.axis,\n ...(form.chain ? { chain: true } : {}),\n ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),\n ...(form.keepMotion ? { keepMotion: true } : {})\n }\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"align.js","sourceRoot":"","sources":["../../client/modeller/align.ts"],"names":[],"mappings":"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","sourcesContent":["import { figureHangingChainOf, figureMotionsOf, figureOccupiedBoxesOf, figureWritersOf, motionStateIdOf } from '@hatiolab/figure-model'\nimport type { FigureAsset, FigureInputChange, FigurePartEdit, FigureWorldBox } from '@hatiolab/figure-model'\n\nimport { derivedOf, type FigureRevision } from './revision.js'\n\nimport type { GizmoReach } from './gizmo.js'\n\n/**\n * 맞추기 — the align tools on a part: centre on X, centre on Z, stand on the floor,\n * snap to the grid; and whether a mirrored copy is offered.\n *\n * The asset's frame has the floor at y 0 and X and Z centred on 0, so centring puts the part's box centre on 0 and the floor puts its lowest point on\n * y 0. The part's box is its evaluated, rotated extent (`figureOccupiedBoxesOf`), so a turned part lands by what it\n * actually covers, not by its unturned size.\n *\n * Every tool writes the part's own position (and, for snap, size) inputs — the same ones the gizmo writes, and only\n * where the gizmo could (`GizmoReach`).\n */\n/* The plane tool is figure-model's now, by placement (`figurePlaneAlignChanges`); these stay the screen's. */\nexport type AlignTool = 'centre-x' | 'centre-z' | 'snap'\n\nconst AXES = ['x', 'y', 'z'] as const\n\n/**\n * Every part's box at this version, as drawn: the pose set with the controls included, as the canvas shows it and as a\n * command sees it (a fastening keeps a part where it is drawn).\n */\nexport function boxesOf(revision: FigureRevision): FigureWorldBox[] {\n return derivedOf(revision, 'boxes', () => figureOccupiedBoxesOf(revision.geometry, figureWritersOf(revision.asset)))\n}\n\nexport function boxOf(revision: FigureRevision, part: string): FigureWorldBox | undefined {\n return boxesOf(revision).find(box => box.part === part)\n}\n\n/** The snapping grid: about five steps across the figure, on 1 · 2 · 5 multiples. */\nexport function gridStepOf(revision: FigureRevision): number {\n const boxes = boxesOf(revision)\n if (!boxes.length) return 1\n const span = Math.max(\n ...AXES.map(axis => {\n const lo = Math.min(...boxes.map(b => b.centre[axis] - b.extent[axis] / 2))\n const hi = Math.max(...boxes.map(b => b.centre[axis] + b.extent[axis] / 2))\n return hi - lo\n })\n )\n return niceStep(span)\n}\n\nexport function alignChanges(revision: FigureRevision, part: string, reach: GizmoReach, tool: AlignTool): FigureInputChange[] {\n if (!reach.translate) return []\n const box = boxOf(revision, part)\n if (!box) return []\n const value = (id: string) => revision.asset.designInputs[id] ?? 0\n const set = (input: string, next: number): FigureInputChange => ({ kind: 'set-design', input, unit: 'mm', value: Math.round(next) })\n const at = (axis: 'x' | 'y' | 'z') => `${part}.position.${axis}`\n switch (tool) {\n case 'centre-x':\n return [set(at('x'), value(at('x')) - box.centre.x)]\n case 'centre-z':\n return [set(at('z'), value(at('z')) - box.centre.z)]\n case 'snap': {\n const step = gridStepOf(revision)\n const round = (n: number) => Math.round(n / step) * step\n const changes = AXES.map(axis => set(at(axis), round(value(at(axis)))))\n /* A size never snaps to 0 — the part would look gone. */\n if (reach.scale && reach.sizes)\n for (const id of reach.sizes) changes.push(set(id, Math.max(step, round(value(id)))))\n return changes\n }\n }\n}\n\n/**\n * Mirroring a part that already sits on the centre plane would put the copy inside it, so it is not offered. The copy\n * itself is figure-model's `mirror-part`, which places it -- fastened or not -- and refuses what it cannot mirror.\n */\nexport function canMirror(revision: FigureRevision, part: string, axis: 'x' | 'z'): boolean {\n const box = boxOf(revision, part)\n return !!box && Math.abs(box.centre[axis]) >= 0.5\n}\n\n/** How a mirrored copy is made: with what hangs from the part, under which names, turning as a mirror or as the original. */\nexport interface MirrorForm {\n axis: 'x' | 'z'\n chain: boolean\n /** The beginning swapped in the copies' names (\\`fl\\` → \\`fr\\`); empty, the copies take \\`-copy\\`. */\n from: string\n to: string\n keepMotion: boolean\n}\n\n/* The beginning a mirror across each axis swaps, by the figure's faces: X is its left and right, Z its front and back. */\nconst SWAPS: Record<'x' | 'z', Record<string, string>> = { x: { l: 'r', r: 'l' }, z: { f: 'b', b: 'f' } }\n\n/**\n * What a mirror of this part asks before it is made: the parts hanging from it (a leg from its hip), whether any of them\n * moves, and the form started -- the chain taken, the names' beginning guessed from the part's own (\\`fl-hip\\` across Z\n * is \\`bl-\\`, across X \\`fr-\\`, by the figure's faces), the motion mirrored. A part with nothing hanging and nothing\n * moving asks nothing.\n */\nexport function mirrorPlanOf(asset: FigureAsset, part: string, axis: 'x' | 'z'): { hanging: string[]; moves: boolean; asks: boolean; form: MirrorForm } {\n const chain = figureHangingChainOf(asset, part)\n const moves = (asset.joints ?? []).some(j => chain.includes(j.body1.part) && !!j.motion?.length)\n const head = /^([a-zA-Z]{1,3})-/.exec(part)?.[1] ?? ''\n const swapped = [...head].map(c => SWAPS[axis][c.toLowerCase()] ?? c).join('')\n const named = head && swapped !== head && chain.every(p => p.startsWith(`${head}-`))\n const hanging = chain.slice(1)\n return {\n hanging,\n moves,\n asks: hanging.length > 0 || moves,\n form: { axis, chain: true, from: named ? `${head}-` : '', to: named ? `${swapped}-` : '', keepMotion: false }\n }\n}\n\n/**\n * The clips whose tracks come with a mirrored copy (figure-model's mirror-part, review 2026-11-09): those that play a\n * control of the part, or of what hangs from it when the chain is taken. Said in the form before the copy is made.\n */\nexport function mirroredClipsOf(asset: FigureAsset, part: string, chain: boolean): string[] {\n const parts = chain ? figureHangingChainOf(asset, part) : [part]\n const controls = new Set(\n (asset.joints ?? []).filter(j => parts.includes(j.body1.part)).flatMap(j => figureMotionsOf(j).filter(m => !m.mimic).map(m => motionStateIdOf(m)))\n )\n return [...new Set((asset.drivers ?? []).filter(d => controls.has(d.state)).map(d => d.clip))]\n}\n\n/** The one edit the form comes to. */\nexport function mirrorEditOf(part: string, form: MirrorForm): FigurePartEdit {\n return {\n kind: 'mirror-part',\n target: part,\n axis: form.axis,\n ...(form.chain ? { chain: true } : {}),\n ...(form.from && form.to && form.from !== form.to ? { prefix: { from: form.from, to: form.to } } : {}),\n ...(form.keepMotion ? { keepMotion: true } : {})\n }\n}\n\n/**\n * A **readable** grid step for a length: about five cells, rounded up to a 1, 2 or 5 multiple as rulers and charts do.\n * Divided plainly it gives 27 or 33, and coordinates snapped to that read as 54 and 81. Never 0.\n */\nexport function niceStep(span: number): number {\n const raw = Math.max(span, 1) / 5\n const magnitude = Math.pow(10, Math.floor(Math.log10(raw)))\n const n = raw / magnitude\n const multiple = n < 1.5 ? 1 : n < 3.5 ? 2 : n < 7.5 ? 5 : 10\n return Math.max(1, multiple * magnitude)\n}\n"]}
|
|
@@ -10,7 +10,7 @@ export interface ClipTrack {
|
|
|
10
10
|
min: number;
|
|
11
11
|
max: number;
|
|
12
12
|
keys: ClipKey[];
|
|
13
|
-
interpolation: 'linear' | 'step';
|
|
13
|
+
interpolation: 'linear' | 'step' | 'smooth';
|
|
14
14
|
loop: 'wrap' | 'hold';
|
|
15
15
|
accumulates: boolean;
|
|
16
16
|
}
|
|
@@ -67,6 +67,11 @@ export declare function withoutKey(keys: readonly ClipKey[], index: number): Cli
|
|
|
67
67
|
export declare function freeClipNameOf(asset: FigureAsset, clip: string): string;
|
|
68
68
|
/** The controls a track of `clip` can be copied onto: those it plays already, and those it may take. */
|
|
69
69
|
export declare const copyTargetsOf: (asset: FigureAsset, clip: string) => string[];
|
|
70
|
+
/**
|
|
71
|
+
* Where a smooth track may be moved round its loop to, in seconds later, when `seconds` is not one of them: figure-model
|
|
72
|
+
* moves a smooth track only so that its new start is one of its keys (`driver-shift-off-key`). Null when it may go there.
|
|
73
|
+
*/
|
|
74
|
+
export declare function shiftOffKeyOf(track: ClipTrack, duration: number, seconds: number): number[] | null;
|
|
70
75
|
/** Whether a track can be moved round its loop: one that wraps and does not add up (figure-model's rule). */
|
|
71
76
|
export declare const shiftable: (track: ClipTrack) => boolean;
|
|
72
77
|
/**
|
|
@@ -77,9 +82,12 @@ export declare function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: C
|
|
|
77
82
|
kind: "set-driver";
|
|
78
83
|
state: string;
|
|
79
84
|
clip: string;
|
|
80
|
-
keys:
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
keys: {
|
|
86
|
+
seconds: number;
|
|
87
|
+
value: number;
|
|
88
|
+
}[];
|
|
89
|
+
loop: "hold" | "wrap";
|
|
90
|
+
interpolation: "linear" | "step" | "smooth";
|
|
83
91
|
accumulates: boolean;
|
|
84
92
|
};
|
|
85
93
|
/** The edit that writes a track back. */
|
|
@@ -88,6 +96,50 @@ export declare const trackEdit: (clip: Clip, track: ClipTrack, patch?: Partial<P
|
|
|
88
96
|
state: string;
|
|
89
97
|
clip: string;
|
|
90
98
|
keys: ClipKey[];
|
|
91
|
-
loop: "
|
|
92
|
-
interpolation: "step" | "
|
|
99
|
+
loop: "hold" | "wrap";
|
|
100
|
+
interpolation: "linear" | "step" | "smooth";
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* A new track's keys. By default it holds the pose the figure stands in now, from start to end: a track added to a pose
|
|
104
|
+
* made with care does not throw it to the control's ends (review 2026-11-09). `range` is the preset that swings the
|
|
105
|
+
* control over its whole range and back, so the loop closes where it began.
|
|
106
|
+
*/
|
|
107
|
+
export declare function newTrackKeys(input: {
|
|
108
|
+
min?: number;
|
|
109
|
+
max?: number;
|
|
110
|
+
}, current: number | undefined, duration: number, how?: 'hold' | 'range'): ClipKey[];
|
|
111
|
+
/** What a new clip on a control joins, so that it may play it: nothing, a group already there, or a group made with the clip that plays it now. */
|
|
112
|
+
export type TrackJoin = {
|
|
113
|
+
kind: 'free';
|
|
114
|
+
} | {
|
|
115
|
+
kind: 'group';
|
|
116
|
+
group: string;
|
|
117
|
+
label: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'new-group';
|
|
120
|
+
group: string;
|
|
121
|
+
label: string;
|
|
122
|
+
clip: string;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The edits that make a new clip `clip` with one track on `state` (ADR-0051 §3.2: only clips of one motion group play
|
|
126
|
+
* one control). A control no clip plays is taken as it is. A control a group's clips play puts the new clip in that
|
|
127
|
+
* group. A control one clip in no group plays makes a group of that clip and the new one -- the person adds 「열기」 to a
|
|
128
|
+
* door that has 「닫기」, and the two become one group, one playing at a time, without having to design the group first.
|
|
129
|
+
* All of it one step of the history.
|
|
130
|
+
*/
|
|
131
|
+
export declare function newClipEdits(asset: FigureAsset, state: string, clip: string, duration: number, keys: ClipKey[]): {
|
|
132
|
+
edits: unknown[];
|
|
133
|
+
join: TrackJoin;
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* 「이 자세 기억하기」: the pose the figure stands in now, kept in `clip` at `seconds` in one step (review 2026-11-09:
|
|
137
|
+
* a six-axis pose was six tracks keyed one by one). A track whose value there differs gets a key; a control the clip
|
|
138
|
+
* does not play yet and that moved since `base` -- the pose the clip was last shown or kept in -- gets a track that
|
|
139
|
+
* starts from where it was and reaches the pose at that moment (from the start when the moment is 0). A moved control
|
|
140
|
+
* another clip outside this clip's group plays is not taken; it is named in `skipped`.
|
|
141
|
+
*/
|
|
142
|
+
export declare function rememberedPoseEdits(asset: FigureAsset, clip: string, seconds: number, values: Record<string, number>, base: Record<string, number>): {
|
|
143
|
+
edits: unknown[];
|
|
144
|
+
skipped: string[];
|
|
93
145
|
};
|