@things-factory/figure-ui 10.1.271 → 10.1.273
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/modeller/canvas.ts +4 -2
- package/client/modeller/clips.ts +74 -4
- package/client/modeller/data-interface.ts +30 -18
- package/client/modeller/editor-styles.ts +17 -6
- package/client/modeller/editor.ts +2 -0
- package/client/modeller/fasten-form.ts +81 -6
- package/client/modeller/fasten-geometry.ts +5 -3
- package/client/modeller/figure-cavity-drawing.ts +1 -3
- package/client/modeller/figure-clips.ts +101 -8
- package/client/modeller/figure-data-panel.ts +384 -200
- package/client/modeller/figure-fasten-dialog.ts +41 -30
- package/client/modeller/figure-fasten-drawing.ts +24 -14
- package/client/modeller/figure-first-save.ts +4 -3
- package/client/modeller/figure-links.ts +109 -37
- package/client/modeller/figure-parts.ts +7 -11
- package/client/modeller/figure-preview.ts +299 -79
- package/client/modeller/figure-size-editor.ts +7 -2
- package/client/modeller/first-save.ts +8 -0
- package/client/modeller/focus-on-render.ts +4 -0
- package/client/modeller/pivot.ts +76 -0
- package/client/modeller/size-response.ts +14 -0
- package/client/modeller/stage-model.ts +50 -6
- package/dist-client/modeller/canvas.d.ts +1 -0
- package/dist-client/modeller/canvas.js +3 -2
- package/dist-client/modeller/canvas.js.map +1 -1
- package/dist-client/modeller/clips.d.ts +17 -3
- package/dist-client/modeller/clips.js +69 -4
- package/dist-client/modeller/clips.js.map +1 -1
- package/dist-client/modeller/data-interface.d.ts +11 -8
- package/dist-client/modeller/data-interface.js +27 -17
- package/dist-client/modeller/data-interface.js.map +1 -1
- package/dist-client/modeller/editor-styles.js +17 -6
- package/dist-client/modeller/editor-styles.js.map +1 -1
- package/dist-client/modeller/editor.js +2 -0
- package/dist-client/modeller/editor.js.map +1 -1
- package/dist-client/modeller/fasten-form.d.ts +13 -0
- package/dist-client/modeller/fasten-form.js +72 -7
- package/dist-client/modeller/fasten-form.js.map +1 -1
- package/dist-client/modeller/fasten-geometry.d.ts +3 -2
- package/dist-client/modeller/fasten-geometry.js +5 -3
- package/dist-client/modeller/fasten-geometry.js.map +1 -1
- package/dist-client/modeller/figure-cavity-drawing.js +1 -3
- package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
- package/dist-client/modeller/figure-clips.d.ts +9 -0
- package/dist-client/modeller/figure-clips.js +104 -8
- package/dist-client/modeller/figure-clips.js.map +1 -1
- package/dist-client/modeller/figure-data-panel.d.ts +10 -19
- package/dist-client/modeller/figure-data-panel.js +382 -190
- package/dist-client/modeller/figure-data-panel.js.map +1 -1
- package/dist-client/modeller/figure-fasten-dialog.js +39 -24
- package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
- package/dist-client/modeller/figure-fasten-drawing.js +24 -14
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-first-save.js +4 -3
- package/dist-client/modeller/figure-first-save.js.map +1 -1
- package/dist-client/modeller/figure-links.d.ts +9 -0
- package/dist-client/modeller/figure-links.js +120 -51
- package/dist-client/modeller/figure-links.js.map +1 -1
- package/dist-client/modeller/figure-parts.js +7 -11
- package/dist-client/modeller/figure-parts.js.map +1 -1
- package/dist-client/modeller/figure-preview.d.ts +13 -0
- package/dist-client/modeller/figure-preview.js +296 -80
- package/dist-client/modeller/figure-preview.js.map +1 -1
- package/dist-client/modeller/figure-size-editor.js +7 -3
- package/dist-client/modeller/figure-size-editor.js.map +1 -1
- package/dist-client/modeller/first-save.d.ts +5 -0
- package/dist-client/modeller/first-save.js +7 -0
- package/dist-client/modeller/first-save.js.map +1 -1
- package/dist-client/modeller/focus-on-render.d.ts +2 -0
- package/dist-client/modeller/focus-on-render.js +4 -0
- package/dist-client/modeller/focus-on-render.js.map +1 -0
- package/dist-client/modeller/pivot.d.ts +12 -0
- package/dist-client/modeller/pivot.js +59 -0
- package/dist-client/modeller/pivot.js.map +1 -0
- package/dist-client/modeller/size-response.d.ts +6 -0
- package/dist-client/modeller/size-response.js +14 -0
- package/dist-client/modeller/size-response.js.map +1 -1
- package/dist-client/modeller/stage-model.d.ts +17 -3
- package/dist-client/modeller/stage-model.js +44 -6
- package/dist-client/modeller/stage-model.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -7
- package/test/browser/editing-page.js +1 -1
- package/test/browser/editing.check.mjs +6 -5
- package/test/browser/links.check.mjs +30 -0
- package/test/browser/modeller.check.mjs +2 -2
- package/test/browser/parts-page.js +4 -0
- package/test/browser/parts.check.mjs +16 -10
- package/test/browser/screen-page.js +12 -1
- package/test/browser/screen.check.mjs +142 -11
- package/test/browser/section.check.mjs +21 -0
- package/test/browser/serve-response.mjs +5 -2
- package/test/browser/side-panel-sweep.check.mjs +171 -10
- package/test/clips.test.ts +49 -4
- package/test/data-motion.test.ts +39 -8
- package/test/fasten-form.test.ts +69 -0
- package/test/fasten-geometry.test.ts +13 -0
- package/test/first-save.test.ts +11 -1
- package/test/pivot.test.ts +57 -0
- package/test/size-response.test.ts +10 -2
- package/test/stage-model.test.ts +23 -1
- package/translations/en.json +31 -11
- package/translations/es.json +30 -10
- package/translations/ja.json +30 -10
- package/translations/ko.json +32 -12
- package/translations/ms.json +30 -10
- package/translations/zh.json +30 -10
- package/tutorial/humanoid/README.md +22 -0
- package/tutorial/humanoid/chapters.mjs +247 -0
- package/tutorial/humanoid/encode.mjs +26 -0
- package/tutorial/humanoid/lib.mjs +197 -0
- package/tutorial/humanoid/record.mjs +65 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import '@material/web/icon/icon.js'
|
|
2
2
|
|
|
3
3
|
import { css, html, LitElement, nothing } from 'lit'
|
|
4
|
+
import JSON5 from 'json5'
|
|
5
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
4
6
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
5
7
|
|
|
6
8
|
import { i18next, localize } from '@operato/i18n'
|
|
@@ -30,6 +32,8 @@ import {
|
|
|
30
32
|
legendRoleOf,
|
|
31
33
|
methodOf,
|
|
32
34
|
methodsOf,
|
|
35
|
+
freeNumberCodeOf,
|
|
36
|
+
tableWithKeep,
|
|
33
37
|
partsOf,
|
|
34
38
|
rotationStepsOf,
|
|
35
39
|
sampleOf,
|
|
@@ -51,7 +55,7 @@ import {
|
|
|
51
55
|
} from './data-interface.js'
|
|
52
56
|
import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js'
|
|
53
57
|
import { dataMoveOf } from '@things-factory/figure-viewer/dist-client/data-connection.js'
|
|
54
|
-
import { kindWords, moveWords } from '@things-factory/figure-viewer/dist-client/data-words.js'
|
|
58
|
+
import { kindName, kindWords, moveWords } from '@things-factory/figure-viewer/dist-client/data-words.js'
|
|
55
59
|
import { figureBindingsTakenOver, figureCapabilityDescriptorOf, type FigureRestingBinding } from '@hatiolab/things-scene'
|
|
56
60
|
import { consumerDescriptors } from './consumer-capabilities.js'
|
|
57
61
|
import { HelpToggles } from './help.js'
|
|
@@ -70,49 +74,94 @@ const KINDS: FigureDataNode['kind'][] = ['number', 'boolean', 'text', 'object',
|
|
|
70
74
|
* running on it (`data-sample`); a sample legend pasted for a table's role shows it as a board's legend would
|
|
71
75
|
* (`legend-sample`).
|
|
72
76
|
*/
|
|
77
|
+
/** A binding being added to the open field: nothing chosen yet. */
|
|
78
|
+
const NO_CONNECTING = { method: '' as DataMethod | '', behavior: '', parts: [] as string[], inputs: [] as string[], group: '' }
|
|
79
|
+
|
|
73
80
|
@customElement('figure-data-panel')
|
|
74
81
|
export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
75
82
|
private readonly help = new HelpToggles(this)
|
|
76
83
|
static styles = [
|
|
84
|
+
/* The panel's scrollbars -- the sample's box among them -- in the shell's look, not the browser's. */
|
|
85
|
+
ScrollbarStyles,
|
|
77
86
|
SidePanelStyles,
|
|
78
87
|
css`
|
|
79
88
|
:host {
|
|
80
89
|
display: block;
|
|
81
90
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
gap: 2px;
|
|
85
|
-
margin: 0;
|
|
86
|
-
padding: 0;
|
|
87
|
-
list-style: none;
|
|
88
|
-
}
|
|
89
|
-
ul[tree] > li > button[row] {
|
|
91
|
+
/* Where the sample shows, said while editing, with the press that goes there. */
|
|
92
|
+
p[sample-where] {
|
|
90
93
|
display: flex;
|
|
91
94
|
flex-wrap: wrap;
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: 4px 8px;
|
|
97
|
+
margin: 4px 0 0;
|
|
98
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
99
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
100
|
+
}
|
|
101
|
+
/*
|
|
102
|
+
The fields as a table -- field, kind, what it moves -- a line between rows: a list of two-line names with their
|
|
103
|
+
kinds pushed right ran together, one field not told from the next (user 2026-11-12).
|
|
104
|
+
*/
|
|
105
|
+
div[fields] {
|
|
106
|
+
display: grid;
|
|
107
|
+
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
|
|
108
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
109
|
+
}
|
|
110
|
+
div[fields] > div[head],
|
|
111
|
+
div[fields] > button[row] {
|
|
112
|
+
grid-column: 1 / -1;
|
|
113
|
+
display: grid;
|
|
114
|
+
grid-template-columns: subgrid;
|
|
115
|
+
column-gap: 8px;
|
|
116
|
+
align-items: center;
|
|
117
|
+
padding: 5px 6px;
|
|
118
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
119
|
+
}
|
|
120
|
+
div[fields] > div[head] {
|
|
121
|
+
font-size: var(--ui-t-micro, 11px);
|
|
122
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
123
|
+
}
|
|
124
|
+
div[fields] > button[row] {
|
|
94
125
|
min-height: var(--figure-control-height);
|
|
95
126
|
margin: 0;
|
|
96
|
-
|
|
97
|
-
border:
|
|
98
|
-
border-
|
|
127
|
+
border-top: none;
|
|
128
|
+
border-left: none;
|
|
129
|
+
border-right: none;
|
|
99
130
|
background: none;
|
|
100
131
|
color: inherit;
|
|
101
132
|
font: inherit;
|
|
102
133
|
text-align: left;
|
|
103
|
-
align-items: center;
|
|
104
134
|
cursor: pointer;
|
|
105
135
|
}
|
|
106
|
-
|
|
107
|
-
|
|
136
|
+
div[fields] > button[row]:hover {
|
|
137
|
+
background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
|
|
138
|
+
}
|
|
139
|
+
div[fields] > button[row][selected] {
|
|
140
|
+
background: var(--md-sys-color-secondary-container, rgba(0, 0, 0, 0.06));
|
|
141
|
+
}
|
|
142
|
+
div[fields] span[role='cell'] {
|
|
143
|
+
min-width: 0;
|
|
144
|
+
overflow-wrap: anywhere;
|
|
108
145
|
}
|
|
109
|
-
|
|
146
|
+
div[fields] span[name] {
|
|
147
|
+
display: grid;
|
|
148
|
+
}
|
|
149
|
+
div[fields] span[name] small,
|
|
150
|
+
div[fields] span[none] {
|
|
110
151
|
color: var(--md-sys-color-on-surface-variant);
|
|
111
152
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
div[fields] div[bindings] > h4 {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 4px;
|
|
157
|
+
margin: 12px 0 6px;
|
|
158
|
+
font-size: var(--ui-t-body, 13px);
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
}
|
|
161
|
+
div[fields] > div[detail] {
|
|
162
|
+
grid-column: 1 / -1;
|
|
163
|
+
padding: 8px 6px;
|
|
164
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
116
165
|
}
|
|
117
166
|
section[item] {
|
|
118
167
|
display: grid;
|
|
@@ -156,18 +205,6 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
156
205
|
margin: 0;
|
|
157
206
|
padding: 0;
|
|
158
207
|
}
|
|
159
|
-
div[cases] {
|
|
160
|
-
display: grid;
|
|
161
|
-
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
|
|
162
|
-
gap: 4px 6px;
|
|
163
|
-
align-items: center;
|
|
164
|
-
}
|
|
165
|
-
div[codes] {
|
|
166
|
-
display: grid;
|
|
167
|
-
grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr) 30px;
|
|
168
|
-
gap: 4px 6px;
|
|
169
|
-
align-items: center;
|
|
170
|
-
}
|
|
171
208
|
div[codes] button[act] {
|
|
172
209
|
width: 30px;
|
|
173
210
|
margin: 0;
|
|
@@ -185,52 +222,132 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
185
222
|
padding: 0;
|
|
186
223
|
}
|
|
187
224
|
/* A table's rows: the key (a code, or from and to), a cell per column, and a remove button. */
|
|
188
|
-
|
|
225
|
+
/* A table's values, a box each: the value on the head line, its settings a property grid under it (user 2026-11-12). */
|
|
226
|
+
div[cases] {
|
|
189
227
|
display: grid;
|
|
190
|
-
gap:
|
|
228
|
+
gap: 8px;
|
|
229
|
+
}
|
|
230
|
+
div[case] {
|
|
231
|
+
border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
232
|
+
border-radius: 6px;
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
}
|
|
235
|
+
div[case-head] {
|
|
236
|
+
display: flex;
|
|
191
237
|
align-items: center;
|
|
192
|
-
|
|
238
|
+
gap: 6px;
|
|
239
|
+
min-height: var(--figure-control-height);
|
|
240
|
+
padding: 2px 6px;
|
|
241
|
+
background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
|
|
193
242
|
}
|
|
194
|
-
div[
|
|
243
|
+
div[case-head] small {
|
|
195
244
|
color: var(--md-sys-color-on-surface-variant);
|
|
196
245
|
}
|
|
197
|
-
div[
|
|
198
|
-
|
|
246
|
+
div[case-head] input {
|
|
247
|
+
flex: 1;
|
|
199
248
|
min-width: 0;
|
|
200
|
-
width: 100%;
|
|
201
249
|
box-sizing: border-box;
|
|
202
250
|
}
|
|
203
|
-
div[
|
|
204
|
-
|
|
251
|
+
div[case-head] button[act] {
|
|
252
|
+
margin-left: auto;
|
|
253
|
+
}
|
|
254
|
+
div[case] > p[hint] {
|
|
205
255
|
margin: 0;
|
|
206
|
-
padding:
|
|
256
|
+
padding: 6px;
|
|
257
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
258
|
+
}
|
|
259
|
+
/* The 「그대로 둠」 of the other-values box: a checkbox and its word beside the box's name. */
|
|
260
|
+
div[case-head] label[keep] {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 4px;
|
|
264
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
265
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
266
|
+
}
|
|
267
|
+
div[case-head] label[keep] input {
|
|
268
|
+
flex: none;
|
|
269
|
+
width: auto;
|
|
270
|
+
margin: 0;
|
|
271
|
+
}
|
|
272
|
+
/* A property grid: a line a setting, its name left, its field right, a line between and between the two. */
|
|
273
|
+
div[props] {
|
|
274
|
+
display: grid;
|
|
275
|
+
grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
|
|
276
|
+
}
|
|
277
|
+
div[props] > span {
|
|
278
|
+
display: flex;
|
|
279
|
+
align-items: center;
|
|
280
|
+
min-width: 0;
|
|
281
|
+
min-height: var(--figure-control-height);
|
|
282
|
+
padding: 2px 6px;
|
|
283
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
284
|
+
}
|
|
285
|
+
div[props] > span:nth-child(odd) {
|
|
286
|
+
border-right: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
287
|
+
}
|
|
288
|
+
div[props] > span:nth-child(odd) small {
|
|
289
|
+
overflow: hidden;
|
|
290
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
text-overflow: ellipsis;
|
|
293
|
+
}
|
|
294
|
+
div[props] input,
|
|
295
|
+
div[props] select {
|
|
296
|
+
width: 100%;
|
|
297
|
+
min-width: 0;
|
|
298
|
+
box-sizing: border-box;
|
|
207
299
|
}
|
|
208
300
|
/* A colour and the button that clears it, on one line at the height of the cells beside it. */
|
|
209
|
-
div[
|
|
301
|
+
div[props] span[colour] {
|
|
210
302
|
display: flex;
|
|
303
|
+
flex: 1;
|
|
211
304
|
gap: 2px;
|
|
212
305
|
align-items: center;
|
|
213
306
|
min-width: 0;
|
|
214
307
|
}
|
|
215
|
-
div[
|
|
308
|
+
div[props] span[colour] input[type='color'] {
|
|
216
309
|
flex: 1;
|
|
217
310
|
height: var(--figure-control-height);
|
|
218
311
|
padding: 0 2px;
|
|
219
312
|
}
|
|
220
|
-
div[
|
|
313
|
+
div[props] span[colour] button[act] {
|
|
221
314
|
flex: none;
|
|
222
315
|
width: 24px;
|
|
223
316
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
317
|
+
/* A table's targets as a property list: what each is, its name, its delete. */
|
|
318
|
+
div[targets] {
|
|
319
|
+
display: grid;
|
|
320
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
228
321
|
align-items: center;
|
|
322
|
+
border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
323
|
+
border-radius: 6px;
|
|
324
|
+
overflow: hidden;
|
|
229
325
|
}
|
|
230
|
-
div[
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
326
|
+
div[targets] > * {
|
|
327
|
+
min-width: 0;
|
|
328
|
+
padding: 2px 6px;
|
|
329
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
330
|
+
}
|
|
331
|
+
div[targets] > small[head] {
|
|
332
|
+
grid-column: 1 / -1;
|
|
333
|
+
border-top: none;
|
|
334
|
+
padding: 4px 6px;
|
|
335
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
336
|
+
background: var(--md-sys-color-surface-container-low, rgba(0, 0, 0, 0.03));
|
|
337
|
+
}
|
|
338
|
+
div[targets] > span:not([none]):nth-of-type(odd) {
|
|
339
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
340
|
+
font-size: var(--ui-t-small, 11.5px);
|
|
341
|
+
}
|
|
342
|
+
div[targets] > span[none] {
|
|
343
|
+
grid-column: 1 / -1;
|
|
344
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
345
|
+
}
|
|
346
|
+
div[targets] > button[act] {
|
|
347
|
+
margin: 0;
|
|
348
|
+
padding: 0 4px;
|
|
349
|
+
border: none;
|
|
350
|
+
border-top: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.12));
|
|
234
351
|
}
|
|
235
352
|
textarea {
|
|
236
353
|
width: 100%;
|
|
@@ -238,10 +355,11 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
238
355
|
box-sizing: border-box;
|
|
239
356
|
font-family: var(--ui-mono, ui-monospace, monospace);
|
|
240
357
|
font-size: inherit;
|
|
358
|
+
resize: vertical;
|
|
241
359
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
360
|
+
/* The sample is read whole, a record of a dozen lines: room for it, not six lines of it (user 2026-11-12). */
|
|
361
|
+
textarea[sample] {
|
|
362
|
+
min-height: 240px;
|
|
245
363
|
}
|
|
246
364
|
md-icon {
|
|
247
365
|
font-size: 16px;
|
|
@@ -255,6 +373,8 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
255
373
|
@property({ type: Boolean }) locked = false
|
|
256
374
|
/** The part chosen in the modeller, put among a table's parts by one button. */
|
|
257
375
|
@property() selected = ''
|
|
376
|
+
/** Whether the editor shows the preview: the sample shows there only, the edit canvas keeping the figure as made. */
|
|
377
|
+
@property({ type: Boolean }) previewing = false
|
|
258
378
|
|
|
259
379
|
/** The schema row chosen, by its path; its field is edited under it. */
|
|
260
380
|
@state() private chosen = ''
|
|
@@ -262,7 +382,7 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
262
382
|
/** A site's data pasted to start the schema from; nothing is kept until the author says so. */
|
|
263
383
|
@state() private pasted = ''
|
|
264
384
|
/** A binding as it is being added: the field (or a new field's name), the method, the behavior or the table's targets. */
|
|
265
|
-
@state() private connecting = {
|
|
385
|
+
@state() private connecting = { ...NO_CONNECTING }
|
|
266
386
|
/** The binding opened to edit its rule, by id. */
|
|
267
387
|
@state() private opened = ''
|
|
268
388
|
/** Sample legends by role, as JSON typed; not saved. */
|
|
@@ -278,7 +398,7 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
278
398
|
if (!asset) return nothing
|
|
279
399
|
const rows = schemaRowsOf(asset)
|
|
280
400
|
return html`
|
|
281
|
-
${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${
|
|
401
|
+
${this.renderCapabilityData(asset)} ${this.renderSchema(asset, rows)} ${rows.length ? this.renderSample(asset) : nothing}
|
|
282
402
|
`
|
|
283
403
|
}
|
|
284
404
|
|
|
@@ -296,31 +416,44 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
296
416
|
|
|
297
417
|
private renderSchema(asset: FigureAsset, rows: FigureDataRow[]) {
|
|
298
418
|
const schema = asset.dataInterface?.schema ?? []
|
|
419
|
+
const bindings = asset.dataInterface?.bindings ?? []
|
|
420
|
+
const choices = behaviorsOf(asset)
|
|
421
|
+
const resting = figureBindingsTakenOver(asset)
|
|
299
422
|
return html`<section>
|
|
300
423
|
<h3>${i18next.t('figure.label.data-schema')}${this.help.button('figure.text.data-schema-explained')}</h3>
|
|
301
424
|
${this.help.hint('figure.text.data-schema-explained')}
|
|
302
425
|
${rows.length
|
|
303
|
-
? html`<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
426
|
+
? html`<div fields role="table">
|
|
427
|
+
<div head role="row">
|
|
428
|
+
<span role="columnheader">${i18next.t('figure.label.data-column-field')}</span>
|
|
429
|
+
<span role="columnheader">${i18next.t('figure.label.data-column-kind')}</span>
|
|
430
|
+
<span role="columnheader">${i18next.t('figure.label.data-column-moves')}</span>
|
|
431
|
+
</div>
|
|
432
|
+
${rows.map(row => {
|
|
433
|
+
/* What the field moves, named, in place of a count of bindings (「작동 1개」 said nothing, user 2026-11-12). */
|
|
434
|
+
const moves = row.bindings.map(id => bindings.find(b => b.id === id)).filter((b): b is FigureDataBinding => !!b).map(b => this.bindingWhat(asset, b))
|
|
435
|
+
return html`<button
|
|
436
|
+
row
|
|
437
|
+
role="row"
|
|
438
|
+
?selected=${row.path === this.chosen}
|
|
439
|
+
@click=${() => {
|
|
440
|
+
this.chosen = row.path === this.chosen ? '' : row.path
|
|
441
|
+
this.connecting = { ...NO_CONNECTING }
|
|
442
|
+
}}
|
|
443
|
+
>
|
|
444
|
+
<span name role="cell" style="padding-left: ${row.depth * 14}px"><span>${row.name}</span>${row.label ? html`<small>${row.label}</small>` : nothing}</span>
|
|
445
|
+
<span role="cell" title=${kindWords(row.node)}>${kindName(row.node)}</span>
|
|
446
|
+
<span role="cell" ?none=${!moves.length}>${moves.length ? moves.join(', ') : '—'}</span>
|
|
309
447
|
</button>
|
|
310
|
-
${row.path === this.chosen ? this.renderField(asset, schema, row) : nothing}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
</ul>`
|
|
448
|
+
${row.path === this.chosen ? html`<div detail>${this.renderField(asset, schema, row)} ${this.renderFieldBindings(asset, row, choices, resting)}</div>` : nothing}`
|
|
449
|
+
})}
|
|
450
|
+
</div>`
|
|
314
451
|
: html`<p hint>${i18next.t('figure.text.data-no-fields')}</p>`}
|
|
452
|
+
${this.renderStrayBindings(asset, rows, choices, resting)} ${this.renderRotation(asset, rows)}
|
|
315
453
|
${this.renderAddField(schema, '')} ${this.renderFromSample(schema)}
|
|
316
454
|
</section>`
|
|
317
455
|
}
|
|
318
456
|
|
|
319
|
-
/** A field's kind in the words every screen says it in (figure-viewer kindWords). */
|
|
320
|
-
private kindWord(node: FigureDataNode): string {
|
|
321
|
-
return kindWords(node)
|
|
322
|
-
}
|
|
323
|
-
|
|
324
457
|
/** The chosen field's own settings, and a field added under it when it holds fields. */
|
|
325
458
|
private renderField(asset: FigureAsset, schema: readonly FigureDataField[], row: FigureDataRow) {
|
|
326
459
|
const field = fieldAt(schema, row.path)
|
|
@@ -424,7 +557,14 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
424
557
|
this.edit({ kind: 'set-data-schema', schema: schemaWithField(schema, under, { name: named, ...nodeOfKind(a.kind) } as FigureDataField) })
|
|
425
558
|
this.adding = { ...a, name: '' }
|
|
426
559
|
}
|
|
427
|
-
|
|
560
|
+
const words = i18next.t(under ? 'figure.button.add-data-child' : 'figure.button.add-data-field', { parent: under })
|
|
561
|
+
/*
|
|
562
|
+
Folded under its summary, as a binding's form is: two fields always open under the table read as one more row of
|
|
563
|
+
it (user 2026-11-12). Open while the figure has no field yet: adding one is the only thing to do.
|
|
564
|
+
*/
|
|
565
|
+
return html`<details add-field ?open=${!schema.length}>
|
|
566
|
+
<summary>${words}</summary>
|
|
567
|
+
<div form>
|
|
428
568
|
<span>${i18next.t('figure.label.data-name')}</span>
|
|
429
569
|
<input
|
|
430
570
|
aria-label=${i18next.t('figure.label.data-name')}
|
|
@@ -438,8 +578,9 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
438
578
|
</select>
|
|
439
579
|
</div>
|
|
440
580
|
<button act ?disabled=${this.locked || !named} @click=${add}>
|
|
441
|
-
<md-icon>add</md-icon>${i18next.t(
|
|
442
|
-
</button
|
|
581
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add')}
|
|
582
|
+
</button>
|
|
583
|
+
</details>`
|
|
443
584
|
}
|
|
444
585
|
|
|
445
586
|
/** 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. */
|
|
@@ -448,7 +589,7 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
448
589
|
let unreadable = false
|
|
449
590
|
if (this.pasted.trim()) {
|
|
450
591
|
try {
|
|
451
|
-
guessed = inferFigureDataSchema(
|
|
592
|
+
guessed = inferFigureDataSchema(JSON5.parse(this.pasted))
|
|
452
593
|
} catch {
|
|
453
594
|
unreadable = true
|
|
454
595
|
}
|
|
@@ -474,34 +615,53 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
474
615
|
|
|
475
616
|
/* ---------------------------------------------------------------- the bindings */
|
|
476
617
|
|
|
618
|
+
/** What a binding moves: a table's parts, a value's behavior -- one wording for the binding's line and the field's row. */
|
|
619
|
+
private bindingWhat(asset: FigureAsset, b: FigureDataBinding): string {
|
|
620
|
+
return moveWords(dataMoveOf(asset, b.rule === 'table' ? { kind: 'table', targets: b.targets } : b.behavior))
|
|
621
|
+
}
|
|
622
|
+
|
|
477
623
|
/** What a behavior moves, in the words every screen says it in (figure-viewer dataMoveOf · moveWords). */
|
|
478
624
|
private behaviorName(asset: FigureAsset, b: FigureDataValueBinding['behavior']): string {
|
|
479
625
|
return moveWords(dataMoveOf(asset, b))
|
|
480
626
|
}
|
|
481
627
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
628
|
+
/*
|
|
629
|
+
A field's bindings under the field, in its opened row: what the field moves and how, edited where the field is. A
|
|
630
|
+
list of bindings of its own below the fields said every field's name and move again (user 2026-11-12).
|
|
631
|
+
*/
|
|
632
|
+
private renderFieldBindings(asset: FigureAsset, row: FigureDataRow, choices: BehaviorChoice[], resting: FigureRestingBinding[]) {
|
|
633
|
+
const bindings = (asset.dataInterface?.bindings ?? []).filter(b => row.bindings.includes(b.id))
|
|
634
|
+
const bindable = row.node.kind !== 'object'
|
|
635
|
+
if (!bindable && !bindings.length) return nothing
|
|
636
|
+
return html`<div bindings>
|
|
637
|
+
<h4>${i18next.t('figure.label.data-bindings-title')}${this.help.button('figure.text.data-bindings-explained')}</h4>
|
|
488
638
|
${this.help.hint('figure.text.data-bindings-explained')}
|
|
489
639
|
${bindings.map(b => this.renderBinding(asset, choices, b, resting))}
|
|
490
|
-
${this.renderAddBinding(asset,
|
|
491
|
-
|
|
492
|
-
|
|
640
|
+
${bindable ? this.renderAddBinding(asset, row, choices) : nothing}
|
|
641
|
+
</div>`
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** A binding no field reads -- its field renamed or taken out -- kept in sight below the fields, to delete or fix. */
|
|
645
|
+
private renderStrayBindings(asset: FigureAsset, rows: FigureDataRow[], choices: BehaviorChoice[], resting: FigureRestingBinding[]) {
|
|
646
|
+
const read = new Set(rows.flatMap(r => r.bindings))
|
|
647
|
+
const stray = (asset.dataInterface?.bindings ?? []).filter(b => !read.has(b.id))
|
|
648
|
+
if (!stray.length) return nothing
|
|
649
|
+
return html`<div bindings>
|
|
650
|
+
<h4>${i18next.t('figure.label.data-bindings-without-field')}</h4>
|
|
651
|
+
${stray.map(b => this.renderBinding(asset, choices, b, resting))}
|
|
652
|
+
</div>`
|
|
493
653
|
}
|
|
494
654
|
|
|
495
655
|
private renderBinding(asset: FigureAsset, choices: BehaviorChoice[], b: FigureDataBinding, resting: FigureRestingBinding[]) {
|
|
496
656
|
const open = this.opened === b.id
|
|
497
657
|
const set = (next: FigureDataBinding) => this.edit({ kind: 'set-data-binding', binding: next })
|
|
498
|
-
const what =
|
|
658
|
+
const what = this.bindingWhat(asset, b)
|
|
499
659
|
const rests = resting.some(r => r.id === b.id)
|
|
500
660
|
return html`<section item>
|
|
501
661
|
<div head>
|
|
502
662
|
<button line aria-expanded=${open ? 'true' : 'false'} @click=${() => (this.opened = open ? '' : b.id)}>
|
|
503
663
|
<strong>${b.label ?? b.accessor}</strong> → ${what}
|
|
504
|
-
<small>${i18next.t('figure.
|
|
664
|
+
<small>${i18next.t('figure.text.data-method-does-' + methodOf(b))}${rests ? html` <span rests>${i18next.t('figure.label.data-binding-resting')}</span>` : nothing}</small>
|
|
505
665
|
</button>
|
|
506
666
|
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-binding')} @click=${() => this.edit({ kind: 'remove-data-binding', id: b.id })}>
|
|
507
667
|
<md-icon>delete</md-icon>
|
|
@@ -604,7 +764,11 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
604
764
|
: nothing}`
|
|
605
765
|
}
|
|
606
766
|
|
|
607
|
-
|
|
767
|
+
/*
|
|
768
|
+
Which parts, controls and motion group a table shows on, as a property list: a row a target chosen -- what it is,
|
|
769
|
+
its name, its delete -- and one choice to add the next. Every part and control as a tick box in one heap took two
|
|
770
|
+
screens and hid which were chosen (user 2026-11-12).
|
|
771
|
+
*/
|
|
608
772
|
private renderTargets(
|
|
609
773
|
parts: string[],
|
|
610
774
|
inputs: Array<{ id: string; label: string }>,
|
|
@@ -612,36 +776,56 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
612
776
|
t: { parts?: string[]; inputs?: string[]; group?: string },
|
|
613
777
|
on: { part: (id: string, on: boolean) => void; input: (id: string, on: boolean) => void; group: (id: string) => void }
|
|
614
778
|
) {
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
779
|
+
const chosenParts = t.parts ?? []
|
|
780
|
+
const chosenInputs = t.inputs ?? []
|
|
781
|
+
const freeParts = parts.filter(p => !chosenParts.includes(p))
|
|
782
|
+
const freeInputs = inputs.filter(i => !chosenInputs.includes(i.id))
|
|
783
|
+
const labelOf = (id: string) => inputs.find(i => i.id === id)?.label ?? id
|
|
784
|
+
const row = (kind: string, name: string, remove: () => void) =>
|
|
785
|
+
html`<span>${i18next.t(kind)}</span><span>${name}</span>
|
|
786
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.data-remove-target')} @click=${remove}><md-icon>delete</md-icon></button>`
|
|
787
|
+
const add = (e: Event) => {
|
|
788
|
+
const select = e.target as HTMLSelectElement
|
|
789
|
+
const [kind, id] = [select.value.slice(0, select.value.indexOf(':')), select.value.slice(select.value.indexOf(':') + 1)]
|
|
790
|
+
select.value = ''
|
|
791
|
+
if (kind === 'part') on.part(id, true)
|
|
792
|
+
else if (kind === 'input') on.input(id, true)
|
|
793
|
+
}
|
|
794
|
+
return html`<div targets>
|
|
795
|
+
<small head>${i18next.t('figure.label.data-targets')}</small>
|
|
796
|
+
${chosenParts.map(p => row('figure.label.data-target-parts', p, () => on.part(p, false)))}
|
|
797
|
+
${chosenInputs.map(id => row('figure.label.data-target-inputs', labelOf(id), () => on.input(id, false)))}
|
|
798
|
+
${chosenParts.length || chosenInputs.length ? nothing : html`<span none>${i18next.t('figure.text.data-no-targets')}</span>`}
|
|
623
799
|
</div>
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
800
|
+
<div form>
|
|
801
|
+
<span>${i18next.t('figure.label.data-target-add')}</span>
|
|
802
|
+
<select ?disabled=${this.locked || !(freeParts.length + freeInputs.length)} .value=${''} @change=${add}>
|
|
803
|
+
<option value="">${i18next.t('figure.label.data-target-choose')}</option>
|
|
804
|
+
${freeParts.length
|
|
805
|
+
? html`<optgroup label=${i18next.t('figure.label.data-target-parts')}>${freeParts.map(p => html`<option value=${'part:' + p}>${p}</option>`)}</optgroup>`
|
|
806
|
+
: nothing}
|
|
807
|
+
${freeInputs.length
|
|
808
|
+
? html`<optgroup label=${i18next.t('figure.label.data-target-inputs')}>${freeInputs.map(i => html`<option value=${'input:' + i.id}>${i.label}</option>`)}</optgroup>`
|
|
809
|
+
: nothing}
|
|
810
|
+
</select>
|
|
811
|
+
${this.selected && freeParts.includes(this.selected)
|
|
812
|
+
? html`<span></span><button act ?disabled=${this.locked} @click=${() => on.part(this.selected, true)}><md-icon>add</md-icon>${i18next.t('figure.button.data-add-selected-part', { part: this.selected })}</button>`
|
|
813
|
+
: nothing}
|
|
814
|
+
${groups.length
|
|
815
|
+
? html`<span>${i18next.t('figure.label.data-target-group')}</span>
|
|
816
|
+
<select ?disabled=${this.locked} @change=${(e: Event) => on.group((e.target as HTMLSelectElement).value)}>
|
|
817
|
+
<option value="" ?selected=${!t.group}>${i18next.t('figure.label.data-none')}</option>
|
|
818
|
+
${groups.map(g => html`<option value=${g.id} ?selected=${g.id === t.group}>${g.label}</option>`)}
|
|
819
|
+
</select>`
|
|
820
|
+
: nothing}
|
|
821
|
+
</div>`
|
|
638
822
|
}
|
|
639
823
|
|
|
640
824
|
/** The column heads of a table's rows, after the key's own one or two. */
|
|
641
825
|
private columnHeads(columns: TableColumn[]) {
|
|
642
826
|
return columns.map(c =>
|
|
643
827
|
c.kind === 'value'
|
|
644
|
-
? html`<small>${c.label}
|
|
828
|
+
? html`<small>${c.label}</small>`
|
|
645
829
|
: html`<small>${i18next.t('figure.label.data-column-' + c.kind)}</small>`
|
|
646
830
|
)
|
|
647
831
|
}
|
|
@@ -665,7 +849,9 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
665
849
|
</select>`
|
|
666
850
|
if (c.kind === 'value') {
|
|
667
851
|
const v = look.values?.[c.id]
|
|
668
|
-
|
|
852
|
+
/* Its unit and range in the field, not beside the name: 「fl-hip (deg -30~30)」 took two lines (user 2026-11-12). */
|
|
853
|
+
const range = [c.min !== undefined && c.max !== undefined ? `${c.min}~${c.max}` : '', c.unit ?? ''].filter(Boolean).join(' ')
|
|
854
|
+
return html`<input type="number" step="any" min=${c.min ?? ''} max=${c.max ?? ''} placeholder=${range} title=${range} aria-label=${c.label} .value=${v === undefined ? '' : String(v)} ?disabled=${this.locked} @change=${(e: Event) => {
|
|
669
855
|
const n = (e.target as HTMLInputElement).valueAsNumber
|
|
670
856
|
const values = { ...look.values }
|
|
671
857
|
if (Number.isNaN(n)) delete values[c.id]
|
|
@@ -690,16 +876,32 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
690
876
|
})
|
|
691
877
|
}
|
|
692
878
|
|
|
879
|
+
/*
|
|
880
|
+
One value's settings as a property grid, a line a setting -- colour, effect, a control's value, clip, speed, pause --
|
|
881
|
+
its name left, its field right. A column a setting ran wider than the panel as controls were added (user 2026-11-12).
|
|
882
|
+
*/
|
|
883
|
+
private renderLook(asset: FigureAsset, columns: TableColumn[], look: FigureDataTableEntry, write: (look: FigureDataTableEntry) => void) {
|
|
884
|
+
if (!columns.length) return html`<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>`
|
|
885
|
+
const heads = this.columnHeads(columns)
|
|
886
|
+
const cells = this.lookCells(asset, columns, look, write)
|
|
887
|
+
return html`<div props>${columns.map((_, i) => html`<span>${heads[i]}</span><span>${cells[i]}</span>`)}</div>`
|
|
888
|
+
}
|
|
889
|
+
|
|
693
890
|
/** The look of every other value: a row like the others, added or taken away; kept when a closed field's values are not all rows. */
|
|
694
|
-
private renderOtherwise(asset: FigureAsset, b: FigureDataTableBinding, columns: TableColumn[], needed: boolean, set: (next: FigureDataBinding) => void
|
|
891
|
+
private renderOtherwise(asset: FigureAsset, b: FigureDataTableBinding, columns: TableColumn[], needed: boolean, set: (next: FigureDataBinding) => void) {
|
|
695
892
|
const look = b.table.default
|
|
696
893
|
if (look === undefined)
|
|
697
894
|
return html`<button act ?disabled=${this.locked} @click=${() => set(tableWithLook(b, 'default', {}))}><md-icon>add</md-icon>${i18next.t('figure.button.add-data-default')}</button>`
|
|
698
895
|
const { default: _, ...rest } = b.table
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
896
|
+
/* 「그대로 둠」 (ADR-0166 amendment 1): a value the table has no row for leaves the targets as it last showed them. */
|
|
897
|
+
const keep = look.keep === true
|
|
898
|
+
return html`<div case>
|
|
899
|
+
<div case-head>
|
|
900
|
+
<strong>${i18next.t('figure.label.data-otherwise')}</strong>
|
|
901
|
+
<label keep><input type="checkbox" .checked=${keep} ?disabled=${this.locked} @change=${(e: Event) => set(tableWithKeep(b, (e.target as HTMLInputElement).checked))} />${i18next.t('figure.label.data-keep')}</label>
|
|
902
|
+
${needed ? nothing : html`<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => set({ ...b, table: rest })}><md-icon>delete</md-icon></button>`}
|
|
903
|
+
</div>
|
|
904
|
+
${keep ? html`<p hint>${i18next.t('figure.text.data-keep')}</p>` : this.renderLook(asset, columns, look, l => set(tableWithLook(b, 'default', l)))}
|
|
703
905
|
</div>`
|
|
704
906
|
}
|
|
705
907
|
|
|
@@ -711,6 +913,8 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
711
913
|
const columns = tableColumnsOf(asset, b)
|
|
712
914
|
const node = fieldAt(asset.dataInterface?.schema ?? [], b.accessor.replace(/\[\d+\]/g, '[]'))
|
|
713
915
|
const boolean = node?.kind === 'boolean'
|
|
916
|
+
/* A number's codes are numbers, as its data gives them (ADR-0166 amendment 1). */
|
|
917
|
+
const numeric = node?.kind === 'number'
|
|
714
918
|
const rows = Object.entries(b.table).filter(([k]) => k !== 'default')
|
|
715
919
|
const domain = node ? domainOf(node).map(String) : []
|
|
716
920
|
const covered = domain.length > 0 && domain.every(v => b.table[v] !== undefined)
|
|
@@ -722,26 +926,31 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
722
926
|
}
|
|
723
927
|
const rekey = (i: number, typed: string) => {
|
|
724
928
|
const key = typed.trim()
|
|
725
|
-
if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key)) return this.requestUpdate()
|
|
929
|
+
if (!key || key === 'default' || rows.some(([k], j) => j !== i && k === key) || (numeric && !Number.isFinite(Number(key)))) return this.requestUpdate()
|
|
726
930
|
rekeyed(rows.map(([k, v], j) => (j === i ? [key, v] : [k, v])))
|
|
727
931
|
}
|
|
728
932
|
const word = (k: string) => (boolean ? i18next.t(k === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : k)
|
|
729
|
-
|
|
730
|
-
|
|
933
|
+
/* A value a box: its code on the box's head line, its settings a property grid under it. */
|
|
934
|
+
return html`<div cases>
|
|
731
935
|
${rows.map(
|
|
732
|
-
([k, look], i) => html
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
936
|
+
([k, look], i) => html`<div case>
|
|
937
|
+
<div case-head>
|
|
938
|
+
<small>${i18next.t('figure.label.data-case-value')}</small>
|
|
939
|
+
${boolean
|
|
940
|
+
? html`<strong>${word(k)}</strong>`
|
|
941
|
+
: html`<input .value=${k} inputmode=${numeric ? 'decimal' : 'text'} aria-label=${i18next.t('figure.label.data-case-value')} ?disabled=${this.locked} @change=${(e: Event) => rekey(i, (e.target as HTMLInputElement).value)} />`}
|
|
942
|
+
${boolean || (rows.length < 2 && domain.length > 0)
|
|
943
|
+
? nothing
|
|
944
|
+
: html`<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-case')} @click=${() => rekeyed(rows.filter((_, j) => j !== i))}><md-icon>delete</md-icon></button>`}
|
|
945
|
+
</div>
|
|
946
|
+
${this.renderLook(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
947
|
+
</div>`
|
|
739
948
|
)}
|
|
949
|
+
${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set)}
|
|
740
950
|
</div>
|
|
741
|
-
${this.renderOtherwise(asset, b, columns, domain.length > 0 && !covered, set, 1)}
|
|
742
951
|
${boolean
|
|
743
952
|
? nothing
|
|
744
|
-
: html`<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [freeCodeOf(rows.map(([k]) => k)), {}]])}>
|
|
953
|
+
: html`<button act ?disabled=${this.locked} @click=${() => rekeyed([...rows, [(numeric ? freeNumberCodeOf : freeCodeOf)(rows.map(([k]) => k)), {}]])}>
|
|
745
954
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-case')}
|
|
746
955
|
</button>`}`
|
|
747
956
|
}
|
|
@@ -771,17 +980,21 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
771
980
|
const last = rows.at(-1)
|
|
772
981
|
const lastEnd = last ? ends(last[0]).to : ''
|
|
773
982
|
const open = !!last && lastEnd === ''
|
|
774
|
-
return html`<div
|
|
775
|
-
<small>${i18next.t('figure.label.data-range-from')}</small><small>${i18next.t('figure.label.data-range-to')}</small>${this.columnHeads(columns)}<span></span>
|
|
983
|
+
return html`<div cases>
|
|
776
984
|
${rows.map(([k, look], i) => {
|
|
777
985
|
const e = ends(k)
|
|
778
|
-
return html`<
|
|
779
|
-
<
|
|
780
|
-
|
|
781
|
-
|
|
986
|
+
return html`<div case>
|
|
987
|
+
<div case-head>
|
|
988
|
+
<input type="number" step="any" aria-label=${i18next.t('figure.label.data-range-from')} placeholder=${i18next.t('figure.label.data-range-from')} .value=${e.from} ?disabled=${this.locked} @change=${(ev: Event) => rekey(i, 'from', (ev.target as HTMLInputElement).value)} />
|
|
989
|
+
<span>~</span>
|
|
990
|
+
<input type="number" step="any" aria-label=${i18next.t('figure.label.data-range-to')} placeholder=${i18next.t('figure.label.data-range-to')} .value=${e.to} ?disabled=${this.locked} @change=${(ev: Event) => rekey(i, 'to', (ev.target as HTMLInputElement).value)} />
|
|
991
|
+
<button act ?disabled=${this.locked} title=${i18next.t('figure.button.remove-data-range')} @click=${() => set(tableWithKeys(b, rows.filter((_, j) => j !== i)))}><md-icon>delete</md-icon></button>
|
|
992
|
+
</div>
|
|
993
|
+
${this.renderLook(asset, columns, look, l => set(tableWithLook(b, k, l)))}
|
|
994
|
+
</div>`
|
|
782
995
|
})}
|
|
996
|
+
${this.renderOtherwise(asset, b, columns, false, set)}
|
|
783
997
|
</div>
|
|
784
|
-
${this.renderOtherwise(asset, b, columns, false, set, 2)}
|
|
785
998
|
${open ? html`<p hint>${i18next.t('figure.text.data-range-close-last')}</p>` : nothing}
|
|
786
999
|
<button act ?disabled=${this.locked || open} @click=${() => set(tableWithKeys(b, [...rows, [`${lastEnd}~`, {}]]))}>
|
|
787
1000
|
<md-icon>add</md-icon>${i18next.t('figure.button.add-data-range')}
|
|
@@ -824,48 +1037,38 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
824
1037
|
* 「연결 추가」 (ADR-0166 10): a field there or a new name, then a method the field's kind allows. As it is or scaled,
|
|
825
1038
|
* the one thing it moves; a table, the parts, controls and motion group it shows on -- a new name makes the field.
|
|
826
1039
|
*/
|
|
827
|
-
|
|
828
|
-
|
|
1040
|
+
/** A binding added to the field whose row is open: how it is read, and what it moves. */
|
|
1041
|
+
private renderAddBinding(asset: FigureAsset, row: FigureDataRow, choices: BehaviorChoice[]) {
|
|
829
1042
|
const a = this.connecting
|
|
830
|
-
|
|
831
|
-
const NEW = '+'
|
|
832
|
-
const row = a.path === NEW || !bindable.length ? null : (bindable.find(r => r.path === a.path) ?? bindable[0]!)
|
|
833
|
-
const methods = methodsOf(row?.node ?? null, choices)
|
|
1043
|
+
const methods = methodsOf(row.node, choices)
|
|
834
1044
|
const method = methods.includes(a.method as DataMethod) ? (a.method as DataMethod) : methods[0]
|
|
835
1045
|
const table = method === 'code' || method === 'ranges'
|
|
836
|
-
const fitting =
|
|
1046
|
+
const fitting = !table ? choices.filter(c => fitsField(row.node, c)) : []
|
|
837
1047
|
const choice = fitting.find(c => c.key === a.behavior) ?? fitting[0]
|
|
838
1048
|
const set = (patch: Partial<typeof a>) => (this.connecting = { ...a, ...patch })
|
|
839
1049
|
const typed = (e: Event) => (e.target as HTMLInputElement | HTMLSelectElement).value
|
|
840
|
-
const name = a.name.trim()
|
|
841
1050
|
const targets = { parts: a.parts, inputs: a.inputs, ...(a.group ? { group: a.group } : {}) }
|
|
842
1051
|
const aimed = a.parts.length > 0 || a.inputs.length > 0 || !!a.group
|
|
843
|
-
const ready = table ? aimed
|
|
1052
|
+
const ready = table ? aimed : !!choice
|
|
844
1053
|
const add = () => {
|
|
845
1054
|
if (table) {
|
|
846
|
-
const { binding
|
|
847
|
-
this.edit({ kind: 'set-data-binding', binding
|
|
1055
|
+
const { binding } = tableDraftOf(asset, { path: row.path, node: row.node }, method as 'code' | 'ranges', targets)
|
|
1056
|
+
this.edit({ kind: 'set-data-binding', binding })
|
|
848
1057
|
this.opened = binding.id
|
|
849
1058
|
} else {
|
|
850
1059
|
/* A list is read whole by items; one value of it, through its first element. */
|
|
851
|
-
const accessor = row
|
|
852
|
-
const binding = bindingDraftOf(asset, accessor, row
|
|
1060
|
+
const accessor = row.node.kind === 'list' && choice!.kind === 'items' ? row.path.replace(/\[\]$/, '') : accessorOfRow(row.path)
|
|
1061
|
+
const binding = bindingDraftOf(asset, accessor, row.node, choice!, method as 'value' | 'scale')
|
|
853
1062
|
this.edit({ kind: 'set-data-binding', binding })
|
|
854
1063
|
this.opened = binding.id
|
|
855
1064
|
}
|
|
856
|
-
this.connecting = {
|
|
1065
|
+
this.connecting = { ...NO_CONNECTING }
|
|
857
1066
|
}
|
|
858
1067
|
const toggle = (list: string[], id: string, on: boolean) => (on ? [...list, id] : list.filter(x => x !== id))
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
<option value=${NEW} ?selected=${row === null}>${i18next.t('figure.label.data-new-field')}</option>
|
|
864
|
-
</select>
|
|
865
|
-
${row === null
|
|
866
|
-
? html`<span>${i18next.t('figure.label.data-name')}</span>
|
|
867
|
-
<input .value=${a.name} @input=${(e: Event) => set({ name: typed(e) })} />`
|
|
868
|
-
: nothing}
|
|
1068
|
+
/* Folded until asked for: open under a field's binding, its fields read as that binding's own (user 2026-11-12). */
|
|
1069
|
+
return html`<details add-binding>
|
|
1070
|
+
<summary>${i18next.t('figure.button.add-data-binding')}</summary>
|
|
1071
|
+
<div form>
|
|
869
1072
|
<span>${i18next.t('figure.label.data-method')}</span>
|
|
870
1073
|
${methods.length === 1
|
|
871
1074
|
? /* One way only (a text is read by a code table): said, not offered as a choice of one (sweep 2026-10-27 U10). */
|
|
@@ -890,8 +1093,9 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
890
1093
|
${table && !aimed ? html`<p hint>${i18next.t('figure.text.data-table-needs-target')}</p>` : nothing}
|
|
891
1094
|
${methods.length ? nothing : html`<p hint>${i18next.t('figure.text.data-no-behavior')}</p>`}
|
|
892
1095
|
<button act ?disabled=${this.locked || !ready} @click=${add}>
|
|
893
|
-
<md-icon>add</md-icon>${i18next.t('figure.button.add
|
|
894
|
-
</button
|
|
1096
|
+
<md-icon>add</md-icon>${i18next.t('figure.button.add')}
|
|
1097
|
+
</button>
|
|
1098
|
+
</details>`
|
|
895
1099
|
}
|
|
896
1100
|
|
|
897
1101
|
/**
|
|
@@ -973,43 +1177,28 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
973
1177
|
|
|
974
1178
|
/** 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. */
|
|
975
1179
|
private renderSample(asset: FigureAsset) {
|
|
976
|
-
|
|
1180
|
+
/* Written and read as JSON5: names without quotes, comments and a last comma are allowed when typed by hand (user 2026-11-12). */
|
|
1181
|
+
const text = this.sample || JSON5.stringify(figureDataExample(asset.dataInterface!), null, 2)
|
|
977
1182
|
let data: unknown
|
|
978
1183
|
let unreadable = false
|
|
979
1184
|
try {
|
|
980
|
-
data =
|
|
1185
|
+
data = JSON5.parse(text)
|
|
981
1186
|
} catch {
|
|
982
1187
|
unreadable = true
|
|
983
1188
|
}
|
|
984
1189
|
const legends = this.sampleLegends()
|
|
985
1190
|
const read = unreadable ? null : sampleOf(asset, data, legends)
|
|
986
|
-
const r = read?.result
|
|
987
1191
|
const tables = (asset.dataInterface?.bindings ?? []).filter((b): b is FigureDataTableBinding => b.rule === 'table')
|
|
988
1192
|
return html`<section>
|
|
989
1193
|
<h3>${i18next.t('figure.label.data-sample')}${this.help.button('figure.text.data-sample-explained')}</h3>
|
|
990
1194
|
${this.help.hint('figure.text.data-sample-explained')}
|
|
991
|
-
<textarea .value=${text} @input=${(e: Event) => this.try((e.target as HTMLTextAreaElement).value)}></textarea>
|
|
1195
|
+
<textarea sample .value=${text} @input=${(e: Event) => this.try((e.target as HTMLTextAreaElement).value)}></textarea>
|
|
992
1196
|
${unreadable
|
|
993
1197
|
? html`<p hint>${i18next.t('figure.text.data-not-json')}</p>`
|
|
994
1198
|
: read!.issues.length
|
|
995
1199
|
? html`<p hint>${read!.issues.map(i => i18next.t('figure.text.data-value-' + i.reason, { path: i.path || '-' })).join(' · ')}</p>`
|
|
996
|
-
:
|
|
997
|
-
${Object.entries(r!.states).map(([input, s]) => html`<li>${asset.stateInputs?.[input]?.label ?? input} = ${s.value}</li>`)}
|
|
998
|
-
${Object.entries(r!.palette).map(([token, colour]) => html`<li>${token} = ${colour}</li>`)}
|
|
999
|
-
${Object.entries(r!.motion).map(
|
|
1000
|
-
([group, m]) =>
|
|
1001
|
-
html`<li>
|
|
1002
|
-
${(asset.motionGroups ?? []).find(g => g.id === group)?.label ?? group}:
|
|
1003
|
-
${[m.clip, m.speed === undefined ? '' : `× ${m.speed}`, m.paused ? i18next.t('figure.label.data-paused') : ''].filter(Boolean).join(' ')}
|
|
1004
|
-
</li>`
|
|
1005
|
-
)}
|
|
1006
|
-
${tables.map(b => this.renderTableUse(asset, b, read!.uses[b.id]))}
|
|
1007
|
-
${Object.entries(r!.parts).map(([part, look]) => html`<li>${part}: ${[look.color, look.effect ? i18next.t('figure.label.data-effect-' + look.effect.kind) : ''].filter(Boolean).join(' ')}</li>`)}
|
|
1008
|
-
${Object.entries(r!.pose).map(([axis, p]) => html`<li>${i18next.t('figure.label.data-axis-' + axis)} = ${p!.value}</li>`)}
|
|
1009
|
-
${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>`)}
|
|
1010
|
-
</ul>`}
|
|
1200
|
+
: nothing}
|
|
1011
1201
|
${read?.notes.length ? html`<p hint>${read.notes.map(n => i18next.t('figure.text.data-note-' + n.reason, { binding: n.binding ?? '', key: n.key ?? '', column: n.column ? i18next.t('figure.label.data-column-' + n.column) : '', value: shownValue(n.value) })).join(' · ')}</p>` : nothing}
|
|
1012
|
-
${r && Object.keys(r.pose).length ? html`<p hint>${i18next.t('figure.text.data-sample-moves-all')}</p>` : nothing}
|
|
1013
1202
|
${this.renderSampleLegends(tables)}
|
|
1014
1203
|
<label toggle>
|
|
1015
1204
|
<input
|
|
@@ -1022,25 +1211,24 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
1022
1211
|
/>
|
|
1023
1212
|
${i18next.t('figure.label.data-sample-in-preview')}
|
|
1024
1213
|
</label>
|
|
1214
|
+
${this.sampling && !this.previewing
|
|
1215
|
+
? html`<p hint sample-where>
|
|
1216
|
+
${i18next.t('figure.text.data-sample-shown-in-preview')}
|
|
1217
|
+
<button quiet @click=${() => this.dispatchEvent(new CustomEvent('show-preview', { bubbles: true, composed: true }))}>
|
|
1218
|
+
${i18next.t('figure.button.data-sample-show-preview')}
|
|
1219
|
+
</button>
|
|
1220
|
+
</p>`
|
|
1221
|
+
: nothing}
|
|
1025
1222
|
</section>`
|
|
1026
1223
|
}
|
|
1027
1224
|
|
|
1028
|
-
/** Which row of a table the sample's value matched, and from which table: the figure's own or the sample legend of its role. */
|
|
1029
|
-
private renderTableUse(asset: FigureAsset, b: FigureDataTableBinding, use: { value: unknown; key: string | null; source: string; legend?: string } | undefined) {
|
|
1030
|
-
if (!use || use.value === undefined) return nothing
|
|
1031
|
-
const word = (v: unknown) => (typeof v === 'boolean' || v === 'true' || v === 'false' ? i18next.t(String(v) === 'true' ? 'figure.label.data-true' : 'figure.label.data-false') : String(v))
|
|
1032
|
-
const row = use.key === null ? i18next.t('figure.label.data-none') : use.key === 'default' ? i18next.t('figure.label.data-otherwise') : word(use.key)
|
|
1033
|
-
const from = use.source === 'figure' ? i18next.t('figure.label.data-table-own') : i18next.t('figure.label.data-table-legend', { name: use.legend ?? '' })
|
|
1034
|
-
return html`<li>${b.label ?? b.accessor} = ${word(use.value)} → ${row} <small>(${from})</small></li>`
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
1225
|
/** The sample legends as read: by role, those that are JSON. */
|
|
1038
1226
|
private sampleLegends(): Record<string, unknown> {
|
|
1039
1227
|
const out: Record<string, unknown> = {}
|
|
1040
1228
|
for (const [role, text] of Object.entries(this.legendSample)) {
|
|
1041
1229
|
if (!text.trim()) continue
|
|
1042
1230
|
try {
|
|
1043
|
-
out[role] =
|
|
1231
|
+
out[role] = JSON5.parse(text)
|
|
1044
1232
|
} catch {
|
|
1045
1233
|
/* Not JSON yet: the figure's own table shows. */
|
|
1046
1234
|
}
|
|
@@ -1075,7 +1263,7 @@ export class FigureDataPanel extends localize(i18next)(LitElement) {
|
|
|
1075
1263
|
private try(typed: string) {
|
|
1076
1264
|
this.sample = typed
|
|
1077
1265
|
try {
|
|
1078
|
-
this.tell(
|
|
1266
|
+
this.tell(JSON5.parse(typed))
|
|
1079
1267
|
} catch {
|
|
1080
1268
|
/* Not JSON yet: the preview keeps what it ran on. */
|
|
1081
1269
|
}
|
|
@@ -1101,7 +1289,3 @@ function nodeOfKind(kind: FigureDataNode['kind']): FigureDataNode {
|
|
|
1101
1289
|
/** A value in a sentence: a text as it is, anything else as JSON. */
|
|
1102
1290
|
const shownValue = (v: unknown): string => (typeof v === 'string' ? v : JSON.stringify(v ?? ''))
|
|
1103
1291
|
|
|
1104
|
-
/** A table row's columns: the key's one or two, a cell per column, and the remove button. */
|
|
1105
|
-
function gridOf(keyCells: number, columns: number): string {
|
|
1106
|
-
return `${'minmax(64px, 1fr) '.repeat(keyCells)}${'minmax(72px, 1fr) '.repeat(columns)}30px`
|
|
1107
|
-
}
|