@nice2dev/ui-3d 1.3.15 → 1.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,564 +1,803 @@
1
- import { jsxs, jsx } from 'react/jsx-runtime';
2
- import { resolveCssColor, NiceIcon, NiceTutorialButton, resolveTutorial, NiceAppShell } from '@nice2dev/ui';
3
- import { useRef, useState, useEffect, useCallback } from 'react';
4
- import { useNiceTranslation } from '../core/i18n.js';
5
- import styles from './UVEditor.module.css.js';
6
- import { DEFAULT_UV_STATE, DEFAULT_RELAX_OPTIONS, UV_TOOLS, UV_SELECTION_MODES, PROJECTION_TYPES, UV_CHANNELS, DEFAULT_PACKING_OPTIONS } from './uvEditorTypes.js';
7
- import { extractUVMeshData, uvSub, uvAdd, uvRotate, selectVerticesInRect, syncToGeometry, packIslands, applyIslandTransforms, relaxUVs, applyProjection, getSelectedVertices } from './uvEditorUtils.js';
8
-
9
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { resolveCssColor, NiceIcon, NiceTutorialButton, resolveTutorial, NiceAppShell } from "@nice2dev/ui";
3
+ import { useRef, useState, useEffect, useCallback } from "react";
4
+ import { useNiceTranslation } from "../core/i18n.js";
5
+ import styles from "./UVEditor.module.css.js";
6
+ import { DEFAULT_UV_STATE, DEFAULT_RELAX_OPTIONS, UV_TOOLS, UV_SELECTION_MODES, PROJECTION_TYPES, UV_CHANNELS, DEFAULT_PACKING_OPTIONS } from "./uvEditorTypes.js";
7
+ import { extractUVMeshData, uvSub, uvAdd, uvRotate, selectVerticesInRect, syncToGeometry, packIslands, applyIslandTransforms, relaxUVs, applyProjection, getSelectedVertices } from "./uvEditorUtils.js";
10
8
  const UV_EDITOR_TOUR = [
11
- {
12
- target: '.nice-uv-editor',
13
- titleKey: 'tutorial.uvEditor.intro.title',
14
- title: 'UV editor',
15
- contentKey: 'tutorial.uvEditor.intro.content',
16
- content: 'Edit the mesh’s UV layout over the texture: select and transform UVs with the tools, switch UV channels, and apply your changes.',
17
- placement: 'auto',
18
- },
9
+ {
10
+ target: ".nice-uv-editor",
11
+ titleKey: "tutorial.uvEditor.intro.title",
12
+ title: "UV editor",
13
+ contentKey: "tutorial.uvEditor.intro.content",
14
+ content: "Edit the mesh’s UV layout over the texture: select and transform UVs with the tools, switch UV channels, and apply your changes.",
15
+ placement: "auto"
16
+ }
19
17
  ];
20
- /* ═══════════════════════════════════════════
21
- Component
22
- ═══════════════════════════════════════════ */
23
- const NiceUVEditor = ({ geometry, texture, channel = 'uv', onUVChange, onSelectionChange, width = '100%', height = 600, showToolbar = true, showSidebar = true, className, readOnly = false, tutorial, }) => {
24
- const { t } = useNiceTranslation();
25
- const canvasRef = useRef(null);
26
- const containerRef = useRef(null);
27
- // State
28
- const [state, setState] = useState({
29
- ...DEFAULT_UV_STATE,
30
- activeChannel: channel,
31
- });
32
- const [meshData, setMeshData] = useState(null);
33
- const [selection, setSelection] = useState({
34
- vertices: new Set(),
35
- edges: new Set(),
36
- faces: new Set(),
37
- islands: new Set(),
18
+ const NiceUVEditor = ({
19
+ geometry,
20
+ texture,
21
+ channel = "uv",
22
+ onUVChange,
23
+ onSelectionChange,
24
+ width = "100%",
25
+ height = 600,
26
+ showToolbar = true,
27
+ showSidebar = true,
28
+ className,
29
+ readOnly = false,
30
+ tutorial
31
+ }) => {
32
+ const { t } = useNiceTranslation();
33
+ const canvasRef = useRef(null);
34
+ const containerRef = useRef(null);
35
+ const [state, setState] = useState({
36
+ ...DEFAULT_UV_STATE,
37
+ activeChannel: channel
38
+ });
39
+ const [meshData, setMeshData] = useState(null);
40
+ const [selection, setSelection] = useState({
41
+ vertices: /* @__PURE__ */ new Set(),
42
+ edges: /* @__PURE__ */ new Set(),
43
+ faces: /* @__PURE__ */ new Set(),
44
+ islands: /* @__PURE__ */ new Set()
45
+ });
46
+ const [dragState, setDragState] = useState({
47
+ active: false,
48
+ startPos: { u: 0, v: 0 },
49
+ currentPos: { u: 0, v: 0 },
50
+ mode: "select",
51
+ initialPositions: /* @__PURE__ */ new Map()
52
+ });
53
+ const [undoStack, setUndoStack] = useState([]);
54
+ const [redoStack, setRedoStack] = useState([]);
55
+ useEffect(() => {
56
+ if (geometry) {
57
+ const data = extractUVMeshData(geometry, state.activeChannel);
58
+ setMeshData(data);
59
+ setSelection({ vertices: /* @__PURE__ */ new Set(), edges: /* @__PURE__ */ new Set(), faces: /* @__PURE__ */ new Set(), islands: /* @__PURE__ */ new Set() });
60
+ } else {
61
+ setMeshData(null);
62
+ }
63
+ }, [geometry, state.activeChannel]);
64
+ useEffect(() => {
65
+ if (!canvasRef.current || !meshData) {
66
+ return;
67
+ }
68
+ const canvas = canvasRef.current;
69
+ const ctx = canvas.getContext("2d");
70
+ if (!ctx) {
71
+ return;
72
+ }
73
+ const { width: w, height: h } = canvas;
74
+ const { zoom, panOffset, display } = state;
75
+ ctx.fillStyle = resolveCssColor("var(--nice-bg-secondary, #2a2a2a)", ctx.canvas);
76
+ ctx.fillRect(0, 0, w, h);
77
+ const toScreen = (uv) => ({
78
+ x: (uv.u + panOffset.u) * zoom * w,
79
+ y: (1 - uv.v - panOffset.v) * zoom * h
38
80
  });
39
- const [dragState, setDragState] = useState({
40
- active: false,
41
- startPos: { u: 0, v: 0 },
42
- currentPos: { u: 0, v: 0 },
43
- mode: 'select',
44
- initialPositions: new Map(),
81
+ if (display.showTexture && texture?.image) {
82
+ ctx.globalAlpha = display.textureOpacity;
83
+ const img = texture.image;
84
+ const screenMin = toScreen({ u: 0, v: 0 });
85
+ const screenMax = toScreen({ u: 1, v: 1 });
86
+ ctx.drawImage(
87
+ img,
88
+ screenMin.x,
89
+ screenMax.y,
90
+ screenMax.x - screenMin.x,
91
+ screenMin.y - screenMax.y
92
+ );
93
+ ctx.globalAlpha = 1;
94
+ }
95
+ if (display.showGrid) {
96
+ ctx.strokeStyle = resolveCssColor("var(--nice-border, #404040)", ctx.canvas);
97
+ ctx.lineWidth = 1;
98
+ const gridStep = 1 / display.gridSize;
99
+ for (let i = 0; i <= display.gridSize; i++) {
100
+ const t2 = i * gridStep;
101
+ const startH = toScreen({ u: t2, v: 0 });
102
+ const endH = toScreen({ u: t2, v: 1 });
103
+ ctx.beginPath();
104
+ ctx.moveTo(startH.x, startH.y);
105
+ ctx.lineTo(endH.x, endH.y);
106
+ ctx.stroke();
107
+ const startV = toScreen({ u: 0, v: t2 });
108
+ const endV = toScreen({ u: 1, v: t2 });
109
+ ctx.beginPath();
110
+ ctx.moveTo(startV.x, startV.y);
111
+ ctx.lineTo(endV.x, endV.y);
112
+ ctx.stroke();
113
+ }
114
+ ctx.strokeStyle = resolveCssColor("var(--nice-text-secondary, #666)", ctx.canvas);
115
+ ctx.lineWidth = 2;
116
+ const corners = [
117
+ toScreen({ u: 0, v: 0 }),
118
+ toScreen({ u: 1, v: 0 }),
119
+ toScreen({ u: 1, v: 1 }),
120
+ toScreen({ u: 0, v: 1 })
121
+ ];
122
+ ctx.beginPath();
123
+ ctx.moveTo(corners[0].x, corners[0].y);
124
+ corners.forEach((c, i) => {
125
+ if (i > 0) {
126
+ ctx.lineTo(c.x, c.y);
127
+ }
128
+ });
129
+ ctx.closePath();
130
+ ctx.stroke();
131
+ }
132
+ const faceSelectedFill = resolveCssColor(
133
+ "var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))",
134
+ ctx.canvas
135
+ );
136
+ const faceFill = resolveCssColor(
137
+ "var(--nice-overlay-10, rgba(100, 100, 100, 0.1))",
138
+ ctx.canvas
139
+ );
140
+ for (const face of meshData.faces.values()) {
141
+ const verts = face.vertices.map((id) => meshData.vertices.get(id));
142
+ const screenVerts = verts.map((v) => toScreen(v.position));
143
+ ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
144
+ ctx.beginPath();
145
+ ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
146
+ screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
147
+ ctx.closePath();
148
+ ctx.fill();
149
+ if (display.showDistortion) {
150
+ const distortion = Math.min(face.distortion, 2);
151
+ const r = Math.floor(255 * Math.min(1, distortion));
152
+ const g = Math.floor(255 * Math.max(0, 1 - distortion));
153
+ ctx.fillStyle = `rgba(${r}, ${g}, 0, 0.5)`;
154
+ ctx.fill();
155
+ }
156
+ }
157
+ for (const edge of meshData.edges.values()) {
158
+ const v1 = meshData.vertices.get(edge.startVertex);
159
+ const v2 = meshData.vertices.get(edge.endVertex);
160
+ const s1 = toScreen(v1.position);
161
+ const s2 = toScreen(v2.position);
162
+ if (edge.seam && display.showSeams) {
163
+ ctx.strokeStyle = display.seamColor;
164
+ ctx.lineWidth = 3;
165
+ } else if (edge.selected) {
166
+ ctx.strokeStyle = display.selectionColor;
167
+ ctx.lineWidth = 2;
168
+ } else {
169
+ ctx.strokeStyle = resolveCssColor("var(--nice-text-secondary, #888)", ctx.canvas);
170
+ ctx.lineWidth = 1;
171
+ }
172
+ ctx.beginPath();
173
+ ctx.moveTo(s1.x, s1.y);
174
+ ctx.lineTo(s2.x, s2.y);
175
+ ctx.stroke();
176
+ }
177
+ for (const vertex of meshData.vertices.values()) {
178
+ const s = toScreen(vertex.position);
179
+ ctx.fillStyle = resolveCssColor(
180
+ vertex.selected ? display.selectionColor : vertex.pinned ? "var(--nice-danger, #ff0000)" : "var(--nice-bg, #fff)",
181
+ ctx.canvas
182
+ );
183
+ ctx.beginPath();
184
+ ctx.arc(s.x, s.y, vertex.selected ? 5 : 3, 0, Math.PI * 2);
185
+ ctx.fill();
186
+ if (display.showIndices) {
187
+ ctx.fillStyle = resolveCssColor("var(--nice-bg, #fff)", ctx.canvas);
188
+ ctx.font = "10px monospace";
189
+ ctx.fillText(String(vertex.index), s.x + 6, s.y - 6);
190
+ }
191
+ }
192
+ if (display.showIslandBounds) {
193
+ ctx.strokeStyle = resolveCssColor("var(--nice-info, #00ffff)", ctx.canvas);
194
+ ctx.lineWidth = 1;
195
+ ctx.setLineDash([5, 5]);
196
+ for (const island of meshData.islands.values()) {
197
+ const min = toScreen({ u: island.bounds.minU, v: island.bounds.minV });
198
+ const max = toScreen({ u: island.bounds.maxU, v: island.bounds.maxV });
199
+ ctx.strokeRect(min.x, max.y, max.x - min.x, min.y - max.y);
200
+ }
201
+ ctx.setLineDash([]);
202
+ }
203
+ if (dragState.active && dragState.mode === "select") {
204
+ const s1 = toScreen(dragState.startPos);
205
+ const s2 = toScreen(dragState.currentPos);
206
+ ctx.strokeStyle = resolveCssColor("var(--nice-bg, #fff)", ctx.canvas);
207
+ ctx.lineWidth = 1;
208
+ ctx.setLineDash([3, 3]);
209
+ ctx.strokeRect(
210
+ Math.min(s1.x, s2.x),
211
+ Math.min(s1.y, s2.y),
212
+ Math.abs(s2.x - s1.x),
213
+ Math.abs(s2.y - s1.y)
214
+ );
215
+ ctx.setLineDash([]);
216
+ }
217
+ }, [meshData, state, texture, dragState, selection]);
218
+ useEffect(() => {
219
+ if (!canvasRef.current || !containerRef.current) {
220
+ return;
221
+ }
222
+ const observer = new ResizeObserver(() => {
223
+ const rect = containerRef.current.getBoundingClientRect();
224
+ canvasRef.current.width = rect.width;
225
+ canvasRef.current.height = rect.height;
45
226
  });
46
- const [undoStack, setUndoStack] = useState([]);
47
- const [redoStack, setRedoStack] = useState([]);
48
- // Extract mesh data when geometry changes
49
- useEffect(() => {
50
- if (geometry) {
51
- const data = extractUVMeshData(geometry, state.activeChannel);
52
- setMeshData(data);
53
- setSelection({ vertices: new Set(), edges: new Set(), faces: new Set(), islands: new Set() });
54
- }
55
- else {
56
- setMeshData(null);
57
- }
58
- }, [geometry, state.activeChannel]);
59
- // Render UV view
60
- useEffect(() => {
61
- if (!canvasRef.current || !meshData) {
62
- return;
63
- }
64
- const canvas = canvasRef.current;
65
- const ctx = canvas.getContext('2d');
66
- if (!ctx) {
67
- return;
68
- }
69
- const { width: w, height: h } = canvas;
70
- const { zoom, panOffset, display } = state;
71
- // Clear
72
- ctx.fillStyle = resolveCssColor('var(--nice-bg-secondary, #2a2a2a)', ctx.canvas);
73
- ctx.fillRect(0, 0, w, h);
74
- // Transform helper
75
- const toScreen = (uv) => ({
76
- x: (uv.u + panOffset.u) * zoom * w,
77
- y: (1 - uv.v - panOffset.v) * zoom * h,
227
+ observer.observe(containerRef.current);
228
+ return () => observer.disconnect();
229
+ }, []);
230
+ const pushUndo = useCallback(() => {
231
+ if (!meshData) {
232
+ return;
233
+ }
234
+ setUndoStack((prev) => [...prev.slice(-20), JSON.parse(JSON.stringify(meshData))]);
235
+ setRedoStack([]);
236
+ }, [meshData]);
237
+ const undo = useCallback(() => {
238
+ if (undoStack.length === 0 || !meshData) {
239
+ return;
240
+ }
241
+ setRedoStack((prev2) => [...prev2, JSON.parse(JSON.stringify(meshData))]);
242
+ const prev = undoStack[undoStack.length - 1];
243
+ setUndoStack((stack) => stack.slice(0, -1));
244
+ setMeshData(prev);
245
+ }, [undoStack, meshData]);
246
+ const redo = useCallback(() => {
247
+ if (redoStack.length === 0) {
248
+ return;
249
+ }
250
+ const next = redoStack[redoStack.length - 1];
251
+ setRedoStack((stack) => stack.slice(0, -1));
252
+ if (meshData) {
253
+ setUndoStack((prev) => [...prev, JSON.parse(JSON.stringify(meshData))]);
254
+ }
255
+ setMeshData(next);
256
+ }, [redoStack, meshData]);
257
+ const screenToUV = useCallback(
258
+ (clientX, clientY) => {
259
+ if (!canvasRef.current) {
260
+ return { u: 0, v: 0 };
261
+ }
262
+ const rect = canvasRef.current.getBoundingClientRect();
263
+ const x = clientX - rect.left;
264
+ const y = clientY - rect.top;
265
+ const { zoom, panOffset } = state;
266
+ return {
267
+ u: x / (zoom * rect.width) - panOffset.u,
268
+ v: 1 - y / (zoom * rect.height) - panOffset.v
269
+ };
270
+ },
271
+ [state]
272
+ );
273
+ const handleMouseDown = useCallback(
274
+ (e) => {
275
+ if (readOnly || !meshData) {
276
+ return;
277
+ }
278
+ const uv = screenToUV(e.clientX, e.clientY);
279
+ if (e.button === 1) {
280
+ setDragState({
281
+ active: true,
282
+ startPos: uv,
283
+ currentPos: uv,
284
+ mode: "pan",
285
+ initialPositions: /* @__PURE__ */ new Map()
78
286
  });
79
- // Draw texture background
80
- if (display.showTexture && (texture === null || texture === void 0 ? void 0 : texture.image)) {
81
- ctx.globalAlpha = display.textureOpacity;
82
- const img = texture.image;
83
- const screenMin = toScreen({ u: 0, v: 0 });
84
- const screenMax = toScreen({ u: 1, v: 1 });
85
- ctx.drawImage(img, screenMin.x, screenMax.y, screenMax.x - screenMin.x, screenMin.y - screenMax.y);
86
- ctx.globalAlpha = 1;
87
- }
88
- // Draw grid
89
- if (display.showGrid) {
90
- ctx.strokeStyle = resolveCssColor('var(--nice-border, #404040)', ctx.canvas);
91
- ctx.lineWidth = 1;
92
- const gridStep = 1 / display.gridSize;
93
- for (let i = 0; i <= display.gridSize; i++) {
94
- const t = i * gridStep;
95
- const startH = toScreen({ u: t, v: 0 });
96
- const endH = toScreen({ u: t, v: 1 });
97
- ctx.beginPath();
98
- ctx.moveTo(startH.x, startH.y);
99
- ctx.lineTo(endH.x, endH.y);
100
- ctx.stroke();
101
- const startV = toScreen({ u: 0, v: t });
102
- const endV = toScreen({ u: 1, v: t });
103
- ctx.beginPath();
104
- ctx.moveTo(startV.x, startV.y);
105
- ctx.lineTo(endV.x, endV.y);
106
- ctx.stroke();
107
- }
108
- // UV space boundary
109
- ctx.strokeStyle = resolveCssColor('var(--nice-text-secondary, #666)', ctx.canvas);
110
- ctx.lineWidth = 2;
111
- const corners = [
112
- toScreen({ u: 0, v: 0 }),
113
- toScreen({ u: 1, v: 0 }),
114
- toScreen({ u: 1, v: 1 }),
115
- toScreen({ u: 0, v: 1 }),
116
- ];
117
- ctx.beginPath();
118
- ctx.moveTo(corners[0].x, corners[0].y);
119
- corners.forEach((c, i) => {
120
- if (i > 0) {
121
- ctx.lineTo(c.x, c.y);
122
- }
123
- });
124
- ctx.closePath();
125
- ctx.stroke();
126
- }
127
- // Draw faces — the two fills resolved once per frame, not once per face
128
- const faceSelectedFill = resolveCssColor('var(--nice-warning-tint-30, rgba(255, 170, 0, 0.3))', ctx.canvas);
129
- const faceFill = resolveCssColor('var(--nice-overlay-10, rgba(100, 100, 100, 0.1))', ctx.canvas);
130
- for (const face of meshData.faces.values()) {
131
- const verts = face.vertices.map((id) => meshData.vertices.get(id));
132
- const screenVerts = verts.map((v) => toScreen(v.position));
133
- // Fill
134
- ctx.fillStyle = face.selected ? faceSelectedFill : faceFill;
135
- ctx.beginPath();
136
- ctx.moveTo(screenVerts[0].x, screenVerts[0].y);
137
- screenVerts.slice(1).forEach((sv) => ctx.lineTo(sv.x, sv.y));
138
- ctx.closePath();
139
- ctx.fill();
140
- // Distortion overlay
141
- if (display.showDistortion) {
142
- const distortion = Math.min(face.distortion, 2);
143
- const r = Math.floor(255 * Math.min(1, distortion));
144
- const g = Math.floor(255 * Math.max(0, 1 - distortion));
145
- ctx.fillStyle = `rgba(${r}, ${g}, 0, 0.5)`;
146
- ctx.fill();
147
- }
148
- }
149
- // Draw edges
150
- for (const edge of meshData.edges.values()) {
151
- const v1 = meshData.vertices.get(edge.startVertex);
152
- const v2 = meshData.vertices.get(edge.endVertex);
153
- const s1 = toScreen(v1.position);
154
- const s2 = toScreen(v2.position);
155
- if (edge.seam && display.showSeams) {
156
- ctx.strokeStyle = display.seamColor;
157
- ctx.lineWidth = 3;
158
- }
159
- else if (edge.selected) {
160
- ctx.strokeStyle = display.selectionColor;
161
- ctx.lineWidth = 2;
162
- }
163
- else {
164
- ctx.strokeStyle = resolveCssColor('var(--nice-text-secondary, #888)', ctx.canvas);
165
- ctx.lineWidth = 1;
166
- }
167
- ctx.beginPath();
168
- ctx.moveTo(s1.x, s1.y);
169
- ctx.lineTo(s2.x, s2.y);
170
- ctx.stroke();
171
- }
172
- // Draw vertices
173
- for (const vertex of meshData.vertices.values()) {
174
- const s = toScreen(vertex.position);
175
- ctx.fillStyle = resolveCssColor(vertex.selected
176
- ? display.selectionColor
177
- : vertex.pinned
178
- ? 'var(--nice-danger, #ff0000)'
179
- : 'var(--nice-bg, #fff)', ctx.canvas);
180
- ctx.beginPath();
181
- ctx.arc(s.x, s.y, vertex.selected ? 5 : 3, 0, Math.PI * 2);
182
- ctx.fill();
183
- if (display.showIndices) {
184
- ctx.fillStyle = resolveCssColor('var(--nice-bg, #fff)', ctx.canvas);
185
- ctx.font = '10px monospace';
186
- ctx.fillText(String(vertex.index), s.x + 6, s.y - 6);
187
- }
188
- }
189
- // Draw island bounds
190
- if (display.showIslandBounds) {
191
- ctx.strokeStyle = resolveCssColor('var(--nice-info, #00ffff)', ctx.canvas);
192
- ctx.lineWidth = 1;
193
- ctx.setLineDash([5, 5]);
194
- for (const island of meshData.islands.values()) {
195
- const min = toScreen({ u: island.bounds.minU, v: island.bounds.minV });
196
- const max = toScreen({ u: island.bounds.maxU, v: island.bounds.maxV });
197
- ctx.strokeRect(min.x, max.y, max.x - min.x, min.y - max.y);
198
- }
199
- ctx.setLineDash([]);
200
- }
201
- // Draw selection rect while dragging
202
- if (dragState.active && dragState.mode === 'select') {
203
- const s1 = toScreen(dragState.startPos);
204
- const s2 = toScreen(dragState.currentPos);
205
- ctx.strokeStyle = resolveCssColor('var(--nice-bg, #fff)', ctx.canvas);
206
- ctx.lineWidth = 1;
207
- ctx.setLineDash([3, 3]);
208
- ctx.strokeRect(Math.min(s1.x, s2.x), Math.min(s1.y, s2.y), Math.abs(s2.x - s1.x), Math.abs(s2.y - s1.y));
209
- ctx.setLineDash([]);
210
- }
211
- }, [meshData, state, texture, dragState, selection]);
212
- // Resize canvas
213
- useEffect(() => {
214
- if (!canvasRef.current || !containerRef.current) {
215
- return;
216
- }
217
- const observer = new ResizeObserver(() => {
218
- const rect = containerRef.current.getBoundingClientRect();
219
- canvasRef.current.width = rect.width;
220
- canvasRef.current.height = rect.height;
287
+ return;
288
+ }
289
+ if (e.button === 2) {
290
+ return;
291
+ }
292
+ if (state.activeTool === "select") {
293
+ setDragState({
294
+ active: true,
295
+ startPos: uv,
296
+ currentPos: uv,
297
+ mode: "select",
298
+ initialPositions: /* @__PURE__ */ new Map()
221
299
  });
222
- observer.observe(containerRef.current);
223
- return () => observer.disconnect();
224
- }, []);
225
- // Push undo state
226
- const pushUndo = useCallback(() => {
227
- if (!meshData) {
228
- return;
229
- }
230
- setUndoStack((prev) => [...prev.slice(-20), JSON.parse(JSON.stringify(meshData))]);
231
- setRedoStack([]);
232
- }, [meshData]);
233
- // Undo
234
- const undo = useCallback(() => {
235
- if (undoStack.length === 0 || !meshData) {
236
- return;
237
- }
238
- setRedoStack((prev) => [...prev, JSON.parse(JSON.stringify(meshData))]);
239
- const prev = undoStack[undoStack.length - 1];
240
- setUndoStack((stack) => stack.slice(0, -1));
241
- setMeshData(prev);
242
- }, [undoStack, meshData]);
243
- // Redo
244
- const redo = useCallback(() => {
245
- if (redoStack.length === 0) {
246
- return;
247
- }
248
- const next = redoStack[redoStack.length - 1];
249
- setRedoStack((stack) => stack.slice(0, -1));
250
- if (meshData) {
251
- setUndoStack((prev) => [...prev, JSON.parse(JSON.stringify(meshData))]);
252
- }
253
- setMeshData(next);
254
- }, [redoStack, meshData]);
255
- // Screen to UV conversion
256
- const screenToUV = useCallback((clientX, clientY) => {
257
- if (!canvasRef.current) {
258
- return { u: 0, v: 0 };
259
- }
260
- const rect = canvasRef.current.getBoundingClientRect();
261
- const x = clientX - rect.left;
262
- const y = clientY - rect.top;
263
- const { zoom, panOffset } = state;
264
- return {
265
- u: x / (zoom * rect.width) - panOffset.u,
266
- v: 1 - y / (zoom * rect.height) - panOffset.v,
267
- };
268
- }, [state]);
269
- // Mouse handlers
270
- const handleMouseDown = useCallback((e) => {
271
- if (readOnly || !meshData) {
272
- return;
273
- }
274
- const uv = screenToUV(e.clientX, e.clientY);
275
- // Middle mouse = pan
276
- if (e.button === 1) {
277
- setDragState({
278
- active: true,
279
- startPos: uv,
280
- currentPos: uv,
281
- mode: 'pan',
282
- initialPositions: new Map(),
283
- });
284
- return;
285
- }
286
- // Right click = context menu (handled elsewhere)
287
- if (e.button === 2) {
288
- return;
289
- }
290
- // Left click
291
- if (state.activeTool === 'select') {
292
- // Start selection rect
293
- setDragState({
294
- active: true,
295
- startPos: uv,
296
- currentPos: uv,
297
- mode: 'select',
298
- initialPositions: new Map(),
299
- });
300
- }
301
- else if (['move', 'rotate', 'scale'].includes(state.activeTool)) {
302
- // Start transform
303
- const initialPositions = new Map();
304
- for (const [id, vertex] of meshData.vertices) {
305
- if (vertex.selected) {
306
- initialPositions.set(id, { ...vertex.position });
307
- }
308
- }
309
- if (initialPositions.size > 0) {
310
- pushUndo();
311
- setDragState({
312
- active: true,
313
- startPos: uv,
314
- currentPos: uv,
315
- mode: 'transform',
316
- initialPositions,
317
- });
318
- }
319
- }
320
- }, [readOnly, meshData, state.activeTool, screenToUV, pushUndo]);
321
- const handleMouseMove = useCallback((e) => {
322
- if (!dragState.active || !meshData) {
323
- return;
324
- }
325
- const uv = screenToUV(e.clientX, e.clientY);
326
- if (dragState.mode === 'pan') {
327
- const delta = uvSub(uv, dragState.currentPos);
328
- setState((prev) => ({
329
- ...prev,
330
- panOffset: uvAdd(prev.panOffset, delta),
331
- }));
332
- }
333
- else if (dragState.mode === 'transform') {
334
- const delta = uvSub(uv, dragState.startPos);
335
- // Calculate selection center
336
- let centerU = 0, centerV = 0, count = 0;
337
- for (const pos of dragState.initialPositions.values()) {
338
- centerU += pos.u;
339
- centerV += pos.v;
340
- count++;
341
- }
342
- const center = { u: centerU / count, v: centerV / count };
343
- // Apply transform based on tool
344
- for (const [id, initPos] of dragState.initialPositions) {
345
- const vertex = meshData.vertices.get(id);
346
- if (!vertex) {
347
- continue;
348
- }
349
- switch (state.activeTool) {
350
- case 'move':
351
- vertex.position = uvAdd(initPos, delta);
352
- break;
353
- case 'rotate': {
354
- const angle = Math.atan2(delta.v, delta.u) * 2;
355
- vertex.position = uvRotate(initPos, angle, center);
356
- break;
357
- }
358
- case 'scale': {
359
- const scale = 1 + delta.u + delta.v;
360
- vertex.position = {
361
- u: center.u + (initPos.u - center.u) * scale,
362
- v: center.v + (initPos.v - center.v) * scale,
363
- };
364
- break;
365
- }
366
- }
367
- }
368
- // Trigger re-render
369
- setMeshData({ ...meshData, dirty: true });
370
- }
371
- setDragState((prev) => ({ ...prev, currentPos: uv }));
372
- }, [dragState, meshData, state.activeTool, screenToUV]);
373
- const handleMouseUp = useCallback((e) => {
374
- if (!dragState.active || !meshData) {
375
- setDragState((prev) => ({ ...prev, active: false }));
376
- return;
377
- }
378
- if (dragState.mode === 'select') {
379
- // Complete selection rect
380
- const rect = {
381
- minU: Math.min(dragState.startPos.u, dragState.currentPos.u),
382
- maxU: Math.max(dragState.startPos.u, dragState.currentPos.u),
383
- minV: Math.min(dragState.startPos.v, dragState.currentPos.v),
384
- maxV: Math.max(dragState.startPos.v, dragState.currentPos.v),
385
- };
386
- const additive = e.shiftKey;
387
- const selected = selectVerticesInRect(meshData, rect, additive);
388
- setSelection((prev) => ({
389
- ...prev,
390
- vertices: new Set(selected),
391
- }));
392
- onSelectionChange === null || onSelectionChange === void 0 ? void 0 : onSelectionChange({
393
- vertices: new Set(selected),
394
- edges: new Set(),
395
- faces: new Set(),
396
- islands: new Set(),
397
- });
398
- }
399
- else if (dragState.mode === 'transform' && meshData.dirty) {
400
- // Sync changes back to geometry
401
- if (geometry) {
402
- syncToGeometry(meshData, geometry, state.activeChannel);
403
- onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
404
- }
405
- }
406
- setDragState((prev) => ({ ...prev, active: false }));
407
- }, [dragState, meshData, geometry, state.activeChannel, onSelectionChange, onUVChange]);
408
- // Wheel handler (zoom)
409
- const handleWheel = useCallback((e) => {
410
- e.preventDefault();
411
- const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
300
+ } else if (["move", "rotate", "scale"].includes(state.activeTool)) {
301
+ const initialPositions = /* @__PURE__ */ new Map();
302
+ for (const [id, vertex] of meshData.vertices) {
303
+ if (vertex.selected) {
304
+ initialPositions.set(id, { ...vertex.position });
305
+ }
306
+ }
307
+ if (initialPositions.size > 0) {
308
+ pushUndo();
309
+ setDragState({
310
+ active: true,
311
+ startPos: uv,
312
+ currentPos: uv,
313
+ mode: "transform",
314
+ initialPositions
315
+ });
316
+ }
317
+ }
318
+ },
319
+ [readOnly, meshData, state.activeTool, screenToUV, pushUndo]
320
+ );
321
+ const handleMouseMove = useCallback(
322
+ (e) => {
323
+ if (!dragState.active || !meshData) {
324
+ return;
325
+ }
326
+ const uv = screenToUV(e.clientX, e.clientY);
327
+ if (dragState.mode === "pan") {
328
+ const delta = uvSub(uv, dragState.currentPos);
412
329
  setState((prev) => ({
413
- ...prev,
414
- zoom: Math.max(0.1, Math.min(10, prev.zoom * zoomFactor)),
330
+ ...prev,
331
+ panOffset: uvAdd(prev.panOffset, delta)
415
332
  }));
416
- }, []);
417
- // Keyboard handler
418
- useEffect(() => {
419
- const handler = (e) => {
420
- if (readOnly) {
421
- return;
422
- }
423
- const ctrl = e.ctrlKey || e.metaKey;
424
- // Tool shortcuts
425
- if (e.key === 'g' && !ctrl) {
426
- setState((prev) => ({ ...prev, activeTool: 'move' }));
427
- }
428
- if (e.key === 'r' && !ctrl) {
429
- setState((prev) => ({ ...prev, activeTool: 'rotate' }));
430
- }
431
- if (e.key === 's' && !ctrl) {
432
- setState((prev) => ({ ...prev, activeTool: 'scale' }));
433
- }
434
- if (e.key === 'q' || e.key === 'w') {
435
- setState((prev) => ({ ...prev, activeTool: 'select' }));
436
- }
437
- // Selection mode shortcuts
438
- if (e.key === '1') {
439
- setState((prev) => ({ ...prev, selectionMode: 'vertex' }));
440
- }
441
- if (e.key === '2') {
442
- setState((prev) => ({ ...prev, selectionMode: 'edge' }));
443
- }
444
- if (e.key === '3') {
445
- setState((prev) => ({ ...prev, selectionMode: 'face' }));
446
- }
447
- if (e.key === '4') {
448
- setState((prev) => ({ ...prev, selectionMode: 'island' }));
449
- }
450
- // Undo/Redo
451
- if (ctrl && e.key === 'z') {
452
- e.preventDefault();
453
- undo();
333
+ } else if (dragState.mode === "transform") {
334
+ const delta = uvSub(uv, dragState.startPos);
335
+ let centerU = 0, centerV = 0, count = 0;
336
+ for (const pos of dragState.initialPositions.values()) {
337
+ centerU += pos.u;
338
+ centerV += pos.v;
339
+ count++;
340
+ }
341
+ const center = { u: centerU / count, v: centerV / count };
342
+ for (const [id, initPos] of dragState.initialPositions) {
343
+ const vertex = meshData.vertices.get(id);
344
+ if (!vertex) {
345
+ continue;
346
+ }
347
+ switch (state.activeTool) {
348
+ case "move":
349
+ vertex.position = uvAdd(initPos, delta);
350
+ break;
351
+ case "rotate": {
352
+ const angle = Math.atan2(delta.v, delta.u) * 2;
353
+ vertex.position = uvRotate(initPos, angle, center);
354
+ break;
454
355
  }
455
- if (ctrl && (e.key === 'y' || (e.shiftKey && e.key === 'z'))) {
456
- e.preventDefault();
457
- redo();
356
+ case "scale": {
357
+ const scale = 1 + delta.u + delta.v;
358
+ vertex.position = {
359
+ u: center.u + (initPos.u - center.u) * scale,
360
+ v: center.v + (initPos.v - center.v) * scale
361
+ };
362
+ break;
458
363
  }
459
- // Select all
460
- if (e.key === 'a' && !ctrl) {
461
- if (meshData) {
462
- for (const v of meshData.vertices.values()) {
463
- v.selected = true;
464
- }
465
- setMeshData({ ...meshData });
466
- }
467
- }
468
- // Deselect
469
- if (e.key === 'Escape') {
470
- if (meshData) {
471
- for (const v of meshData.vertices.values()) {
472
- v.selected = false;
473
- }
474
- for (const e of meshData.edges.values()) {
475
- e.selected = false;
476
- }
477
- for (const f of meshData.faces.values()) {
478
- f.selected = false;
479
- }
480
- for (const i of meshData.islands.values()) {
481
- i.selected = false;
482
- }
483
- setMeshData({ ...meshData });
484
- }
485
- }
486
- };
487
- window.addEventListener('keydown', handler);
488
- return () => window.removeEventListener('keydown', handler);
489
- }, [readOnly, meshData, undo, redo]);
490
- // Tool actions
491
- const handlePack = useCallback(() => {
492
- if (!meshData || readOnly) {
493
- return;
494
- }
495
- pushUndo();
496
- const result = packIslands(meshData, DEFAULT_PACKING_OPTIONS);
497
- applyIslandTransforms(meshData, result.transforms);
498
- if (geometry) {
499
- syncToGeometry(meshData, geometry, state.activeChannel);
500
- onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
364
+ }
501
365
  }
502
- setMeshData({ ...meshData });
503
- }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
504
- const handleRelax = useCallback(() => {
505
- if (!meshData || readOnly) {
506
- return;
507
- }
508
- pushUndo();
509
- const selectedVerts = new Set(Array.from(meshData.vertices.values())
510
- .filter((v) => v.selected)
511
- .map((v) => v.id));
512
- relaxUVs(meshData, DEFAULT_RELAX_OPTIONS, selectedVerts.size > 0 ? selectedVerts : undefined);
366
+ setMeshData({ ...meshData, dirty: true });
367
+ }
368
+ setDragState((prev) => ({ ...prev, currentPos: uv }));
369
+ },
370
+ [dragState, meshData, state.activeTool, screenToUV]
371
+ );
372
+ const handleMouseUp = useCallback(
373
+ (e) => {
374
+ if (!dragState.active || !meshData) {
375
+ setDragState((prev) => ({ ...prev, active: false }));
376
+ return;
377
+ }
378
+ if (dragState.mode === "select") {
379
+ const rect = {
380
+ minU: Math.min(dragState.startPos.u, dragState.currentPos.u),
381
+ maxU: Math.max(dragState.startPos.u, dragState.currentPos.u),
382
+ minV: Math.min(dragState.startPos.v, dragState.currentPos.v),
383
+ maxV: Math.max(dragState.startPos.v, dragState.currentPos.v)
384
+ };
385
+ const additive = e.shiftKey;
386
+ const selected = selectVerticesInRect(meshData, rect, additive);
387
+ setSelection((prev) => ({
388
+ ...prev,
389
+ vertices: new Set(selected)
390
+ }));
391
+ onSelectionChange?.({
392
+ vertices: new Set(selected),
393
+ edges: /* @__PURE__ */ new Set(),
394
+ faces: /* @__PURE__ */ new Set(),
395
+ islands: /* @__PURE__ */ new Set()
396
+ });
397
+ } else if (dragState.mode === "transform" && meshData.dirty) {
513
398
  if (geometry) {
514
- syncToGeometry(meshData, geometry, state.activeChannel);
515
- onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
399
+ syncToGeometry(meshData, geometry, state.activeChannel);
400
+ onUVChange?.(geometry);
516
401
  }
517
- setMeshData({ ...meshData });
518
- }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
519
- const handleProject = useCallback((type) => {
520
- if (!meshData || !geometry || readOnly) {
521
- return;
402
+ }
403
+ setDragState((prev) => ({ ...prev, active: false }));
404
+ },
405
+ [dragState, meshData, geometry, state.activeChannel, onSelectionChange, onUVChange]
406
+ );
407
+ const handleWheel = useCallback((e) => {
408
+ e.preventDefault();
409
+ const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
410
+ setState((prev) => ({
411
+ ...prev,
412
+ zoom: Math.max(0.1, Math.min(10, prev.zoom * zoomFactor))
413
+ }));
414
+ }, []);
415
+ useEffect(() => {
416
+ const handler = (e) => {
417
+ if (readOnly) {
418
+ return;
419
+ }
420
+ const ctrl = e.ctrlKey || e.metaKey;
421
+ if (e.key === "g" && !ctrl) {
422
+ setState((prev) => ({ ...prev, activeTool: "move" }));
423
+ }
424
+ if (e.key === "r" && !ctrl) {
425
+ setState((prev) => ({ ...prev, activeTool: "rotate" }));
426
+ }
427
+ if (e.key === "s" && !ctrl) {
428
+ setState((prev) => ({ ...prev, activeTool: "scale" }));
429
+ }
430
+ if (e.key === "q" || e.key === "w") {
431
+ setState((prev) => ({ ...prev, activeTool: "select" }));
432
+ }
433
+ if (e.key === "1") {
434
+ setState((prev) => ({ ...prev, selectionMode: "vertex" }));
435
+ }
436
+ if (e.key === "2") {
437
+ setState((prev) => ({ ...prev, selectionMode: "edge" }));
438
+ }
439
+ if (e.key === "3") {
440
+ setState((prev) => ({ ...prev, selectionMode: "face" }));
441
+ }
442
+ if (e.key === "4") {
443
+ setState((prev) => ({ ...prev, selectionMode: "island" }));
444
+ }
445
+ if (ctrl && e.key === "z") {
446
+ e.preventDefault();
447
+ undo();
448
+ }
449
+ if (ctrl && (e.key === "y" || e.shiftKey && e.key === "z")) {
450
+ e.preventDefault();
451
+ redo();
452
+ }
453
+ if (e.key === "a" && !ctrl) {
454
+ if (meshData) {
455
+ for (const v of meshData.vertices.values()) {
456
+ v.selected = true;
457
+ }
458
+ setMeshData({ ...meshData });
522
459
  }
523
- pushUndo();
524
- const settings = {
525
- type,
526
- scale: { x: 1, y: 1, z: 1 },
527
- offset: { x: 0, y: 0, z: 0 },
528
- rotation: { x: 0, y: 0, z: 0 },
529
- };
530
- applyProjection(geometry, settings);
531
- const newData = extractUVMeshData(geometry, state.activeChannel);
532
- setMeshData(newData);
533
- onUVChange === null || onUVChange === void 0 ? void 0 : onUVChange(geometry);
534
- }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
535
- const toolbarSlot = showToolbar ? (jsxs("div", { className: styles.toolbar, children: [jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => (jsx("button", { className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, activeTool: tool })), title: tool, disabled: readOnly, children: tool[0].toUpperCase() }, tool))) }), jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => (jsx("button", { className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ''}`, onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })), title: mode, children: mode[0].toUpperCase() }, mode))) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: handlePack, disabled: readOnly || !meshData, title: t('uvEditor.packIslandsP', 'Pack Islands (P)'), children: t('uvEditor.pack', 'Pack') }), jsx("button", { className: styles.toolBtn, onClick: handleRelax, disabled: readOnly || !meshData, title: t('uvEditor.relaxUvs', 'Relax UVs'), children: t('uvEditor.relax', 'Relax') })] }), jsx("div", { className: styles.toolGroup, children: jsxs("select", { className: styles.select, onChange: (e) => handleProject(e.target.value), disabled: readOnly || !meshData, defaultValue: "", children: [jsx("option", { value: "", disabled: true, children: t('uvEditor.project', 'Project...') }), PROJECTION_TYPES.map((type) => (jsx("option", { value: type, children: type }, type)))] }) }), jsx("div", { className: styles.toolGroup, children: jsx("select", { className: styles.select, value: state.activeChannel, onChange: (e) => setState((prev) => ({
536
- ...prev,
537
- activeChannel: e.target.value,
538
- })), children: UV_CHANNELS.map((ch) => (jsx("option", { value: ch, children: ch.toUpperCase() }, ch))) }) }), jsxs("div", { className: styles.toolGroup, children: [jsx("button", { className: styles.toolBtn, onClick: undo, disabled: undoStack.length === 0, title: t('uvEditor.undoCtrlZ', 'Undo (Ctrl+Z)'), "aria-label": t('uvEditor.undo', 'Undo'), children: jsx(NiceIcon, { name: "arrow-left", size: 16 }) }), jsx("button", { className: styles.toolBtn, onClick: redo, disabled: redoStack.length === 0, title: t('uvEditor.redoCtrlY', 'Redo (Ctrl+Y)'), "aria-label": t('uvEditor.redo', 'Redo'), children: jsx(NiceIcon, { name: "arrow-right", size: 16 }) })] })] })) : undefined;
539
- const sidebarSlot = showSidebar && meshData ? (jsxs("div", { className: styles.sidebar, children: [jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.display', 'Display') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showGrid, onChange: (e) => setState((prev) => ({
540
- ...prev,
541
- display: { ...prev.display, showGrid: e.target.checked },
542
- })) }), t('uvEditor.showGrid', 'Show Grid')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showTexture, onChange: (e) => setState((prev) => ({
543
- ...prev,
544
- display: { ...prev.display, showTexture: e.target.checked },
545
- })) }), t('uvEditor.showTexture', 'Show Texture')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showSeams, onChange: (e) => setState((prev) => ({
546
- ...prev,
547
- display: { ...prev.display, showSeams: e.target.checked },
548
- })) }), t('uvEditor.showSeams', 'Show Seams')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showDistortion, onChange: (e) => setState((prev) => ({
549
- ...prev,
550
- display: { ...prev.display, showDistortion: e.target.checked },
551
- })) }), t('uvEditor.showDistortion', 'Show Distortion')] }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.display.showIslandBounds, onChange: (e) => setState((prev) => ({
552
- ...prev,
553
- display: { ...prev.display, showIslandBounds: e.target.checked },
554
- })) }), t('uvEditor.showIslandBounds', 'Show Island Bounds')] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.statistics', 'Statistics') }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.vertices', 'Vertices:') }), jsx("span", { children: meshData.vertices.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.faces', 'Faces:') }), jsx("span", { children: meshData.faces.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.islands', 'Islands:') }), jsx("span", { children: meshData.islands.size })] }), jsxs("div", { className: styles.stat, children: [jsx("span", { children: t('uvEditor.selected', 'Selected:') }), jsx("span", { children: getSelectedVertices(meshData).length })] })] }), jsxs("div", { className: styles.panel, children: [jsx("h4", { className: styles.panelTitle, children: t('uvEditor.snapping', 'Snapping') }), jsxs("label", { className: styles.checkbox, children: [jsx("input", { type: "checkbox", checked: state.snapping, onChange: (e) => setState((prev) => ({
555
- ...prev,
556
- snapping: e.target.checked,
557
- })) }), t('uvEditor.enableSnapping', 'Enable Snapping')] }), jsxs("label", { className: styles.rangeLabel, children: [t('uvEditor.snapSize', 'Snap Size: {snapSize}').replace('{snapSize}', String(state.snapSize.toFixed(4))), jsx("input", { type: "range", min: "0.001", max: "0.25", step: "0.001", value: state.snapSize, onChange: (e) => setState((prev) => ({
558
- ...prev,
559
- snapSize: parseFloat(e.target.value),
560
- })) })] })] })] })) : undefined;
561
- return (jsxs("div", { style: { position: 'relative', width, height }, children: [jsx("span", { style: { position: 'absolute', top: 6, right: 6, zIndex: 30 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }), jsx(NiceAppShell, { className: `nice-uv-editor ${styles.uvEditor} ${className !== null && className !== void 0 ? className : ''}`, flush: true, toolbar: toolbarSlot, rightSidebar: sidebarSlot, statusBar: jsxs("div", { className: styles.statusBar, children: [jsx("span", { children: t('uvEditor.tool', 'Tool: {activeTool}').replace('{activeTool}', String(state.activeTool)) }), jsx("span", { children: t('uvEditor.mode', 'Mode: {selectionMode}').replace('{selectionMode}', String(state.selectionMode)) }), jsx("span", { children: t('uvEditor.zoom', 'Zoom: {zoom}%').replace('{zoom}', String((state.zoom * 100).toFixed(0))) }), jsx("span", { children: t('uvEditor.channel', 'Channel: {activeChannel}').replace('{activeChannel}', String(state.activeChannel)) })] }), children: jsxs("div", { ref: containerRef, className: styles.viewport, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, children: [jsx("canvas", { ref: canvasRef, className: styles.canvas }), !meshData && (jsx("div", { className: styles.noGeometry, children: t('uvEditor.noGeometrySelected', 'No geometry selected') }))] }) })] }));
460
+ }
461
+ if (e.key === "Escape") {
462
+ if (meshData) {
463
+ for (const v of meshData.vertices.values()) {
464
+ v.selected = false;
465
+ }
466
+ for (const e2 of meshData.edges.values()) {
467
+ e2.selected = false;
468
+ }
469
+ for (const f of meshData.faces.values()) {
470
+ f.selected = false;
471
+ }
472
+ for (const i of meshData.islands.values()) {
473
+ i.selected = false;
474
+ }
475
+ setMeshData({ ...meshData });
476
+ }
477
+ }
478
+ };
479
+ window.addEventListener("keydown", handler);
480
+ return () => window.removeEventListener("keydown", handler);
481
+ }, [readOnly, meshData, undo, redo]);
482
+ const handlePack = useCallback(() => {
483
+ if (!meshData || readOnly) {
484
+ return;
485
+ }
486
+ pushUndo();
487
+ const result = packIslands(meshData, DEFAULT_PACKING_OPTIONS);
488
+ applyIslandTransforms(meshData, result.transforms);
489
+ if (geometry) {
490
+ syncToGeometry(meshData, geometry, state.activeChannel);
491
+ onUVChange?.(geometry);
492
+ }
493
+ setMeshData({ ...meshData });
494
+ }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
495
+ const handleRelax = useCallback(() => {
496
+ if (!meshData || readOnly) {
497
+ return;
498
+ }
499
+ pushUndo();
500
+ const selectedVerts = new Set(
501
+ Array.from(meshData.vertices.values()).filter((v) => v.selected).map((v) => v.id)
502
+ );
503
+ relaxUVs(meshData, DEFAULT_RELAX_OPTIONS, selectedVerts.size > 0 ? selectedVerts : void 0);
504
+ if (geometry) {
505
+ syncToGeometry(meshData, geometry, state.activeChannel);
506
+ onUVChange?.(geometry);
507
+ }
508
+ setMeshData({ ...meshData });
509
+ }, [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]);
510
+ const handleProject = useCallback(
511
+ (type) => {
512
+ if (!meshData || !geometry || readOnly) {
513
+ return;
514
+ }
515
+ pushUndo();
516
+ const settings = {
517
+ type,
518
+ scale: { x: 1, y: 1, z: 1 },
519
+ offset: { x: 0, y: 0, z: 0 },
520
+ rotation: { x: 0, y: 0, z: 0 }
521
+ };
522
+ applyProjection(geometry, settings);
523
+ const newData = extractUVMeshData(geometry, state.activeChannel);
524
+ setMeshData(newData);
525
+ onUVChange?.(geometry);
526
+ },
527
+ [meshData, geometry, state.activeChannel, readOnly, pushUndo, onUVChange]
528
+ );
529
+ const toolbarSlot = showToolbar ? /* @__PURE__ */ jsxs("div", { className: styles.toolbar, children: [
530
+ /* @__PURE__ */ jsx("div", { className: styles.toolGroup, children: UV_TOOLS.slice(0, 5).map((tool) => /* @__PURE__ */ jsx(
531
+ "button",
532
+ {
533
+ className: `${styles.toolBtn} ${state.activeTool === tool ? styles.active : ""}`,
534
+ onClick: () => setState((prev) => ({ ...prev, activeTool: tool })),
535
+ title: tool,
536
+ disabled: readOnly,
537
+ children: tool[0].toUpperCase()
538
+ },
539
+ tool
540
+ )) }),
541
+ /* @__PURE__ */ jsx("div", { className: styles.toolGroup, children: UV_SELECTION_MODES.map((mode) => /* @__PURE__ */ jsx(
542
+ "button",
543
+ {
544
+ className: `${styles.toolBtn} ${state.selectionMode === mode ? styles.active : ""}`,
545
+ onClick: () => setState((prev) => ({ ...prev, selectionMode: mode })),
546
+ title: mode,
547
+ children: mode[0].toUpperCase()
548
+ },
549
+ mode
550
+ )) }),
551
+ /* @__PURE__ */ jsxs("div", { className: styles.toolGroup, children: [
552
+ /* @__PURE__ */ jsx(
553
+ "button",
554
+ {
555
+ className: styles.toolBtn,
556
+ onClick: handlePack,
557
+ disabled: readOnly || !meshData,
558
+ title: t("uvEditor.packIslandsP", "Pack Islands (P)"),
559
+ children: t("uvEditor.pack", "Pack")
560
+ }
561
+ ),
562
+ /* @__PURE__ */ jsx(
563
+ "button",
564
+ {
565
+ className: styles.toolBtn,
566
+ onClick: handleRelax,
567
+ disabled: readOnly || !meshData,
568
+ title: t("uvEditor.relaxUvs", "Relax UVs"),
569
+ children: t("uvEditor.relax", "Relax")
570
+ }
571
+ )
572
+ ] }),
573
+ /* @__PURE__ */ jsx("div", { className: styles.toolGroup, children: /* @__PURE__ */ jsxs(
574
+ "select",
575
+ {
576
+ className: styles.select,
577
+ onChange: (e) => handleProject(e.target.value),
578
+ disabled: readOnly || !meshData,
579
+ defaultValue: "",
580
+ children: [
581
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: t("uvEditor.project", "Project...") }),
582
+ PROJECTION_TYPES.map((type) => /* @__PURE__ */ jsx("option", { value: type, children: type }, type))
583
+ ]
584
+ }
585
+ ) }),
586
+ /* @__PURE__ */ jsx("div", { className: styles.toolGroup, children: /* @__PURE__ */ jsx(
587
+ "select",
588
+ {
589
+ className: styles.select,
590
+ value: state.activeChannel,
591
+ onChange: (e) => setState((prev) => ({
592
+ ...prev,
593
+ activeChannel: e.target.value
594
+ })),
595
+ children: UV_CHANNELS.map((ch) => /* @__PURE__ */ jsx("option", { value: ch, children: ch.toUpperCase() }, ch))
596
+ }
597
+ ) }),
598
+ /* @__PURE__ */ jsxs("div", { className: styles.toolGroup, children: [
599
+ /* @__PURE__ */ jsx(
600
+ "button",
601
+ {
602
+ className: styles.toolBtn,
603
+ onClick: undo,
604
+ disabled: undoStack.length === 0,
605
+ title: t("uvEditor.undoCtrlZ", "Undo (Ctrl+Z)"),
606
+ "aria-label": t("uvEditor.undo", "Undo"),
607
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "arrow-left", size: 16 })
608
+ }
609
+ ),
610
+ /* @__PURE__ */ jsx(
611
+ "button",
612
+ {
613
+ className: styles.toolBtn,
614
+ onClick: redo,
615
+ disabled: redoStack.length === 0,
616
+ title: t("uvEditor.redoCtrlY", "Redo (Ctrl+Y)"),
617
+ "aria-label": t("uvEditor.redo", "Redo"),
618
+ children: /* @__PURE__ */ jsx(NiceIcon, { name: "arrow-right", size: 16 })
619
+ }
620
+ )
621
+ ] })
622
+ ] }) : void 0;
623
+ const sidebarSlot = showSidebar && meshData ? /* @__PURE__ */ jsxs("div", { className: styles.sidebar, children: [
624
+ /* @__PURE__ */ jsxs("div", { className: styles.panel, children: [
625
+ /* @__PURE__ */ jsx("h4", { className: styles.panelTitle, children: t("uvEditor.display", "Display") }),
626
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
627
+ /* @__PURE__ */ jsx(
628
+ "input",
629
+ {
630
+ type: "checkbox",
631
+ checked: state.display.showGrid,
632
+ onChange: (e) => setState((prev) => ({
633
+ ...prev,
634
+ display: { ...prev.display, showGrid: e.target.checked }
635
+ }))
636
+ }
637
+ ),
638
+ t("uvEditor.showGrid", "Show Grid")
639
+ ] }),
640
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
641
+ /* @__PURE__ */ jsx(
642
+ "input",
643
+ {
644
+ type: "checkbox",
645
+ checked: state.display.showTexture,
646
+ onChange: (e) => setState((prev) => ({
647
+ ...prev,
648
+ display: { ...prev.display, showTexture: e.target.checked }
649
+ }))
650
+ }
651
+ ),
652
+ t("uvEditor.showTexture", "Show Texture")
653
+ ] }),
654
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
655
+ /* @__PURE__ */ jsx(
656
+ "input",
657
+ {
658
+ type: "checkbox",
659
+ checked: state.display.showSeams,
660
+ onChange: (e) => setState((prev) => ({
661
+ ...prev,
662
+ display: { ...prev.display, showSeams: e.target.checked }
663
+ }))
664
+ }
665
+ ),
666
+ t("uvEditor.showSeams", "Show Seams")
667
+ ] }),
668
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
669
+ /* @__PURE__ */ jsx(
670
+ "input",
671
+ {
672
+ type: "checkbox",
673
+ checked: state.display.showDistortion,
674
+ onChange: (e) => setState((prev) => ({
675
+ ...prev,
676
+ display: { ...prev.display, showDistortion: e.target.checked }
677
+ }))
678
+ }
679
+ ),
680
+ t("uvEditor.showDistortion", "Show Distortion")
681
+ ] }),
682
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
683
+ /* @__PURE__ */ jsx(
684
+ "input",
685
+ {
686
+ type: "checkbox",
687
+ checked: state.display.showIslandBounds,
688
+ onChange: (e) => setState((prev) => ({
689
+ ...prev,
690
+ display: { ...prev.display, showIslandBounds: e.target.checked }
691
+ }))
692
+ }
693
+ ),
694
+ t("uvEditor.showIslandBounds", "Show Island Bounds")
695
+ ] })
696
+ ] }),
697
+ /* @__PURE__ */ jsxs("div", { className: styles.panel, children: [
698
+ /* @__PURE__ */ jsx("h4", { className: styles.panelTitle, children: t("uvEditor.statistics", "Statistics") }),
699
+ /* @__PURE__ */ jsxs("div", { className: styles.stat, children: [
700
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.vertices", "Vertices:") }),
701
+ /* @__PURE__ */ jsx("span", { children: meshData.vertices.size })
702
+ ] }),
703
+ /* @__PURE__ */ jsxs("div", { className: styles.stat, children: [
704
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.faces", "Faces:") }),
705
+ /* @__PURE__ */ jsx("span", { children: meshData.faces.size })
706
+ ] }),
707
+ /* @__PURE__ */ jsxs("div", { className: styles.stat, children: [
708
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.islands", "Islands:") }),
709
+ /* @__PURE__ */ jsx("span", { children: meshData.islands.size })
710
+ ] }),
711
+ /* @__PURE__ */ jsxs("div", { className: styles.stat, children: [
712
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.selected", "Selected:") }),
713
+ /* @__PURE__ */ jsx("span", { children: getSelectedVertices(meshData).length })
714
+ ] })
715
+ ] }),
716
+ /* @__PURE__ */ jsxs("div", { className: styles.panel, children: [
717
+ /* @__PURE__ */ jsx("h4", { className: styles.panelTitle, children: t("uvEditor.snapping", "Snapping") }),
718
+ /* @__PURE__ */ jsxs("label", { className: styles.checkbox, children: [
719
+ /* @__PURE__ */ jsx(
720
+ "input",
721
+ {
722
+ type: "checkbox",
723
+ checked: state.snapping,
724
+ onChange: (e) => setState((prev) => ({
725
+ ...prev,
726
+ snapping: e.target.checked
727
+ }))
728
+ }
729
+ ),
730
+ t("uvEditor.enableSnapping", "Enable Snapping")
731
+ ] }),
732
+ /* @__PURE__ */ jsxs("label", { className: styles.rangeLabel, children: [
733
+ t("uvEditor.snapSize", "Snap Size: {snapSize}").replace(
734
+ "{snapSize}",
735
+ String(state.snapSize.toFixed(4))
736
+ ),
737
+ /* @__PURE__ */ jsx(
738
+ "input",
739
+ {
740
+ type: "range",
741
+ min: "0.001",
742
+ max: "0.25",
743
+ step: "0.001",
744
+ value: state.snapSize,
745
+ onChange: (e) => setState((prev) => ({
746
+ ...prev,
747
+ snapSize: parseFloat(e.target.value)
748
+ }))
749
+ }
750
+ )
751
+ ] })
752
+ ] })
753
+ ] }) : void 0;
754
+ return /* @__PURE__ */ jsxs("div", { style: { position: "relative", width, height }, children: [
755
+ /* @__PURE__ */ jsx("span", { style: { position: "absolute", top: 6, right: 6, zIndex: 30 }, children: /* @__PURE__ */ jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, UV_EDITOR_TOUR) }) }),
756
+ /* @__PURE__ */ jsx(
757
+ NiceAppShell,
758
+ {
759
+ className: `nice-uv-editor ${styles.uvEditor} ${className ?? ""}`,
760
+ flush: true,
761
+ toolbar: toolbarSlot,
762
+ rightSidebar: sidebarSlot,
763
+ statusBar: /* @__PURE__ */ jsxs("div", { className: styles.statusBar, children: [
764
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.tool", "Tool: {activeTool}").replace(
765
+ "{activeTool}",
766
+ String(state.activeTool)
767
+ ) }),
768
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.mode", "Mode: {selectionMode}").replace(
769
+ "{selectionMode}",
770
+ String(state.selectionMode)
771
+ ) }),
772
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.zoom", "Zoom: {zoom}%").replace(
773
+ "{zoom}",
774
+ String((state.zoom * 100).toFixed(0))
775
+ ) }),
776
+ /* @__PURE__ */ jsx("span", { children: t("uvEditor.channel", "Channel: {activeChannel}").replace(
777
+ "{activeChannel}",
778
+ String(state.activeChannel)
779
+ ) })
780
+ ] }),
781
+ children: /* @__PURE__ */ jsxs(
782
+ "div",
783
+ {
784
+ ref: containerRef,
785
+ className: styles.viewport,
786
+ onMouseDown: handleMouseDown,
787
+ onMouseMove: handleMouseMove,
788
+ onMouseUp: handleMouseUp,
789
+ onMouseLeave: handleMouseUp,
790
+ onWheel: handleWheel,
791
+ children: [
792
+ /* @__PURE__ */ jsx("canvas", { ref: canvasRef, className: styles.canvas }),
793
+ !meshData && /* @__PURE__ */ jsx("div", { className: styles.noGeometry, children: t("uvEditor.noGeometrySelected", "No geometry selected") })
794
+ ]
795
+ }
796
+ )
797
+ }
798
+ )
799
+ ] });
800
+ };
801
+ export {
802
+ NiceUVEditor
562
803
  };
563
-
564
- export { NiceUVEditor };