@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,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';
|
|
@@ -14,6 +16,7 @@ import { dataValueAt, domainOf, motionWrittenByData, sampledStatesOf, typedValue
|
|
|
14
16
|
import { derivedOf } from './revision.js';
|
|
15
17
|
import { authoredSizeOf, bodyAtSize, PREVIEW_FACTOR, PREVIEW_WAYS, previewClampsOf, previewInstances, previewOneInstance, previewTypeOf, setupPartOf, sizeSpanOf, sizeWithinRange } from '@things-factory/figure-viewer/dist-client/preview-model.js';
|
|
16
18
|
import { previewSizeWordsOf } from './preview-size.js';
|
|
19
|
+
import { playedControlsOf } from './clips.js';
|
|
17
20
|
const PREVIEW_VIEWS = ['perspective', 'front', 'right', 'top'];
|
|
18
21
|
let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
19
22
|
constructor() {
|
|
@@ -92,21 +95,43 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
92
95
|
background-color: rgba(28, 28, 30, 0.72);
|
|
93
96
|
border-radius: 4px;
|
|
94
97
|
}
|
|
95
|
-
|
|
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] {
|
|
96
104
|
position: absolute;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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;
|
|
100
120
|
display: flex;
|
|
101
121
|
flex-wrap: wrap-reverse;
|
|
102
122
|
justify-content: space-between;
|
|
103
123
|
align-items: flex-end;
|
|
124
|
+
align-content: flex-start;
|
|
104
125
|
gap: 8px;
|
|
105
126
|
pointer-events: none;
|
|
106
127
|
}
|
|
107
128
|
div[foot] > * { pointer-events: auto; }
|
|
108
|
-
|
|
109
|
-
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; }
|
|
110
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). */
|
|
111
136
|
div[setup] select,
|
|
112
137
|
div[setup] button {
|
|
@@ -129,9 +154,8 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
129
154
|
}
|
|
130
155
|
/* Which is which: side by side means nothing without a legend. */
|
|
131
156
|
div[legend] {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
left: 8px;
|
|
157
|
+
max-width: 100%;
|
|
158
|
+
box-sizing: border-box;
|
|
135
159
|
display: flex;
|
|
136
160
|
gap: 6px;
|
|
137
161
|
padding: 4px 8px;
|
|
@@ -176,13 +200,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
176
200
|
}
|
|
177
201
|
/* The one figure's size: under the legend, in the controls' look. */
|
|
178
202
|
div[sizes] {
|
|
179
|
-
position: absolute;
|
|
180
|
-
top: 40px;
|
|
181
|
-
left: 8px;
|
|
182
203
|
display: grid;
|
|
183
204
|
gap: 6px;
|
|
184
205
|
padding: 8px 10px;
|
|
185
|
-
max-width:
|
|
206
|
+
max-width: 100%;
|
|
186
207
|
box-sizing: border-box;
|
|
187
208
|
color: rgba(255, 255, 255, 0.92);
|
|
188
209
|
background-color: rgba(28, 28, 30, 0.72);
|
|
@@ -203,8 +224,12 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
203
224
|
font-variant-numeric: tabular-nums;
|
|
204
225
|
text-align: right;
|
|
205
226
|
}
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
233
|
font-variant-numeric: tabular-nums;
|
|
209
234
|
font-weight: 600;
|
|
210
235
|
}
|
|
@@ -219,8 +244,12 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
219
244
|
*/
|
|
220
245
|
div[clips] {
|
|
221
246
|
margin-left: auto;
|
|
247
|
+
max-height: 100%;
|
|
248
|
+
box-sizing: border-box;
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
overscroll-behavior: contain;
|
|
222
251
|
display: grid;
|
|
223
|
-
grid-template-columns:
|
|
252
|
+
grid-template-columns: max-content 20px 96px 44px;
|
|
224
253
|
gap: 6px 8px;
|
|
225
254
|
align-items: center;
|
|
226
255
|
padding: 8px 10px;
|
|
@@ -237,6 +266,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
237
266
|
grid-template-columns: subgrid;
|
|
238
267
|
align-items: center;
|
|
239
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
|
+
}
|
|
240
273
|
/* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
|
|
241
274
|
div[clips] select {
|
|
242
275
|
grid-column: 2 / -1;
|
|
@@ -247,6 +280,28 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
247
280
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
248
281
|
border-radius: 5px;
|
|
249
282
|
}
|
|
283
|
+
/* A motion group's clips as buttons: a list the system draws is not seen in a recording of the preview. */
|
|
284
|
+
div[clips] span[clip-choices] {
|
|
285
|
+
grid-column: 2 / -1;
|
|
286
|
+
display: flex;
|
|
287
|
+
flex-wrap: wrap;
|
|
288
|
+
gap: 4px;
|
|
289
|
+
min-width: 0;
|
|
290
|
+
}
|
|
291
|
+
div[clips] span[clip-choices] button {
|
|
292
|
+
font: inherit;
|
|
293
|
+
color: inherit;
|
|
294
|
+
padding: 1px 8px;
|
|
295
|
+
background-color: rgba(255, 255, 255, 0.08);
|
|
296
|
+
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
297
|
+
border-radius: 10px;
|
|
298
|
+
cursor: pointer;
|
|
299
|
+
}
|
|
300
|
+
div[clips] span[clip-choices] button[aria-pressed='true'] {
|
|
301
|
+
background-color: rgba(255, 255, 255, 0.3);
|
|
302
|
+
border-color: rgba(255, 255, 255, 0.6);
|
|
303
|
+
font-weight: 600;
|
|
304
|
+
}
|
|
250
305
|
div[clips] svg[dial] {
|
|
251
306
|
width: 20px;
|
|
252
307
|
height: 20px;
|
|
@@ -277,9 +332,65 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
277
332
|
div[clips] button[fold-controls]:hover {
|
|
278
333
|
opacity: 1;
|
|
279
334
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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;
|
|
283
394
|
font: inherit;
|
|
284
395
|
font-size: var(--ui-t-micro, 11px);
|
|
285
396
|
color: rgba(255, 255, 255, 0.92);
|
|
@@ -288,7 +399,13 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
288
399
|
border-radius: 5px;
|
|
289
400
|
cursor: pointer;
|
|
290
401
|
}
|
|
291
|
-
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 {
|
|
292
409
|
background-color: rgba(255, 255, 255, 0.24);
|
|
293
410
|
}
|
|
294
411
|
div[clips] span[drive] {
|
|
@@ -300,7 +417,20 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
300
417
|
accent-color: var(--md-sys-color-primary);
|
|
301
418
|
}
|
|
302
419
|
/* A value typed, not slid: in the look of the group's clip choice, starting where the sliders start. */
|
|
303
|
-
|
|
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']) {
|
|
304
434
|
box-sizing: border-box;
|
|
305
435
|
min-width: 0;
|
|
306
436
|
font: inherit;
|
|
@@ -309,9 +439,15 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
309
439
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
310
440
|
border-radius: 5px;
|
|
311
441
|
}
|
|
312
|
-
|
|
313
|
-
|
|
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;
|
|
314
449
|
width: auto;
|
|
450
|
+
padding: 1px 4px;
|
|
315
451
|
}
|
|
316
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). */
|
|
317
453
|
div[clips] input[typed] {
|
|
@@ -321,7 +457,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
321
457
|
font-variant-numeric: tabular-nums;
|
|
322
458
|
}
|
|
323
459
|
div[clips] label[sampled] output {
|
|
324
|
-
grid-column:
|
|
460
|
+
grid-column: 2 / -1;
|
|
325
461
|
text-align: left;
|
|
326
462
|
}
|
|
327
463
|
div[clips] output {
|
|
@@ -375,7 +511,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
375
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}
|
|
376
512
|
${this.cannot
|
|
377
513
|
? html `<div cannot>${this.cannot}</div>`
|
|
378
|
-
: html `<div legend>
|
|
514
|
+
: html `<div over><div legend>
|
|
379
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>`)}
|
|
380
516
|
<span views>
|
|
381
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>`)}
|
|
@@ -389,16 +525,29 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
389
525
|
: nothing}
|
|
390
526
|
</span>`;
|
|
391
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}
|
|
392
531
|
</div>
|
|
393
532
|
${this.way === 'one' && asset && this.trying ? this.renderSizes(asset, this.trying) : nothing}
|
|
394
|
-
<div foot>${asset?.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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>`;
|
|
402
551
|
}
|
|
403
552
|
lookFrom(view) {
|
|
404
553
|
this.scene?.root?.setCameraView?.(view);
|
|
@@ -406,7 +555,8 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
406
555
|
}
|
|
407
556
|
/** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
|
|
408
557
|
renderSizes(asset, size) {
|
|
409
|
-
|
|
558
|
+
if (!['x', 'y', 'z'].some(axis => sizeSpanOf(asset, axis)))
|
|
559
|
+
return nothing;
|
|
410
560
|
return html `<div sizes>
|
|
411
561
|
${['x', 'y', 'z'].map(axis => {
|
|
412
562
|
const range = sizeSpanOf(asset, axis);
|
|
@@ -420,7 +570,6 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
420
570
|
<output>${Math.round(size[axis]).toLocaleString()}</output>
|
|
421
571
|
</label>`;
|
|
422
572
|
})}
|
|
423
|
-
<p said>${previewSizeWordsOf(asset, size, say)}</p>
|
|
424
573
|
</div>`;
|
|
425
574
|
}
|
|
426
575
|
/*
|
|
@@ -495,27 +644,38 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
495
644
|
this.scene?.fit?.('ratio');
|
|
496
645
|
this.scene?.root?.fitCameraToModel?.();
|
|
497
646
|
}
|
|
498
|
-
renderDrivers(asset, controls, clips) {
|
|
647
|
+
renderDrivers(asset, controls, clips, setup) {
|
|
499
648
|
const steps = new Map(controlsOf(asset).map(c => [c.id, c.step]));
|
|
500
649
|
const sampledStates = sampledStatesOf(asset, this.sample);
|
|
501
650
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id]);
|
|
502
|
-
|
|
503
|
-
|
|
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>
|
|
504
655
|
</button>`;
|
|
656
|
+
/* Folded, the icon says what it opens: an icon alone in an empty box said nothing (user 2026-11-12). */
|
|
505
657
|
if (this.controlsFolded)
|
|
506
|
-
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>`;
|
|
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
|
+
*/
|
|
507
669
|
return html `<div clips>
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
</button>`
|
|
518
|
-
: 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>
|
|
519
679
|
${controls.map(control => {
|
|
520
680
|
/* A control the data tab's sample sets is what the figure shows, as a value row the sample gives: said, not offered. */
|
|
521
681
|
const fromSample = sampledStates[control.id];
|
|
@@ -528,8 +688,20 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
528
688
|
/* The step the editor's controls take (\`controlsOf\`): a degree for an angle, a hundredth of the range otherwise. */
|
|
529
689
|
const step = steps.get(control.id) ?? (control.max - control.min) / 100;
|
|
530
690
|
const label = asset.stateInputs?.[control.id]?.label ?? control.id;
|
|
691
|
+
/*
|
|
692
|
+
A control a playing clip drives moves on the figure while its row stood still at its last number -- 0 under a
|
|
693
|
+
walking leg (review 2026-11-11). The row says the clip has it, and is not given a value until the clip stops.
|
|
694
|
+
*/
|
|
695
|
+
const by = played.get(control.id);
|
|
696
|
+
if (by)
|
|
697
|
+
return html `<label played title=${i18next.t('figure.text.control-played-explained', { clip: by })}>
|
|
698
|
+
<span>${label} <span drive>${unitWord(control.unit)}</span></span>
|
|
699
|
+
<span></span>
|
|
700
|
+
<input type="range" min=${control.min} max=${control.max} .value=${String(value)} disabled />
|
|
701
|
+
<output>${i18next.t('figure.text.control-played', { clip: by })}</output>
|
|
702
|
+
</label>`;
|
|
531
703
|
return html `<label>
|
|
532
|
-
|
|
704
|
+
${this.scrubName(html `${label} <span drive>${unitWord(control.unit)}</span>`, control.unit, value, control.min, control.max, v => this.give(control.id, v))}
|
|
533
705
|
${this.renderDial(control, value)}
|
|
534
706
|
<input
|
|
535
707
|
type="range"
|
|
@@ -539,20 +711,7 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
539
711
|
.value=${String(value)}
|
|
540
712
|
@input=${(e) => this.give(control.id, Number(e.target.value))}
|
|
541
713
|
/>
|
|
542
|
-
|
|
543
|
-
typed
|
|
544
|
-
type="number"
|
|
545
|
-
step="any"
|
|
546
|
-
min=${control.min}
|
|
547
|
-
max=${control.max}
|
|
548
|
-
aria-label=${label}
|
|
549
|
-
.value=${String(Math.round(value * 100) / 100)}
|
|
550
|
-
@change=${(e) => {
|
|
551
|
-
const typed = e.target.valueAsNumber;
|
|
552
|
-
if (Number.isFinite(typed))
|
|
553
|
-
this.give(control.id, Math.min(Math.max(typed, control.min), control.max));
|
|
554
|
-
}}
|
|
555
|
-
/>
|
|
714
|
+
${this.typedBox(label, value, control.min, control.max, typed => this.give(control.id, typed))}
|
|
556
715
|
</label>`;
|
|
557
716
|
})}
|
|
558
717
|
${clips.map(clip => {
|
|
@@ -562,10 +721,10 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
562
721
|
*/
|
|
563
722
|
const value = this.driving[clip] ?? 0;
|
|
564
723
|
return html `<label>
|
|
565
|
-
|
|
724
|
+
${this.scrubName(html `${clip} <span drive>${i18next.t('figure.label.playback-speed')}</span>`, 'ratio', value, 0, 3, v => this.drive(clip, v))}
|
|
566
725
|
<span></span>
|
|
567
726
|
<input type="range" min="0" max="3" step="0.05" .value=${String(value)} @input=${(e) => this.drive(clip, Number(e.target.value))} />
|
|
568
|
-
|
|
727
|
+
${this.typedBox(clip, value, 0, 3, typed => this.drive(clip, typed))}
|
|
569
728
|
</label>`;
|
|
570
729
|
})}
|
|
571
730
|
${valueRowsOf(asset).map(row => this.renderDataValue(row))}
|
|
@@ -577,21 +736,63 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
577
736
|
? nothing
|
|
578
737
|
: html `<label>
|
|
579
738
|
<span>${group.label} <span drive>${i18next.t('figure.label.clip')}</span></span>
|
|
580
|
-
<
|
|
581
|
-
${group.clips.map(c => html `<
|
|
582
|
-
</
|
|
739
|
+
<span clip-choices role="group" aria-label=${group.label}>
|
|
740
|
+
${group.clips.map(c => html `<button type="button" aria-pressed=${String(c === playing.clip)} @click=${() => this.move(group.id, { ...playing, clip: c })}>${c}</button>`)}
|
|
741
|
+
</span>
|
|
583
742
|
</label>`}
|
|
584
743
|
${byData.speed
|
|
585
744
|
? nothing
|
|
586
745
|
: html `<label>
|
|
587
|
-
|
|
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 }))}
|
|
588
747
|
<span></span>
|
|
589
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) })} />
|
|
590
|
-
|
|
749
|
+
${this.typedBox(group.label, playing.speed, 0, 3, typed => this.move(group.id, { ...playing, speed: typed }))}
|
|
591
750
|
</label>`}`;
|
|
592
751
|
})}
|
|
752
|
+
${setup}
|
|
593
753
|
</div>`;
|
|
594
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
|
+
}
|
|
595
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. */
|
|
596
797
|
renderDataValue(row) {
|
|
597
798
|
const node = row.node;
|
|
@@ -604,22 +805,37 @@ let FigurePreview = class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
604
805
|
if (sampled !== undefined)
|
|
605
806
|
return html `<label sampled title=${i18next.t('figure.text.data-value-from-sample')}>
|
|
606
807
|
<span>${label} <span drive>${i18next.t('figure.label.data-value-from-sample')}</span></span>
|
|
607
|
-
<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>
|
|
608
809
|
</label>`;
|
|
609
810
|
const fallback = 'default' in node ? node.default : undefined;
|
|
610
811
|
const value = this.valuing[row.path] ?? fallback;
|
|
611
812
|
if (node.kind === 'number') {
|
|
612
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);
|
|
613
822
|
return html `<label>
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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)} />`}
|
|
621
831
|
</label>`;
|
|
622
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>`;
|
|
623
839
|
const domain = domainOf(node);
|
|
624
840
|
if (!domain.length)
|
|
625
841
|
return html `<label>
|