@things-factory/figure-ui 10.1.162 → 10.1.163
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/modeller/data-interface.ts +227 -76
- package/client/modeller/editor.ts +3 -3
- package/client/modeller/figure-data-panel.ts +602 -193
- package/client/modeller/figure-preview.ts +46 -30
- package/client/modeller/refusals.ts +18 -14
- package/dist-client/modeller/data-interface.d.ts +53 -30
- package/dist-client/modeller/data-interface.js +228 -69
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +1 -1
- package/dist-client/modeller/editor.js +2 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +35 -13
- package/dist-client/modeller/figure-data-panel.js +598 -186
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +7 -5
- package/dist-client/modeller/figure-preview.js +47 -30
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +2 -2
- package/dist-client/modeller/refusals.js +24 -18
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/data-motion.test.ts +130 -52
- package/translations/en.json +109 -43
- package/translations/es.json +109 -43
- package/translations/ja.json +109 -43
- package/translations/ko.json +109 -43
- package/translations/ms.json +109 -43
- package/translations/zh.json +109 -43
|
@@ -3,12 +3,12 @@ import { customElement, property, query, state } from 'lit/decorators.js'
|
|
|
3
3
|
|
|
4
4
|
import { i18next, localize } from '@operato/i18n'
|
|
5
5
|
import { compileFigureAsset } from '@hatiolab/figure-model'
|
|
6
|
-
import type { FigureAsset,
|
|
6
|
+
import type { FigureAsset, FigureDataRow, FigureDataValue } from '@hatiolab/figure-model'
|
|
7
7
|
import { create, registerFigure, unregisterFigure } from '@hatiolab/things-scene'
|
|
8
8
|
|
|
9
9
|
import { DEFAULT_VIEW, previewBoardDepth, sceneLook, WORKBENCH, type ViewSettings } from '@things-factory/figure-viewer/dist-client/figure-view.js'
|
|
10
10
|
import { dialShape } from './angle-dial.js'
|
|
11
|
-
import { domainOf, motionWrittenByData,
|
|
11
|
+
import { domainOf, motionWrittenByData, typedValueOf, valueRowsOf } from './data-interface.js'
|
|
12
12
|
import { derivedOf, type FigureRevision } from './revision.js'
|
|
13
13
|
import { authoredSizeOf, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewTypeOf, setupPartOf } from '@things-factory/figure-viewer/dist-client/preview-model.js'
|
|
14
14
|
|
|
@@ -209,10 +209,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
209
209
|
@state() private giving: Record<string, number> = {}
|
|
210
210
|
/** Per motion group, the clip played here and how fast (ADR-0051 §3.2) -- the preview's, as a board's \`motion\`. */
|
|
211
211
|
@state() private moving: Record<string, { clip: string; speed: number }> = {}
|
|
212
|
-
/** Data values given here, as a board's saved `figureValues`: what moves a group whose clip or speed the data writes. */
|
|
212
|
+
/** Data values given here by accessor, as a board's saved `figureValues`: what moves a group whose clip or speed the data writes. */
|
|
213
213
|
@state() private valuing: Record<string, FigureDataValue> = {}
|
|
214
|
-
/** Data
|
|
215
|
-
@property({ attribute: false }) sample:
|
|
214
|
+
/** Data tried in the data tab: the preview's figures take it as their \`data\`, as a board's data source gives it; null when none is. */
|
|
215
|
+
@property({ attribute: false }) sample: unknown = null
|
|
216
216
|
/** The part chosen in the editor. The size help starts on it, so the help speaks of the part the author was working on. */
|
|
217
217
|
@property() selected = ''
|
|
218
218
|
@state() private setupPart = ''
|
|
@@ -264,7 +264,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
264
264
|
</select>
|
|
265
265
|
<button @click=${() => this.dispatchEvent(new CustomEvent('configure-size', { detail: { part: setupPart }, bubbles: true, composed: true }))}>${i18next.t('figure.button.configure-size')}</button>
|
|
266
266
|
</div>` : nothing}
|
|
267
|
-
${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset
|
|
267
|
+
${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0) ? this.renderDrivers(asset!, controls, clips) : nothing}</div>`}`
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
private renderDrivers(asset: FigureAsset, controls: FigureAsset['document']['model']['inputs'], clips: string[]) {
|
|
@@ -306,7 +306,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
306
306
|
<output>${value.toFixed(2)}</output>
|
|
307
307
|
</label>`
|
|
308
308
|
})}
|
|
309
|
-
${(asset
|
|
309
|
+
${valueRowsOf(asset).map(row => this.renderDataValue(row))}
|
|
310
310
|
${(asset.motionGroups ?? []).map(group => {
|
|
311
311
|
const playing = this.moving[group.id] ?? { clip: group.defaultClip, speed: 0 }
|
|
312
312
|
/* A field the data writes wins over the group's own setting, here as on a board: it is moved by the data values above. */
|
|
@@ -331,28 +331,44 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
331
331
|
</div>`
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
/** A data
|
|
335
|
-
private renderDataValue(
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
334
|
+
/** A value the data's bindings read, as the board's saved values give it: a number on a slider when it has a range, a choice when it is closed. */
|
|
335
|
+
private renderDataValue(row: FigureDataRow) {
|
|
336
|
+
const node = row.node
|
|
337
|
+
const label = row.label ?? row.path
|
|
338
|
+
const fallback = 'default' in node ? node.default : undefined
|
|
339
|
+
const value = this.valuing[row.path] ?? fallback
|
|
340
|
+
if (node.kind === 'number') {
|
|
341
|
+
const ranged = node.min !== undefined && node.max !== undefined && node.max > node.min
|
|
339
342
|
return html`<label>
|
|
340
|
-
<span>${
|
|
343
|
+
<span>${label} <span drive>${node.unit ?? ''}</span></span>
|
|
341
344
|
<span></span>
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
${ranged
|
|
346
|
+
? html`<input type="range" min=${node.min!} max=${node.max!} step=${Math.max((node.max! - node.min!) / 100, 0.01)} .value=${String(value ?? node.min)} @input=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
|
|
347
|
+
<output>${value === undefined ? '' : Number(value).toFixed(2)}</output>`
|
|
348
|
+
: html`<input type="number" step="any" .value=${value === undefined ? '' : String(value)} @change=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
|
|
349
|
+
<span></span>`}
|
|
344
350
|
</label>`
|
|
345
351
|
}
|
|
352
|
+
const domain = domainOf(node)
|
|
353
|
+
if (!domain.length)
|
|
354
|
+
return html`<label>
|
|
355
|
+
<span>${label}</span>
|
|
356
|
+
<input aria-label=${label} .value=${value === undefined ? '' : String(value)} @change=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
|
|
357
|
+
</label>`
|
|
346
358
|
return html`<label>
|
|
347
|
-
<span>${
|
|
348
|
-
<select aria-label=${
|
|
349
|
-
${
|
|
359
|
+
<span>${label}</span>
|
|
360
|
+
<select aria-label=${label} @change=${(e: Event) => this.value(row, (e.target as HTMLSelectElement).value)}>
|
|
361
|
+
${domain.map(v => html`<option value=${String(v)} ?selected=${v === value}>${typeof v === 'boolean' ? i18next.t(v ? 'figure.label.data-true' : 'figure.label.data-false') : v}</option>`)}
|
|
350
362
|
</select>
|
|
351
363
|
</label>`
|
|
352
364
|
}
|
|
353
365
|
|
|
354
|
-
private value(
|
|
355
|
-
|
|
366
|
+
private value(row: FigureDataRow, typed: string) {
|
|
367
|
+
const value = typedValueOf(row.node, typed)
|
|
368
|
+
const next = { ...this.valuing }
|
|
369
|
+
if (typeof value === 'number' && Number.isNaN(value)) delete next[row.path]
|
|
370
|
+
else next[row.path] = value
|
|
371
|
+
this.valuing = next
|
|
356
372
|
this.tellInstances('figureValues', this.valuing)
|
|
357
373
|
}
|
|
358
374
|
|
|
@@ -387,23 +403,23 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
387
403
|
this.giving = {}
|
|
388
404
|
}
|
|
389
405
|
|
|
390
|
-
private tellInstances(key: 'clips' | 'parameters' | 'motion' | '
|
|
391
|
-
for (const component of this.scene?.root?.components ?? []) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
406
|
+
private tellInstances(key: 'clips' | 'parameters' | 'motion' | 'figureValues', values: Record<string, unknown> | null) {
|
|
407
|
+
for (const component of this.scene?.root?.components ?? []) component.set?.(key, values === null ? null : { ...values })
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The tried data to the figures standing now, as their \`data\`: the preview's board is not saved. */
|
|
411
|
+
private tellData(data: unknown) {
|
|
412
|
+
for (const component of this.scene?.root?.components ?? []) component.set?.('data', data === null || data === undefined ? null : structuredClone(data))
|
|
397
413
|
}
|
|
398
414
|
|
|
399
415
|
|
|
400
|
-
/** The data sample to the figures standing now
|
|
416
|
+
/** The data sample to the figures standing now. */
|
|
401
417
|
private tellSample() {
|
|
402
|
-
if (this.sample
|
|
418
|
+
if (this.sample !== null && this.sample !== undefined) this.tellData(this.sample)
|
|
403
419
|
}
|
|
404
420
|
|
|
405
421
|
willUpdate(changed: Map<string, unknown>) {
|
|
406
|
-
if (changed.has('sample') && this.scene) this.
|
|
422
|
+
if (changed.has('sample') && this.scene) this.tellData(this.sample)
|
|
407
423
|
if (!this.hasUpdated) return
|
|
408
424
|
if (!this.active) {
|
|
409
425
|
this.stale = true
|
|
@@ -243,25 +243,27 @@ export function editRefusalOf(error: unknown, say: Say): string {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
|
-
* A data
|
|
247
|
-
* numbers the kernel gave. Nothing for any other refusal.
|
|
246
|
+
* A data field, a binding, a status port, a motion group or a clip's tracks refused (ADR-0051 §8, §3.2), said with the ids
|
|
247
|
+
* and numbers the kernel gave. Nothing for any other refusal.
|
|
248
248
|
*/
|
|
249
249
|
export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
|
|
250
250
|
const invalidId = detailsOf(error, 'data-id-invalid')
|
|
251
251
|
if (invalidId) return say('figure.text.data-id-invalid', { id: invalidId.id })
|
|
252
252
|
const twice = detailsOf(error, 'data-id-duplicate')
|
|
253
253
|
if (twice) return say('figure.text.data-id-duplicate', { id: twice.id })
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
262
|
-
const
|
|
254
|
+
const field = detailsOf(error, 'data-field-invalid')
|
|
255
|
+
if (field) return say('figure.text.data-field-invalid-' + field.what, {})
|
|
256
|
+
const fieldTwice = detailsOf(error, 'data-field-duplicate')
|
|
257
|
+
if (fieldTwice) return say('figure.text.data-field-duplicate', { name: fieldTwice.name })
|
|
258
|
+
const accessor = detailsOf(error, 'data-accessor-invalid')
|
|
259
|
+
if (accessor) return say('figure.text.data-accessor-invalid-' + accessor.why, { accessor: accessor.accessor })
|
|
260
|
+
const behavior = detailsOf(error, 'data-behavior-absent')
|
|
261
|
+
if (behavior) return say('figure.text.data-behavior-absent-' + behavior.behavior, { id: behavior.id })
|
|
262
|
+
const writers = detailsOf(error, 'data-behavior-duplicate')
|
|
263
|
+
if (writers) return say('figure.text.data-behavior-duplicate', { a: writers.bindings[0], b: writers.bindings[1], id: writers.id })
|
|
264
|
+
const domain = detailsOf(error, 'data-rule-domain')
|
|
263
265
|
if (domain)
|
|
264
|
-
return say('figure.text.data-
|
|
266
|
+
return say('figure.text.data-rule-' + domain.why, {
|
|
265
267
|
from: domain.from ?? '',
|
|
266
268
|
to: domain.to ?? '',
|
|
267
269
|
min: domain.min === undefined ? '' : numberOf(domain.min),
|
|
@@ -270,8 +272,10 @@ export function dataMotionRefusalOf(error: unknown, say: Say): string | null {
|
|
|
270
272
|
high: domain.within ? numberOf(domain.within[1]) : '',
|
|
271
273
|
value: domain.value === undefined ? '' : String(domain.value)
|
|
272
274
|
})
|
|
273
|
-
const
|
|
274
|
-
if (
|
|
275
|
+
const port = detailsOf(error, 'data-port-invalid')
|
|
276
|
+
if (port) return say('figure.text.data-port-invalid-' + port.what, { port: port.port, id: port.id ?? '' })
|
|
277
|
+
const bound = detailsOf(error, 'data-port-bound')
|
|
278
|
+
if (bound) return say('figure.text.data-port-bound', { port: bound.port, bindings: bound.bindings.join(', ') })
|
|
275
279
|
const clipAbsent = detailsOf(error, 'motion-clip-absent')
|
|
276
280
|
if (clipAbsent) return say('figure.text.motion-clip-absent', { clip: clipAbsent.clip, group: clipAbsent.group })
|
|
277
281
|
const shared = detailsOf(error, 'motion-clip-shared')
|
|
@@ -1,49 +1,72 @@
|
|
|
1
|
-
import type { FigureAsset,
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type { FigureAsset, FigureDataBehavior, FigureDataBinding, FigureDataField, FigureDataNode, FigureDataRow, FigureDataValue, FigureDataValueIssue, FigureStatusPort } from '@hatiolab/figure-model';
|
|
2
|
+
import { type FigureDataResult } from '@hatiolab/things-scene';
|
|
3
|
+
/** Something a binding can move, with what the screen shows of it. */
|
|
4
|
+
export interface BehaviorChoice {
|
|
5
|
+
/** One string per behavior, for a select's value. */
|
|
5
6
|
key: string;
|
|
6
|
-
|
|
7
|
-
/** The
|
|
7
|
+
behavior: FigureDataBehavior;
|
|
8
|
+
/** The screen's word for the kind: a control, a colour, a group's clip, … */
|
|
9
|
+
kind: FigureDataBehavior['kind'];
|
|
10
|
+
/** The control's label, the colour token, the group's label, the port's label, the axis. */
|
|
8
11
|
name: string;
|
|
9
12
|
unit?: string;
|
|
10
13
|
min?: number;
|
|
11
14
|
max?: number;
|
|
12
15
|
}
|
|
13
|
-
export declare const
|
|
16
|
+
export declare const behaviorKeyOf: (b: FigureDataBehavior) => string;
|
|
14
17
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* value read from the machine applies as it comes.
|
|
18
|
+
* Everything the figure can do that data may drive: each control, each colour, each motion group's clip, speed and
|
|
19
|
+
* pause, each status port, its position and heading, and -- when it has places -- the items in them.
|
|
18
20
|
*/
|
|
19
|
-
export declare function
|
|
20
|
-
/**
|
|
21
|
-
export declare
|
|
21
|
+
export declare function behaviorsOf(asset: FigureAsset): BehaviorChoice[];
|
|
22
|
+
/** Whether a field of this kind can drive the behavior, by some rule -- the choices offered for it. */
|
|
23
|
+
export declare function fitsField(node: FigureDataNode, choice: BehaviorChoice): boolean;
|
|
22
24
|
/** An id not taken among `taken`: `base`, then `base-2`, `base-3`, … made into the kernel's id shape. */
|
|
23
|
-
export declare function
|
|
25
|
+
export declare function freeIdOf(taken: Iterable<string>, base: string): string;
|
|
26
|
+
/** The values a closed field takes: a closed text's values, both booleans. Empty when open. */
|
|
27
|
+
export declare const domainOf: (node: FigureDataNode) => FigureDataValue[];
|
|
24
28
|
/**
|
|
25
|
-
* A first
|
|
26
|
-
*
|
|
27
|
-
* rest value, the colour, the group's
|
|
29
|
+
* A first binding of a field to a behavior, for the author to adjust: the value as it is when it already fits, a scale
|
|
30
|
+
* when only its unit or range differs, one result per value (a map) for a closed field, a default for an open one.
|
|
31
|
+
* Each result starts at what the behavior shows now -- the control's rest value, the colour, the group's default clip --
|
|
32
|
+
* or, for a clip, the clip of the value's own name. The kernel says whether it holds.
|
|
28
33
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
/**
|
|
31
|
-
export declare
|
|
32
|
-
/**
|
|
33
|
-
export declare function
|
|
34
|
+
export declare function bindingDraftOf(asset: FigureAsset, accessor: string, node: FigureDataNode, choice: BehaviorChoice): FigureDataBinding;
|
|
35
|
+
/** A row's path (`L[].F`) as the accessor a value binding reads (`L[0].F`): the first element of each list. */
|
|
36
|
+
export declare const accessorOfRow: (path: string) => string;
|
|
37
|
+
/** The field at a row's path, or undefined. */
|
|
38
|
+
export declare function fieldAt(schema: readonly FigureDataField[], path: string): FigureDataField | undefined;
|
|
39
|
+
/** The schema with a field added under a path ('' for the top): a copy, the given one left as it is. */
|
|
40
|
+
export declare function schemaWithField(schema: readonly FigureDataField[], under: string, field: FigureDataField): FigureDataField[];
|
|
41
|
+
/** The schema with the field at a path replaced (its name kept unless the new one says another). */
|
|
42
|
+
export declare function schemaWithFieldReplaced(schema: readonly FigureDataField[], path: string, field: FigureDataField): FigureDataField[];
|
|
43
|
+
/** The schema without the field at a path. */
|
|
44
|
+
export declare function schemaWithoutField(schema: readonly FigureDataField[], path: string): FigureDataField[];
|
|
45
|
+
/** A field of another kind, its name, label and json flag kept, the rest started fresh. */
|
|
46
|
+
export declare function fieldOfKind(field: FigureDataField, kind: FigureDataNode['kind']): FigureDataField;
|
|
47
|
+
/** The rows of the schema, each with the bindings that read it (figure-model's view). */
|
|
48
|
+
export declare const schemaRowsOf: (asset: FigureAsset) => FigureDataRow[];
|
|
34
49
|
/**
|
|
35
|
-
* What a sample of
|
|
36
|
-
*
|
|
50
|
+
* What a sample of data would do on this figure, read the one way a placed figure reads it (things-scene's figure
|
|
51
|
+
* inputs): what it moves, or why it is refused, each value's reason. Nothing here reads data itself.
|
|
37
52
|
*/
|
|
38
|
-
export declare function
|
|
39
|
-
result: FigureDataResult;
|
|
40
|
-
} | {
|
|
53
|
+
export declare function sampleOf(asset: FigureAsset, sample: unknown): {
|
|
41
54
|
issues: FigureDataValueIssue[];
|
|
55
|
+
result: FigureDataResult;
|
|
42
56
|
};
|
|
57
|
+
/** The rows a person gives a value by hand, as a board's saved values: one value, read by a binding, not inside a list. */
|
|
58
|
+
export declare const valueRowsOf: (asset: FigureAsset) => FigureDataRow[];
|
|
59
|
+
/** A value typed or chosen for a row, as its kind reads it: a number (NaN when empty), true or false, or the text. */
|
|
60
|
+
export declare function typedValueOf(node: FigureDataNode, typed: string): FigureDataValue;
|
|
61
|
+
/** The parts a status port may show on: the parts the figure's appearance names. */
|
|
62
|
+
export declare const partsOf: (asset: FigureAsset) => string[];
|
|
63
|
+
/** A first status port: the label as given, on the first colour token, no part yet. The kernel says whether it holds. */
|
|
64
|
+
export declare function portDraftOf(asset: FigureAsset, label: string): FigureStatusPort;
|
|
65
|
+
/** Whether a text is an accessor the kernel takes. */
|
|
66
|
+
export declare const accessorTaken: (accessor: string) => boolean;
|
|
43
67
|
/**
|
|
44
|
-
* Which fields of each motion group the data
|
|
45
|
-
*
|
|
46
|
-
* wins over a group's own setting for that field (detail design §5.1), so a control for such a field moves nothing.
|
|
68
|
+
* Which fields of each motion group the data binds: its clip, its speed. A field bound wins over the group's own setting
|
|
69
|
+
* whenever the data, a saved value or the schema's default gives it a value, so the preview drives it through the data.
|
|
47
70
|
*/
|
|
48
71
|
export declare function motionWrittenByData(asset: FigureAsset): Record<string, {
|
|
49
72
|
clip: boolean;
|