@things-factory/figure-ui 10.1.45 → 10.1.47
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/angle-dial.ts +70 -0
- package/client/modeller/figure-settings.ts +328 -0
- package/client/modeller/figure-v3-capability.ts +137 -0
- package/client/modeller/figure-v3-clips.ts +253 -0
- package/client/modeller/figure-v3-controls.ts +212 -0
- package/client/modeller/figure-v3-links.ts +150 -79
- package/client/modeller/figure-v3-material.ts +108 -35
- package/client/modeller/figure-v3-parts.ts +96 -3
- package/client/modeller/figure-v3-preview.ts +326 -0
- package/client/modeller/figure-v3-properties.ts +29 -57
- package/client/modeller/figure-v3-verdict.ts +598 -60
- package/client/modeller/figure-view.ts +152 -0
- package/client/modeller/side-panel-styles.ts +326 -0
- package/client/modeller/v3-canvas.ts +221 -263
- package/client/modeller/v3-clips.ts +89 -0
- package/client/modeller/v3-controls.ts +54 -0
- package/client/modeller/v3-editor.ts +532 -223
- package/client/modeller/v3-fields.ts +62 -0
- package/client/modeller/v3-help.ts +41 -0
- package/client/modeller/v3-keys.ts +34 -0
- package/client/modeller/v3-links.ts +126 -10
- package/client/modeller/v3-live.ts +79 -0
- package/client/modeller/v3-maturity.ts +415 -0
- package/client/modeller/v3-palette.ts +45 -0
- package/client/modeller/v3-preview-model.ts +54 -0
- package/client/modeller/v3-stage-model.ts +159 -0
- package/client/pages/figure-list-page.ts +4 -4
- package/client/pages/figure-v3-modeller-page.ts +66 -134
- package/dist-client/modeller/angle-dial.d.ts +32 -0
- package/dist-client/modeller/angle-dial.js +41 -0
- package/dist-client/modeller/angle-dial.js.map +1 -0
- package/dist-client/modeller/figure-settings.d.ts +39 -0
- package/dist-client/modeller/figure-settings.js +317 -0
- package/dist-client/modeller/figure-settings.js.map +1 -0
- package/dist-client/modeller/figure-v3-capability.d.ts +22 -0
- package/dist-client/modeller/figure-v3-capability.js +141 -0
- package/dist-client/modeller/figure-v3-capability.js.map +1 -0
- package/dist-client/modeller/figure-v3-clips.d.ts +31 -0
- package/dist-client/modeller/figure-v3-clips.js +268 -0
- package/dist-client/modeller/figure-v3-clips.js.map +1 -0
- package/dist-client/modeller/figure-v3-controls.d.ts +36 -0
- package/dist-client/modeller/figure-v3-controls.js +226 -0
- package/dist-client/modeller/figure-v3-controls.js.map +1 -0
- package/dist-client/modeller/figure-v3-links.d.ts +10 -2
- package/dist-client/modeller/figure-v3-links.js +134 -79
- package/dist-client/modeller/figure-v3-links.js.map +1 -1
- package/dist-client/modeller/figure-v3-material.d.ts +5 -2
- package/dist-client/modeller/figure-v3-material.js +109 -35
- package/dist-client/modeller/figure-v3-material.js.map +1 -1
- package/dist-client/modeller/figure-v3-parts.d.ts +10 -0
- package/dist-client/modeller/figure-v3-parts.js +88 -3
- package/dist-client/modeller/figure-v3-parts.js.map +1 -1
- package/dist-client/modeller/figure-v3-preview.d.ts +34 -0
- package/dist-client/modeller/figure-v3-preview.js +322 -0
- package/dist-client/modeller/figure-v3-preview.js.map +1 -0
- package/dist-client/modeller/figure-v3-properties.d.ts +2 -2
- package/dist-client/modeller/figure-v3-properties.js +29 -57
- package/dist-client/modeller/figure-v3-properties.js.map +1 -1
- package/dist-client/modeller/figure-v3-verdict.d.ts +31 -17
- package/dist-client/modeller/figure-v3-verdict.js +579 -59
- package/dist-client/modeller/figure-v3-verdict.js.map +1 -1
- package/dist-client/modeller/figure-view.d.ts +118 -0
- package/dist-client/modeller/figure-view.js +78 -0
- package/dist-client/modeller/figure-view.js.map +1 -0
- package/dist-client/modeller/side-panel-styles.d.ts +23 -0
- package/dist-client/modeller/side-panel-styles.js +326 -0
- package/dist-client/modeller/side-panel-styles.js.map +1 -0
- package/dist-client/modeller/v3-canvas.d.ts +29 -37
- package/dist-client/modeller/v3-canvas.js +188 -260
- package/dist-client/modeller/v3-canvas.js.map +1 -1
- package/dist-client/modeller/v3-clips.d.ts +39 -0
- package/dist-client/modeller/v3-clips.js +58 -0
- package/dist-client/modeller/v3-clips.js.map +1 -0
- package/dist-client/modeller/v3-controls.d.ts +28 -0
- package/dist-client/modeller/v3-controls.js +28 -0
- package/dist-client/modeller/v3-controls.js.map +1 -0
- package/dist-client/modeller/v3-editor.d.ts +58 -1
- package/dist-client/modeller/v3-editor.js +556 -218
- package/dist-client/modeller/v3-editor.js.map +1 -1
- package/dist-client/modeller/v3-fields.d.ts +30 -0
- package/dist-client/modeller/v3-fields.js +46 -0
- package/dist-client/modeller/v3-fields.js.map +1 -0
- package/dist-client/modeller/v3-help.d.ts +18 -0
- package/dist-client/modeller/v3-help.js +37 -0
- package/dist-client/modeller/v3-help.js.map +1 -0
- package/dist-client/modeller/v3-keys.d.ts +14 -0
- package/dist-client/modeller/v3-keys.js +23 -0
- package/dist-client/modeller/v3-keys.js.map +1 -0
- package/dist-client/modeller/v3-links.d.ts +44 -2
- package/dist-client/modeller/v3-links.js +125 -8
- package/dist-client/modeller/v3-links.js.map +1 -1
- package/dist-client/modeller/v3-live.d.ts +31 -0
- package/dist-client/modeller/v3-live.js +58 -0
- package/dist-client/modeller/v3-live.js.map +1 -0
- package/dist-client/modeller/v3-maturity.d.ts +120 -0
- package/dist-client/modeller/v3-maturity.js +317 -0
- package/dist-client/modeller/v3-maturity.js.map +1 -0
- package/dist-client/modeller/v3-palette.d.ts +7 -0
- package/dist-client/modeller/v3-palette.js +42 -0
- package/dist-client/modeller/v3-palette.js.map +1 -0
- package/dist-client/modeller/v3-preview-model.d.ts +32 -0
- package/dist-client/modeller/v3-preview-model.js +48 -0
- package/dist-client/modeller/v3-preview-model.js.map +1 -0
- package/dist-client/modeller/v3-stage-model.d.ts +31 -0
- package/dist-client/modeller/v3-stage-model.js +121 -0
- package/dist-client/modeller/v3-stage-model.js.map +1 -0
- package/dist-client/pages/figure-list-page.js +4 -4
- package/dist-client/pages/figure-list-page.js.map +1 -1
- package/dist-client/pages/figure-v3-modeller-page.d.ts +2 -6
- package/dist-client/pages/figure-v3-modeller-page.js +64 -136
- package/dist-client/pages/figure-v3-modeller-page.js.map +1 -1
- package/dist-client/tsconfig.chk.tsbuildinfo +1 -0
- package/dist-client/tsconfig.scratch-fm.tsbuildinfo +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/test/v3-clips.test.ts +70 -0
- package/test/v3-controls.test.ts +68 -0
- package/test/v3-fields.test.ts +62 -0
- package/test/v3-gizmo.test.ts +3 -5
- package/test/v3-keys.test.ts +37 -0
- package/test/v3-layout.test.ts +78 -0
- package/test/v3-links.test.ts +118 -7
- package/test/v3-live.test.ts +99 -0
- package/test/v3-material.test.ts +6 -0
- package/test/v3-maturity.test.ts +268 -0
- package/test/v3-parts-wiring.test.ts +3 -1
- package/test/v3-preview.test.ts +42 -0
- package/test/v3-properties.test.ts +4 -2
- package/test/v3-scene-canvas.test.ts +85 -0
- package/test/v3-stage-model.test.ts +25 -0
- package/test/v3-theme.test.ts +16 -25
- package/test/v3-verdict.test.ts +12 -6
- package/test/v3-view.test.ts +20 -0
- package/translations/en.json +98 -5
- package/translations/ja.json +100 -7
- package/translations/ko.json +98 -5
- package/translations/ms.json +100 -7
- package/translations/zh.json +100 -7
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { LitElement, html, css, type PropertyValues } from 'lit'
|
|
1
|
+
import { LitElement, html, css, nothing, type PropertyValues } from 'lit'
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
|
+
import type { OxThreePane } from '@operato/layout'
|
|
4
|
+
import '@operato/layout'
|
|
3
5
|
import { customElement, property, state } from 'lit/decorators.js'
|
|
4
|
-
import type { V3AuthoringAction, V3InputChange, V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
6
|
+
import type { V3Asset, V3AuthoringAction, V3InputChange, V3PartEdit, V3PrimitiveCreation } from '@hatiolab/figure-model'
|
|
5
7
|
import type { V3ProductSession } from './v3-product-session.js'
|
|
6
8
|
import { captureV3Thumbnail } from './v3-thumbnail.js'
|
|
7
9
|
import { drawableV3Parts } from './v3-renderer.js'
|
|
@@ -9,96 +11,184 @@ import './v3-canvas.js'
|
|
|
9
11
|
import './figure-v3-parts.js'
|
|
10
12
|
import './figure-v3-material.js'
|
|
11
13
|
import './figure-v3-links.js'
|
|
14
|
+
import './figure-v3-controls.js'
|
|
15
|
+
import './figure-v3-clips.js'
|
|
16
|
+
import './figure-v3-preview.js'
|
|
17
|
+
import './figure-settings.js'
|
|
18
|
+
import { DEFAULT_VIEW, type ViewSettings } from './figure-view.js'
|
|
19
|
+
import './figure-v3-capability.js'
|
|
12
20
|
import { gizmoChanges, gizmoReachOf, type GizmoReach, type GizmoResult } from './v3-gizmo.js'
|
|
21
|
+
import { keyActionOf } from './v3-keys.js'
|
|
22
|
+
import { groupFields, type FieldLike, type FieldGroup } from './v3-fields.js'
|
|
23
|
+
import { SidePanelStyles } from './side-panel-styles.js'
|
|
13
24
|
import { alignChanges, boxOf, canMirror, mirrorChanges, type AlignTool } from './v3-align.js'
|
|
14
|
-
import type { V3PartRow } from './figure-v3-parts.js'
|
|
25
|
+
import type { V3PaletteKind, V3PartRow } from './figure-v3-parts.js'
|
|
26
|
+
import { paletteRecipe } from './v3-palette.js'
|
|
15
27
|
import type { FigureV3Canvas } from './v3-canvas.js'
|
|
28
|
+
import type { V3BudgetLine } from './v3-maturity.js'
|
|
29
|
+
import { instanceRangeOf, rangeDiffers } from './v3-links.js'
|
|
30
|
+
import { isLive, livePartsOf, NOTHING_LIVE, scrubbed, scrubStep, type LiveValues } from './v3-live.js'
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
depth: '깊이',
|
|
25
|
-
radius: '반지름',
|
|
26
|
-
length: '길이',
|
|
27
|
-
round: '모서리 반경',
|
|
28
|
-
left: '왼쪽',
|
|
29
|
-
right: '오른쪽',
|
|
30
|
-
bottom: '아랫면',
|
|
31
|
-
top: '윗면',
|
|
32
|
-
openingLeft: '개구부 왼쪽',
|
|
33
|
-
openingRight: '개구부 오른쪽',
|
|
34
|
-
openingTop: '개구부 윗면'
|
|
35
|
-
}
|
|
36
|
-
function fieldLabel(id: string) {
|
|
37
|
-
return id
|
|
38
|
-
.split('.')
|
|
39
|
-
.map(p => names[p] ?? p.toUpperCase())
|
|
40
|
-
.join(' · ')
|
|
41
|
-
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The side's tabs, V2's six in V2's order where V3 has them: properties, what it does (places), motion, the verdict,
|
|
35
|
+
* the release history. A figure-level tab (verdict, history) gives way to properties when a part is chosen; a tab about
|
|
36
|
+
* parts keeps showing the newly chosen part.
|
|
37
|
+
*/
|
|
38
|
+
type SideTab = 'properties' | 'capability' | 'motion' | 'verdict' | 'view' | 'history'
|
|
42
39
|
|
|
43
40
|
/** Product editor surface. The injected session is shared by page persistence and AI commands. */
|
|
44
41
|
@customElement('figure-v3-editor')
|
|
45
42
|
export class FigureV3Editor extends LitElement {
|
|
46
|
-
static styles = css`
|
|
43
|
+
static styles = [SidePanelStyles, css`
|
|
47
44
|
:host {
|
|
48
45
|
display: flex;
|
|
49
46
|
flex-direction: column;
|
|
50
47
|
min-height: 0;
|
|
51
48
|
height: 100%;
|
|
52
|
-
/*
|
|
53
|
-
The editor folds by the width it is given, not the window's. The rail (208 open, 56 closed) and the page's
|
|
54
|
-
320 side take their share first, so a window rule was wrong whenever either was there: at 945 with the rail
|
|
55
|
-
open the editor had 417 and still drew its 660 of columns, over the side (UX ruling, 2026-09-24).
|
|
56
|
-
*/
|
|
49
|
+
/* The editor folds by the width it is given, not the window's (UX ruling, 2026-09-24). */
|
|
57
50
|
container-type: inline-size;
|
|
58
51
|
font: 13px sans-serif;
|
|
59
|
-
color: var(--md-sys-color-on-surface
|
|
52
|
+
color: var(--md-sys-color-on-surface);
|
|
53
|
+
}
|
|
54
|
+
/* The bar over the work is ox-canvas-bar (ADR-0060 decision 8): the folds at its edges, undo, redo and
|
|
55
|
+
fit in the suite's places. What the editor puts in it - the mode switch, the status, save - is styled here. */
|
|
56
|
+
ox-canvas-bar button[mode] {
|
|
57
|
+
height: 28px;
|
|
58
|
+
padding: 0 10px;
|
|
59
|
+
border-radius: 999em;
|
|
60
|
+
border-color: transparent;
|
|
61
|
+
background: none;
|
|
62
|
+
}
|
|
63
|
+
ox-canvas-bar button[mode][aria-pressed='true'] {
|
|
64
|
+
border-color: var(--md-sys-color-primary);
|
|
60
65
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
figure-v3-preview {
|
|
67
|
+
min-width: 0;
|
|
68
|
+
min-height: 0;
|
|
69
|
+
}
|
|
70
|
+
ox-canvas-bar .status {
|
|
71
|
+
padding: 0 8px;
|
|
72
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
73
|
+
}
|
|
74
|
+
ox-canvas-bar .status[dirty] {
|
|
75
|
+
color: var(--md-sys-color-primary);
|
|
76
|
+
font-weight: 600;
|
|
68
77
|
}
|
|
69
|
-
|
|
78
|
+
ox-canvas-bar button[save] {
|
|
79
|
+
height: 32px;
|
|
80
|
+
padding: 0 14px;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 999em;
|
|
83
|
+
color: var(--md-sys-color-on-primary);
|
|
84
|
+
background: var(--md-sys-color-primary);
|
|
85
|
+
}
|
|
86
|
+
/* The three panes are ox-three-pane's (ADR-0060 decision 8): widths 200 | 1fr | 340, 160 | 300 under
|
|
87
|
+
860px and one column with the middle first under 560px are the frame's, as are the dividers. */
|
|
88
|
+
ox-three-pane {
|
|
70
89
|
flex: 1;
|
|
71
90
|
min-height: 0;
|
|
72
|
-
display: grid;
|
|
73
|
-
grid-template-columns: 180px minmax(180px, 1fr) 300px;
|
|
74
91
|
}
|
|
92
|
+
figure-v3-parts {
|
|
93
|
+
min-height: 0;
|
|
94
|
+
}
|
|
95
|
+
figure-v3-canvas {
|
|
96
|
+
min-width: 0;
|
|
97
|
+
min-height: 0;
|
|
98
|
+
}
|
|
99
|
+
/* The side: tabs over one panel at a time, each using the whole height (V2's side, and why it had tabs). */
|
|
75
100
|
aside {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
display: flex;
|
|
102
|
+
flex-direction: column;
|
|
103
|
+
min-height: 0;
|
|
104
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
79
105
|
}
|
|
80
|
-
|
|
106
|
+
div[tabs] {
|
|
107
|
+
display: flex;
|
|
108
|
+
flex: none;
|
|
109
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
110
|
+
}
|
|
111
|
+
button[tab] {
|
|
112
|
+
flex: 1;
|
|
113
|
+
position: relative;
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
justify-content: center;
|
|
117
|
+
height: 38px;
|
|
118
|
+
padding: 0;
|
|
119
|
+
border: none;
|
|
120
|
+
border-radius: 0;
|
|
121
|
+
border-bottom: 2px solid transparent;
|
|
122
|
+
background: none;
|
|
123
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
124
|
+
}
|
|
125
|
+
button[tab]:hover:not([on]) {
|
|
126
|
+
background: var(--md-sys-color-surface-container);
|
|
127
|
+
}
|
|
128
|
+
button[tab][on] {
|
|
129
|
+
color: var(--md-sys-color-primary);
|
|
130
|
+
border-bottom-color: var(--md-sys-color-primary);
|
|
131
|
+
}
|
|
132
|
+
button[tab] md-icon {
|
|
133
|
+
font-size: 20px;
|
|
134
|
+
width: 20px;
|
|
135
|
+
height: 20px;
|
|
136
|
+
}
|
|
137
|
+
span[badge] {
|
|
138
|
+
position: absolute;
|
|
139
|
+
top: 3px;
|
|
140
|
+
right: 2px;
|
|
141
|
+
padding: 0 5px;
|
|
142
|
+
font-size: 0.62rem;
|
|
143
|
+
font-weight: 700;
|
|
144
|
+
line-height: 1.5;
|
|
145
|
+
color: var(--md-sys-color-on-primary-container);
|
|
146
|
+
background: var(--md-sys-color-primary-container);
|
|
147
|
+
border-radius: 999em;
|
|
148
|
+
pointer-events: none;
|
|
149
|
+
}
|
|
150
|
+
/* The sections inside carry the margins (side-panel-styles); the panel only scrolls. */
|
|
151
|
+
div[panel] {
|
|
152
|
+
flex: 1;
|
|
81
153
|
min-height: 0;
|
|
154
|
+
overflow: auto;
|
|
155
|
+
padding: 0;
|
|
82
156
|
}
|
|
83
|
-
|
|
84
|
-
|
|
157
|
+
div[panel][flush] {
|
|
158
|
+
padding: 0;
|
|
85
159
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
160
|
+
/* Hidden, not removed: a panel keeps what it holds (the verdict its last answer) while another tab is open. */
|
|
161
|
+
[off] {
|
|
162
|
+
display: none !important;
|
|
89
163
|
}
|
|
90
|
-
|
|
91
|
-
|
|
164
|
+
/* Which part the panel speaks of, and the way back to the whole figure. */
|
|
165
|
+
div[subject] {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
gap: 6px;
|
|
169
|
+
padding: 6px 10px 0;
|
|
92
170
|
}
|
|
93
|
-
button
|
|
94
|
-
|
|
95
|
-
|
|
171
|
+
div[subject] button[chip] {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 2px;
|
|
175
|
+
height: 26px;
|
|
176
|
+
padding: 0 10px 0 6px;
|
|
177
|
+
border-radius: 999em;
|
|
178
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
179
|
+
}
|
|
180
|
+
div[subject] md-icon {
|
|
181
|
+
font-size: 16px;
|
|
182
|
+
width: 16px;
|
|
183
|
+
height: 16px;
|
|
184
|
+
}
|
|
185
|
+
button {
|
|
96
186
|
font: inherit;
|
|
97
187
|
box-sizing: border-box;
|
|
98
|
-
height:
|
|
188
|
+
height: 32px;
|
|
99
189
|
border: 1px solid var(--md-sys-color-outline-variant);
|
|
100
190
|
border-radius: 6px;
|
|
101
|
-
background: var(--md-sys-color-surface
|
|
191
|
+
background: var(--md-sys-color-surface);
|
|
102
192
|
color: inherit;
|
|
103
193
|
padding: 0 8px;
|
|
104
194
|
min-width: 0;
|
|
@@ -115,104 +205,80 @@ export class FigureV3Editor extends LitElement {
|
|
|
115
205
|
color: var(--md-sys-color-on-secondary-container);
|
|
116
206
|
border-color: var(--md-sys-color-primary);
|
|
117
207
|
}
|
|
118
|
-
label[name-field] {
|
|
119
|
-
margin: 0 0 8px;
|
|
120
|
-
}
|
|
121
|
-
label[name-field] input {
|
|
122
|
-
font-weight: 600;
|
|
123
|
-
}
|
|
124
|
-
div[tools] {
|
|
125
|
-
display: flex;
|
|
126
|
-
flex-wrap: wrap;
|
|
127
|
-
gap: 4px;
|
|
128
|
-
margin: 0 0 8px;
|
|
129
|
-
}
|
|
130
|
-
div[tools] button {
|
|
131
|
-
display: inline-flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
justify-content: center;
|
|
134
|
-
width: 34px;
|
|
135
|
-
padding: 0;
|
|
136
|
-
}
|
|
137
|
-
div[tools] md-icon {
|
|
138
|
-
font-size: 18px;
|
|
139
|
-
width: 18px;
|
|
140
|
-
height: 18px;
|
|
141
|
-
}
|
|
142
|
-
.parts {
|
|
143
|
-
display: grid;
|
|
144
|
-
gap: 6px;
|
|
145
|
-
}
|
|
146
|
-
.recipes {
|
|
147
|
-
display: grid;
|
|
148
|
-
grid-template-columns: 1fr 1fr;
|
|
149
|
-
gap: 6px;
|
|
150
|
-
}
|
|
151
|
-
label {
|
|
152
|
-
display: grid;
|
|
153
|
-
grid-template-columns: 112px minmax(0, 1fr);
|
|
154
|
-
align-items: center;
|
|
155
|
-
gap: 8px;
|
|
156
|
-
margin: 8px 0;
|
|
157
|
-
}
|
|
158
|
-
label span {
|
|
159
|
-
overflow-wrap: anywhere;
|
|
160
|
-
}
|
|
161
208
|
.error {
|
|
162
209
|
padding: 10px;
|
|
163
210
|
background: var(--md-sys-color-error-container);
|
|
164
211
|
color: var(--md-sys-color-on-error-container);
|
|
165
212
|
white-space: pre-wrap;
|
|
166
213
|
}
|
|
167
|
-
.status {
|
|
168
|
-
margin-left: auto;
|
|
169
|
-
}
|
|
170
214
|
.hint {
|
|
171
215
|
color: var(--md-sys-color-on-surface-variant);
|
|
172
216
|
line-height: 1.5;
|
|
173
217
|
}
|
|
174
|
-
|
|
175
|
-
@container (max-width: 659px) {
|
|
176
|
-
main {
|
|
177
|
-
grid-template-columns: 140px minmax(160px, 1fr);
|
|
178
|
-
}
|
|
179
|
-
aside:last-child {
|
|
180
|
-
grid-column: 1/-1;
|
|
181
|
-
max-height: 260px;
|
|
182
|
-
border-left: none;
|
|
183
|
-
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/* Under 420: one column. The canvas first — seeing what is being made comes before the lists — and main scrolls. */
|
|
187
|
-
@container (max-width: 419px) {
|
|
188
|
-
main {
|
|
189
|
-
grid-template-columns: minmax(0, 1fr);
|
|
190
|
-
grid-template-rows: minmax(240px, 45%);
|
|
191
|
-
grid-auto-rows: auto;
|
|
192
|
-
overflow-y: auto;
|
|
193
|
-
}
|
|
194
|
-
figure-v3-canvas {
|
|
195
|
-
order: -1;
|
|
196
|
-
}
|
|
197
|
-
figure-v3-parts {
|
|
198
|
-
border-right: none;
|
|
199
|
-
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
200
|
-
}
|
|
201
|
-
aside:last-child {
|
|
202
|
-
max-height: none;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
`
|
|
218
|
+
`]
|
|
206
219
|
@property({ attribute: false }) session?: V3ProductSession
|
|
207
220
|
@property({ type: Number }) revision = 0
|
|
221
|
+
/** The verdict's count, drawn on its tab so it stays in sight while another tab is open (V2's badge). */
|
|
222
|
+
@property({ type: Number }) instances?: number
|
|
223
|
+
/** The cost line the parts list shows, from the maturity report. */
|
|
224
|
+
@property({ attribute: false }) budget?: V3BudgetLine
|
|
208
225
|
@state() private selected = ''
|
|
209
226
|
@state() private error = ''
|
|
227
|
+
@state() private tab: SideTab = 'properties'
|
|
228
|
+
/** The frame the bar folds - found after the first render, since the bar is drawn before it. */
|
|
229
|
+
@state() private panes?: OxThreePane
|
|
230
|
+
/** What the middle shows: the parts being edited, or the figure placed on a board at eight sizes (V2's two modes). */
|
|
231
|
+
@state() private middle: 'edit' | 'preview' = 'edit'
|
|
232
|
+
/**
|
|
233
|
+
* How the workbench shows the figure -- V2's view settings, the board's own state names. The viewer's, kept in
|
|
234
|
+
* their browser, never in the figure.
|
|
235
|
+
*/
|
|
236
|
+
@state() private view: ViewSettings = FigureV3Editor.storedView()
|
|
237
|
+
private static storedView(): ViewSettings {
|
|
238
|
+
try {
|
|
239
|
+
const stored = JSON.parse(localStorage.getItem('figure-v3-view') ?? 'null')
|
|
240
|
+
return stored && typeof stored === 'object' ? { ...DEFAULT_VIEW, ...stored } : { ...DEFAULT_VIEW }
|
|
241
|
+
} catch {
|
|
242
|
+
return { ...DEFAULT_VIEW }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
private setView(view: ViewSettings) {
|
|
246
|
+
this.view = { ...DEFAULT_VIEW, ...view }
|
|
247
|
+
try {
|
|
248
|
+
localStorage.setItem('figure-v3-view', JSON.stringify(this.view))
|
|
249
|
+
} catch {
|
|
250
|
+
/* A browser that keeps nothing still shows the change for this visit. */
|
|
251
|
+
}
|
|
252
|
+
}
|
|
210
253
|
/**
|
|
211
254
|
* Parts hidden on the workbench — the author's view while working, not the asset's visibility. Not saved, as in
|
|
212
255
|
* the V2 modeller: a part hidden here is still in the figure, still checked, still drawn on a board.
|
|
213
256
|
*/
|
|
214
257
|
@state() private hiddenParts = new Set<string>()
|
|
258
|
+
/** A value in hand -- typed, held on a spinner, scrubbed, dragged -- drawn at once and recorded on release. */
|
|
259
|
+
@state() private live: LiveValues = NOTHING_LIVE
|
|
215
260
|
private lastContext: string | null = null
|
|
261
|
+
/** The modeller's keys (`v3-keys.ts`): undo, redo, duplicate, remove, back to the whole figure. */
|
|
262
|
+
private onKey = (e: KeyboardEvent) => {
|
|
263
|
+
const view = this.session?.read()
|
|
264
|
+
if (!view || view.loading || view.saving || !view.context || !this.isConnected) return
|
|
265
|
+
const action = keyActionOf({ key: e.key, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey, altKey: e.altKey, target: e.composedPath()[0] as HTMLElement }, this.selected)
|
|
266
|
+
if (!action) return
|
|
267
|
+
e.preventDefault()
|
|
268
|
+
if (action === 'undo') view.canUndo && this.run(() => this.session!.undo())
|
|
269
|
+
else if (action === 'redo') view.canRedo && this.run(() => this.session!.redo())
|
|
270
|
+
else if (action === 'duplicate') this.duplicatePart(this.selected)
|
|
271
|
+
else if (action === 'remove') this.removePart(this.selected)
|
|
272
|
+
else this.selected = ''
|
|
273
|
+
}
|
|
274
|
+
connectedCallback() {
|
|
275
|
+
super.connectedCallback()
|
|
276
|
+
window.addEventListener('keydown', this.onKey)
|
|
277
|
+
}
|
|
278
|
+
disconnectedCallback() {
|
|
279
|
+
window.removeEventListener('keydown', this.onKey)
|
|
280
|
+
super.disconnectedCallback()
|
|
281
|
+
}
|
|
216
282
|
protected willUpdate(_changed: PropertyValues) {
|
|
217
283
|
const view = this.session?.read()
|
|
218
284
|
if (view?.context !== this.lastContext) {
|
|
@@ -230,8 +296,20 @@ export class FigureV3Editor extends LitElement {
|
|
|
230
296
|
this.changed()
|
|
231
297
|
} catch (error) {
|
|
232
298
|
this.error = error instanceof Error ? error.message : String(error)
|
|
299
|
+
} finally {
|
|
300
|
+
/* Committed or refused, the value in hand is gone: the figure as it stands is drawn again. */
|
|
301
|
+
this.live = NOTHING_LIVE
|
|
233
302
|
}
|
|
234
303
|
}
|
|
304
|
+
/** What the canvas draws: the figure with the value in hand when there is one, and without the parts hidden on the workbench. */
|
|
305
|
+
private drawnParts(geometry: Parameters<typeof drawableV3Parts>[0] | undefined, asset: unknown) {
|
|
306
|
+
const parts = (isLive(this.live) && asset ? livePartsOf(asset as V3Asset, this.live) : null) ?? drawableV3Parts(geometry ?? [])
|
|
307
|
+
return parts.filter(body => !this.hiddenParts.has((body as { id: string }).id))
|
|
308
|
+
}
|
|
309
|
+
/** A value in hand: an input's value (design or pose) or a part edit. Nothing is recorded. */
|
|
310
|
+
private hold(values: Record<string, number>, edit?: V3PartEdit) {
|
|
311
|
+
this.live = { values: { ...this.live.values, ...values }, ...(edit ? { edit } : this.live.edit ? { edit: this.live.edit } : {}) }
|
|
312
|
+
}
|
|
235
313
|
private changed() {
|
|
236
314
|
this.requestUpdate()
|
|
237
315
|
this.dispatchEvent(new CustomEvent('v3-changed', { bubbles: true, composed: true }))
|
|
@@ -256,35 +334,13 @@ export class FigureV3Editor extends LitElement {
|
|
|
256
334
|
this.changed()
|
|
257
335
|
}
|
|
258
336
|
}
|
|
259
|
-
private add(kind:
|
|
337
|
+
private add(kind: V3PaletteKind) {
|
|
260
338
|
if (!this.session) return
|
|
261
339
|
this.run(() => {
|
|
262
340
|
const ids = new Set(this.session!.read().geometry.map(g => g.id))
|
|
263
341
|
let n = 1
|
|
264
342
|
while (ids.has(`${kind}-${n}`)) n++
|
|
265
|
-
const
|
|
266
|
-
const part: V3PrimitiveCreation =
|
|
267
|
-
kind === 'box'
|
|
268
|
-
? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100 } }
|
|
269
|
-
: kind === 'cylinder'
|
|
270
|
-
? { ...common, kind, dimensions: { radius: 50, length: 100 } }
|
|
271
|
-
: kind === 'rounded-box'
|
|
272
|
-
? { ...common, kind, dimensions: { width: 100, height: 100, depth: 100, round: 10 } }
|
|
273
|
-
: {
|
|
274
|
-
...common,
|
|
275
|
-
position: { x: 0, y: 0, z: 0 },
|
|
276
|
-
kind: 'portal',
|
|
277
|
-
dimensions: {
|
|
278
|
-
left: -100,
|
|
279
|
-
right: 100,
|
|
280
|
-
bottom: 0,
|
|
281
|
-
top: 200,
|
|
282
|
-
openingLeft: -70,
|
|
283
|
-
openingRight: 70,
|
|
284
|
-
openingTop: 170,
|
|
285
|
-
depth: 30
|
|
286
|
-
}
|
|
287
|
-
}
|
|
343
|
+
const part = paletteRecipe(kind, `${kind}-${n}`)
|
|
288
344
|
this.session!.add(part)
|
|
289
345
|
this.selected = part.id
|
|
290
346
|
})
|
|
@@ -422,7 +478,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
422
478
|
}
|
|
423
479
|
}
|
|
424
480
|
/** Size response, fastening and motion for the selected part (parity ④ ⑤ ⑥), as authoring commands. */
|
|
425
|
-
private renderLinks(unavailable: boolean) {
|
|
481
|
+
private renderLinks(unavailable: boolean, section: 'shape' | 'places' | 'motion') {
|
|
426
482
|
let asset
|
|
427
483
|
try {
|
|
428
484
|
asset = JSON.parse(this.session!.source())
|
|
@@ -437,12 +493,40 @@ export class FigureV3Editor extends LitElement {
|
|
|
437
493
|
}
|
|
438
494
|
return html`<figure-v3-links
|
|
439
495
|
.asset=${asset}
|
|
496
|
+
.section=${section}
|
|
440
497
|
.partId=${this.selected}
|
|
441
498
|
.dims=${dims}
|
|
442
499
|
?locked=${unavailable}
|
|
443
500
|
@v3-command=${(e: CustomEvent) => this.linkCommand(e.detail.action)}
|
|
501
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
444
502
|
></figure-v3-links>`
|
|
445
503
|
}
|
|
504
|
+
/** The figure's controls: try a value, name it, set its range and travel time, keep the pose (parity row ⑥). */
|
|
505
|
+
private renderControls(view: { fields: Array<{ id: string; role?: string; value: number }> }, unavailable: boolean) {
|
|
506
|
+
let asset
|
|
507
|
+
try {
|
|
508
|
+
asset = JSON.parse(this.session!.source())
|
|
509
|
+
} catch {
|
|
510
|
+
return null
|
|
511
|
+
}
|
|
512
|
+
const values = Object.fromEntries(view.fields.filter(f => f.role === 'state').map(f => [f.id, f.value]))
|
|
513
|
+
return html`<figure-v3-controls
|
|
514
|
+
.asset=${asset}
|
|
515
|
+
.values=${values}
|
|
516
|
+
?locked=${unavailable}
|
|
517
|
+
@v3-state=${(e: CustomEvent) => this.run(() => this.session!.apply([{ kind: 'set-state', input: e.detail.input, unit: e.detail.unit, value: e.detail.value }]))}
|
|
518
|
+
@v3-state-live=${(e: CustomEvent) => this.hold({ [e.detail.input]: e.detail.value })}
|
|
519
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
520
|
+
></figure-v3-controls>
|
|
521
|
+
<figure-v3-clips
|
|
522
|
+
.asset=${asset}
|
|
523
|
+
.values=${values}
|
|
524
|
+
?locked=${unavailable}
|
|
525
|
+
@v3-states=${(e: CustomEvent) => this.run(() => this.session!.apply(e.detail.changes))}
|
|
526
|
+
@v3-states-live=${(e: CustomEvent) => this.hold(e.detail.values)}
|
|
527
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
528
|
+
></figure-v3-clips>`
|
|
529
|
+
}
|
|
446
530
|
/** One command, one step of the history. A refusal a person can meet here is said in their words. */
|
|
447
531
|
private linkCommand(action: V3AuthoringAction) {
|
|
448
532
|
try {
|
|
@@ -472,34 +556,164 @@ export class FigureV3Editor extends LitElement {
|
|
|
472
556
|
// Rejected edits must also restore the displayed numeric value.
|
|
473
557
|
input.value = String(this.session!.read().fields.find(i => i.id === field.id)!.value)
|
|
474
558
|
}
|
|
559
|
+
/** Typing or a held spinner: each number on the way is drawn; the field's change records it. */
|
|
560
|
+
private typing(field: { id: string }, event: Event) {
|
|
561
|
+
const value = (event.target as HTMLInputElement).valueAsNumber
|
|
562
|
+
if (Number.isFinite(value)) this.hold({ [field.id]: value })
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Drag a field's label (or an axis letter) sideways to scrub its value, as vector tools do: the figure follows the
|
|
566
|
+
* pointer, and the release records one step. The field's number follows too, so the value in hand is always read.
|
|
567
|
+
*/
|
|
568
|
+
private scrub(field: { id: string; unit: V3InputChange['unit']; role?: string; value: number }, down: PointerEvent, unavailable: boolean) {
|
|
569
|
+
if (unavailable || down.button !== 0) return
|
|
570
|
+
const label = down.currentTarget as HTMLElement
|
|
571
|
+
const input = (label.closest('div[cell]')?.querySelector('input') ?? label.nextElementSibling) as HTMLInputElement | null
|
|
572
|
+
if (!input) return
|
|
573
|
+
down.preventDefault()
|
|
574
|
+
label.setPointerCapture(down.pointerId)
|
|
575
|
+
const start = field.value
|
|
576
|
+
let value = start
|
|
577
|
+
const move = (e: PointerEvent) => {
|
|
578
|
+
value = scrubbed(start, e.clientX - down.clientX, scrubStep(field.unit, start, e.shiftKey))
|
|
579
|
+
input.value = String(value)
|
|
580
|
+
this.hold({ [field.id]: value })
|
|
581
|
+
}
|
|
582
|
+
const up = (e: PointerEvent) => {
|
|
583
|
+
label.removeEventListener('pointermove', move)
|
|
584
|
+
label.removeEventListener('pointerup', up)
|
|
585
|
+
label.removeEventListener('pointercancel', up)
|
|
586
|
+
/* A drag the system took away is not the author's value: the field goes back to where it was. */
|
|
587
|
+
if (e.type === 'pointercancel') value = start
|
|
588
|
+
if (value === start) {
|
|
589
|
+
input.value = String(start)
|
|
590
|
+
this.live = NOTHING_LIVE
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
input.value = String(value)
|
|
594
|
+
this.changeField(field, { target: input } as unknown as Event)
|
|
595
|
+
}
|
|
596
|
+
label.addEventListener('pointermove', move)
|
|
597
|
+
label.addEventListener('pointerup', up)
|
|
598
|
+
label.addEventListener('pointercancel', up)
|
|
599
|
+
}
|
|
600
|
+
/** The measured range, kept for the revision it was measured at: measuring evaluates the figure at its edges. */
|
|
601
|
+
private range?: { at: string; of: ReturnType<typeof instanceRangeOf> }
|
|
602
|
+
/** The sizes a board may give the figure, and the way to measure them again after its parts changed. */
|
|
603
|
+
private renderInstanceRange(asset: V3Asset | undefined, unavailable: boolean) {
|
|
604
|
+
if (!asset) return nothing
|
|
605
|
+
/* Another figure, or another step of this one, is measured again. */
|
|
606
|
+
const at = `${this.session?.read().context ?? ''}:${this.revision}`
|
|
607
|
+
if (this.range?.at !== at) {
|
|
608
|
+
let of: ReturnType<typeof instanceRangeOf>
|
|
609
|
+
try {
|
|
610
|
+
of = instanceRangeOf(asset)
|
|
611
|
+
} catch {
|
|
612
|
+
of = undefined
|
|
613
|
+
}
|
|
614
|
+
this.range = { at, of }
|
|
615
|
+
}
|
|
616
|
+
const range = this.range.of
|
|
617
|
+
if (!range) return nothing
|
|
618
|
+
const mm = (v: number) => Math.round(v).toLocaleString()
|
|
619
|
+
const differs = rangeDiffers(range.declared, range.measured)
|
|
620
|
+
return html`<section>
|
|
621
|
+
<h3>${i18next.t('figure.label.instance-range')}</h3>
|
|
622
|
+
${(['x', 'y', 'z'] as const).map(
|
|
623
|
+
axis => html`<div inline-field>
|
|
624
|
+
<label>${axis.toUpperCase()}</label><span>${mm(range.declared[axis].min)} – ${mm(range.declared[axis].max)} mm</span>
|
|
625
|
+
</div>`
|
|
626
|
+
)}
|
|
627
|
+
${differs
|
|
628
|
+
? html`<p hint>
|
|
629
|
+
${i18next.t('figure.text.instance-range-can-widen', {
|
|
630
|
+
ranges: (['x', 'y', 'z'] as const)
|
|
631
|
+
.filter(axis => rangeDiffers(range.declared, { ...range.declared, [axis]: range.measured[axis] }))
|
|
632
|
+
.map(axis => `${axis.toUpperCase()} ${mm(range.measured[axis].min)} – ${mm(range.measured[axis].max)}`)
|
|
633
|
+
.join(' · ')
|
|
634
|
+
})}
|
|
635
|
+
</p>
|
|
636
|
+
<button act ?disabled=${unavailable} @click=${() => this.linkCommand({ kind: 'declare-instance-size', range: range.measured, replace: true })}>
|
|
637
|
+
<md-icon>straighten</md-icon>${i18next.t('figure.button.remeasure-range')}
|
|
638
|
+
</button>`
|
|
639
|
+
: nothing}
|
|
640
|
+
</section>`
|
|
641
|
+
}
|
|
642
|
+
/** Choose a part, from the list, the canvas or a place row. A figure-level tab gives way to the part's properties. */
|
|
643
|
+
private choose(id: string) {
|
|
644
|
+
this.selected = id
|
|
645
|
+
if (id && (this.tab === 'verdict' || this.tab === 'view' || this.tab === 'history')) this.tab = 'properties'
|
|
646
|
+
}
|
|
647
|
+
private asset(): unknown {
|
|
648
|
+
try {
|
|
649
|
+
return JSON.parse(this.session!.source())
|
|
650
|
+
} catch {
|
|
651
|
+
return undefined
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
private renderTab(tab: SideTab, icon: string, label: string, badge?: number) {
|
|
655
|
+
return html`<button tab ?on=${this.tab === tab} title=${label} aria-label=${label} @click=${() => (this.tab = tab)}>
|
|
656
|
+
<md-icon>${icon}</md-icon>${badge == null ? '' : html`<span badge>${badge.toLocaleString()}</span>`}
|
|
657
|
+
</button>`
|
|
658
|
+
}
|
|
659
|
+
updated(changed: PropertyValues<this>) {
|
|
660
|
+
super.updated?.(changed)
|
|
661
|
+
const panes = this.renderRoot.querySelector<OxThreePane>('ox-three-pane') ?? undefined
|
|
662
|
+
if (panes !== this.panes) this.panes = panes
|
|
663
|
+
}
|
|
664
|
+
/** Which part the panel speaks of, with the way back to the whole figure. */
|
|
665
|
+
private renderSubject() {
|
|
666
|
+
if (!this.selected) return null
|
|
667
|
+
return html`<div subject>
|
|
668
|
+
<button chip title=${i18next.t('figure.button.whole-figure')} @click=${() => (this.selected = '')}>
|
|
669
|
+
<md-icon>arrow_back</md-icon>${i18next.t('figure.button.whole-figure')}
|
|
670
|
+
</button>
|
|
671
|
+
</div>`
|
|
672
|
+
}
|
|
475
673
|
render() {
|
|
476
674
|
const view = this.session?.read()
|
|
477
|
-
if (!view) return html`<p
|
|
675
|
+
if (!view) return html`<p>${i18next.t('figure.text.opening-model')}</p>`
|
|
478
676
|
const unavailable = view.loading || !!view.error || view.saving || !view.context
|
|
479
677
|
const part = view.geometry.find(g => g.id === this.selected)
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
678
|
+
/* The controls (state inputs) have their own place in the motion tab; these are the design values. */
|
|
679
|
+
const design = view.fields.filter(i => i.role !== 'state')
|
|
680
|
+
const own = design.filter(i => this.selected && i.id.startsWith(this.selected + '.'))
|
|
681
|
+
const common = design.filter(i => !view.geometry.some(g => i.id.startsWith(g.id + '.')))
|
|
682
|
+
const status = view.loading
|
|
683
|
+
? 'figure.text.loading-figure'
|
|
684
|
+
: view.saving
|
|
685
|
+
? 'figure.text.saving-figure'
|
|
686
|
+
: view.dirty || !view.id
|
|
687
|
+
? 'figure.text.unsaved-changes'
|
|
688
|
+
: 'figure.text.saved'
|
|
689
|
+
const asset = this.asset()
|
|
690
|
+
const tab = this.tab
|
|
691
|
+
return html`<ox-canvas-bar
|
|
692
|
+
history
|
|
693
|
+
fit
|
|
694
|
+
undo-shortcut="Ctrl+Z"
|
|
695
|
+
redo-shortcut="Ctrl+Y"
|
|
696
|
+
?can-undo=${!unavailable && view.canUndo}
|
|
697
|
+
?can-redo=${!unavailable && view.canRedo}
|
|
698
|
+
.pane=${this.panes}
|
|
699
|
+
@canvas-undo=${() => this.run(() => this.session!.undo())}
|
|
700
|
+
@canvas-redo=${() => this.run(() => this.session!.redo())}
|
|
701
|
+
@canvas-fit=${() => this.renderRoot.querySelector<FigureV3Canvas>('figure-v3-canvas')?.fit()}
|
|
702
|
+
>
|
|
703
|
+
${(['edit', 'preview'] as const).map(
|
|
704
|
+
mode => html`<button slot="mode" mode aria-pressed=${String(this.middle === mode)} title=${i18next.t('figure.text.mode-' + mode + '-explained')} @click=${() => (this.middle = mode)}>
|
|
705
|
+
${i18next.t('figure.label.mode-' + mode)}
|
|
706
|
+
</button>`
|
|
707
|
+
)}
|
|
708
|
+
<span slot="status" class="status" role="status" ?dirty=${status === 'figure.text.unsaved-changes'}>${i18next.t(status)}</span>
|
|
709
|
+
<button slot="commit" save ?disabled=${unavailable || (!view.dirty && !!view.id)} @click=${this.save}>${i18next.t('figure.button.save')}</button>
|
|
710
|
+
</ox-canvas-bar>
|
|
500
711
|
${this.error || view.error ? html`<div role="alert" class="error">${this.error || view.error}</div>` : null}
|
|
501
|
-
<
|
|
712
|
+
<ox-three-pane>
|
|
502
713
|
<figure-v3-parts
|
|
714
|
+
slot="start"
|
|
715
|
+
.budget=${this.budget}
|
|
716
|
+
@show-maturity=${() => (this.tab = 'verdict')}
|
|
503
717
|
.parts=${view.geometry.map(
|
|
504
718
|
(g): V3PartRow => ({
|
|
505
719
|
id: g.id,
|
|
@@ -511,7 +725,7 @@ export class FigureV3Editor extends LitElement {
|
|
|
511
725
|
.selected=${this.selected}
|
|
512
726
|
?locked=${unavailable}
|
|
513
727
|
@add-part=${(e: CustomEvent) => this.add(e.detail.kind)}
|
|
514
|
-
@select-part=${(e: CustomEvent) =>
|
|
728
|
+
@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
|
|
515
729
|
@toggle-part-hidden=${(e: CustomEvent) => this.toggleHiddenPart(e.detail.id)}
|
|
516
730
|
@set-all-parts-hidden=${(e: CustomEvent) =>
|
|
517
731
|
(this.hiddenParts = e.detail.hidden ? new Set(view.geometry.map(g => g.id)) : new Set())}
|
|
@@ -520,50 +734,145 @@ export class FigureV3Editor extends LitElement {
|
|
|
520
734
|
@duplicate-part=${(e: CustomEvent) => this.duplicatePart(e.detail.id)}
|
|
521
735
|
@remove-part=${(e: CustomEvent) => this.removePart(e.detail.id)}
|
|
522
736
|
></figure-v3-parts>
|
|
523
|
-
|
|
524
|
-
|
|
737
|
+
${this.middle === 'preview'
|
|
738
|
+
? html`<figure-v3-preview
|
|
739
|
+
.asset=${asset}
|
|
740
|
+
.context=${view.context ?? ''}
|
|
741
|
+
.view=${this.view}
|
|
742
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
743
|
+
></figure-v3-preview>`
|
|
744
|
+
: html`<figure-v3-canvas
|
|
745
|
+
.view=${this.view}
|
|
746
|
+
.parts=${this.drawnParts(view.geometry, asset)}
|
|
525
747
|
.palette=${view.palette ?? {}}
|
|
526
748
|
.context=${view.context ?? ''}
|
|
527
749
|
.selected=${this.selected}
|
|
528
750
|
.reach=${unavailable ? { translate: false, rotate: false, scale: false } : this.reachOf(this.selected)}
|
|
529
751
|
@part-transformed=${(e: CustomEvent) => this.transformed(e.detail.id, e.detail.result)}
|
|
530
|
-
@part-selected=${(e: CustomEvent<string>) =>
|
|
531
|
-
this.selected = e.detail
|
|
532
|
-
}}
|
|
752
|
+
@part-selected=${(e: CustomEvent<string>) => this.choose(e.detail)}
|
|
533
753
|
@render-error=${(e: CustomEvent<Error>) => {
|
|
534
754
|
this.error = e.detail.message
|
|
535
755
|
}}
|
|
536
|
-
></figure-v3-canvas
|
|
537
|
-
<aside>
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
${
|
|
564
|
-
|
|
565
|
-
|
|
756
|
+
></figure-v3-canvas>`}
|
|
757
|
+
<aside slot="end">
|
|
758
|
+
<div tabs>
|
|
759
|
+
${this.renderTab('properties', 'tune', i18next.t('figure.label.properties'))}
|
|
760
|
+
${this.renderTab('capability', 'conveyor_belt', i18next.t('figure.label.tab-capability'))}
|
|
761
|
+
${this.renderTab('motion', 'animation', i18next.t('figure.label.motion'))}
|
|
762
|
+
${this.renderTab('verdict', 'insights', i18next.t('figure.label.maturity'), this.instances)}
|
|
763
|
+
${this.renderTab('view', 'wb_sunny', i18next.t('figure.label.tab-environment'))}
|
|
764
|
+
${this.renderTab('history', 'history', i18next.t('figure.label.history'))}
|
|
765
|
+
</div>
|
|
766
|
+
<slot name="notice"></slot>
|
|
767
|
+
<div panel ?off=${tab !== 'properties'}>
|
|
768
|
+
${this.selected && part
|
|
769
|
+
? html`${this.renderSubject()}
|
|
770
|
+
<section>
|
|
771
|
+
<h3>${i18next.t('figure.label.group-part')}</h3>
|
|
772
|
+
<div inline-field>
|
|
773
|
+
<label>${i18next.t('figure.label.name')}</label>
|
|
774
|
+
<input
|
|
775
|
+
name-field
|
|
776
|
+
aria-label=${i18next.t('figure.label.name')}
|
|
777
|
+
.value=${this.selected}
|
|
778
|
+
?disabled=${unavailable}
|
|
779
|
+
@change=${(e: Event) => this.renamePart(e.target as HTMLInputElement)}
|
|
780
|
+
/>
|
|
781
|
+
</div>
|
|
782
|
+
</section>
|
|
783
|
+
${this.renderGroups(groupFields(design, this.selected), 'figure.label.group-placement-size', 'figure.label.group-shape', unavailable, this.renderTools(unavailable))}
|
|
784
|
+
${this.renderLinks(unavailable, 'shape')}
|
|
785
|
+
<figure-v3-material
|
|
786
|
+
.body=${part}
|
|
787
|
+
.palette=${view.palette ?? {}}
|
|
788
|
+
?locked=${unavailable}
|
|
789
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
790
|
+
@v3-part-live=${(e: CustomEvent) => this.hold({}, e.detail.edit)}
|
|
791
|
+
></figure-v3-material>`
|
|
792
|
+
: html`<slot name="figure"></slot>
|
|
793
|
+
${this.renderGroups(groupFields(common), 'figure.label.group-figure-size', 'figure.label.design-inputs', unavailable)}
|
|
794
|
+
${this.renderInstanceRange(asset as V3Asset | undefined, unavailable)}`}
|
|
795
|
+
</div>
|
|
796
|
+
<div panel ?off=${tab !== 'capability'}>
|
|
797
|
+
${this.renderSubject()}
|
|
798
|
+
${this.selected && part ? this.renderLinks(unavailable, 'places') : null}
|
|
799
|
+
<figure-v3-capability
|
|
800
|
+
.asset=${asset}
|
|
801
|
+
.selected=${this.selected}
|
|
802
|
+
?locked=${unavailable}
|
|
803
|
+
@select-part=${(e: CustomEvent) => this.choose(e.detail.id)}
|
|
804
|
+
@v3-part-edit=${(e: CustomEvent) => this.run(() => this.session!.edit(e.detail.edit))}
|
|
805
|
+
></figure-v3-capability>
|
|
806
|
+
</div>
|
|
807
|
+
<div panel ?off=${tab !== 'motion'}>
|
|
808
|
+
${this.renderSubject()}
|
|
809
|
+
${this.selected && part ? this.renderLinks(unavailable, 'motion') : html`<section><p hint>${i18next.t('figure.text.choose-a-part-to-move')}</p></section>`}
|
|
810
|
+
${this.renderControls(view, unavailable)}
|
|
811
|
+
</div>
|
|
812
|
+
<div panel flush ?off=${tab !== 'verdict'}><slot name="verdict"></slot></div>
|
|
813
|
+
<div panel flush ?off=${tab !== 'view'}>
|
|
814
|
+
<figure-settings .view=${this.view} @update-view=${(e: CustomEvent) => this.setView(e.detail.view)}></figure-settings>
|
|
815
|
+
</div>
|
|
816
|
+
<div panel flush ?off=${tab !== 'history'}><slot name="history"></slot></div>
|
|
566
817
|
</aside>
|
|
567
|
-
</
|
|
818
|
+
</ox-three-pane>`
|
|
819
|
+
}
|
|
820
|
+
/** Position, rotation and size as rows of three cells; the shape's own measures one to a line, under their title. */
|
|
821
|
+
private renderGroups(
|
|
822
|
+
groups: { vectors: FieldGroup[]; singles: FieldGroup[] },
|
|
823
|
+
vectorTitle: string,
|
|
824
|
+
singleTitle: string,
|
|
825
|
+
unavailable: boolean,
|
|
826
|
+
tools: unknown = null
|
|
827
|
+
) {
|
|
828
|
+
return html`${groups.vectors.length || tools
|
|
829
|
+
? html`<section>
|
|
830
|
+
<h3>${i18next.t(vectorTitle)}</h3>
|
|
831
|
+
${groups.vectors.map(g => this.renderVector(g, unavailable))}
|
|
832
|
+
${tools ? html`<div vector-row><label>${i18next.t('figure.label.tools')}</label>${tools}</div>` : nothing}
|
|
833
|
+
</section>`
|
|
834
|
+
: nothing}
|
|
835
|
+
${groups.singles.length
|
|
836
|
+
? html`<section>
|
|
837
|
+
<h3>${i18next.t(singleTitle)}</h3>
|
|
838
|
+
<div form>${groups.singles.map(g => (g.kind === 'single' ? this.renderSingle(g.name, g.field, unavailable) : nothing))}</div>
|
|
839
|
+
</section>`
|
|
840
|
+
: nothing}`
|
|
841
|
+
}
|
|
842
|
+
private renderVector(group: FieldGroup, unavailable: boolean) {
|
|
843
|
+
if (group.kind !== 'vector') return nothing
|
|
844
|
+
const unit = group.cells.x.unit === 'deg' ? '°' : group.cells.x.unit
|
|
845
|
+
return html`<div vector-row>
|
|
846
|
+
<label>${i18next.t('figure.label.group-' + group.name)}${group.name === 'rotation' ? '' : html` <span unit>${unit}</span>`}</label>
|
|
847
|
+
<div triple>
|
|
848
|
+
${(['x', 'y', 'z'] as const).map(
|
|
849
|
+
axis => html`<div cell>
|
|
850
|
+
<span axis ?scrub=${!unavailable} @pointerdown=${(e: PointerEvent) => this.scrub(group.cells[axis] as never, e, unavailable)}>${axis.toUpperCase()}</span>
|
|
851
|
+
<input
|
|
852
|
+
aria-label=${group.cells[axis].id}
|
|
853
|
+
type="number"
|
|
854
|
+
step="any"
|
|
855
|
+
.value=${String(group.cells[axis].value)}
|
|
856
|
+
?disabled=${unavailable}
|
|
857
|
+
@input=${(e: Event) => this.typing(group.cells[axis] as never, e)}
|
|
858
|
+
@change=${(e: Event) => this.changeField(group.cells[axis] as never, e)}
|
|
859
|
+
/>
|
|
860
|
+
</div>`
|
|
861
|
+
)}
|
|
862
|
+
</div>
|
|
863
|
+
</div>`
|
|
864
|
+
}
|
|
865
|
+
private renderSingle(name: string, field: FieldLike, unavailable: boolean) {
|
|
866
|
+
const words = name.split('.').map(part => i18next.t('figure.label.field-' + part, { defaultValue: part }))
|
|
867
|
+
return html`<span ?scrub=${!unavailable} @pointerdown=${(e: PointerEvent) => this.scrub(field as never, e, unavailable)}>${words.join(' · ')} (${field.unit})</span
|
|
868
|
+
><input
|
|
869
|
+
aria-label=${field.id}
|
|
870
|
+
type="number"
|
|
871
|
+
step="any"
|
|
872
|
+
.value=${String(field.value)}
|
|
873
|
+
?disabled=${unavailable}
|
|
874
|
+
@input=${(e: Event) => this.typing(field as never, e)}
|
|
875
|
+
@change=${(e: Event) => this.changeField(field as never, e)}
|
|
876
|
+
/>`
|
|
568
877
|
}
|
|
569
878
|
}
|