@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
|
+
import '@material/web/icon/icon.js'
|
|
1
2
|
import { css, html, LitElement, nothing, unsafeCSS } from 'lit'
|
|
2
3
|
import { customElement, property, query, state } from 'lit/decorators.js'
|
|
3
4
|
|
|
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 type { FigureAsset, FigureDataRow, FigureDataValue, FigureGraphStateInput } from '@hatiolab/figure-model'
|
|
@@ -96,21 +98,43 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
96
98
|
background-color: rgba(28, 28, 30, 0.72);
|
|
97
99
|
border-radius: 4px;
|
|
98
100
|
}
|
|
99
|
-
|
|
101
|
+
/*
|
|
102
|
+
The legend, the size sliders and the controls in one column over the stage: the controls take what is left under
|
|
103
|
+
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
|
|
104
|
+
fold buttons with them, and nothing scrolled (2026-11-12); a legend on two lines in a narrow preview is room taken too.
|
|
105
|
+
*/
|
|
106
|
+
div[over] {
|
|
100
107
|
position: absolute;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
inset: 8px;
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: column;
|
|
111
|
+
align-items: flex-start;
|
|
112
|
+
gap: 8px;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
}
|
|
115
|
+
div[over] > div[legend],
|
|
116
|
+
div[over] > div[sizes] {
|
|
117
|
+
pointer-events: auto;
|
|
118
|
+
}
|
|
119
|
+
div[foot] {
|
|
120
|
+
align-self: stretch;
|
|
121
|
+
flex: 1;
|
|
122
|
+
min-height: 0;
|
|
104
123
|
display: flex;
|
|
105
124
|
flex-wrap: wrap-reverse;
|
|
106
125
|
justify-content: space-between;
|
|
107
126
|
align-items: flex-end;
|
|
127
|
+
align-content: flex-start;
|
|
108
128
|
gap: 8px;
|
|
109
129
|
pointer-events: none;
|
|
110
130
|
}
|
|
111
131
|
div[foot] > * { pointer-events: auto; }
|
|
112
|
-
|
|
113
|
-
div[setup]
|
|
132
|
+
/* One line over the figure -- the part and the button -- its words in its title: three lines covered the figure (user 2026-11-12). */
|
|
133
|
+
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); }
|
|
134
|
+
div[setup] select { flex: 1 1 auto; }
|
|
135
|
+
div[setup] button { flex: none; white-space: nowrap; }
|
|
136
|
+
/* The controls' last row: as wide as they are, a line over it in place of a box of its own. */
|
|
137
|
+
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; }
|
|
114
138
|
/* The help's choice and button in the look of the controls beside it, not the browser's grey (sweep 2026-10-27 U7). */
|
|
115
139
|
div[setup] select,
|
|
116
140
|
div[setup] button {
|
|
@@ -133,9 +157,8 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
133
157
|
}
|
|
134
158
|
/* Which is which: side by side means nothing without a legend. */
|
|
135
159
|
div[legend] {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
left: 8px;
|
|
160
|
+
max-width: 100%;
|
|
161
|
+
box-sizing: border-box;
|
|
139
162
|
display: flex;
|
|
140
163
|
gap: 6px;
|
|
141
164
|
padding: 4px 8px;
|
|
@@ -180,13 +203,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
180
203
|
}
|
|
181
204
|
/* The one figure's size: under the legend, in the controls' look. */
|
|
182
205
|
div[sizes] {
|
|
183
|
-
position: absolute;
|
|
184
|
-
top: 40px;
|
|
185
|
-
left: 8px;
|
|
186
206
|
display: grid;
|
|
187
207
|
gap: 6px;
|
|
188
208
|
padding: 8px 10px;
|
|
189
|
-
max-width:
|
|
209
|
+
max-width: 100%;
|
|
190
210
|
box-sizing: border-box;
|
|
191
211
|
color: rgba(255, 255, 255, 0.92);
|
|
192
212
|
background-color: rgba(28, 28, 30, 0.72);
|
|
@@ -207,8 +227,12 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
207
227
|
font-variant-numeric: tabular-nums;
|
|
208
228
|
text-align: right;
|
|
209
229
|
}
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
/* 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). */
|
|
231
|
+
div[legend] span[said] {
|
|
232
|
+
padding-left: 6px;
|
|
233
|
+
border-left: 1px solid rgba(255, 255, 255, 0.22);
|
|
234
|
+
align-self: center;
|
|
235
|
+
white-space: nowrap;
|
|
212
236
|
font-variant-numeric: tabular-nums;
|
|
213
237
|
font-weight: 600;
|
|
214
238
|
}
|
|
@@ -223,8 +247,12 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
223
247
|
*/
|
|
224
248
|
div[clips] {
|
|
225
249
|
margin-left: auto;
|
|
250
|
+
max-height: 100%;
|
|
251
|
+
box-sizing: border-box;
|
|
252
|
+
overflow-y: auto;
|
|
253
|
+
overscroll-behavior: contain;
|
|
226
254
|
display: grid;
|
|
227
|
-
grid-template-columns:
|
|
255
|
+
grid-template-columns: max-content 20px 96px 44px;
|
|
228
256
|
gap: 6px 8px;
|
|
229
257
|
align-items: center;
|
|
230
258
|
padding: 8px 10px;
|
|
@@ -241,6 +269,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
241
269
|
grid-template-columns: subgrid;
|
|
242
270
|
align-items: center;
|
|
243
271
|
}
|
|
272
|
+
/* 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). */
|
|
273
|
+
div[clips] label > span:first-child {
|
|
274
|
+
white-space: nowrap;
|
|
275
|
+
}
|
|
244
276
|
/* A motion group's clip: as wide as the dial, slider and value together, so its name reads. */
|
|
245
277
|
div[clips] select {
|
|
246
278
|
grid-column: 2 / -1;
|
|
@@ -303,9 +335,65 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
303
335
|
div[clips] button[fold-controls]:hover {
|
|
304
336
|
opacity: 1;
|
|
305
337
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
338
|
+
/* The keep, clear and fold line, and the size help's, stay in sight while the rows between them scroll. */
|
|
339
|
+
div[clips] div[actions],
|
|
340
|
+
div[clips] div[setup] {
|
|
341
|
+
position: sticky;
|
|
342
|
+
z-index: 1;
|
|
343
|
+
margin-left: -10px;
|
|
344
|
+
margin-right: -10px;
|
|
345
|
+
padding-left: 10px;
|
|
346
|
+
padding-right: 10px;
|
|
347
|
+
background-color: rgba(28, 28, 30, 0.96);
|
|
348
|
+
}
|
|
349
|
+
div[clips] div[actions] {
|
|
350
|
+
top: -8px;
|
|
351
|
+
margin-top: -8px;
|
|
352
|
+
padding-top: 8px;
|
|
353
|
+
padding-bottom: 4px;
|
|
354
|
+
}
|
|
355
|
+
div[clips] div[setup] {
|
|
356
|
+
bottom: -8px;
|
|
357
|
+
margin-bottom: -8px;
|
|
358
|
+
padding-bottom: 8px;
|
|
359
|
+
}
|
|
360
|
+
div[clips] div[actions] {
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
gap: 6px;
|
|
364
|
+
min-width: 0;
|
|
365
|
+
}
|
|
366
|
+
/* Folded, the box is as wide as its button: the icon and the words that say it opens the controls. */
|
|
367
|
+
div[clips][folded] {
|
|
368
|
+
display: flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
align-items: stretch;
|
|
371
|
+
gap: 6px;
|
|
372
|
+
}
|
|
373
|
+
div[clips][folded] button[fold-controls] {
|
|
374
|
+
gap: 4px;
|
|
375
|
+
padding: 2px 4px;
|
|
376
|
+
font-size: var(--ui-t-micro, 11px);
|
|
377
|
+
}
|
|
378
|
+
/* An icon as tall as the two buttons beside it, at the line's right end. */
|
|
379
|
+
div[clips] button[fold-controls] {
|
|
380
|
+
display: inline-flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
}
|
|
383
|
+
div[clips] button[fold-controls] md-icon {
|
|
384
|
+
--md-icon-size: 16px;
|
|
385
|
+
}
|
|
386
|
+
div[clips] div[actions] button[fold-controls] {
|
|
387
|
+
margin-left: auto;
|
|
388
|
+
padding: 2px 3px;
|
|
389
|
+
border: 1px solid transparent;
|
|
390
|
+
}
|
|
391
|
+
/* On one line with the fold, each word on one line: a label broken inside a word ("기본으 / 로") read badly (user 2026-11-12). */
|
|
392
|
+
div[clips] button[pose],
|
|
393
|
+
div[clips] button[clear-tried] {
|
|
394
|
+
flex: none;
|
|
395
|
+
white-space: nowrap;
|
|
396
|
+
padding: 3px 8px;
|
|
309
397
|
font: inherit;
|
|
310
398
|
font-size: var(--ui-t-micro, 11px);
|
|
311
399
|
color: rgba(255, 255, 255, 0.92);
|
|
@@ -314,7 +402,13 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
314
402
|
border-radius: 5px;
|
|
315
403
|
cursor: pointer;
|
|
316
404
|
}
|
|
317
|
-
div[clips] button[pose]:
|
|
405
|
+
div[clips] button[pose]:disabled,
|
|
406
|
+
div[clips] button[clear-tried]:disabled {
|
|
407
|
+
opacity: 0.4;
|
|
408
|
+
cursor: default;
|
|
409
|
+
}
|
|
410
|
+
div[clips] button[pose]:not(:disabled):hover,
|
|
411
|
+
div[clips] button[clear-tried]:not(:disabled):hover {
|
|
318
412
|
background-color: rgba(255, 255, 255, 0.24);
|
|
319
413
|
}
|
|
320
414
|
div[clips] span[drive] {
|
|
@@ -326,7 +420,20 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
326
420
|
accent-color: var(--md-sys-color-primary);
|
|
327
421
|
}
|
|
328
422
|
/* A value typed, not slid: in the look of the group's clip choice, starting where the sliders start. */
|
|
329
|
-
|
|
423
|
+
/* A name that scrubs its value, in the look the side panel gives it (figure-viewer side-panel-styles \`[scrub]\`). */
|
|
424
|
+
div[clips] label > span[scrub] {
|
|
425
|
+
cursor: ew-resize;
|
|
426
|
+
user-select: none;
|
|
427
|
+
touch-action: none;
|
|
428
|
+
}
|
|
429
|
+
/* A check box in the dial's column, where every field starts. */
|
|
430
|
+
div[clips] input[type='checkbox'] {
|
|
431
|
+
grid-column: 2 / -1;
|
|
432
|
+
justify-self: start;
|
|
433
|
+
width: auto;
|
|
434
|
+
margin: 0;
|
|
435
|
+
}
|
|
436
|
+
div[clips] input:not([type='range']):not([type='checkbox']) {
|
|
330
437
|
box-sizing: border-box;
|
|
331
438
|
min-width: 0;
|
|
332
439
|
font: inherit;
|
|
@@ -335,9 +442,15 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
335
442
|
border: 1px solid rgba(255, 255, 255, 0.22);
|
|
336
443
|
border-radius: 5px;
|
|
337
444
|
}
|
|
338
|
-
|
|
339
|
-
|
|
445
|
+
/*
|
|
446
|
+
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
|
|
447
|
+
no range -- from where the dial starts to the right edge, as the choices are (user 2026-11-12: every width differed).
|
|
448
|
+
*/
|
|
449
|
+
div[clips] input:not([type='range']):not([type='number']):not([type='checkbox']),
|
|
450
|
+
div[clips] input[wide] {
|
|
451
|
+
grid-column: 2 / -1;
|
|
340
452
|
width: auto;
|
|
453
|
+
padding: 1px 4px;
|
|
341
454
|
}
|
|
342
455
|
/* A control's value, typed where the slider cannot land on it (a step of the range's hundredth, robot dog 2026-11-03). */
|
|
343
456
|
div[clips] input[typed] {
|
|
@@ -347,7 +460,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
347
460
|
font-variant-numeric: tabular-nums;
|
|
348
461
|
}
|
|
349
462
|
div[clips] label[sampled] output {
|
|
350
|
-
grid-column:
|
|
463
|
+
grid-column: 2 / -1;
|
|
351
464
|
text-align: left;
|
|
352
465
|
}
|
|
353
466
|
div[clips] output {
|
|
@@ -449,7 +562,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
449
562
|
${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}
|
|
450
563
|
${this.cannot
|
|
451
564
|
? html`<div cannot>${this.cannot}</div>`
|
|
452
|
-
: html`<div legend>
|
|
565
|
+
: html`<div over><div legend>
|
|
453
566
|
${(['one', 'eight'] as const).map(
|
|
454
567
|
way => html`<button way=${way} aria-pressed=${String(this.way === way)} @click=${() => this.chooseWay(way)}>${i18next.t(`figure.label.preview-way-${way}`)}</button>`
|
|
455
568
|
)}
|
|
@@ -467,16 +580,29 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
467
580
|
: nothing}
|
|
468
581
|
</span>`
|
|
469
582
|
})}
|
|
583
|
+
${this.way === 'one' && asset && this.trying
|
|
584
|
+
? html`<span said>${previewSizeWordsOf(asset, this.trying, (key: string, params?: Record<string, string>) => i18next.t(key, params))}</span>`
|
|
585
|
+
: nothing}
|
|
470
586
|
</div>
|
|
471
587
|
${this.way === 'one' && asset && this.trying ? this.renderSizes(asset, this.trying) : nothing}
|
|
472
|
-
<div foot>${asset?.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
588
|
+
<div foot>${controls.length + clips.length + (asset?.motionGroups?.length ?? 0) + (asset ? valueRowsOf(asset).length : 0)
|
|
589
|
+
? this.renderDrivers(asset!, controls, clips, this.renderSetup(asset, setupPart))
|
|
590
|
+
: this.renderSetup(asset, setupPart)}</div></div>`}`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/*
|
|
594
|
+
The size help, shown only where the figure's size can change: the part and the button on one line, its words in its
|
|
595
|
+
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
|
|
596
|
+
two stood one over the other, and three lines of it covered the figure (user 2026-11-12).
|
|
597
|
+
*/
|
|
598
|
+
private renderSetup(asset: FigureAsset | undefined, setupPart: string) {
|
|
599
|
+
if (!asset?.appearance.length || !(['x', 'y', 'z'] as const).some(axis => sizeSpanOf(asset, axis))) return nothing
|
|
600
|
+
return html`<div setup title=${i18next.t('figure.text.preview-setup')}>
|
|
601
|
+
<select aria-label=${i18next.t('figure.label.resize-part')} @change=${(e: Event) => (this.setupPart = (e.target as HTMLSelectElement).value)}>
|
|
602
|
+
${asset.appearance.map(p => html`<option value=${p.target} ?selected=${p.target === setupPart}>${p.target}</option>`)}
|
|
603
|
+
</select>
|
|
604
|
+
<button @click=${() => this.dispatchEvent(new CustomEvent('configure-size', { detail: { part: setupPart }, bubbles: true, composed: true }))}>${i18next.t('figure.button.configure-size')}</button>
|
|
605
|
+
</div>`
|
|
480
606
|
}
|
|
481
607
|
|
|
482
608
|
private lookFrom(view: PreviewView) {
|
|
@@ -486,7 +612,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
486
612
|
|
|
487
613
|
/** The one figure's size: a slider per axis the figure lets change, and what it is at the size tried. */
|
|
488
614
|
private renderSizes(asset: FigureAsset, size: { x: number; y: number; z: number }) {
|
|
489
|
-
|
|
615
|
+
if (!(['x', 'y', 'z'] as const).some(axis => sizeSpanOf(asset, axis))) return nothing
|
|
490
616
|
return html`<div sizes>
|
|
491
617
|
${(['x', 'y', 'z'] as const).map(axis => {
|
|
492
618
|
const range = sizeSpanOf(asset, axis)
|
|
@@ -499,7 +625,6 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
499
625
|
<output>${Math.round(size[axis]).toLocaleString()}</output>
|
|
500
626
|
</label>`
|
|
501
627
|
})}
|
|
502
|
-
<p said>${previewSizeWordsOf(asset, size, say)}</p>
|
|
503
628
|
</div>`
|
|
504
629
|
}
|
|
505
630
|
|
|
@@ -571,27 +696,38 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
571
696
|
this.scene?.root?.fitCameraToModel?.()
|
|
572
697
|
}
|
|
573
698
|
|
|
574
|
-
private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[]) {
|
|
699
|
+
private renderDrivers(asset: FigureAsset, controls: FigureGraphStateInput[], clips: string[], setup: unknown) {
|
|
575
700
|
const steps = new Map(controlsOf(asset).map(c => [c.id, c.step]))
|
|
576
701
|
const sampledStates = sampledStatesOf(asset, this.sample)
|
|
577
702
|
const changed = controls.filter(c => this.giving[c.id] !== undefined && this.giving[c.id] !== asset.stateDefaults[c.id])
|
|
578
|
-
|
|
579
|
-
|
|
703
|
+
/* An icon, its words in its title: the words took the room of a button (user 2026-11-12). */
|
|
704
|
+
const foldWords = i18next.t(this.controlsFolded ? 'figure.button.controls-unfold' : 'figure.button.controls-fold')
|
|
705
|
+
const fold = html`<button fold-controls aria-expanded=${String(!this.controlsFolded)} title=${foldWords} aria-label=${foldWords} @click=${() => (this.controlsFolded = !this.controlsFolded)}>
|
|
706
|
+
<md-icon>${this.controlsFolded ? 'unfold_more' : 'unfold_less'}</md-icon>
|
|
580
707
|
</button>`
|
|
581
|
-
|
|
708
|
+
/* Folded, the icon says what it opens: an icon alone in an empty box said nothing (user 2026-11-12). */
|
|
709
|
+
if (this.controlsFolded)
|
|
710
|
+
return html`<div clips folded>
|
|
711
|
+
<button fold-controls aria-expanded="false" @click=${() => (this.controlsFolded = false)}>
|
|
712
|
+
<md-icon>unfold_more</md-icon>${foldWords}
|
|
713
|
+
</button>
|
|
714
|
+
${setup}
|
|
715
|
+
</div>`
|
|
582
716
|
const played = playedControlsOf(asset, this.driving, this.moving)
|
|
717
|
+
/*
|
|
718
|
+
Keep, clear and fold on one line, there from the start: keep and clear unable until something is tried. Coming and
|
|
719
|
+
going, they moved the rows under them while a slider was being moved (user 2026-11-12).
|
|
720
|
+
*/
|
|
583
721
|
return html`<div clips>
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
</button>`
|
|
594
|
-
: nothing}
|
|
722
|
+
<div actions>
|
|
723
|
+
<button pose ?disabled=${!changed.length} title=${i18next.t('figure.text.pose-as-default-explained')} @click=${() => this.poseAsDefault()}>
|
|
724
|
+
${i18next.t('figure.button.pose-as-default')}
|
|
725
|
+
</button>
|
|
726
|
+
<button clear-tried ?disabled=${!Object.keys(this.giving).length} title=${i18next.t('figure.text.clear-tried-explained')} @click=${() => this.clearTried()}>
|
|
727
|
+
${i18next.t('figure.button.clear-tried')}
|
|
728
|
+
</button>
|
|
729
|
+
${fold}
|
|
730
|
+
</div>
|
|
595
731
|
${controls.map(control => {
|
|
596
732
|
/* A control the data tab's sample sets is what the figure shows, as a value row the sample gives: said, not offered. */
|
|
597
733
|
const fromSample = sampledStates[control.id]
|
|
@@ -617,7 +753,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
617
753
|
<output>${i18next.t('figure.text.control-played', { clip: by })}</output>
|
|
618
754
|
</label>`
|
|
619
755
|
return html`<label>
|
|
620
|
-
|
|
756
|
+
${this.scrubName(html`${label} <span drive>${unitWord(control.unit)}</span>`, control.unit, value, control.min, control.max, v => this.give(control.id, v))}
|
|
621
757
|
${this.renderDial(control, value)}
|
|
622
758
|
<input
|
|
623
759
|
type="range"
|
|
@@ -627,19 +763,7 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
627
763
|
.value=${String(value)}
|
|
628
764
|
@input=${(e: Event) => this.give(control.id, Number((e.target as HTMLInputElement).value))}
|
|
629
765
|
/>
|
|
630
|
-
|
|
631
|
-
typed
|
|
632
|
-
type="number"
|
|
633
|
-
step="any"
|
|
634
|
-
min=${control.min}
|
|
635
|
-
max=${control.max}
|
|
636
|
-
aria-label=${label}
|
|
637
|
-
.value=${String(Math.round(value * 100) / 100)}
|
|
638
|
-
@change=${(e: Event) => {
|
|
639
|
-
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
640
|
-
if (Number.isFinite(typed)) this.give(control.id, Math.min(Math.max(typed, control.min), control.max))
|
|
641
|
-
}}
|
|
642
|
-
/>
|
|
766
|
+
${this.typedBox(label, value, control.min, control.max, typed => this.give(control.id, typed))}
|
|
643
767
|
</label>`
|
|
644
768
|
})}
|
|
645
769
|
${clips.map(clip => {
|
|
@@ -649,10 +773,10 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
649
773
|
*/
|
|
650
774
|
const value = this.driving[clip] ?? 0
|
|
651
775
|
return html`<label>
|
|
652
|
-
|
|
776
|
+
${this.scrubName(html`${clip} <span drive>${i18next.t('figure.label.playback-speed')}</span>`, 'ratio', value, 0, 3, v => this.drive(clip, v))}
|
|
653
777
|
<span></span>
|
|
654
778
|
<input type="range" min="0" max="3" step="0.05" .value=${String(value)} @input=${(e: Event) => this.drive(clip, Number((e.target as HTMLInputElement).value))} />
|
|
655
|
-
|
|
779
|
+
${this.typedBox(clip, value, 0, 3, typed => this.drive(clip, typed))}
|
|
656
780
|
</label>`
|
|
657
781
|
})}
|
|
658
782
|
${valueRowsOf(asset).map(row => this.renderDataValue(row))}
|
|
@@ -673,15 +797,58 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
673
797
|
${byData.speed
|
|
674
798
|
? nothing
|
|
675
799
|
: html`<label>
|
|
676
|
-
|
|
800
|
+
${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 }))}
|
|
677
801
|
<span></span>
|
|
678
802
|
<input type="range" min="0" max="3" step="0.05" .value=${String(playing.speed)} @input=${(e: Event) => this.move(group.id, { ...playing, speed: Number((e.target as HTMLInputElement).value) })} />
|
|
679
|
-
|
|
803
|
+
${this.typedBox(group.label, playing.speed, 0, 3, typed => this.move(group.id, { ...playing, speed: typed }))}
|
|
680
804
|
</label>`}`
|
|
681
805
|
})}
|
|
806
|
+
${setup}
|
|
682
807
|
</div>`
|
|
683
808
|
}
|
|
684
809
|
|
|
810
|
+
/**
|
|
811
|
+
* A row's name that scrubs its value when dragged sideways, as the property panel's names do (\`scrubLabel\`): Shift for
|
|
812
|
+
* finer steps, held within the row's range. The preview's values are tried, not recorded, so each move is the value.
|
|
813
|
+
* The sliders ran 360 degrees over 96 pixels, too coarse to land an angle (user 2026-11-12).
|
|
814
|
+
*/
|
|
815
|
+
private scrubName(name: unknown, unit: string, value: number, min: number, max: number, set: (value: number) => void) {
|
|
816
|
+
const held = (v: number) => Math.min(Math.max(v, min), max)
|
|
817
|
+
return html`<span
|
|
818
|
+
scrub
|
|
819
|
+
@pointerdown=${(down: PointerEvent) => {
|
|
820
|
+
/* A press that did not move gives nothing: an empty field is not given its slider's start by a click on its name. */
|
|
821
|
+
let moved = false
|
|
822
|
+
scrubLabel(down, { value, unit }, {
|
|
823
|
+
move: v => ((moved = true), set(held(v))),
|
|
824
|
+
release: v => set(held(v)),
|
|
825
|
+
cancel: () => moved && set(value)
|
|
826
|
+
})
|
|
827
|
+
}}
|
|
828
|
+
>${name}</span
|
|
829
|
+
>`
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* 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
|
|
834
|
+
* range's hundredth, robot dog 2026-11-03); one column of boxes, not boxes over plain numbers (user 2026-11-12).
|
|
835
|
+
*/
|
|
836
|
+
private typedBox(label: string, value: number, min: number, max: number, set: (typed: number) => void) {
|
|
837
|
+
return html`<input
|
|
838
|
+
typed
|
|
839
|
+
type="number"
|
|
840
|
+
step="any"
|
|
841
|
+
min=${Number.isFinite(min) ? min : nothing}
|
|
842
|
+
max=${Number.isFinite(max) ? max : nothing}
|
|
843
|
+
aria-label=${label}
|
|
844
|
+
.value=${String(Math.round(value * 100) / 100)}
|
|
845
|
+
@change=${(e: Event) => {
|
|
846
|
+
const typed = (e.target as HTMLInputElement).valueAsNumber
|
|
847
|
+
if (Number.isFinite(typed)) set(Math.min(Math.max(typed, min), max))
|
|
848
|
+
}}
|
|
849
|
+
/>`
|
|
850
|
+
}
|
|
851
|
+
|
|
685
852
|
/** 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. */
|
|
686
853
|
private renderDataValue(row: FigureDataRow) {
|
|
687
854
|
const node = row.node
|
|
@@ -694,22 +861,37 @@ export class FigurePreview extends localize(i18next)(LitElement) {
|
|
|
694
861
|
if (sampled !== undefined)
|
|
695
862
|
return html`<label sampled title=${i18next.t('figure.text.data-value-from-sample')}>
|
|
696
863
|
<span>${label} <span drive>${i18next.t('figure.label.data-value-from-sample')}</span></span>
|
|
697
|
-
<output>${typeof sampled === 'object' ? JSON.stringify(sampled) : String(sampled)}</output>
|
|
864
|
+
<output>${typeof sampled === 'boolean' ? i18next.t(sampled ? 'figure.label.data-true' : 'figure.label.data-false') : typeof sampled === 'object' ? JSON.stringify(sampled) : String(sampled)}</output>
|
|
698
865
|
</label>`
|
|
699
866
|
const fallback = 'default' in node ? node.default : undefined
|
|
700
867
|
const value = this.valuing[row.path] ?? fallback
|
|
701
868
|
if (node.kind === 'number') {
|
|
702
869
|
const ranged = node.min !== undefined && node.max !== undefined && node.max > node.min
|
|
870
|
+
/*
|
|
871
|
+
An angle is drawn as the controls' angles are -- dial, slider, box -- whether or not its field declares a range:
|
|
872
|
+
one with none slides over one turn, and any angle can still be typed. 「방향 deg」 was a bare number field under
|
|
873
|
+
「머리 각도 deg」 on a slider (user 2026-11-12).
|
|
874
|
+
*/
|
|
875
|
+
const angle = node.unit === 'deg'
|
|
876
|
+
const slide = ranged ? { min: node.min!, max: node.max! } : angle ? { min: 0, max: 360 } : undefined
|
|
877
|
+
const scrubFrom = Number(value ?? slide?.min ?? 0)
|
|
703
878
|
return html`<label>
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
879
|
+
${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)))}
|
|
880
|
+
${slide
|
|
881
|
+
? html`${this.renderDial({ unit: node.unit ?? '', ...slide }, Number(value ?? slide.min))}
|
|
882
|
+
<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: Event) => this.value(row, (e.target as HTMLInputElement).value)} />
|
|
883
|
+
${ranged
|
|
884
|
+
? this.typedBox(label, Number(value ?? slide.min), slide.min, slide.max, typed => this.value(row, String(typed)))
|
|
885
|
+
: this.typedBox(label, Number(value ?? slide.min), -Infinity, Infinity, typed => this.value(row, String(typed)))}`
|
|
886
|
+
: html`<input wide type="number" step="any" aria-label=${label} .value=${value === undefined ? '' : String(value)} @change=${(e: Event) => this.value(row, (e.target as HTMLInputElement).value)} />`}
|
|
711
887
|
</label>`
|
|
712
888
|
}
|
|
889
|
+
/* 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. */
|
|
890
|
+
if (node.kind === 'boolean')
|
|
891
|
+
return html`<label>
|
|
892
|
+
<span>${label}</span>
|
|
893
|
+
<input type="checkbox" aria-label=${label} .checked=${value === true} @change=${(e: Event) => this.value(row, String((e.target as HTMLInputElement).checked))} />
|
|
894
|
+
</label>`
|
|
713
895
|
const domain = domainOf(node)
|
|
714
896
|
if (!domain.length)
|
|
715
897
|
return html`<label>
|
|
@@ -96,10 +96,7 @@ export declare function copiedTrackEdit(asset: FigureAsset, clip: Clip, track: C
|
|
|
96
96
|
kind: "set-driver";
|
|
97
97
|
state: string;
|
|
98
98
|
clip: string;
|
|
99
|
-
keys:
|
|
100
|
-
seconds: number;
|
|
101
|
-
value: number;
|
|
102
|
-
}[];
|
|
99
|
+
keys: ClipKey[];
|
|
103
100
|
loop: "hold" | "wrap";
|
|
104
101
|
interpolation: "linear" | "step" | "smooth";
|
|
105
102
|
accumulates: boolean;
|
|
@@ -30,8 +30,9 @@ export declare function fitsField(node: FigureDataNode, choice: BehaviorChoice):
|
|
|
30
30
|
import { methodOf, type DataMethod } from '@things-factory/figure-viewer/dist-client/data-connection.js';
|
|
31
31
|
export { methodOf, type DataMethod };
|
|
32
32
|
/**
|
|
33
|
-
* The methods a field allows (ADR-0166 2): a number as it is, scaled
|
|
34
|
-
*
|
|
33
|
+
* The methods a field allows (ADR-0166 2, amendment 1): a number as it is, scaled, by a code table (status codes sent as
|
|
34
|
+
* numbers, 4002) or by a range table; a text or true/false by a code table, or as it is where a behavior takes it so; a
|
|
35
|
+
* list as it is (its items). A new field is made by a table.
|
|
35
36
|
*/
|
|
36
37
|
export declare function methodsOf(node: FigureDataNode | null, choices: BehaviorChoice[]): DataMethod[];
|
|
37
38
|
/** The method a binding is shown by. */
|
|
@@ -45,28 +46,30 @@ export declare const domainOf: (node: FigureDataNode) => FigureDataValue[];
|
|
|
45
46
|
*/
|
|
46
47
|
export declare function bindingDraftOf(asset: FigureAsset, accessor: string, node: FigureDataNode, choice: BehaviorChoice, method?: 'value' | 'scale'): FigureDataValueBinding;
|
|
47
48
|
/** The first code a new code table names, and the next ones: `CODE`, `CODE2`, … */
|
|
49
|
+
/** A new code for a number field's code table: one past the largest taken, or 0 (ADR-0166 amendment 1). */
|
|
50
|
+
export declare function freeNumberCodeOf(taken: string[]): string;
|
|
48
51
|
export declare function freeCodeOf(taken: string[]): string;
|
|
49
52
|
/** A legend role from a binding's id: lower case letters, digits and `-`. */
|
|
50
53
|
export declare const legendRoleOf: (id: string) => string;
|
|
51
54
|
/**
|
|
52
55
|
* A first table on a field (ADR-0166 10): a row per value of a closed field, an open field's rows typed later, one
|
|
53
|
-
* interval over the whole of a number.
|
|
54
|
-
* table's keys, starting with one code; a range table's a number. Returns the binding and, for a new field, the schema
|
|
55
|
-
* with it.
|
|
56
|
+
* interval over the whole of a number. The field is one the schema has: the data tab adds a binding in the field's row.
|
|
56
57
|
*/
|
|
57
58
|
export declare function tableDraftOf(asset: FigureAsset, field: {
|
|
58
59
|
path: string;
|
|
59
60
|
node: FigureDataNode;
|
|
60
|
-
} | {
|
|
61
|
-
name: string;
|
|
62
61
|
}, method: 'code' | 'ranges', targets: FigureDataTargets): {
|
|
63
62
|
binding: FigureDataTableBinding;
|
|
64
|
-
schema?: FigureDataField[];
|
|
65
63
|
};
|
|
66
64
|
/** The table with these targets: each look keeps only the columns they take. */
|
|
67
65
|
export declare function tableWithTargets(b: FigureDataTableBinding, targets: FigureDataTargets): FigureDataTableBinding;
|
|
68
66
|
/** The table with one look set: the whole look given, a column left out cleared. */
|
|
69
67
|
export declare const tableWithLook: (b: FigureDataTableBinding, key: string, look: FigureDataTableEntry) => FigureDataTableBinding;
|
|
68
|
+
/**
|
|
69
|
+
* The table whose other values keep what it last showed, or go to the default look (an empty one): the 「그대로 둠」 of
|
|
70
|
+
* the 「그 밖의 값」 row (ADR-0166 amendment 1).
|
|
71
|
+
*/
|
|
72
|
+
export declare const tableWithKeep: (b: FigureDataTableBinding, keep: boolean) => FigureDataTableBinding;
|
|
70
73
|
/** The table's rows renamed, added or taken away, in order; `default` kept last. */
|
|
71
74
|
export declare function tableWithKeys(b: FigureDataTableBinding, rows: Array<[string, FigureDataTableEntry]>): FigureDataTableBinding;
|
|
72
75
|
/**
|