@things-factory/figure-ui 10.1.46 → 10.1.48

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 (148) hide show
  1. package/client/modeller/angle-dial.ts +70 -0
  2. package/client/modeller/figure-settings.ts +328 -0
  3. package/client/modeller/figure-v3-capability.ts +137 -0
  4. package/client/modeller/figure-v3-clips.ts +253 -0
  5. package/client/modeller/figure-v3-controls.ts +212 -0
  6. package/client/modeller/figure-v3-links.ts +61 -81
  7. package/client/modeller/figure-v3-material.ts +108 -35
  8. package/client/modeller/figure-v3-parts.ts +96 -3
  9. package/client/modeller/figure-v3-preview.ts +326 -0
  10. package/client/modeller/figure-v3-properties.ts +29 -57
  11. package/client/modeller/figure-v3-verdict.ts +598 -60
  12. package/client/modeller/figure-view.ts +152 -0
  13. package/client/modeller/side-panel-styles.ts +326 -0
  14. package/client/modeller/v3-canvas.ts +254 -262
  15. package/client/modeller/v3-clips.ts +89 -0
  16. package/client/modeller/v3-controls.ts +54 -0
  17. package/client/modeller/v3-editor.ts +566 -223
  18. package/client/modeller/v3-fields.ts +84 -0
  19. package/client/modeller/v3-help.ts +41 -0
  20. package/client/modeller/v3-joint-fan.ts +292 -0
  21. package/client/modeller/v3-keys.ts +34 -0
  22. package/client/modeller/v3-links.ts +101 -9
  23. package/client/modeller/v3-live.ts +79 -0
  24. package/client/modeller/v3-maturity.ts +415 -0
  25. package/client/modeller/v3-palette.ts +45 -0
  26. package/client/modeller/v3-preview-model.ts +54 -0
  27. package/client/modeller/v3-stage-model.ts +159 -0
  28. package/client/modeller/v3-thumbnail.ts +8 -4
  29. package/client/pages/figure-list-page.ts +4 -4
  30. package/client/pages/figure-v3-modeller-page.ts +66 -134
  31. package/dist-client/modeller/angle-dial.d.ts +32 -0
  32. package/dist-client/modeller/angle-dial.js +41 -0
  33. package/dist-client/modeller/angle-dial.js.map +1 -0
  34. package/dist-client/modeller/figure-settings.d.ts +39 -0
  35. package/dist-client/modeller/figure-settings.js +317 -0
  36. package/dist-client/modeller/figure-settings.js.map +1 -0
  37. package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
  38. package/dist-client/modeller/figure-v3-capability.js +141 -0
  39. package/dist-client/modeller/figure-v3-capability.js.map +1 -0
  40. package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
  41. package/dist-client/modeller/figure-v3-clips.js +268 -0
  42. package/dist-client/modeller/figure-v3-clips.js.map +1 -0
  43. package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
  44. package/dist-client/modeller/figure-v3-controls.js +226 -0
  45. package/dist-client/modeller/figure-v3-controls.js.map +1 -0
  46. package/dist-client/modeller/figure-v3-links.d.ts +4 -1
  47. package/dist-client/modeller/figure-v3-links.js +64 -82
  48. package/dist-client/modeller/figure-v3-links.js.map +1 -1
  49. package/dist-client/modeller/figure-v3-material.d.ts +4 -1
  50. package/dist-client/modeller/figure-v3-material.js +109 -35
  51. package/dist-client/modeller/figure-v3-material.js.map +1 -1
  52. package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
  53. package/dist-client/modeller/figure-v3-parts.js +88 -3
  54. package/dist-client/modeller/figure-v3-parts.js.map +1 -1
  55. package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
  56. package/dist-client/modeller/figure-v3-preview.js +322 -0
  57. package/dist-client/modeller/figure-v3-preview.js.map +1 -0
  58. package/dist-client/modeller/figure-v3-properties.d.ts +1 -1
  59. package/dist-client/modeller/figure-v3-properties.js +29 -57
  60. package/dist-client/modeller/figure-v3-properties.js.map +1 -1
  61. package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
  62. package/dist-client/modeller/figure-v3-verdict.js +579 -59
  63. package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
  64. package/dist-client/modeller/figure-view.d.ts +118 -0
  65. package/dist-client/modeller/figure-view.js +78 -0
  66. package/dist-client/modeller/figure-view.js.map +1 -0
  67. package/dist-client/modeller/side-panel-styles.d.ts +23 -0
  68. package/dist-client/modeller/side-panel-styles.js +326 -0
  69. package/dist-client/modeller/side-panel-styles.js.map +1 -0
  70. package/dist-client/modeller/v3-canvas.d.ts +44 -37
  71. package/dist-client/modeller/v3-canvas.js +230 -260
  72. package/dist-client/modeller/v3-canvas.js.map +1 -1
  73. package/dist-client/modeller/v3-clips.d.ts +39 -0
  74. package/dist-client/modeller/v3-clips.js +58 -0
  75. package/dist-client/modeller/v3-clips.js.map +1 -0
  76. package/dist-client/modeller/v3-controls.d.ts +28 -0
  77. package/dist-client/modeller/v3-controls.js +28 -0
  78. package/dist-client/modeller/v3-controls.js.map +1 -0
  79. package/dist-client/modeller/v3-editor.d.ts +73 -1
  80. package/dist-client/modeller/v3-editor.js +593 -218
  81. package/dist-client/modeller/v3-editor.js.map +1 -1
  82. package/dist-client/modeller/v3-fields.d.ts +39 -0
  83. package/dist-client/modeller/v3-fields.js +69 -0
  84. package/dist-client/modeller/v3-fields.js.map +1 -0
  85. package/dist-client/modeller/v3-help.d.ts +18 -0
  86. package/dist-client/modeller/v3-help.js +37 -0
  87. package/dist-client/modeller/v3-help.js.map +1 -0
  88. package/dist-client/modeller/v3-joint-fan.d.ts +9 -0
  89. package/dist-client/modeller/v3-joint-fan.js +264 -0
  90. package/dist-client/modeller/v3-joint-fan.js.map +1 -0
  91. package/dist-client/modeller/v3-keys.d.ts +14 -0
  92. package/dist-client/modeller/v3-keys.js +23 -0
  93. package/dist-client/modeller/v3-keys.js.map +1 -0
  94. package/dist-client/modeller/v3-links.d.ts +35 -1
  95. package/dist-client/modeller/v3-links.js +101 -8
  96. package/dist-client/modeller/v3-links.js.map +1 -1
  97. package/dist-client/modeller/v3-live.d.ts +31 -0
  98. package/dist-client/modeller/v3-live.js +58 -0
  99. package/dist-client/modeller/v3-live.js.map +1 -0
  100. package/dist-client/modeller/v3-maturity.d.ts +120 -0
  101. package/dist-client/modeller/v3-maturity.js +317 -0
  102. package/dist-client/modeller/v3-maturity.js.map +1 -0
  103. package/dist-client/modeller/v3-palette.d.ts +7 -0
  104. package/dist-client/modeller/v3-palette.js +42 -0
  105. package/dist-client/modeller/v3-palette.js.map +1 -0
  106. package/dist-client/modeller/v3-preview-model.d.ts +32 -0
  107. package/dist-client/modeller/v3-preview-model.js +48 -0
  108. package/dist-client/modeller/v3-preview-model.js.map +1 -0
  109. package/dist-client/modeller/v3-stage-model.d.ts +31 -0
  110. package/dist-client/modeller/v3-stage-model.js +121 -0
  111. package/dist-client/modeller/v3-stage-model.js.map +1 -0
  112. package/dist-client/modeller/v3-thumbnail.js +6 -3
  113. package/dist-client/modeller/v3-thumbnail.js.map +1 -1
  114. package/dist-client/pages/figure-list-page.js +4 -4
  115. package/dist-client/pages/figure-list-page.js.map +1 -1
  116. package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
  117. package/dist-client/pages/figure-v3-modeller-page.js +64 -136
  118. package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
  119. package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
  120. package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -1
  121. package/dist-client/tsconfig.tsbuildinfo +1 -1
  122. package/package.json +6 -6
  123. package/test/v3-clips.test.ts +70 -0
  124. package/test/v3-controls.test.ts +68 -0
  125. package/test/v3-fields.test.ts +71 -0
  126. package/test/v3-gizmo.test.ts +3 -5
  127. package/test/v3-joint-fan.test.ts +210 -0
  128. package/test/v3-keys.test.ts +37 -0
  129. package/test/v3-layout.test.ts +78 -0
  130. package/test/v3-links.test.ts +89 -6
  131. package/test/v3-live.test.ts +99 -0
  132. package/test/v3-material.test.ts +6 -0
  133. package/test/v3-maturity.test.ts +268 -0
  134. package/test/v3-palette-icons.test.ts +22 -0
  135. package/test/v3-parts-wiring.test.ts +3 -1
  136. package/test/v3-preview.test.ts +42 -0
  137. package/test/v3-properties.test.ts +4 -2
  138. package/test/v3-scene-canvas.test.ts +85 -0
  139. package/test/v3-stage-model.test.ts +25 -0
  140. package/test/v3-theme.test.ts +16 -25
  141. package/test/v3-thumbnail.test.ts +29 -0
  142. package/test/v3-verdict.test.ts +12 -6
  143. package/test/v3-view.test.ts +20 -0
  144. package/translations/en.json +90 -6
  145. package/translations/ja.json +92 -8
  146. package/translations/ko.json +90 -6
  147. package/translations/ms.json +92 -8
  148. package/translations/zh.json +92 -8
@@ -1,6 +1,8 @@
1
+ var FigureV3Editor_1;
1
2
  import { __decorate, __metadata } from "tslib";
2
- import { LitElement, html, css } from 'lit';
3
+ import { LitElement, html, css, nothing } from 'lit';
3
4
  import { i18next } from '@operato/i18n';
5
+ import '@operato/layout';
4
6
  import { customElement, property, state } from 'lit/decorators.js';
5
7
  import { captureV3Thumbnail } from './v3-thumbnail.js';
6
8
  import { drawableV3Parts } from './v3-renderer.js';
@@ -8,33 +10,21 @@ import './v3-canvas.js';
8
10
  import './figure-v3-parts.js';
9
11
  import './figure-v3-material.js';
10
12
  import './figure-v3-links.js';
13
+ import './figure-v3-controls.js';
14
+ import './figure-v3-clips.js';
15
+ import './figure-v3-preview.js';
16
+ import './figure-settings.js';
17
+ import { DEFAULT_VIEW } from './figure-view.js';
18
+ import './figure-v3-capability.js';
11
19
  import { gizmoChanges, gizmoReachOf } from './v3-gizmo.js';
20
+ import { keyActionOf } from './v3-keys.js';
21
+ import { groupFields, partLabelOf, repeatOf } from './v3-fields.js';
22
+ import { SidePanelStyles } from './side-panel-styles.js';
12
23
  import { alignChanges, boxOf, canMirror, mirrorChanges } from './v3-align.js';
13
- const names = {
14
- position: '위치',
15
- rotation: '회전',
16
- size: '크기',
17
- dimension: '치수',
18
- width: '너비',
19
- height: '높이',
20
- depth: '깊이',
21
- radius: '반지름',
22
- length: '길이',
23
- round: '모서리 반경',
24
- left: '왼쪽',
25
- right: '오른쪽',
26
- bottom: '아랫면',
27
- top: '윗면',
28
- openingLeft: '개구부 왼쪽',
29
- openingRight: '개구부 오른쪽',
30
- openingTop: '개구부 윗면'
31
- };
32
- function fieldLabel(id) {
33
- return id
34
- .split('.')
35
- .map(p => names[p] ?? p.toUpperCase())
36
- .join(' · ');
37
- }
24
+ import { paletteRecipe } from './v3-palette.js';
25
+ import { instanceRangeOf, rangeDiffers, turnsAboutAJoint } from './v3-links.js';
26
+ import { jointFanOf } from './v3-joint-fan.js';
27
+ import { isLive, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep } from './v3-live.js';
38
28
  /** Product editor surface. The injected session is shared by page persistence and AI commands. */
39
29
  let FigureV3Editor = class FigureV3Editor extends LitElement {
40
30
  constructor() {
@@ -42,69 +32,193 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
42
32
  this.revision = 0;
43
33
  this.selected = '';
44
34
  this.error = '';
35
+ this.tab = 'properties';
36
+ /** What the middle shows: the parts being edited, or the figure placed on a board at eight sizes (V2's two modes). */
37
+ this.middle = 'edit';
38
+ /**
39
+ * How the workbench shows the figure -- V2's view settings, the board's own state names. The viewer's, kept in
40
+ * their browser, never in the figure.
41
+ */
42
+ this.view = FigureV3Editor_1.storedView();
45
43
  /**
46
44
  * Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved, as in
47
45
  * the V2 modeller: a part hidden here is still in the figure, still checked, still drawn on a board.
48
46
  */
49
47
  this.hiddenParts = new Set();
48
+ /** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
49
+ this.live = NOTHING_LIVE;
50
50
  this.lastContext = null;
51
+ /** The modeller's keys (`v3-keys.ts`): undo, redo, duplicate, remove, back to the whole figure. */
52
+ this.onKey = (e) => {
53
+ const view = this.session?.read();
54
+ if (!view || view.loading || view.saving || !view.context || !this.isConnected)
55
+ return;
56
+ const action = keyActionOf({ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, target: e.composedPath()[0] }, this.selected);
57
+ if (!action)
58
+ return;
59
+ e.preventDefault();
60
+ if (action === 'undo')
61
+ view.canUndo && this.run(() => this.session.undo());
62
+ else if (action === 'redo')
63
+ view.canRedo && this.run(() => this.session.redo());
64
+ else if (action === 'duplicate')
65
+ this.duplicatePart(this.selected);
66
+ else if (action === 'remove')
67
+ this.removePart(this.selected);
68
+ else
69
+ this.selected = '';
70
+ };
51
71
  }
52
- static { this.styles = css `
72
+ static { FigureV3Editor_1 = this; }
73
+ static { this.styles = [SidePanelStyles, css `
53
74
  :host {
54
75
  display: flex;
55
76
  flex-direction: column;
56
77
  min-height: 0;
57
78
  height: 100%;
58
- /*
59
- The editor folds by the width it is given, not the window's. The rail (208 open, 56 closed) and the page's
60
- 320 side take their share first, so a window rule was wrong whenever either was there: at 945 with the rail
61
- open the editor had 417 and still drew its 660 of columns, over the side (UX ruling, 2026-09-24).
62
- */
79
+ /* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */
63
80
  container-type: inline-size;
64
81
  font: 13px sans-serif;
65
- color: var(--md-sys-color-on-surface, #22343f);
82
+ color: var(--md-sys-color-on-surface);
83
+ }
84
+ /* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and
85
+ fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
86
+ ox-canvas-bar button[mode] {
87
+ height: 28px;
88
+ padding: 0 10px;
89
+ border-radius: 999em;
90
+ border-color: transparent;
91
+ background: none;
92
+ }
93
+ ox-canvas-bar button[mode][aria-pressed='true'] {
94
+ border-color: var(--md-sys-color-primary);
66
95
  }
67
- header {
68
- display: flex;
69
- align-items: center;
70
- gap: 8px;
71
- padding: 10px;
72
- border-bottom: 1px solid var(--md-sys-color-outline-variant);
73
- flex-wrap: wrap;
96
+ figure-v3-preview {
97
+ min-width: 0;
98
+ min-height: 0;
99
+ }
100
+ ox-canvas-bar .status {
101
+ padding: 0 8px;
102
+ color: var(--md-sys-color-on-surface-variant);
103
+ }
104
+ ox-canvas-bar .status[dirty] {
105
+ color: var(--md-sys-color-primary);
106
+ font-weight: 600;
74
107
  }
75
- main {
108
+ ox-canvas-bar button[save] {
109
+ height: 32px;
110
+ padding: 0 14px;
111
+ border: none;
112
+ border-radius: 999em;
113
+ color: var(--md-sys-color-on-primary);
114
+ background: var(--md-sys-color-primary);
115
+ }
116
+ /* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under
117
+ 860px and one column with the middle first under 560px are the frame's, as are the dividers. */
118
+ ox-three-pane {
76
119
  flex: 1;
77
120
  min-height: 0;
78
- display: grid;
79
- grid-template-columns: 180px minmax(180px, 1fr) 300px;
80
121
  }
122
+ figure-v3-parts {
123
+ min-height: 0;
124
+ }
125
+ figure-v3-canvas {
126
+ min-width: 0;
127
+ min-height: 0;
128
+ }
129
+ /* The side: tabs over one panel at a time, each using the whole height (V2's side, and why it had tabs). */
81
130
  aside {
82
- padding: 12px;
83
- overflow: auto;
84
- background: var(--md-sys-color-surface, #fff);
131
+ display: flex;
132
+ flex-direction: column;
133
+ min-height: 0;
134
+ background: var(--md-sys-color-surface-container-lowest);
85
135
  }
86
- figure-v3-parts {
136
+ div[tabs] {
137
+ display: flex;
138
+ flex: none;
139
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
140
+ }
141
+ button[tab] {
142
+ flex: 1;
143
+ position: relative;
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ height: 38px;
148
+ padding: 0;
149
+ border: none;
150
+ border-radius: 0;
151
+ border-bottom: 2px solid transparent;
152
+ background: none;
153
+ color: var(--md-sys-color-on-surface-variant);
154
+ }
155
+ button[tab]:hover:not([on]) {
156
+ background: var(--md-sys-color-surface-container);
157
+ }
158
+ button[tab][on] {
159
+ color: var(--md-sys-color-primary);
160
+ border-bottom-color: var(--md-sys-color-primary);
161
+ }
162
+ button[tab] md-icon {
163
+ font-size: 20px;
164
+ width: 20px;
165
+ height: 20px;
166
+ }
167
+ span[badge] {
168
+ position: absolute;
169
+ top: 3px;
170
+ right: 2px;
171
+ padding: 0 5px;
172
+ font-size: 0.62rem;
173
+ font-weight: 700;
174
+ line-height: 1.5;
175
+ color: var(--md-sys-color-on-primary-container);
176
+ background: var(--md-sys-color-primary-container);
177
+ border-radius: 999em;
178
+ pointer-events: none;
179
+ }
180
+ /* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */
181
+ div[panel] {
182
+ flex: 1;
87
183
  min-height: 0;
184
+ overflow: auto;
185
+ padding: 0;
186
+ }
187
+ div[panel][flush] {
188
+ padding: 0;
189
+ }
190
+ /* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */
191
+ [off] {
192
+ display: none !important;
88
193
  }
89
- aside:last-child {
90
- border-left: 1px solid var(--md-sys-color-outline-variant);
194
+ /* Which part the panel speaks of, and the way back to the whole figure. */
195
+ div[subject] {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 6px;
199
+ padding: 6px 10px 0;
91
200
  }
92
- h3 {
93
- font-size: var(--ui-t-body, 13px);
94
- margin: 22px 0 10px;
201
+ div[subject] button[chip] {
202
+ display: inline-flex;
203
+ align-items: center;
204
+ gap: 2px;
205
+ height: 26px;
206
+ padding: 0 10px 0 6px;
207
+ border-radius: 999em;
208
+ color: var(--md-sys-color-on-surface-variant);
95
209
  }
96
- h3:first-child {
97
- margin-top: 0;
210
+ div[subject] md-icon {
211
+ font-size: 16px;
212
+ width: 16px;
213
+ height: 16px;
98
214
  }
99
- button,
100
- input,
101
- select {
215
+ button {
102
216
  font: inherit;
103
217
  box-sizing: border-box;
104
- height: 34px;
218
+ height: 32px;
105
219
  border: 1px solid var(--md-sys-color-outline-variant);
106
220
  border-radius: 6px;
107
- background: var(--md-sys-color-surface, #fff);
221
+ background: var(--md-sys-color-surface);
108
222
  color: inherit;
109
223
  padding: 0 8px;
110
224
  min-width: 0;
@@ -121,94 +235,43 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
121
235
  color: var(--md-sys-color-on-secondary-container);
122
236
  border-color: var(--md-sys-color-primary);
123
237
  }
124
- label[name-field] {
125
- margin: 0 0 8px;
126
- }
127
- label[name-field] input {
128
- font-weight: 600;
129
- }
130
- div[tools] {
131
- display: flex;
132
- flex-wrap: wrap;
133
- gap: 4px;
134
- margin: 0 0 8px;
135
- }
136
- div[tools] button {
137
- display: inline-flex;
138
- align-items: center;
139
- justify-content: center;
140
- width: 34px;
141
- padding: 0;
142
- }
143
- div[tools] md-icon {
144
- font-size: 18px;
145
- width: 18px;
146
- height: 18px;
147
- }
148
- .parts {
149
- display: grid;
150
- gap: 6px;
151
- }
152
- .recipes {
153
- display: grid;
154
- grid-template-columns: 1fr 1fr;
155
- gap: 6px;
156
- }
157
- label {
158
- display: grid;
159
- grid-template-columns: 112px minmax(0, 1fr);
160
- align-items: center;
161
- gap: 8px;
162
- margin: 8px 0;
163
- }
164
- label span {
165
- overflow-wrap: anywhere;
166
- }
167
238
  .error {
168
239
  padding: 10px;
169
240
  background: var(--md-sys-color-error-container);
170
241
  color: var(--md-sys-color-on-error-container);
171
242
  white-space: pre-wrap;
172
243
  }
173
- .status {
174
- margin-left: auto;
175
- }
176
244
  .hint {
177
245
  color: var(--md-sys-color-on-surface-variant);
178
246
  line-height: 1.5;
179
247
  }
180
- /* 660 and over: parts | canvas | inputs. Below that the sum of the columns' least widths no longer fits. */
181
- @container (max-width: 659px) {
182
- main {
183
- grid-template-columns: 140px minmax(160px, 1fr);
184
- }
185
- aside:last-child {
186
- grid-column: 1/-1;
187
- max-height: 260px;
188
- border-left: none;
189
- border-top: 1px solid var(--md-sys-color-outline-variant);
190
- }
191
- }
192
- /* Under 420: one column. The canvas first — seeing what is being made comes before the lists — and main scrolls. */
193
- @container (max-width: 419px) {
194
- main {
195
- grid-template-columns: minmax(0, 1fr);
196
- grid-template-rows: minmax(240px, 45%);
197
- grid-auto-rows: auto;
198
- overflow-y: auto;
199
- }
200
- figure-v3-canvas {
201
- order: -1;
202
- }
203
- figure-v3-parts {
204
- border-right: none;
205
- border-top: 1px solid var(--md-sys-color-outline-variant);
206
- }
207
- aside:last-child {
208
- max-height: none;
209
- }
210
- }
211
- `; }
248
+ `]; }
249
+ static storedView() {
250
+ try {
251
+ const stored = JSON.parse(localStorage.getItem('figure-v3-view') ?? 'null');
252
+ return stored && typeof stored === 'object' ? { ...DEFAULT_VIEW, ...stored } : { ...DEFAULT_VIEW };
253
+ }
254
+ catch {
255
+ return { ...DEFAULT_VIEW };
256
+ }
257
+ }
258
+ setView(view) {
259
+ this.view = { ...DEFAULT_VIEW, ...view };
260
+ try {
261
+ localStorage.setItem('figure-v3-view', JSON.stringify(this.view));
262
+ }
263
+ catch {
264
+ /* A browser that keeps nothing still shows the change for this visit. */
265
+ }
266
+ }
267
+ connectedCallback() {
268
+ super.connectedCallback();
269
+ window.addEventListener('keydown', this.onKey);
270
+ }
271
+ disconnectedCallback() {
272
+ window.removeEventListener('keydown', this.onKey);
273
+ super.disconnectedCallback();
274
+ }
212
275
  willUpdate(_changed) {
213
276
  const view = this.session?.read();
214
277
  if (view?.context !== this.lastContext) {
@@ -229,6 +292,33 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
229
292
  catch (error) {
230
293
  this.error = error instanceof Error ? error.message : String(error);
231
294
  }
295
+ finally {
296
+ /* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
297
+ this.live = NOTHING_LIVE;
298
+ }
299
+ }
300
+ /** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
301
+ drawnParts(geometry, asset) {
302
+ const parts = (isLive(this.live) && asset ? livePartsOf(asset, this.live) : null) ?? drawableV3Parts(geometry ?? []);
303
+ return parts.filter(body => !this.hiddenParts.has(body.id));
304
+ }
305
+ /**
306
+ * Where the selected part turns and how far, with the pose as it is drawn now -- a value in hand included, so the
307
+ * fan follows the arm while a control is dragged. Its size comes from what the joint turns (`v3-joint-fan`).
308
+ */
309
+ fanOf(asset) {
310
+ if (!asset || !this.selected || this.hiddenParts.has(this.selected))
311
+ return null;
312
+ try {
313
+ return jointFanOf(asset, this.selected, this.live.values);
314
+ }
315
+ catch {
316
+ return null;
317
+ }
318
+ }
319
+ /** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
320
+ hold(values, edit) {
321
+ this.live = { values: { ...this.live.values, ...values }, ...(edit ? { edit } : this.live.edit ? { edit: this.live.edit } : {}) };
232
322
  }
233
323
  changed() {
234
324
  this.requestUpdate();
@@ -265,28 +355,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
265
355
  let n = 1;
266
356
  while (ids.has(`${kind}-${n}`))
267
357
  n++;
268
- const common = { id: `${kind}-${n}`, position: { x: 0, y: 50, z: 0 } };
269
- const part = kind === 'box'
270
- ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100 } }
271
- : kind === 'cylinder'
272
- ? { ...common, kind, dimensions: { radius: 50, length: 100 } }
273
- : kind === 'rounded-box'
274
- ? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }
275
- : {
276
- ...common,
277
- position: { x: 0, y: 0, z: 0 },
278
- kind: 'portal',
279
- dimensions: {
280
- left: -100,
281
- right: 100,
282
- bottom: 0,
283
- top: 200,
284
- openingLeft: -70,
285
- openingRight: 70,
286
- openingTop: 170,
287
- depth: 30
288
- }
289
- };
358
+ const part = paletteRecipe(kind, `${kind}-${n}`);
290
359
  this.session.add(part);
291
360
  this.selected = part.id;
292
361
  });
@@ -411,6 +480,14 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
411
480
  * A part's name — the V2 part name field. It renames every value the part owns, and what refers to them follows
412
481
  * (figure-model's rename-part), as one step of the history.
413
482
  */
483
+ /**
484
+ * A repeated body is one of its repeat's copies and is named after it -- the list and this field read `roller 15`, not
485
+ * its path through the repeat (user, 2026-09-24: 「부품 이름 ???」). A copy has no name of its own to change.
486
+ */
487
+ renderRepeatNote(geometry) {
488
+ const repeat = repeatOf(this.selected, geometry);
489
+ return repeat ? html `<p hint>${i18next.t('figure.text.repeat-member', { name: repeat.name, count: repeat.count, index: repeat.index })}</p>` : nothing;
490
+ }
414
491
  renamePart(input) {
415
492
  const name = input.value.trim();
416
493
  const from = this.selected;
@@ -436,7 +513,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
436
513
  }
437
514
  }
438
515
  /** Size response, fastening and motion for the selected part (parity ④ ⑤ ⑥), as authoring commands. */
439
- renderLinks(unavailable) {
516
+ renderLinks(unavailable, section) {
440
517
  let asset;
441
518
  try {
442
519
  asset = JSON.parse(this.session.source());
@@ -453,6 +530,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
453
530
  }
454
531
  return html `<figure-v3-links
455
532
  .asset=${asset}
533
+ .section=${section}
456
534
  .partId=${this.selected}
457
535
  .dims=${dims}
458
536
  ?locked=${unavailable}
@@ -460,6 +538,33 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
460
538
  @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
461
539
  ></figure-v3-links>`;
462
540
  }
541
+ /** The figure's controls: try a value, name it, set its range and travel time, keep the pose (parity row ⑥). */
542
+ renderControls(view, unavailable) {
543
+ let asset;
544
+ try {
545
+ asset = JSON.parse(this.session.source());
546
+ }
547
+ catch {
548
+ return null;
549
+ }
550
+ const values = Object.fromEntries(view.fields.filter(f => f.role === 'state').map(f => [f.id, f.value]));
551
+ return html `<figure-v3-controls
552
+ .asset=${asset}
553
+ .values=${values}
554
+ ?locked=${unavailable}
555
+ @v3-state=${(e) => this.run(() => this.session.apply([{ kind: 'set-state', input: e.detail.input, unit: e.detail.unit, value: e.detail.value }]))}
556
+ @v3-state-live=${(e) => this.hold({ [e.detail.input]: e.detail.value })}
557
+ @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
558
+ ></figure-v3-controls>
559
+ <figure-v3-clips
560
+ .asset=${asset}
561
+ .values=${values}
562
+ ?locked=${unavailable}
563
+ @v3-states=${(e) => this.run(() => this.session.apply(e.detail.changes))}
564
+ @v3-states-live=${(e) => this.hold(e.detail.values)}
565
+ @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
566
+ ></figure-v3-clips>`;
567
+ }
463
568
  /** One command, one step of the history. A refusal a person can meet here is said in their words. */
464
569
  linkCommand(action) {
465
570
  try {
@@ -487,35 +592,183 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
487
592
  // Rejected edits must also restore the displayed numeric value.
488
593
  input.value = String(this.session.read().fields.find(i => i.id === field.id).value);
489
594
  }
595
+ /** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
596
+ typing(field, event) {
597
+ const value = event.target.valueAsNumber;
598
+ if (Number.isFinite(value))
599
+ this.hold({ [field.id]: value });
600
+ }
601
+ /**
602
+ * Drag a field's label (or an axis letter) sideways to scrub its value, as vector tools do: the figure follows the
603
+ * pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
604
+ */
605
+ scrub(field, down, unavailable) {
606
+ if (unavailable || down.button !== 0)
607
+ return;
608
+ const label = down.currentTarget;
609
+ const input = (label.closest('div[cell]')?.querySelector('input') ?? label.nextElementSibling);
610
+ if (!input)
611
+ return;
612
+ down.preventDefault();
613
+ label.setPointerCapture(down.pointerId);
614
+ const start = field.value;
615
+ let value = start;
616
+ const move = (e) => {
617
+ value = scrubbed(start, e.clientX - down.clientX, scrubStep(field.unit, start, e.shiftKey));
618
+ input.value = String(value);
619
+ this.hold({ [field.id]: value });
620
+ };
621
+ const up = (e) => {
622
+ label.removeEventListener('pointermove', move);
623
+ label.removeEventListener('pointerup', up);
624
+ label.removeEventListener('pointercancel', up);
625
+ /* A drag the system took away is not the author's value: the field goes back to where it was. */
626
+ if (e.type === 'pointercancel')
627
+ value = start;
628
+ if (value === start) {
629
+ input.value = String(start);
630
+ this.live = NOTHING_LIVE;
631
+ return;
632
+ }
633
+ input.value = String(value);
634
+ this.changeField(field, { target: input });
635
+ };
636
+ label.addEventListener('pointermove', move);
637
+ label.addEventListener('pointerup', up);
638
+ label.addEventListener('pointercancel', up);
639
+ }
640
+ /** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
641
+ renderInstanceRange(asset, unavailable) {
642
+ if (!asset)
643
+ return nothing;
644
+ /* Another figure, or another step of this one, is measured again. */
645
+ const at = `${this.session?.read().context ?? ''}:${this.revision}`;
646
+ if (this.range?.at !== at) {
647
+ let of;
648
+ try {
649
+ of = instanceRangeOf(asset);
650
+ }
651
+ catch {
652
+ of = undefined;
653
+ }
654
+ this.range = { at, of };
655
+ }
656
+ const range = this.range.of;
657
+ if (!range)
658
+ return nothing;
659
+ const mm = (v) => Math.round(v).toLocaleString();
660
+ const differs = rangeDiffers(range.declared, range.measured);
661
+ return html `<section>
662
+ <h3>${i18next.t('figure.label.instance-range')}</h3>
663
+ ${['x', 'y', 'z'].map(axis => html `<div inline-field>
664
+ <label>${axis.toUpperCase()}</label><span>${mm(range.declared[axis].min)} – ${mm(range.declared[axis].max)} mm</span>
665
+ </div>`)}
666
+ ${differs
667
+ ? html `<p hint>
668
+ ${i18next.t('figure.text.instance-range-can-widen', {
669
+ ranges: ['x', 'y', 'z']
670
+ .filter(axis => rangeDiffers(range.declared, { ...range.declared, [axis]: range.measured[axis] }))
671
+ .map(axis => `${axis.toUpperCase()} ${mm(range.measured[axis].min)} – ${mm(range.measured[axis].max)}`)
672
+ .join(' · ')
673
+ })}
674
+ </p>
675
+ <button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
676
+ <md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
677
+ </button>`
678
+ : nothing}
679
+ ${this.renderSizingRule(asset, unavailable)}
680
+ </section>`;
681
+ }
682
+ /**
683
+ * How the figure grows. One ratio (`sizing: 'uniform'`) is said as it is; a figure with joints that has not declared
684
+ * it is offered it -- a mechanism does not shear -- and the author decides (chief architect's ruling 2026-09-24).
685
+ */
686
+ renderSizingRule(asset, unavailable) {
687
+ if (asset.sizing === 'uniform')
688
+ return html `<div inline-field><label>${i18next.t('figure.label.sizing-rule')}</label><span>${i18next.t('figure.label.sizing-uniform')}</span></div>`;
689
+ return html `${turnsAboutAJoint(asset) ? html `<p hint>${i18next.t('figure.text.sizing-uniform-proposed')}</p>` : nothing}
690
+ <button act ?disabled=${unavailable} title=${i18next.t('figure.text.sizing-uniform-explained')} @click=${() => this.linkCommand({ kind: 'scale-uniformly' })}>
691
+ <md-icon>aspect_ratio</md-icon>${i18next.t('figure.button.scale-uniformly')}
692
+ </button>`;
693
+ }
694
+ /** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
695
+ choose(id) {
696
+ this.selected = id;
697
+ if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history'))
698
+ this.tab = 'properties';
699
+ }
700
+ asset() {
701
+ try {
702
+ return JSON.parse(this.session.source());
703
+ }
704
+ catch {
705
+ return undefined;
706
+ }
707
+ }
708
+ renderTab(tab, icon, label, badge) {
709
+ return html `<button tab ?on=${this.tab === tab} title=${label} aria-label=${label} @click=${() => (this.tab = tab)}>
710
+ <md-icon>${icon}</md-icon>${badge == null ? '' : html `<span badge>${badge.toLocaleString()}</span>`}
711
+ </button>`;
712
+ }
713
+ updated(changed) {
714
+ super.updated?.(changed);
715
+ const panes = this.renderRoot.querySelector('ox-three-pane') ?? undefined;
716
+ if (panes !== this.panes)
717
+ this.panes = panes;
718
+ }
719
+ /** Which part the panel speaks of, with the way back to the whole figure. */
720
+ renderSubject() {
721
+ if (!this.selected)
722
+ return null;
723
+ return html `<div subject>
724
+ <button chip title=${i18next.t('figure.button.whole-figure')} @click=${() => (this.selected = '')}>
725
+ <md-icon>arrow_back</md-icon>${i18next.t('figure.button.whole-figure')}
726
+ </button>
727
+ </div>`;
728
+ }
490
729
  render() {
491
730
  const view = this.session?.read();
492
731
  if (!view)
493
- return html `<p>모델을 여는 중입니다.</p>`;
732
+ return html `<p>${i18next.t('figure.text.opening-model')}</p>`;
494
733
  const unavailable = view.loading || !!view.error || view.saving || !view.context;
495
734
  const part = view.geometry.find(g => g.id === this.selected);
496
- // Shared/root parameters remain available even when a part is selected.
497
- const own = view.fields.filter(i => this.selected && i.id.startsWith(this.selected + '.'));
498
- const common = view.fields.filter(i => !view.geometry.some(g => i.id.startsWith(g.id + '.')));
499
- const fields = this.selected ? [...own, ...common] : view.fields;
500
- return html ` <header>
501
- <strong>V3 모델링</strong>
502
- <button ?disabled=${unavailable || !view.canUndo} @click=${() => this.run(() => this.session.undo())}>
503
- 실행 취소
504
- </button>
505
- <button ?disabled=${unavailable || !view.canRedo} @click=${() => this.run(() => this.session.redo())}>
506
- 다시 실행
507
- </button>
508
- <button @click=${() => this.renderRoot.querySelector('figure-v3-canvas')?.fit()}>
509
- 전체 보기
510
- </button>
511
- <span class="status" role="status"
512
- >${view.loading ? '불러오는 중' : view.saving ? '저장 중' : view.dirty || !view.id ? '저장 안 됨' : '저장됨'}</span
513
- >
514
- <button ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>저장</button>
515
- </header>
735
+ /* The controls (state inputs) have their own place in the motion tab; these are the design values. */
736
+ const design = view.fields.filter(i => i.role !== 'state');
737
+ const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.'));
738
+ const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(g.id + '.')));
739
+ const status = view.loading
740
+ ? 'figure.text.loading-figure'
741
+ : view.saving
742
+ ? 'figure.text.saving-figure'
743
+ : view.dirty || !view.id
744
+ ? 'figure.text.unsaved-changes'
745
+ : 'figure.text.saved';
746
+ const asset = this.asset();
747
+ const tab = this.tab;
748
+ return html `<ox-canvas-bar
749
+ history
750
+ fit
751
+ undo-shortcut="Ctrl+Z"
752
+ redo-shortcut="Ctrl+Y"
753
+ ?can-undo=${!unavailable && view.canUndo}
754
+ ?can-redo=${!unavailable && view.canRedo}
755
+ .pane=${this.panes}
756
+ @canvas-undo=${() => this.run(() => this.session.undo())}
757
+ @canvas-redo=${() => this.run(() => this.session.redo())}
758
+ @canvas-fit=${() => this.renderRoot.querySelector('figure-v3-canvas')?.fit()}
759
+ >
760
+ ${['edit', 'preview'].map(mode => html `<button slot="mode" mode aria-pressed=${String(this.middle === mode)} title=${i18next.t('figure.text.mode-' + mode + '-explained')} @click=${() => (this.middle = mode)}>
761
+ ${i18next.t('figure.label.mode-' + mode)}
762
+ </button>`)}
763
+ <span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
764
+ <button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
765
+ </ox-canvas-bar>
516
766
  ${this.error || view.error ? html `<div role="alert" class="error">${this.error || view.error}</div>` : null}
517
- <main>
767
+ <ox-three-pane>
518
768
  <figure-v3-parts
769
+ slot="start"
770
+ .budget=${this.budget}
771
+ @show-maturity=${() => (this.tab = 'verdict')}
519
772
  .parts=${view.geometry.map((g) => ({
520
773
  id: g.id,
521
774
  provider: g.shape?.provider ?? '',
@@ -525,7 +778,7 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
525
778
  .selected=${this.selected}
526
779
  ?locked=${unavailable}
527
780
  @add-part=${(e) => this.add(e.detail.kind)}
528
- @select-part=${(e) => (this.selected = e.detail.id)}
781
+ @select-part=${(e) => this.choose(e.detail.id)}
529
782
  @toggle-part-hidden=${(e) => this.toggleHiddenPart(e.detail.id)}
530
783
  @set-all-parts-hidden=${(e) => (this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
531
784
  @move-part-up=${(e) => this.movePart(e.detail.id, e.detail.to)}
@@ -533,47 +786,141 @@ let FigureV3Editor = class FigureV3Editor extends LitElement {
533
786
  @duplicate-part=${(e) => this.duplicatePart(e.detail.id)}
534
787
  @remove-part=${(e) => this.removePart(e.detail.id)}
535
788
  ></figure-v3-parts>
536
- <figure-v3-canvas
537
- .parts=${drawableV3Parts(view.geometry ?? []).filter(body => !this.hiddenParts.has(body.id))}
789
+ ${this.middle === 'preview'
790
+ ? html `<figure-v3-preview
791
+ .asset=${asset}
792
+ .context=${view.context ?? ''}
793
+ .view=${this.view}
794
+ @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
795
+ ></figure-v3-preview>`
796
+ : html `<figure-v3-canvas
797
+ .view=${this.view}
798
+ .parts=${this.drawnParts(view.geometry, asset)}
799
+ .fan=${this.fanOf(asset)}
538
800
  .palette=${view.palette ?? {}}
539
801
  .context=${view.context ?? ''}
540
802
  .selected=${this.selected}
541
803
  .reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
542
804
  @part-transformed=${(e) => this.transformed(e.detail.id, e.detail.result)}
543
- @part-selected=${(e) => {
544
- this.selected = e.detail;
545
- }}
805
+ @part-selected=${(e) => this.choose(e.detail)}
546
806
  @render-error=${(e) => {
547
- this.error = e.detail.message;
548
- }}
549
- ></figure-v3-canvas>
550
- <aside>
551
- ${this.selected && part
552
- ? html `<label name-field
553
- ><span>${i18next.t('figure.label.name')}</span
554
- ><input
555
- aria-label=${i18next.t('figure.label.name')}
556
- .value=${this.selected}
557
- ?disabled=${unavailable}
558
- @change=${(e) => this.renamePart(e.target)}
559
- /></label>`
560
- : html `<h3>${this.selected || '모델 입력'}</h3>`}
561
- ${this.renderTools(unavailable)}
562
- ${fields.map(field => html `<label><span>${fieldLabel(field.id)} (${field.unit})${field.role === 'state' ? ' · 동작' : ''}</span><input aria-label=${field.id} type="number" step="any" .value=${String(field.value)} ?disabled=${unavailable} @change=${(e) => this.changeField(field, e)} /></label>`)}
563
- ${part
564
- ? html `${this.renderLinks(unavailable)}<figure-v3-material
565
- .body=${part}
566
- .palette=${view.palette ?? {}}
567
- ?locked=${unavailable}
568
- @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
569
- ></figure-v3-material>`
570
- : null}
571
- ${view.bindings.length
572
- ? html `<h3>${i18next.t('figure.label.places')}</h3>
573
- ${view.bindings.map(b => html `<p>${b.label} · ${i18next.t('figure.label.place-' + b.role)}</p>`)}`
574
- : null}
807
+ this.error = e.detail.message;
808
+ }}
809
+ ></figure-v3-canvas>`}
810
+ <aside slot="end">
811
+ <div tabs>
812
+ ${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
813
+ ${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
814
+ ${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
815
+ ${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}
816
+ ${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}
817
+ ${this.renderTab('history', 'history', i18next.t('figure.label.history'))}
818
+ </div>
819
+ <slot name="notice"></slot>
820
+ <div panel ?off=${tab !== 'properties'}>
821
+ ${this.selected && part
822
+ ? html `${this.renderSubject()}
823
+ <section>
824
+ <h3>${i18next.t('figure.label.group-part')}</h3>
825
+ <div inline-field>
826
+ <label>${i18next.t('figure.label.name')}</label>
827
+ <input
828
+ name-field
829
+ aria-label=${i18next.t('figure.label.name')}
830
+ .value=${partLabelOf(this.selected)}
831
+ ?disabled=${unavailable || !!repeatOf(this.selected, view.geometry)}
832
+ @change=${(e) => this.renamePart(e.target)}
833
+ />
834
+ </div>
835
+ ${this.renderRepeatNote(view.geometry)}
836
+ </section>
837
+ ${this.renderGroups(groupFields(design, this.selected), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable))}
838
+ ${this.renderLinks(unavailable, 'shape')}
839
+ <figure-v3-material
840
+ .body=${part}
841
+ .palette=${view.palette ?? {}}
842
+ ?locked=${unavailable}
843
+ @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
844
+ @v3-part-live=${(e) => this.hold({}, e.detail.edit)}
845
+ ></figure-v3-material>`
846
+ : html `<slot name="figure"></slot>
847
+ ${this.renderGroups(groupFields(common), 'figure.label.group-figure-size', 'figure.label.design-inputs', unavailable)}
848
+ ${this.renderInstanceRange(asset, unavailable)}`}
849
+ </div>
850
+ <div panel ?off=${tab !== 'capability'}>
851
+ ${this.renderSubject()}
852
+ ${this.selected && part ? this.renderLinks(unavailable, 'places') : null}
853
+ <figure-v3-capability
854
+ .asset=${asset}
855
+ .selected=${this.selected}
856
+ ?locked=${unavailable}
857
+ @select-part=${(e) => this.choose(e.detail.id)}
858
+ @v3-part-edit=${(e) => this.run(() => this.session.edit(e.detail.edit))}
859
+ ></figure-v3-capability>
860
+ </div>
861
+ <div panel ?off=${tab !== 'motion'}>
862
+ ${this.renderSubject()}
863
+ ${this.selected && part ? this.renderLinks(unavailable, 'motion') : html `<section><p hint>${i18next.t('figure.text.choose-a-part-to-move')}</p></section>`}
864
+ ${this.renderControls(view, unavailable)}
865
+ </div>
866
+ <div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
867
+ <div panel flush ?off=${tab !== 'view'}>
868
+ <figure-settings .view=${this.view} @update-view=${(e) => this.setView(e.detail.view)}></figure-settings>
869
+ </div>
870
+ <div panel flush ?off=${tab !== 'history'}><slot name="history"></slot></div>
575
871
  </aside>
576
- </main>`;
872
+ </ox-three-pane>`;
873
+ }
874
+ /** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
875
+ renderGroups(groups, vectorTitle, singleTitle, unavailable, tools = null) {
876
+ return html `${groups.vectors.length || tools
877
+ ? html `<section>
878
+ <h3>${i18next.t(vectorTitle)}</h3>
879
+ ${groups.vectors.map(g => this.renderVector(g, unavailable))}
880
+ ${tools ? html `<div vector-row><label>${i18next.t('figure.label.tools')}</label>${tools}</div>` : nothing}
881
+ </section>`
882
+ : nothing}
883
+ ${groups.singles.length
884
+ ? html `<section>
885
+ <h3>${i18next.t(singleTitle)}</h3>
886
+ <div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>
887
+ </section>`
888
+ : nothing}`;
889
+ }
890
+ renderVector(group, unavailable) {
891
+ if (group.kind !== 'vector')
892
+ return nothing;
893
+ const unit = group.cells.x.unit === 'deg' ? '°' : group.cells.x.unit;
894
+ return html `<div vector-row>
895
+ <label>${i18next.t('figure.label.group-' + group.name)}${group.name === 'rotation' ? '' : html ` <span unit>${unit}</span>`}</label>
896
+ <div triple>
897
+ ${['x', 'y', 'z'].map(axis => html `<div cell>
898
+ <span axis ?scrub=${!unavailable} @pointerdown=${(e) => this.scrub(group.cells[axis], e, unavailable)}>${axis.toUpperCase()}</span>
899
+ <input
900
+ aria-label=${group.cells[axis].id}
901
+ type="number"
902
+ step="any"
903
+ .value=${String(group.cells[axis].value)}
904
+ ?disabled=${unavailable}
905
+ @input=${(e) => this.typing(group.cells[axis], e)}
906
+ @change=${(e) => this.changeField(group.cells[axis], e)}
907
+ />
908
+ </div>`)}
909
+ </div>
910
+ </div>`;
911
+ }
912
+ renderSingle(name, field, unavailable) {
913
+ const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }));
914
+ return html `<span ?scrub=${!unavailable} @pointerdown=${(e) => this.scrub(field, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
915
+ ><input
916
+ aria-label=${field.id}
917
+ type="number"
918
+ step="any"
919
+ .value=${String(field.value)}
920
+ ?disabled=${unavailable}
921
+ @input=${(e) => this.typing(field, e)}
922
+ @change=${(e) => this.changeField(field, e)}
923
+ />`;
577
924
  }
578
925
  };
579
926
  __decorate([
@@ -584,6 +931,14 @@ __decorate([
584
931
  property({ type: Number }),
585
932
  __metadata("design:type", Object)
586
933
  ], FigureV3Editor.prototype, "revision", void 0);
934
+ __decorate([
935
+ property({ type: Number }),
936
+ __metadata("design:type", Number)
937
+ ], FigureV3Editor.prototype, "instances", void 0);
938
+ __decorate([
939
+ property({ attribute: false }),
940
+ __metadata("design:type", Object)
941
+ ], FigureV3Editor.prototype, "budget", void 0);
587
942
  __decorate([
588
943
  state(),
589
944
  __metadata("design:type", Object)
@@ -592,11 +947,31 @@ __decorate([
592
947
  state(),
593
948
  __metadata("design:type", Object)
594
949
  ], FigureV3Editor.prototype, "error", void 0);
950
+ __decorate([
951
+ state(),
952
+ __metadata("design:type", String)
953
+ ], FigureV3Editor.prototype, "tab", void 0);
954
+ __decorate([
955
+ state(),
956
+ __metadata("design:type", Function)
957
+ ], FigureV3Editor.prototype, "panes", void 0);
958
+ __decorate([
959
+ state(),
960
+ __metadata("design:type", String)
961
+ ], FigureV3Editor.prototype, "middle", void 0);
962
+ __decorate([
963
+ state(),
964
+ __metadata("design:type", Object)
965
+ ], FigureV3Editor.prototype, "view", void 0);
595
966
  __decorate([
596
967
  state(),
597
968
  __metadata("design:type", Object)
598
969
  ], FigureV3Editor.prototype, "hiddenParts", void 0);
599
- FigureV3Editor = __decorate([
970
+ __decorate([
971
+ state(),
972
+ __metadata("design:type", Object)
973
+ ], FigureV3Editor.prototype, "live", void 0);
974
+ FigureV3Editor = FigureV3Editor_1 = __decorate([
600
975
  customElement('figure-v3-editor')
601
976
  ], FigureV3Editor);
602
977
  export { FigureV3Editor };