@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.
Files changed (112) hide show
  1. package/client/modeller/canvas.ts +4 -2
  2. package/client/modeller/clips.ts +74 -4
  3. package/client/modeller/data-interface.ts +30 -18
  4. package/client/modeller/editor-styles.ts +17 -6
  5. package/client/modeller/editor.ts +2 -0
  6. package/client/modeller/fasten-form.ts +81 -6
  7. package/client/modeller/fasten-geometry.ts +5 -3
  8. package/client/modeller/figure-cavity-drawing.ts +1 -3
  9. package/client/modeller/figure-clips.ts +101 -8
  10. package/client/modeller/figure-data-panel.ts +384 -200
  11. package/client/modeller/figure-fasten-dialog.ts +41 -30
  12. package/client/modeller/figure-fasten-drawing.ts +24 -14
  13. package/client/modeller/figure-first-save.ts +4 -3
  14. package/client/modeller/figure-links.ts +109 -37
  15. package/client/modeller/figure-parts.ts +7 -11
  16. package/client/modeller/figure-preview.ts +299 -79
  17. package/client/modeller/figure-size-editor.ts +7 -2
  18. package/client/modeller/first-save.ts +8 -0
  19. package/client/modeller/focus-on-render.ts +4 -0
  20. package/client/modeller/pivot.ts +76 -0
  21. package/client/modeller/size-response.ts +14 -0
  22. package/client/modeller/stage-model.ts +50 -6
  23. package/dist-client/modeller/canvas.d.ts +1 -0
  24. package/dist-client/modeller/canvas.js +3 -2
  25. package/dist-client/modeller/canvas.js.map +1 -1
  26. package/dist-client/modeller/clips.d.ts +17 -3
  27. package/dist-client/modeller/clips.js +69 -4
  28. package/dist-client/modeller/clips.js.map +1 -1
  29. package/dist-client/modeller/data-interface.d.ts +11 -8
  30. package/dist-client/modeller/data-interface.js +27 -17
  31. package/dist-client/modeller/data-interface.js.map +1 -1
  32. package/dist-client/modeller/editor-styles.js +17 -6
  33. package/dist-client/modeller/editor-styles.js.map +1 -1
  34. package/dist-client/modeller/editor.js +2 -0
  35. package/dist-client/modeller/editor.js.map +1 -1
  36. package/dist-client/modeller/fasten-form.d.ts +13 -0
  37. package/dist-client/modeller/fasten-form.js +72 -7
  38. package/dist-client/modeller/fasten-form.js.map +1 -1
  39. package/dist-client/modeller/fasten-geometry.d.ts +3 -2
  40. package/dist-client/modeller/fasten-geometry.js +5 -3
  41. package/dist-client/modeller/fasten-geometry.js.map +1 -1
  42. package/dist-client/modeller/figure-cavity-drawing.js +1 -3
  43. package/dist-client/modeller/figure-cavity-drawing.js.map +1 -1
  44. package/dist-client/modeller/figure-clips.d.ts +9 -0
  45. package/dist-client/modeller/figure-clips.js +104 -8
  46. package/dist-client/modeller/figure-clips.js.map +1 -1
  47. package/dist-client/modeller/figure-data-panel.d.ts +10 -19
  48. package/dist-client/modeller/figure-data-panel.js +382 -190
  49. package/dist-client/modeller/figure-data-panel.js.map +1 -1
  50. package/dist-client/modeller/figure-fasten-dialog.js +39 -24
  51. package/dist-client/modeller/figure-fasten-dialog.js.map +1 -1
  52. package/dist-client/modeller/figure-fasten-drawing.js +24 -14
  53. package/dist-client/modeller/figure-fasten-drawing.js.map +1 -1
  54. package/dist-client/modeller/figure-first-save.js +4 -3
  55. package/dist-client/modeller/figure-first-save.js.map +1 -1
  56. package/dist-client/modeller/figure-links.d.ts +9 -0
  57. package/dist-client/modeller/figure-links.js +120 -51
  58. package/dist-client/modeller/figure-links.js.map +1 -1
  59. package/dist-client/modeller/figure-parts.js +7 -11
  60. package/dist-client/modeller/figure-parts.js.map +1 -1
  61. package/dist-client/modeller/figure-preview.d.ts +13 -0
  62. package/dist-client/modeller/figure-preview.js +296 -80
  63. package/dist-client/modeller/figure-preview.js.map +1 -1
  64. package/dist-client/modeller/figure-size-editor.js +7 -3
  65. package/dist-client/modeller/figure-size-editor.js.map +1 -1
  66. package/dist-client/modeller/first-save.d.ts +5 -0
  67. package/dist-client/modeller/first-save.js +7 -0
  68. package/dist-client/modeller/first-save.js.map +1 -1
  69. package/dist-client/modeller/focus-on-render.d.ts +2 -0
  70. package/dist-client/modeller/focus-on-render.js +4 -0
  71. package/dist-client/modeller/focus-on-render.js.map +1 -0
  72. package/dist-client/modeller/pivot.d.ts +12 -0
  73. package/dist-client/modeller/pivot.js +59 -0
  74. package/dist-client/modeller/pivot.js.map +1 -0
  75. package/dist-client/modeller/size-response.d.ts +6 -0
  76. package/dist-client/modeller/size-response.js +14 -0
  77. package/dist-client/modeller/size-response.js.map +1 -1
  78. package/dist-client/modeller/stage-model.d.ts +17 -3
  79. package/dist-client/modeller/stage-model.js +44 -6
  80. package/dist-client/modeller/stage-model.js.map +1 -1
  81. package/dist-client/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +8 -7
  83. package/test/browser/editing-page.js +1 -1
  84. package/test/browser/editing.check.mjs +6 -5
  85. package/test/browser/links.check.mjs +30 -0
  86. package/test/browser/modeller.check.mjs +2 -2
  87. package/test/browser/parts-page.js +4 -0
  88. package/test/browser/parts.check.mjs +16 -10
  89. package/test/browser/screen-page.js +12 -1
  90. package/test/browser/screen.check.mjs +142 -11
  91. package/test/browser/section.check.mjs +21 -0
  92. package/test/browser/serve-response.mjs +5 -2
  93. package/test/browser/side-panel-sweep.check.mjs +171 -10
  94. package/test/clips.test.ts +49 -4
  95. package/test/data-motion.test.ts +39 -8
  96. package/test/fasten-form.test.ts +69 -0
  97. package/test/fasten-geometry.test.ts +13 -0
  98. package/test/first-save.test.ts +11 -1
  99. package/test/pivot.test.ts +57 -0
  100. package/test/size-response.test.ts +10 -2
  101. package/test/stage-model.test.ts +23 -1
  102. package/translations/en.json +31 -11
  103. package/translations/es.json +30 -10
  104. package/translations/ja.json +30 -10
  105. package/translations/ko.json +32 -12
  106. package/translations/ms.json +30 -10
  107. package/translations/zh.json +30 -10
  108. package/tutorial/humanoid/README.md +22 -0
  109. package/tutorial/humanoid/chapters.mjs +247 -0
  110. package/tutorial/humanoid/encode.mjs +26 -0
  111. package/tutorial/humanoid/lib.mjs +197 -0
  112. package/tutorial/humanoid/record.mjs +65 -0
@@ -9,15 +9,26 @@ import { i18next, localize } from '@operato/i18n';
9
9
  import { unitWord } from './unit-words.js';
10
10
  import { figureFaceOverhangOf, figureMotionsOf, motionIdOf } from '@hatiolab/figure-model';
11
11
  import { acceptsOf, AXES, canFasten, canFollow, positionIsShare, heldBy, shownWhenOf, visibilityControlsOf, fasteningOf, hasInstanceSize, holdsThings, leadersFor, motionOf, motionFormOf, motionFormRefusalOf, endPlaceKeyOf, motionJoint, controlIdFor, withoutAxis, placeFacesOf, placeIdFor, placesOn, rowAxesOf, rowBlockOf, fixedJointOf, rowOf, positionsWorkedOut, followOf, followableAxes, measureNow, freshStateSize, stateSizeIdFor, stateSizeCommandOf, stateSizeFixedOf } from './links.js';
12
- import { measureFactor } from './size-response.js';
12
+ import { measureFactor, sizeAxisWord } from './size-response.js';
13
13
  import { SidePanelStyles } from '@things-factory/figure-viewer/dist-client/side-panel-styles.js';
14
14
  import { insideAlignmentOf } from './fasten-geometry.js';
15
15
  import { fastenFormOf, fastenPlanOf, fastenSentenceOf } from './fasten-form.js';
16
16
  import { HelpToggles } from './help.js';
17
17
  import { groupMenu, GroupMenuStyles } from './group-menu.js';
18
18
  import { jointAxisWord } from './joint-axis.js';
19
+ import { PIVOT_POINTS, pivotPointOf, pivotPointsOf } from './pivot.js';
19
20
  import { placeTerms } from '@things-factory/figure-viewer/dist-client/detail-facts.js';
20
21
  const freshMotion = () => ({ kind: 'slide', axis: 'x', id: '', min: 0, max: 500, follow: '', multiplier: 1, offset: 0, travel: '', travelPlus: 0 });
22
+ /**
23
+ * The motion a part's form starts from: a part hung on another part most often turns there -- a thigh at the hip, a door
24
+ * at its frame -- so it starts as a turn; a part on the plane, or on nothing, as a slide (review 2026-11-11: eight
25
+ * joints of a humanoid, each changed from slide to turn by hand).
26
+ */
27
+ export function motionStartOf(revision, part) {
28
+ const own = revision ? fasteningOf(revision, part) : undefined;
29
+ return own && !own.toPlane && own.target ? { ...freshMotion(), kind: 'turn', min: -90, max: 90 } : freshMotion();
30
+ }
31
+ const sameMotion = (a, b) => JSON.stringify(a) === JSON.stringify(b);
21
32
  const freshRow = () => ({ axis: '', pitch: 200, fills: true, count: 5 });
22
33
  const freshPlace = () => ({ role: 'grip', face: 'top', label: '', accepts: '', capacity: 1 });
23
34
  /**
@@ -76,9 +87,31 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
76
87
  this.motionAxis = 0;
77
88
  }
78
89
  static { this.styles = [SidePanelStyles, GroupMenuStyles, css `
90
+ /* A motion's delete and edit as icons at the line's end, their words in their titles: two word buttons took the line (user 2026-11-12). */
91
+ div[pair] > button[act][icon] {
92
+ flex: none;
93
+ width: var(--figure-control-height, 28px);
94
+ padding: 0;
95
+ justify-content: center;
96
+ }
97
+ /* The size-by-state form folded under its summary, opened to add one. */
98
+ details[state-size-add][open] > summary {
99
+ margin-bottom: 6px;
100
+ }
79
101
  :host {
80
102
  display: block;
81
103
  }
104
+ /* The turn's axis through the part's top, middle or bottom, by name, under the numbers it fills in. */
105
+ div[pivot-points] {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
109
+ margin: 4px 0 8px;
110
+ }
111
+ div[pivot-points] > span {
112
+ color: var(--md-sys-color-on-surface-variant);
113
+ font-size: var(--ui-t-small, 11.5px);
114
+ }
82
115
  /* A group's sub-question, under its title: quieter than the title, as a label is. */
83
116
  p[group-question] {
84
117
  margin: 4px 0 0;
@@ -155,7 +188,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
155
188
  this.fixNote = '';
156
189
  if (this.asset && this.partId && this.partId !== this.loadedFor) {
157
190
  this.loadedFor = this.partId;
158
- this.motion = freshMotion();
191
+ this.motion = motionStartOf(this.revision, this.partId);
159
192
  this.place = freshPlace();
160
193
  this.placeEditing = null;
161
194
  this.row = freshRow();
@@ -164,6 +197,12 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
164
197
  this.editingMotion = false;
165
198
  this.motionAxis = 0;
166
199
  }
200
+ else if (changed.has('revision') && this.partId && !this.editingMotion) {
201
+ /* Fastened since the form was loaded, a form nobody has touched starts as that part's motion now does. */
202
+ const was = motionStartOf(changed.get('revision'), this.partId);
203
+ if (sameMotion(this.motion, was))
204
+ this.motion = motionStartOf(this.revision, this.partId);
205
+ }
167
206
  }
168
207
  updated() {
169
208
  /* Only the sections that hold a form speak of what it would do, and only while their tab is shown: the panel has
@@ -555,7 +594,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
555
594
  const axes = followableAxes(asset, part);
556
595
  if (!axes.length)
557
596
  return nothing;
558
- const word = (axis) => `${axis.toUpperCase()}${measureFactor(asset, part, axis) === 2 ? ' · ' + i18next.t('figure.label.size-diameter') : ''}`;
597
+ const word = (axis) => sizeAxisWord(asset, part, axis, i18next.t('figure.label.size-diameter'));
559
598
  const sized = axes.flatMap(axis => {
560
599
  const f = followOf(asset, part, axis);
561
600
  return f.of === 'state' ? [{ axis, f }] : [];
@@ -576,7 +615,7 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
576
615
  : { ...form, id: value };
577
616
  };
578
617
  const number = (key) => (e) => (this.stateSize = { ...form, axis, [key]: e.target.valueAsNumber });
579
- return html `<h4 state-size>${i18next.t('figure.label.state-size')}</h4>
618
+ return html `${sized.length ? html `<h4 state-size>${i18next.t('figure.label.state-size')}</h4>` : nothing}
580
619
  ${sized.map(({ axis: one, f }) => {
581
620
  const k = measureFactor(asset, part, one);
582
621
  const input = controls.find(c => c.id === f.input);
@@ -599,7 +638,10 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
599
638
  </div>`;
600
639
  })}
601
640
  ${free.length
602
- ? html `<div form state-size-form>
641
+ ? /* Folded until asked for: six lines of a form few parts use pushed the controls below it down (user 2026-11-12). */
642
+ html `<details state-size-add>
643
+ <summary>${i18next.t('figure.button.add-state-size')}</summary>
644
+ <div form state-size-form>
603
645
  <span>${i18next.t('figure.label.state-size-measure')}</span>
604
646
  <select ?disabled=${this.locked} @change=${(e) => (this.stateSize = { ...form, axis: e.target.value, atMax: NaN })}>
605
647
  ${free.map(one => html `<option value=${one} ?selected=${one === axis}>${word(one)}</option>`)}
@@ -609,9 +651,9 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
609
651
  <datalist id="state-size-controls">${controls.map(c => html `<option value=${c.id}>${c.label}</option>`)}</datalist>
610
652
  <span>${i18next.t('figure.label.state-size-unit')}</span>
611
653
  <select ?disabled=${this.locked} @change=${(e) => {
612
- const unit = e.target.value;
613
- this.stateSize = { ...form, axis, unit, ...(unit === 'percent' ? { min: 0, max: 100 } : unit === 'ratio' ? { min: 0, max: 1 } : {}) };
614
- }}>
654
+ const unit = e.target.value;
655
+ this.stateSize = { ...form, axis, unit, ...(unit === 'percent' ? { min: 0, max: 100 } : unit === 'ratio' ? { min: 0, max: 1 } : {}) };
656
+ }}>
615
657
  ${['percent', 'ratio', 'mm'].map(u => html `<option value=${u} ?selected=${u === form.unit}>${unitWord(u)}</option>`)}
616
658
  </select>
617
659
  <span>${i18next.t('figure.label.control-range')} (${unitWord(form.unit)})</span>
@@ -625,11 +667,12 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
625
667
  <input type="number" step="any" min="0" state-size-at-max .value=${Number.isFinite(atMax) ? String(atMax) : ''} ?disabled=${this.locked} @change=${number('atMax')} />
626
668
  </div>
627
669
  <button act add-state-size ?disabled=${this.locked || !command} @click=${() => {
628
- if (!command)
629
- return;
630
- this.command(command);
631
- this.stateSize = freshStateSize();
632
- }}><md-icon>open_in_full</md-icon>${i18next.t('figure.button.add-state-size')}</button>`
670
+ if (!command)
671
+ return;
672
+ this.command(command);
673
+ this.stateSize = freshStateSize();
674
+ }}><md-icon>add</md-icon>${i18next.t('figure.button.add')}</button>
675
+ </details>`
633
676
  : nothing}`;
634
677
  }
635
678
  renderShown(asset) {
@@ -690,7 +733,30 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
690
733
  <input type="number" step="any" aria-label=${`${i18next.t('figure.label.motion-through')} ${axis.toUpperCase()}`} .value=${String(m.through?.[axis] ?? 0)} @change=${typed(axis)} />
691
734
  </div>`)}
692
735
  </div>
693
- </div>`;
736
+ </div>
737
+ ${this.renderPivotPoints(m)}`;
738
+ }
739
+ renderPivotPoints(m) {
740
+ if (!this.revision || m.throughFollows)
741
+ return nothing;
742
+ const key = `${this.revision.revision}:${this.partId}:${m.axis}`;
743
+ if (this.pivotCache?.key !== key)
744
+ this.pivotCache = { key, points: pivotPointsOf(this.revision, this.partId, m.axis) };
745
+ const points = this.pivotCache.points;
746
+ if (!points)
747
+ return nothing;
748
+ return html `<div pivot-points>
749
+ <span>${i18next.t('figure.label.pivot-at')}</span>
750
+ <ox-input-select-buttons
751
+ .value=${pivotPointOf(points, m.through, m.axis) ?? ''}
752
+ .options=${PIVOT_POINTS.map(p => ({ value: p, display: i18next.t(`figure.label.pivot-${p}`) }))}
753
+ @change=${(e) => {
754
+ const at = points[e.detail];
755
+ if (at)
756
+ this.motion = { ...m, through: { ...at } };
757
+ }}
758
+ ></ox-input-select-buttons>
759
+ </div>`;
694
760
  }
695
761
  renderMotion(asset) {
696
762
  const now = motionOf(asset, this.partId);
@@ -727,14 +793,14 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
727
793
  : nothing}
728
794
  </span>
729
795
  ${count > 1
730
- ? html `<button act remove-axis ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
731
- <md-icon>close</md-icon>${i18next.t('figure.button.remove-axis')}
796
+ ? html `<button act icon remove-axis title=${i18next.t('figure.button.remove-axis')} aria-label=${i18next.t('figure.button.remove-axis')} ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: withoutAxis(joint, k) })}>
797
+ <md-icon>delete</md-icon>
732
798
  </button>`
733
- : html `<button act ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
734
- <md-icon>close</md-icon>${i18next.t('figure.button.remove-motion')}
799
+ : html `<button act icon title=${i18next.t('figure.button.remove-motion')} aria-label=${i18next.t('figure.button.remove-motion')} ?disabled=${this.locked || !joint} @click=${() => joint && this.command({ kind: 'set-joint', joint: fixedJointOf(joint) })}>
800
+ <md-icon>delete</md-icon>
735
801
  </button>`}
736
- <button act ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
737
- <md-icon>edit</md-icon>${i18next.t('figure.button.change-motion')}
802
+ <button act icon title=${i18next.t('figure.button.change-motion')} aria-label=${i18next.t('figure.button.change-motion')} ?disabled=${this.locked || !joint} @click=${() => edit(k)}>
803
+ <md-icon>edit</md-icon>
738
804
  </button>
739
805
  </div>`;
740
806
  };
@@ -775,6 +841,8 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
775
841
  const adding = k > 0 && k >= figureMotionsOf(joint).length;
776
842
  const follow = leaders.some(j => j.id === m.follow) ? m.follow : '';
777
843
  const refusal = motionFormRefusalOf(m, follow);
844
+ /* Following another joint is offered after the part's own control: a row above it moved every field down a row, and
845
+ a name typed there chose a joint to follow (review 2026-11-11). */
778
846
  return html `
779
847
  ${k > 0 ? html `<p motion-axis-heading><b>${i18next.t('figure.label.motion-axis-n', { n: k + 1 })}</b></p>` : nothing}
780
848
  <div pair>
@@ -789,6 +857,35 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
789
857
  ${AXES.map(a => html `<option value=${a} ?selected=${a === m.axis}>${i18next.t('figure.label.motion-axis')} ${a.toUpperCase()}</option>`)}
790
858
  </select>`}
791
859
  </div>
860
+ ${m.followSum || follow
861
+ ? nothing
862
+ : html `<div form>
863
+ ${m.kind === 'slide'
864
+ ? html `<span>${i18next.t('figure.label.motion-travel')}</span>
865
+ <select @change=${(e) => (this.motion = { ...m, travel: e.target.value })}>
866
+ ${m.travelSum
867
+ ? html `<option value="" ?selected=${!m.travel}>${m.travelSum.sum.map(t => `${t.from.of === 'input' ? t.from.input : `${t.from.of === 'part' ? t.from.part : i18next.t('figure.label.motion-travel')} · ${t.from.axis.toUpperCase()}`}${t.times !== undefined && t.times !== 1 ? ` × ${t.times}` : ''}`).join(' + ')}</option>`
868
+ : html `<option value="" ?selected=${!m.travel}>${i18next.t('figure.label.motion-travel-mm')}</option>`}
869
+ ${asset.appearance
870
+ .map(look => look.target)
871
+ .filter(one => canFollow(asset, one))
872
+ .flatMap(one => AXES.map(a => html `<option value=${`${one}:${a}`} ?selected=${m.travel === `${one}:${a}`}>${one} · ${a.toUpperCase()}</option>`))}
873
+ </select>
874
+ ${m.travel
875
+ ? html `<span>${i18next.t('figure.label.follows-plus')}</span>
876
+ <input type="number" step="any" .value=${String(m.travelPlus ?? 0)} @change=${(e) => (this.motion = { ...m, travelPlus: Number(e.target.value) || 0 })} />`
877
+ : nothing}`
878
+ : nothing}
879
+ <span>${i18next.t('figure.label.control-id')}</span>
880
+ <input .value=${m.id} placeholder=${controlIdFor(this.partId, m.kind, k)} @change=${(e) => (this.motion = { ...m, id: e.target.value.trim() })} />
881
+ ${m.kind === 'continuous' || (m.kind === 'slide' && (m.travel || m.travelSum))
882
+ ? nothing
883
+ : html `<span>${i18next.t('figure.label.control-range')} (${unit})</span>
884
+ <div pair>
885
+ <input type="number" step="any" .value=${String(m.min)} @change=${(e) => (this.motion = { ...m, min: Number(e.target.value) })} />
886
+ <input type="number" step="any" .value=${String(m.max)} @change=${(e) => (this.motion = { ...m, max: Number(e.target.value) })} />
887
+ </div>`}
888
+ </div>`}
792
889
  ${m.followSum
793
890
  ? html `<div form>
794
891
  <span>${i18next.t('figure.label.motion-follow')}</span>
@@ -805,42 +902,14 @@ let FigureLinks = class FigureLinks extends localize(i18next)(LitElement) {
805
902
  </select>
806
903
  </div>`
807
904
  : nothing}
808
- ${m.followSum
809
- ? nothing
810
- : follow
811
- ? html `<div form>
905
+ ${!m.followSum && follow
906
+ ? html `<div form>
812
907
  <span>${i18next.t('figure.label.motion-multiplier')}</span>
813
908
  <input type="number" step="any" .value=${String(m.multiplier)} @change=${(e) => (this.motion = { ...m, multiplier: Number(e.target.value) })} />
814
909
  <span>${i18next.t('figure.label.motion-offset')} (${unit})</span>
815
910
  <input type="number" step="any" .value=${String(m.offset)} @change=${(e) => (this.motion = { ...m, offset: Number(e.target.value) || 0 })} />
816
911
  </div>`
817
- : html `<div form>
818
- ${m.kind === 'slide'
819
- ? html `<span>${i18next.t('figure.label.motion-travel')}</span>
820
- <select @change=${(e) => (this.motion = { ...m, travel: e.target.value })}>
821
- ${m.travelSum
822
- ? html `<option value="" ?selected=${!m.travel}>${m.travelSum.sum.map(t => `${t.from.of === 'input' ? t.from.input : `${t.from.of === 'part' ? t.from.part : i18next.t('figure.label.motion-travel')} · ${t.from.axis.toUpperCase()}`}${t.times !== undefined && t.times !== 1 ? ` × ${t.times}` : ''}`).join(' + ')}</option>`
823
- : html `<option value="" ?selected=${!m.travel}>${i18next.t('figure.label.motion-travel-mm')}</option>`}
824
- ${asset.appearance
825
- .map(look => look.target)
826
- .filter(one => canFollow(asset, one))
827
- .flatMap(one => AXES.map(a => html `<option value=${`${one}:${a}`} ?selected=${m.travel === `${one}:${a}`}>${one} · ${a.toUpperCase()}</option>`))}
828
- </select>
829
- ${m.travel
830
- ? html `<span>${i18next.t('figure.label.follows-plus')}</span>
831
- <input type="number" step="any" .value=${String(m.travelPlus ?? 0)} @change=${(e) => (this.motion = { ...m, travelPlus: Number(e.target.value) || 0 })} />`
832
- : nothing}`
833
- : nothing}
834
- <span>${i18next.t('figure.label.control-id')}</span>
835
- <input .value=${m.id} placeholder=${controlIdFor(this.partId, m.kind, k)} @change=${(e) => (this.motion = { ...m, id: e.target.value.trim() })} />
836
- ${m.kind === 'continuous' || (m.kind === 'slide' && (m.travel || m.travelSum))
837
- ? nothing
838
- : html `<span>${i18next.t('figure.label.control-range')} (${unit})</span>
839
- <div pair>
840
- <input type="number" step="any" .value=${String(m.min)} @change=${(e) => (this.motion = { ...m, min: Number(e.target.value) })} />
841
- <input type="number" step="any" .value=${String(m.max)} @change=${(e) => (this.motion = { ...m, max: Number(e.target.value) })} />
842
- </div>`}
843
- </div>`}
912
+ : nothing}
844
913
  ${m.kind === 'slide' ? nothing : this.renderThrough(m)}
845
914
  <button
846
915
  act