@womp/mesh-editor 0.1.0
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/README.md +33 -0
- package/dist/browser/src/mesh_editor/editor.d.ts +603 -0
- package/dist/browser/src/mesh_editor/editor.js +7297 -0
- package/dist/browser/src/mesh_editor/mesh_data.d.ts +14 -0
- package/dist/browser/src/mesh_editor/mesh_data.js +543 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_worker.d.ts +36 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_worker.js +83 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_workers.d.ts +27 -0
- package/dist/browser/src/mesh_editor/sculpt_normal_workers.js +138 -0
- package/dist/browser/src/mesh_editor/three_geometry_adapter.d.ts +6 -0
- package/dist/browser/src/mesh_editor/three_geometry_adapter.js +334 -0
- package/dist/browser/src/mesh_editor/types.d.ts +44 -0
- package/dist/browser/src/mesh_editor/types.js +1 -0
- package/dist/browser/src/session/edit_session.d.ts +23 -0
- package/dist/browser/src/session/edit_session.js +1472 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1472 @@
|
|
|
1
|
+
import * as DEFAULT_THREE from "three";
|
|
2
|
+
import { PortableMeshEditor } from "../mesh_editor/editor.js";
|
|
3
|
+
const DEFAULT_TOOL_STRENGTH = 0.35;
|
|
4
|
+
const DEFAULT_PRIMARY_SCULPT_STRENGTH = 1.0;
|
|
5
|
+
const PRIMARY_SCULPT_TOOLS = new Set(["clay", "brush", "pore", "spiral", "rake", "scale", "crumple"]);
|
|
6
|
+
function default_strength_for_tool(tool) {
|
|
7
|
+
return PRIMARY_SCULPT_TOOLS.has(tool)
|
|
8
|
+
? DEFAULT_PRIMARY_SCULPT_STRENGTH
|
|
9
|
+
: DEFAULT_TOOL_STRENGTH;
|
|
10
|
+
}
|
|
11
|
+
function add_styles_once() {
|
|
12
|
+
if (document.getElementById("womp-mesh-editor-edit-styles") !== null) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const style = document.createElement("style");
|
|
16
|
+
style.id = "womp-mesh-editor-edit-styles";
|
|
17
|
+
style.textContent = `
|
|
18
|
+
.womp-mesh-editor-modal.womp-mesh-editor-edit-root {
|
|
19
|
+
--editor-bg: var(--womp-mesh-editor-color-canvas, #0d1117);
|
|
20
|
+
--editor-panel: var(--womp-mesh-editor-color-panel, #151b23);
|
|
21
|
+
--editor-panel-raised: var(--womp-mesh-editor-color-surface, #1b232d);
|
|
22
|
+
--editor-border: var(--womp-mesh-editor-color-border, #2b3542);
|
|
23
|
+
--editor-border-strong: var(--womp-mesh-editor-color-border-strong, #3b4858);
|
|
24
|
+
--editor-text: var(--womp-mesh-editor-color-text, #edf2f7);
|
|
25
|
+
--editor-muted: var(--womp-mesh-editor-color-muted, #8d9aaa);
|
|
26
|
+
--editor-accent: var(--womp-mesh-editor-color-accent, #69a5ff);
|
|
27
|
+
--editor-accent-bg: var(--womp-mesh-editor-color-accent-surface, #203f68);
|
|
28
|
+
--editor-success: var(--womp-mesh-editor-color-success, #68c995);
|
|
29
|
+
--editor-danger: var(--womp-mesh-editor-color-danger, #f08e96);
|
|
30
|
+
--editor-radius-control: var(--womp-mesh-editor-radius-control, 6px);
|
|
31
|
+
--editor-space-1: var(--womp-mesh-editor-space-1, 4px);
|
|
32
|
+
--editor-space-2: var(--womp-mesh-editor-space-2, 8px);
|
|
33
|
+
--editor-space-3: var(--womp-mesh-editor-space-3, 12px);
|
|
34
|
+
--editor-space-4: var(--womp-mesh-editor-space-4, 16px);
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset: 0;
|
|
37
|
+
z-index: var(--womp-mesh-editor-z-index, 100000);
|
|
38
|
+
display: grid;
|
|
39
|
+
grid-template-areas:
|
|
40
|
+
"header header header"
|
|
41
|
+
"controls viewport tools"
|
|
42
|
+
"footer footer footer";
|
|
43
|
+
grid-template-columns: var(--womp-mesh-editor-panel-width, 248px) minmax(0, 1fr) 112px;
|
|
44
|
+
grid-template-rows: 54px minmax(0, 1fr) 52px;
|
|
45
|
+
background: var(--editor-bg);
|
|
46
|
+
color: var(--editor-text);
|
|
47
|
+
font-family: var(--womp-mesh-editor-font-family, Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
|
|
48
|
+
font-size: var(--womp-mesh-editor-font-size, 13px);
|
|
49
|
+
line-height: var(--womp-mesh-editor-line-height, 1.35);
|
|
50
|
+
color-scheme: dark;
|
|
51
|
+
}
|
|
52
|
+
.womp-mesh-editor-edit-root [hidden] { display: none !important; }
|
|
53
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header {
|
|
54
|
+
grid-area: header;
|
|
55
|
+
z-index: 2;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: space-between;
|
|
59
|
+
gap: 16px;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
padding: 0 12px 0 16px;
|
|
62
|
+
border-bottom: 1px solid var(--editor-border);
|
|
63
|
+
background: #121821;
|
|
64
|
+
box-shadow: 0 3px 16px rgb(0 0 0 / 18%);
|
|
65
|
+
}
|
|
66
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-heading { min-width: 0; }
|
|
67
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-heading-row {
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 9px;
|
|
71
|
+
min-width: 0;
|
|
72
|
+
}
|
|
73
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-title {
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
color: #fff;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
font-weight: 650;
|
|
78
|
+
text-overflow: ellipsis;
|
|
79
|
+
white-space: nowrap;
|
|
80
|
+
}
|
|
81
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-mode {
|
|
82
|
+
margin-bottom: 2px;
|
|
83
|
+
color: var(--editor-accent);
|
|
84
|
+
font-size: 9px;
|
|
85
|
+
font-weight: 750;
|
|
86
|
+
letter-spacing: .1em;
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
}
|
|
89
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-dirty {
|
|
90
|
+
flex: 0 0 auto;
|
|
91
|
+
padding: 2px 7px;
|
|
92
|
+
border: 1px solid #8c6632;
|
|
93
|
+
border-radius: 999px;
|
|
94
|
+
background: #362a1d;
|
|
95
|
+
color: #f0bf72;
|
|
96
|
+
font-size: 9px;
|
|
97
|
+
font-weight: 700;
|
|
98
|
+
letter-spacing: .04em;
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
}
|
|
101
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header-actions {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
gap: 7px;
|
|
105
|
+
}
|
|
106
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-viewport {
|
|
107
|
+
position: relative;
|
|
108
|
+
grid-area: viewport;
|
|
109
|
+
min-width: 0;
|
|
110
|
+
min-height: 0;
|
|
111
|
+
overflow: hidden;
|
|
112
|
+
background: var(--editor-bg);
|
|
113
|
+
}
|
|
114
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-canvas {
|
|
115
|
+
width: 100%;
|
|
116
|
+
height: 100%;
|
|
117
|
+
display: block;
|
|
118
|
+
touch-action: none;
|
|
119
|
+
}
|
|
120
|
+
.womp-mesh-editor-edit-root[data-busy="true"] .womp-mesh-editor-canvas { pointer-events: none; }
|
|
121
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-panel {
|
|
122
|
+
grid-area: controls;
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
gap: var(--editor-space-2);
|
|
126
|
+
min-width: 0;
|
|
127
|
+
padding: var(--editor-space-3) 14px;
|
|
128
|
+
border-right: 1px solid var(--editor-border);
|
|
129
|
+
background: var(--editor-panel);
|
|
130
|
+
overflow: auto;
|
|
131
|
+
}
|
|
132
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-header {
|
|
133
|
+
display: grid;
|
|
134
|
+
gap: 5px;
|
|
135
|
+
padding-bottom: 9px;
|
|
136
|
+
border-bottom: 1px solid var(--editor-border);
|
|
137
|
+
}
|
|
138
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-title {
|
|
139
|
+
color: #fff;
|
|
140
|
+
font-size: 15px;
|
|
141
|
+
font-weight: 650;
|
|
142
|
+
}
|
|
143
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-hint {
|
|
144
|
+
color: var(--editor-muted);
|
|
145
|
+
font-size: 11px;
|
|
146
|
+
line-height: 1.45;
|
|
147
|
+
}
|
|
148
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-pane {
|
|
149
|
+
display: grid;
|
|
150
|
+
align-content: start;
|
|
151
|
+
gap: var(--editor-space-2);
|
|
152
|
+
}
|
|
153
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-pane + .womp-mesh-editor-inspector-pane {
|
|
154
|
+
padding-top: 10px;
|
|
155
|
+
border-top: 1px solid var(--editor-border);
|
|
156
|
+
}
|
|
157
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-heading {
|
|
158
|
+
color: var(--editor-muted);
|
|
159
|
+
font-size: 10px;
|
|
160
|
+
font-weight: 750;
|
|
161
|
+
letter-spacing: .08em;
|
|
162
|
+
text-transform: uppercase;
|
|
163
|
+
}
|
|
164
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-section {
|
|
165
|
+
display: grid;
|
|
166
|
+
gap: 6px;
|
|
167
|
+
}
|
|
168
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-section + .womp-mesh-editor-inspector-section {
|
|
169
|
+
padding-top: var(--editor-space-2);
|
|
170
|
+
border-top: 1px solid var(--editor-border);
|
|
171
|
+
}
|
|
172
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-actions {
|
|
173
|
+
display: grid;
|
|
174
|
+
grid-template-columns: 1fr 1fr;
|
|
175
|
+
gap: var(--editor-space-2);
|
|
176
|
+
}
|
|
177
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-empty {
|
|
178
|
+
margin: 0;
|
|
179
|
+
color: var(--editor-muted);
|
|
180
|
+
font-size: 11px;
|
|
181
|
+
line-height: 1.5;
|
|
182
|
+
}
|
|
183
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-panel {
|
|
184
|
+
grid-area: tools;
|
|
185
|
+
display: flex;
|
|
186
|
+
flex-direction: column;
|
|
187
|
+
justify-content: flex-start;
|
|
188
|
+
gap: 4px;
|
|
189
|
+
min-width: 0;
|
|
190
|
+
padding: 8px 10px;
|
|
191
|
+
border-left: 1px solid var(--editor-border);
|
|
192
|
+
background: var(--editor-panel);
|
|
193
|
+
overflow: hidden;
|
|
194
|
+
}
|
|
195
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-panel-header {
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
min-height: 22px;
|
|
199
|
+
}
|
|
200
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-grid {
|
|
201
|
+
display: grid;
|
|
202
|
+
align-content: start;
|
|
203
|
+
gap: 12px;
|
|
204
|
+
overflow: auto;
|
|
205
|
+
scrollbar-width: thin;
|
|
206
|
+
}
|
|
207
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section {
|
|
208
|
+
display: grid;
|
|
209
|
+
gap: 6px;
|
|
210
|
+
padding-top: 11px;
|
|
211
|
+
border-top: 1px solid var(--editor-border);
|
|
212
|
+
}
|
|
213
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section:first-child {
|
|
214
|
+
padding-top: 0;
|
|
215
|
+
border-top: 0;
|
|
216
|
+
}
|
|
217
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-label {
|
|
218
|
+
color: var(--editor-muted);
|
|
219
|
+
font-size: 9px;
|
|
220
|
+
font-weight: 700;
|
|
221
|
+
letter-spacing: .06em;
|
|
222
|
+
text-transform: uppercase;
|
|
223
|
+
}
|
|
224
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons {
|
|
225
|
+
display: grid;
|
|
226
|
+
grid-template-columns: repeat(2, 42px);
|
|
227
|
+
justify-content: end;
|
|
228
|
+
gap: 6px;
|
|
229
|
+
}
|
|
230
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons .womp-mesh-editor-button {
|
|
231
|
+
display: grid;
|
|
232
|
+
place-items: center;
|
|
233
|
+
width: 42px;
|
|
234
|
+
height: 42px;
|
|
235
|
+
min-width: 42px;
|
|
236
|
+
min-height: 42px;
|
|
237
|
+
aspect-ratio: 1;
|
|
238
|
+
padding: 7px;
|
|
239
|
+
}
|
|
240
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons .womp-mesh-editor-button.is-labeled {
|
|
241
|
+
grid-column: 1 / -1;
|
|
242
|
+
display: flex;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
gap: 7px;
|
|
245
|
+
width: 90px;
|
|
246
|
+
height: 36px;
|
|
247
|
+
min-width: 90px;
|
|
248
|
+
min-height: 36px;
|
|
249
|
+
padding: 6px 10px;
|
|
250
|
+
}
|
|
251
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-button-label {
|
|
252
|
+
font-size: 11px;
|
|
253
|
+
font-weight: 650;
|
|
254
|
+
}
|
|
255
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-icon {
|
|
256
|
+
width: 23px;
|
|
257
|
+
height: 23px;
|
|
258
|
+
overflow: visible;
|
|
259
|
+
color: #b8c3cf;
|
|
260
|
+
fill: none;
|
|
261
|
+
stroke: currentColor;
|
|
262
|
+
stroke-width: 1.65;
|
|
263
|
+
stroke-linecap: round;
|
|
264
|
+
stroke-linejoin: round;
|
|
265
|
+
transition: color 120ms ease, filter 120ms ease;
|
|
266
|
+
}
|
|
267
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button:hover:not(:disabled) .womp-mesh-editor-tool-icon {
|
|
268
|
+
color: #e3ebf4;
|
|
269
|
+
}
|
|
270
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button.is-active .womp-mesh-editor-tool-icon {
|
|
271
|
+
color: #fff;
|
|
272
|
+
filter: drop-shadow(0 0 4px rgb(105 165 255 / 42%));
|
|
273
|
+
}
|
|
274
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer {
|
|
275
|
+
grid-area: footer;
|
|
276
|
+
z-index: 2;
|
|
277
|
+
display: grid;
|
|
278
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
279
|
+
align-items: center;
|
|
280
|
+
gap: 12px;
|
|
281
|
+
min-width: 0;
|
|
282
|
+
padding: 7px 12px;
|
|
283
|
+
border-top: 1px solid var(--editor-border);
|
|
284
|
+
background: #121821;
|
|
285
|
+
box-shadow: 0 -3px 16px rgb(0 0 0 / 18%);
|
|
286
|
+
}
|
|
287
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer-actions { display: flex; gap: 7px; }
|
|
288
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status {
|
|
289
|
+
position: absolute;
|
|
290
|
+
left: 16px;
|
|
291
|
+
bottom: 16px;
|
|
292
|
+
z-index: 3;
|
|
293
|
+
display: grid;
|
|
294
|
+
gap: 5px;
|
|
295
|
+
width: min(420px, calc(100% - 32px));
|
|
296
|
+
padding: 10px 12px;
|
|
297
|
+
border: 1px solid var(--editor-border-strong);
|
|
298
|
+
border-radius: 8px;
|
|
299
|
+
background: rgb(18 24 33 / 94%);
|
|
300
|
+
box-shadow: 0 12px 32px rgb(0 0 0 / 32%);
|
|
301
|
+
color: #b9c4d0;
|
|
302
|
+
backdrop-filter: blur(12px);
|
|
303
|
+
}
|
|
304
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status-header { display: flex; justify-content: space-between; gap: 8px; }
|
|
305
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status-phase { color: var(--editor-text); font-size: 11px; font-weight: 700; }
|
|
306
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status-state { color: var(--editor-muted); font-size: 9px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; }
|
|
307
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status[data-state="running"] { border-color: #345d8e; }
|
|
308
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status[data-state="running"] .womp-mesh-editor-status-state { color: var(--editor-accent); }
|
|
309
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status[data-state="success"] .womp-mesh-editor-status-state { color: var(--editor-success); }
|
|
310
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status[data-state="error"] { border-color: #824d53; background: rgb(48 24 29 / 96%); }
|
|
311
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status[data-state="error"] .womp-mesh-editor-status-state { color: var(--editor-danger); }
|
|
312
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status-message { overflow-wrap: anywhere; }
|
|
313
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-group { display: grid; gap: 8px; }
|
|
314
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-label {
|
|
315
|
+
color: var(--editor-muted);
|
|
316
|
+
font-size: 10px;
|
|
317
|
+
font-weight: 700;
|
|
318
|
+
letter-spacing: .07em;
|
|
319
|
+
text-transform: uppercase;
|
|
320
|
+
}
|
|
321
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
322
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button {
|
|
323
|
+
min-height: 32px;
|
|
324
|
+
padding: 5px 10px;
|
|
325
|
+
border: 1px solid var(--editor-border-strong);
|
|
326
|
+
border-radius: var(--editor-radius-control);
|
|
327
|
+
background: var(--editor-panel-raised);
|
|
328
|
+
color: var(--editor-text);
|
|
329
|
+
font: inherit;
|
|
330
|
+
font-weight: 550;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, opacity 120ms ease;
|
|
333
|
+
}
|
|
334
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button:hover:not(:disabled) { border-color: #536277; background: #232e3b; }
|
|
335
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button:focus-visible,
|
|
336
|
+
.womp-mesh-editor-edit-root input:focus-visible { outline: 2px solid var(--editor-accent); outline-offset: 2px; }
|
|
337
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons .womp-mesh-editor-button:focus-visible {
|
|
338
|
+
outline: none;
|
|
339
|
+
border-color: #9ec7ff;
|
|
340
|
+
box-shadow: inset 0 0 0 1px #9ec7ff;
|
|
341
|
+
}
|
|
342
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button.is-active { border-color: var(--editor-accent); background: var(--editor-accent-bg); color: #fff; }
|
|
343
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button.is-primary { min-width: 96px; border-color: #72aafa; background: #316fbd; color: #fff; }
|
|
344
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button.is-primary:hover:not(:disabled) { border-color: #91bcfa; background: #3a7dcc; }
|
|
345
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button.is-secondary { background: transparent; }
|
|
346
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-button:disabled { cursor: default; opacity: .5; }
|
|
347
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-range-row {
|
|
348
|
+
display: grid;
|
|
349
|
+
grid-template-columns: 62px minmax(0, 1fr) 38px;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 8px;
|
|
352
|
+
min-height: 26px;
|
|
353
|
+
color: #c5ced8;
|
|
354
|
+
font-size: 11px;
|
|
355
|
+
}
|
|
356
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-range-value {
|
|
357
|
+
color: var(--editor-muted);
|
|
358
|
+
font-variant-numeric: tabular-nums;
|
|
359
|
+
text-align: right;
|
|
360
|
+
}
|
|
361
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-range-row input {
|
|
362
|
+
width: 100%;
|
|
363
|
+
margin: 0;
|
|
364
|
+
accent-color: var(--editor-accent);
|
|
365
|
+
}
|
|
366
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-color-row {
|
|
367
|
+
display: grid;
|
|
368
|
+
grid-template-columns: 62px minmax(0, 1fr);
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 8px;
|
|
371
|
+
min-height: 28px;
|
|
372
|
+
color: #c5ced8;
|
|
373
|
+
font-size: 11px;
|
|
374
|
+
}
|
|
375
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-color-row input {
|
|
376
|
+
width: 100%;
|
|
377
|
+
height: 28px;
|
|
378
|
+
padding: 2px;
|
|
379
|
+
border: 1px solid var(--editor-border-strong);
|
|
380
|
+
border-radius: 5px;
|
|
381
|
+
background: var(--editor-panel-raised);
|
|
382
|
+
}
|
|
383
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-alternate-row {
|
|
384
|
+
display: grid;
|
|
385
|
+
grid-template-columns: 62px minmax(0, 1fr);
|
|
386
|
+
align-items: center;
|
|
387
|
+
gap: 8px;
|
|
388
|
+
min-height: 32px;
|
|
389
|
+
color: #c5ced8;
|
|
390
|
+
font-size: 11px;
|
|
391
|
+
}
|
|
392
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-alternate-row .womp-mesh-editor-button {
|
|
393
|
+
min-height: 30px;
|
|
394
|
+
padding: 4px 10px;
|
|
395
|
+
}
|
|
396
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-check-row {
|
|
397
|
+
display: inline-flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
gap: 7px;
|
|
400
|
+
min-height: 30px;
|
|
401
|
+
padding: 0 9px;
|
|
402
|
+
border: 1px solid var(--editor-border-strong);
|
|
403
|
+
border-radius: var(--editor-radius-control);
|
|
404
|
+
background: var(--editor-panel-raised);
|
|
405
|
+
color: #c5ced8;
|
|
406
|
+
font-size: 11px;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
white-space: nowrap;
|
|
409
|
+
}
|
|
410
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-check-row input {
|
|
411
|
+
width: 14px;
|
|
412
|
+
height: 14px;
|
|
413
|
+
margin: 0;
|
|
414
|
+
accent-color: var(--editor-accent);
|
|
415
|
+
}
|
|
416
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-field-heading {
|
|
417
|
+
color: var(--editor-muted);
|
|
418
|
+
font-size: 11px;
|
|
419
|
+
font-weight: 650;
|
|
420
|
+
}
|
|
421
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-symmetry-planes {
|
|
422
|
+
display: grid;
|
|
423
|
+
grid-template-columns: repeat(2, 1fr);
|
|
424
|
+
gap: 7px;
|
|
425
|
+
}
|
|
426
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-symmetry-planes .womp-mesh-editor-check-row {
|
|
427
|
+
justify-content: space-between;
|
|
428
|
+
min-width: 0;
|
|
429
|
+
}
|
|
430
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer-hint {
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
color: var(--editor-muted);
|
|
433
|
+
font-size: 10px;
|
|
434
|
+
text-overflow: ellipsis;
|
|
435
|
+
white-space: nowrap;
|
|
436
|
+
}
|
|
437
|
+
@media (max-width: 760px) {
|
|
438
|
+
.womp-mesh-editor-modal.womp-mesh-editor-edit-root {
|
|
439
|
+
grid-template-areas:
|
|
440
|
+
"header"
|
|
441
|
+
"viewport"
|
|
442
|
+
"tools"
|
|
443
|
+
"controls"
|
|
444
|
+
"footer";
|
|
445
|
+
grid-template-columns: minmax(0, 1fr);
|
|
446
|
+
grid-template-rows: 50px minmax(0, 1fr) auto auto 52px;
|
|
447
|
+
}
|
|
448
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header { gap: 8px; padding: 0 8px 0 10px; }
|
|
449
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-mode { display: none; }
|
|
450
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-heading { flex: 1 1 auto; }
|
|
451
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-title { max-width: 150px; font-size: 12px; }
|
|
452
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-dirty { display: none; }
|
|
453
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header-actions { gap: 5px; }
|
|
454
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header-actions .womp-mesh-editor-button { min-width: 0; min-height: 34px; padding: 6px 9px; font-size: 11px; }
|
|
455
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-header-actions .is-primary { min-width: 74px; }
|
|
456
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-panel {
|
|
457
|
+
gap: 6px;
|
|
458
|
+
padding: 7px 10px 8px;
|
|
459
|
+
border-top: 1px solid var(--editor-border);
|
|
460
|
+
border-left: 0;
|
|
461
|
+
}
|
|
462
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-panel-header > .womp-mesh-editor-label { display: none; }
|
|
463
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-grid {
|
|
464
|
+
display: flex;
|
|
465
|
+
gap: 6px;
|
|
466
|
+
overflow-x: auto;
|
|
467
|
+
scrollbar-width: none;
|
|
468
|
+
}
|
|
469
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-grid::-webkit-scrollbar { display: none; }
|
|
470
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section {
|
|
471
|
+
display: flex;
|
|
472
|
+
flex: 0 0 auto;
|
|
473
|
+
gap: 6px;
|
|
474
|
+
padding: 0 0 0 8px;
|
|
475
|
+
border-top: 0;
|
|
476
|
+
border-left: 1px solid var(--editor-border-strong);
|
|
477
|
+
}
|
|
478
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section:first-child {
|
|
479
|
+
padding-left: 0;
|
|
480
|
+
border-left: 0;
|
|
481
|
+
}
|
|
482
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-label { display: none; }
|
|
483
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons { display: flex; gap: 6px; }
|
|
484
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons .womp-mesh-editor-button {
|
|
485
|
+
flex: 0 0 auto;
|
|
486
|
+
width: 40px;
|
|
487
|
+
height: 40px;
|
|
488
|
+
min-width: 40px;
|
|
489
|
+
min-height: 40px;
|
|
490
|
+
padding: 7px;
|
|
491
|
+
}
|
|
492
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-tool-section-buttons .womp-mesh-editor-button.is-labeled {
|
|
493
|
+
grid-column: auto;
|
|
494
|
+
width: 72px;
|
|
495
|
+
height: 40px;
|
|
496
|
+
min-width: 72px;
|
|
497
|
+
min-height: 40px;
|
|
498
|
+
padding: 7px 9px;
|
|
499
|
+
}
|
|
500
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-panel {
|
|
501
|
+
gap: 8px;
|
|
502
|
+
max-height: var(--womp-mesh-editor-panel-mobile-max-height, 190px);
|
|
503
|
+
padding: 9px 12px 10px;
|
|
504
|
+
border-top: 1px solid var(--editor-border);
|
|
505
|
+
border-right: 0;
|
|
506
|
+
}
|
|
507
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-header { display: flex; align-items: baseline; gap: 8px; padding-bottom: 7px; }
|
|
508
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-title { flex: 0 0 auto; font-size: 12px; }
|
|
509
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-context-hint { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
510
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-inspector-pane { gap: 8px; }
|
|
511
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer { gap: 8px; padding: 7px 8px; }
|
|
512
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer-hint { display: none; }
|
|
513
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-footer .womp-mesh-editor-button { min-height: 36px; padding: 6px 10px; font-size: 11px; white-space: nowrap; }
|
|
514
|
+
.womp-mesh-editor-edit-root .womp-mesh-editor-status { left: 10px; bottom: 10px; width: min(360px, calc(100% - 20px)); }
|
|
515
|
+
}`;
|
|
516
|
+
document.head.appendChild(style);
|
|
517
|
+
}
|
|
518
|
+
function set_hint(element, hint) {
|
|
519
|
+
element.title = hint;
|
|
520
|
+
element.setAttribute("aria-label", hint);
|
|
521
|
+
}
|
|
522
|
+
function button(label, hint, className = "womp-mesh-editor-button") {
|
|
523
|
+
const element = document.createElement("button");
|
|
524
|
+
element.type = "button";
|
|
525
|
+
element.className = className;
|
|
526
|
+
element.textContent = label;
|
|
527
|
+
set_hint(element, hint);
|
|
528
|
+
return element;
|
|
529
|
+
}
|
|
530
|
+
const TOOL_ICON_PATHS = {
|
|
531
|
+
view: `
|
|
532
|
+
<path d="M2.75 12s3.25-5.25 9.25-5.25S21.25 12 21.25 12 18 17.25 12 17.25 2.75 12 2.75 12Z" />
|
|
533
|
+
<circle cx="12" cy="12" r="2.65" />
|
|
534
|
+
`,
|
|
535
|
+
drag: `
|
|
536
|
+
<path d="m5 3 11.4 8.1-5.25 1.05-2.1 5.1L5 3Z" />
|
|
537
|
+
<circle cx="17.5" cy="17.5" r="2.25" />
|
|
538
|
+
<path d="m11.15 12.15 4.75 3.75" />
|
|
539
|
+
`,
|
|
540
|
+
move: `
|
|
541
|
+
<path d="M12 3v18M3 12h18" />
|
|
542
|
+
<path d="m8.75 6.25 3.25-3.25 3.25 3.25M8.75 17.75 12 21l3.25-3.25M6.25 8.75 3 12l3.25 3.25M17.75 8.75 21 12l-3.25 3.25" />
|
|
543
|
+
`,
|
|
544
|
+
clay: `
|
|
545
|
+
<path d="M3 17.5c2.4-1.1 4.3-1.7 6.2-1.8 2.4-.1 3.8.8 5.8.5 2-.3 3.7-1.5 6-3.7" />
|
|
546
|
+
<path d="M4.5 13.5c2.5-3.6 5-5.2 7.5-5.2s5 1.6 7.5 5.2" />
|
|
547
|
+
<path d="M8.25 9.25C8.8 6.8 10.05 5.5 12 5.5s3.2 1.3 3.75 3.75" />
|
|
548
|
+
`,
|
|
549
|
+
brush: `
|
|
550
|
+
<path d="M4 18h16" />
|
|
551
|
+
<path d="M6 18c.8-5.2 2.8-8 6-8s5.2 2.8 6 8" />
|
|
552
|
+
<path d="M12 3v4m0-4-2 2m2-2 2 2" />
|
|
553
|
+
`,
|
|
554
|
+
smooth: `
|
|
555
|
+
<path d="M3 15.75c3-5 5.75-5 9-1.75s5.8 3.25 9-1.75" />
|
|
556
|
+
<path d="M4.5 10.5c2-2.75 4-3.25 6.25-1.25M13.25 9.4c2.25 1.5 4.25 1.4 6.25-.65" />
|
|
557
|
+
<circle cx="3" cy="15.75" r=".8" /><circle cx="12" cy="14" r=".8" /><circle cx="21" cy="12.25" r=".8" />
|
|
558
|
+
`,
|
|
559
|
+
flatten: `
|
|
560
|
+
<path d="M3 17h18M5 20h14" />
|
|
561
|
+
<path d="M7 4v8m0 0-2.5-2.5M7 12l2.5-2.5M17 4v8m0 0-2.5-2.5M17 12l2.5-2.5" />
|
|
562
|
+
`,
|
|
563
|
+
crease: `
|
|
564
|
+
<path d="M3 7.5c3.3 0 5.2 1.1 6.5 3.4L12 15l2.5-4.1c1.3-2.3 3.2-3.4 6.5-3.4" />
|
|
565
|
+
<path d="M12 4v6m0 0-2-2m2 2 2-2" />
|
|
566
|
+
<path d="M6 19h12" />
|
|
567
|
+
`,
|
|
568
|
+
pore: `
|
|
569
|
+
<circle cx="12" cy="12" r="8" />
|
|
570
|
+
<circle cx="12" cy="12" r="4.5" />
|
|
571
|
+
<path d="M9.5 12c.8 1.6 1.6 2.4 2.5 2.4s1.7-.8 2.5-2.4" />
|
|
572
|
+
`,
|
|
573
|
+
spiral: `
|
|
574
|
+
<path d="M12 12c0-2.2 3.6-2.2 3.6.6 0 3.8-6.8 4.6-8.8.2-2.6-5.8 4.2-10.5 9.7-7.2 4.8 2.9 1.8 13.9-7.5 14.6" />
|
|
575
|
+
`,
|
|
576
|
+
rake: `
|
|
577
|
+
<path d="M5 4v8M9.5 4v8M14 4v8M18.5 4v8" />
|
|
578
|
+
<path d="M3.5 16c3-1.7 5.8-1.7 8.5 0s5.5 1.7 8.5 0M4.5 20h15" />
|
|
579
|
+
`,
|
|
580
|
+
scale: `
|
|
581
|
+
<path d="M4 17c2.4-5 5.1-7.5 8-7.5s5.6 2.5 8 7.5c-2.8-1.3-5.5-1.3-8 0-2.5-1.3-5.2-1.3-8 0Z" />
|
|
582
|
+
<path d="M7 10c1.5-3.3 3.2-5 5-5s3.5 1.7 5 5" />
|
|
583
|
+
`,
|
|
584
|
+
crumple: `
|
|
585
|
+
<path d="m3.5 15 3-7 3 5 2.5-8 3 7 2.5-4 3 7" />
|
|
586
|
+
<path d="M4 19c3-1.5 5.5-1.5 8 0s5 1.5 8 0" />
|
|
587
|
+
`,
|
|
588
|
+
pinch: `
|
|
589
|
+
<circle cx="12" cy="12" r="1.75" />
|
|
590
|
+
<path d="M4 4l5 5m0 0V5.75M9 9H5.75M20 4l-5 5m0 0V5.75M15 9h3.25M4 20l5-5m0 0v3.25M9 15H5.75M20 20l-5-5m0 0v3.25M15 15h3.25" />
|
|
591
|
+
`,
|
|
592
|
+
inflate: `
|
|
593
|
+
<circle cx="12" cy="12" r="4.25" />
|
|
594
|
+
<path d="M12 5V2.75m0 0L10.25 4.5M12 2.75l1.75 1.75M19 12h2.25m0 0-1.75-1.75M21.25 12l-1.75 1.75M12 19v2.25m0 0-1.75-1.75M12 21.25l1.75-1.75M5 12H2.75m0 0L4.5 10.25M2.75 12l1.75 1.75" />
|
|
595
|
+
`,
|
|
596
|
+
paint: `
|
|
597
|
+
<path d="m15.75 3.25 5 5-9.8 9.8-5.8.85.85-5.8 9.75-9.85Z" />
|
|
598
|
+
<path d="m13.35 5.65 5 5M6 13.1l4.95 4.95" />
|
|
599
|
+
<circle cx="4" cy="5" r="1" /><circle cx="8" cy="4" r="1" />
|
|
600
|
+
`,
|
|
601
|
+
"texture-paint": `
|
|
602
|
+
<rect x="3" y="3" width="13" height="13" rx="1.5" />
|
|
603
|
+
<path d="M3 9.5h13M9.5 3v13M3 3l6.5 6.5L16 3M3 16l6.5-6.5L16 16" opacity=".48" />
|
|
604
|
+
<path d="m17 10.5 4 4-5.6 5.6-4.75.75.75-4.75 5.6-5.6Z" />
|
|
605
|
+
`,
|
|
606
|
+
"fill-vertices": `
|
|
607
|
+
<path d="m13.5 3 7.5 7.5-9.75 9.75a2.4 2.4 0 0 1-3.4 0l-4.1-4.1a2.4 2.4 0 0 1 0-3.4L13.5 3Z" />
|
|
608
|
+
<path d="m6.5 10 7.5 7.5M11 5.5l2.5-2.5" />
|
|
609
|
+
<circle cx="17.5" cy="19" r="1" /><circle cx="20.5" cy="16" r="1" />
|
|
610
|
+
`,
|
|
611
|
+
"fill-texture": `
|
|
612
|
+
<rect x="3" y="3" width="11" height="11" rx="1.5" />
|
|
613
|
+
<path d="M3 8.5h11M8.5 3v11M3 3l5.5 5.5L14 3M3 14l5.5-5.5L14 14" opacity=".48" />
|
|
614
|
+
<path d="m16.5 9 4.5 4.5-5.4 5.4a2 2 0 0 1-2.8 0l-1.7-1.7 5.4-8.2Z" />
|
|
615
|
+
<circle cx="20" cy="20" r="1" />
|
|
616
|
+
`,
|
|
617
|
+
};
|
|
618
|
+
function set_button_icon(element, paths, className) {
|
|
619
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
620
|
+
icon.classList.add(className);
|
|
621
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
622
|
+
icon.setAttribute("aria-hidden", "true");
|
|
623
|
+
icon.setAttribute("focusable", "false");
|
|
624
|
+
icon.innerHTML = paths;
|
|
625
|
+
element.replaceChildren(icon);
|
|
626
|
+
}
|
|
627
|
+
function set_tool_button_icon(element, tool) {
|
|
628
|
+
const iconPaths = TOOL_ICON_PATHS[tool];
|
|
629
|
+
if (iconPaths === undefined) {
|
|
630
|
+
throw new Error(`Missing icon for mesh editor tool '${tool}'.`);
|
|
631
|
+
}
|
|
632
|
+
set_button_icon(element, iconPaths, "womp-mesh-editor-tool-icon");
|
|
633
|
+
if (tool === "view") {
|
|
634
|
+
const label = document.createElement("span");
|
|
635
|
+
label.className = "womp-mesh-editor-tool-button-label";
|
|
636
|
+
label.textContent = "View";
|
|
637
|
+
element.classList.add("is-labeled");
|
|
638
|
+
element.appendChild(label);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function make_range(label, min, max, step, value, hint) {
|
|
642
|
+
const row = document.createElement("label");
|
|
643
|
+
row.className = "womp-mesh-editor-range-row";
|
|
644
|
+
row.title = hint;
|
|
645
|
+
const text = document.createElement("span");
|
|
646
|
+
text.textContent = label;
|
|
647
|
+
const input = document.createElement("input");
|
|
648
|
+
input.type = "range";
|
|
649
|
+
input.min = min;
|
|
650
|
+
input.max = max;
|
|
651
|
+
input.step = step;
|
|
652
|
+
input.value = value;
|
|
653
|
+
set_hint(input, label);
|
|
654
|
+
const output = document.createElement("output");
|
|
655
|
+
output.className = "womp-mesh-editor-range-value";
|
|
656
|
+
output.textContent = Number(value).toFixed(2);
|
|
657
|
+
row.append(text, input, output);
|
|
658
|
+
return { row, input, output };
|
|
659
|
+
}
|
|
660
|
+
function format_model_unit_value(value, step) {
|
|
661
|
+
const absoluteStep = Math.abs(step);
|
|
662
|
+
const absoluteValue = Math.abs(value);
|
|
663
|
+
if (absoluteValue > 0 && (absoluteValue < 0.000001 || absoluteValue >= 1000000)) {
|
|
664
|
+
return value.toExponential(2);
|
|
665
|
+
}
|
|
666
|
+
const decimals = absoluteStep > 0
|
|
667
|
+
? Math.min(Math.max(Math.ceil(-Math.log10(absoluteStep)), 0), 6)
|
|
668
|
+
: 2;
|
|
669
|
+
const fixed = value.toFixed(decimals);
|
|
670
|
+
return fixed.includes(".") ? fixed.replace(/0+$/, "").replace(/\.$/, "") : fixed;
|
|
671
|
+
}
|
|
672
|
+
function brush_radius_unit_step(minimumRadius) {
|
|
673
|
+
const desiredStep = Math.min(Math.max(minimumRadius * 0.1, Number.MIN_VALUE), 1);
|
|
674
|
+
return Math.max(10 ** Math.floor(Math.log10(desiredStep)), Number.MIN_VALUE);
|
|
675
|
+
}
|
|
676
|
+
function brush_radius_from_slider(input) {
|
|
677
|
+
const minimum = Number(input.dataset.unitMin);
|
|
678
|
+
const maximum = Number(input.dataset.unitMax);
|
|
679
|
+
const anchor = Number(input.dataset.unitAnchor);
|
|
680
|
+
const t = Math.min(Math.max(Number(input.value) / 1000, 0), 1);
|
|
681
|
+
if (minimum <= 0 || maximum <= minimum) {
|
|
682
|
+
return Math.max(minimum, Number.MIN_VALUE);
|
|
683
|
+
}
|
|
684
|
+
if (anchor > minimum && anchor < maximum) {
|
|
685
|
+
if (t <= 0.5) {
|
|
686
|
+
return minimum * ((anchor / minimum) ** (t * 2));
|
|
687
|
+
}
|
|
688
|
+
return anchor * ((maximum / anchor) ** ((t - 0.5) * 2));
|
|
689
|
+
}
|
|
690
|
+
return minimum * ((maximum / minimum) ** t);
|
|
691
|
+
}
|
|
692
|
+
function set_brush_radius_slider(input, radius) {
|
|
693
|
+
const minimum = Number(input.dataset.unitMin);
|
|
694
|
+
const maximum = Number(input.dataset.unitMax);
|
|
695
|
+
const anchor = Number(input.dataset.unitAnchor);
|
|
696
|
+
const clampedRadius = Math.min(Math.max(radius, minimum), maximum);
|
|
697
|
+
let t = 0;
|
|
698
|
+
if (maximum > minimum && minimum > 0) {
|
|
699
|
+
if (anchor > minimum && anchor < maximum) {
|
|
700
|
+
t = clampedRadius <= anchor
|
|
701
|
+
? 0.5 * (Math.log(clampedRadius / minimum) / Math.log(anchor / minimum))
|
|
702
|
+
: 0.5 + (0.5 * (Math.log(clampedRadius / anchor) / Math.log(maximum / anchor)));
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
t = Math.log(clampedRadius / minimum) / Math.log(maximum / minimum);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
input.value = String(Math.round(Math.min(Math.max(t, 0), 1) * 1000));
|
|
709
|
+
}
|
|
710
|
+
function make_check(label, checked, hint) {
|
|
711
|
+
const row = document.createElement("label");
|
|
712
|
+
row.className = "womp-mesh-editor-check-row";
|
|
713
|
+
row.title = hint;
|
|
714
|
+
const text = document.createElement("span");
|
|
715
|
+
text.textContent = label;
|
|
716
|
+
const input = document.createElement("input");
|
|
717
|
+
input.type = "checkbox";
|
|
718
|
+
input.checked = checked;
|
|
719
|
+
set_hint(input, hint);
|
|
720
|
+
row.append(text, input);
|
|
721
|
+
return { row, input };
|
|
722
|
+
}
|
|
723
|
+
function make_color(label, value, hint) {
|
|
724
|
+
const row = document.createElement("label");
|
|
725
|
+
row.className = "womp-mesh-editor-color-row";
|
|
726
|
+
row.title = hint;
|
|
727
|
+
const text = document.createElement("span");
|
|
728
|
+
text.textContent = label;
|
|
729
|
+
const input = document.createElement("input");
|
|
730
|
+
input.type = "color";
|
|
731
|
+
input.value = value;
|
|
732
|
+
set_hint(input, hint);
|
|
733
|
+
row.append(text, input);
|
|
734
|
+
return { row, input };
|
|
735
|
+
}
|
|
736
|
+
function set_active_tool(toolButtons, activeTool) {
|
|
737
|
+
for (const [tool, toolButton] of toolButtons) {
|
|
738
|
+
const isActive = tool === activeTool;
|
|
739
|
+
toolButton.classList.toggle("is-active", isActive);
|
|
740
|
+
toolButton.setAttribute("aria-pressed", String(isActive));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
export async function open_edit_session(options) {
|
|
744
|
+
if (options === undefined || options === null) {
|
|
745
|
+
throw new Error("Mesh edit session options are required.");
|
|
746
|
+
}
|
|
747
|
+
if (options.editable_mesh === undefined || options.editable_mesh === null) {
|
|
748
|
+
throw new Error("Editing sessions require editable_mesh.");
|
|
749
|
+
}
|
|
750
|
+
add_styles_once();
|
|
751
|
+
const THREE = options.three || DEFAULT_THREE;
|
|
752
|
+
const previouslyFocusedElement = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
753
|
+
const root = document.createElement("div");
|
|
754
|
+
root.className = "womp-mesh-editor-modal womp-mesh-editor-edit-root";
|
|
755
|
+
for (const className of String(options.root_class || "").split(/\s+/)) {
|
|
756
|
+
if (className !== "")
|
|
757
|
+
root.classList.add(className);
|
|
758
|
+
}
|
|
759
|
+
root.dataset.mode = "edit";
|
|
760
|
+
root.setAttribute("role", "dialog");
|
|
761
|
+
root.setAttribute("aria-modal", "true");
|
|
762
|
+
root.setAttribute("aria-label", "Mesh editing session");
|
|
763
|
+
root.tabIndex = -1;
|
|
764
|
+
const viewport = document.createElement("div");
|
|
765
|
+
viewport.className = "womp-mesh-editor-viewport";
|
|
766
|
+
const canvas = document.createElement("canvas");
|
|
767
|
+
canvas.className = "womp-mesh-editor-canvas";
|
|
768
|
+
canvas.title = "Mesh editor viewport. Primary drag in View orbits; secondary drag pans; Alt-drag or middle mouse orbits; brush tools edit on drag.";
|
|
769
|
+
canvas.setAttribute("aria-label", "Mesh editor viewport");
|
|
770
|
+
viewport.appendChild(canvas);
|
|
771
|
+
const header = document.createElement("div");
|
|
772
|
+
header.className = "womp-mesh-editor-header";
|
|
773
|
+
const heading = document.createElement("div");
|
|
774
|
+
heading.className = "womp-mesh-editor-heading";
|
|
775
|
+
const modeLabel = document.createElement("div");
|
|
776
|
+
modeLabel.className = "womp-mesh-editor-mode";
|
|
777
|
+
modeLabel.textContent = "Mesh editing";
|
|
778
|
+
const headingRow = document.createElement("div");
|
|
779
|
+
headingRow.className = "womp-mesh-editor-heading-row";
|
|
780
|
+
const title = document.createElement("div");
|
|
781
|
+
title.className = "womp-mesh-editor-title";
|
|
782
|
+
title.textContent = options.title || "Edit mesh";
|
|
783
|
+
const dirtyState = document.createElement("span");
|
|
784
|
+
dirtyState.className = "womp-mesh-editor-dirty";
|
|
785
|
+
dirtyState.textContent = "Unsaved";
|
|
786
|
+
dirtyState.hidden = true;
|
|
787
|
+
headingRow.append(title, dirtyState);
|
|
788
|
+
heading.append(modeLabel, headingRow);
|
|
789
|
+
const headerActions = document.createElement("div");
|
|
790
|
+
headerActions.className = "womp-mesh-editor-header-actions";
|
|
791
|
+
const undoButton = button("Undo", "Undo the last mesh edit (Ctrl/Cmd+Z).");
|
|
792
|
+
const redoButton = button("Redo", "Redo the last undone mesh edit (Ctrl/Cmd+Shift+Z or Ctrl+Y).");
|
|
793
|
+
const commitButton = button("Apply", "Return the edited mesh to the host.", "womp-mesh-editor-button is-primary");
|
|
794
|
+
undoButton.disabled = true;
|
|
795
|
+
redoButton.disabled = true;
|
|
796
|
+
headerActions.append(undoButton, redoButton, commitButton);
|
|
797
|
+
header.append(heading, headerActions);
|
|
798
|
+
const panel = document.createElement("div");
|
|
799
|
+
panel.className = "womp-mesh-editor-panel";
|
|
800
|
+
const contextHeader = document.createElement("div");
|
|
801
|
+
contextHeader.className = "womp-mesh-editor-context-header";
|
|
802
|
+
const contextTitle = document.createElement("div");
|
|
803
|
+
contextTitle.className = "womp-mesh-editor-context-title";
|
|
804
|
+
const contextHint = document.createElement("div");
|
|
805
|
+
contextHint.className = "womp-mesh-editor-context-hint";
|
|
806
|
+
contextHeader.append(contextTitle, contextHint);
|
|
807
|
+
const inspectorPanes = new Map();
|
|
808
|
+
const inspectorDefinitions = [
|
|
809
|
+
["tool", "Tool"],
|
|
810
|
+
["stroke", "Stroke"],
|
|
811
|
+
["paint", "Paint"],
|
|
812
|
+
["view", "View"],
|
|
813
|
+
];
|
|
814
|
+
for (const [inspector, label] of inspectorDefinitions) {
|
|
815
|
+
const pane = document.createElement("section");
|
|
816
|
+
pane.className = "womp-mesh-editor-inspector-pane";
|
|
817
|
+
pane.id = `womp-mesh-editor-${inspector}-pane`;
|
|
818
|
+
pane.setAttribute("role", "region");
|
|
819
|
+
pane.setAttribute("aria-label", `${label} settings`);
|
|
820
|
+
const paneHeading = document.createElement("div");
|
|
821
|
+
paneHeading.className = "womp-mesh-editor-inspector-heading";
|
|
822
|
+
paneHeading.textContent = label;
|
|
823
|
+
pane.appendChild(paneHeading);
|
|
824
|
+
inspectorPanes.set(inspector, pane);
|
|
825
|
+
}
|
|
826
|
+
const toolInspector = inspectorPanes.get("tool");
|
|
827
|
+
const strokeInspector = inspectorPanes.get("stroke");
|
|
828
|
+
const paintInspector = inspectorPanes.get("paint");
|
|
829
|
+
const viewInspector = inspectorPanes.get("view");
|
|
830
|
+
if (toolInspector === undefined ||
|
|
831
|
+
strokeInspector === undefined ||
|
|
832
|
+
paintInspector === undefined ||
|
|
833
|
+
viewInspector === undefined) {
|
|
834
|
+
throw new Error("Mesh editor inspector initialization failed.");
|
|
835
|
+
}
|
|
836
|
+
panel.append(contextHeader, toolInspector, strokeInspector, paintInspector, viewInspector);
|
|
837
|
+
const status = document.createElement("div");
|
|
838
|
+
status.className = "womp-mesh-editor-status";
|
|
839
|
+
status.setAttribute("role", "status");
|
|
840
|
+
status.setAttribute("aria-live", "polite");
|
|
841
|
+
const statusHeader = document.createElement("div");
|
|
842
|
+
statusHeader.className = "womp-mesh-editor-status-header";
|
|
843
|
+
const statusPhase = document.createElement("span");
|
|
844
|
+
statusPhase.className = "womp-mesh-editor-status-phase";
|
|
845
|
+
const statusState = document.createElement("span");
|
|
846
|
+
statusState.className = "womp-mesh-editor-status-state";
|
|
847
|
+
const statusMessage = document.createElement("div");
|
|
848
|
+
statusMessage.className = "womp-mesh-editor-status-message";
|
|
849
|
+
statusHeader.append(statusPhase, statusState);
|
|
850
|
+
status.append(statusHeader, statusMessage);
|
|
851
|
+
status.hidden = true;
|
|
852
|
+
viewport.appendChild(status);
|
|
853
|
+
let statusHideTimer = null;
|
|
854
|
+
function set_status(phase, message, state, persistent = false) {
|
|
855
|
+
if (statusHideTimer !== null) {
|
|
856
|
+
window.clearTimeout(statusHideTimer);
|
|
857
|
+
statusHideTimer = null;
|
|
858
|
+
}
|
|
859
|
+
if (state === "idle") {
|
|
860
|
+
status.hidden = true;
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
status.hidden = false;
|
|
864
|
+
status.dataset.state = state;
|
|
865
|
+
statusPhase.textContent = phase;
|
|
866
|
+
statusState.textContent = state;
|
|
867
|
+
statusMessage.textContent = message;
|
|
868
|
+
if (persistent === false && state !== "error") {
|
|
869
|
+
statusHideTimer = window.setTimeout(() => {
|
|
870
|
+
status.hidden = true;
|
|
871
|
+
statusHideTimer = null;
|
|
872
|
+
}, 1800);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const toolGroup = document.createElement("div");
|
|
876
|
+
toolGroup.className = "womp-mesh-editor-tool-panel";
|
|
877
|
+
const toolLabel = document.createElement("div");
|
|
878
|
+
toolLabel.className = "womp-mesh-editor-label";
|
|
879
|
+
toolLabel.textContent = "Tools";
|
|
880
|
+
const toolPanelHeader = document.createElement("div");
|
|
881
|
+
toolPanelHeader.className = "womp-mesh-editor-tool-panel-header";
|
|
882
|
+
toolPanelHeader.appendChild(toolLabel);
|
|
883
|
+
const toolGrid = document.createElement("div");
|
|
884
|
+
toolGrid.className = "womp-mesh-editor-tool-grid";
|
|
885
|
+
const toolButtons = new Map();
|
|
886
|
+
const actionButtons = new Map();
|
|
887
|
+
const toolGroups = [
|
|
888
|
+
{
|
|
889
|
+
label: "Navigation",
|
|
890
|
+
tools: [["view", "View", "Orbit, pan, and inspect the mesh without changing it."]],
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
label: "Sculpt",
|
|
894
|
+
tools: [
|
|
895
|
+
["clay", "Clay", "Build up material with a soft, clay-like stroke."],
|
|
896
|
+
["brush", "Brush", "Raise the surface with a smooth, even stroke."],
|
|
897
|
+
["smooth", "Smooth", "Soften bumps and blend uneven areas."],
|
|
898
|
+
["crease", "Crease", "Cut a narrow groove into the surface."],
|
|
899
|
+
["pore", "Pore", "Stamp rounded pores with a raised rim."],
|
|
900
|
+
["spiral", "Spiral", "Sculpt curled ridges and grooves."],
|
|
901
|
+
["rake", "Rake", "Drag parallel grooves across the surface."],
|
|
902
|
+
["scale", "Scale", "Layer overlapping, scale-shaped details."],
|
|
903
|
+
["crumple", "Crumple", "Add natural-looking wrinkles and surface variation."],
|
|
904
|
+
["move", "Move", "Push and pull a broad area of the surface."],
|
|
905
|
+
["flatten", "Flatten", "Level the surface under the brush."],
|
|
906
|
+
["pinch", "Pinch", "Pull the surface together toward the stroke."],
|
|
907
|
+
["inflate", "Inflate", "Expand the surface outward."],
|
|
908
|
+
],
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
label: "Painting",
|
|
912
|
+
tools: [
|
|
913
|
+
["paint", "Vertex Paint", "Paint color directly onto the mesh vertices."],
|
|
914
|
+
["texture-paint", "Texture Paint", "Paint color onto the material texture."],
|
|
915
|
+
],
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
label: "Single vertex",
|
|
919
|
+
tools: [["drag", "Drag", "Move one visible vertex at a time. Joined seam vertices move together."]],
|
|
920
|
+
requiresVertices: true,
|
|
921
|
+
},
|
|
922
|
+
];
|
|
923
|
+
const tools = toolGroups.flatMap((group) => group.tools);
|
|
924
|
+
let activeTool = "clay";
|
|
925
|
+
let singleVertexToolSection = null;
|
|
926
|
+
for (const toolGroupInfo of toolGroups) {
|
|
927
|
+
const toolSection = document.createElement("div");
|
|
928
|
+
toolSection.className = "womp-mesh-editor-tool-section";
|
|
929
|
+
toolSection.setAttribute("role", "group");
|
|
930
|
+
toolSection.setAttribute("aria-label", toolGroupInfo.label);
|
|
931
|
+
if (toolGroupInfo.requiresVertices === true) {
|
|
932
|
+
toolSection.hidden = true;
|
|
933
|
+
singleVertexToolSection = toolSection;
|
|
934
|
+
}
|
|
935
|
+
const toolSectionLabel = document.createElement("div");
|
|
936
|
+
toolSectionLabel.className = "womp-mesh-editor-tool-section-label";
|
|
937
|
+
toolSectionLabel.textContent = toolGroupInfo.label;
|
|
938
|
+
const toolSectionButtons = document.createElement("div");
|
|
939
|
+
toolSectionButtons.className = "womp-mesh-editor-tool-section-buttons";
|
|
940
|
+
for (const [tool, label, hint] of toolGroupInfo.tools) {
|
|
941
|
+
const toolKind = tool === "view" || tool === "drag" || tool === "brush" ? "tool" : "brush";
|
|
942
|
+
const toolButton = button(label, `${label} ${toolKind}: ${hint}`);
|
|
943
|
+
set_tool_button_icon(toolButton, tool);
|
|
944
|
+
toolButtons.set(tool, toolButton);
|
|
945
|
+
toolSectionButtons.appendChild(toolButton);
|
|
946
|
+
}
|
|
947
|
+
toolSection.append(toolSectionLabel, toolSectionButtons);
|
|
948
|
+
toolGrid.appendChild(toolSection);
|
|
949
|
+
}
|
|
950
|
+
set_active_tool(toolButtons, activeTool);
|
|
951
|
+
toolGroup.append(toolPanelHeader, toolGrid);
|
|
952
|
+
const brushGroup = document.createElement("div");
|
|
953
|
+
brushGroup.className = "womp-mesh-editor-group womp-mesh-editor-inspector-section";
|
|
954
|
+
brushGroup.setAttribute("role", "group");
|
|
955
|
+
brushGroup.setAttribute("aria-label", "Tool controls");
|
|
956
|
+
const radius = make_range("Radius", "0.01", "2.0", "0.01", "1.0", "Brush size in model units.");
|
|
957
|
+
const strength = make_range("Strength", "0.0", "4.0", "0.01", String(default_strength_for_tool(activeTool)), "How strongly the brush affects the surface.");
|
|
958
|
+
const softness = make_range("Softness", "0.0", "1.0", "0.0001", "0.6666", "How gently the brush fades toward its edge.");
|
|
959
|
+
const alternateRow = document.createElement("div");
|
|
960
|
+
alternateRow.className = "womp-mesh-editor-alternate-row";
|
|
961
|
+
const alternateLabel = document.createElement("span");
|
|
962
|
+
alternateLabel.textContent = "Effect";
|
|
963
|
+
const alternateButton = button("Remove", "Remove material. Hold Ctrl to use this effect temporarily.");
|
|
964
|
+
alternateButton.setAttribute("aria-pressed", "false");
|
|
965
|
+
alternateRow.append(alternateLabel, alternateButton);
|
|
966
|
+
alternateRow.hidden = true;
|
|
967
|
+
const color = make_color("Color", "#ff0000", "Brush color for vertex and texture painting.");
|
|
968
|
+
brushGroup.append(radius.row, strength.row, alternateRow);
|
|
969
|
+
brushGroup.hidden = true;
|
|
970
|
+
toolInspector.appendChild(brushGroup);
|
|
971
|
+
const toolInspectorEmpty = document.createElement("p");
|
|
972
|
+
toolInspectorEmpty.className = "womp-mesh-editor-inspector-empty";
|
|
973
|
+
toolInspectorEmpty.textContent = "This tool has no adjustable settings.";
|
|
974
|
+
toolInspector.appendChild(toolInspectorEmpty);
|
|
975
|
+
const paintActions = document.createElement("div");
|
|
976
|
+
paintActions.className = "womp-mesh-editor-inspector-section";
|
|
977
|
+
paintActions.setAttribute("role", "group");
|
|
978
|
+
paintActions.setAttribute("aria-label", "Painting actions");
|
|
979
|
+
const paintActionButtons = document.createElement("div");
|
|
980
|
+
paintActionButtons.className = "womp-mesh-editor-inspector-actions";
|
|
981
|
+
const fillVerticesButton = button("Fill vertices", "Fill vertex colors: apply the selected color to every vertex on the object.");
|
|
982
|
+
const fillTextureButton = button("Fill texture", "Fill textures: apply the selected color to every material texture on the object.");
|
|
983
|
+
actionButtons.set("fill-vertices", fillVerticesButton);
|
|
984
|
+
actionButtons.set("fill-texture", fillTextureButton);
|
|
985
|
+
paintActionButtons.append(fillVerticesButton, fillTextureButton);
|
|
986
|
+
paintActions.appendChild(paintActionButtons);
|
|
987
|
+
const paintSettings = document.createElement("div");
|
|
988
|
+
paintSettings.className = "womp-mesh-editor-inspector-section";
|
|
989
|
+
paintSettings.setAttribute("role", "group");
|
|
990
|
+
paintSettings.setAttribute("aria-label", "Paint settings");
|
|
991
|
+
paintSettings.appendChild(color.row);
|
|
992
|
+
paintInspector.append(paintSettings, paintActions);
|
|
993
|
+
const brushTools = new Set([
|
|
994
|
+
"clay",
|
|
995
|
+
"brush",
|
|
996
|
+
"move",
|
|
997
|
+
"smooth",
|
|
998
|
+
"flatten",
|
|
999
|
+
"crease",
|
|
1000
|
+
"pore",
|
|
1001
|
+
"spiral",
|
|
1002
|
+
"rake",
|
|
1003
|
+
"scale",
|
|
1004
|
+
"crumple",
|
|
1005
|
+
"pinch",
|
|
1006
|
+
"inflate",
|
|
1007
|
+
"paint",
|
|
1008
|
+
"texture-paint",
|
|
1009
|
+
]);
|
|
1010
|
+
const paintTools = new Set(["paint", "texture-paint"]);
|
|
1011
|
+
const strokeSettings = document.createElement("div");
|
|
1012
|
+
strokeSettings.className = "womp-mesh-editor-inspector-section";
|
|
1013
|
+
strokeSettings.setAttribute("role", "group");
|
|
1014
|
+
strokeSettings.setAttribute("aria-label", "Stroke falloff");
|
|
1015
|
+
strokeSettings.appendChild(softness.row);
|
|
1016
|
+
strokeInspector.appendChild(strokeSettings);
|
|
1017
|
+
const strokeInspectorEmpty = document.createElement("p");
|
|
1018
|
+
strokeInspectorEmpty.className = "womp-mesh-editor-inspector-empty";
|
|
1019
|
+
strokeInspectorEmpty.textContent = "Choose a brush to edit stroke settings.";
|
|
1020
|
+
strokeInspector.appendChild(strokeInspectorEmpty);
|
|
1021
|
+
const toolStrengths = new Map();
|
|
1022
|
+
const alternateModes = new Map([
|
|
1023
|
+
["clay", { label: "Remove", hint: "Remove material with Clay. Hold Ctrl to use this effect temporarily." }],
|
|
1024
|
+
["brush", { label: "Lower", hint: "Lower the surface with Brush. Hold Ctrl to use this effect temporarily." }],
|
|
1025
|
+
["smooth", { label: "Relax", hint: "Relax the mesh while preserving surface detail. Hold Ctrl to use this effect temporarily." }],
|
|
1026
|
+
["flatten", { label: "Fill", hint: "Raise low areas toward the highest point under the brush. Hold Ctrl to use this effect temporarily." }],
|
|
1027
|
+
["crease", { label: "Raise", hint: "Create a raised crease. Hold Ctrl to use this effect temporarily." }],
|
|
1028
|
+
["pore", { label: "Invert", hint: "Raise the center and recess the rim. Hold Ctrl to use this effect temporarily." }],
|
|
1029
|
+
["spiral", { label: "Invert", hint: "Swap the spiral ridges and grooves. Hold Ctrl to use this effect temporarily." }],
|
|
1030
|
+
["rake", { label: "Raise", hint: "Raise parallel ribs instead of cutting grooves. Hold Ctrl to use this effect temporarily." }],
|
|
1031
|
+
["scale", { label: "Recess", hint: "Press scales into the surface. Hold Ctrl to use this effect temporarily." }],
|
|
1032
|
+
["crumple", { label: "Invert", hint: "Reverse the crumple pattern. Hold Ctrl to use this effect temporarily." }],
|
|
1033
|
+
["pinch", { label: "Spread", hint: "Spread the surface away from the brush center. Hold Ctrl to use this effect temporarily." }],
|
|
1034
|
+
["inflate", { label: "Deflate", hint: "Push the surface inward. Hold Ctrl to use this effect temporarily." }],
|
|
1035
|
+
]);
|
|
1036
|
+
const persistentAlternateModes = new Map();
|
|
1037
|
+
let controlAlternateMode = false;
|
|
1038
|
+
function configure_strength_control(tool) {
|
|
1039
|
+
if (brushTools.has(tool) === false) {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
const maximum = tool === "smooth" ? 0.5 : 4;
|
|
1043
|
+
const configured = toolStrengths.get(tool) ?? default_strength_for_tool(tool);
|
|
1044
|
+
const value = Math.min(Math.max(configured, 0), maximum);
|
|
1045
|
+
strength.input.max = String(maximum);
|
|
1046
|
+
strength.input.value = String(value);
|
|
1047
|
+
strength.output.textContent = value.toFixed(2);
|
|
1048
|
+
strength.row.title = tool === "smooth"
|
|
1049
|
+
? "How strongly the brush softens the surface."
|
|
1050
|
+
: "How strongly the brush affects the surface.";
|
|
1051
|
+
}
|
|
1052
|
+
function alternate_mode_enabled() {
|
|
1053
|
+
return alternateModes.has(activeTool) &&
|
|
1054
|
+
(persistentAlternateModes.get(activeTool) === true || controlAlternateMode);
|
|
1055
|
+
}
|
|
1056
|
+
function update_alternate_control() {
|
|
1057
|
+
const alternateMode = alternateModes.get(activeTool);
|
|
1058
|
+
alternateRow.hidden = alternateMode === undefined;
|
|
1059
|
+
if (alternateMode === undefined) {
|
|
1060
|
+
alternateButton.classList.remove("is-active");
|
|
1061
|
+
alternateButton.setAttribute("aria-pressed", "false");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
alternateButton.textContent = alternateMode.label;
|
|
1065
|
+
set_hint(alternateButton, alternateMode.hint);
|
|
1066
|
+
const enabled = alternate_mode_enabled();
|
|
1067
|
+
alternateButton.classList.toggle("is-active", enabled);
|
|
1068
|
+
alternateButton.setAttribute("aria-pressed", String(enabled));
|
|
1069
|
+
}
|
|
1070
|
+
function update_tool_context(tool) {
|
|
1071
|
+
const activeToolInfo = tools.find(([toolId]) => toolId === tool);
|
|
1072
|
+
if (activeToolInfo === undefined)
|
|
1073
|
+
return;
|
|
1074
|
+
contextTitle.textContent = activeToolInfo[1];
|
|
1075
|
+
contextHint.textContent = activeToolInfo[2];
|
|
1076
|
+
const isBrush = brushTools.has(tool);
|
|
1077
|
+
const isPaint = paintTools.has(tool);
|
|
1078
|
+
brushGroup.hidden = isBrush === false;
|
|
1079
|
+
toolInspectorEmpty.hidden = isBrush;
|
|
1080
|
+
strokeSettings.hidden = isBrush === false;
|
|
1081
|
+
symmetryInspectorSection.hidden = isBrush === false || tool === "move";
|
|
1082
|
+
strokeInspectorEmpty.hidden = isBrush;
|
|
1083
|
+
configure_strength_control(tool);
|
|
1084
|
+
update_alternate_control();
|
|
1085
|
+
}
|
|
1086
|
+
const viewGroup = document.createElement("div");
|
|
1087
|
+
viewGroup.className = "womp-mesh-editor-inspector-section";
|
|
1088
|
+
viewGroup.setAttribute("role", "group");
|
|
1089
|
+
viewGroup.setAttribute("aria-label", "Viewport display");
|
|
1090
|
+
const vertices = make_check("Show vertices", false, "Show editable mesh vertices.");
|
|
1091
|
+
const wireframe = make_check("Show wireframe", false, "Show the mesh wireframe.");
|
|
1092
|
+
const smoothShading = make_check("Smooth shading", false, "Display the surface with smooth shading.");
|
|
1093
|
+
const frameButton = button("Frame mesh", "Frame the mesh in the viewport.");
|
|
1094
|
+
viewGroup.append(vertices.row, wireframe.row, smoothShading.row, frameButton);
|
|
1095
|
+
viewInspector.appendChild(viewGroup);
|
|
1096
|
+
const initialSymmetryEnabled = options.symmetry_enabled !== false;
|
|
1097
|
+
const symmetryVertical = make_check("Vertical", initialSymmetryEnabled && options.symmetry_vertical !== false, "Mirror strokes from left to right.");
|
|
1098
|
+
const symmetryHorizontal = make_check("Horizontal", initialSymmetryEnabled && options.symmetry_horizontal === true, "Mirror strokes from top to bottom.");
|
|
1099
|
+
const symmetryInspectorSection = document.createElement("div");
|
|
1100
|
+
symmetryInspectorSection.className = "womp-mesh-editor-inspector-section";
|
|
1101
|
+
symmetryInspectorSection.setAttribute("role", "group");
|
|
1102
|
+
symmetryInspectorSection.setAttribute("aria-label", "Stroke symmetry");
|
|
1103
|
+
const symmetryHeading = document.createElement("div");
|
|
1104
|
+
symmetryHeading.className = "womp-mesh-editor-field-heading";
|
|
1105
|
+
symmetryHeading.textContent = "Symmetry";
|
|
1106
|
+
const symmetryPlanes = document.createElement("div");
|
|
1107
|
+
symmetryPlanes.className = "womp-mesh-editor-symmetry-planes";
|
|
1108
|
+
symmetryPlanes.append(symmetryVertical.row, symmetryHorizontal.row);
|
|
1109
|
+
symmetryInspectorSection.append(symmetryHeading, symmetryPlanes);
|
|
1110
|
+
strokeInspector.appendChild(symmetryInspectorSection);
|
|
1111
|
+
const cancelButton = button("Cancel", "Close the mesh editor without applying changes.", "womp-mesh-editor-button is-secondary");
|
|
1112
|
+
const footerHint = document.createElement("div");
|
|
1113
|
+
footerHint.className = "womp-mesh-editor-footer-hint";
|
|
1114
|
+
footerHint.textContent = "Orbit: Alt or middle drag · Pan: right drag · Zoom: wheel";
|
|
1115
|
+
const footerActions = document.createElement("div");
|
|
1116
|
+
footerActions.className = "womp-mesh-editor-footer-actions";
|
|
1117
|
+
footerActions.appendChild(cancelButton);
|
|
1118
|
+
const footer = document.createElement("div");
|
|
1119
|
+
footer.className = "womp-mesh-editor-footer";
|
|
1120
|
+
footer.append(footerHint, footerActions);
|
|
1121
|
+
root.append(header, panel, viewport, toolGroup, footer);
|
|
1122
|
+
const hostContainer = options.container || document.body;
|
|
1123
|
+
const previousDocumentOverflow = document.documentElement.style.overflow;
|
|
1124
|
+
const previousBodyOverflow = document.body.style.overflow;
|
|
1125
|
+
document.documentElement.style.overflow = "hidden";
|
|
1126
|
+
document.body.style.overflow = "hidden";
|
|
1127
|
+
hostContainer.appendChild(root);
|
|
1128
|
+
const backgroundInertStates = new Map();
|
|
1129
|
+
for (const element of Array.from(document.body.children)) {
|
|
1130
|
+
if (element instanceof HTMLElement && element !== root && element.contains(root) === false) {
|
|
1131
|
+
backgroundInertStates.set(element, element.inert);
|
|
1132
|
+
element.inert = true;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
root.focus();
|
|
1136
|
+
let settled = false;
|
|
1137
|
+
let settle = null;
|
|
1138
|
+
let rejectSettle = null;
|
|
1139
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
1140
|
+
settle = resolve;
|
|
1141
|
+
rejectSettle = reject;
|
|
1142
|
+
});
|
|
1143
|
+
let busy = false;
|
|
1144
|
+
let canUndo = false;
|
|
1145
|
+
let canRedo = false;
|
|
1146
|
+
let editor;
|
|
1147
|
+
const lockedControls = [
|
|
1148
|
+
...toolButtons.values(),
|
|
1149
|
+
...actionButtons.values(),
|
|
1150
|
+
radius.input,
|
|
1151
|
+
strength.input,
|
|
1152
|
+
softness.input,
|
|
1153
|
+
alternateButton,
|
|
1154
|
+
color.input,
|
|
1155
|
+
vertices.input,
|
|
1156
|
+
wireframe.input,
|
|
1157
|
+
smoothShading.input,
|
|
1158
|
+
symmetryVertical.input,
|
|
1159
|
+
symmetryHorizontal.input,
|
|
1160
|
+
frameButton,
|
|
1161
|
+
cancelButton,
|
|
1162
|
+
commitButton,
|
|
1163
|
+
];
|
|
1164
|
+
function sync_history_controls() {
|
|
1165
|
+
undoButton.disabled = busy || canUndo === false;
|
|
1166
|
+
redoButton.disabled = busy || canRedo === false;
|
|
1167
|
+
}
|
|
1168
|
+
function set_busy(nextBusy) {
|
|
1169
|
+
busy = nextBusy;
|
|
1170
|
+
root.dataset.busy = String(nextBusy);
|
|
1171
|
+
root.setAttribute("aria-busy", String(nextBusy));
|
|
1172
|
+
canvas.setAttribute("aria-disabled", String(nextBusy));
|
|
1173
|
+
canvas.style.pointerEvents = nextBusy ? "none" : "";
|
|
1174
|
+
commitButton.textContent = nextBusy ? "Applying…" : "Apply";
|
|
1175
|
+
for (const control of lockedControls) {
|
|
1176
|
+
control.disabled = nextBusy;
|
|
1177
|
+
}
|
|
1178
|
+
sync_history_controls();
|
|
1179
|
+
}
|
|
1180
|
+
set_busy(false);
|
|
1181
|
+
try {
|
|
1182
|
+
editor = new PortableMeshEditor({
|
|
1183
|
+
THREE,
|
|
1184
|
+
canvas,
|
|
1185
|
+
symmetryEnabled: options.symmetry_enabled,
|
|
1186
|
+
symmetryVertical: options.symmetry_vertical,
|
|
1187
|
+
symmetryHorizontal: options.symmetry_horizontal,
|
|
1188
|
+
onStatus: (message) => set_status("Mesh editing", message, "running"),
|
|
1189
|
+
onHistoryChange: (historyState) => {
|
|
1190
|
+
canUndo = historyState.canUndo;
|
|
1191
|
+
canRedo = historyState.canRedo;
|
|
1192
|
+
dirtyState.hidden = historyState.dirty === false;
|
|
1193
|
+
configure_radius_control();
|
|
1194
|
+
update_brush();
|
|
1195
|
+
sync_history_controls();
|
|
1196
|
+
},
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
root.remove();
|
|
1201
|
+
document.documentElement.style.overflow = previousDocumentOverflow;
|
|
1202
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
1203
|
+
for (const [element, wasInert] of backgroundInertStates) {
|
|
1204
|
+
element.inert = wasInert;
|
|
1205
|
+
}
|
|
1206
|
+
previouslyFocusedElement?.focus();
|
|
1207
|
+
throw error;
|
|
1208
|
+
}
|
|
1209
|
+
function update_brush() {
|
|
1210
|
+
const worldRadius = brush_radius_from_slider(radius.input);
|
|
1211
|
+
const unitStep = Number(radius.input.dataset.unitStep);
|
|
1212
|
+
const formattedRadius = format_model_unit_value(worldRadius, unitStep);
|
|
1213
|
+
radius.output.textContent = formattedRadius;
|
|
1214
|
+
radius.input.setAttribute("aria-valuetext", `${formattedRadius} model units`);
|
|
1215
|
+
radius.input.setAttribute("aria-valuenow", formattedRadius);
|
|
1216
|
+
const strengthValue = Number(strength.input.value);
|
|
1217
|
+
strength.output.textContent = strengthValue.toFixed(2);
|
|
1218
|
+
softness.output.textContent = Number(softness.input.value).toFixed(2);
|
|
1219
|
+
if (brushTools.has(activeTool)) {
|
|
1220
|
+
toolStrengths.set(activeTool, strengthValue);
|
|
1221
|
+
}
|
|
1222
|
+
editor.set_brush_world_radius(worldRadius, strengthValue, Number(softness.input.value));
|
|
1223
|
+
editor.set_brush_color(color.input.value);
|
|
1224
|
+
}
|
|
1225
|
+
function sync_alternate_mode() {
|
|
1226
|
+
update_alternate_control();
|
|
1227
|
+
editor.set_brush_sub_mode(alternate_mode_enabled());
|
|
1228
|
+
}
|
|
1229
|
+
function configure_radius_control() {
|
|
1230
|
+
const recommendedMinimum = editor.recommended_brush_radius_minimum();
|
|
1231
|
+
const unitStep = brush_radius_unit_step(recommendedMinimum);
|
|
1232
|
+
const minimum = Math.max(Math.ceil((recommendedMinimum / unitStep) - 0.0000001) * unitStep, unitStep);
|
|
1233
|
+
const maximum = Math.max(Number((editor.brushReferenceRadius * 2).toPrecision(6)), 1, minimum);
|
|
1234
|
+
const currentRadius = Math.min(Math.max(editor.active_brush_world_radius(), minimum), maximum);
|
|
1235
|
+
const unitMinimum = Number(minimum.toPrecision(6));
|
|
1236
|
+
const unitAnchor = Math.min(Math.max(1, unitMinimum), maximum);
|
|
1237
|
+
radius.input.dataset.unitMin = String(unitMinimum);
|
|
1238
|
+
radius.input.dataset.unitMax = String(maximum);
|
|
1239
|
+
radius.input.dataset.unitAnchor = String(unitAnchor);
|
|
1240
|
+
radius.input.dataset.unitStep = String(unitStep);
|
|
1241
|
+
radius.input.min = "0";
|
|
1242
|
+
radius.input.max = "1000";
|
|
1243
|
+
radius.input.step = "1";
|
|
1244
|
+
radius.input.setAttribute("aria-valuemin", String(unitMinimum));
|
|
1245
|
+
radius.input.setAttribute("aria-valuemax", String(maximum));
|
|
1246
|
+
set_brush_radius_slider(radius.input, currentRadius);
|
|
1247
|
+
radius.row.title = `Brush radius in mesh coordinate units (${format_model_unit_value(unitMinimum, unitStep)}–${format_model_unit_value(maximum, unitStep)}).`;
|
|
1248
|
+
}
|
|
1249
|
+
function update_visuals() {
|
|
1250
|
+
editor.set_visuals({
|
|
1251
|
+
showVertices: vertices.input.checked,
|
|
1252
|
+
showWireframe: wireframe.input.checked,
|
|
1253
|
+
smoothShading: smoothShading.input.checked,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
function cleanup() {
|
|
1257
|
+
document.removeEventListener("keydown", on_key_down);
|
|
1258
|
+
document.removeEventListener("keyup", on_key_up);
|
|
1259
|
+
window.removeEventListener("blur", on_window_blur);
|
|
1260
|
+
if (statusHideTimer !== null) {
|
|
1261
|
+
window.clearTimeout(statusHideTimer);
|
|
1262
|
+
statusHideTimer = null;
|
|
1263
|
+
}
|
|
1264
|
+
root.remove();
|
|
1265
|
+
document.documentElement.style.overflow = previousDocumentOverflow;
|
|
1266
|
+
document.body.style.overflow = previousBodyOverflow;
|
|
1267
|
+
for (const [element, wasInert] of backgroundInertStates) {
|
|
1268
|
+
element.inert = wasInert;
|
|
1269
|
+
}
|
|
1270
|
+
previouslyFocusedElement?.focus();
|
|
1271
|
+
editor.dispose();
|
|
1272
|
+
}
|
|
1273
|
+
function close(result) {
|
|
1274
|
+
if (settled)
|
|
1275
|
+
return;
|
|
1276
|
+
settled = true;
|
|
1277
|
+
cleanup();
|
|
1278
|
+
settle?.(result);
|
|
1279
|
+
}
|
|
1280
|
+
function fail(error) {
|
|
1281
|
+
if (settled)
|
|
1282
|
+
return;
|
|
1283
|
+
settled = true;
|
|
1284
|
+
cleanup();
|
|
1285
|
+
rejectSettle?.(error);
|
|
1286
|
+
}
|
|
1287
|
+
function confirm_discard_mesh_changes() {
|
|
1288
|
+
return editor.is_dirty() === false || window.confirm("Discard unsaved mesh edits?");
|
|
1289
|
+
}
|
|
1290
|
+
function on_key_down(event) {
|
|
1291
|
+
if (event.key === "Control" && controlAlternateMode === false) {
|
|
1292
|
+
controlAlternateMode = true;
|
|
1293
|
+
sync_alternate_mode();
|
|
1294
|
+
}
|
|
1295
|
+
if (event.key === "Tab") {
|
|
1296
|
+
const focusableElements = Array.from(root.querySelectorAll("button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex='-1'])")).filter((element) => element.hidden === false && element.closest("[hidden]") === null);
|
|
1297
|
+
if (focusableElements.length > 0) {
|
|
1298
|
+
const first = focusableElements[0];
|
|
1299
|
+
const last = focusableElements[focusableElements.length - 1];
|
|
1300
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
1301
|
+
event.preventDefault();
|
|
1302
|
+
last.focus();
|
|
1303
|
+
}
|
|
1304
|
+
else if (event.shiftKey === false && document.activeElement === last) {
|
|
1305
|
+
event.preventDefault();
|
|
1306
|
+
first.focus();
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (busy) {
|
|
1312
|
+
event.preventDefault();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const modifierPressed = event.ctrlKey || event.metaKey;
|
|
1316
|
+
const key = event.key.toLowerCase();
|
|
1317
|
+
const eventTarget = event.target instanceof HTMLElement ? event.target : null;
|
|
1318
|
+
const targetHandlesTextEditing = eventTarget instanceof HTMLInputElement ||
|
|
1319
|
+
eventTarget instanceof HTMLTextAreaElement ||
|
|
1320
|
+
eventTarget?.isContentEditable === true;
|
|
1321
|
+
if (modifierPressed && event.altKey === false && targetHandlesTextEditing === false) {
|
|
1322
|
+
if (key === "z") {
|
|
1323
|
+
event.preventDefault();
|
|
1324
|
+
if (event.shiftKey)
|
|
1325
|
+
editor.redo();
|
|
1326
|
+
else
|
|
1327
|
+
editor.undo();
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
if (key === "y") {
|
|
1331
|
+
event.preventDefault();
|
|
1332
|
+
editor.redo();
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (event.key === "Escape") {
|
|
1337
|
+
event.preventDefault();
|
|
1338
|
+
if (confirm_discard_mesh_changes() === false)
|
|
1339
|
+
return;
|
|
1340
|
+
editor.cancel();
|
|
1341
|
+
close({ action: "cancel" });
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function on_key_up(event) {
|
|
1345
|
+
if (event.key === "Control" && controlAlternateMode) {
|
|
1346
|
+
controlAlternateMode = false;
|
|
1347
|
+
sync_alternate_mode();
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function on_window_blur() {
|
|
1351
|
+
if (controlAlternateMode) {
|
|
1352
|
+
controlAlternateMode = false;
|
|
1353
|
+
sync_alternate_mode();
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
for (const [tool, toolButton] of toolButtons) {
|
|
1357
|
+
toolButton.addEventListener("click", () => {
|
|
1358
|
+
if (brushTools.has(activeTool)) {
|
|
1359
|
+
toolStrengths.set(activeTool, Number(strength.input.value));
|
|
1360
|
+
}
|
|
1361
|
+
activeTool = tool;
|
|
1362
|
+
editor.set_tool(tool);
|
|
1363
|
+
set_active_tool(toolButtons, tool);
|
|
1364
|
+
update_tool_context(tool);
|
|
1365
|
+
sync_alternate_mode();
|
|
1366
|
+
if (brushTools.has(tool)) {
|
|
1367
|
+
update_brush();
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
for (const [action, actionButton] of actionButtons) {
|
|
1372
|
+
actionButton.addEventListener("click", () => {
|
|
1373
|
+
update_brush();
|
|
1374
|
+
try {
|
|
1375
|
+
if (action === "fill-vertices") {
|
|
1376
|
+
editor.fill_vertex_colors();
|
|
1377
|
+
}
|
|
1378
|
+
else {
|
|
1379
|
+
editor.fill_texture_colors();
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
catch (error) {
|
|
1383
|
+
set_status("Fill failed", error instanceof Error ? error.message : String(error), "error");
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
alternateButton.addEventListener("click", () => {
|
|
1388
|
+
persistentAlternateModes.set(activeTool, persistentAlternateModes.get(activeTool) !== true);
|
|
1389
|
+
sync_alternate_mode();
|
|
1390
|
+
});
|
|
1391
|
+
radius.input.addEventListener("input", update_brush);
|
|
1392
|
+
strength.input.addEventListener("input", update_brush);
|
|
1393
|
+
softness.input.addEventListener("input", update_brush);
|
|
1394
|
+
color.input.addEventListener("input", update_brush);
|
|
1395
|
+
vertices.input.addEventListener("change", () => {
|
|
1396
|
+
singleVertexToolSection?.toggleAttribute("hidden", vertices.input.checked === false);
|
|
1397
|
+
if (vertices.input.checked === false && activeTool === "drag") {
|
|
1398
|
+
activeTool = "view";
|
|
1399
|
+
editor.set_tool(activeTool);
|
|
1400
|
+
set_active_tool(toolButtons, activeTool);
|
|
1401
|
+
update_tool_context(activeTool);
|
|
1402
|
+
sync_alternate_mode();
|
|
1403
|
+
}
|
|
1404
|
+
update_visuals();
|
|
1405
|
+
});
|
|
1406
|
+
wireframe.input.addEventListener("change", update_visuals);
|
|
1407
|
+
smoothShading.input.addEventListener("change", update_visuals);
|
|
1408
|
+
const update_symmetry = () => {
|
|
1409
|
+
const enabled = symmetryVertical.input.checked || symmetryHorizontal.input.checked;
|
|
1410
|
+
editor.set_symmetry({
|
|
1411
|
+
enabled,
|
|
1412
|
+
vertical: symmetryVertical.input.checked,
|
|
1413
|
+
horizontal: symmetryHorizontal.input.checked,
|
|
1414
|
+
});
|
|
1415
|
+
};
|
|
1416
|
+
symmetryVertical.input.addEventListener("change", update_symmetry);
|
|
1417
|
+
symmetryHorizontal.input.addEventListener("change", update_symmetry);
|
|
1418
|
+
update_symmetry();
|
|
1419
|
+
frameButton.addEventListener("click", () => editor.frame_mesh());
|
|
1420
|
+
undoButton.addEventListener("click", () => {
|
|
1421
|
+
editor.undo();
|
|
1422
|
+
});
|
|
1423
|
+
redoButton.addEventListener("click", () => {
|
|
1424
|
+
editor.redo();
|
|
1425
|
+
});
|
|
1426
|
+
cancelButton.addEventListener("click", () => {
|
|
1427
|
+
if (confirm_discard_mesh_changes() === false)
|
|
1428
|
+
return;
|
|
1429
|
+
editor.cancel();
|
|
1430
|
+
close({ action: "cancel" });
|
|
1431
|
+
});
|
|
1432
|
+
commitButton.addEventListener("click", async () => {
|
|
1433
|
+
if (busy)
|
|
1434
|
+
return;
|
|
1435
|
+
set_busy(true);
|
|
1436
|
+
set_status("Apply changes", "Preparing the edited mesh...", "running", true);
|
|
1437
|
+
try {
|
|
1438
|
+
const result = {
|
|
1439
|
+
action: "commit",
|
|
1440
|
+
mode: "edit",
|
|
1441
|
+
mesh: await editor.prepare_commit_async(),
|
|
1442
|
+
};
|
|
1443
|
+
await options.on_commit?.(result);
|
|
1444
|
+
editor.accept_commit();
|
|
1445
|
+
close(result);
|
|
1446
|
+
}
|
|
1447
|
+
catch (error) {
|
|
1448
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1449
|
+
set_status("Could not apply changes", `${message} Fix the problem and try again.`, "error");
|
|
1450
|
+
set_busy(false);
|
|
1451
|
+
commitButton.focus();
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
document.addEventListener("keydown", on_key_down);
|
|
1455
|
+
document.addEventListener("keyup", on_key_up);
|
|
1456
|
+
window.addEventListener("blur", on_window_blur);
|
|
1457
|
+
try {
|
|
1458
|
+
set_status("Opening", "Preparing mesh tools...", "running", true);
|
|
1459
|
+
editor.open_with_initial_tessellation(options.editable_mesh);
|
|
1460
|
+
configure_radius_control();
|
|
1461
|
+
update_tool_context(activeTool);
|
|
1462
|
+
update_brush();
|
|
1463
|
+
sync_alternate_mode();
|
|
1464
|
+
update_visuals();
|
|
1465
|
+
set_status("Mesh editing", "Mesh tools are ready.", "idle");
|
|
1466
|
+
toolButtons.get(activeTool)?.focus();
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
fail(error);
|
|
1470
|
+
}
|
|
1471
|
+
return resultPromise;
|
|
1472
|
+
}
|