@nice2dev/ui-3d 1.3.15 → 1.3.17

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