@things-factory/figure-ui 10.1.162 → 10.1.163
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/data-interface.ts +227 -76
- package/client/modeller/editor.ts +3 -3
- package/client/modeller/figure-data-panel.ts +602 -193
- package/client/modeller/figure-preview.ts +46 -30
- package/client/modeller/refusals.ts +18 -14
- package/dist-client/modeller/data-interface.d.ts +53 -30
- package/dist-client/modeller/data-interface.js +228 -69
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor.d.ts +1 -1
- package/dist-client/modeller/editor.js +2 -2
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +35 -13
- package/dist-client/modeller/figure-data-panel.js +598 -186
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +7 -5
- package/dist-client/modeller/figure-preview.js +47 -30
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/refusals.d.ts +2 -2
- package/dist-client/modeller/refusals.js +24 -18
- package/dist-client/modeller/refusals.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/data-motion.test.ts +130 -52
- package/translations/en.json +109 -43
- package/translations/es.json +109 -43
- package/translations/ja.json +109 -43
- package/translations/ko.json +109 -43
- package/translations/ms.json +109 -43
- package/translations/zh.json +109 -43
|
@@ -3,284 +3,655 @@ import '@material/web/icon/icon.js';
|
|
|
3
3
|
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
|
-
import { FIGURE_UNITS } from '@hatiolab/figure-model';
|
|
7
|
-
import {
|
|
6
|
+
import { FIGURE_UNITS, figureDataExample, inferFigureDataSchema } from '@hatiolab/figure-model';
|
|
7
|
+
import { accessorOfRow, behaviorKeyOf, behaviorsOf, bindingDraftOf, domainOf, fieldAt, fieldOfKind, fitsField, partsOf, portDraftOf, sampleOf, schemaRowsOf, schemaWithField, schemaWithFieldReplaced, schemaWithoutField } from './data-interface.js';
|
|
8
8
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
|
|
9
9
|
import { HelpToggles } from './help.js';
|
|
10
|
+
const KINDS = ['number', 'boolean', 'text', 'object', 'list'];
|
|
10
11
|
/**
|
|
11
|
-
* 데이터 -- the figure
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* 데이터 -- the data a site sends this figure, in its own names, and what each value moves (ADR-0051 §8, data binding
|
|
13
|
+
* 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 moves one thing the figure can do -- a control, a colour, a motion group's clip, speed or pause, a status
|
|
15
|
+
* port, its position, the items in its places -- by the board's rules (value, map, range, scale). Every change is one
|
|
14
16
|
* part edit, and whether it holds is the kernel's check.
|
|
15
17
|
*
|
|
16
|
-
* The sample is not saved:
|
|
17
|
-
*
|
|
18
|
+
* 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`).
|
|
18
20
|
*/
|
|
19
21
|
let FigureDataPanel = class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
20
22
|
constructor() {
|
|
21
23
|
super(...arguments);
|
|
22
24
|
this.help = new HelpToggles(this);
|
|
23
25
|
this.locked = false;
|
|
24
|
-
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/** The schema row chosen, by its path; its field is edited under it. */
|
|
27
|
+
this.chosen = '';
|
|
28
|
+
this.adding = { name: '', kind: 'number' };
|
|
29
|
+
/** A site's data pasted to start the schema from; nothing is kept until the author says so. */
|
|
30
|
+
this.pasted = '';
|
|
31
|
+
this.connecting = { path: '', behavior: '' };
|
|
32
|
+
/** The binding opened to edit its rule, by id. */
|
|
33
|
+
this.opened = '';
|
|
34
|
+
this.porting = '';
|
|
35
|
+
/** The sample as typed, JSON; empty until edited, the schema's example shown until then. */
|
|
36
|
+
this.sample = '';
|
|
28
37
|
this.sampling = false;
|
|
29
38
|
}
|
|
30
|
-
static { this.styles = [
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
39
|
+
static { this.styles = [
|
|
40
|
+
SidePanelStyles,
|
|
41
|
+
css `
|
|
42
|
+
:host {
|
|
43
|
+
display: block;
|
|
44
|
+
}
|
|
45
|
+
ul[tree] {
|
|
46
|
+
display: grid;
|
|
47
|
+
gap: 2px;
|
|
48
|
+
margin: 0;
|
|
49
|
+
padding: 0;
|
|
50
|
+
list-style: none;
|
|
51
|
+
}
|
|
52
|
+
ul[tree] > li > button[row] {
|
|
53
|
+
display: flex;
|
|
54
|
+
flex-wrap: wrap;
|
|
55
|
+
gap: 0 6px;
|
|
56
|
+
width: 100%;
|
|
57
|
+
min-height: var(--figure-control-height);
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 4px 6px;
|
|
60
|
+
border: 1px solid transparent;
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
background: none;
|
|
63
|
+
color: inherit;
|
|
64
|
+
font: inherit;
|
|
65
|
+
text-align: left;
|
|
66
|
+
align-items: center;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
}
|
|
69
|
+
ul[tree] > li > button[row][selected] {
|
|
70
|
+
border-color: var(--md-sys-color-primary);
|
|
71
|
+
}
|
|
72
|
+
ul[tree] > li > button[row] small {
|
|
73
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
74
|
+
}
|
|
75
|
+
/* What the field is and what reads it: beside the name when it fits, under it when not. */
|
|
76
|
+
ul[tree] > li > button[row] small[meta] {
|
|
77
|
+
margin-left: auto;
|
|
78
|
+
text-align: right;
|
|
79
|
+
}
|
|
80
|
+
section[item] {
|
|
81
|
+
display: grid;
|
|
82
|
+
gap: 8px;
|
|
83
|
+
padding: 8px;
|
|
84
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
85
|
+
border-radius: 8px;
|
|
86
|
+
}
|
|
87
|
+
div[head] {
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-columns: minmax(0, 1fr) 30px;
|
|
90
|
+
gap: 6px;
|
|
91
|
+
align-items: center;
|
|
92
|
+
}
|
|
93
|
+
div[head] button[line] {
|
|
94
|
+
min-width: 0;
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
border: none;
|
|
98
|
+
background: none;
|
|
99
|
+
color: inherit;
|
|
100
|
+
font: inherit;
|
|
101
|
+
text-align: left;
|
|
102
|
+
overflow-wrap: anywhere;
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
div[head] button[act] {
|
|
106
|
+
width: 30px;
|
|
107
|
+
min-height: var(--figure-control-height);
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
div[cases] {
|
|
112
|
+
display: grid;
|
|
113
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
|
|
114
|
+
gap: 4px 6px;
|
|
115
|
+
align-items: center;
|
|
116
|
+
}
|
|
117
|
+
div[ranges] {
|
|
118
|
+
display: grid;
|
|
119
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
120
|
+
gap: 4px 6px;
|
|
121
|
+
align-items: center;
|
|
122
|
+
}
|
|
123
|
+
div[ranges] button[act] {
|
|
124
|
+
width: 30px;
|
|
125
|
+
margin: 0;
|
|
126
|
+
padding: 0;
|
|
127
|
+
}
|
|
128
|
+
textarea {
|
|
129
|
+
width: 100%;
|
|
130
|
+
min-height: 96px;
|
|
131
|
+
box-sizing: border-box;
|
|
132
|
+
font-family: var(--ui-font-mono, monospace);
|
|
133
|
+
font-size: inherit;
|
|
134
|
+
}
|
|
135
|
+
ul[result] {
|
|
136
|
+
margin: 0;
|
|
137
|
+
padding-left: 16px;
|
|
138
|
+
}
|
|
139
|
+
md-icon {
|
|
140
|
+
font-size: 16px;
|
|
141
|
+
width: 16px;
|
|
142
|
+
height: 16px;
|
|
143
|
+
}
|
|
144
|
+
`
|
|
145
|
+
]; }
|
|
73
146
|
render() {
|
|
74
147
|
const asset = this.asset;
|
|
75
148
|
if (!asset)
|
|
76
149
|
return nothing;
|
|
77
|
-
const
|
|
78
|
-
const connections = asset.dataInterface?.connections ?? [];
|
|
150
|
+
const rows = schemaRowsOf(asset);
|
|
79
151
|
return html `
|
|
80
|
-
|
|
81
|
-
<h3>${i18next.t('figure.label.data-properties')}${this.help.button('figure.text.data-properties-explained')}</h3>
|
|
82
|
-
${this.help.hint('figure.text.data-properties-explained')}
|
|
83
|
-
${properties.map(p => this.renderProperty(p))}
|
|
84
|
-
${this.renderAddProperty(asset)}
|
|
85
|
-
</section>
|
|
86
|
-
<section>
|
|
87
|
-
<h3>${i18next.t('figure.label.data-connections')}${this.help.button('figure.text.data-connections-explained')}</h3>
|
|
88
|
-
${this.help.hint('figure.text.data-connections-explained')}
|
|
89
|
-
${connections.map(c => this.renderConnection(asset, c, properties.find(p => p.id === c.property)))}
|
|
90
|
-
${properties.length ? this.renderAddConnection(asset, properties) : html `<p hint>${i18next.t('figure.text.data-no-properties')}</p>`}
|
|
91
|
-
</section>
|
|
92
|
-
${properties.length ? this.renderSample(asset, properties) : nothing}
|
|
152
|
+
${this.renderSchema(asset, rows)} ${this.renderBindings(asset, rows)} ${this.renderPorts(asset)} ${rows.length ? this.renderSample(asset) : nothing}
|
|
93
153
|
`;
|
|
94
154
|
}
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
/* ---------------------------------------------------------------- the schema */
|
|
156
|
+
renderSchema(asset, rows) {
|
|
157
|
+
const schema = asset.dataInterface?.schema ?? [];
|
|
158
|
+
return html `<section>
|
|
159
|
+
<h3>${i18next.t('figure.label.data-schema')}${this.help.button('figure.text.data-schema-explained')}</h3>
|
|
160
|
+
${this.help.hint('figure.text.data-schema-explained')}
|
|
161
|
+
${rows.length
|
|
162
|
+
? html `<ul tree>
|
|
163
|
+
${rows.map(row => html `<li>
|
|
164
|
+
<button row ?selected=${row.path === this.chosen} style="padding-left: ${6 + row.depth * 14}px" @click=${() => (this.chosen = row.path === this.chosen ? '' : row.path)}>
|
|
165
|
+
<span>${row.name}${row.label ? html ` <small>${row.label}</small>` : nothing}</span>
|
|
166
|
+
<small meta>${this.kindWord(row.node)}${row.bindings.length ? ` · ${i18next.t('figure.label.data-bound', { count: row.bindings.length })}` : ''}</small>
|
|
167
|
+
</button>
|
|
168
|
+
${row.path === this.chosen ? this.renderField(asset, schema, row) : nothing}
|
|
169
|
+
</li>`)}
|
|
170
|
+
</ul>`
|
|
171
|
+
: html `<p hint>${i18next.t('figure.text.data-no-fields')}</p>`}
|
|
172
|
+
${this.renderAddField(schema, '')} ${this.renderFromSample(schema)}
|
|
173
|
+
</section>`;
|
|
174
|
+
}
|
|
175
|
+
kindWord(node) {
|
|
176
|
+
const kind = i18next.t('figure.label.data-kind-' + node.kind);
|
|
177
|
+
if (node.kind === 'number') {
|
|
178
|
+
const range = node.min !== undefined || node.max !== undefined ? ` ${node.min ?? ''}~${node.max ?? ''}` : '';
|
|
179
|
+
return `${kind}${node.unit ? ` ${node.unit}` : ''}${range}`;
|
|
180
|
+
}
|
|
181
|
+
if (node.kind === 'text' && node.values)
|
|
182
|
+
return `${kind} (${node.values.join(', ')})`;
|
|
183
|
+
if (node.kind === 'list')
|
|
184
|
+
return `${kind} · ${i18next.t('figure.label.data-kind-' + node.item.kind)}`;
|
|
185
|
+
return kind;
|
|
186
|
+
}
|
|
187
|
+
/** The chosen field's own settings, and a field added under it when it holds fields. */
|
|
188
|
+
renderField(asset, schema, row) {
|
|
189
|
+
const field = fieldAt(schema, row.path);
|
|
190
|
+
if (!field)
|
|
191
|
+
return nothing;
|
|
192
|
+
const replace = (next) => this.edit({ kind: 'set-data-schema', schema: schemaWithFieldReplaced(schema, row.path, next) });
|
|
193
|
+
const set = (patch) => {
|
|
194
|
+
const next = { ...field, ...patch };
|
|
195
|
+
for (const [k, v] of Object.entries(patch))
|
|
196
|
+
if (v === undefined || (typeof v === 'number' && Number.isNaN(v)))
|
|
197
|
+
delete next[k];
|
|
198
|
+
replace(next);
|
|
199
|
+
};
|
|
97
200
|
const value = (e) => e.target.value;
|
|
98
201
|
const number = (e) => e.target.valueAsNumber;
|
|
202
|
+
const rename = (to) => {
|
|
203
|
+
if (!to || to === field.name)
|
|
204
|
+
return;
|
|
205
|
+
/* A field outside any list is renamed with what reads it; inside a list it is the schema alone. */
|
|
206
|
+
if (!row.path.includes('[]'))
|
|
207
|
+
this.edit({ kind: 'rename-data-field', accessor: row.path, to });
|
|
208
|
+
else
|
|
209
|
+
replace({ ...field, name: to });
|
|
210
|
+
};
|
|
211
|
+
let holder = field;
|
|
212
|
+
while (holder.kind === 'list')
|
|
213
|
+
holder = holder.item;
|
|
99
214
|
return html `<section item>
|
|
100
215
|
<div head>
|
|
101
|
-
<strong>${
|
|
102
|
-
<button
|
|
216
|
+
<strong>${row.path}</strong>
|
|
217
|
+
<button
|
|
218
|
+
act
|
|
219
|
+
?disabled=${this.locked}
|
|
220
|
+
title=${i18next.t('figure.button.remove-data-field')}
|
|
221
|
+
@click=${() => {
|
|
222
|
+
this.chosen = '';
|
|
223
|
+
this.edit({ kind: 'set-data-schema', schema: schemaWithoutField(schema, row.path) });
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
103
226
|
<md-icon>close</md-icon>
|
|
104
227
|
</button>
|
|
105
228
|
</div>
|
|
106
229
|
<div form>
|
|
107
|
-
<span>${i18next.t('figure.label.data-
|
|
108
|
-
<input .value=${
|
|
109
|
-
<span>${i18next.t('figure.label.
|
|
110
|
-
<input .value=${
|
|
111
|
-
|
|
230
|
+
<span>${i18next.t('figure.label.data-name')}</span>
|
|
231
|
+
<input .value=${field.name} ?disabled=${this.locked} @change=${(e) => rename(value(e).trim())} />
|
|
232
|
+
<span>${i18next.t('figure.label.data-label')}</span>
|
|
233
|
+
<input .value=${field.label ?? ''} ?disabled=${this.locked} @change=${(e) => set({ label: value(e).trim() || undefined })} />
|
|
234
|
+
<span>${i18next.t('figure.label.data-kind')}</span>
|
|
235
|
+
<select ?disabled=${this.locked} @change=${(e) => replace(fieldOfKind(field, value(e)))}>
|
|
236
|
+
${KINDS.map(k => html `<option value=${k} ?selected=${k === field.kind}>${i18next.t('figure.label.data-kind-' + k)}</option>`)}
|
|
237
|
+
</select>
|
|
238
|
+
${field.kind === 'number'
|
|
112
239
|
? html `<span>${i18next.t('figure.label.data-unit')}</span>
|
|
113
|
-
<select ?disabled=${this.locked} @change=${(e) => set({ unit: value(e) })}>
|
|
114
|
-
|
|
240
|
+
<select ?disabled=${this.locked} @change=${(e) => set({ unit: value(e) || undefined })}>
|
|
241
|
+
<option value="" ?selected=${!field.unit}>-</option>
|
|
242
|
+
${FIGURE_UNITS.map(u => html `<option value=${u} ?selected=${u === field.unit}>${u}</option>`)}
|
|
115
243
|
</select>
|
|
116
244
|
<span>${i18next.t('figure.label.data-min')}</span>
|
|
117
|
-
<input type="number" step="any" .value=${String(
|
|
245
|
+
<input type="number" step="any" .value=${field.min === undefined ? '' : String(field.min)} ?disabled=${this.locked} @change=${(e) => set({ min: number(e) })} />
|
|
118
246
|
<span>${i18next.t('figure.label.data-max')}</span>
|
|
119
|
-
<input type="number" step="any" .value=${String(
|
|
247
|
+
<input type="number" step="any" .value=${field.max === undefined ? '' : String(field.max)} ?disabled=${this.locked} @change=${(e) => set({ max: number(e) })} />
|
|
120
248
|
<span>${i18next.t('figure.label.data-default')}</span>
|
|
121
|
-
<input type="number" step="any" .value=${String(
|
|
122
|
-
:
|
|
249
|
+
<input type="number" step="any" .value=${field.default === undefined ? '' : String(field.default)} ?disabled=${this.locked} @change=${(e) => set({ default: number(e) })} />`
|
|
250
|
+
: field.kind === 'boolean'
|
|
123
251
|
? html `<span>${i18next.t('figure.label.data-default')}</span>
|
|
124
|
-
<select ?disabled=${this.locked} @change=${(e) => set({ default: value(e) === 'true' })}>
|
|
125
|
-
|
|
252
|
+
<select ?disabled=${this.locked} @change=${(e) => set({ default: value(e) === '' ? undefined : value(e) === 'true' })}>
|
|
253
|
+
<option value="" ?selected=${field.default === undefined}>-</option>
|
|
254
|
+
${[false, true].map(b => html `<option value=${String(b)} ?selected=${b === field.default}>${i18next.t(b ? 'figure.label.data-true' : 'figure.label.data-false')}</option>`)}
|
|
126
255
|
</select>`
|
|
127
|
-
:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
${
|
|
139
|
-
|
|
256
|
+
: field.kind === 'text'
|
|
257
|
+
? html `<span>${i18next.t('figure.label.data-values')}</span>
|
|
258
|
+
<input
|
|
259
|
+
.value=${(field.values ?? []).join(', ')}
|
|
260
|
+
?disabled=${this.locked}
|
|
261
|
+
@change=${(e) => {
|
|
262
|
+
const values = value(e).split(',').map(v => v.trim()).filter(Boolean);
|
|
263
|
+
set({ values: values.length ? values : undefined, default: field.default !== undefined && (!values.length || values.includes(field.default)) ? field.default : undefined });
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
<span>${i18next.t('figure.label.data-default')}</span>
|
|
267
|
+
${field.values
|
|
268
|
+
? html `<select ?disabled=${this.locked} @change=${(e) => set({ default: value(e) || undefined })}>
|
|
269
|
+
<option value="" ?selected=${field.default === undefined}>-</option>
|
|
270
|
+
${field.values.map(v => html `<option value=${v} ?selected=${v === field.default}>${v}</option>`)}
|
|
271
|
+
</select>`
|
|
272
|
+
: html `<input .value=${field.default ?? ''} ?disabled=${this.locked} @change=${(e) => set({ default: value(e) || undefined })} />`}`
|
|
273
|
+
: html `<span></span>
|
|
274
|
+
<label toggle>
|
|
275
|
+
<input type="checkbox" .checked=${!!field.json} ?disabled=${this.locked} @change=${(e) => set({ json: e.target.checked ? true : undefined })} />
|
|
276
|
+
${i18next.t('figure.label.data-json')}
|
|
277
|
+
</label>`}
|
|
278
|
+
${field.kind === 'list'
|
|
279
|
+
? html `<span>${i18next.t('figure.label.data-item-kind')}</span>
|
|
280
|
+
<select ?disabled=${this.locked} @change=${(e) => replace({ ...field, item: nodeOfKind(value(e)) })}>
|
|
281
|
+
${KINDS.filter(k => k !== 'list').map(k => html `<option value=${k} ?selected=${k === field.item.kind}>${i18next.t('figure.label.data-kind-' + k)}</option>`)}
|
|
282
|
+
</select>`
|
|
283
|
+
: nothing}
|
|
140
284
|
</div>
|
|
285
|
+
${holder.kind === 'object' ? this.renderAddField(schema, row.path) : nothing}
|
|
141
286
|
</section>`;
|
|
142
287
|
}
|
|
143
|
-
|
|
288
|
+
renderAddField(schema, under) {
|
|
144
289
|
const a = this.adding;
|
|
290
|
+
const named = a.name.trim();
|
|
145
291
|
return html `<div form>
|
|
146
|
-
<span>${i18next.t('figure.label.name')}</span>
|
|
147
|
-
<input .value=${a.
|
|
292
|
+
<span>${i18next.t('figure.label.data-name')}</span>
|
|
293
|
+
<input .value=${a.name} @input=${(e) => (this.adding = { ...a, name: e.target.value })} />
|
|
148
294
|
<span>${i18next.t('figure.label.data-kind')}</span>
|
|
149
295
|
<select @change=${(e) => (this.adding = { ...a, kind: e.target.value })}>
|
|
150
|
-
${
|
|
296
|
+
${KINDS.map(k => html `<option value=${k} ?selected=${k === a.kind}>${i18next.t('figure.label.data-kind-' + k)}</option>`)}
|
|
151
297
|
</select>
|
|
152
298
|
</div>
|
|
153
299
|
<button
|
|
154
300
|
act
|
|
155
|
-
?disabled=${this.locked || !
|
|
301
|
+
?disabled=${this.locked || !named}
|
|
156
302
|
@click=${() => {
|
|
157
|
-
this.edit({ kind: 'set-data-
|
|
158
|
-
this.adding = { ...a,
|
|
303
|
+
this.edit({ kind: 'set-data-schema', schema: schemaWithField(schema, under, { name: named, ...nodeOfKind(a.kind) }) });
|
|
304
|
+
this.adding = { ...a, name: '' };
|
|
159
305
|
}}
|
|
160
306
|
>
|
|
161
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-
|
|
307
|
+
<md-icon>add</md-icon>${i18next.t(under ? 'figure.button.add-data-child' : 'figure.button.add-data-field', { parent: under })}
|
|
162
308
|
</button>`;
|
|
163
309
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
310
|
+
/** A site's data pasted: the schema guessed from it, kept only when the author says so -- units, ranges and closed values are theirs to set. */
|
|
311
|
+
renderFromSample(schema) {
|
|
312
|
+
let guessed = null;
|
|
313
|
+
let unreadable = false;
|
|
314
|
+
if (this.pasted.trim()) {
|
|
315
|
+
try {
|
|
316
|
+
guessed = inferFigureDataSchema(JSON.parse(this.pasted));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
unreadable = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return html `<details>
|
|
323
|
+
<summary>${i18next.t('figure.label.data-from-sample')}</summary>
|
|
324
|
+
<p hint>${i18next.t('figure.text.data-from-sample-explained')}</p>
|
|
325
|
+
<textarea .value=${this.pasted} ?disabled=${this.locked} @input=${(e) => (this.pasted = e.target.value)}></textarea>
|
|
326
|
+
${unreadable ? html `<p hint>${i18next.t('figure.text.data-not-json')}</p>` : nothing}
|
|
327
|
+
${guessed ? html `<p hint>${i18next.t(schema.length ? 'figure.text.data-guessed-replaces' : 'figure.text.data-guessed', { count: guessed.length })}</p>` : nothing}
|
|
328
|
+
<button
|
|
329
|
+
act
|
|
330
|
+
?disabled=${this.locked || !guessed?.length}
|
|
331
|
+
@click=${() => {
|
|
332
|
+
this.edit({ kind: 'set-data-schema', schema: guessed });
|
|
333
|
+
this.pasted = '';
|
|
334
|
+
}}
|
|
335
|
+
>
|
|
336
|
+
${i18next.t('figure.button.data-use-guessed')}
|
|
337
|
+
</button>
|
|
338
|
+
</details>`;
|
|
339
|
+
}
|
|
340
|
+
/* ---------------------------------------------------------------- the bindings */
|
|
341
|
+
behaviorName(asset, choices, b) {
|
|
342
|
+
const key = behaviorKeyOf(b);
|
|
343
|
+
const choice = choices.find(c => c.key === key);
|
|
344
|
+
const name = b.kind === 'pose' ? i18next.t('figure.label.data-axis-' + b.axis) : (choice?.name ?? key.slice(key.indexOf(':') + 1));
|
|
345
|
+
return i18next.t('figure.label.data-behavior-' + b.kind, { name });
|
|
168
346
|
}
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
347
|
+
renderBindings(asset, rows) {
|
|
348
|
+
const bindings = asset.dataInterface?.bindings ?? [];
|
|
349
|
+
const choices = behaviorsOf(asset);
|
|
350
|
+
return html `<section>
|
|
351
|
+
<h3>${i18next.t('figure.label.data-bindings')}${this.help.button('figure.text.data-bindings-explained')}</h3>
|
|
352
|
+
${this.help.hint('figure.text.data-bindings-explained')}
|
|
353
|
+
${bindings.map(b => this.renderBinding(asset, choices, b))}
|
|
354
|
+
${rows.length ? this.renderAddBinding(asset, rows, choices) : html `<p hint>${i18next.t('figure.text.data-no-fields')}</p>`}
|
|
355
|
+
</section>`;
|
|
356
|
+
}
|
|
357
|
+
renderBinding(asset, choices, b) {
|
|
358
|
+
const open = this.opened === b.id;
|
|
359
|
+
const set = (patch) => {
|
|
360
|
+
const next = { ...b, ...patch };
|
|
361
|
+
if (patch.param === undefined && 'param' in patch)
|
|
362
|
+
delete next.param;
|
|
363
|
+
this.edit({ kind: 'set-data-binding', binding: next });
|
|
364
|
+
};
|
|
173
365
|
return html `<section item>
|
|
174
366
|
<div head>
|
|
175
|
-
<
|
|
176
|
-
|
|
367
|
+
<button line aria-expanded=${open ? 'true' : 'false'} @click=${() => (this.opened = open ? '' : b.id)}>
|
|
368
|
+
<strong>${b.accessor}</strong> → ${this.behaviorName(asset, choices, b.behavior)} <small>· ${i18next.t('figure.label.data-rule-' + b.rule)}</small>
|
|
369
|
+
</button>
|
|
370
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-binding')} @click=${() => this.edit({ kind: 'remove-data-binding', id: b.id })}>
|
|
177
371
|
<md-icon>close</md-icon>
|
|
178
372
|
</button>
|
|
179
373
|
</div>
|
|
180
|
-
|
|
181
|
-
|
|
374
|
+
${open ? this.renderBindingDetail(asset, b, set) : nothing}
|
|
375
|
+
</section>`;
|
|
376
|
+
}
|
|
377
|
+
renderBindingDetail(asset, b, set) {
|
|
378
|
+
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'));
|
|
379
|
+
const value = (e) => e.target.value;
|
|
380
|
+
const behavior = b.behavior;
|
|
381
|
+
if (behavior.kind === 'items')
|
|
382
|
+
return this.renderItemFields(asset, b, node, set);
|
|
383
|
+
return html `<div form>
|
|
384
|
+
<span>${i18next.t('figure.label.data-rule')}</span>
|
|
385
|
+
<select ?disabled=${this.locked} @change=${(e) => set({ rule: value(e), param: firstParamOf(value(e), node, b) })}>
|
|
386
|
+
${['value', 'map', 'range', 'scale'].map(r => html `<option value=${r} ?selected=${r === b.rule}>${i18next.t('figure.label.data-rule-' + r)}</option>`)}
|
|
387
|
+
</select>
|
|
388
|
+
${behavior.kind === 'state'
|
|
182
389
|
? html `<span>${i18next.t('figure.label.data-source-mode')}</span>
|
|
183
|
-
<select ?disabled=${this.locked} @change=${(e) => set({
|
|
184
|
-
${['observed', 'commanded'].map(m => html `<option value=${m} ?selected=${
|
|
390
|
+
<select ?disabled=${this.locked} @change=${(e) => set({ behavior: { ...behavior, sourceMode: value(e) } })}>
|
|
391
|
+
${['observed', 'commanded'].map(m => html `<option value=${m} ?selected=${behavior.sourceMode === m}>${i18next.t('figure.label.data-' + m)}</option>`)}
|
|
185
392
|
</select>`
|
|
186
393
|
: nothing}
|
|
187
|
-
${
|
|
188
|
-
?
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<option value="affine" ?selected=${t.kind === 'affine'}>${i18next.t('figure.label.data-affine')}</option>
|
|
193
|
-
</select>`}
|
|
194
|
-
${t.kind === 'affine'
|
|
394
|
+
${behavior.kind === 'pose'
|
|
395
|
+
? html `<span>${i18next.t('figure.label.data-seconds')}</span>
|
|
396
|
+
<input type="number" min="0" step="any" .value=${String(behavior.seconds ?? 0)} ?disabled=${this.locked} @change=${(e) => set({ behavior: { ...behavior, seconds: e.target.valueAsNumber || 0 } })} />`
|
|
397
|
+
: nothing}
|
|
398
|
+
${b.rule === 'scale'
|
|
195
399
|
? html `<span>${i18next.t('figure.label.data-scale')}</span>
|
|
196
|
-
<input type="number" step="any" .value=${String(
|
|
400
|
+
<input type="number" step="any" .value=${String(b.param?.scale ?? 1)} ?disabled=${this.locked} @change=${(e) => set({ param: { ...b.param, scale: e.target.valueAsNumber } })} />
|
|
197
401
|
<span>${i18next.t('figure.label.data-offset')}</span>
|
|
198
|
-
<input type="number" step="any" .value=${String(
|
|
402
|
+
<input type="number" step="any" .value=${String(b.param?.offset ?? 0)} ?disabled=${this.locked} @change=${(e) => set({ param: { ...b.param, offset: e.target.valueAsNumber } })} />`
|
|
199
403
|
: nothing}
|
|
200
404
|
</div>
|
|
201
|
-
${
|
|
202
|
-
</section>`;
|
|
405
|
+
${b.rule === 'map' ? this.renderCases(asset, b, node, set) : b.rule === 'range' ? this.renderRanges(asset, b, set) : nothing}`;
|
|
203
406
|
}
|
|
204
|
-
/** A
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
407
|
+
/** A result editor fitting what the behavior takes: a colour, a clip of the group, true or false, a code, a number. */
|
|
408
|
+
renderResult(asset, b, value, write) {
|
|
409
|
+
const behavior = b.behavior;
|
|
410
|
+
const typed = (e) => e.target.value;
|
|
411
|
+
if (behavior.kind === 'palette')
|
|
412
|
+
/* A colour not set is not black: it is offered, started at the colour the token has now. */
|
|
413
|
+
return value === undefined
|
|
414
|
+
? html `<button act ?disabled=${this.locked} @click=${() => write(asset.palette[behavior.token] ?? '#000000')}>${i18next.t('figure.button.data-set-result')}</button>`
|
|
415
|
+
: html `<input type="color" .value=${String(value)} ?disabled=${this.locked} @change=${(e) => write(typed(e))} />`;
|
|
416
|
+
if (behavior.kind === 'motion-clip') {
|
|
417
|
+
const clips = (asset.motionGroups ?? []).find(g => g.id === behavior.group)?.clips ?? [];
|
|
418
|
+
return html `<select ?disabled=${this.locked} @change=${(e) => write(typed(e))}>
|
|
419
|
+
${clips.map(clip => html `<option value=${clip} ?selected=${clip === value}>${clip}</option>`)}
|
|
420
|
+
</select>`;
|
|
421
|
+
}
|
|
422
|
+
if (behavior.kind === 'motion-paused')
|
|
423
|
+
return html `<select ?disabled=${this.locked} @change=${(e) => write(typed(e) === 'true')}>
|
|
424
|
+
${[false, true].map(v => html `<option value=${String(v)} ?selected=${v === value}>${i18next.t(v ? 'figure.label.data-paused' : 'figure.label.data-playing')}</option>`)}
|
|
425
|
+
</select>`;
|
|
426
|
+
if (behavior.kind === 'status')
|
|
427
|
+
return html `<input .value=${String(value ?? '')} ?disabled=${this.locked} @change=${(e) => write(typed(e))} />`;
|
|
428
|
+
return html `<input type="number" step="any" .value=${value === undefined ? '' : String(value)} ?disabled=${this.locked} @change=${(e) => write(e.target.valueAsNumber)} />`;
|
|
429
|
+
}
|
|
430
|
+
/** A result per value of a closed field, and one for every other value. */
|
|
431
|
+
renderCases(asset, b, node, set) {
|
|
432
|
+
const param = b.param ?? {};
|
|
433
|
+
const domain = node ? domainOf(node).map(String) : [];
|
|
434
|
+
const keys = [...new Set([...domain, ...Object.keys(param).filter(k => k !== 'default')])];
|
|
435
|
+
const write = (key) => (v) => {
|
|
436
|
+
const next = { ...param };
|
|
437
|
+
if (v === undefined || (typeof v === 'number' && Number.isNaN(v)))
|
|
438
|
+
delete next[key];
|
|
439
|
+
else
|
|
440
|
+
next[key] = v;
|
|
441
|
+
set({ param: next });
|
|
442
|
+
};
|
|
443
|
+
const word = (k) => (node?.kind === 'boolean' ? i18next.t(k === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : k);
|
|
444
|
+
/* Every value of a closed field has its own result: no other value comes, unless one was set before. */
|
|
445
|
+
const covered = domain.length > 0 && domain.every(v => param[v] !== undefined);
|
|
211
446
|
return html `<div cases>
|
|
212
|
-
${
|
|
213
|
-
|
|
214
|
-
? html `<input type="color" .value=${String(k.value)} ?disabled=${this.locked} @change=${(e) => write(i, e.target.value)} />`
|
|
215
|
-
: c.target.kind === 'motion-clip'
|
|
216
|
-
? html `<select ?disabled=${this.locked} @change=${(e) => write(i, e.target.value)}>
|
|
217
|
-
${clips.map(clip => html `<option value=${clip} ?selected=${clip === k.value}>${clip}</option>`)}
|
|
218
|
-
</select>`
|
|
219
|
-
: html `<input type="number" step="any" .value=${String(k.value)} ?disabled=${this.locked} @change=${(e) => write(i, e.target.valueAsNumber)} />`}`)}
|
|
447
|
+
${keys.map(k => html `<span>${word(k)}</span>${this.renderResult(asset, b, param[k], write(k))}`)}
|
|
448
|
+
${covered && param.default === undefined ? nothing : html `<span>${i18next.t('figure.label.data-otherwise')}</span>${this.renderResult(asset, b, param.default, write('default'))}`}
|
|
220
449
|
</div>`;
|
|
221
450
|
}
|
|
222
|
-
|
|
451
|
+
/** Intervals of a number, each from (inclusive) to (exclusive), either end left open, and one result for the rest. */
|
|
452
|
+
renderRanges(asset, b, set) {
|
|
453
|
+
const param = b.param ?? {};
|
|
454
|
+
const entries = Object.entries(param).filter(([k]) => k !== 'default');
|
|
455
|
+
const rebuild = (list) => set({ param: { ...Object.fromEntries(list), ...(param.default !== undefined ? { default: param.default } : {}) } });
|
|
456
|
+
const ends = (key) => {
|
|
457
|
+
const [from = '', to = ''] = key.split('~');
|
|
458
|
+
return { from, to };
|
|
459
|
+
};
|
|
460
|
+
const rekey = (i, end, typed) => rebuild(entries.map(([k, v], j) => {
|
|
461
|
+
if (j !== i)
|
|
462
|
+
return [k, v];
|
|
463
|
+
const e = ends(k);
|
|
464
|
+
const next = { ...e, [end]: typed.trim() };
|
|
465
|
+
return [`${next.from}~${next.to}`, v];
|
|
466
|
+
}));
|
|
467
|
+
return html `<div ranges>
|
|
468
|
+
<small>${i18next.t('figure.label.data-range-from')}</small><small>${i18next.t('figure.label.data-range-to')}</small><small></small><span></span>
|
|
469
|
+
${entries.map(([k, v], i) => {
|
|
470
|
+
const e = ends(k);
|
|
471
|
+
return html `<input type="number" step="any" .value=${e.from} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'from', ev.target.value)} />
|
|
472
|
+
<input type="number" step="any" .value=${e.to} ?disabled=${this.locked} @change=${(ev) => rekey(i, 'to', ev.target.value)} />
|
|
473
|
+
${this.renderResult(asset, b, v, out => rebuild(entries.map(([kk, vv], j) => [kk, j === i ? out : vv])))}
|
|
474
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => rebuild(entries.filter((_, j) => j !== i))}><md-icon>close</md-icon></button>`;
|
|
475
|
+
})}
|
|
476
|
+
</div>
|
|
477
|
+
<div cases>
|
|
478
|
+
<span>${i18next.t('figure.label.data-otherwise')}</span>${this.renderResult(asset, b, param.default, v => set({ param: { ...param, ...(v === undefined ? {} : { default: v }) } }))}
|
|
479
|
+
</div>
|
|
480
|
+
<button act ?disabled=${this.locked} @click=${() => rebuild([...entries, [freeRangeKey(entries.map(([k]) => k)), entries.at(-1)?.[1] ?? param.default ?? 0]])}>
|
|
481
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-range')}
|
|
482
|
+
</button>`;
|
|
483
|
+
}
|
|
484
|
+
/** Which field of each element is its id, its type, its place, its height in the stack, its status. */
|
|
485
|
+
renderItemFields(asset, b, node, set) {
|
|
486
|
+
const behavior = b.behavior;
|
|
487
|
+
if (behavior.kind !== 'items')
|
|
488
|
+
return nothing;
|
|
489
|
+
let element = node;
|
|
490
|
+
while (element?.kind === 'list')
|
|
491
|
+
element = element.item;
|
|
492
|
+
const fields = element?.kind === 'object' ? element.fields.filter(f => f.kind === 'number' || f.kind === 'text' || f.kind === 'boolean').map(f => f.name) : [];
|
|
493
|
+
const pick = (role, typed) => {
|
|
494
|
+
const next = { ...behavior, [role]: typed };
|
|
495
|
+
if (!typed)
|
|
496
|
+
delete next[role];
|
|
497
|
+
set({ behavior: next });
|
|
498
|
+
};
|
|
499
|
+
return html `<div form>
|
|
500
|
+
${node?.kind === 'list'
|
|
501
|
+
? ['id', 'type', 'place', 'stack', 'status'].map(role => html `<span>${i18next.t('figure.label.data-item-' + role)}</span>
|
|
502
|
+
<select ?disabled=${this.locked} @change=${(e) => pick(role, e.target.value)}>
|
|
503
|
+
<option value="" ?selected=${!behavior[role]}>-</option>
|
|
504
|
+
${fields.map(f => html `<option value=${f} ?selected=${f === behavior[role]}>${f}</option>`)}
|
|
505
|
+
</select>`)
|
|
506
|
+
: nothing}
|
|
507
|
+
<span>${i18next.t('figure.label.data-item-slot')}</span>
|
|
508
|
+
<input .value=${behavior.slot ?? ''} ?disabled=${this.locked} @change=${(e) => {
|
|
509
|
+
const slot = e.target.value.trim();
|
|
510
|
+
const next = { ...behavior, slot };
|
|
511
|
+
if (!slot)
|
|
512
|
+
delete next.slot;
|
|
513
|
+
set({ behavior: next });
|
|
514
|
+
}} />
|
|
515
|
+
</div>`;
|
|
516
|
+
}
|
|
517
|
+
renderAddBinding(asset, rows, choices) {
|
|
518
|
+
const bindable = rows.filter(r => r.node.kind !== 'object');
|
|
519
|
+
if (!bindable.length)
|
|
520
|
+
return nothing;
|
|
223
521
|
const a = this.connecting;
|
|
224
|
-
const
|
|
225
|
-
const
|
|
226
|
-
const choice =
|
|
522
|
+
const row = bindable.find(r => r.path === a.path) ?? bindable[0];
|
|
523
|
+
const fitting = choices.filter(c => fitsField(row.node, c));
|
|
524
|
+
const choice = fitting.find(c => c.key === a.behavior) ?? fitting[0];
|
|
525
|
+
/* A list is read whole by items; one value of it, through its first element. */
|
|
526
|
+
const accessor = row.node.kind === 'list' && choice?.kind === 'items' ? row.path.replace(/\[\]$/, '') : accessorOfRow(row.path);
|
|
227
527
|
return html `<div form>
|
|
228
|
-
<span>${i18next.t('figure.label.data-
|
|
229
|
-
<select @change=${(e) => (this.connecting = { ...a,
|
|
230
|
-
${
|
|
528
|
+
<span>${i18next.t('figure.label.data-field')}</span>
|
|
529
|
+
<select @change=${(e) => (this.connecting = { ...a, path: e.target.value })}>
|
|
530
|
+
${bindable.map(r => html `<option value=${r.path} ?selected=${r === row}>${r.path}</option>`)}
|
|
231
531
|
</select>
|
|
232
|
-
<span>${i18next.t('figure.label.data-
|
|
233
|
-
<select ?disabled=${!
|
|
234
|
-
${
|
|
532
|
+
<span>${i18next.t('figure.label.data-behavior')}</span>
|
|
533
|
+
<select ?disabled=${!fitting.length} @change=${(e) => (this.connecting = { ...a, behavior: e.target.value })}>
|
|
534
|
+
${fitting.map(c => html `<option value=${c.key} ?selected=${c === choice}>${this.behaviorName(asset, choices, c.behavior)}</option>`)}
|
|
235
535
|
</select>
|
|
236
536
|
</div>
|
|
237
|
-
|
|
238
|
-
|
|
537
|
+
${fitting.length ? nothing : html `<p hint>${i18next.t('figure.text.data-no-behavior')}</p>`}
|
|
538
|
+
<button
|
|
539
|
+
act
|
|
540
|
+
?disabled=${this.locked || !choice}
|
|
541
|
+
@click=${() => {
|
|
542
|
+
const binding = bindingDraftOf(asset, accessor, row.node, choice);
|
|
543
|
+
this.edit({ kind: 'set-data-binding', binding });
|
|
544
|
+
this.opened = binding.id;
|
|
545
|
+
}}
|
|
546
|
+
>
|
|
547
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-binding')}
|
|
239
548
|
</button>`;
|
|
240
549
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
const
|
|
550
|
+
/* ---------------------------------------------------------------- status ports */
|
|
551
|
+
renderPorts(asset) {
|
|
552
|
+
const ports = asset.dataInterface?.ports ?? [];
|
|
553
|
+
const tokens = Object.keys(asset.palette);
|
|
554
|
+
const parts = partsOf(asset);
|
|
555
|
+
const set = (port, patch) => this.edit({ kind: 'set-status-port', port: { ...port, ...patch } });
|
|
556
|
+
const chosen = (e) => [...e.target.selectedOptions].map(o => o.value);
|
|
245
557
|
return html `<section>
|
|
246
|
-
<h3>${i18next.t('figure.label.data-
|
|
247
|
-
${this.help.hint('figure.text.data-
|
|
558
|
+
<h3>${i18next.t('figure.label.data-ports')}${this.help.button('figure.text.data-ports-explained')}</h3>
|
|
559
|
+
${this.help.hint('figure.text.data-ports-explained')}
|
|
560
|
+
${ports.map(port => html `<section item>
|
|
561
|
+
<div head>
|
|
562
|
+
<strong>${port.label}</strong>
|
|
563
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-port')} @click=${() => this.edit({ kind: 'remove-status-port', id: port.id })}>
|
|
564
|
+
<md-icon>close</md-icon>
|
|
565
|
+
</button>
|
|
566
|
+
</div>
|
|
567
|
+
<div form>
|
|
568
|
+
<span>${i18next.t('figure.label.data-label')}</span>
|
|
569
|
+
<input .value=${port.label} ?disabled=${this.locked} @change=${(e) => set(port, { label: e.target.value.trim() })} />
|
|
570
|
+
<span>${i18next.t('figure.label.data-port-tokens')}</span>
|
|
571
|
+
<select multiple size=${Math.min(4, Math.max(tokens.length, 1))} ?disabled=${this.locked} @change=${(e) => set(port, { tokens: chosen(e) })}>
|
|
572
|
+
${tokens.map(t => html `<option value=${t} ?selected=${port.tokens.includes(t)}>${t}</option>`)}
|
|
573
|
+
</select>
|
|
574
|
+
<span>${i18next.t('figure.label.data-port-parts')}</span>
|
|
575
|
+
<select multiple size=${Math.min(4, Math.max(parts.length, 1))} ?disabled=${this.locked} @change=${(e) => set(port, { parts: chosen(e) })}>
|
|
576
|
+
${parts.map(p => html `<option value=${p} ?selected=${port.parts.includes(p)}>${p}</option>`)}
|
|
577
|
+
</select>
|
|
578
|
+
</div>
|
|
579
|
+
</section>`)}
|
|
248
580
|
<div form>
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
? html `<input type="number" step="any" .value=${String(values[p.id])} @change=${(e) => this.try(p, e.target.value)} />`
|
|
252
|
-
: html `<select @change=${(e) => this.try(p, e.target.value)}>
|
|
253
|
-
${domainOf(p).map(v => html `<option value=${String(v)} ?selected=${v === values[p.id]}>${typeof v === 'boolean' ? i18next.t(v ? 'figure.label.data-true' : 'figure.label.data-false') : v}</option>`)}
|
|
254
|
-
</select>`}`)}
|
|
581
|
+
<span>${i18next.t('figure.label.data-label')}</span>
|
|
582
|
+
<input .value=${this.porting} @input=${(e) => (this.porting = e.target.value)} />
|
|
255
583
|
</div>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
584
|
+
<button
|
|
585
|
+
act
|
|
586
|
+
?disabled=${this.locked || !this.porting.trim()}
|
|
587
|
+
@click=${() => {
|
|
588
|
+
this.edit({ kind: 'set-status-port', port: portDraftOf(asset, this.porting) });
|
|
589
|
+
this.porting = '';
|
|
590
|
+
}}
|
|
591
|
+
>
|
|
592
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-port')}
|
|
593
|
+
</button>
|
|
594
|
+
</section>`;
|
|
595
|
+
}
|
|
596
|
+
/* ---------------------------------------------------------------- the sample */
|
|
597
|
+
/** 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. */
|
|
598
|
+
renderSample(asset) {
|
|
599
|
+
const text = this.sample || JSON.stringify(figureDataExample(asset.dataInterface), null, 2);
|
|
600
|
+
let data;
|
|
601
|
+
let unreadable = false;
|
|
602
|
+
try {
|
|
603
|
+
data = JSON.parse(text);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
unreadable = true;
|
|
607
|
+
}
|
|
608
|
+
const read = unreadable ? null : sampleOf(asset, data);
|
|
609
|
+
const r = read?.result;
|
|
610
|
+
return html `<section>
|
|
611
|
+
<h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
|
|
612
|
+
${this.help.hint('figure.text.data-sample-explained')}
|
|
613
|
+
<textarea .value=${text} @input=${(e) => this.try(e.target.value)}></textarea>
|
|
614
|
+
${unreadable
|
|
615
|
+
? html `<p hint>${i18next.t('figure.text.data-not-json')}</p>`
|
|
616
|
+
: read.issues.length
|
|
617
|
+
? html `<p hint>${read.issues.map(i => i18next.t('figure.text.data-value-' + i.reason, { path: i.path || '-' })).join(' · ')}</p>`
|
|
618
|
+
: html `<ul result>
|
|
619
|
+
${Object.entries(r.states).map(([input, s]) => html `<li>${asset.stateInputs?.[input]?.label ?? input} = ${s.value}</li>`)}
|
|
620
|
+
${Object.entries(r.palette).map(([token, colour]) => html `<li>${token} = ${colour}</li>`)}
|
|
621
|
+
${Object.entries(r.motion).map(([group, m]) => html `<li>
|
|
622
|
+
${(asset.motionGroups ?? []).find(g => g.id === group)?.label ?? group}:
|
|
623
|
+
${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`, m.paused ? i18next.t('figure.label.data-paused') : ''].filter(Boolean).join(' ')}
|
|
624
|
+
</li>`)}
|
|
625
|
+
${Object.entries(r.status).map(([port, code]) => html `<li>${(asset.dataInterface?.ports ?? []).find(p => p.id === port)?.label ?? port}: ${code}</li>`)}
|
|
626
|
+
${Object.entries(r.pose).map(([axis, p]) => html `<li>${i18next.t('figure.label.data-axis-' + axis)} = ${p.value}</li>`)}
|
|
627
|
+
${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>`)}
|
|
628
|
+
</ul>`}
|
|
629
|
+
${r && Object.keys(r.pose).length ? html `<p hint>${i18next.t('figure.text.data-sample-moves-all')}</p>` : nothing}
|
|
263
630
|
<label toggle>
|
|
264
631
|
<input
|
|
265
632
|
type="checkbox"
|
|
266
633
|
.checked=${this.sampling}
|
|
267
634
|
@change=${(e) => {
|
|
268
635
|
this.sampling = e.target.checked;
|
|
269
|
-
this.tell(
|
|
636
|
+
this.tell(unreadable ? null : data);
|
|
270
637
|
}}
|
|
271
638
|
/>
|
|
272
639
|
${i18next.t('figure.label.data-sample-in-preview')}
|
|
273
640
|
</label>
|
|
274
641
|
</section>`;
|
|
275
642
|
}
|
|
276
|
-
try(
|
|
277
|
-
this.sample =
|
|
278
|
-
|
|
279
|
-
|
|
643
|
+
try(typed) {
|
|
644
|
+
this.sample = typed;
|
|
645
|
+
try {
|
|
646
|
+
this.tell(JSON.parse(typed));
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
/* Not JSON yet: the preview keeps what it ran on. */
|
|
650
|
+
}
|
|
280
651
|
}
|
|
281
652
|
/** The sample to the preview while it is asked for; null lets the preview's figure go back to its own values. */
|
|
282
|
-
tell(
|
|
283
|
-
this.dispatchEvent(new CustomEvent('data-sample', { detail: {
|
|
653
|
+
tell(data) {
|
|
654
|
+
this.dispatchEvent(new CustomEvent('data-sample', { detail: { data: this.sampling ? data : null }, bubbles: true, composed: true }));
|
|
284
655
|
}
|
|
285
656
|
edit(edit) {
|
|
286
657
|
this.dispatchEvent(new CustomEvent('part-edit', { detail: { edit }, bubbles: true, composed: true }));
|
|
@@ -294,14 +665,30 @@ __decorate([
|
|
|
294
665
|
property({ type: Boolean }),
|
|
295
666
|
__metadata("design:type", Object)
|
|
296
667
|
], FigureDataPanel.prototype, "locked", void 0);
|
|
668
|
+
__decorate([
|
|
669
|
+
state(),
|
|
670
|
+
__metadata("design:type", Object)
|
|
671
|
+
], FigureDataPanel.prototype, "chosen", void 0);
|
|
297
672
|
__decorate([
|
|
298
673
|
state(),
|
|
299
674
|
__metadata("design:type", Object)
|
|
300
675
|
], FigureDataPanel.prototype, "adding", void 0);
|
|
676
|
+
__decorate([
|
|
677
|
+
state(),
|
|
678
|
+
__metadata("design:type", Object)
|
|
679
|
+
], FigureDataPanel.prototype, "pasted", void 0);
|
|
301
680
|
__decorate([
|
|
302
681
|
state(),
|
|
303
682
|
__metadata("design:type", Object)
|
|
304
683
|
], FigureDataPanel.prototype, "connecting", void 0);
|
|
684
|
+
__decorate([
|
|
685
|
+
state(),
|
|
686
|
+
__metadata("design:type", Object)
|
|
687
|
+
], FigureDataPanel.prototype, "opened", void 0);
|
|
688
|
+
__decorate([
|
|
689
|
+
state(),
|
|
690
|
+
__metadata("design:type", Object)
|
|
691
|
+
], FigureDataPanel.prototype, "porting", void 0);
|
|
305
692
|
__decorate([
|
|
306
693
|
state(),
|
|
307
694
|
__metadata("design:type", Object)
|
|
@@ -314,4 +701,29 @@ FigureDataPanel = __decorate([
|
|
|
314
701
|
customElement('figure-data-panel')
|
|
315
702
|
], FigureDataPanel);
|
|
316
703
|
export { FigureDataPanel };
|
|
704
|
+
/** A node of a kind, started empty. */
|
|
705
|
+
function nodeOfKind(kind) {
|
|
706
|
+
if (kind === 'object')
|
|
707
|
+
return { kind, fields: [] };
|
|
708
|
+
if (kind === 'list')
|
|
709
|
+
return { kind, item: { kind: 'object', fields: [] } };
|
|
710
|
+
return { kind };
|
|
711
|
+
}
|
|
712
|
+
/** The param a rule starts with when the author switches to it: one result per value, one open interval, a unit scale, or none. */
|
|
713
|
+
function firstParamOf(rule, node, b) {
|
|
714
|
+
const keep = b.param?.default;
|
|
715
|
+
if (rule === 'value')
|
|
716
|
+
return undefined;
|
|
717
|
+
if (rule === 'scale')
|
|
718
|
+
return { scale: 1, offset: 0 };
|
|
719
|
+
if (rule === 'range')
|
|
720
|
+
return { [`${node?.kind === 'number' ? (node.min ?? '') : ''}~`]: keep ?? 0 };
|
|
721
|
+
const domain = node ? domainOf(node).map(String) : [];
|
|
722
|
+
return domain.length ? Object.fromEntries(domain.map(v => [v, keep ?? 0])) : { default: keep ?? 0 };
|
|
723
|
+
}
|
|
724
|
+
/** The next interval: from the last one's end, open above; open at both ends when there is none. */
|
|
725
|
+
function freeRangeKey(taken) {
|
|
726
|
+
const end = taken.at(-1)?.split('~')[1] ?? '';
|
|
727
|
+
return `${end}~`;
|
|
728
|
+
}
|
|
317
729
|
//# sourceMappingURL=figure-data-panel.js.map
|