@things-factory/figure-ui 10.1.223 → 10.1.225
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-styles.ts +3 -2
- package/client/modeller/editor.ts +17 -8
- package/client/modeller/figure-capability-panel.ts +151 -1
- package/client/modeller/figure-clips.ts +1 -1
- package/client/modeller/figure-data-panel.ts +509 -199
- package/client/modeller/figure-links.ts +130 -12
- package/client/modeller/figure-preview.ts +65 -8
- package/client/modeller/figure-slot-layout-panel.ts +7 -3
- package/client/modeller/links.ts +58 -0
- package/client/modeller/product-session.ts +3 -2
- package/client/modeller/refusals.ts +12 -4
- package/client/modeller/slot-layout-host.ts +76 -5
- package/client/pages/figure-modeller-page.ts +2 -2
- 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-styles.js +3 -2
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +5 -2
- package/dist-client/modeller/editor.js +21 -8
- 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 +129 -1
- package/dist-client/modeller/figure-capability-panel.js.map +1 -1
- package/dist-client/modeller/figure-clips.js +1 -1
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +57 -13
- package/dist-client/modeller/figure-data-panel.js +476 -191
- 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 +65 -7
- 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/product-session.d.ts +4 -2
- package/dist-client/modeller/product-session.js +3 -2
- package/dist-client/modeller/product-session.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/pages/figure-modeller-page.js +2 -2
- package/dist-client/pages/figure-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/test/ai-proposal.test.ts +2 -2
- package/test/browser/data-table-page.js +9 -0
- package/test/browser/links.check.mjs +25 -3
- package/test/browser/product-page.check.mjs +25 -0
- package/test/browser/side-panel-sweep.check.mjs +182 -0
- package/test/consumer-capabilities.test.ts +116 -0
- package/test/data-motion.test.ts +94 -27
- package/test/figure-assistant-surface.test.ts +1 -1
- package/test/first-save.test.ts +2 -2
- package/test/links.test.ts +15 -0
- package/test/product-session.test.ts +5 -5
- package/test/proposal-record.test.ts +3 -3
- package/test/revert-keeping.test.ts +1 -1
- package/test/slot-layout-host.test.ts +63 -5
- package/translations/en.json +82 -23
- package/translations/es.json +82 -23
- package/translations/ja.json +82 -23
- package/translations/ko.json +82 -23
- package/translations/ms.json +82 -23
- package/translations/zh.json +82 -23
|
@@ -4,34 +4,42 @@ import { css, html, LitElement, nothing } from 'lit';
|
|
|
4
4
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
5
5
|
import { i18next, localize } from '@operato/i18n';
|
|
6
6
|
import { FIGURE_UNITS, figureDataExample, inferFigureDataSchema } from '@hatiolab/figure-model';
|
|
7
|
-
import { accessorOfRow, behaviorKeyOf, behaviorsOf, bindingDraftOf, domainOf, endlessJointsOf, fieldAt, fieldOfKind, fitsField,
|
|
7
|
+
import { accessorOfRow, behaviorKeyOf, behaviorsOf, bindingDraftOf, domainOf, endlessJointsOf, fieldAt, fieldOfKind, fitsField, freeCodeOf, legendRoleOf, methodOf, methodsOf, partsOf, rotationStepsOf, sampleOf, schemaRowsOf, schemaWithField, schemaWithFieldReplaced, schemaWithoutField, schemaFollowingCodes, stateInputsOf, tableColumnsOf, tableDraftOf, tableWithKeys, tableWithLook, tableWithTargets } from './data-interface.js';
|
|
8
8
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
|
|
9
|
+
import { figureBindingsTakenOver, figureCapabilityDescriptorOf } from '@hatiolab/things-scene';
|
|
10
|
+
import { consumerDescriptors } from './consumer-capabilities.js';
|
|
9
11
|
import { HelpToggles } from './help.js';
|
|
10
12
|
const KINDS = ['number', 'boolean', 'text', 'object', 'list'];
|
|
11
13
|
/**
|
|
12
14
|
* 데이터 -- the data a site sends this figure, in its own names, and what each value moves (ADR-0051 §8, data binding
|
|
13
15
|
* design decision 4). The schema is free: fields the author names, a tree of objects and lists. A binding reads one place
|
|
14
|
-
* of it and
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* 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
|
|
17
|
+
* group's clip, speed or pause, its position, the items in its places; by a code table or a range table, each value's
|
|
18
|
+
* look goes to several targets at once -- parts' colour and effect, controls' values, a motion group. The field's kind
|
|
19
|
+
* says which methods there are. Every change is one part edit, and whether it holds is the kernel's check.
|
|
17
20
|
*
|
|
18
21
|
* The sample is not saved: data tried here, read the one way a placed figure reads it, and -- in the preview -- the figure
|
|
19
|
-
* running on it (`data-sample`)
|
|
22
|
+
* running on it (`data-sample`); a sample legend pasted for a table's role shows it as a board's legend would
|
|
23
|
+
* (`legend-sample`).
|
|
20
24
|
*/
|
|
21
25
|
let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
22
26
|
constructor() {
|
|
23
27
|
super(...arguments);
|
|
24
28
|
this.help = new HelpToggles(this);
|
|
25
29
|
this.locked = false;
|
|
30
|
+
/** The part chosen in the modeller, put among a table's parts by one button. */
|
|
31
|
+
this.selected = '';
|
|
26
32
|
/** The schema row chosen, by its path; its field is edited under it. */
|
|
27
33
|
this.chosen = '';
|
|
28
34
|
this.adding = { name: '', kind: 'number' };
|
|
29
35
|
/** A site's data pasted to start the schema from; nothing is kept until the author says so. */
|
|
30
36
|
this.pasted = '';
|
|
31
|
-
|
|
37
|
+
/** A binding as it is being added: the field (or a new field's name), the method, the behavior or the table's targets. */
|
|
38
|
+
this.connecting = { path: '', name: '', method: '', behavior: '', parts: [], inputs: [], group: '' };
|
|
32
39
|
/** The binding opened to edit its rule, by id. */
|
|
33
40
|
this.opened = '';
|
|
34
|
-
|
|
41
|
+
/** Sample legends by role, as JSON typed; not saved. */
|
|
42
|
+
this.legendSample = {};
|
|
35
43
|
/** The rotation card as filled in: the joint, and the fields for on, speed (and its most rpm) and direction. */
|
|
36
44
|
this.rotating = { input: '', on: '', speed: '', max: 0, direction: '', forward: '', reverse: '' };
|
|
37
45
|
/** The sample as typed, JSON; empty until edited, the schema's example shown until then. */
|
|
@@ -116,6 +124,17 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
116
124
|
gap: 4px 6px;
|
|
117
125
|
align-items: center;
|
|
118
126
|
}
|
|
127
|
+
div[codes] {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
130
|
+
gap: 4px 6px;
|
|
131
|
+
align-items: center;
|
|
132
|
+
}
|
|
133
|
+
div[codes] button[act] {
|
|
134
|
+
width: 30px;
|
|
135
|
+
margin: 0;
|
|
136
|
+
padding: 0;
|
|
137
|
+
}
|
|
119
138
|
div[ranges] {
|
|
120
139
|
display: grid;
|
|
121
140
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
@@ -127,6 +146,54 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
127
146
|
margin: 0;
|
|
128
147
|
padding: 0;
|
|
129
148
|
}
|
|
149
|
+
/* A table's rows: the key (a code, or from and to), a cell per column, and a remove button. */
|
|
150
|
+
div[looks] {
|
|
151
|
+
display: grid;
|
|
152
|
+
gap: 4px 6px;
|
|
153
|
+
align-items: center;
|
|
154
|
+
overflow-x: auto;
|
|
155
|
+
}
|
|
156
|
+
div[looks] > small {
|
|
157
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
158
|
+
}
|
|
159
|
+
div[looks] input,
|
|
160
|
+
div[looks] select {
|
|
161
|
+
min-width: 0;
|
|
162
|
+
width: 100%;
|
|
163
|
+
box-sizing: border-box;
|
|
164
|
+
}
|
|
165
|
+
div[looks] button[act] {
|
|
166
|
+
width: 30px;
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: 0;
|
|
169
|
+
}
|
|
170
|
+
/* A colour and the button that clears it, on one line at the height of the cells beside it. */
|
|
171
|
+
div[looks] span[colour] {
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: 2px;
|
|
174
|
+
align-items: center;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
}
|
|
177
|
+
div[looks] span[colour] input[type='color'] {
|
|
178
|
+
flex: 1;
|
|
179
|
+
height: var(--figure-control-height);
|
|
180
|
+
padding: 0 2px;
|
|
181
|
+
}
|
|
182
|
+
div[looks] span[colour] button[act] {
|
|
183
|
+
flex: none;
|
|
184
|
+
width: 24px;
|
|
185
|
+
}
|
|
186
|
+
div[chips] {
|
|
187
|
+
display: flex;
|
|
188
|
+
flex-wrap: wrap;
|
|
189
|
+
gap: 4px 10px;
|
|
190
|
+
align-items: center;
|
|
191
|
+
}
|
|
192
|
+
div[chips] label {
|
|
193
|
+
display: inline-flex;
|
|
194
|
+
gap: 4px;
|
|
195
|
+
align-items: center;
|
|
196
|
+
}
|
|
130
197
|
textarea {
|
|
131
198
|
width: 100%;
|
|
132
199
|
min-height: 96px;
|
|
@@ -151,9 +218,19 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
151
218
|
return nothing;
|
|
152
219
|
const rows = schemaRowsOf(asset);
|
|
153
220
|
return html `
|
|
154
|
-
${this.
|
|
221
|
+
${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${this.renderBindings(asset, rows)} ${rows.length ? this.renderSample(asset) : nothing}
|
|
155
222
|
`;
|
|
156
223
|
}
|
|
224
|
+
/** The data a product's capability the figure wears reads itself (ADR-0167 14): said, read only. */
|
|
225
|
+
renderCapabilityData(asset) {
|
|
226
|
+
const names = asset.document.capabilities.names ?? [];
|
|
227
|
+
const reading = consumerDescriptors().filter(d => names.includes(d.name) && d.data?.length);
|
|
228
|
+
if (!reading.length)
|
|
229
|
+
return nothing;
|
|
230
|
+
return html `<section>
|
|
231
|
+
${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>`)}
|
|
232
|
+
</section>`;
|
|
233
|
+
}
|
|
157
234
|
/* ---------------------------------------------------------------- the schema */
|
|
158
235
|
renderSchema(asset, rows) {
|
|
159
236
|
const schema = asset.dataInterface?.schema ?? [];
|
|
@@ -225,7 +302,7 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
225
302
|
this.edit({ kind: 'set-data-schema', schema: schemaWithoutField(schema, row.path) });
|
|
226
303
|
}}
|
|
227
304
|
>
|
|
228
|
-
<md-icon>
|
|
305
|
+
<md-icon>delete</md-icon>
|
|
229
306
|
</button>
|
|
230
307
|
</div>
|
|
231
308
|
<div form>
|
|
@@ -290,22 +367,26 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
290
367
|
renderAddField(schema, under) {
|
|
291
368
|
const a = this.adding;
|
|
292
369
|
const named = a.name.trim();
|
|
370
|
+
const add = () => {
|
|
371
|
+
if (this.locked || !named)
|
|
372
|
+
return;
|
|
373
|
+
this.edit({ kind: 'set-data-schema', schema: schemaWithField(schema, under, { name: named, ...nodeOfKind(a.kind) }) });
|
|
374
|
+
this.adding = { ...a, name: '' };
|
|
375
|
+
};
|
|
293
376
|
return html `<div form>
|
|
294
377
|
<span>${i18next.t('figure.label.data-name')}</span>
|
|
295
|
-
<input
|
|
378
|
+
<input
|
|
379
|
+
aria-label=${i18next.t('figure.label.data-name')}
|
|
380
|
+
.value=${a.name}
|
|
381
|
+
@input=${(e) => (this.adding = { ...a, name: e.target.value })}
|
|
382
|
+
@keydown=${(e) => e.key === 'Enter' && !e.isComposing && add()}
|
|
383
|
+
/>
|
|
296
384
|
<span>${i18next.t('figure.label.data-kind')}</span>
|
|
297
385
|
<select @change=${(e) => (this.adding = { ...a, kind: e.target.value })}>
|
|
298
386
|
${KINDS.map(k => html `<option value=${k} ?selected=${k === a.kind}>${i18next.t('figure.label.data-kind-' + k)}</option>`)}
|
|
299
387
|
</select>
|
|
300
388
|
</div>
|
|
301
|
-
<button
|
|
302
|
-
act
|
|
303
|
-
?disabled=${this.locked || !named}
|
|
304
|
-
@click=${() => {
|
|
305
|
-
this.edit({ kind: 'set-data-schema', schema: schemaWithField(schema, under, { name: named, ...nodeOfKind(a.kind) }) });
|
|
306
|
-
this.adding = { ...a, name: '' };
|
|
307
|
-
}}
|
|
308
|
-
>
|
|
389
|
+
<button act ?disabled=${this.locked || !named} @click=${add}>
|
|
309
390
|
<md-icon>add</md-icon>${i18next.t(under ? 'figure.button.add-data-child' : 'figure.button.add-data-field', { parent: under })}
|
|
310
391
|
</button>`;
|
|
311
392
|
}
|
|
@@ -346,141 +427,309 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
346
427
|
const name = b.kind === 'pose' ? i18next.t('figure.label.data-axis-' + b.axis) : (choice?.name ?? key.slice(key.indexOf(':') + 1));
|
|
347
428
|
return i18next.t('figure.label.data-behavior-' + b.kind, { name });
|
|
348
429
|
}
|
|
430
|
+
/** What a table shows on, as one line: its parts, the controls it sets, its motion group. */
|
|
431
|
+
targetsName(asset, b) {
|
|
432
|
+
const inputs = stateInputsOf(asset);
|
|
433
|
+
const group = (asset.motionGroups ?? []).find(g => g.id === b.targets.group);
|
|
434
|
+
return [
|
|
435
|
+
...(b.targets.parts ?? []),
|
|
436
|
+
...(b.targets.inputs ?? []).map(id => inputs.find(i => i.id === id)?.label ?? id),
|
|
437
|
+
...(group ? [group.label] : [])
|
|
438
|
+
].join(', ');
|
|
439
|
+
}
|
|
349
440
|
renderBindings(asset, rows) {
|
|
350
441
|
const bindings = asset.dataInterface?.bindings ?? [];
|
|
351
442
|
const choices = behaviorsOf(asset);
|
|
443
|
+
const resting = figureBindingsTakenOver(asset);
|
|
352
444
|
return html `<section>
|
|
353
445
|
<h3>${i18next.t('figure.label.data-bindings')}${this.help.button('figure.text.data-bindings-explained')}</h3>
|
|
354
446
|
${this.help.hint('figure.text.data-bindings-explained')}
|
|
355
|
-
${bindings.map(b => this.renderBinding(asset, choices, b))}
|
|
356
|
-
${
|
|
447
|
+
${bindings.map(b => this.renderBinding(asset, choices, b, resting))}
|
|
448
|
+
${this.renderAddBinding(asset, rows, choices)}
|
|
357
449
|
${this.renderRotation(asset, rows)}
|
|
358
450
|
</section>`;
|
|
359
451
|
}
|
|
360
|
-
renderBinding(asset, choices, b) {
|
|
452
|
+
renderBinding(asset, choices, b, resting) {
|
|
361
453
|
const open = this.opened === b.id;
|
|
362
|
-
const set = (
|
|
363
|
-
|
|
364
|
-
if (patch.param === undefined && 'param' in patch)
|
|
365
|
-
delete next.param;
|
|
366
|
-
this.edit({ kind: 'set-data-binding', binding: next });
|
|
367
|
-
};
|
|
454
|
+
const set = (next) => this.edit({ kind: 'set-data-binding', binding: next });
|
|
455
|
+
const what = b.rule === 'table' ? this.targetsName(asset, b) : this.behaviorName(asset, choices, b.behavior);
|
|
368
456
|
return html `<section item>
|
|
369
457
|
<div head>
|
|
370
458
|
<button line aria-expanded=${open ? 'true' : 'false'} @click=${() => (this.opened = open ? '' : b.id)}>
|
|
371
|
-
<strong>${b.accessor}</strong> → ${
|
|
459
|
+
<strong>${b.label ?? b.accessor}</strong> → ${what} <small>· ${i18next.t('figure.label.data-method-' + methodOf(b))}</small>
|
|
372
460
|
</button>
|
|
373
461
|
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-binding')} @click=${() => this.edit({ kind: 'remove-data-binding', id: b.id })}>
|
|
374
|
-
<md-icon>
|
|
462
|
+
<md-icon>delete</md-icon>
|
|
375
463
|
</button>
|
|
376
464
|
</div>
|
|
377
|
-
${
|
|
465
|
+
${resting.filter(r => r.id === b.id).map(r => this.renderResting(r))}
|
|
466
|
+
${open ? (b.rule === 'table' ? this.renderTable(asset, b, set) : this.renderValueDetail(asset, b, set)) : nothing}
|
|
378
467
|
</section>`;
|
|
379
468
|
}
|
|
380
|
-
|
|
469
|
+
/**
|
|
470
|
+
* A binding a product's capability takes over where the product registers it (ADR-0167 17): there it rests, whole or for
|
|
471
|
+
* the targets named; elsewhere (a board without the product) it works.
|
|
472
|
+
*/
|
|
473
|
+
renderResting(r) {
|
|
474
|
+
const where = r.capability.split('.')[0];
|
|
475
|
+
const name = figureCapabilityDescriptorOf(r.capability)?.label ?? r.capability;
|
|
476
|
+
const targets = [...r.parts, ...r.inputs].join(', ');
|
|
477
|
+
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>`;
|
|
478
|
+
}
|
|
479
|
+
renderValueDetail(asset, b, set) {
|
|
381
480
|
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'));
|
|
382
481
|
const value = (e) => e.target.value;
|
|
383
482
|
const behavior = b.behavior;
|
|
483
|
+
const patch = (p) => {
|
|
484
|
+
const next = { ...b, ...p };
|
|
485
|
+
if ('param' in p && p.param === undefined)
|
|
486
|
+
delete next.param;
|
|
487
|
+
set(next);
|
|
488
|
+
};
|
|
384
489
|
if (behavior.kind === 'items')
|
|
385
|
-
return this.renderItemFields(asset, b, node,
|
|
490
|
+
return this.renderItemFields(asset, b, node, patch);
|
|
386
491
|
return html `<div form>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
492
|
+
<span>${i18next.t('figure.label.data-method')}</span>
|
|
493
|
+
${node?.kind === 'number'
|
|
494
|
+
? html `<select ?disabled=${this.locked} @change=${(e) => patch({ rule: value(e), param: value(e) === 'scale' ? { scale: 1, offset: 0 } : undefined })}>
|
|
495
|
+
${['value', 'scale'].map(r => html `<option value=${r} ?selected=${r === b.rule}>${i18next.t('figure.label.data-method-' + r)}</option>`)}
|
|
496
|
+
</select>`
|
|
497
|
+
: html `<span>${i18next.t('figure.label.data-method-value')}</span>`}
|
|
498
|
+
${behavior.kind === 'state'
|
|
392
499
|
? html `<span>${i18next.t('figure.label.data-source-mode')}</span>
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
500
|
+
<select ?disabled=${this.locked} @change=${(e) => patch({ behavior: { ...behavior, sourceMode: value(e) } })}>
|
|
501
|
+
${['observed', 'commanded'].map(m => html `<option value=${m} ?selected=${behavior.sourceMode === m}>${i18next.t('figure.label.data-' + m)}</option>`)}
|
|
502
|
+
</select>`
|
|
396
503
|
: nothing}
|
|
397
|
-
|
|
504
|
+
${behavior.kind === 'pose'
|
|
398
505
|
? html `<span>${i18next.t('figure.label.data-seconds')}</span>
|
|
399
|
-
|
|
506
|
+
<input type="number" min="0" step="any" .value=${String(behavior.seconds ?? 0)} ?disabled=${this.locked} @change=${(e) => patch({ behavior: { ...behavior, seconds: e.target.valueAsNumber || 0 } })} />`
|
|
400
507
|
: nothing}
|
|
401
|
-
|
|
508
|
+
${b.rule === 'scale'
|
|
402
509
|
? html `<span>${i18next.t('figure.label.data-scale')}</span>
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
510
|
+
<input type="number" step="any" .value=${String(b.param?.scale ?? 1)} ?disabled=${this.locked} @change=${(e) => patch({ param: { ...b.param, scale: e.target.valueAsNumber } })} />
|
|
511
|
+
<span>${i18next.t('figure.label.data-offset')}</span>
|
|
512
|
+
<input type="number" step="any" .value=${String(b.param?.offset ?? 0)} ?disabled=${this.locked} @change=${(e) => patch({ param: { ...b.param, offset: e.target.valueAsNumber } })} />`
|
|
513
|
+
: nothing}
|
|
514
|
+
</div>`;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* A table (ADR-0166 10): what it shows on -- parts, controls, a motion group -- its rows, a code or an interval each
|
|
518
|
+
* with a cell per column, the look of every other value, and whether a board may swap it for a legend.
|
|
519
|
+
*/
|
|
520
|
+
renderTable(asset, b, set) {
|
|
521
|
+
const value = (e) => e.target.value;
|
|
522
|
+
const parts = partsOf(asset);
|
|
523
|
+
const inputs = stateInputsOf(asset);
|
|
524
|
+
const groups = asset.motionGroups ?? [];
|
|
525
|
+
const targets = b.targets;
|
|
526
|
+
const toggle = (list, id, on) => (on ? [...(list ?? []), id] : (list ?? []).filter(x => x !== id));
|
|
527
|
+
const retarget = (t) => set(tableWithTargets(b, t));
|
|
528
|
+
return html `<div form>
|
|
529
|
+
<span>${i18next.t('figure.label.data-label')}</span>
|
|
530
|
+
<input .value=${b.label ?? ''} placeholder=${b.accessor} ?disabled=${this.locked} @change=${(e) => {
|
|
531
|
+
const label = value(e).trim();
|
|
532
|
+
const next = { ...b, label };
|
|
533
|
+
if (!label)
|
|
534
|
+
delete next.label;
|
|
535
|
+
set(next);
|
|
536
|
+
}} />
|
|
537
|
+
</div>
|
|
538
|
+
${this.renderTargets(parts, inputs, groups, targets, {
|
|
539
|
+
part: (id, on) => retarget({ ...targets, parts: toggle(targets.parts, id, on) }),
|
|
540
|
+
input: (id, on) => retarget({ ...targets, inputs: toggle(targets.inputs, id, on) }),
|
|
541
|
+
group: id => retarget({ ...targets, group: id || undefined })
|
|
542
|
+
})}
|
|
543
|
+
${b.lookup === 'code' ? this.renderCodeRows(asset, b, set) : this.renderRangeRows(asset, b, set)}
|
|
544
|
+
<label toggle>
|
|
545
|
+
<input type="checkbox" .checked=${!!b.legend} ?disabled=${this.locked} @change=${(e) => {
|
|
546
|
+
const next = { ...b };
|
|
547
|
+
if (e.target.checked)
|
|
548
|
+
next.legend = { role: legendRoleOf(b.id) };
|
|
549
|
+
else
|
|
550
|
+
delete next.legend;
|
|
551
|
+
set(next);
|
|
552
|
+
}} />
|
|
553
|
+
${i18next.t('figure.label.data-legend-swappable')}
|
|
554
|
+
</label>
|
|
555
|
+
${b.legend
|
|
556
|
+
? html `<div form>
|
|
557
|
+
<span>${i18next.t('figure.label.data-legend-role')}</span>
|
|
558
|
+
<input .value=${b.legend.role} ?disabled=${this.locked} @change=${(e) => set({ ...b, legend: { role: value(e).trim() } })} />
|
|
559
|
+
</div>
|
|
560
|
+
<p hint>${i18next.t('figure.text.data-legend-explained')}</p>`
|
|
561
|
+
: nothing}`;
|
|
562
|
+
}
|
|
563
|
+
/** Which parts, controls and motion group a table shows on: chips to tick, and the part chosen in the modeller by one button. */
|
|
564
|
+
renderTargets(parts, inputs, groups, t, on) {
|
|
565
|
+
const chosen = (e) => e.target.checked;
|
|
566
|
+
return html `<div form>
|
|
567
|
+
<span>${i18next.t('figure.label.data-target-parts')}</span>
|
|
568
|
+
<div chips>
|
|
569
|
+
${parts.map(p => html `<label><input type="checkbox" .checked=${!!t.parts?.includes(p)} ?disabled=${this.locked} @change=${(e) => on.part(p, chosen(e))} />${p}</label>`)}
|
|
570
|
+
${this.selected && parts.includes(this.selected) && !t.parts?.includes(this.selected)
|
|
571
|
+
? 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>`
|
|
406
572
|
: nothing}
|
|
407
573
|
</div>
|
|
408
|
-
${
|
|
574
|
+
${inputs.length
|
|
575
|
+
? html `<span>${i18next.t('figure.label.data-target-inputs')}</span>
|
|
576
|
+
<div chips>
|
|
577
|
+
${inputs.map(i => html `<label><input type="checkbox" .checked=${!!t.inputs?.includes(i.id)} ?disabled=${this.locked} @change=${(e) => on.input(i.id, chosen(e))} />${i.label}</label>`)}
|
|
578
|
+
</div>`
|
|
579
|
+
: nothing}
|
|
580
|
+
${groups.length
|
|
581
|
+
? html `<span>${i18next.t('figure.label.data-target-group')}</span>
|
|
582
|
+
<select ?disabled=${this.locked} @change=${(e) => on.group(e.target.value)}>
|
|
583
|
+
<option value="" ?selected=${!t.group}>${i18next.t('figure.label.data-none')}</option>
|
|
584
|
+
${groups.map(g => html `<option value=${g.id} ?selected=${g.id === t.group}>${g.label}</option>`)}
|
|
585
|
+
</select>`
|
|
586
|
+
: nothing}
|
|
587
|
+
</div>`;
|
|
409
588
|
}
|
|
410
|
-
/**
|
|
411
|
-
|
|
412
|
-
|
|
589
|
+
/** The column heads of a table's rows, after the key's own one or two. */
|
|
590
|
+
columnHeads(columns) {
|
|
591
|
+
return columns.map(c => c.kind === 'value'
|
|
592
|
+
? html `<small>${c.label}${c.unit ? ` (${c.unit}${c.min !== undefined && c.max !== undefined ? ` ${c.min}~${c.max}` : ''})` : ''}</small>`
|
|
593
|
+
: html `<small>${i18next.t('figure.label.data-column-' + c.kind)}</small>`);
|
|
594
|
+
}
|
|
595
|
+
/** 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. */
|
|
596
|
+
lookCells(asset, columns, look, write) {
|
|
597
|
+
const without = (key) => {
|
|
598
|
+
const next = { ...look };
|
|
599
|
+
delete next[key];
|
|
600
|
+
return next;
|
|
601
|
+
};
|
|
413
602
|
const typed = (e) => e.target.value;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
603
|
+
return columns.map(c => {
|
|
604
|
+
if (c.kind === 'color')
|
|
605
|
+
return look.color === undefined
|
|
606
|
+
? html `<button act ?disabled=${this.locked} @click=${() => write({ ...look, color: '#888888' })}>${i18next.t('figure.button.data-set-result')}</button>`
|
|
607
|
+
: html `<span colour><input type="color" .value=${look.color} ?disabled=${this.locked} @change=${(e) => 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>`;
|
|
608
|
+
if (c.kind === 'effect')
|
|
609
|
+
return html `<select ?disabled=${this.locked} @change=${(e) => (typed(e) ? write({ ...look, effect: { kind: typed(e) } }) : write(without('effect')))}>
|
|
610
|
+
${['', 'blink', 'glow'].map(k => html `<option value=${k} ?selected=${(look.effect?.kind ?? '') === k}>${i18next.t('figure.label.data-effect-' + (k || 'none'))}</option>`)}
|
|
611
|
+
</select>`;
|
|
612
|
+
if (c.kind === 'value') {
|
|
613
|
+
const v = look.values?.[c.id];
|
|
614
|
+
return html `<input type="number" step="any" min=${c.min ?? ''} max=${c.max ?? ''} .value=${v === undefined ? '' : String(v)} ?disabled=${this.locked} @change=${(e) => {
|
|
615
|
+
const n = e.target.valueAsNumber;
|
|
616
|
+
const values = { ...look.values };
|
|
617
|
+
if (Number.isNaN(n))
|
|
618
|
+
delete values[c.id];
|
|
619
|
+
else
|
|
620
|
+
values[c.id] = n;
|
|
621
|
+
write(Object.keys(values).length ? { ...look, values } : without('values'));
|
|
622
|
+
}} />`;
|
|
623
|
+
}
|
|
624
|
+
if (c.kind === 'clip')
|
|
625
|
+
return html `<select ?disabled=${this.locked} @change=${(e) => (typed(e) ? write({ ...look, clip: typed(e) }) : write(without('clip')))}>
|
|
626
|
+
<option value="" ?selected=${look.clip === undefined}>-</option>
|
|
627
|
+
${c.clips.map(clip => html `<option value=${clip} ?selected=${clip === look.clip}>${clip}</option>`)}
|
|
628
|
+
</select>`;
|
|
629
|
+
if (c.kind === 'speed')
|
|
630
|
+
return html `<input type="number" min="0" max="3" step="any" .value=${look.speed === undefined ? '' : String(look.speed)} ?disabled=${this.locked} @change=${(e) => {
|
|
631
|
+
const n = e.target.valueAsNumber;
|
|
632
|
+
write(Number.isNaN(n) ? without('speed') : { ...look, speed: n });
|
|
633
|
+
}} />`;
|
|
634
|
+
return html `<select ?disabled=${this.locked} @change=${(e) => (typed(e) ? write({ ...look, paused: typed(e) === 'true' }) : write(without('paused')))}>
|
|
635
|
+
<option value="" ?selected=${look.paused === undefined}>-</option>
|
|
636
|
+
${[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>`)}
|
|
428
637
|
</select>`;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
/** 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. */
|
|
641
|
+
renderOtherwise(asset, b, columns, needed, set, keyCells) {
|
|
642
|
+
const look = b.table.default;
|
|
643
|
+
if (look === undefined)
|
|
644
|
+
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>`;
|
|
645
|
+
const { default: _, ...rest } = b.table;
|
|
646
|
+
return html `<div looks style="grid-template-columns: ${gridOf(keyCells, columns.length)}">
|
|
647
|
+
<span style="grid-column: span ${keyCells}">${i18next.t('figure.label.data-otherwise')}</span>
|
|
648
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, 'default', l)))}
|
|
649
|
+
${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>delete</md-icon></button>`}
|
|
650
|
+
</div>`;
|
|
432
651
|
}
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
652
|
+
/**
|
|
653
|
+
* A code table's rows: a code each, typed, with its look. A closed text's values follow the codes (ADR-0166 risk 3):
|
|
654
|
+
* a code added, renamed or taken away is the field's value too; true and false are both rows.
|
|
655
|
+
*/
|
|
656
|
+
renderCodeRows(asset, b, set) {
|
|
657
|
+
const columns = tableColumnsOf(asset, b);
|
|
658
|
+
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'));
|
|
659
|
+
const boolean = node?.kind === 'boolean';
|
|
660
|
+
const rows = Object.entries(b.table).filter(([k]) => k !== 'default');
|
|
436
661
|
const domain = node ? domainOf(node).map(String) : [];
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
next[key] = v;
|
|
444
|
-
set({ param: next });
|
|
662
|
+
const covered = domain.length > 0 && domain.every(v => b.table[v] !== undefined);
|
|
663
|
+
/* The binding and the field it reads, in one undo step. */
|
|
664
|
+
const rekeyed = (list) => {
|
|
665
|
+
const next = tableWithKeys(b, list);
|
|
666
|
+
const schema = boolean ? undefined : schemaFollowingCodes(asset, next, list.map(([k]) => k));
|
|
667
|
+
this.edit({ kind: 'set-data-binding', binding: next, ...(schema ? { schema } : {}) });
|
|
445
668
|
};
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
669
|
+
const rekey = (i, typed) => {
|
|
670
|
+
const key = typed.trim();
|
|
671
|
+
if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key))
|
|
672
|
+
return this.requestUpdate();
|
|
673
|
+
rekeyed(rows.map(([k, v], j) => (j === i ? [key, v] : [k, v])));
|
|
674
|
+
};
|
|
675
|
+
const word = (k) => (boolean ? i18next.t(k === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : k);
|
|
676
|
+
return html `<div looks style="grid-template-columns: ${gridOf(1, columns.length)}">
|
|
677
|
+
<small>${i18next.t('figure.label.data-case-value')}</small>${this.columnHeads(columns)}<span></span>
|
|
678
|
+
${rows.map(([k, look], i) => html `${boolean
|
|
679
|
+
? html `<span>${word(k)}</span>`
|
|
680
|
+
: html `<input .value=${k} aria-label=${i18next.t('figure.label.data-case-value')} ?disabled=${this.locked} @change=${(e) => rekey(i, e.target.value)} />`}
|
|
681
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
682
|
+
${boolean || rows.length < 2 && domain.length > 0
|
|
683
|
+
? html `<span></span>`
|
|
684
|
+
: html `<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => rekeyed(rows.filter((_, j) => j !== i))}><md-icon>delete</md-icon></button>`}`)}
|
|
685
|
+
</div>
|
|
686
|
+
${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set, 1)}
|
|
687
|
+
${boolean
|
|
688
|
+
? nothing
|
|
689
|
+
: html `<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [freeCodeOf(rows.map(([k]) => k)), {}]])}>
|
|
690
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-case')}
|
|
691
|
+
</button>`}`;
|
|
453
692
|
}
|
|
454
|
-
/**
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
693
|
+
/**
|
|
694
|
+
* A range table's rows: from (inclusive) and to (exclusive), either end open, with a look each. Giving the last row,
|
|
695
|
+
* open above, an end starts the next row there; a row open above is the last there can be.
|
|
696
|
+
*/
|
|
697
|
+
renderRangeRows(asset, b, set) {
|
|
698
|
+
const columns = tableColumnsOf(asset, b);
|
|
699
|
+
const rows = Object.entries(b.table).filter(([k]) => k !== 'default');
|
|
459
700
|
const ends = (key) => {
|
|
460
701
|
const [from = '', to = ''] = key.split('~');
|
|
461
702
|
return { from, to };
|
|
462
703
|
};
|
|
463
|
-
const rekey = (i, end, typed) =>
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
704
|
+
const rekey = (i, end, typed) => {
|
|
705
|
+
const was = ends(rows[i][0]);
|
|
706
|
+
const next = { ...was, [end]: typed.trim() };
|
|
707
|
+
const key = `${next.from}~${next.to}`;
|
|
708
|
+
/* A key another row has would take that row's place: not taken. */
|
|
709
|
+
if (rows.some(([k], j) => j !== i && k === key))
|
|
710
|
+
return this.requestUpdate();
|
|
711
|
+
const list = rows.map(([k, v], j) => (j === i ? [key, v] : [k, v]));
|
|
712
|
+
/* The last row, open above, given an end: the next row starts there, open above, so every value still has a row. */
|
|
713
|
+
if (i === rows.length - 1 && end === 'to' && was.to === '' && next.to !== '')
|
|
714
|
+
list.push([`${next.to}~`, {}]);
|
|
715
|
+
set(tableWithKeys(b, list));
|
|
716
|
+
};
|
|
717
|
+
const last = rows.at(-1);
|
|
718
|
+
const lastEnd = last ? ends(last[0]).to : '';
|
|
719
|
+
const open = !!last && lastEnd === '';
|
|
720
|
+
return html `<div looks style="grid-template-columns: ${gridOf(2, columns.length)}">
|
|
721
|
+
<small>${i18next.t('figure.label.data-range-from')}</small><small>${i18next.t('figure.label.data-range-to')}</small>${this.columnHeads(columns)}<span></span>
|
|
722
|
+
${rows.map(([k, look], i) => {
|
|
473
723
|
const e = ends(k);
|
|
474
724
|
return html `<input type="number" step="any" .value=${e.from} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'from', ev.target.value)} />
|
|
475
725
|
<input type="number" step="any" .value=${e.to} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'to', ev.target.value)} />
|
|
476
|
-
${this.
|
|
477
|
-
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() =>
|
|
726
|
+
${this.lookCells(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
727
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => set(tableWithKeys(b, rows.filter((_, j) => j !== i)))}><md-icon>delete</md-icon></button>`;
|
|
478
728
|
})}
|
|
479
729
|
</div>
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
<button act ?disabled=${this.locked} @click=${() => rebuild([...entries, [freeRangeKey(entries.map(([k]) => k)), entries.at(-1)?.[1] ?? param.default ?? 0]])}>
|
|
730
|
+
${this.renderOtherwise(asset, b, columns, false, set, 2)}
|
|
731
|
+
${open ? html `<p hint>${i18next.t('figure.text.data-range-close-last')}</p>` : nothing}
|
|
732
|
+
<button act ?disabled=${this.locked || open} @click=${() => set(tableWithKeys(b, [...rows, [`${lastEnd}~`, {}]]))}>
|
|
484
733
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-range')}
|
|
485
734
|
</button>`;
|
|
486
735
|
}
|
|
@@ -517,36 +766,74 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
517
766
|
}} />
|
|
518
767
|
</div>`;
|
|
519
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* 「연결 추가」 (ADR-0166 10): a field there or a new name, then a method the field's kind allows. As it is or scaled,
|
|
771
|
+
* the one thing it moves; a table, the parts, controls and motion group it shows on -- a new name makes the field.
|
|
772
|
+
*/
|
|
520
773
|
renderAddBinding(asset, rows, choices) {
|
|
521
774
|
const bindable = rows.filter(r => r.node.kind !== 'object');
|
|
522
|
-
if (!bindable.length)
|
|
523
|
-
return nothing;
|
|
524
775
|
const a = this.connecting;
|
|
525
|
-
|
|
526
|
-
const
|
|
776
|
+
/* 「새 필드」: a value no field path can have (a path is names and `[]`). */
|
|
777
|
+
const NEW = '+';
|
|
778
|
+
const row = a.path === NEW || !bindable.length ? null : (bindable.find(r => r.path === a.path) ?? bindable[0]);
|
|
779
|
+
const methods = methodsOf(row?.node ?? null, choices);
|
|
780
|
+
const method = methods.includes(a.method) ? a.method : methods[0];
|
|
781
|
+
const table = method === 'code' || method === 'ranges';
|
|
782
|
+
const fitting = row && !table ? choices.filter(c => fitsField(row.node, c)) : [];
|
|
527
783
|
const choice = fitting.find(c => c.key === a.behavior) ?? fitting[0];
|
|
528
|
-
|
|
529
|
-
const
|
|
784
|
+
const set = (patch) => (this.connecting = { ...a, ...patch });
|
|
785
|
+
const typed = (e) => e.target.value;
|
|
786
|
+
const name = a.name.trim();
|
|
787
|
+
const targets = { parts: a.parts, inputs: a.inputs, ...(a.group ? { group: a.group } : {}) };
|
|
788
|
+
const aimed = a.parts.length > 0 || a.inputs.length > 0 || !!a.group;
|
|
789
|
+
const ready = table ? aimed && (row !== null || /^[A-Za-z_][\w-]*$/.test(name)) : !!choice && !!row;
|
|
790
|
+
const add = () => {
|
|
791
|
+
if (table) {
|
|
792
|
+
const { binding, schema } = tableDraftOf(asset, row ? { path: row.path, node: row.node } : { name }, method, targets);
|
|
793
|
+
this.edit({ kind: 'set-data-binding', binding, ...(schema ? { schema } : {}) });
|
|
794
|
+
this.opened = binding.id;
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
/* A list is read whole by items; one value of it, through its first element. */
|
|
798
|
+
const accessor = row.node.kind === 'list' && choice.kind === 'items' ? row.path.replace(/\[\]$/, '') : accessorOfRow(row.path);
|
|
799
|
+
const binding = bindingDraftOf(asset, accessor, row.node, choice, method);
|
|
800
|
+
this.edit({ kind: 'set-data-binding', binding });
|
|
801
|
+
this.opened = binding.id;
|
|
802
|
+
}
|
|
803
|
+
this.connecting = { path: '', name: '', method: '', behavior: '', parts: [], inputs: [], group: '' };
|
|
804
|
+
};
|
|
805
|
+
const toggle = (list, id, on) => (on ? [...list, id] : list.filter(x => x !== id));
|
|
530
806
|
return html `<div form>
|
|
531
807
|
<span>${i18next.t('figure.label.data-field')}</span>
|
|
532
|
-
<select @change=${(e) => (
|
|
808
|
+
<select @change=${(e) => set({ path: typed(e), name: '' })}>
|
|
533
809
|
${bindable.map(r => html `<option value=${r.path} ?selected=${r === row}>${r.path}</option>`)}
|
|
810
|
+
<option value=${NEW} ?selected=${row === null}>${i18next.t('figure.label.data-new-field')}</option>
|
|
534
811
|
</select>
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
812
|
+
${row === null
|
|
813
|
+
? html `<span>${i18next.t('figure.label.data-name')}</span>
|
|
814
|
+
<input .value=${a.name} @input=${(e) => set({ name: typed(e) })} />`
|
|
815
|
+
: nothing}
|
|
816
|
+
<span>${i18next.t('figure.label.data-method')}</span>
|
|
817
|
+
<select ?disabled=${!methods.length} @change=${(e) => set({ method: typed(e) })}>
|
|
818
|
+
${methods.map(m => html `<option value=${m} ?selected=${m === method}>${i18next.t('figure.label.data-method-' + m)}</option>`)}
|
|
538
819
|
</select>
|
|
820
|
+
${!table && fitting.length
|
|
821
|
+
? html `<span>${i18next.t('figure.label.data-behavior')}</span>
|
|
822
|
+
<select @change=${(e) => set({ behavior: typed(e) })}>
|
|
823
|
+
${fitting.map(c => html `<option value=${c.key} ?selected=${c === choice}>${this.behaviorName(asset, choices, c.behavior)}</option>`)}
|
|
824
|
+
</select>`
|
|
825
|
+
: nothing}
|
|
539
826
|
</div>
|
|
540
|
-
${
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
>
|
|
827
|
+
${table
|
|
828
|
+
? this.renderTargets(partsOf(asset), stateInputsOf(asset), asset.motionGroups ?? [], targets, {
|
|
829
|
+
part: (id, on) => set({ parts: toggle(a.parts, id, on) }),
|
|
830
|
+
input: (id, on) => set({ inputs: toggle(a.inputs, id, on) }),
|
|
831
|
+
group: id => set({ group: id })
|
|
832
|
+
})
|
|
833
|
+
: nothing}
|
|
834
|
+
${table && !aimed ? html `<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>` : nothing}
|
|
835
|
+
${methods.length ? nothing : html `<p hint>${i18next.t('figure.text.data-no-behavior')}</p>`}
|
|
836
|
+
<button act ?disabled=${this.locked || !ready} @click=${add}>
|
|
550
837
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-binding')}
|
|
551
838
|
</button>`;
|
|
552
839
|
}
|
|
@@ -624,52 +911,6 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
624
911
|
</button>
|
|
625
912
|
</details>`;
|
|
626
913
|
}
|
|
627
|
-
/* ---------------------------------------------------------------- status ports */
|
|
628
|
-
renderPorts(asset) {
|
|
629
|
-
const ports = asset.dataInterface?.ports ?? [];
|
|
630
|
-
const tokens = Object.keys(asset.palette);
|
|
631
|
-
const parts = partsOf(asset);
|
|
632
|
-
const set = (port, patch) => this.edit({ kind: 'set-status-port', port: { ...port, ...patch } });
|
|
633
|
-
const chosen = (e) => [...e.target.selectedOptions].map(o => o.value);
|
|
634
|
-
return html `<section>
|
|
635
|
-
<h3>${i18next.t('figure.label.data-ports')}${this.help.button('figure.text.data-ports-explained')}</h3>
|
|
636
|
-
${this.help.hint('figure.text.data-ports-explained')}
|
|
637
|
-
${ports.map(port => html `<section item>
|
|
638
|
-
<div head>
|
|
639
|
-
<strong>${port.label}</strong>
|
|
640
|
-
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-port')} @click=${() => this.edit({ kind: 'remove-status-port', id: port.id })}>
|
|
641
|
-
<md-icon>close</md-icon>
|
|
642
|
-
</button>
|
|
643
|
-
</div>
|
|
644
|
-
<div form>
|
|
645
|
-
<span>${i18next.t('figure.label.data-label')}</span>
|
|
646
|
-
<input .value=${port.label} ?disabled=${this.locked} @change=${(e) => set(port, { label: e.target.value.trim() })} />
|
|
647
|
-
<span>${i18next.t('figure.label.data-port-tokens')}</span>
|
|
648
|
-
<select multiple size=${Math.min(4, Math.max(tokens.length, 1))} ?disabled=${this.locked} @change=${(e) => set(port, { tokens: chosen(e) })}>
|
|
649
|
-
${tokens.map(t => html `<option value=${t} ?selected=${port.tokens.includes(t)}>${t}</option>`)}
|
|
650
|
-
</select>
|
|
651
|
-
<span>${i18next.t('figure.label.data-port-parts')}</span>
|
|
652
|
-
<select multiple size=${Math.min(4, Math.max(parts.length, 1))} ?disabled=${this.locked} @change=${(e) => set(port, { parts: chosen(e) })}>
|
|
653
|
-
${parts.map(p => html `<option value=${p} ?selected=${port.parts.includes(p)}>${p}</option>`)}
|
|
654
|
-
</select>
|
|
655
|
-
</div>
|
|
656
|
-
</section>`)}
|
|
657
|
-
<div form>
|
|
658
|
-
<span>${i18next.t('figure.label.data-label')}</span>
|
|
659
|
-
<input .value=${this.porting} @input=${(e) => (this.porting = e.target.value)} />
|
|
660
|
-
</div>
|
|
661
|
-
<button
|
|
662
|
-
act
|
|
663
|
-
?disabled=${this.locked || !this.porting.trim()}
|
|
664
|
-
@click=${() => {
|
|
665
|
-
this.edit({ kind: 'set-status-port', port: portDraftOf(asset, this.porting) });
|
|
666
|
-
this.porting = '';
|
|
667
|
-
}}
|
|
668
|
-
>
|
|
669
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-port')}
|
|
670
|
-
</button>
|
|
671
|
-
</section>`;
|
|
672
|
-
}
|
|
673
914
|
/* ---------------------------------------------------------------- the sample */
|
|
674
915
|
/** 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. */
|
|
675
916
|
renderSample(asset) {
|
|
@@ -682,8 +923,10 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
682
923
|
catch {
|
|
683
924
|
unreadable = true;
|
|
684
925
|
}
|
|
685
|
-
const
|
|
926
|
+
const legends = this.sampleLegends();
|
|
927
|
+
const read = unreadable ? null : sampleOf(asset, data, legends);
|
|
686
928
|
const r = read?.result;
|
|
929
|
+
const tables = (asset.dataInterface?.bindings ?? []).filter((b) => b.rule === 'table');
|
|
687
930
|
return html `<section>
|
|
688
931
|
<h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
|
|
689
932
|
${this.help.hint('figure.text.data-sample-explained')}
|
|
@@ -699,11 +942,14 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
699
942
|
${(asset.motionGroups ?? []).find(g => g.id === group)?.label ?? group}:
|
|
700
943
|
${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`, m.paused ? i18next.t('figure.label.data-paused') : ''].filter(Boolean).join(' ')}
|
|
701
944
|
</li>`)}
|
|
702
|
-
${
|
|
945
|
+
${tables.map(b => this.renderTableUse(asset, b, read.uses[b.id]))}
|
|
946
|
+
${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>`)}
|
|
703
947
|
${Object.entries(r.pose).map(([axis, p]) => html `<li>${i18next.t('figure.label.data-axis-' + axis)} = ${p.value}</li>`)}
|
|
704
948
|
${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>`)}
|
|
705
949
|
</ul>`}
|
|
950
|
+
${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}
|
|
706
951
|
${r && Object.keys(r.pose).length ? html `<p hint>${i18next.t('figure.text.data-sample-moves-all')}</p>` : nothing}
|
|
952
|
+
${this.renderSampleLegends(tables)}
|
|
707
953
|
<label toggle>
|
|
708
954
|
<input
|
|
709
955
|
type="checkbox"
|
|
@@ -717,6 +963,52 @@ let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement
|
|
|
717
963
|
</label>
|
|
718
964
|
</section>`;
|
|
719
965
|
}
|
|
966
|
+
/** 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. */
|
|
967
|
+
renderTableUse(asset, b, use) {
|
|
968
|
+
if (!use || use.value === undefined)
|
|
969
|
+
return nothing;
|
|
970
|
+
const word = (v) => (typeof v === 'boolean' || v === 'true' || v === 'false' ? i18next.t(String(v) === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : String(v));
|
|
971
|
+
const row = use.key === null ? i18next.t('figure.label.data-none') : use.key === 'default' ? i18next.t('figure.label.data-otherwise') : word(use.key);
|
|
972
|
+
const from = use.source === 'figure' ? i18next.t('figure.label.data-table-own') : i18next.t('figure.label.data-table-legend', { name: use.legend ?? '' });
|
|
973
|
+
return html `<li>${b.label ?? b.accessor} = ${word(use.value)} → ${row} <small>(${from})</small></li>`;
|
|
974
|
+
}
|
|
975
|
+
/** The sample legends as read: by role, those that are JSON. */
|
|
976
|
+
sampleLegends() {
|
|
977
|
+
const out = {};
|
|
978
|
+
for (const [role, text] of Object.entries(this.legendSample)) {
|
|
979
|
+
if (!text.trim())
|
|
980
|
+
continue;
|
|
981
|
+
try {
|
|
982
|
+
out[role] = JSON.parse(text);
|
|
983
|
+
}
|
|
984
|
+
catch {
|
|
985
|
+
/* Not JSON yet: the figure's own table shows. */
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* A legend pasted for each table role, to see the figure as a board's legend of that role would show it -- not saved.
|
|
992
|
+
* The preview shows it too, as a scene's legend named as the role.
|
|
993
|
+
*/
|
|
994
|
+
renderSampleLegends(tables) {
|
|
995
|
+
const roles = [...new Set(tables.flatMap(b => (b.legend ? [b.legend.role] : [])))];
|
|
996
|
+
if (!roles.length)
|
|
997
|
+
return nothing;
|
|
998
|
+
return html `<details>
|
|
999
|
+
<summary>${i18next.t('figure.label.data-sample-legend')}</summary>
|
|
1000
|
+
<p hint>${i18next.t('figure.text.data-sample-legend-explained')}</p>
|
|
1001
|
+
${roles.map(role => html `<label>${role}</label>
|
|
1002
|
+
<textarea
|
|
1003
|
+
.value=${this.legendSample[role] ?? ''}
|
|
1004
|
+
placeholder='{ "CODE": { "color": "#22c55e" }, "default": {} }'
|
|
1005
|
+
@input=${(e) => {
|
|
1006
|
+
this.legendSample = { ...this.legendSample, [role]: e.target.value };
|
|
1007
|
+
this.dispatchEvent(new CustomEvent('legend-sample', { detail: { legends: this.sampleLegends() }, bubbles: true, composed: true }));
|
|
1008
|
+
}}
|
|
1009
|
+
></textarea>`)}
|
|
1010
|
+
</details>`;
|
|
1011
|
+
}
|
|
720
1012
|
try(typed) {
|
|
721
1013
|
this.sample = typed;
|
|
722
1014
|
try {
|
|
@@ -742,6 +1034,10 @@ __decorate([
|
|
|
742
1034
|
property({ type: Boolean }),
|
|
743
1035
|
__metadata("design:type", Object)
|
|
744
1036
|
], FigureDataPanel.prototype, "locked", void 0);
|
|
1037
|
+
__decorate([
|
|
1038
|
+
property(),
|
|
1039
|
+
__metadata("design:type", Object)
|
|
1040
|
+
], FigureDataPanel.prototype, "selected", void 0);
|
|
745
1041
|
__decorate([
|
|
746
1042
|
state(),
|
|
747
1043
|
__metadata("design:type", Object)
|
|
@@ -765,7 +1061,7 @@ __decorate([
|
|
|
765
1061
|
__decorate([
|
|
766
1062
|
state(),
|
|
767
1063
|
__metadata("design:type", Object)
|
|
768
|
-
], FigureDataPanel.prototype, "
|
|
1064
|
+
], FigureDataPanel.prototype, "legendSample", void 0);
|
|
769
1065
|
__decorate([
|
|
770
1066
|
state(),
|
|
771
1067
|
__metadata("design:type", Object)
|
|
@@ -790,21 +1086,10 @@ function nodeOfKind(kind) {
|
|
|
790
1086
|
return { kind, item: { kind: 'object', fields: [] } };
|
|
791
1087
|
return { kind };
|
|
792
1088
|
}
|
|
793
|
-
/**
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (rule === 'scale')
|
|
799
|
-
return { scale: 1, offset: 0 };
|
|
800
|
-
if (rule === 'range')
|
|
801
|
-
return { [`${node?.kind === 'number' ? (node.min ?? '') : ''}~`]: keep ?? 0 };
|
|
802
|
-
const domain = node ? domainOf(node).map(String) : [];
|
|
803
|
-
return domain.length ? Object.fromEntries(domain.map(v => [v, keep ?? 0])) : { default: keep ?? 0 };
|
|
804
|
-
}
|
|
805
|
-
/** The next interval: from the last one's end, open above; open at both ends when there is none. */
|
|
806
|
-
function freeRangeKey(taken) {
|
|
807
|
-
const end = taken.at(-1)?.split('~')[1] ?? '';
|
|
808
|
-
return `${end}~`;
|
|
1089
|
+
/** A value in a sentence: a text as it is, anything else as JSON. */
|
|
1090
|
+
const shownValue = (v) => (typeof v === 'string' ? v : JSON.stringify(v ?? ''));
|
|
1091
|
+
/** A table row's columns: the key's one or two, a cell per column, and the remove button. */
|
|
1092
|
+
function gridOf(keyCells, columns) {
|
|
1093
|
+
return `${'minmax(64px, 1fr) '.repeat(keyCells)}${'minmax(72px, 1fr) '.repeat(columns)}30px`;
|
|
809
1094
|
}
|
|
810
1095
|
//# sourceMappingURL=figure-data-panel.js.map
|