@things-factory/figure-ui 10.1.272 → 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/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 +12 -2
- package/client/modeller/fasten-geometry.ts +5 -3
- package/client/modeller/figure-data-panel.ts +384 -200
- package/client/modeller/figure-fasten-drawing.ts +21 -13
- package/client/modeller/figure-links.ts +24 -9
- package/client/modeller/figure-parts.ts +7 -11
- package/client/modeller/figure-preview.ts +258 -76
- package/dist-client/modeller/clips.d.ts +1 -4
- 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 +5 -0
- package/dist-client/modeller/fasten-form.js +8 -2
- 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-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-drawing.js +21 -13
- package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
- package/dist-client/modeller/figure-links.js +31 -16
- 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 +257 -77
- package/dist-client/modeller/figure-preview.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/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/serve-response.mjs +5 -2
- package/test/browser/side-panel-sweep.check.mjs +171 -10
- package/test/data-motion.test.ts +39 -8
- package/test/fasten-form.test.ts +25 -0
- package/test/fasten-geometry.test.ts +13 -0
- package/translations/en.json +23 -11
- package/translations/es.json +22 -10
- package/translations/ja.json +22 -10
- package/translations/ko.json +24 -12
- package/translations/ms.json +22 -10
- package/translations/zh.json +22 -10
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import '@material/web/icon/icon.js';
|
|
2
3
|
import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
|
|
3
4
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
4
5
|
import { i18next, localize } from '@operato/i18n';
|
|
5
6
|
import { unitWord } from './unit-words.js';
|
|
7
|
+
import { scrubLabel } from './live.js';
|
|
6
8
|
import * as THREE from 'three';
|
|
7
9
|
import { compileFigureAsset } from '@hatiolab/figure-model';
|
|
8
10
|
import { create, figureLegendSourcesChanged, registerFigure, unregisterFigure } from '@hatiolab/things-scene';
|
|
@@ -93,21 +95,43 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
93
95
|
background-color: rgba(28, 28, 30, 0.72);
|
|
94
96
|
border-radius: 4px;
|
|
95
97
|
}
|
|
96
|
-
|
|
98
|
+
/*
|
|
99
|
+
The legend, the size sliders and the controls in one column over the stage: the controls take what is left under
|
|
100
|
+
the two above and scroll inside it. A tall robot's rows in a short preview ran off the stage's top, the keep and
|
|
101
|
+
fold buttons with them, and nothing scrolled (2026-11-12); a legend on two lines in a narrow preview is room taken too.
|
|
102
|
+
*/
|
|
103
|
+
div[over] {
|
|
97
104
|
position: absolute;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
inset: 8px;
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
align-items: flex-start;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
pointer-events: none;
|
|
111
|
+
}
|
|
112
|
+
div[over] > div[legend],
|
|
113
|
+
div[over] > div[sizes] {
|
|
114
|
+
pointer-events: auto;
|
|
115
|
+
}
|
|
116
|
+
div[foot] {
|
|
117
|
+
align-self: stretch;
|
|
118
|
+
flex: 1;
|
|
119
|
+
min-height: 0;
|
|
101
120
|
display: flex;
|
|
102
121
|
flex-wrap: wrap-reverse;
|
|
103
122
|
justify-content: space-between;
|
|
104
123
|
align-items: flex-end;
|
|
124
|
+
align-content: flex-start;
|
|
105
125
|
gap: 8px;
|
|
106
126
|
pointer-events: none;
|
|
107
127
|
}
|
|
108
128
|
div[foot] > * { pointer-events: auto; }
|
|
109
|
-
|
|
110
|
-
div[setup]
|
|
129
|
+
/* One line over the figure -- the part and the button -- its words in its title: three lines covered the figure (user 2026-11-12). */
|
|
130
|
+
div[setup] { max-width: min(320px, 100%); box-sizing: border-box; padding: 6px 8px; border-radius: 8px; background: rgba(28,28,30,.9); color: #fff; display: flex; align-items: center; gap: 6px; font-size: var(--ui-t-micro, 11px); }
|
|
131
|
+
div[setup] select { flex: 1 1 auto; }
|
|
132
|
+
div[setup] button { flex: none; white-space: nowrap; }
|
|
133
|
+
/* The controls' last row: as wide as they are, a line over it in place of a box of its own. */
|
|
134
|
+
div[clips] div[setup] { max-width: none; padding-top: 8px; border-top: 1px solid rgba(255, 255, 255, 0.14); border-radius: 0; color: inherit; }
|
|
111
135
|
/* The help's choice and button in the look of the controls beside it, not the browser's grey (sweep 2026-10-27 U7). */
|
|
112
136
|
div[setup] select,
|
|
113
137
|
div[setup] button {
|
|
@@ -130,9 +154,8 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
130
154
|
}
|
|
131
155
|
/* Which is which: side by side means nothing without a legend. */
|
|
132
156
|
div[legend] {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
left: 8px;
|
|
157
|
+
max-width: 100%;
|
|
158
|
+
box-sizing: border-box;
|
|
136
159
|
display: flex;
|
|
137
160
|
gap: 6px;
|
|
138
161
|
padding: 4px 8px;
|
|
@@ -177,13 +200,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
177
200
|
}
|
|
178
201
|
/* The one figure's size: under the legend, in the controls' look. */
|
|
179
202
|
div[sizes] {
|
|
180
|
-
position: absolute;
|
|
181
|
-
top: 40px;
|
|
182
|
-
left: 8px;
|
|
183
203
|
display: grid;
|
|
184
204
|
gap: 6px;
|
|
185
205
|
padding: 8px 10px;
|
|
186
|
-
max-width:
|
|
206
|
+
max-width: 100%;
|
|
187
207
|
box-sizing: border-box;
|
|
188
208
|
color: rgba(255, 255, 255, 0.92);
|
|
189
209
|
background-color: rgba(28, 28, 30, 0.72);
|
|
@@ -204,8 +224,12 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
204
224
|
font-variant-numeric: tabular-nums;
|
|
205
225
|
text-align: right;
|
|
206
226
|
}
|
|
207
|
-
|
|
208
|
-
|
|
227
|
+
/* The size said at the legend's end, on its line: a box of its own under the legend took a second line (user 2026-11-12). */
|
|
228
|
+
div[legend] span[said] {
|
|
229
|
+
padding-left: 6px;
|
|
230
|
+
border-left: 1px solid rgba(255, 255, 255, 0.22);
|
|
231
|
+
align-self: center;
|
|
232
|
+
white-space: nowrap;
|
|
209
233
|
font-variant-numeric: tabular-nums;
|
|
210
234
|
font-weight: 600;
|
|
211
235
|
}
|
|
@@ -220,8 +244,12 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
220
244
|
*/
|
|
221
245
|
div[clips] {
|
|
222
246
|
margin-left: auto;
|
|
247
|
+
max-height: 100%;
|
|
248
|
+
box-sizing: border-box;
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
overscroll-behavior: contain;
|
|
223
251
|
display: grid;
|
|
224
|
-
grid-template-columns:
|
|
252
|
+
grid-template-columns: max-content 20px 96px 44px;
|
|
225
253
|
gap: 6px 8px;
|
|
226
254
|
align-items: center;
|
|
227
255
|
padding: 8px 10px;
|
|
@@ -238,6 +266,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
238
266
|
grid-template-columns: subgrid;
|
|
239
267
|
align-items: center;
|
|
240
268
|
}
|
|
269
|
+
/* A name and its unit on one line: 「position.x」 put its 「mm」 on a second line with room beside it (user 2026-11-12). */
|
|
270
|
+
div[clips] label > span:first-child {
|
|
271
|
+
white-space: nowrap;
|
|
272
|
+
}
|
|
241
273
|
/* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
|
|
242
274
|
div[clips] select {
|
|
243
275
|
grid-column: 2 / -1;
|
|
@@ -300,9 +332,65 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
300
332
|
div[clips] button[fold-controls]:hover {
|
|
301
333
|
opacity: 1;
|
|
302
334
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
335
|
+
/* The keep, clear and fold line, and the size help's, stay in sight while the rows between them scroll. */
|
|
336
|
+
div[clips] div[actions],
|
|
337
|
+
div[clips] div[setup] {
|
|
338
|
+
position: sticky;
|
|
339
|
+
z-index: 1;
|
|
340
|
+
margin-left: -10px;
|
|
341
|
+
margin-right: -10px;
|
|
342
|
+
padding-left: 10px;
|
|
343
|
+
padding-right: 10px;
|
|
344
|
+
background-color: rgba(28, 28, 30, 0.96);
|
|
345
|
+
}
|
|
346
|
+
div[clips] div[actions] {
|
|
347
|
+
top: -8px;
|
|
348
|
+
margin-top: -8px;
|
|
349
|
+
padding-top: 8px;
|
|
350
|
+
padding-bottom: 4px;
|
|
351
|
+
}
|
|
352
|
+
div[clips] div[setup] {
|
|
353
|
+
bottom: -8px;
|
|
354
|
+
margin-bottom: -8px;
|
|
355
|
+
padding-bottom: 8px;
|
|
356
|
+
}
|
|
357
|
+
div[clips] div[actions] {
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
gap: 6px;
|
|
361
|
+
min-width: 0;
|
|
362
|
+
}
|
|
363
|
+
/* Folded, the box is as wide as its button: the icon and the words that say it opens the controls. */
|
|
364
|
+
div[clips][folded] {
|
|
365
|
+
display: flex;
|
|
366
|
+
flex-direction: column;
|
|
367
|
+
align-items: stretch;
|
|
368
|
+
gap: 6px;
|
|
369
|
+
}
|
|
370
|
+
div[clips][folded] button[fold-controls] {
|
|
371
|
+
gap: 4px;
|
|
372
|
+
padding: 2px 4px;
|
|
373
|
+
font-size: var(--ui-t-micro, 11px);
|
|
374
|
+
}
|
|
375
|
+
/* An icon as tall as the two buttons beside it, at the line's right end. */
|
|
376
|
+
div[clips] button[fold-controls] {
|
|
377
|
+
display: inline-flex;
|
|
378
|
+
align-items: center;
|
|
379
|
+
}
|
|
380
|
+
div[clips] button[fold-controls] md-icon {
|
|
381
|
+
--md-icon-size: 16px;
|
|
382
|
+
}
|
|
383
|
+
div[clips] div[actions] button[fold-controls] {
|
|
384
|
+
margin-left: auto;
|
|
385
|
+
padding: 2px 3px;
|
|
386
|
+
border: 1px solid transparent;
|
|
387
|
+
}
|
|
388
|
+
/* On one line with the fold, each word on one line: a label broken inside a word ("기본으 / 로") read badly (user 2026-11-12). */
|
|
389
|
+
div[clips] button[pose],
|
|
390
|
+
div[clips] button[clear-tried] {
|
|
391
|
+
flex: none;
|
|
392
|
+
white-space: nowrap;
|
|
393
|
+
padding: 3px 8px;
|
|
306
394
|
font: inherit;
|
|
307
395
|
font-size: var(--ui-t-micro, 11px);
|
|
308
396
|
color: rgba(255, 255, 255, 0.92);
|
|
@@ -311,7 +399,13 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
311
399
|
border-radius: 5px;
|
|
312
400
|
cursor: pointer;
|
|
313
401
|
}
|
|
314
|
-
div[clips] button[pose]:
|
|
402
|
+
div[clips] button[pose]:disabled,
|
|
403
|
+
div[clips] button[clear-tried]:disabled {
|
|
404
|
+
opacity: 0.4;
|
|
405
|
+
cursor: default;
|
|
406
|
+
}
|
|
407
|
+
div[clips] button[pose]:not(:disabled):hover,
|
|
408
|
+
div[clips] button[clear-tried]:not(:disabled):hover {
|
|
315
409
|
background-color: rgba(255, 255, 255, 0.24);
|
|
316
410
|
}
|
|
317
411
|
div[clips] span[drive] {
|
|
@@ -323,7 +417,20 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
323
417
|
accent-color: var(--md-sys-color-primary);
|
|
324
418
|
}
|
|
325
419
|
/* A value typed, not slid: in the look of the group's clip choice, starting where the sliders start. */
|
|
326
|
-
|
|
420
|
+
/* A name that scrubs its value, in the look the side panel gives it (figure-viewer side-panel-styles \`[scrub]\`). */
|
|
421
|
+
div[clips] label > span[scrub] {
|
|
422
|
+
cursor: ew-resize;
|
|
423
|
+
user-select: none;
|
|
424
|
+
touch-action: none;
|
|
425
|
+
}
|
|
426
|
+
/* A check box in the dial's column, where every field starts. */
|
|
427
|
+
div[clips] input[type='checkbox'] {
|
|
428
|
+
grid-column: 2 / -1;
|
|
429
|
+
justify-self: start;
|
|
430
|
+
width: auto;
|
|
431
|
+
margin: 0;
|
|
432
|
+
}
|
|
433
|
+
div[clips] input:not([type='range']):not([type='checkbox']) {
|
|
327
434
|
box-sizing: border-box;
|
|
328
435
|
min-width: 0;
|
|
329
436
|
font: inherit;
|
|
@@ -332,9 +439,15 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
332
439
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
333
440
|
border-radius: 5px;
|
|
334
441
|
}
|
|
335
|
-
|
|
336
|
-
|
|
442
|
+
/*
|
|
443
|
+
Three widths only: a slider, its value's box, and a field that is the whole row's -- a choice, a text, a number with
|
|
444
|
+
no range -- from where the dial starts to the right edge, as the choices are (user 2026-11-12: every width differed).
|
|
445
|
+
*/
|
|
446
|
+
div[clips] input:not([type='range']):not([type='number']):not([type='checkbox']),
|
|
447
|
+
div[clips] input[wide] {
|
|
448
|
+
grid-column: 2 / -1;
|
|
337
449
|
width: auto;
|
|
450
|
+
padding: 1px 4px;
|
|
338
451
|
}
|
|
339
452
|
/* A control's value, typed where the slider cannot land on it (a step of the range's hundredth, robot dog 2026-11-03). */
|
|
340
453
|
div[clips] input[typed] {
|
|
@@ -344,7 +457,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
344
457
|
font-variant-numeric: tabular-nums;
|
|
345
458
|
}
|
|
346
459
|
div[clips] label[sampled] output {
|
|
347
|
-
grid-column:
|
|
460
|
+
grid-column: 2 / -1;
|
|
348
461
|
text-align: left;
|
|
349
462
|
}
|
|
350
463
|
div[clips] output {
|
|
@@ -398,7 +511,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
398
511
|
${this.tags.length ? html `<div tags>${this.tags.map(t => html `<span style=${`left:${t.x}px;top:${t.y}px`}>${t.name}</span>`)}</div>` : nothing}
|
|
399
512
|
${this.cannot
|
|
400
513
|
? html `<div cannot>${this.cannot}</div>`
|
|
401
|
-
: html `<div legend>
|
|
514
|
+
: html `<div over><div legend>
|
|
402
515
|
${['one', 'eight'].map(way => html `<button way=${way} aria-pressed=${String(this.way === way)} @click=${() => this.chooseWay(way)}>${i18next.t(`figure.label.preview-way-${way}`)}</button>`)}
|
|
403
516
|
<span views>
|
|
404
517
|
${PREVIEW_VIEWS.map(view => html `<button view=${view} aria-pressed=${String(this.looking === view)} @click=${() => this.lookFrom(view)}>${i18next.t(`figure.label.preview-view-${view}`)}</button>`)}
|
|
@@ -412,16 +525,29 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
412
525
|
: nothing}
|
|
413
526
|
</span>`;
|
|
414
527
|
})}
|
|
528
|
+
${this.way === 'one' && asset && this.trying
|
|
529
|
+
? html `<span said>${previewSizeWordsOf(asset, this.trying, (key, params) => i18next.t(key, params))}</span>`
|
|
530
|
+
: nothing}
|
|
415
531
|
</div>
|
|
416
532
|
${this.way === 'one' && asset && this.trying ? this.renderSizes(asset, this.trying) : nothing}
|
|
417
|
-
<div foot>${asset?.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
533
|
+
<div foot>${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0)
|
|
534
|
+
? this.renderDrivers(asset, controls, clips, this.renderSetup(asset, setupPart))
|
|
535
|
+
: this.renderSetup(asset, setupPart)}</div></div>`}`;
|
|
536
|
+
}
|
|
537
|
+
/*
|
|
538
|
+
The size help, shown only where the figure's size can change: the part and the button on one line, its words in its
|
|
539
|
+
title. With controls it is their last row, as wide as they are -- a box of its own left a gap at its right when the
|
|
540
|
+
two stood one over the other, and three lines of it covered the figure (user 2026-11-12).
|
|
541
|
+
*/
|
|
542
|
+
renderSetup(asset, setupPart) {
|
|
543
|
+
if (!asset?.appearance.length || !['x', 'y', 'z'].some(axis => sizeSpanOf(asset, axis)))
|
|
544
|
+
return nothing;
|
|
545
|
+
return html `<div setup title=${i18next.t('figure.text.preview-setup')}>
|
|
546
|
+
<select aria-label=${i18next.t('figure.label.resize-part')} @change=${(e) => (this.setupPart = e.target.value)}>
|
|
547
|
+
${asset.appearance.map(p => html `<option value=${p.target} ?selected=${p.target === setupPart}>${p.target}</option>`)}
|
|
548
|
+
</select>
|
|
549
|
+
<button @click=${() => this.dispatchEvent(new CustomEvent('configure-size', { detail: { part: setupPart }, bubbles: true, composed: true }))}>${i18next.t('figure.button.configure-size')}</button>
|
|
550
|
+
</div>`;
|
|
425
551
|
}
|
|
426
552
|
lookFrom(view) {
|
|
427
553
|
this.scene?.root?.setCameraView?.(view);
|
|
@@ -429,7 +555,8 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
429
555
|
}
|
|
430
556
|
/** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
|
|
431
557
|
renderSizes(asset, size) {
|
|
432
|
-
|
|
558
|
+
if (!['x', 'y', 'z'].some(axis => sizeSpanOf(asset, axis)))
|
|
559
|
+
return nothing;
|
|
433
560
|
return html `<div sizes>
|
|
434
561
|
${['x', 'y', 'z'].map(axis => {
|
|
435
562
|
const range = sizeSpanOf(asset, axis);
|
|
@@ -443,7 +570,6 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
443
570
|
<output>${Math.round(size[axis]).toLocaleString()}</output>
|
|
444
571
|
</label>`;
|
|
445
572
|
})}
|
|
446
|
-
<p said>${previewSizeWordsOf(asset, size, say)}</p>
|
|
447
573
|
</div>`;
|
|
448
574
|
}
|
|
449
575
|
/*
|
|
@@ -518,28 +644,38 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
518
644
|
this.scene?.fit?.('ratio');
|
|
519
645
|
this.scene?.root?.fitCameraToModel?.();
|
|
520
646
|
}
|
|
521
|
-
renderDrivers(asset, controls, clips) {
|
|
647
|
+
renderDrivers(asset, controls, clips, setup) {
|
|
522
648
|
const steps = new Map(controlsOf(asset).map(c => [c.id, c.step]));
|
|
523
649
|
const sampledStates = sampledStatesOf(asset, this.sample);
|
|
524
650
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id]);
|
|
525
|
-
|
|
526
|
-
|
|
651
|
+
/* An icon, its words in its title: the words took the room of a button (user 2026-11-12). */
|
|
652
|
+
const foldWords = i18next.t(this.controlsFolded ? 'figure.button.controls-unfold' : 'figure.button.controls-fold');
|
|
653
|
+
const fold = html `<button fold-controls aria-expanded=${String(!this.controlsFolded)} title=${foldWords} aria-label=${foldWords} @click=${() => (this.controlsFolded = !this.controlsFolded)}>
|
|
654
|
+
<md-icon>${this.controlsFolded ? 'unfold_more' : 'unfold_less'}</md-icon>
|
|
527
655
|
</button>`;
|
|
656
|
+
/* Folded, the icon says what it opens: an icon alone in an empty box said nothing (user 2026-11-12). */
|
|
528
657
|
if (this.controlsFolded)
|
|
529
|
-
return html `<div clips folded
|
|
658
|
+
return html `<div clips folded>
|
|
659
|
+
<button fold-controls aria-expanded="false" @click=${() => (this.controlsFolded = false)}>
|
|
660
|
+
<md-icon>unfold_more</md-icon>${foldWords}
|
|
661
|
+
</button>
|
|
662
|
+
${setup}
|
|
663
|
+
</div>`;
|
|
530
664
|
const played = playedControlsOf(asset, this.driving, this.moving);
|
|
665
|
+
/*
|
|
666
|
+
Keep, clear and fold on one line, there from the start: keep and clear unable until something is tried. Coming and
|
|
667
|
+
going, they moved the rows under them while a slider was being moved (user 2026-11-12).
|
|
668
|
+
*/
|
|
531
669
|
return html `<div clips>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
</button>`
|
|
542
|
-
: nothing}
|
|
670
|
+
<div actions>
|
|
671
|
+
<button pose ?disabled=${!changed.length} title=${i18next.t('figure.text.pose-as-default-explained')} @click=${() => this.poseAsDefault()}>
|
|
672
|
+
${i18next.t('figure.button.pose-as-default')}
|
|
673
|
+
</button>
|
|
674
|
+
<button clear-tried ?disabled=${!Object.keys(this.giving).length} title=${i18next.t('figure.text.clear-tried-explained')} @click=${() => this.clearTried()}>
|
|
675
|
+
${i18next.t('figure.button.clear-tried')}
|
|
676
|
+
</button>
|
|
677
|
+
${fold}
|
|
678
|
+
</div>
|
|
543
679
|
${controls.map(control => {
|
|
544
680
|
/* A control the data tab's sample sets is what the figure shows, as a value row the sample gives: said, not offered. */
|
|
545
681
|
const fromSample = sampledStates[control.id];
|
|
@@ -565,7 +701,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
565
701
|
<output>${i18next.t('figure.text.control-played', { clip: by })}</output>
|
|
566
702
|
</label>`;
|
|
567
703
|
return html `<label>
|
|
568
|
-
|
|
704
|
+
${this.scrubName(html `${label} <span drive>${unitWord(control.unit)}</span>`, control.unit, value, control.min, control.max, v => this.give(control.id, v))}
|
|
569
705
|
${this.renderDial(control, value)}
|
|
570
706
|
<input
|
|
571
707
|
type="range"
|
|
@@ -575,20 +711,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
575
711
|
.value=${String(value)}
|
|
576
712
|
@input=${(e) => this.give(control.id, Number(e.target.value))}
|
|
577
713
|
/>
|
|
578
|
-
|
|
579
|
-
typed
|
|
580
|
-
type="number"
|
|
581
|
-
step="any"
|
|
582
|
-
min=${control.min}
|
|
583
|
-
max=${control.max}
|
|
584
|
-
aria-label=${label}
|
|
585
|
-
.value=${String(Math.round(value * 100) / 100)}
|
|
586
|
-
@change=${(e) => {
|
|
587
|
-
const typed = e.target.valueAsNumber;
|
|
588
|
-
if (Number.isFinite(typed))
|
|
589
|
-
this.give(control.id, Math.min(Math.max(typed, control.min), control.max));
|
|
590
|
-
}}
|
|
591
|
-
/>
|
|
714
|
+
${this.typedBox(label, value, control.min, control.max, typed => this.give(control.id, typed))}
|
|
592
715
|
</label>`;
|
|
593
716
|
})}
|
|
594
717
|
${clips.map(clip => {
|
|
@@ -598,10 +721,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
598
721
|
*/
|
|
599
722
|
const value = this.driving[clip] ?? 0;
|
|
600
723
|
return html `<label>
|
|
601
|
-
|
|
724
|
+
${this.scrubName(html `${clip} <span drive>${i18next.t('figure.label.playback-speed')}</span>`, 'ratio', value, 0, 3, v => this.drive(clip, v))}
|
|
602
725
|
<span></span>
|
|
603
726
|
<input type="range" min="0" max="3" step="0.05" .value=${String(value)} @input=${(e) => this.drive(clip, Number(e.target.value))} />
|
|
604
|
-
|
|
727
|
+
${this.typedBox(clip, value, 0, 3, typed => this.drive(clip, typed))}
|
|
605
728
|
</label>`;
|
|
606
729
|
})}
|
|
607
730
|
${valueRowsOf(asset).map(row => this.renderDataValue(row))}
|
|
@@ -620,14 +743,56 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
620
743
|
${byData.speed
|
|
621
744
|
? nothing
|
|
622
745
|
: html `<label>
|
|
623
|
-
|
|
746
|
+
${this.scrubName(html `${group.label} <span drive>${i18next.t('figure.label.playback-speed')}</span>`, 'ratio', playing.speed, 0, 3, v => this.move(group.id, { ...playing, speed: v }))}
|
|
624
747
|
<span></span>
|
|
625
748
|
<input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e) => this.move(group.id, { ...playing, speed: Number(e.target.value) })} />
|
|
626
|
-
|
|
749
|
+
${this.typedBox(group.label, playing.speed, 0, 3, typed => this.move(group.id, { ...playing, speed: typed }))}
|
|
627
750
|
</label>`}`;
|
|
628
751
|
})}
|
|
752
|
+
${setup}
|
|
629
753
|
</div>`;
|
|
630
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* A row's name that scrubs its value when dragged sideways, as the property panel's names do (\`scrubLabel\`): Shift for
|
|
757
|
+
* finer steps, held within the row's range. The preview's values are tried, not recorded, so each move is the value.
|
|
758
|
+
* The sliders ran 360 degrees over 96 pixels, too coarse to land an angle (user 2026-11-12).
|
|
759
|
+
*/
|
|
760
|
+
scrubName(name, unit, value, min, max, set) {
|
|
761
|
+
const held = (v) => Math.min(Math.max(v, min), max);
|
|
762
|
+
return html `<span
|
|
763
|
+
scrub
|
|
764
|
+
@pointerdown=${(down) => {
|
|
765
|
+
/* A press that did not move gives nothing: an empty field is not given its slider's start by a click on its name. */
|
|
766
|
+
let moved = false;
|
|
767
|
+
scrubLabel(down, { value, unit }, {
|
|
768
|
+
move: v => ((moved = true), set(held(v))),
|
|
769
|
+
release: v => set(held(v)),
|
|
770
|
+
cancel: () => moved && set(value)
|
|
771
|
+
});
|
|
772
|
+
}}
|
|
773
|
+
>${name}</span
|
|
774
|
+
>`;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* A slider's value as a box to type in, the same on every row: the value a slider cannot land on, typed (a step of the
|
|
778
|
+
* range's hundredth, robot dog 2026-11-03); one column of boxes, not boxes over plain numbers (user 2026-11-12).
|
|
779
|
+
*/
|
|
780
|
+
typedBox(label, value, min, max, set) {
|
|
781
|
+
return html `<input
|
|
782
|
+
typed
|
|
783
|
+
type="number"
|
|
784
|
+
step="any"
|
|
785
|
+
min=${Number.isFinite(min) ? min : nothing}
|
|
786
|
+
max=${Number.isFinite(max) ? max : nothing}
|
|
787
|
+
aria-label=${label}
|
|
788
|
+
.value=${String(Math.round(value * 100) / 100)}
|
|
789
|
+
@change=${(e) => {
|
|
790
|
+
const typed = e.target.valueAsNumber;
|
|
791
|
+
if (Number.isFinite(typed))
|
|
792
|
+
set(Math.min(Math.max(typed, min), max));
|
|
793
|
+
}}
|
|
794
|
+
/>`;
|
|
795
|
+
}
|
|
631
796
|
/** A value the data's bindings read, as the board's saved values give it: a number on a slider when it has a range, a choice when it is closed. */
|
|
632
797
|
renderDataValue(row) {
|
|
633
798
|
const node = row.node;
|
|
@@ -640,22 +805,37 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
640
805
|
if (sampled !== undefined)
|
|
641
806
|
return html `<label sampled title=${i18next.t('figure.text.data-value-from-sample')}>
|
|
642
807
|
<span>${label} <span drive>${i18next.t('figure.label.data-value-from-sample')}</span></span>
|
|
643
|
-
<output>${typeof sampled === 'object' ? JSON.stringify(sampled) : String(sampled)}</output>
|
|
808
|
+
<output>${typeof sampled === 'boolean' ? i18next.t(sampled ? 'figure.label.data-true' : 'figure.label.data-false') : typeof sampled === 'object' ? JSON.stringify(sampled) : String(sampled)}</output>
|
|
644
809
|
</label>`;
|
|
645
810
|
const fallback = 'default' in node ? node.default : undefined;
|
|
646
811
|
const value = this.valuing[row.path] ?? fallback;
|
|
647
812
|
if (node.kind === 'number') {
|
|
648
813
|
const ranged = node.min !== undefined && node.max !== undefined && node.max > node.min;
|
|
814
|
+
/*
|
|
815
|
+
An angle is drawn as the controls' angles are -- dial, slider, box -- whether or not its field declares a range:
|
|
816
|
+
one with none slides over one turn, and any angle can still be typed. 「방향 deg」 was a bare number field under
|
|
817
|
+
「머리 각도 deg」 on a slider (user 2026-11-12).
|
|
818
|
+
*/
|
|
819
|
+
const angle = node.unit === 'deg';
|
|
820
|
+
const slide = ranged ? { min: node.min, max: node.max } : angle ? { min: 0, max: 360 } : undefined;
|
|
821
|
+
const scrubFrom = Number(value ?? slide?.min ?? 0);
|
|
649
822
|
return html `<label>
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
823
|
+
${this.scrubName(html `${label} <span drive>${unitWord(node.unit)}</span>`, node.unit ?? '', Number.isFinite(scrubFrom) ? scrubFrom : 0, ranged ? node.min : -Infinity, ranged ? node.max : Infinity, v => this.value(row, String(v)))}
|
|
824
|
+
${slide
|
|
825
|
+
? html `${this.renderDial({ unit: node.unit ?? '', ...slide }, Number(value ?? slide.min))}
|
|
826
|
+
<input type="range" min=${slide.min} max=${slide.max} step=${angle ? 1 : Math.max((slide.max - slide.min) / 100, 0.01)} .value=${String(value ?? slide.min)} @input=${(e) => this.value(row, e.target.value)} />
|
|
827
|
+
${ranged
|
|
828
|
+
? this.typedBox(label, Number(value ?? slide.min), slide.min, slide.max, typed => this.value(row, String(typed)))
|
|
829
|
+
: this.typedBox(label, Number(value ?? slide.min), -Infinity, Infinity, typed => this.value(row, String(typed)))}`
|
|
830
|
+
: html `<input wide type="number" step="any" aria-label=${label} .value=${value === undefined ? '' : String(value)} @change=${(e) => this.value(row, e.target.value)} />`}
|
|
657
831
|
</label>`;
|
|
658
832
|
}
|
|
833
|
+
/* A yes or no is a check box, not a list of two words to open (user 2026-11-12). Unchecked is false, as the board reads it. */
|
|
834
|
+
if (node.kind === 'boolean')
|
|
835
|
+
return html `<label>
|
|
836
|
+
<span>${label}</span>
|
|
837
|
+
<input type="checkbox" aria-label=${label} .checked=${value === true} @change=${(e) => this.value(row, String(e.target.checked))} />
|
|
838
|
+
</label>`;
|
|
659
839
|
const domain = domainOf(node);
|
|
660
840
|
if (!domain.length)
|
|
661
841
|
return html `<label>
|