@things-factory/figure-ui 10.1.178 → 10.1.179
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/figure-released.ts +6 -0
- package/client/graphql/index.ts +101 -25
- package/client/graphql/store.ts +2 -1
- package/client/index.ts +1 -0
- package/client/modeller/editor.ts +10 -2
- package/client/modeller/figure-first-save.ts +23 -3
- package/client/modeller/figure-parts.ts +53 -23
- package/client/modeller/figure-properties.ts +41 -7
- package/client/modeller/first-save.ts +2 -0
- package/client/modeller/product-session.ts +5 -3
- package/client/modeller/property-sections.ts +45 -0
- package/client/pages/figure-list-page.ts +52 -10
- package/client/pages/figure-modeller-page.ts +15 -3
- package/client/setting-let/figure-namespace-setting-let.ts +113 -31
- package/client/types.ts +8 -0
- package/client/viewparts/figure-detail-dock.ts +124 -9
- package/client/viewparts/figure-thumb.ts +6 -3
- package/dist-client/figure-released.d.ts +3 -0
- package/dist-client/figure-released.js +5 -0
- package/dist-client/figure-released.js.map +1 -0
- package/dist-client/graphql/index.d.ts +25 -13
- package/dist-client/graphql/index.js +85 -16
- package/dist-client/graphql/index.js.map +1 -1
- package/dist-client/graphql/store.js +2 -1
- package/dist-client/graphql/store.js.map +1 -1
- 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/editor.d.ts +2 -0
- package/dist-client/modeller/editor.js +13 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-first-save.d.ts +8 -0
- package/dist-client/modeller/figure-first-save.js +28 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-parts.d.ts +5 -0
- package/dist-client/modeller/figure-parts.js +54 -22
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-properties.d.ts +4 -0
- package/dist-client/modeller/figure-properties.js +45 -7
- package/dist-client/modeller/figure-properties.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +2 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/product-session.d.ts +4 -1
- package/dist-client/modeller/product-session.js +3 -3
- package/dist-client/modeller/product-session.js.map +1 -1
- package/dist-client/modeller/property-sections.d.ts +26 -0
- package/dist-client/modeller/property-sections.js +17 -0
- package/dist-client/modeller/property-sections.js.map +1 -0
- package/dist-client/pages/figure-list-page.d.ts +4 -1
- package/dist-client/pages/figure-list-page.js +56 -10
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-modeller-page.d.ts +2 -0
- package/dist-client/pages/figure-modeller-page.js +19 -3
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/setting-let/figure-namespace-setting-let.d.ts +16 -5
- package/dist-client/setting-let/figure-namespace-setting-let.js +129 -31
- package/dist-client/setting-let/figure-namespace-setting-let.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/types.d.ts +12 -0
- package/dist-client/types.js.map +1 -1
- package/dist-client/viewparts/figure-detail-dock.d.ts +33 -2
- package/dist-client/viewparts/figure-detail-dock.js +171 -11
- package/dist-client/viewparts/figure-detail-dock.js.map +1 -1
- package/dist-client/viewparts/figure-thumb.d.ts +2 -0
- package/dist-client/viewparts/figure-thumb.js +13 -3
- package/dist-client/viewparts/figure-thumb.js.map +1 -1
- package/package.json +8 -7
- package/test/browser/category-page.js +28 -0
- package/test/browser/category.check.mjs +54 -0
- package/test/browser/mesh-select.check.mjs +1 -1
- package/test/browser/namespace-page.js +75 -0
- package/test/browser/namespace.check.mjs +104 -0
- package/test/browser/parts-page.js +8 -2
- package/test/browser/places-mesh.check.mjs +30 -2
- package/test/browser/property-section-page.js +45 -0
- package/test/browser/property-section.check.mjs +48 -0
- package/test/browser/props.check.mjs +13 -2
- package/test/browser/public-page.js +39 -0
- package/test/browser/public.check.mjs +79 -0
- package/test/browser/retire-page.js +37 -0
- package/test/browser/retire.check.mjs +96 -0
- package/test/figure-released.test.ts +54 -0
- package/test/modeller-route.test.ts +2 -1
- package/translations/en.json +20 -5
- package/translations/es.json +20 -5
- package/translations/ja.json +20 -5
- package/translations/ko.json +20 -5
- package/translations/ms.json +20 -5
- package/translations/zh.json +20 -5
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Figure } from './types.js'
|
|
2
|
+
|
|
3
|
+
/** Whether a figure has ever been released: such a figure is retired, never deleted (the server refuses it too). */
|
|
4
|
+
export function hasBeenReleased(figure: Pick<Figure, 'version'>): boolean {
|
|
5
|
+
return Number(figure.version) > 0
|
|
6
|
+
}
|
package/client/graphql/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import gql from 'graphql-tag'
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
|
-
import { client } from '@operato/graphql'
|
|
3
|
+
import { client, uploadContext, type UploadProgress } from '@operato/graphql'
|
|
4
4
|
|
|
5
5
|
import { reached, refusedIn } from './reached.js'
|
|
6
6
|
|
|
@@ -57,6 +57,9 @@ const FIGURE_LIST_FIELDS = `
|
|
|
57
57
|
tags
|
|
58
58
|
state
|
|
59
59
|
version
|
|
60
|
+
deprecated
|
|
61
|
+
supersededBy
|
|
62
|
+
public
|
|
60
63
|
score
|
|
61
64
|
triangles
|
|
62
65
|
groups
|
|
@@ -77,12 +80,15 @@ export async function fetchFigureList(params: {
|
|
|
77
80
|
limit?: number
|
|
78
81
|
filters?: unknown[]
|
|
79
82
|
sortings?: { name: string; desc?: boolean }[]
|
|
83
|
+
/** 'public': other domains' public figures, with their domain (LV figure-server design G2). The domain's own by default. */
|
|
84
|
+
scope?: 'own' | 'public'
|
|
80
85
|
}): Promise<FigureListResult> {
|
|
86
|
+
const field = params.scope === 'public' ? 'publicFigures' : 'figures'
|
|
81
87
|
const response = await client.query({
|
|
82
88
|
query: gql`
|
|
83
89
|
query ($filters: [Filter!], $pagination: Pagination, $sortings: [Sorting!]) {
|
|
84
|
-
|
|
85
|
-
items { ${FIGURE_LIST_FIELDS} }
|
|
90
|
+
${field}(filters: $filters, pagination: $pagination, sortings: $sortings) {
|
|
91
|
+
items { ${FIGURE_LIST_FIELDS} ${params.scope === 'public' ? 'domain { id name subdomain }' : ''} }
|
|
86
92
|
total
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -95,7 +101,7 @@ export async function fetchFigureList(params: {
|
|
|
95
101
|
fetchPolicy: 'network-only'
|
|
96
102
|
})
|
|
97
103
|
|
|
98
|
-
return unwrap<FigureListResult>(response,
|
|
104
|
+
return unwrap<FigureListResult>(response, field)
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
/** 저작면에 필요한 전부 — `source` 를 포함한다. */
|
|
@@ -174,6 +180,38 @@ export async function updateFigure(id: string, patch: FigurePatch): Promise<Save
|
|
|
174
180
|
return unwrap<SaveResult>(response, 'updateFigure')
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
/** Makes a released figure public to other domains, or private again (LV figure-server design G2). */
|
|
184
|
+
export async function setFigurePublic(id: string, isPublic: boolean): Promise<Figure> {
|
|
185
|
+
const response = await client.mutate({
|
|
186
|
+
mutation: gql`
|
|
187
|
+
mutation ($id: String!, $public: Boolean!) {
|
|
188
|
+
setFigurePublic(id: $id, public: $public) {
|
|
189
|
+
${FIGURE_LIST_FIELDS}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
`,
|
|
193
|
+
variables: { id, public: isPublic }
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
return unwrap<Figure>(response, 'setFigurePublic')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Retires a released figure, naming the type to use instead, or brings it back. Release rows are not touched. */
|
|
200
|
+
export async function setFigureDeprecation(id: string, deprecated: boolean, supersededBy?: string | null): Promise<Figure> {
|
|
201
|
+
const response = await client.mutate({
|
|
202
|
+
mutation: gql`
|
|
203
|
+
mutation ($id: String!, $deprecated: Boolean!, $supersededBy: String) {
|
|
204
|
+
setFigureDeprecation(id: $id, deprecated: $deprecated, supersededBy: $supersededBy) {
|
|
205
|
+
${FIGURE_LIST_FIELDS}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
`,
|
|
209
|
+
variables: { id, deprecated, supersededBy: supersededBy || null }
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
return unwrap<Figure>(response, 'setFigureDeprecation')
|
|
213
|
+
}
|
|
214
|
+
|
|
177
215
|
export async function deleteFigure(id: string): Promise<boolean> {
|
|
178
216
|
const response = await client.mutate({
|
|
179
217
|
mutation: gql`
|
|
@@ -211,42 +249,80 @@ export async function releaseFigure(id: string, comment?: string): Promise<Figur
|
|
|
211
249
|
return unwrap<Figure>(response, 'releaseFigure')
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
/** A domain
|
|
215
|
-
export interface
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
/** The domain's figures carry it: it cannot be set any more. */
|
|
222
|
-
fixed: boolean
|
|
252
|
+
/** A namespace the domain owns (LV figure-server design G1): the `{namespace}` of the full name `{namespace}/{type}`. */
|
|
253
|
+
export interface FigureNamespace {
|
|
254
|
+
id: string
|
|
255
|
+
name: string
|
|
256
|
+
description?: string | null
|
|
257
|
+
state: 'active' | 'suspended'
|
|
258
|
+
isDefault: boolean
|
|
223
259
|
}
|
|
224
260
|
|
|
225
|
-
const FIGURE_NAMESPACE_FIELDS = '
|
|
261
|
+
const FIGURE_NAMESPACE_FIELDS = 'id name description state isDefault'
|
|
226
262
|
|
|
227
|
-
|
|
263
|
+
/** The categories the server allows, or null when a category is free text (LV figure-server design G5). */
|
|
264
|
+
export async function fetchFigureCategories(): Promise<string[] | null> {
|
|
228
265
|
const response = await client.query({
|
|
229
266
|
query: gql`
|
|
230
267
|
query {
|
|
231
|
-
|
|
268
|
+
figureCategories
|
|
232
269
|
}
|
|
233
270
|
`,
|
|
234
271
|
fetchPolicy: 'network-only'
|
|
235
272
|
})
|
|
236
|
-
return unwrap<
|
|
273
|
+
return unwrap<string[] | null>(response, 'figureCategories') ?? null
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The domain's namespaces, its default first. */
|
|
277
|
+
export async function fetchFigureNamespaces(): Promise<FigureNamespace[]> {
|
|
278
|
+
const response = await client.query({
|
|
279
|
+
query: gql`
|
|
280
|
+
query {
|
|
281
|
+
figureNamespaces { ${FIGURE_NAMESPACE_FIELDS} }
|
|
282
|
+
}
|
|
283
|
+
`,
|
|
284
|
+
fetchPolicy: 'network-only'
|
|
285
|
+
})
|
|
286
|
+
return unwrap<FigureNamespace[]>(response, 'figureNamespaces') ?? []
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Adds a namespace to the domain; the server says why when it refuses (not a namespace, another domain's). */
|
|
290
|
+
export async function createFigureNamespace(name: string, isDefault = false): Promise<FigureNamespace> {
|
|
291
|
+
const response = await client.mutate({
|
|
292
|
+
mutation: gql`
|
|
293
|
+
mutation ($name: String!, $isDefault: Boolean) {
|
|
294
|
+
createFigureNamespace(name: $name, isDefault: $isDefault) { ${FIGURE_NAMESPACE_FIELDS} }
|
|
295
|
+
}
|
|
296
|
+
`,
|
|
297
|
+
variables: { name, isDefault }
|
|
298
|
+
})
|
|
299
|
+
return unwrap<FigureNamespace>(response, 'createFigureNamespace')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Makes one of the domain's active namespaces its default. */
|
|
303
|
+
export async function setDefaultFigureNamespace(name: string): Promise<FigureNamespace> {
|
|
304
|
+
const response = await client.mutate({
|
|
305
|
+
mutation: gql`
|
|
306
|
+
mutation ($name: String!) {
|
|
307
|
+
setDefaultFigureNamespace(name: $name) { ${FIGURE_NAMESPACE_FIELDS} }
|
|
308
|
+
}
|
|
309
|
+
`,
|
|
310
|
+
variables: { name }
|
|
311
|
+
})
|
|
312
|
+
return unwrap<FigureNamespace>(response, 'setDefaultFigureNamespace')
|
|
237
313
|
}
|
|
238
314
|
|
|
239
|
-
/**
|
|
240
|
-
export async function
|
|
315
|
+
/** Suspends a namespace or makes it active again. */
|
|
316
|
+
export async function setFigureNamespaceState(name: string, state: 'active' | 'suspended'): Promise<FigureNamespace> {
|
|
241
317
|
const response = await client.mutate({
|
|
242
318
|
mutation: gql`
|
|
243
|
-
mutation ($
|
|
244
|
-
|
|
319
|
+
mutation ($name: String!, $state: String!) {
|
|
320
|
+
setFigureNamespaceState(name: $name, state: $state) { ${FIGURE_NAMESPACE_FIELDS} }
|
|
245
321
|
}
|
|
246
322
|
`,
|
|
247
|
-
variables: {
|
|
323
|
+
variables: { name, state }
|
|
248
324
|
})
|
|
249
|
-
return unwrap<
|
|
325
|
+
return unwrap<FigureNamespace>(response, 'setFigureNamespaceState')
|
|
250
326
|
}
|
|
251
327
|
|
|
252
328
|
/** A draft a revert kept when it wrote over it (ADR-0097 §5.6). Not a released version. */
|
|
@@ -518,7 +594,7 @@ export async function fetchRecipeGuide(name: string, lang: string): Promise<stri
|
|
|
518
594
|
* 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
|
|
519
595
|
* cannot draw is refused with the reason in the screen's words.
|
|
520
596
|
*/
|
|
521
|
-
export async function uploadFigureMesh(file: File): Promise<import('../modeller/mesh-import.js').MeshUpload> {
|
|
597
|
+
export async function uploadFigureMesh(file: File, onProgress: UploadProgress): Promise<import('../modeller/mesh-import.js').MeshUpload> {
|
|
522
598
|
const response = await client.mutate({
|
|
523
599
|
mutation: gql`
|
|
524
600
|
mutation UploadFigureMesh($file: Upload!) {
|
|
@@ -529,7 +605,7 @@ export async function uploadFigureMesh(file: File): Promise<import('../modeller/
|
|
|
529
605
|
}
|
|
530
606
|
`,
|
|
531
607
|
variables: { file },
|
|
532
|
-
context:
|
|
608
|
+
context: uploadContext(onProgress)
|
|
533
609
|
})
|
|
534
610
|
return unwrap(response, 'uploadFigureMesh')
|
|
535
611
|
}
|
package/client/graphql/store.ts
CHANGED
|
@@ -34,7 +34,7 @@ export const figureProductStore: FigureProductStore = {
|
|
|
34
34
|
async save(request) {
|
|
35
35
|
const response = await reached(client.mutate({
|
|
36
36
|
mutation: gql`
|
|
37
|
-
mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String, $proposalKeys: [String!]) {
|
|
37
|
+
mutation SaveFigure($source: String!, $id: String, $expectedSource: String, $creationId: String, $thumbnail: String, $type: String, $proposalKeys: [String!], $namespace: String) {
|
|
38
38
|
saveFigure(
|
|
39
39
|
source: $source
|
|
40
40
|
id: $id
|
|
@@ -43,6 +43,7 @@ export const figureProductStore: FigureProductStore = {
|
|
|
43
43
|
thumbnail: $thumbnail
|
|
44
44
|
type: $type
|
|
45
45
|
proposalKeys: $proposalKeys
|
|
46
|
+
namespace: $namespace
|
|
46
47
|
) {
|
|
47
48
|
id
|
|
48
49
|
source
|
package/client/index.ts
CHANGED
|
@@ -124,6 +124,8 @@ export class FigureEditor extends LitElement {
|
|
|
124
124
|
@property({ attribute: false }) firstSave?: (name: string) => Promise<FirstSave | undefined>
|
|
125
125
|
@state() private selected = ''
|
|
126
126
|
@state() private error = ''
|
|
127
|
+
/** How far the GLB being brought in has gone up (0‒1), while it does. */
|
|
128
|
+
@state() private meshProgress?: number
|
|
127
129
|
/*
|
|
128
130
|
* An edit that takes a face away which something stands on (ADR-0100 A8): held, with what the kernel says stands
|
|
129
131
|
* there, until the author chooses where each goes -- then the same edit goes again with the remap. Shown where the
|
|
@@ -392,6 +394,7 @@ export class FigureEditor extends LitElement {
|
|
|
392
394
|
be taken is not an error: the save goes on without it (see `thumbnail.ts`).
|
|
393
395
|
*/
|
|
394
396
|
let type: string | undefined
|
|
397
|
+
let namespace: string | undefined
|
|
395
398
|
if (!this.session.read().id && this.firstSave) {
|
|
396
399
|
const answer = await this.firstSave(this.session.read().name ?? '')
|
|
397
400
|
if (!answer) return
|
|
@@ -401,10 +404,11 @@ export class FigureEditor extends LitElement {
|
|
|
401
404
|
this.changed()
|
|
402
405
|
}
|
|
403
406
|
type = answer.type
|
|
407
|
+
namespace = answer.namespace
|
|
404
408
|
}
|
|
405
409
|
const view = this.session.read()
|
|
406
410
|
const thumbnail = await captureFigureThumbnail(drawableFigureParts(view.geometry ?? []), view.palette ?? {})
|
|
407
|
-
const promise = this.session.save({ thumbnail, type })
|
|
411
|
+
const promise = this.session.save({ thumbnail, type, namespace })
|
|
408
412
|
this.changed()
|
|
409
413
|
await promise
|
|
410
414
|
this.error = ''
|
|
@@ -432,11 +436,14 @@ export class FigureEditor extends LitElement {
|
|
|
432
436
|
private async importMesh(file: File) {
|
|
433
437
|
if (!this.session) return
|
|
434
438
|
let upload: Awaited<ReturnType<typeof uploadFigureMesh>>
|
|
439
|
+
this.meshProgress = 0
|
|
435
440
|
try {
|
|
436
|
-
upload = await uploadFigureMesh(file)
|
|
441
|
+
upload = await uploadFigureMesh(file, fraction => (this.meshProgress = fraction))
|
|
437
442
|
} catch (error) {
|
|
438
443
|
this.error = this.said(error)
|
|
439
444
|
return
|
|
445
|
+
} finally {
|
|
446
|
+
this.meshProgress = undefined
|
|
440
447
|
}
|
|
441
448
|
const choices = meshChoicesOf(upload)
|
|
442
449
|
/* Only the whole file and the one node that is all of it: there is nothing to choose between. */
|
|
@@ -1710,6 +1717,7 @@ export class FigureEditor extends LitElement {
|
|
|
1710
1717
|
?locked=${unavailable}
|
|
1711
1718
|
@add-part=${(e: CustomEvent) => this.add(e.detail.kind)}
|
|
1712
1719
|
@import-mesh=${(e: CustomEvent) => this.importMesh(e.detail.file)}
|
|
1720
|
+
.meshProgress=${this.meshProgress}
|
|
1713
1721
|
.recipeCount=${this.templates.length}
|
|
1714
1722
|
@open-recipe-catalog=${() => ((this.catalogOpen = true), (this.recipeError = ''))}
|
|
1715
1723
|
@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
|
|
@@ -31,11 +31,12 @@ export class FigureFirstSave extends LitElement {
|
|
|
31
31
|
font-size: var(--popup-font-size-small);
|
|
32
32
|
color: var(--md-sys-color-on-surface-variant);
|
|
33
33
|
}
|
|
34
|
-
div[field] input
|
|
34
|
+
div[field] input,
|
|
35
|
+
div[field] select {
|
|
35
36
|
box-sizing: border-box;
|
|
36
37
|
width: 100%;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
[type-field] {
|
|
39
40
|
font-family: var(--ui-mono, ui-monospace, monospace);
|
|
40
41
|
}
|
|
41
42
|
`
|
|
@@ -46,6 +47,9 @@ export class FigureFirstSave extends LitElement {
|
|
|
46
47
|
@property() name = ''
|
|
47
48
|
/** Type names already used in this domain. */
|
|
48
49
|
@property({ attribute: false }) taken: string[] = []
|
|
50
|
+
/** The domain's active namespaces, its default first. Asked only when there are several (LV figure-server design G1). */
|
|
51
|
+
@property({ attribute: false }) namespaces: { name: string; isDefault: boolean }[] = []
|
|
52
|
+
@state() private namespace = ''
|
|
49
53
|
@state() private refusal: { key: string; type?: string } | null = null
|
|
50
54
|
/** The identifier offered, while the author has not changed it. */
|
|
51
55
|
@state() private suggested = ''
|
|
@@ -105,6 +109,16 @@ export class FigureFirstSave extends LitElement {
|
|
|
105
109
|
? html`<div class="note">${i18next.t('figure.text.identifier-suggested')}</div>`
|
|
106
110
|
: nothing}
|
|
107
111
|
</div>
|
|
112
|
+
${this.namespaces.length > 1
|
|
113
|
+
? html`<div field>
|
|
114
|
+
<label for="namespace">${i18next.t('figure.label.namespace')}</label>
|
|
115
|
+
<select id="namespace" type-field @change=${(e: Event) => (this.namespace = (e.target as HTMLSelectElement).value)}>
|
|
116
|
+
${this.namespaces.map(
|
|
117
|
+
namespace => html`<option value=${namespace.name} ?selected=${namespace.name === this.chosenNamespace}>${namespace.name}</option>`
|
|
118
|
+
)}
|
|
119
|
+
</select>
|
|
120
|
+
</div>`
|
|
121
|
+
: nothing}
|
|
108
122
|
${this.refusal ? html`<div class="failure" role="alert">${i18next.t(this.refusal.key, { type: this.refusal.type })}</div>` : nothing}
|
|
109
123
|
<ox-popup-actions>
|
|
110
124
|
<md-text-button slot="cancel" @click=${() => closePopup(this)}>${i18next.t('figure.button.cancel')}</md-text-button>
|
|
@@ -120,6 +134,12 @@ export class FigureFirstSave extends LitElement {
|
|
|
120
134
|
private confirm = () => {
|
|
121
135
|
this.refusal = firstSaveRefusalOf(this.type, this.taken)
|
|
122
136
|
if (this.refusal) return
|
|
123
|
-
|
|
137
|
+
const namespace = this.namespaces.length > 1 ? this.chosenNamespace : undefined
|
|
138
|
+
closePopup(this, { type: this.type, name: this.name.trim() || this.type, ...(namespace ? { namespace } : {}) } satisfies FirstSave)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The namespace chosen, or the domain's default until one is. */
|
|
142
|
+
private get chosenNamespace(): string {
|
|
143
|
+
return this.namespace || this.namespaces.find(namespace => namespace.isDefault)?.name || this.namespaces[0]?.name || ''
|
|
124
144
|
}
|
|
125
145
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
|
+
import '@operato/input/ox-input-file.js'
|
|
2
3
|
import { css, html, LitElement, nothing } from 'lit'
|
|
3
|
-
import { customElement, property } from 'lit/decorators.js'
|
|
4
|
+
import { customElement, property, query } from 'lit/decorators.js'
|
|
4
5
|
import { i18next, localize } from '@operato/i18n'
|
|
5
6
|
import { ScrollbarStyles } from '@operato/styles'
|
|
6
7
|
import { DETAIL_LEVELS, PART_LIMIT, type DetailLevel } from '@hatiolab/figure-model'
|
|
@@ -263,13 +264,38 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
263
264
|
overflow: hidden;
|
|
264
265
|
text-overflow: ellipsis;
|
|
265
266
|
}
|
|
266
|
-
|
|
267
|
+
/*
|
|
268
|
+
The one file picker (ox-input-file, quiet) on that row: a GLB refused or going up is said on the spot. Its button
|
|
269
|
+
wears the tiles' words and lit hand -- the picker's own 13/600 stood louder than the shapes' heading.
|
|
270
|
+
*/
|
|
271
|
+
ox-input-file[mesh-file] {
|
|
267
272
|
grid-column: 1 / -1;
|
|
268
|
-
flex-direction: row;
|
|
269
273
|
justify-content: center;
|
|
274
|
+
font-size: var(--ui-t-micro, 11px);
|
|
275
|
+
--ox-input-file-action-height: 26px;
|
|
276
|
+
}
|
|
277
|
+
ox-input-file[mesh-file]::part(attach) {
|
|
270
278
|
gap: 6px;
|
|
271
|
-
min-height: 26px;
|
|
272
279
|
padding: 0 8px;
|
|
280
|
+
font: var(--label-font, inherit);
|
|
281
|
+
font-size: var(--ui-t-micro, 11px);
|
|
282
|
+
font-weight: 400;
|
|
283
|
+
color: var(--md-sys-color-on-surface-variant, var(--md-sys-color-on-surface));
|
|
284
|
+
border-radius: 4px;
|
|
285
|
+
}
|
|
286
|
+
ox-input-file[mesh-file]:not([disabled])::part(attach):hover {
|
|
287
|
+
color: var(--md-sys-color-on-primary-container);
|
|
288
|
+
background-color: var(--md-sys-color-primary-container);
|
|
289
|
+
}
|
|
290
|
+
ox-input-file[mesh-file][disabled]::part(attach) {
|
|
291
|
+
opacity: 0.35;
|
|
292
|
+
}
|
|
293
|
+
ox-input-file[mesh-file] md-icon {
|
|
294
|
+
--md-icon-size: 18px;
|
|
295
|
+
}
|
|
296
|
+
section[shapes].candrop {
|
|
297
|
+
outline: 1px dashed var(--md-sys-color-primary);
|
|
298
|
+
outline-offset: -1px;
|
|
273
299
|
}
|
|
274
300
|
/* An action in the side panel's look: icon and words in the primary colour, no pill. */
|
|
275
301
|
button[recipe-catalog] {
|
|
@@ -460,12 +486,22 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
460
486
|
@property({ attribute: false }) partBudget?: { level: DetailLevel | null; used: number; limit: number | null }
|
|
461
487
|
/** How many recipe templates the catalog holds; none, and the section is not drawn. */
|
|
462
488
|
@property({ type: Number }) recipeCount = 0
|
|
489
|
+
/** How far the GLB being brought in has gone up (0‒1), while it does; the picker shows it. */
|
|
490
|
+
@property({ type: Number }) meshProgress?: number
|
|
491
|
+
|
|
492
|
+
/* A GLB dragged over the shapes is a part brought in: the section is where it is dropped (page standard §7-1). */
|
|
493
|
+
@query('section[shapes]') private shapes?: HTMLElement
|
|
494
|
+
|
|
495
|
+
firstUpdated() {
|
|
496
|
+
/* The drop area is drawn by the first render; the picker is told it by the next. */
|
|
497
|
+
this.requestUpdate()
|
|
498
|
+
}
|
|
463
499
|
|
|
464
500
|
render() {
|
|
465
501
|
const b = this.partBudget
|
|
466
502
|
const full = !!b && b.limit !== null && b.used >= b.limit
|
|
467
503
|
return html`
|
|
468
|
-
<section>
|
|
504
|
+
<section shapes>
|
|
469
505
|
<h3>${i18next.t('figure.label.shapes')}</h3>
|
|
470
506
|
<div palette>
|
|
471
507
|
${RECIPES.map(
|
|
@@ -481,28 +517,22 @@ export class FigureParts extends localize(i18next)(LitElement) {
|
|
|
481
517
|
</button>
|
|
482
518
|
`
|
|
483
519
|
)}
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
?disabled=${this.locked || full}
|
|
488
|
-
title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t('figure.text.import-mesh-hint')}
|
|
489
|
-
@click=${() => this.renderRoot.querySelector<HTMLInputElement>('input[mesh-file]')?.click()}
|
|
490
|
-
>
|
|
491
|
-
<md-icon>view_in_ar</md-icon>
|
|
492
|
-
<span>${i18next.t('figure.button.import-mesh')}</span>
|
|
493
|
-
</button>
|
|
494
|
-
<input
|
|
520
|
+
<ox-input-file
|
|
521
|
+
quiet
|
|
522
|
+
hide-filelist
|
|
495
523
|
mesh-file
|
|
496
|
-
type="file"
|
|
497
524
|
accept=".glb,model/gltf-binary"
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
525
|
+
.label=${i18next.t('figure.button.import-mesh')}
|
|
526
|
+
title=${full ? i18next.t('figure.text.part-limit-reached') : i18next.t('figure.text.import-mesh-hint')}
|
|
527
|
+
?disabled=${this.locked || full}
|
|
528
|
+
.dropTarget=${this.shapes}
|
|
529
|
+
.progress=${this.meshProgress}
|
|
530
|
+
@change=${(e: CustomEvent) => {
|
|
531
|
+
const file = (e.detail as File[] | undefined)?.[0]
|
|
503
532
|
if (file) this.ask('import-mesh', { file })
|
|
533
|
+
;(e.currentTarget as HTMLElement & { reset(): void }).reset()
|
|
504
534
|
}}
|
|
505
|
-
|
|
535
|
+
><md-icon slot="icon">view_in_ar</md-icon></ox-input-file>
|
|
506
536
|
</div>
|
|
507
537
|
${full ? html`<p full>${i18next.t('figure.text.part-limit-reached-short', { level: b!.level, limit: b!.limit })}</p>` : nothing}
|
|
508
538
|
</section>
|
|
@@ -12,6 +12,7 @@ import { occupancyFromParts } from './occupancy.js'
|
|
|
12
12
|
import { tagsOf, figureFigureFacts, type Placement, type FigureFigureFacts } from './figure-facts.js'
|
|
13
13
|
import { offeredMountingOf } from './mounting-part.js'
|
|
14
14
|
import type { FigureRevision } from './revision.js'
|
|
15
|
+
import { figurePropertySections } from './property-sections.js'
|
|
15
16
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
16
17
|
import { ScrollbarStyles } from '@operato/styles'
|
|
17
18
|
|
|
@@ -65,6 +66,8 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
65
66
|
/** The saved row, if any. Category and tags live on it. */
|
|
66
67
|
@property({ type: String }) figureId?: string
|
|
67
68
|
@property({ type: String }) figureCategory?: string
|
|
69
|
+
/** The categories the server allows (LV figure-server design G5): chosen from, and fixed once set. Null: free text. */
|
|
70
|
+
@property({ attribute: false }) figureCategories?: string[] | null
|
|
68
71
|
@property({ type: Array }) figureTags?: string[]
|
|
69
72
|
/** The part and face picked for the mounting face, before 「이 면으로」 is pressed. */
|
|
70
73
|
@state() private pickedPart?: string
|
|
@@ -127,7 +130,8 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
127
130
|
<section>
|
|
128
131
|
<h3>${i18next.t('figure.label.group-catalog')}</h3>
|
|
129
132
|
${this.renderCatalog()}
|
|
130
|
-
</section
|
|
133
|
+
</section>
|
|
134
|
+
${this.renderAppSections()}`
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
private renderName(facts: FigureFigureFacts) {
|
|
@@ -226,17 +230,47 @@ export class FigureProperties extends localize(i18next)(LitElement) {
|
|
|
226
230
|
`
|
|
227
231
|
}
|
|
228
232
|
|
|
233
|
+
/** The sections an app added (G11), after the panel's own, for the figures they are for. A section that throws says so in its place. */
|
|
234
|
+
private renderAppSections() {
|
|
235
|
+
const asset = this.revision?.asset
|
|
236
|
+
if (!asset) return nothing
|
|
237
|
+
const figure = { id: this.figureId, category: this.figureCategory }
|
|
238
|
+
return figurePropertySections().map(section => {
|
|
239
|
+
try {
|
|
240
|
+
if (!section.when({ asset, figure })) return nothing
|
|
241
|
+
return html`<section data-section=${section.id}>
|
|
242
|
+
<h3>${section.title()}</h3>
|
|
243
|
+
${section.render({ asset, figure, command: action => this.command(action) })}
|
|
244
|
+
</section>`
|
|
245
|
+
} catch (error) {
|
|
246
|
+
console.error(`[figure-ui] the property section '${section.id}' failed`, error)
|
|
247
|
+
return html`<section data-section=${section.id}>
|
|
248
|
+
<p role="alert">${i18next.t('figure.text.property-section-failed', { section: section.id, reason: (error as Error)?.message ?? String(error) })}</p>
|
|
249
|
+
</section>`
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
229
254
|
private renderCatalog() {
|
|
230
255
|
const saved = !!this.figureId
|
|
231
256
|
return html`
|
|
232
257
|
<div field>
|
|
233
258
|
<label>${i18next.t('figure.label.category')}</label>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
${this.figureCategories?.length
|
|
260
|
+
? html`<select
|
|
261
|
+
category
|
|
262
|
+
?disabled=${!saved || !!this.figureCategory}
|
|
263
|
+
@change=${(e: Event) => this.catalog({ category: (e.target as HTMLSelectElement).value })}
|
|
264
|
+
>
|
|
265
|
+
${this.figureCategory ? nothing : html`<option value="" selected disabled>${i18next.t('figure.text.category-choose')}</option>`}
|
|
266
|
+
${this.figureCategories.map(code => html`<option value=${code} ?selected=${code === this.figureCategory}>${code}</option>`)}
|
|
267
|
+
</select>`
|
|
268
|
+
: html`<input
|
|
269
|
+
.value=${this.figureCategory ?? ''}
|
|
270
|
+
?disabled=${!saved}
|
|
271
|
+
placeholder=${i18next.t('figure.text.category-placeholder')}
|
|
272
|
+
@change=${(e: Event) => this.catalog({ category: (e.target as HTMLInputElement).value.trim() })}
|
|
273
|
+
/>`}
|
|
240
274
|
</div>
|
|
241
275
|
<div field>
|
|
242
276
|
<label>${i18next.t('figure.label.tags')}</label>
|
|
@@ -69,4 +69,6 @@ export function firstSaveRefusalOf(type: string, taken: readonly string[]): { ke
|
|
|
69
69
|
export interface FirstSave {
|
|
70
70
|
type: string
|
|
71
71
|
name: string
|
|
72
|
+
/** The domain's namespace to create it under, when the domain has several; its default when absent. */
|
|
73
|
+
namespace?: string
|
|
72
74
|
}
|
|
@@ -47,6 +47,8 @@ export interface FigureProductStore {
|
|
|
47
47
|
thumbnail?: string
|
|
48
48
|
/** The type name a new figure is created under (the first save asks for it). Not sent for an existing one. */
|
|
49
49
|
type?: string
|
|
50
|
+
/** The namespace a new figure is created under, when the first save chose one. Not sent for an existing one. */
|
|
51
|
+
namespace?: string
|
|
50
52
|
/** Keys of the AI proposals whose steps the source holds: the server records them with the save. */
|
|
51
53
|
proposalKeys?: string[]
|
|
52
54
|
}): Promise<FigureProductRecord>
|
|
@@ -280,7 +282,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
280
282
|
* nothing about drawing. A save with no picture is a save: a tab in the background cannot draw, and the
|
|
281
283
|
* source matters more than the card (see `thumbnail.ts`).
|
|
282
284
|
*/
|
|
283
|
-
async save({ thumbnail, type }: { thumbnail?: string; type?: string } = {}) {
|
|
285
|
+
async save({ thumbnail, type, namespace }: { thumbnail?: string; type?: string; namespace?: string } = {}) {
|
|
284
286
|
const control = writable(),
|
|
285
287
|
requestEpoch = epoch
|
|
286
288
|
const exported = workspace.export(control)
|
|
@@ -290,7 +292,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
290
292
|
try {
|
|
291
293
|
let record: FigureProductRecord
|
|
292
294
|
try {
|
|
293
|
-
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })
|
|
295
|
+
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })
|
|
294
296
|
} catch (error) {
|
|
295
297
|
/*
|
|
296
298
|
A save that failed may still have landed: the answer lost on the way back (SAVE_UNREACHABLE), or a second
|
|
@@ -323,7 +325,7 @@ export function createFigureProductSession(store: FigureProductStore) {
|
|
|
323
325
|
*/
|
|
324
326
|
baseline = landed.source
|
|
325
327
|
unconfirmed.clear()
|
|
326
|
-
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }) })
|
|
328
|
+
record = await store.save({ id, source: exported.text, expectedSource: baseline, creationId, thumbnail, proposalKeys, ...(id || !type ? {} : { type }), ...(id || !namespace ? {} : { namespace }) })
|
|
327
329
|
} else {
|
|
328
330
|
if (code === 'SAVE_UNREACHABLE') unconfirmed.set(exported.text, { thumbnail })
|
|
329
331
|
throw error
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { TemplateResult } from 'lit'
|
|
2
|
+
import type { FigureAsset, FigureAuthoringAction } from '@hatiolab/figure-model'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Sections an app adds to the modeller's properties panel (LV figure-server design G11). The panel draws them after its
|
|
6
|
+
* own, in the order registered, for the figures they are for. A section changes the figure only by the modeller's
|
|
7
|
+
* own commands -- one step of the history each, undone like any other -- so an app's section is no second editor. The
|
|
8
|
+
* LV figure-server's CARRIER section writes the `fmsim/carrier` extension with `set-extension` (ADR-0123).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** What a section is shown, and the one way it changes the figure. */
|
|
12
|
+
export interface FigurePropertySectionContext {
|
|
13
|
+
asset: FigureAsset
|
|
14
|
+
/** The figure's row: its id once saved, its category. */
|
|
15
|
+
figure: { id?: string; category?: string }
|
|
16
|
+
/** Runs one modeller command, as the panel's own fields do. */
|
|
17
|
+
command(action: FigureAuthoringAction): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FigurePropertySection {
|
|
21
|
+
/** Unique among sections; drawn as the section's `data-section`. */
|
|
22
|
+
id: string
|
|
23
|
+
/** The heading over the section. */
|
|
24
|
+
title(): string
|
|
25
|
+
/** Whether this figure gets the section, e.g. by its category. */
|
|
26
|
+
when(context: Omit<FigurePropertySectionContext, 'command'>): boolean
|
|
27
|
+
render(context: FigurePropertySectionContext): TemplateResult
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sections: FigurePropertySection[] = []
|
|
31
|
+
|
|
32
|
+
/** Adds a section to the properties panel. Returns the way to take it off again. A second section of one id is refused. */
|
|
33
|
+
export function registerFigurePropertySection(section: FigurePropertySection): () => void {
|
|
34
|
+
if (sections.some(known => known.id === section.id)) throw new Error(`a figure property section '${section.id}' is already registered`)
|
|
35
|
+
sections.push(section)
|
|
36
|
+
return () => {
|
|
37
|
+
const at = sections.indexOf(section)
|
|
38
|
+
if (at >= 0) sections.splice(at, 1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The sections registered, in their order. */
|
|
43
|
+
export function figurePropertySections(): readonly FigurePropertySection[] {
|
|
44
|
+
return sections
|
|
45
|
+
}
|