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