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