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