@things-factory/figure-ui 10.1.222 → 10.1.224
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/canvas.ts +2 -2
- package/client/modeller/consumer-capabilities.ts +66 -0
- package/client/modeller/data-interface.ts +230 -80
- package/client/modeller/editor.ts +14 -7
- package/client/modeller/figure-capability-panel.ts +145 -1
- package/client/modeller/figure-data-panel.ts +495 -188
- package/client/modeller/figure-links.ts +130 -12
- package/client/modeller/figure-preview.ts +19 -2
- package/client/modeller/figure-slot-layout-panel.ts +7 -3
- package/client/modeller/links.ts +58 -0
- package/client/modeller/refusals.ts +12 -4
- package/client/modeller/slot-layout-host.ts +76 -5
- package/dist-client/modeller/canvas.js +2 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/consumer-capabilities.d.ts +29 -0
- package/dist-client/modeller/consumer-capabilities.js +60 -0
- package/dist-client/modeller/consumer-capabilities.js.map +1 -0
- package/dist-client/modeller/data-interface.d.ts +83 -17
- package/dist-client/modeller/data-interface.js +194 -82
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +5 -2
- package/dist-client/modeller/editor.js +16 -5
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-capability-panel.d.ts +12 -0
- package/dist-client/modeller/figure-capability-panel.js +123 -1
- package/dist-client/modeller/figure-capability-panel.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +57 -13
- package/dist-client/modeller/figure-data-panel.js +461 -180
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +11 -0
- package/dist-client/modeller/figure-links.js +137 -16
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +4 -0
- package/dist-client/modeller/figure-preview.js +19 -1
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-slot-layout-panel.js +6 -4
- package/dist-client/modeller/figure-slot-layout-panel.js.map +1 -1
- package/dist-client/modeller/links.d.ts +22 -0
- package/dist-client/modeller/links.js +48 -0
- package/dist-client/modeller/links.js.map +1 -1
- package/dist-client/modeller/refusals.js +14 -6
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/modeller/slot-layout-host.d.ts +28 -3
- package/dist-client/modeller/slot-layout-host.js +68 -6
- package/dist-client/modeller/slot-layout-host.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/test/browser/data-table-page.js +9 -0
- package/test/browser/links.check.mjs +25 -3
- package/test/consumer-capabilities.test.ts +116 -0
- package/test/data-motion.test.ts +94 -27
- package/test/links.test.ts +15 -0
- package/test/slot-layout-host.test.ts +63 -5
- package/translations/en.json +81 -23
- package/translations/es.json +81 -23
- package/translations/ja.json +81 -23
- package/translations/ko.json +81 -23
- package/translations/ms.json +81 -23
- package/translations/zh.json +81 -23
|
@@ -5,7 +5,16 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
|
5
5
|
|
|
6
6
|
import { i18next, localize } from '@operato/i18n'
|
|
7
7
|
import { FIGURE_UNITS, figureDataExample, inferFigureDataSchema } from '@hatiolab/figure-model'
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
FigureAsset,
|
|
10
|
+
FigureDataBinding,
|
|
11
|
+
FigureDataField,
|
|
12
|
+
FigureDataNode,
|
|
13
|
+
FigureDataRow,
|
|
14
|
+
FigureDataTableBinding,
|
|
15
|
+
FigureDataTableEntry,
|
|
16
|
+
FigureDataValueBinding
|
|
17
|
+
} from '@hatiolab/figure-model'
|
|
9
18
|
|
|
10
19
|
import {
|
|
11
20
|
accessorOfRow,
|
|
@@ -17,8 +26,11 @@ import {
|
|
|
17
26
|
fieldAt,
|
|
18
27
|
fieldOfKind,
|
|
19
28
|
fitsField,
|
|
29
|
+
freeCodeOf,
|
|
30
|
+
legendRoleOf,
|
|
31
|
+
methodOf,
|
|
32
|
+
methodsOf,
|
|
20
33
|
partsOf,
|
|
21
|
-
portDraftOf,
|
|
22
34
|
rotationStepsOf,
|
|
23
35
|
sampleOf,
|
|
24
36
|
schemaRowsOf,
|
|
@@ -26,9 +38,20 @@ import {
|
|
|
26
38
|
schemaWithField,
|
|
27
39
|
schemaWithFieldReplaced,
|
|
28
40
|
schemaWithoutField,
|
|
29
|
-
|
|
41
|
+
schemaFollowingCodes,
|
|
42
|
+
stateInputsOf,
|
|
43
|
+
tableColumnsOf,
|
|
44
|
+
tableDraftOf,
|
|
45
|
+
tableWithKeys,
|
|
46
|
+
tableWithLook,
|
|
47
|
+
tableWithTargets,
|
|
48
|
+
type BehaviorChoice,
|
|
49
|
+
type DataMethod,
|
|
50
|
+
type TableColumn
|
|
30
51
|
} from './data-interface.js'
|
|
31
52
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
53
|
+
import { figureBindingsTakenOver, figureCapabilityDescriptorOf, type FigureRestingBinding } from '@hatiolab/things-scene'
|
|
54
|
+
import { consumerDescriptors } from './consumer-capabilities.js'
|
|
32
55
|
import { HelpToggles } from './help.js'
|
|
33
56
|
|
|
34
57
|
const KINDS: FigureDataNode['kind'][] = ['number', 'boolean', 'text', 'object', 'list']
|
|
@@ -36,12 +59,14 @@ const KINDS: FigureDataNode['kind'][] = ['number', 'boolean', 'text', 'object',
|
|
|
36
59
|
/**
|
|
37
60
|
* 데이터 -- the data a site sends this figure, in its own names, and what each value moves (ADR-0051 §8, data binding
|
|
38
61
|
* design decision 4). The schema is free: fields the author names, a tree of objects and lists. A binding reads one place
|
|
39
|
-
* of it and
|
|
40
|
-
*
|
|
41
|
-
*
|
|
62
|
+
* of it and shows it in the figure (ADR-0166): as it is or scaled, it moves one thing -- a control, a colour, a motion
|
|
63
|
+
* group's clip, speed or pause, its position, the items in its places; by a code table or a range table, each value's
|
|
64
|
+
* look goes to several targets at once -- parts' colour and effect, controls' values, a motion group. The field's kind
|
|
65
|
+
* says which methods there are. Every change is one part edit, and whether it holds is the kernel's check.
|
|
42
66
|
*
|
|
43
67
|
* The sample is not saved: data tried here, read the one way a placed figure reads it, and -- in the preview -- the figure
|
|
44
|
-
* running on it (`data-sample`)
|
|
68
|
+
* running on it (`data-sample`); a sample legend pasted for a table's role shows it as a board's legend would
|
|
69
|
+
* (`legend-sample`).
|
|
45
70
|
*/
|
|
46
71
|
@customElement('figure-data-panel')
|
|
47
72
|
export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
@@ -124,6 +149,17 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
124
149
|
gap: 4px 6px;
|
|
125
150
|
align-items: center;
|
|
126
151
|
}
|
|
152
|
+
div[codes] {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
155
|
+
gap: 4px 6px;
|
|
156
|
+
align-items: center;
|
|
157
|
+
}
|
|
158
|
+
div[codes] button[act] {
|
|
159
|
+
width: 30px;
|
|
160
|
+
margin: 0;
|
|
161
|
+
padding: 0;
|
|
162
|
+
}
|
|
127
163
|
div[ranges] {
|
|
128
164
|
display: grid;
|
|
129
165
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
@@ -135,6 +171,54 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
135
171
|
margin: 0;
|
|
136
172
|
padding: 0;
|
|
137
173
|
}
|
|
174
|
+
/* A table's rows: the key (a code, or from and to), a cell per column, and a remove button. */
|
|
175
|
+
div[looks] {
|
|
176
|
+
display: grid;
|
|
177
|
+
gap: 4px 6px;
|
|
178
|
+
align-items: center;
|
|
179
|
+
overflow-x: auto;
|
|
180
|
+
}
|
|
181
|
+
div[looks] > small {
|
|
182
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
183
|
+
}
|
|
184
|
+
div[looks] input,
|
|
185
|
+
div[looks] select {
|
|
186
|
+
min-width: 0;
|
|
187
|
+
width: 100%;
|
|
188
|
+
box-sizing: border-box;
|
|
189
|
+
}
|
|
190
|
+
div[looks] button[act] {
|
|
191
|
+
width: 30px;
|
|
192
|
+
margin: 0;
|
|
193
|
+
padding: 0;
|
|
194
|
+
}
|
|
195
|
+
/* A colour and the button that clears it, on one line at the height of the cells beside it. */
|
|
196
|
+
div[looks] span[colour] {
|
|
197
|
+
display: flex;
|
|
198
|
+
gap: 2px;
|
|
199
|
+
align-items: center;
|
|
200
|
+
min-width: 0;
|
|
201
|
+
}
|
|
202
|
+
div[looks] span[colour] input[type='color'] {
|
|
203
|
+
flex: 1;
|
|
204
|
+
height: var(--figure-control-height);
|
|
205
|
+
padding: 0 2px;
|
|
206
|
+
}
|
|
207
|
+
div[looks] span[colour] button[act] {
|
|
208
|
+
flex: none;
|
|
209
|
+
width: 24px;
|
|
210
|
+
}
|
|
211
|
+
div[chips] {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-wrap: wrap;
|
|
214
|
+
gap: 4px 10px;
|
|
215
|
+
align-items: center;
|
|
216
|
+
}
|
|
217
|
+
div[chips] label {
|
|
218
|
+
display: inline-flex;
|
|
219
|
+
gap: 4px;
|
|
220
|
+
align-items: center;
|
|
221
|
+
}
|
|
138
222
|
textarea {
|
|
139
223
|
width: 100%;
|
|
140
224
|
min-height: 96px;
|
|
@@ -156,16 +240,20 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
156
240
|
|
|
157
241
|
@property({ attribute: false }) asset?: FigureAsset
|
|
158
242
|
@property({ type: Boolean }) locked = false
|
|
243
|
+
/** The part chosen in the modeller, put among a table's parts by one button. */
|
|
244
|
+
@property() selected = ''
|
|
159
245
|
|
|
160
246
|
/** The schema row chosen, by its path; its field is edited under it. */
|
|
161
247
|
@state() private chosen = ''
|
|
162
248
|
@state() private adding = { name: '', kind: 'number' as FigureDataNode['kind'] }
|
|
163
249
|
/** A site's data pasted to start the schema from; nothing is kept until the author says so. */
|
|
164
250
|
@state() private pasted = ''
|
|
165
|
-
|
|
251
|
+
/** A binding as it is being added: the field (or a new field's name), the method, the behavior or the table's targets. */
|
|
252
|
+
@state() private connecting = { path: '', name: '', method: '' as DataMethod | '', behavior: '', parts: [] as string[], inputs: [] as string[], group: '' }
|
|
166
253
|
/** The binding opened to edit its rule, by id. */
|
|
167
254
|
@state() private opened = ''
|
|
168
|
-
|
|
255
|
+
/** Sample legends by role, as JSON typed; not saved. */
|
|
256
|
+
@state() private legendSample: Record<string, string> = {}
|
|
169
257
|
/** The rotation card as filled in: the joint, and the fields for on, speed (and its most rpm) and direction. */
|
|
170
258
|
@state() private rotating = { input: '', on: '', speed: '', max: 0, direction: '', forward: '', reverse: '' }
|
|
171
259
|
/** The sample as typed, JSON; empty until edited, the schema's example shown until then. */
|
|
@@ -177,10 +265,20 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
177
265
|
if (!asset) return nothing
|
|
178
266
|
const rows = schemaRowsOf(asset)
|
|
179
267
|
return html`
|
|
180
|
-
${this.
|
|
268
|
+
${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${this.renderBindings(asset, rows)} ${rows.length ? this.renderSample(asset) : nothing}
|
|
181
269
|
`
|
|
182
270
|
}
|
|
183
271
|
|
|
272
|
+
/** The data a product's capability the figure wears reads itself (ADR-0167 14): said, read only. */
|
|
273
|
+
private renderCapabilityData(asset: FigureAsset) {
|
|
274
|
+
const names = asset.document.capabilities.names ?? []
|
|
275
|
+
const reading = consumerDescriptors().filter(d => names.includes(d.name) && d.data?.length)
|
|
276
|
+
if (!reading.length) return nothing
|
|
277
|
+
return html`<section>
|
|
278
|
+
${reading.map(d => html`<p hint>${i18next.t('figure.text.data-capability-fields', { name: d.label, fields: d.data!.map(f => f.name).join(', ') })}</p>`)}
|
|
279
|
+
</section>`
|
|
280
|
+
}
|
|
281
|
+
|
|
184
282
|
/* ---------------------------------------------------------------- the schema */
|
|
185
283
|
|
|
186
284
|
private renderSchema(asset: FigureAsset, rows: FigureDataRow[]) {
|
|
@@ -366,156 +464,333 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
366
464
|
|
|
367
465
|
/* ---------------------------------------------------------------- the bindings */
|
|
368
466
|
|
|
369
|
-
private behaviorName(asset: FigureAsset, choices: BehaviorChoice[], b:
|
|
467
|
+
private behaviorName(asset: FigureAsset, choices: BehaviorChoice[], b: FigureDataValueBinding['behavior']): string {
|
|
370
468
|
const key = behaviorKeyOf(b)
|
|
371
469
|
const choice = choices.find(c => c.key === key)
|
|
372
470
|
const name = b.kind === 'pose' ? i18next.t('figure.label.data-axis-' + b.axis) : (choice?.name ?? key.slice(key.indexOf(':') + 1))
|
|
373
471
|
return i18next.t('figure.label.data-behavior-' + b.kind, { name })
|
|
374
472
|
}
|
|
375
473
|
|
|
474
|
+
/** What a table shows on, as one line: its parts, the controls it sets, its motion group. */
|
|
475
|
+
private targetsName(asset: FigureAsset, b: FigureDataTableBinding): string {
|
|
476
|
+
const inputs = stateInputsOf(asset)
|
|
477
|
+
const group = (asset.motionGroups ?? []).find(g => g.id === b.targets.group)
|
|
478
|
+
return [
|
|
479
|
+
...(b.targets.parts ?? []),
|
|
480
|
+
...(b.targets.inputs ?? []).map(id => inputs.find(i => i.id === id)?.label ?? id),
|
|
481
|
+
...(group ? [group.label] : [])
|
|
482
|
+
].join(', ')
|
|
483
|
+
}
|
|
484
|
+
|
|
376
485
|
private renderBindings(asset: FigureAsset, rows: FigureDataRow[]) {
|
|
377
486
|
const bindings = asset.dataInterface?.bindings ?? []
|
|
378
487
|
const choices = behaviorsOf(asset)
|
|
488
|
+
const resting = figureBindingsTakenOver(asset)
|
|
379
489
|
return html`<section>
|
|
380
490
|
<h3>${i18next.t('figure.label.data-bindings')}${this.help.button('figure.text.data-bindings-explained')}</h3>
|
|
381
491
|
${this.help.hint('figure.text.data-bindings-explained')}
|
|
382
|
-
${bindings.map(b => this.renderBinding(asset, choices, b))}
|
|
383
|
-
${
|
|
492
|
+
${bindings.map(b => this.renderBinding(asset, choices, b, resting))}
|
|
493
|
+
${this.renderAddBinding(asset, rows, choices)}
|
|
384
494
|
${this.renderRotation(asset, rows)}
|
|
385
495
|
</section>`
|
|
386
496
|
}
|
|
387
497
|
|
|
388
|
-
private renderBinding(asset: FigureAsset, choices: BehaviorChoice[], b: FigureDataBinding) {
|
|
498
|
+
private renderBinding(asset: FigureAsset, choices: BehaviorChoice[], b: FigureDataBinding, resting: FigureRestingBinding[]) {
|
|
389
499
|
const open = this.opened === b.id
|
|
390
|
-
const set = (
|
|
391
|
-
|
|
392
|
-
if (patch.param === undefined && 'param' in patch) delete next.param
|
|
393
|
-
this.edit({ kind: 'set-data-binding', binding: next })
|
|
394
|
-
}
|
|
500
|
+
const set = (next: FigureDataBinding) => this.edit({ kind: 'set-data-binding', binding: next })
|
|
501
|
+
const what = b.rule === 'table' ? this.targetsName(asset, b) : this.behaviorName(asset, choices, b.behavior)
|
|
395
502
|
return html`<section item>
|
|
396
503
|
<div head>
|
|
397
504
|
<button line aria-expanded=${open ? 'true' : 'false'} @click=${() => (this.opened = open ? '' : b.id)}>
|
|
398
|
-
<strong>${b.accessor}</strong> → ${
|
|
505
|
+
<strong>${b.label ?? b.accessor}</strong> → ${what} <small>· ${i18next.t('figure.label.data-method-' + methodOf(b))}</small>
|
|
399
506
|
</button>
|
|
400
507
|
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-binding')} @click=${() => this.edit({ kind: 'remove-data-binding', id: b.id })}>
|
|
401
508
|
<md-icon>close</md-icon>
|
|
402
509
|
</button>
|
|
403
510
|
</div>
|
|
404
|
-
${
|
|
511
|
+
${resting.filter(r => r.id === b.id).map(r => this.renderResting(r))}
|
|
512
|
+
${open ? (b.rule === 'table' ? this.renderTable(asset, b, set) : this.renderValueDetail(asset, b, set)) : nothing}
|
|
405
513
|
</section>`
|
|
406
514
|
}
|
|
407
515
|
|
|
408
|
-
|
|
516
|
+
/**
|
|
517
|
+
* A binding a product's capability takes over where the product registers it (ADR-0167 17): there it rests, whole or for
|
|
518
|
+
* the targets named; elsewhere (a board without the product) it works.
|
|
519
|
+
*/
|
|
520
|
+
private renderResting(r: FigureRestingBinding) {
|
|
521
|
+
const where = r.capability.split('.')[0]
|
|
522
|
+
const name = figureCapabilityDescriptorOf(r.capability)?.label ?? r.capability
|
|
523
|
+
const targets = [...r.parts, ...r.inputs].join(', ')
|
|
524
|
+
return html`<p hint>${r.whole ? i18next.t('figure.text.data-binding-rests', { where, name }) : i18next.t('figure.text.data-binding-rests-targets', { where, name, targets })}</p>`
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private renderValueDetail(asset: FigureAsset, b: FigureDataValueBinding, set: (next: FigureDataBinding) => void) {
|
|
409
528
|
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'))
|
|
410
529
|
const value = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
411
530
|
const behavior = b.behavior
|
|
412
|
-
|
|
531
|
+
const patch = (p: Partial<FigureDataValueBinding>) => {
|
|
532
|
+
const next = { ...b, ...p }
|
|
533
|
+
if ('param' in p && p.param === undefined) delete next.param
|
|
534
|
+
set(next)
|
|
535
|
+
}
|
|
536
|
+
if (behavior.kind === 'items') return this.renderItemFields(asset, b, node, patch)
|
|
413
537
|
return html`<div form>
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
538
|
+
<span>${i18next.t('figure.label.data-method')}</span>
|
|
539
|
+
${node?.kind === 'number'
|
|
540
|
+
? html`<select ?disabled=${this.locked} @change=${(e: Event) => patch({ rule: value(e) as 'value' | 'scale', param: value(e) === 'scale' ? { scale: 1, offset: 0 } : undefined })}>
|
|
541
|
+
${(['value', 'scale'] as const).map(r => html`<option value=${r} ?selected=${r === b.rule}>${i18next.t('figure.label.data-method-' + r)}</option>`)}
|
|
542
|
+
</select>`
|
|
543
|
+
: html`<span>${i18next.t('figure.label.data-method-value')}</span>`}
|
|
544
|
+
${behavior.kind === 'state'
|
|
545
|
+
? html`<span>${i18next.t('figure.label.data-source-mode')}</span>
|
|
546
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => patch({ behavior: { ...behavior, sourceMode: value(e) as 'observed' | 'commanded' } })}>
|
|
547
|
+
${(['observed', 'commanded'] as const).map(m => html`<option value=${m} ?selected=${behavior.sourceMode === m}>${i18next.t('figure.label.data-' + m)}</option>`)}
|
|
548
|
+
</select>`
|
|
549
|
+
: nothing}
|
|
550
|
+
${behavior.kind === 'pose'
|
|
551
|
+
? html`<span>${i18next.t('figure.label.data-seconds')}</span>
|
|
552
|
+
<input type="number" min="0" step="any" .value=${String(behavior.seconds ?? 0)} ?disabled=${this.locked} @change=${(e: Event) => patch({ behavior: { ...behavior, seconds: (e.target as HTMLInputElement).valueAsNumber || 0 } })} />`
|
|
553
|
+
: nothing}
|
|
554
|
+
${b.rule === 'scale'
|
|
555
|
+
? html`<span>${i18next.t('figure.label.data-scale')}</span>
|
|
556
|
+
<input type="number" step="any" .value=${String(b.param?.scale ?? 1)} ?disabled=${this.locked} @change=${(e: Event) => patch({ param: { ...b.param, scale: (e.target as HTMLInputElement).valueAsNumber } })} />
|
|
557
|
+
<span>${i18next.t('figure.label.data-offset')}</span>
|
|
558
|
+
<input type="number" step="any" .value=${String(b.param?.offset ?? 0)} ?disabled=${this.locked} @change=${(e: Event) => patch({ param: { ...b.param, offset: (e.target as HTMLInputElement).valueAsNumber } })} />`
|
|
559
|
+
: nothing}
|
|
560
|
+
</div>`
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* A table (ADR-0166 10): what it shows on -- parts, controls, a motion group -- its rows, a code or an interval each
|
|
565
|
+
* with a cell per column, the look of every other value, and whether a board may swap it for a legend.
|
|
566
|
+
*/
|
|
567
|
+
private renderTable(asset: FigureAsset, b: FigureDataTableBinding, set: (next: FigureDataBinding) => void) {
|
|
568
|
+
const value = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
569
|
+
const parts = partsOf(asset)
|
|
570
|
+
const inputs = stateInputsOf(asset)
|
|
571
|
+
const groups = asset.motionGroups ?? []
|
|
572
|
+
const targets = b.targets
|
|
573
|
+
const toggle = (list: string[] | undefined, id: string, on: boolean) => (on ? [...(list ?? []), id] : (list ?? []).filter(x => x !== id))
|
|
574
|
+
const retarget = (t: typeof targets) => set(tableWithTargets(b, t))
|
|
575
|
+
return html`<div form>
|
|
576
|
+
<span>${i18next.t('figure.label.data-label')}</span>
|
|
577
|
+
<input .value=${b.label ?? ''} placeholder=${b.accessor} ?disabled=${this.locked} @change=${(e: Event) => {
|
|
578
|
+
const label = value(e).trim()
|
|
579
|
+
const next = { ...b, label }
|
|
580
|
+
if (!label) delete (next as { label?: string }).label
|
|
581
|
+
set(next)
|
|
582
|
+
}} />
|
|
583
|
+
</div>
|
|
584
|
+
${this.renderTargets(parts, inputs, groups, targets, {
|
|
585
|
+
part: (id, on) => retarget({ ...targets, parts: toggle(targets.parts, id, on) }),
|
|
586
|
+
input: (id, on) => retarget({ ...targets, inputs: toggle(targets.inputs, id, on) }),
|
|
587
|
+
group: id => retarget({ ...targets, group: id || undefined })
|
|
588
|
+
})}
|
|
589
|
+
${b.lookup === 'code' ? this.renderCodeRows(asset, b, set) : this.renderRangeRows(asset, b, set)}
|
|
590
|
+
<label toggle>
|
|
591
|
+
<input type="checkbox" .checked=${!!b.legend} ?disabled=${this.locked} @change=${(e: Event) => {
|
|
592
|
+
const next = { ...b }
|
|
593
|
+
if ((e.target as HTMLInputElement).checked) next.legend = { role: legendRoleOf(b.id) }
|
|
594
|
+
else delete next.legend
|
|
595
|
+
set(next)
|
|
596
|
+
}} />
|
|
597
|
+
${i18next.t('figure.label.data-legend-swappable')}
|
|
598
|
+
</label>
|
|
599
|
+
${b.legend
|
|
600
|
+
? html`<div form>
|
|
601
|
+
<span>${i18next.t('figure.label.data-legend-role')}</span>
|
|
602
|
+
<input .value=${b.legend.role} ?disabled=${this.locked} @change=${(e: Event) => set({ ...b, legend: { role: value(e).trim() } })} />
|
|
603
|
+
</div>
|
|
604
|
+
<p hint>${i18next.t('figure.text.data-legend-explained')}</p>`
|
|
605
|
+
: nothing}`
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Which parts, controls and motion group a table shows on: chips to tick, and the part chosen in the modeller by one button. */
|
|
609
|
+
private renderTargets(
|
|
610
|
+
parts: string[],
|
|
611
|
+
inputs: Array<{ id: string; label: string }>,
|
|
612
|
+
groups: NonNullable<FigureAsset['motionGroups']>,
|
|
613
|
+
t: { parts?: string[]; inputs?: string[]; group?: string },
|
|
614
|
+
on: { part: (id: string, on: boolean) => void; input: (id: string, on: boolean) => void; group: (id: string) => void }
|
|
615
|
+
) {
|
|
616
|
+
const chosen = (e: Event) => (e.target as HTMLInputElement).checked
|
|
617
|
+
return html`<div form>
|
|
618
|
+
<span>${i18next.t('figure.label.data-target-parts')}</span>
|
|
619
|
+
<div chips>
|
|
620
|
+
${parts.map(p => html`<label><input type="checkbox" .checked=${!!t.parts?.includes(p)} ?disabled=${this.locked} @change=${(e: Event) => on.part(p, chosen(e))} />${p}</label>`)}
|
|
621
|
+
${this.selected && parts.includes(this.selected) && !t.parts?.includes(this.selected)
|
|
622
|
+
? html`<button act ?disabled=${this.locked} @click=${() => on.part(this.selected, true)}><md-icon>add</md-icon>${i18next.t('figure.button.data-add-selected-part', { part: this.selected })}</button>`
|
|
433
623
|
: nothing}
|
|
434
624
|
</div>
|
|
435
|
-
${
|
|
625
|
+
${inputs.length
|
|
626
|
+
? html`<span>${i18next.t('figure.label.data-target-inputs')}</span>
|
|
627
|
+
<div chips>
|
|
628
|
+
${inputs.map(i => html`<label><input type="checkbox" .checked=${!!t.inputs?.includes(i.id)} ?disabled=${this.locked} @change=${(e: Event) => on.input(i.id, chosen(e))} />${i.label}</label>`)}
|
|
629
|
+
</div>`
|
|
630
|
+
: nothing}
|
|
631
|
+
${groups.length
|
|
632
|
+
? html`<span>${i18next.t('figure.label.data-target-group')}</span>
|
|
633
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => on.group((e.target as HTMLSelectElement).value)}>
|
|
634
|
+
<option value="" ?selected=${!t.group}>${i18next.t('figure.label.data-none')}</option>
|
|
635
|
+
${groups.map(g => html`<option value=${g.id} ?selected=${g.id === t.group}>${g.label}</option>`)}
|
|
636
|
+
</select>`
|
|
637
|
+
: nothing}
|
|
638
|
+
</div>`
|
|
436
639
|
}
|
|
437
640
|
|
|
438
|
-
/**
|
|
439
|
-
private
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
641
|
+
/** The column heads of a table's rows, after the key's own one or two. */
|
|
642
|
+
private columnHeads(columns: TableColumn[]) {
|
|
643
|
+
return columns.map(c =>
|
|
644
|
+
c.kind === 'value'
|
|
645
|
+
? html`<small>${c.label}${c.unit ? ` (${c.unit}${c.min !== undefined && c.max !== undefined ? ` ${c.min}~${c.max}` : ''})` : ''}</small>`
|
|
646
|
+
: html`<small>${i18next.t('figure.label.data-column-' + c.kind)}</small>`
|
|
647
|
+
)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** One look's cells, a column each: a colour, an effect, a control's value, a clip, a speed, a pause -- empty leaves the target as it is. */
|
|
651
|
+
private lookCells(asset: FigureAsset, columns: TableColumn[], look: FigureDataTableEntry, write: (look: FigureDataTableEntry) => void) {
|
|
652
|
+
const without = (key: keyof FigureDataTableEntry) => {
|
|
653
|
+
const next = { ...look }
|
|
654
|
+
delete next[key]
|
|
655
|
+
return next
|
|
452
656
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
657
|
+
const typed = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
658
|
+
return columns.map(c => {
|
|
659
|
+
if (c.kind === 'color')
|
|
660
|
+
return look.color === undefined
|
|
661
|
+
? html`<button act ?disabled=${this.locked} @click=${() => write({ ...look, color: '#888888' })}>${i18next.t('figure.button.data-set-result')}</button>`
|
|
662
|
+
: html`<span colour><input type="color" .value=${look.color} ?disabled=${this.locked} @change=${(e: Event) => write({ ...look, color: typed(e) })} /><button act ?disabled=${this.locked} title=${i18next.t('figure.button.data-clear-result')} @click=${() => write(without('color'))}><md-icon>close</md-icon></button></span>`
|
|
663
|
+
if (c.kind === 'effect')
|
|
664
|
+
return html`<select ?disabled=${this.locked} @change=${(e: Event) => (typed(e) ? write({ ...look, effect: { kind: typed(e) as 'blink' | 'glow' } }) : write(without('effect')))}>
|
|
665
|
+
${(['', 'blink', 'glow'] as const).map(k => html`<option value=${k} ?selected=${(look.effect?.kind ?? '') === k}>${i18next.t('figure.label.data-effect-' + (k || 'none'))}</option>`)}
|
|
666
|
+
</select>`
|
|
667
|
+
if (c.kind === 'value') {
|
|
668
|
+
const v = look.values?.[c.id]
|
|
669
|
+
return html`<input type="number" step="any" min=${c.min ?? ''} max=${c.max ?? ''} .value=${v === undefined ? '' : String(v)} ?disabled=${this.locked} @change=${(e: Event) => {
|
|
670
|
+
const n = (e.target as HTMLInputElement).valueAsNumber
|
|
671
|
+
const values = { ...look.values }
|
|
672
|
+
if (Number.isNaN(n)) delete values[c.id]
|
|
673
|
+
else values[c.id] = n
|
|
674
|
+
write(Object.keys(values).length ? { ...look, values } : without('values'))
|
|
675
|
+
}} />`
|
|
676
|
+
}
|
|
677
|
+
if (c.kind === 'clip')
|
|
678
|
+
return html`<select ?disabled=${this.locked} @change=${(e: Event) => (typed(e) ? write({ ...look, clip: typed(e) }) : write(without('clip')))}>
|
|
679
|
+
<option value="" ?selected=${look.clip === undefined}>-</option>
|
|
680
|
+
${c.clips.map(clip => html`<option value=${clip} ?selected=${clip === look.clip}>${clip}</option>`)}
|
|
681
|
+
</select>`
|
|
682
|
+
if (c.kind === 'speed')
|
|
683
|
+
return html`<input type="number" min="0" max="3" step="any" .value=${look.speed === undefined ? '' : String(look.speed)} ?disabled=${this.locked} @change=${(e: Event) => {
|
|
684
|
+
const n = (e.target as HTMLInputElement).valueAsNumber
|
|
685
|
+
write(Number.isNaN(n) ? without('speed') : { ...look, speed: n })
|
|
686
|
+
}} />`
|
|
687
|
+
return html`<select ?disabled=${this.locked} @change=${(e: Event) => (typed(e) ? write({ ...look, paused: typed(e) === 'true' }) : write(without('paused')))}>
|
|
688
|
+
<option value="" ?selected=${look.paused === undefined}>-</option>
|
|
689
|
+
${[false, true].map(v => html`<option value=${String(v)} ?selected=${v === look.paused}>${i18next.t(v ? 'figure.label.data-paused' : 'figure.label.data-playing')}</option>`)}
|
|
456
690
|
</select>`
|
|
457
|
-
|
|
458
|
-
return html`<input type="number" step="any" .value=${value === undefined ? '' : String(value)} ?disabled=${this.locked} @change=${(e: Event) => write((e.target as HTMLInputElement).valueAsNumber)} />`
|
|
691
|
+
})
|
|
459
692
|
}
|
|
460
693
|
|
|
461
|
-
/**
|
|
462
|
-
private
|
|
463
|
-
const
|
|
694
|
+
/** The look of every other value: a row like the others, added or taken away; kept when a closed field's values are not all rows. */
|
|
695
|
+
private renderOtherwise(asset: FigureAsset, b: FigureDataTableBinding, columns: TableColumn[], needed: boolean, set: (next: FigureDataBinding) => void, keyCells: number) {
|
|
696
|
+
const look = b.table.default
|
|
697
|
+
if (look === undefined)
|
|
698
|
+
return html`<button act ?disabled=${this.locked} @click=${() => set(tableWithLook(b, 'default', {}))}><md-icon>add</md-icon>${i18next.t('figure.button.add-data-default')}</button>`
|
|
699
|
+
const { default: _, ...rest } = b.table
|
|
700
|
+
return html`<div looks style="grid-template-columns: ${gridOf(keyCells, columns.length)}">
|
|
701
|
+
<span style="grid-column: span ${keyCells}">${i18next.t('figure.label.data-otherwise')}</span>
|
|
702
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, 'default', l)))}
|
|
703
|
+
${needed ? html`<span></span>` : html`<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => set({ ...b, table: rest })}><md-icon>close</md-icon></button>`}
|
|
704
|
+
</div>`
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* A code table's rows: a code each, typed, with its look. A closed text's values follow the codes (ADR-0166 risk 3):
|
|
709
|
+
* a code added, renamed or taken away is the field's value too; true and false are both rows.
|
|
710
|
+
*/
|
|
711
|
+
private renderCodeRows(asset: FigureAsset, b: FigureDataTableBinding, set: (next: FigureDataBinding) => void) {
|
|
712
|
+
const columns = tableColumnsOf(asset, b)
|
|
713
|
+
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'))
|
|
714
|
+
const boolean = node?.kind === 'boolean'
|
|
715
|
+
const rows = Object.entries(b.table).filter(([k]) => k !== 'default')
|
|
464
716
|
const domain = node ? domainOf(node).map(String) : []
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
717
|
+
const covered = domain.length > 0 && domain.every(v => b.table[v] !== undefined)
|
|
718
|
+
/* The binding and the field it reads, in one undo step. */
|
|
719
|
+
const rekeyed = (list: Array<[string, FigureDataTableEntry]>) => {
|
|
720
|
+
const next = tableWithKeys(b, list)
|
|
721
|
+
const schema = boolean ? undefined : schemaFollowingCodes(asset, next, list.map(([k]) => k))
|
|
722
|
+
this.edit({ kind: 'set-data-binding', binding: next, ...(schema ? { schema } : {}) })
|
|
471
723
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
724
|
+
const rekey = (i: number, typed: string) => {
|
|
725
|
+
const key = typed.trim()
|
|
726
|
+
if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key)) return this.requestUpdate()
|
|
727
|
+
rekeyed(rows.map(([k, v], j) => (j === i ? [key, v] : [k, v])))
|
|
728
|
+
}
|
|
729
|
+
const word = (k: string) => (boolean ? i18next.t(k === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : k)
|
|
730
|
+
return html`<div looks style="grid-template-columns: ${gridOf(1, columns.length)}">
|
|
731
|
+
<small>${i18next.t('figure.label.data-case-value')}</small>${this.columnHeads(columns)}<span></span>
|
|
732
|
+
${rows.map(
|
|
733
|
+
([k, look], i) => html`${boolean
|
|
734
|
+
? html`<span>${word(k)}</span>`
|
|
735
|
+
: html`<input .value=${k} aria-label=${i18next.t('figure.label.data-case-value')} ?disabled=${this.locked} @change=${(e: Event) => rekey(i, (e.target as HTMLInputElement).value)} />`}
|
|
736
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
737
|
+
${boolean || rows.length < 2 && domain.length > 0
|
|
738
|
+
? html`<span></span>`
|
|
739
|
+
: html`<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => rekeyed(rows.filter((_, j) => j !== i))}><md-icon>close</md-icon></button>`}`
|
|
740
|
+
)}
|
|
741
|
+
</div>
|
|
742
|
+
${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set, 1)}
|
|
743
|
+
${boolean
|
|
744
|
+
? nothing
|
|
745
|
+
: html`<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [freeCodeOf(rows.map(([k]) => k)), {}]])}>
|
|
746
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-case')}
|
|
747
|
+
</button>`}`
|
|
479
748
|
}
|
|
480
749
|
|
|
481
|
-
/**
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
750
|
+
/**
|
|
751
|
+
* A range table's rows: from (inclusive) and to (exclusive), either end open, with a look each. Giving the last row,
|
|
752
|
+
* open above, an end starts the next row there; a row open above is the last there can be.
|
|
753
|
+
*/
|
|
754
|
+
private renderRangeRows(asset: FigureAsset, b: FigureDataTableBinding, set: (next: FigureDataBinding) => void) {
|
|
755
|
+
const columns = tableColumnsOf(asset, b)
|
|
756
|
+
const rows = Object.entries(b.table).filter(([k]) => k !== 'default')
|
|
486
757
|
const ends = (key: string) => {
|
|
487
758
|
const [from = '', to = ''] = key.split('~')
|
|
488
759
|
return { from, to }
|
|
489
760
|
}
|
|
490
|
-
const rekey = (i: number, end: 'from' | 'to', typed: string) =>
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
761
|
+
const rekey = (i: number, end: 'from' | 'to', typed: string) => {
|
|
762
|
+
const was = ends(rows[i]![0])
|
|
763
|
+
const next = { ...was, [end]: typed.trim() }
|
|
764
|
+
const key = `${next.from}~${next.to}`
|
|
765
|
+
/* A key another row has would take that row's place: not taken. */
|
|
766
|
+
if (rows.some(([k], j) => j !== i && k === key)) return this.requestUpdate()
|
|
767
|
+
const list: Array<[string, FigureDataTableEntry]> = rows.map(([k, v], j) => (j === i ? [key, v] : [k, v]))
|
|
768
|
+
/* The last row, open above, given an end: the next row starts there, open above, so every value still has a row. */
|
|
769
|
+
if (i === rows.length - 1 && end === 'to' && was.to === '' && next.to !== '') list.push([`${next.to}~`, {}])
|
|
770
|
+
set(tableWithKeys(b, list))
|
|
771
|
+
}
|
|
772
|
+
const last = rows.at(-1)
|
|
773
|
+
const lastEnd = last ? ends(last[0]).to : ''
|
|
774
|
+
const open = !!last && lastEnd === ''
|
|
775
|
+
return html`<div looks style="grid-template-columns: ${gridOf(2, columns.length)}">
|
|
776
|
+
<small>${i18next.t('figure.label.data-range-from')}</small><small>${i18next.t('figure.label.data-range-to')}</small>${this.columnHeads(columns)}<span></span>
|
|
777
|
+
${rows.map(([k, look], i) => {
|
|
502
778
|
const e = ends(k)
|
|
503
779
|
return html`<input type="number" step="any" .value=${e.from} ?disabled=${this.locked} @change=${(ev: Event) => rekey(i, 'from', (ev.target as HTMLInputElement).value)} />
|
|
504
780
|
<input type="number" step="any" .value=${e.to} ?disabled=${this.locked} @change=${(ev: Event) => rekey(i, 'to', (ev.target as HTMLInputElement).value)} />
|
|
505
|
-
${this.
|
|
506
|
-
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() =>
|
|
781
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
782
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => set(tableWithKeys(b, rows.filter((_, j) => j !== i)))}><md-icon>close</md-icon></button>`
|
|
507
783
|
})}
|
|
508
784
|
</div>
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<button act ?disabled=${this.locked} @click=${() => rebuild([...entries, [freeRangeKey(entries.map(([k]) => k)), entries.at(-1)?.[1] ?? param.default ?? 0]])}>
|
|
785
|
+
${this.renderOtherwise(asset, b, columns, false, set, 2)}
|
|
786
|
+
${open ? html`<p hint>${i18next.t('figure.text.data-range-close-last')}</p>` : nothing}
|
|
787
|
+
<button act ?disabled=${this.locked || open} @click=${() => set(tableWithKeys(b, [...rows, [`${lastEnd}~`, {}]]))}>
|
|
513
788
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-range')}
|
|
514
789
|
</button>`
|
|
515
790
|
}
|
|
516
791
|
|
|
517
792
|
/** Which field of each element is its id, its type, its place, its height in the stack, its status. */
|
|
518
|
-
private renderItemFields(asset: FigureAsset, b:
|
|
793
|
+
private renderItemFields(asset: FigureAsset, b: FigureDataValueBinding, node: FigureDataNode | undefined, set: (patch: Partial<FigureDataValueBinding>) => void) {
|
|
519
794
|
const behavior = b.behavior
|
|
520
795
|
if (behavior.kind !== 'items') return nothing
|
|
521
796
|
let element: FigureDataNode | undefined = node
|
|
@@ -524,7 +799,7 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
524
799
|
const pick = (role: 'id' | 'type' | 'place' | 'stack' | 'status', typed: string) => {
|
|
525
800
|
const next: Record<string, unknown> = { ...behavior, [role]: typed }
|
|
526
801
|
if (!typed) delete next[role]
|
|
527
|
-
set({ behavior: next as
|
|
802
|
+
set({ behavior: next as FigureDataValueBinding['behavior'] })
|
|
528
803
|
}
|
|
529
804
|
return html`<div form>
|
|
530
805
|
${node?.kind === 'list'
|
|
@@ -541,40 +816,78 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
541
816
|
const slot = (e.target as HTMLInputElement).value.trim()
|
|
542
817
|
const next: Record<string, unknown> = { ...behavior, slot }
|
|
543
818
|
if (!slot) delete next.slot
|
|
544
|
-
set({ behavior: next as
|
|
819
|
+
set({ behavior: next as FigureDataValueBinding['behavior'] })
|
|
545
820
|
}} />
|
|
546
821
|
</div>`
|
|
547
822
|
}
|
|
548
823
|
|
|
824
|
+
/**
|
|
825
|
+
* 「연결 추가」 (ADR-0166 10): a field there or a new name, then a method the field's kind allows. As it is or scaled,
|
|
826
|
+
* the one thing it moves; a table, the parts, controls and motion group it shows on -- a new name makes the field.
|
|
827
|
+
*/
|
|
549
828
|
private renderAddBinding(asset: FigureAsset, rows: FigureDataRow[], choices: BehaviorChoice[]) {
|
|
550
829
|
const bindable = rows.filter(r => r.node.kind !== 'object')
|
|
551
|
-
if (!bindable.length) return nothing
|
|
552
830
|
const a = this.connecting
|
|
553
|
-
|
|
554
|
-
const
|
|
831
|
+
/* 「새 필드」: a value no field path can have (a path is names and `[]`). */
|
|
832
|
+
const NEW = '+'
|
|
833
|
+
const row = a.path === NEW || !bindable.length ? null : (bindable.find(r => r.path === a.path) ?? bindable[0]!)
|
|
834
|
+
const methods = methodsOf(row?.node ?? null, choices)
|
|
835
|
+
const method = methods.includes(a.method as DataMethod) ? (a.method as DataMethod) : methods[0]
|
|
836
|
+
const table = method === 'code' || method === 'ranges'
|
|
837
|
+
const fitting = row && !table ? choices.filter(c => fitsField(row.node, c)) : []
|
|
555
838
|
const choice = fitting.find(c => c.key === a.behavior) ?? fitting[0]
|
|
556
|
-
|
|
557
|
-
const
|
|
839
|
+
const set = (patch: Partial<typeof a>) => (this.connecting = { ...a, ...patch })
|
|
840
|
+
const typed = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
841
|
+
const name = a.name.trim()
|
|
842
|
+
const targets = { parts: a.parts, inputs: a.inputs, ...(a.group ? { group: a.group } : {}) }
|
|
843
|
+
const aimed = a.parts.length > 0 || a.inputs.length > 0 || !!a.group
|
|
844
|
+
const ready = table ? aimed && (row !== null || /^[A-Za-z_][\w-]*$/.test(name)) : !!choice && !!row
|
|
845
|
+
const add = () => {
|
|
846
|
+
if (table) {
|
|
847
|
+
const { binding, schema } = tableDraftOf(asset, row ? { path: row.path, node: row.node } : { name }, method as 'code' | 'ranges', targets)
|
|
848
|
+
this.edit({ kind: 'set-data-binding', binding, ...(schema ? { schema } : {}) })
|
|
849
|
+
this.opened = binding.id
|
|
850
|
+
} else {
|
|
851
|
+
/* A list is read whole by items; one value of it, through its first element. */
|
|
852
|
+
const accessor = row!.node.kind === 'list' && choice!.kind === 'items' ? row!.path.replace(/\[\]$/, '') : accessorOfRow(row!.path)
|
|
853
|
+
const binding = bindingDraftOf(asset, accessor, row!.node, choice!, method as 'value' | 'scale')
|
|
854
|
+
this.edit({ kind: 'set-data-binding', binding })
|
|
855
|
+
this.opened = binding.id
|
|
856
|
+
}
|
|
857
|
+
this.connecting = { path: '', name: '', method: '', behavior: '', parts: [], inputs: [], group: '' }
|
|
858
|
+
}
|
|
859
|
+
const toggle = (list: string[], id: string, on: boolean) => (on ? [...list, id] : list.filter(x => x !== id))
|
|
558
860
|
return html`<div form>
|
|
559
861
|
<span>${i18next.t('figure.label.data-field')}</span>
|
|
560
|
-
<select @change=${(e: Event) => (
|
|
862
|
+
<select @change=${(e: Event) => set({ path: typed(e), name: '' })}>
|
|
561
863
|
${bindable.map(r => html`<option value=${r.path} ?selected=${r === row}>${r.path}</option>`)}
|
|
864
|
+
<option value=${NEW} ?selected=${row === null}>${i18next.t('figure.label.data-new-field')}</option>
|
|
562
865
|
</select>
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
866
|
+
${row === null
|
|
867
|
+
? html`<span>${i18next.t('figure.label.data-name')}</span>
|
|
868
|
+
<input .value=${a.name} @input=${(e: Event) => set({ name: typed(e) })} />`
|
|
869
|
+
: nothing}
|
|
870
|
+
<span>${i18next.t('figure.label.data-method')}</span>
|
|
871
|
+
<select ?disabled=${!methods.length} @change=${(e: Event) => set({ method: typed(e) as DataMethod })}>
|
|
872
|
+
${methods.map(m => html`<option value=${m} ?selected=${m === method}>${i18next.t('figure.label.data-method-' + m)}</option>`)}
|
|
566
873
|
</select>
|
|
874
|
+
${!table && fitting.length
|
|
875
|
+
? html`<span>${i18next.t('figure.label.data-behavior')}</span>
|
|
876
|
+
<select @change=${(e: Event) => set({ behavior: typed(e) })}>
|
|
877
|
+
${fitting.map(c => html`<option value=${c.key} ?selected=${c === choice}>${this.behaviorName(asset, choices, c.behavior)}</option>`)}
|
|
878
|
+
</select>`
|
|
879
|
+
: nothing}
|
|
567
880
|
</div>
|
|
568
|
-
${
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
>
|
|
881
|
+
${table
|
|
882
|
+
? this.renderTargets(partsOf(asset), stateInputsOf(asset), asset.motionGroups ?? [], targets, {
|
|
883
|
+
part: (id, on) => set({ parts: toggle(a.parts, id, on) }),
|
|
884
|
+
input: (id, on) => set({ inputs: toggle(a.inputs, id, on) }),
|
|
885
|
+
group: id => set({ group: id })
|
|
886
|
+
})
|
|
887
|
+
: nothing}
|
|
888
|
+
${table && !aimed ? html`<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>` : nothing}
|
|
889
|
+
${methods.length ? nothing : html`<p hint>${i18next.t('figure.text.data-no-behavior')}</p>`}
|
|
890
|
+
<button act ?disabled=${this.locked || !ready} @click=${add}>
|
|
578
891
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-binding')}
|
|
579
892
|
</button>`
|
|
580
893
|
}
|
|
@@ -654,56 +967,6 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
654
967
|
</details>`
|
|
655
968
|
}
|
|
656
969
|
|
|
657
|
-
/* ---------------------------------------------------------------- status ports */
|
|
658
|
-
|
|
659
|
-
private renderPorts(asset: FigureAsset) {
|
|
660
|
-
const ports = asset.dataInterface?.ports ?? []
|
|
661
|
-
const tokens = Object.keys(asset.palette)
|
|
662
|
-
const parts = partsOf(asset)
|
|
663
|
-
const set = (port: FigureStatusPort, patch: Partial<FigureStatusPort>) => this.edit({ kind: 'set-status-port', port: { ...port, ...patch } })
|
|
664
|
-
const chosen = (e: Event) => [...(e.target as HTMLSelectElement).selectedOptions].map(o => o.value)
|
|
665
|
-
return html`<section>
|
|
666
|
-
<h3>${i18next.t('figure.label.data-ports')}${this.help.button('figure.text.data-ports-explained')}</h3>
|
|
667
|
-
${this.help.hint('figure.text.data-ports-explained')}
|
|
668
|
-
${ports.map(
|
|
669
|
-
port => html`<section item>
|
|
670
|
-
<div head>
|
|
671
|
-
<strong>${port.label}</strong>
|
|
672
|
-
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-port')} @click=${() => this.edit({ kind: 'remove-status-port', id: port.id })}>
|
|
673
|
-
<md-icon>close</md-icon>
|
|
674
|
-
</button>
|
|
675
|
-
</div>
|
|
676
|
-
<div form>
|
|
677
|
-
<span>${i18next.t('figure.label.data-label')}</span>
|
|
678
|
-
<input .value=${port.label} ?disabled=${this.locked} @change=${(e: Event) => set(port, { label: (e.target as HTMLInputElement).value.trim() })} />
|
|
679
|
-
<span>${i18next.t('figure.label.data-port-tokens')}</span>
|
|
680
|
-
<select multiple size=${Math.min(4, Math.max(tokens.length, 1))} ?disabled=${this.locked} @change=${(e: Event) => set(port, { tokens: chosen(e) })}>
|
|
681
|
-
${tokens.map(t => html`<option value=${t} ?selected=${port.tokens.includes(t)}>${t}</option>`)}
|
|
682
|
-
</select>
|
|
683
|
-
<span>${i18next.t('figure.label.data-port-parts')}</span>
|
|
684
|
-
<select multiple size=${Math.min(4, Math.max(parts.length, 1))} ?disabled=${this.locked} @change=${(e: Event) => set(port, { parts: chosen(e) })}>
|
|
685
|
-
${parts.map(p => html`<option value=${p} ?selected=${port.parts.includes(p)}>${p}</option>`)}
|
|
686
|
-
</select>
|
|
687
|
-
</div>
|
|
688
|
-
</section>`
|
|
689
|
-
)}
|
|
690
|
-
<div form>
|
|
691
|
-
<span>${i18next.t('figure.label.data-label')}</span>
|
|
692
|
-
<input .value=${this.porting} @input=${(e: Event) => (this.porting = (e.target as HTMLInputElement).value)} />
|
|
693
|
-
</div>
|
|
694
|
-
<button
|
|
695
|
-
act
|
|
696
|
-
?disabled=${this.locked || !this.porting.trim()}
|
|
697
|
-
@click=${() => {
|
|
698
|
-
this.edit({ kind: 'set-status-port', port: portDraftOf(asset, this.porting) })
|
|
699
|
-
this.porting = ''
|
|
700
|
-
}}
|
|
701
|
-
>
|
|
702
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-port')}
|
|
703
|
-
</button>
|
|
704
|
-
</section>`
|
|
705
|
-
}
|
|
706
|
-
|
|
707
970
|
/* ---------------------------------------------------------------- the sample */
|
|
708
971
|
|
|
709
972
|
/** Data to try, read the way a placed figure reads it: what it moves, or why it is refused; in the preview, the figure running on it. */
|
|
@@ -716,8 +979,10 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
716
979
|
} catch {
|
|
717
980
|
unreadable = true
|
|
718
981
|
}
|
|
719
|
-
const
|
|
982
|
+
const legends = this.sampleLegends()
|
|
983
|
+
const read = unreadable ? null : sampleOf(asset, data, legends)
|
|
720
984
|
const r = read?.result
|
|
985
|
+
const tables = (asset.dataInterface?.bindings ?? []).filter((b): b is FigureDataTableBinding => b.rule === 'table')
|
|
721
986
|
return html`<section>
|
|
722
987
|
<h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
|
|
723
988
|
${this.help.hint('figure.text.data-sample-explained')}
|
|
@@ -736,11 +1001,14 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
736
1001
|
${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`, m.paused ? i18next.t('figure.label.data-paused') : ''].filter(Boolean).join(' ')}
|
|
737
1002
|
</li>`
|
|
738
1003
|
)}
|
|
739
|
-
${
|
|
1004
|
+
${tables.map(b => this.renderTableUse(asset, b, read!.uses[b.id]))}
|
|
1005
|
+
${Object.entries(r!.parts).map(([part, look]) => html`<li>${part}: ${[look.color, look.effect ? i18next.t('figure.label.data-effect-' + look.effect.kind) : ''].filter(Boolean).join(' ')}</li>`)}
|
|
740
1006
|
${Object.entries(r!.pose).map(([axis, p]) => html`<li>${i18next.t('figure.label.data-axis-' + axis)} = ${p!.value}</li>`)}
|
|
741
1007
|
${r!.items.map(i => html`<li>${i18next.t('figure.label.data-items-count', { count: i.records.length })}${i.records.length ? `: ${i.records.map(x => x.id).join(', ')}` : ''}</li>`)}
|
|
742
1008
|
</ul>`}
|
|
1009
|
+
${read?.notes.length ? html`<p hint>${read.notes.map(n => i18next.t('figure.text.data-note-' + n.reason, { binding: n.binding ?? '', key: n.key ?? '', column: n.column ? i18next.t('figure.label.data-column-' + n.column) : '', value: shownValue(n.value) })).join(' · ')}</p>` : nothing}
|
|
743
1010
|
${r && Object.keys(r.pose).length ? html`<p hint>${i18next.t('figure.text.data-sample-moves-all')}</p>` : nothing}
|
|
1011
|
+
${this.renderSampleLegends(tables)}
|
|
744
1012
|
<label toggle>
|
|
745
1013
|
<input
|
|
746
1014
|
type="checkbox"
|
|
@@ -755,6 +1023,53 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
755
1023
|
</section>`
|
|
756
1024
|
}
|
|
757
1025
|
|
|
1026
|
+
/** Which row of a table the sample's value matched, and from which table: the figure's own or the sample legend of its role. */
|
|
1027
|
+
private renderTableUse(asset: FigureAsset, b: FigureDataTableBinding, use: { value: unknown; key: string | null; source: string; legend?: string } | undefined) {
|
|
1028
|
+
if (!use || use.value === undefined) return nothing
|
|
1029
|
+
const word = (v: unknown) => (typeof v === 'boolean' || v === 'true' || v === 'false' ? i18next.t(String(v) === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : String(v))
|
|
1030
|
+
const row = use.key === null ? i18next.t('figure.label.data-none') : use.key === 'default' ? i18next.t('figure.label.data-otherwise') : word(use.key)
|
|
1031
|
+
const from = use.source === 'figure' ? i18next.t('figure.label.data-table-own') : i18next.t('figure.label.data-table-legend', { name: use.legend ?? '' })
|
|
1032
|
+
return html`<li>${b.label ?? b.accessor} = ${word(use.value)} → ${row} <small>(${from})</small></li>`
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/** The sample legends as read: by role, those that are JSON. */
|
|
1036
|
+
private sampleLegends(): Record<string, unknown> {
|
|
1037
|
+
const out: Record<string, unknown> = {}
|
|
1038
|
+
for (const [role, text] of Object.entries(this.legendSample)) {
|
|
1039
|
+
if (!text.trim()) continue
|
|
1040
|
+
try {
|
|
1041
|
+
out[role] = JSON.parse(text)
|
|
1042
|
+
} catch {
|
|
1043
|
+
/* Not JSON yet: the figure's own table shows. */
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return out
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* A legend pasted for each table role, to see the figure as a board's legend of that role would show it -- not saved.
|
|
1051
|
+
* The preview shows it too, as a scene's legend named as the role.
|
|
1052
|
+
*/
|
|
1053
|
+
private renderSampleLegends(tables: FigureDataTableBinding[]) {
|
|
1054
|
+
const roles = [...new Set(tables.flatMap(b => (b.legend ? [b.legend.role] : [])))]
|
|
1055
|
+
if (!roles.length) return nothing
|
|
1056
|
+
return html`<details>
|
|
1057
|
+
<summary>${i18next.t('figure.label.data-sample-legend')}</summary>
|
|
1058
|
+
<p hint>${i18next.t('figure.text.data-sample-legend-explained')}</p>
|
|
1059
|
+
${roles.map(
|
|
1060
|
+
role => html`<label>${role}</label>
|
|
1061
|
+
<textarea
|
|
1062
|
+
.value=${this.legendSample[role] ?? ''}
|
|
1063
|
+
placeholder='{ "CODE": { "color": "#22c55e" }, "default": {} }'
|
|
1064
|
+
@input=${(e: Event) => {
|
|
1065
|
+
this.legendSample = { ...this.legendSample, [role]: (e.target as HTMLTextAreaElement).value }
|
|
1066
|
+
this.dispatchEvent(new CustomEvent('legend-sample', { detail: { legends: this.sampleLegends() }, bubbles: true, composed: true }))
|
|
1067
|
+
}}
|
|
1068
|
+
></textarea>`
|
|
1069
|
+
)}
|
|
1070
|
+
</details>`
|
|
1071
|
+
}
|
|
1072
|
+
|
|
758
1073
|
private try(typed: string) {
|
|
759
1074
|
this.sample = typed
|
|
760
1075
|
try {
|
|
@@ -781,18 +1096,10 @@ function nodeOfKind(kind: FigureDataNode['kind']): FigureDataNode {
|
|
|
781
1096
|
return { kind } as FigureDataNode
|
|
782
1097
|
}
|
|
783
1098
|
|
|
784
|
-
/**
|
|
785
|
-
|
|
786
|
-
const keep = b.param?.default
|
|
787
|
-
if (rule === 'value') return undefined
|
|
788
|
-
if (rule === 'scale') return { scale: 1, offset: 0 }
|
|
789
|
-
if (rule === 'range') return { [`${node?.kind === 'number' ? (node.min ?? '') : ''}~`]: keep ?? 0 }
|
|
790
|
-
const domain = node ? domainOf(node).map(String) : []
|
|
791
|
-
return domain.length ? Object.fromEntries(domain.map(v => [v, keep ?? 0])) : { default: keep ?? 0 }
|
|
792
|
-
}
|
|
1099
|
+
/** A value in a sentence: a text as it is, anything else as JSON. */
|
|
1100
|
+
const shownValue = (v: unknown): string => (typeof v === 'string' ? v : JSON.stringify(v ?? ''))
|
|
793
1101
|
|
|
794
|
-
/**
|
|
795
|
-
function
|
|
796
|
-
|
|
797
|
-
return `${end}~`
|
|
1102
|
+
/** A table row's columns: the key's one or two, a cell per column, and the remove button. */
|
|
1103
|
+
function gridOf(keyCells: number, columns: number): string {
|
|
1104
|
+
return `${'minmax(64px, 1fr) '.repeat(keyCells)}${'minmax(72px, 1fr) '.repeat(columns)}30px`
|
|
798
1105
|
}
|