@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,386 +1,601 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { NiceTutorialButton, resolveTutorial, NiceIcon } from '@nice2dev/ui';
3
- import { useState, useRef, useEffect, useCallback } from 'react';
4
- import * as THREE from 'three';
5
- import { useNiceTranslation } from '../core/i18n.js';
6
-
7
- /** Built-in guided tour (EN inline + i18n keys → PL and other locales). */
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { NiceTutorialButton, resolveTutorial, NiceIcon } from "@nice2dev/ui";
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import * as THREE from "three";
5
+ import { useNiceTranslation } from "../core/i18n.js";
8
6
  const TERRAIN_EDITOR_TOUR = [
9
- {
10
- target: '.nice-terrain-editor',
11
- titleKey: 'tutorial.terrainEditor.intro.title',
12
- title: 'Terrain editor',
13
- contentKey: 'tutorial.terrainEditor.intro.content',
14
- content: 'Sculpt the heightmap with brushes (raise, lower, flatten, smooth, noise), splat texture layers, generate procedural terrain, and import/export heightmaps.',
15
- placement: 'auto',
16
- },
7
+ {
8
+ target: ".nice-terrain-editor",
9
+ titleKey: "tutorial.terrainEditor.intro.title",
10
+ title: "Terrain editor",
11
+ contentKey: "tutorial.terrainEditor.intro.content",
12
+ content: "Sculpt the heightmap with brushes (raise, lower, flatten, smooth, noise), splat texture layers, generate procedural terrain, and import/export heightmaps.",
13
+ placement: "auto"
14
+ }
17
15
  ];
18
16
  function fractalNoise(x, z, octaves, persistence, lacunarity, _rng) {
19
- let value = 0;
20
- let amplitude = 1;
21
- let frequency = 1;
22
- let maxValue = 0;
23
- for (let i = 0; i < octaves; i++) {
24
- // Simple value noise using sin-based hash
25
- const nx = x * frequency;
26
- const nz = z * frequency;
27
- const noise = Math.sin(nx * 12.9898 + nz * 78.233) * 43758.5453;
28
- value += (noise - Math.floor(noise)) * amplitude;
29
- maxValue += amplitude;
30
- amplitude *= persistence;
31
- frequency *= lacunarity;
32
- }
33
- return value / maxValue;
17
+ let value = 0;
18
+ let amplitude = 1;
19
+ let frequency = 1;
20
+ let maxValue = 0;
21
+ for (let i = 0; i < octaves; i++) {
22
+ const nx = x * frequency;
23
+ const nz = z * frequency;
24
+ const noise = Math.sin(nx * 12.9898 + nz * 78.233) * 43758.5453;
25
+ value += (noise - Math.floor(noise)) * amplitude;
26
+ maxValue += amplitude;
27
+ amplitude *= persistence;
28
+ frequency *= lacunarity;
29
+ }
30
+ return value / maxValue;
34
31
  }
35
- /* ── Hook ── */
36
32
  function useTerrainEditor(scene, camera, canvas, config = { width: 100, depth: 100, segments: 128, maxHeight: 20 }, onTerrainChange) {
37
- const [brush, setBrushState] = useState({
38
- mode: 'raise',
39
- radius: 5,
40
- strength: 0.3,
41
- falloff: 'smooth',
33
+ const [brush, setBrushState] = useState({
34
+ mode: "raise",
35
+ radius: 5,
36
+ strength: 0.3,
37
+ falloff: "smooth"
38
+ });
39
+ const [isPainting, setIsPainting] = useState(false);
40
+ const [textureLayers, setTextureLayers] = useState([
41
+ { name: "Grass", texture: null, scale: 10 },
42
+ { name: "Rock", texture: null, scale: 8 },
43
+ { name: "Sand", texture: null, scale: 12 },
44
+ { name: "Snow", texture: null, scale: 10 }
45
+ ]);
46
+ const [activeTextureLayer, setActiveTextureLayer] = useState(0);
47
+ const terrainRef = useRef(null);
48
+ const splatMapRef = useRef(null);
49
+ const raycaster = useRef(new THREE.Raycaster());
50
+ useEffect(() => {
51
+ if (!scene) {
52
+ return;
53
+ }
54
+ const geometry = new THREE.PlaneGeometry(
55
+ config.width,
56
+ config.depth,
57
+ config.segments,
58
+ config.segments
59
+ );
60
+ geometry.rotateX(-Math.PI / 2);
61
+ const material = new THREE.MeshStandardMaterial({
62
+ color: 4881487,
63
+ wireframe: false,
64
+ flatShading: false,
65
+ side: THREE.DoubleSide
42
66
  });
43
- const [isPainting, setIsPainting] = useState(false);
44
- const [textureLayers, setTextureLayers] = useState([
45
- { name: 'Grass', texture: null, scale: 10 },
46
- { name: 'Rock', texture: null, scale: 8 },
47
- { name: 'Sand', texture: null, scale: 12 },
48
- { name: 'Snow', texture: null, scale: 10 },
49
- ]);
50
- const [activeTextureLayer, setActiveTextureLayer] = useState(0);
51
- const terrainRef = useRef(null);
52
- const splatMapRef = useRef(null);
53
- const raycaster = useRef(new THREE.Raycaster());
54
- // Create terrain mesh
55
- useEffect(() => {
56
- if (!scene) {
57
- return;
58
- }
59
- const geometry = new THREE.PlaneGeometry(config.width, config.depth, config.segments, config.segments);
60
- geometry.rotateX(-Math.PI / 2);
61
- const material = new THREE.MeshStandardMaterial({
62
- color: 0x4a7c4f,
63
- wireframe: false,
64
- flatShading: false,
65
- side: THREE.DoubleSide,
66
- });
67
- const mesh = new THREE.Mesh(geometry, material);
68
- mesh.receiveShadow = true;
69
- mesh.castShadow = true;
70
- mesh.name = 'NiceTerrain';
71
- scene.add(mesh);
72
- terrainRef.current = mesh;
73
- // Initialize splat map (RGBA per vertex)
74
- const vertexCount = geometry.attributes.position.count;
75
- const splatMap = new Float32Array(vertexCount * 4);
76
- for (let i = 0; i < vertexCount; i++) {
77
- splatMap[i * 4] = 1; // R = grass by default
78
- }
79
- splatMapRef.current = splatMap;
80
- return () => {
81
- scene.remove(mesh);
82
- geometry.dispose();
83
- material.dispose();
84
- };
85
- }, [scene, config.width, config.depth, config.segments]);
86
- const setBrush = useCallback((partial) => {
87
- setBrushState((prev) => ({ ...prev, ...partial }));
88
- }, []);
89
- const applyBrushAtPoint = useCallback((point) => {
90
- const mesh = terrainRef.current;
91
- if (!mesh) {
92
- return;
93
- }
94
- const posAttr = mesh.geometry.attributes.position;
95
- const vertex = new THREE.Vector3();
96
- for (let i = 0; i < posAttr.count; i++) {
97
- vertex.fromBufferAttribute(posAttr, i);
98
- const dx = vertex.x - point.x;
99
- const dz = vertex.z - point.z;
100
- const dist = Math.sqrt(dx * dx + dz * dz);
101
- if (dist > brush.radius) {
102
- continue;
103
- }
104
- // Falloff
105
- const t = dist / brush.radius;
106
- let falloff;
107
- switch (brush.falloff) {
108
- case 'constant':
109
- falloff = 1;
110
- break;
111
- case 'linear':
112
- falloff = 1 - t;
113
- break;
114
- case 'smooth':
115
- falloff = 1 - t * t * (3 - 2 * t);
116
- break;
117
- default:
118
- falloff = 1 - t;
119
- }
120
- const influence = falloff * brush.strength;
121
- switch (brush.mode) {
122
- case 'raise':
123
- posAttr.setY(i, posAttr.getY(i) + influence);
124
- break;
125
- case 'lower':
126
- posAttr.setY(i, posAttr.getY(i) - influence);
127
- break;
128
- case 'flatten':
129
- posAttr.setY(i, posAttr.getY(i) + (point.y - posAttr.getY(i)) * influence);
130
- break;
131
- case 'smooth': {
132
- // Average with neighbors
133
- let avg = posAttr.getY(i);
134
- let count = 1;
135
- const seg = config.segments + 1;
136
- const row = Math.floor(i / seg);
137
- const col = i % seg;
138
- const neighbors = [
139
- row > 0 ? i - seg : -1,
140
- row < config.segments ? i + seg : -1,
141
- col > 0 ? i - 1 : -1,
142
- col < config.segments ? i + 1 : -1,
143
- ];
144
- for (const ni of neighbors) {
145
- if (ni >= 0 && ni < posAttr.count) {
146
- avg += posAttr.getY(ni);
147
- count++;
148
- }
149
- }
150
- avg /= count;
151
- posAttr.setY(i, posAttr.getY(i) + (avg - posAttr.getY(i)) * influence);
152
- break;
153
- }
154
- case 'noise':
155
- posAttr.setY(i, posAttr.getY(i) + (Math.random() - 0.5) * influence * 2);
156
- break;
157
- case 'paint': {
158
- // Paint active texture layer onto vertices
159
- if (splatMapRef.current) {
160
- const offset = i * 4;
161
- // Reduce all channels slightly
162
- for (let c = 0; c < 4; c++) {
163
- splatMapRef.current[offset + c] *= 1 - influence * 0.5;
164
- }
165
- splatMapRef.current[offset + activeTextureLayer] += influence;
166
- // Normalize
167
- let total = 0;
168
- for (let c = 0; c < 4; c++) {
169
- total += splatMapRef.current[offset + c];
170
- }
171
- if (total > 0) {
172
- for (let c = 0; c < 4; c++) {
173
- splatMapRef.current[offset + c] /= total;
174
- }
175
- }
176
- }
177
- break;
178
- }
179
- }
180
- }
181
- posAttr.needsUpdate = true;
182
- mesh.geometry.computeVertexNormals();
183
- onTerrainChange === null || onTerrainChange === void 0 ? void 0 : onTerrainChange(mesh);
184
- }, [brush, config.segments, activeTextureLayer, onTerrainChange]);
185
- // Mouse handling
186
- useEffect(() => {
187
- if (!canvas || !camera || !isPainting) {
188
- return;
189
- }
190
- const onPointerMove = (e) => {
191
- const rect = canvas.getBoundingClientRect();
192
- const mouse = new THREE.Vector2(((e.clientX - rect.left) / rect.width) * 2 - 1, -((e.clientY - rect.top) / rect.height) * 2 + 1);
193
- raycaster.current.setFromCamera(mouse, camera);
194
- const mesh = terrainRef.current;
195
- if (!mesh) {
196
- return;
197
- }
198
- const intersects = raycaster.current.intersectObject(mesh);
199
- if (intersects.length > 0) {
200
- applyBrushAtPoint(intersects[0].point);
201
- }
202
- };
203
- canvas.addEventListener('pointermove', onPointerMove);
204
- return () => canvas.removeEventListener('pointermove', onPointerMove);
205
- }, [canvas, camera, isPainting, applyBrushAtPoint]);
206
- const generateFlat = useCallback(() => {
207
- const mesh = terrainRef.current;
208
- if (!mesh) {
209
- return;
210
- }
211
- const posAttr = mesh.geometry.attributes.position;
212
- for (let i = 0; i < posAttr.count; i++) {
213
- posAttr.setY(i, 0);
214
- }
215
- posAttr.needsUpdate = true;
216
- mesh.geometry.computeVertexNormals();
217
- onTerrainChange === null || onTerrainChange === void 0 ? void 0 : onTerrainChange(mesh);
218
- }, [onTerrainChange]);
219
- const generateNoise = useCallback((octaves = 6, persistence = 0.5, lacunarity = 2.0) => {
220
- const mesh = terrainRef.current;
221
- if (!mesh) {
222
- return;
223
- }
224
- const posAttr = mesh.geometry.attributes.position;
225
- for (let i = 0; i < posAttr.count; i++) {
226
- const x = posAttr.getX(i) / config.width;
227
- const z = posAttr.getZ(i) / config.depth;
228
- const height = fractalNoise(x, z, octaves, persistence, lacunarity) * config.maxHeight;
229
- posAttr.setY(i, height);
230
- }
231
- posAttr.needsUpdate = true;
232
- mesh.geometry.computeVertexNormals();
233
- onTerrainChange === null || onTerrainChange === void 0 ? void 0 : onTerrainChange(mesh);
234
- }, [config, onTerrainChange]);
235
- const importHeightmap = useCallback((image) => {
236
- const mesh = terrainRef.current;
237
- if (!mesh) {
238
- return;
67
+ const mesh = new THREE.Mesh(geometry, material);
68
+ mesh.receiveShadow = true;
69
+ mesh.castShadow = true;
70
+ mesh.name = "NiceTerrain";
71
+ scene.add(mesh);
72
+ terrainRef.current = mesh;
73
+ const vertexCount = geometry.attributes.position.count;
74
+ const splatMap = new Float32Array(vertexCount * 4);
75
+ for (let i = 0; i < vertexCount; i++) {
76
+ splatMap[i * 4] = 1;
77
+ }
78
+ splatMapRef.current = splatMap;
79
+ return () => {
80
+ scene.remove(mesh);
81
+ geometry.dispose();
82
+ material.dispose();
83
+ };
84
+ }, [scene, config.width, config.depth, config.segments]);
85
+ const setBrush = useCallback((partial) => {
86
+ setBrushState((prev) => ({ ...prev, ...partial }));
87
+ }, []);
88
+ const applyBrushAtPoint = useCallback(
89
+ (point) => {
90
+ const mesh = terrainRef.current;
91
+ if (!mesh) {
92
+ return;
93
+ }
94
+ const posAttr = mesh.geometry.attributes.position;
95
+ const vertex = new THREE.Vector3();
96
+ for (let i = 0; i < posAttr.count; i++) {
97
+ vertex.fromBufferAttribute(posAttr, i);
98
+ const dx = vertex.x - point.x;
99
+ const dz = vertex.z - point.z;
100
+ const dist = Math.sqrt(dx * dx + dz * dz);
101
+ if (dist > brush.radius) {
102
+ continue;
239
103
  }
240
- const tmpCanvas = document.createElement('canvas');
241
- tmpCanvas.width = image.width;
242
- tmpCanvas.height = image.height;
243
- const ctx = tmpCanvas.getContext('2d');
244
- if (!ctx) {
245
- return;
104
+ const t = dist / brush.radius;
105
+ let falloff;
106
+ switch (brush.falloff) {
107
+ case "constant":
108
+ falloff = 1;
109
+ break;
110
+ case "linear":
111
+ falloff = 1 - t;
112
+ break;
113
+ case "smooth":
114
+ falloff = 1 - t * t * (3 - 2 * t);
115
+ break;
116
+ default:
117
+ falloff = 1 - t;
246
118
  }
247
- ctx.drawImage(image, 0, 0);
248
- const imageData = ctx.getImageData(0, 0, image.width, image.height);
249
- const posAttr = mesh.geometry.attributes.position;
250
- const seg = config.segments + 1;
251
- for (let i = 0; i < posAttr.count; i++) {
119
+ const influence = falloff * brush.strength;
120
+ switch (brush.mode) {
121
+ case "raise":
122
+ posAttr.setY(i, posAttr.getY(i) + influence);
123
+ break;
124
+ case "lower":
125
+ posAttr.setY(i, posAttr.getY(i) - influence);
126
+ break;
127
+ case "flatten":
128
+ posAttr.setY(i, posAttr.getY(i) + (point.y - posAttr.getY(i)) * influence);
129
+ break;
130
+ case "smooth": {
131
+ let avg = posAttr.getY(i);
132
+ let count = 1;
133
+ const seg = config.segments + 1;
252
134
  const row = Math.floor(i / seg);
253
135
  const col = i % seg;
254
- const px = Math.floor((col / config.segments) * (image.width - 1));
255
- const py = Math.floor((row / config.segments) * (image.height - 1));
256
- const idx = (py * image.width + px) * 4;
257
- const gray = imageData.data[idx] / 255;
258
- posAttr.setY(i, gray * config.maxHeight);
259
- }
260
- posAttr.needsUpdate = true;
261
- mesh.geometry.computeVertexNormals();
262
- onTerrainChange === null || onTerrainChange === void 0 ? void 0 : onTerrainChange(mesh);
263
- }, [config, onTerrainChange]);
264
- const exportHeightmap = useCallback(() => {
265
- const mesh = terrainRef.current;
266
- if (!mesh) {
267
- return null;
268
- }
269
- const posAttr = mesh.geometry.attributes.position;
270
- const seg = config.segments + 1;
271
- const exportCanvas = document.createElement('canvas');
272
- exportCanvas.width = seg;
273
- exportCanvas.height = seg;
274
- const ctx = exportCanvas.getContext('2d');
275
- if (!ctx) {
276
- return null;
277
- }
278
- const imageData = ctx.createImageData(seg, seg);
279
- let minY = Infinity;
280
- let maxY = -Infinity;
281
- for (let i = 0; i < posAttr.count; i++) {
282
- const y = posAttr.getY(i);
283
- if (y < minY) {
284
- minY = y;
136
+ const neighbors = [
137
+ row > 0 ? i - seg : -1,
138
+ row < config.segments ? i + seg : -1,
139
+ col > 0 ? i - 1 : -1,
140
+ col < config.segments ? i + 1 : -1
141
+ ];
142
+ for (const ni of neighbors) {
143
+ if (ni >= 0 && ni < posAttr.count) {
144
+ avg += posAttr.getY(ni);
145
+ count++;
146
+ }
285
147
  }
286
- if (y > maxY) {
287
- maxY = y;
148
+ avg /= count;
149
+ posAttr.setY(i, posAttr.getY(i) + (avg - posAttr.getY(i)) * influence);
150
+ break;
151
+ }
152
+ case "noise":
153
+ posAttr.setY(i, posAttr.getY(i) + (Math.random() - 0.5) * influence * 2);
154
+ break;
155
+ case "paint": {
156
+ if (splatMapRef.current) {
157
+ const offset = i * 4;
158
+ for (let c = 0; c < 4; c++) {
159
+ splatMapRef.current[offset + c] *= 1 - influence * 0.5;
160
+ }
161
+ splatMapRef.current[offset + activeTextureLayer] += influence;
162
+ let total = 0;
163
+ for (let c = 0; c < 4; c++) {
164
+ total += splatMapRef.current[offset + c];
165
+ }
166
+ if (total > 0) {
167
+ for (let c = 0; c < 4; c++) {
168
+ splatMapRef.current[offset + c] /= total;
169
+ }
170
+ }
288
171
  }
172
+ break;
173
+ }
289
174
  }
290
- const range = maxY - minY || 1;
291
- for (let i = 0; i < posAttr.count; i++) {
292
- const gray = Math.round(((posAttr.getY(i) - minY) / range) * 255);
293
- imageData.data[i * 4] = gray;
294
- imageData.data[i * 4 + 1] = gray;
295
- imageData.data[i * 4 + 2] = gray;
296
- imageData.data[i * 4 + 3] = 255;
297
- }
298
- ctx.putImageData(imageData, 0, 0);
299
- return exportCanvas;
300
- }, [config.segments]);
301
- const setTextureLayer = useCallback((index, partial) => {
302
- setTextureLayers((prev) => prev.map((l, i) => (i === index ? { ...l, ...partial } : l)));
303
- }, []);
304
- return {
305
- terrainMesh: terrainRef.current,
306
- brush,
307
- setBrush,
308
- isPainting,
309
- startPainting: () => setIsPainting(true),
310
- stopPainting: () => setIsPainting(false),
311
- generateFlat,
312
- generateNoise,
313
- importHeightmap,
314
- exportHeightmap,
315
- textureLayers,
316
- setTextureLayer,
317
- activeTextureLayer,
318
- setActiveTextureLayer,
175
+ }
176
+ posAttr.needsUpdate = true;
177
+ mesh.geometry.computeVertexNormals();
178
+ onTerrainChange?.(mesh);
179
+ },
180
+ [brush, config.segments, activeTextureLayer, onTerrainChange]
181
+ );
182
+ useEffect(() => {
183
+ if (!canvas || !camera || !isPainting) {
184
+ return;
185
+ }
186
+ const onPointerMove = (e) => {
187
+ const rect = canvas.getBoundingClientRect();
188
+ const mouse = new THREE.Vector2(
189
+ (e.clientX - rect.left) / rect.width * 2 - 1,
190
+ -((e.clientY - rect.top) / rect.height) * 2 + 1
191
+ );
192
+ raycaster.current.setFromCamera(mouse, camera);
193
+ const mesh = terrainRef.current;
194
+ if (!mesh) {
195
+ return;
196
+ }
197
+ const intersects = raycaster.current.intersectObject(mesh);
198
+ if (intersects.length > 0) {
199
+ applyBrushAtPoint(intersects[0].point);
200
+ }
319
201
  };
202
+ canvas.addEventListener("pointermove", onPointerMove);
203
+ return () => canvas.removeEventListener("pointermove", onPointerMove);
204
+ }, [canvas, camera, isPainting, applyBrushAtPoint]);
205
+ const generateFlat = useCallback(() => {
206
+ const mesh = terrainRef.current;
207
+ if (!mesh) {
208
+ return;
209
+ }
210
+ const posAttr = mesh.geometry.attributes.position;
211
+ for (let i = 0; i < posAttr.count; i++) {
212
+ posAttr.setY(i, 0);
213
+ }
214
+ posAttr.needsUpdate = true;
215
+ mesh.geometry.computeVertexNormals();
216
+ onTerrainChange?.(mesh);
217
+ }, [onTerrainChange]);
218
+ const generateNoise = useCallback(
219
+ (octaves = 6, persistence = 0.5, lacunarity = 2) => {
220
+ const mesh = terrainRef.current;
221
+ if (!mesh) {
222
+ return;
223
+ }
224
+ const posAttr = mesh.geometry.attributes.position;
225
+ for (let i = 0; i < posAttr.count; i++) {
226
+ const x = posAttr.getX(i) / config.width;
227
+ const z = posAttr.getZ(i) / config.depth;
228
+ const height = fractalNoise(x, z, octaves, persistence, lacunarity) * config.maxHeight;
229
+ posAttr.setY(i, height);
230
+ }
231
+ posAttr.needsUpdate = true;
232
+ mesh.geometry.computeVertexNormals();
233
+ onTerrainChange?.(mesh);
234
+ },
235
+ [config, onTerrainChange]
236
+ );
237
+ const importHeightmap = useCallback(
238
+ (image) => {
239
+ const mesh = terrainRef.current;
240
+ if (!mesh) {
241
+ return;
242
+ }
243
+ const tmpCanvas = document.createElement("canvas");
244
+ tmpCanvas.width = image.width;
245
+ tmpCanvas.height = image.height;
246
+ const ctx = tmpCanvas.getContext("2d");
247
+ if (!ctx) {
248
+ return;
249
+ }
250
+ ctx.drawImage(image, 0, 0);
251
+ const imageData = ctx.getImageData(0, 0, image.width, image.height);
252
+ const posAttr = mesh.geometry.attributes.position;
253
+ const seg = config.segments + 1;
254
+ for (let i = 0; i < posAttr.count; i++) {
255
+ const row = Math.floor(i / seg);
256
+ const col = i % seg;
257
+ const px = Math.floor(col / config.segments * (image.width - 1));
258
+ const py = Math.floor(row / config.segments * (image.height - 1));
259
+ const idx = (py * image.width + px) * 4;
260
+ const gray = imageData.data[idx] / 255;
261
+ posAttr.setY(i, gray * config.maxHeight);
262
+ }
263
+ posAttr.needsUpdate = true;
264
+ mesh.geometry.computeVertexNormals();
265
+ onTerrainChange?.(mesh);
266
+ },
267
+ [config, onTerrainChange]
268
+ );
269
+ const exportHeightmap = useCallback(() => {
270
+ const mesh = terrainRef.current;
271
+ if (!mesh) {
272
+ return null;
273
+ }
274
+ const posAttr = mesh.geometry.attributes.position;
275
+ const seg = config.segments + 1;
276
+ const exportCanvas = document.createElement("canvas");
277
+ exportCanvas.width = seg;
278
+ exportCanvas.height = seg;
279
+ const ctx = exportCanvas.getContext("2d");
280
+ if (!ctx) {
281
+ return null;
282
+ }
283
+ const imageData = ctx.createImageData(seg, seg);
284
+ let minY = Infinity;
285
+ let maxY = -Infinity;
286
+ for (let i = 0; i < posAttr.count; i++) {
287
+ const y = posAttr.getY(i);
288
+ if (y < minY) {
289
+ minY = y;
290
+ }
291
+ if (y > maxY) {
292
+ maxY = y;
293
+ }
294
+ }
295
+ const range = maxY - minY || 1;
296
+ for (let i = 0; i < posAttr.count; i++) {
297
+ const gray = Math.round((posAttr.getY(i) - minY) / range * 255);
298
+ imageData.data[i * 4] = gray;
299
+ imageData.data[i * 4 + 1] = gray;
300
+ imageData.data[i * 4 + 2] = gray;
301
+ imageData.data[i * 4 + 3] = 255;
302
+ }
303
+ ctx.putImageData(imageData, 0, 0);
304
+ return exportCanvas;
305
+ }, [config.segments]);
306
+ const setTextureLayer = useCallback((index, partial) => {
307
+ setTextureLayers((prev) => prev.map((l, i) => i === index ? { ...l, ...partial } : l));
308
+ }, []);
309
+ return {
310
+ terrainMesh: terrainRef.current,
311
+ brush,
312
+ setBrush,
313
+ isPainting,
314
+ startPainting: () => setIsPainting(true),
315
+ stopPainting: () => setIsPainting(false),
316
+ generateFlat,
317
+ generateNoise,
318
+ importHeightmap,
319
+ exportHeightmap,
320
+ textureLayers,
321
+ setTextureLayer,
322
+ activeTextureLayer,
323
+ setActiveTextureLayer
324
+ };
320
325
  }
321
- /* ── Component ── */
322
- const NiceTerrainEditor = ({ scene, camera, canvas, config: configProp, onTerrainChange, className = '', tutorial, }) => {
323
- const { t } = useNiceTranslation();
324
- const config = {
325
- width: 100,
326
- depth: 100,
327
- segments: 128,
328
- maxHeight: 20,
329
- ...configProp,
330
- };
331
- const { brush, setBrush, isPainting, startPainting, stopPainting, generateFlat, generateNoise, exportHeightmap, textureLayers, setTextureLayer, activeTextureLayer, setActiveTextureLayer, } = useTerrainEditor(scene, camera, canvas, config, onTerrainChange);
332
- const [noiseOctaves, setNoiseOctaves] = useState(6);
333
- const [noisePersistence, setNoisePersistence] = useState(0.5);
334
- return (jsxs("div", { className: `nice-terrain-editor ${className}`, style: { position: 'relative', display: 'grid', gap: 8, fontSize: 13 }, children: [jsx("span", { style: { position: 'absolute', top: 0, right: 0, zIndex: 20 }, children: jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }), jsx("strong", { children: t('terrainEditor.terrainEditor', 'Terrain Editor') }), jsxs("fieldset", { style: {
335
- border: '1px solid var(--nice-border, #e2e8f0)',
336
- borderRadius: 6,
337
- padding: 4,
338
- display: 'flex',
339
- flexWrap: 'wrap',
340
- gap: 2,
341
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.brush', 'Brush') }), ['raise', 'lower', 'flatten', 'smooth', 'noise', 'paint'].map((m) => (jsx("button", { type: "button", onClick: () => setBrush({ mode: m }), style: {
342
- padding: '3px 6px',
343
- fontSize: 11,
344
- cursor: 'pointer',
345
- borderRadius: 4,
346
- border: '1px solid var(--nice-border, #e2e8f0)',
347
- background: brush.mode === m ? 'var(--nice-color-accent, #6366f1)' : 'transparent',
348
- color: brush.mode === m ? 'var(--nice-color-white, #fff)' : 'inherit',
349
- }, children: m }, m)))] }), jsxs("label", { children: [t('terrainEditor.radius', 'Radius: {radius}').replace('{radius}', String(brush.radius.toFixed(1))), jsx("input", { type: "range", min: 0.5, max: 20, step: 0.5, value: brush.radius, onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsxs("label", { children: [t('terrainEditor.strength', 'Strength: {strength}').replace('{strength}', String(brush.strength.toFixed(2))), jsx("input", { type: "range", min: 0.01, max: 1, step: 0.01, value: brush.strength, onChange: (e) => setBrush({ strength: parseFloat(e.target.value) }), style: { width: '100%' } })] }), jsx("button", { type: "button", onClick: isPainting ? stopPainting : startPainting, style: {
350
- background: isPainting
351
- ? 'var(--nice-danger, #ef4444)'
352
- : 'var(--nice-color-accent, #6366f1)',
353
- color: 'var(--nice-color-white, #fff)',
354
- border: 'none',
355
- borderRadius: 4,
356
- padding: '6px 12px',
357
- cursor: 'pointer',
358
- }, children: isPainting ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "stop", size: 14 }), " ", t('terrainEditor.stopPainting', 'Stop Painting')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "brush", size: 14 }), " ", t('terrainEditor.startPainting', 'Start Painting')] })) }), jsxs("fieldset", { style: {
359
- border: '1px solid var(--nice-border, #e2e8f0)',
360
- borderRadius: 6,
361
- padding: 6,
362
- display: 'grid',
363
- gap: 4,
364
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.procedural', 'Procedural') }), jsxs("label", { children: [t('terrainEditor.octaves', 'Octaves: {noiseOctaves}').replace('{noiseOctaves}', String(noiseOctaves)), jsx("input", { type: "range", min: 1, max: 10, value: noiseOctaves, onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)), style: { width: '100%' } })] }), jsxs("label", { children: [t('terrainEditor.persistence', 'Persistence: {noisePersistence}').replace('{noisePersistence}', String(noisePersistence.toFixed(2))), jsx("input", { type: "range", min: 0.1, max: 1, step: 0.05, value: noisePersistence, onChange: (e) => setNoisePersistence(parseFloat(e.target.value)), style: { width: '100%' } })] }), jsxs("div", { style: { display: 'flex', gap: 4 }, children: [jsxs("button", { type: "button", onClick: () => generateNoise(noiseOctaves, noisePersistence), style: { flex: 1 }, children: [jsx(NiceIcon, { name: "mountain", size: 14 }), ' ', t('terrainEditor.generateNoise', 'Generate Noise')] }), jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [jsx(NiceIcon, { name: "square", size: 14 }), " ", t('terrainEditor.flatten', 'Flatten')] })] })] }), brush.mode === 'paint' && (jsxs("fieldset", { style: {
365
- border: '1px solid var(--nice-border, #e2e8f0)',
366
- borderRadius: 6,
367
- padding: 6,
368
- display: 'grid',
369
- gap: 4,
370
- }, children: [jsx("legend", { style: { fontSize: 11 }, children: t('terrainEditor.textureLayers', 'Texture Layers') }), textureLayers.map((layer, i) => (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [jsx("input", { type: "radio", name: "texLayer", checked: activeTextureLayer === i, onChange: () => setActiveTextureLayer(i) }), jsx("input", { type: "text", value: layer.name, onChange: (e) => setTextureLayer(i, { name: e.target.value }), style: {
371
- flex: 1,
372
- padding: '2px 6px',
373
- border: '1px solid var(--nice-border, #e2e8f0)',
374
- borderRadius: 4,
375
- } })] }, i)))] })), jsxs("button", { type: "button", onClick: () => {
376
- const c = exportHeightmap();
377
- if (c) {
378
- const link = document.createElement('a');
379
- link.download = 'heightmap.png';
380
- link.href = c.toDataURL('image/png');
381
- link.click();
326
+ const NiceTerrainEditor = ({
327
+ scene,
328
+ camera,
329
+ canvas,
330
+ config: configProp,
331
+ onTerrainChange,
332
+ className = "",
333
+ tutorial
334
+ }) => {
335
+ const { t } = useNiceTranslation();
336
+ const config = {
337
+ width: 100,
338
+ depth: 100,
339
+ segments: 128,
340
+ maxHeight: 20,
341
+ ...configProp
342
+ };
343
+ const {
344
+ brush,
345
+ setBrush,
346
+ isPainting,
347
+ startPainting,
348
+ stopPainting,
349
+ generateFlat,
350
+ generateNoise,
351
+ exportHeightmap,
352
+ textureLayers,
353
+ setTextureLayer,
354
+ activeTextureLayer,
355
+ setActiveTextureLayer
356
+ } = useTerrainEditor(scene, camera, canvas, config, onTerrainChange);
357
+ const [noiseOctaves, setNoiseOctaves] = useState(6);
358
+ const [noisePersistence, setNoisePersistence] = useState(0.5);
359
+ return /* @__PURE__ */ jsxs(
360
+ "div",
361
+ {
362
+ className: `nice-terrain-editor ${className}`,
363
+ style: { position: "relative", display: "grid", gap: 8, fontSize: 13 },
364
+ children: [
365
+ /* @__PURE__ */ jsx("span", { style: { position: "absolute", top: 0, right: 0, zIndex: 20 }, children: /* @__PURE__ */ jsx(NiceTutorialButton, { steps: resolveTutorial(tutorial, TERRAIN_EDITOR_TOUR) }) }),
366
+ /* @__PURE__ */ jsx("strong", { children: t("terrainEditor.terrainEditor", "Terrain Editor") }),
367
+ /* @__PURE__ */ jsxs(
368
+ "fieldset",
369
+ {
370
+ style: {
371
+ border: "1px solid var(--nice-border, #e2e8f0)",
372
+ borderRadius: 6,
373
+ padding: 4,
374
+ display: "flex",
375
+ flexWrap: "wrap",
376
+ gap: 2
377
+ },
378
+ children: [
379
+ /* @__PURE__ */ jsx("legend", { style: { fontSize: 11 }, children: t("terrainEditor.brush", "Brush") }),
380
+ ["raise", "lower", "flatten", "smooth", "noise", "paint"].map(
381
+ (m) => /* @__PURE__ */ jsx(
382
+ "button",
383
+ {
384
+ type: "button",
385
+ onClick: () => setBrush({ mode: m }),
386
+ style: {
387
+ padding: "3px 6px",
388
+ fontSize: 11,
389
+ cursor: "pointer",
390
+ borderRadius: 4,
391
+ border: "1px solid var(--nice-border, #e2e8f0)",
392
+ background: brush.mode === m ? "var(--nice-color-accent, #6366f1)" : "transparent",
393
+ color: brush.mode === m ? "var(--nice-color-white, #fff)" : "inherit"
394
+ },
395
+ children: m
396
+ },
397
+ m
398
+ )
399
+ )
400
+ ]
401
+ }
402
+ ),
403
+ /* @__PURE__ */ jsxs("label", { children: [
404
+ t("terrainEditor.radius", "Radius: {radius}").replace(
405
+ "{radius}",
406
+ String(brush.radius.toFixed(1))
407
+ ),
408
+ /* @__PURE__ */ jsx(
409
+ "input",
410
+ {
411
+ type: "range",
412
+ min: 0.5,
413
+ max: 20,
414
+ step: 0.5,
415
+ value: brush.radius,
416
+ onChange: (e) => setBrush({ radius: parseFloat(e.target.value) }),
417
+ style: { width: "100%" }
418
+ }
419
+ )
420
+ ] }),
421
+ /* @__PURE__ */ jsxs("label", { children: [
422
+ t("terrainEditor.strength", "Strength: {strength}").replace(
423
+ "{strength}",
424
+ String(brush.strength.toFixed(2))
425
+ ),
426
+ /* @__PURE__ */ jsx(
427
+ "input",
428
+ {
429
+ type: "range",
430
+ min: 0.01,
431
+ max: 1,
432
+ step: 0.01,
433
+ value: brush.strength,
434
+ onChange: (e) => setBrush({ strength: parseFloat(e.target.value) }),
435
+ style: { width: "100%" }
436
+ }
437
+ )
438
+ ] }),
439
+ /* @__PURE__ */ jsx(
440
+ "button",
441
+ {
442
+ type: "button",
443
+ onClick: isPainting ? stopPainting : startPainting,
444
+ style: {
445
+ background: isPainting ? "var(--nice-danger, #ef4444)" : "var(--nice-color-accent, #6366f1)",
446
+ color: "var(--nice-color-white, #fff)",
447
+ border: "none",
448
+ borderRadius: 4,
449
+ padding: "6px 12px",
450
+ cursor: "pointer"
451
+ },
452
+ children: isPainting ? /* @__PURE__ */ jsxs(Fragment, { children: [
453
+ /* @__PURE__ */ jsx(NiceIcon, { name: "stop", size: 14 }),
454
+ " ",
455
+ t("terrainEditor.stopPainting", "Stop Painting")
456
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
457
+ /* @__PURE__ */ jsx(NiceIcon, { name: "brush", size: 14 }),
458
+ " ",
459
+ t("terrainEditor.startPainting", "Start Painting")
460
+ ] })
461
+ }
462
+ ),
463
+ /* @__PURE__ */ jsxs(
464
+ "fieldset",
465
+ {
466
+ style: {
467
+ border: "1px solid var(--nice-border, #e2e8f0)",
468
+ borderRadius: 6,
469
+ padding: 6,
470
+ display: "grid",
471
+ gap: 4
472
+ },
473
+ children: [
474
+ /* @__PURE__ */ jsx("legend", { style: { fontSize: 11 }, children: t("terrainEditor.procedural", "Procedural") }),
475
+ /* @__PURE__ */ jsxs("label", { children: [
476
+ t("terrainEditor.octaves", "Octaves: {noiseOctaves}").replace(
477
+ "{noiseOctaves}",
478
+ String(noiseOctaves)
479
+ ),
480
+ /* @__PURE__ */ jsx(
481
+ "input",
482
+ {
483
+ type: "range",
484
+ min: 1,
485
+ max: 10,
486
+ value: noiseOctaves,
487
+ onChange: (e) => setNoiseOctaves(parseInt(e.target.value, 10)),
488
+ style: { width: "100%" }
489
+ }
490
+ )
491
+ ] }),
492
+ /* @__PURE__ */ jsxs("label", { children: [
493
+ t("terrainEditor.persistence", "Persistence: {noisePersistence}").replace(
494
+ "{noisePersistence}",
495
+ String(noisePersistence.toFixed(2))
496
+ ),
497
+ /* @__PURE__ */ jsx(
498
+ "input",
499
+ {
500
+ type: "range",
501
+ min: 0.1,
502
+ max: 1,
503
+ step: 0.05,
504
+ value: noisePersistence,
505
+ onChange: (e) => setNoisePersistence(parseFloat(e.target.value)),
506
+ style: { width: "100%" }
507
+ }
508
+ )
509
+ ] }),
510
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4 }, children: [
511
+ /* @__PURE__ */ jsxs(
512
+ "button",
513
+ {
514
+ type: "button",
515
+ onClick: () => generateNoise(noiseOctaves, noisePersistence),
516
+ style: { flex: 1 },
517
+ children: [
518
+ /* @__PURE__ */ jsx(NiceIcon, { name: "mountain", size: 14 }),
519
+ " ",
520
+ t("terrainEditor.generateNoise", "Generate Noise")
521
+ ]
522
+ }
523
+ ),
524
+ /* @__PURE__ */ jsxs("button", { type: "button", onClick: generateFlat, style: { flex: 1 }, children: [
525
+ /* @__PURE__ */ jsx(NiceIcon, { name: "square", size: 14 }),
526
+ " ",
527
+ t("terrainEditor.flatten", "Flatten")
528
+ ] })
529
+ ] })
530
+ ]
531
+ }
532
+ ),
533
+ brush.mode === "paint" && /* @__PURE__ */ jsxs(
534
+ "fieldset",
535
+ {
536
+ style: {
537
+ border: "1px solid var(--nice-border, #e2e8f0)",
538
+ borderRadius: 6,
539
+ padding: 6,
540
+ display: "grid",
541
+ gap: 4
542
+ },
543
+ children: [
544
+ /* @__PURE__ */ jsx("legend", { style: { fontSize: 11 }, children: t("terrainEditor.textureLayers", "Texture Layers") }),
545
+ textureLayers.map((layer, i) => /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
546
+ /* @__PURE__ */ jsx(
547
+ "input",
548
+ {
549
+ type: "radio",
550
+ name: "texLayer",
551
+ checked: activeTextureLayer === i,
552
+ onChange: () => setActiveTextureLayer(i)
553
+ }
554
+ ),
555
+ /* @__PURE__ */ jsx(
556
+ "input",
557
+ {
558
+ type: "text",
559
+ value: layer.name,
560
+ onChange: (e) => setTextureLayer(i, { name: e.target.value }),
561
+ style: {
562
+ flex: 1,
563
+ padding: "2px 6px",
564
+ border: "1px solid var(--nice-border, #e2e8f0)",
565
+ borderRadius: 4
382
566
  }
383
- }, style: { fontSize: 11 }, children: [jsx(NiceIcon, { name: "download", size: 14 }), ' ', t('terrainEditor.exportHeightmapPng', 'Export Heightmap PNG')] })] }));
567
+ }
568
+ )
569
+ ] }, i))
570
+ ]
571
+ }
572
+ ),
573
+ /* @__PURE__ */ jsxs(
574
+ "button",
575
+ {
576
+ type: "button",
577
+ onClick: () => {
578
+ const c = exportHeightmap();
579
+ if (c) {
580
+ const link = document.createElement("a");
581
+ link.download = "heightmap.png";
582
+ link.href = c.toDataURL("image/png");
583
+ link.click();
584
+ }
585
+ },
586
+ style: { fontSize: 11 },
587
+ children: [
588
+ /* @__PURE__ */ jsx(NiceIcon, { name: "download", size: 14 }),
589
+ " ",
590
+ t("terrainEditor.exportHeightmapPng", "Export Heightmap PNG")
591
+ ]
592
+ }
593
+ )
594
+ ]
595
+ }
596
+ );
597
+ };
598
+ export {
599
+ NiceTerrainEditor,
600
+ useTerrainEditor
384
601
  };
385
-
386
- export { NiceTerrainEditor, useTerrainEditor };