@nice2dev/ui-3d 1.3.15 → 1.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/cjs/collaborative/collaborativeScene.js +160 -206
  3. package/dist/cjs/core/editorAsk.js +51 -41
  4. package/dist/cjs/core/i18n.js +13 -14
  5. package/dist/cjs/core/threeCompat.js +29 -42
  6. package/dist/cjs/dance/DanceBridge.js +148 -177
  7. package/dist/cjs/dance/DanceScoreEngine.js +209 -221
  8. package/dist/cjs/dance/PoseDetector.js +302 -275
  9. package/dist/cjs/index.js +110 -113
  10. package/dist/cjs/material/MaterialEditor.module.css.js +242 -5
  11. package/dist/cjs/material/NiceMaterialEditor.js +1213 -757
  12. package/dist/cjs/material/materialEditorTypes.js +35 -49
  13. package/dist/cjs/material/materialEditorUtils.js +684 -747
  14. package/dist/cjs/material/materialNodeDefinitions.js +1411 -1459
  15. package/dist/cjs/model/ModelEditor.js +90 -40
  16. package/dist/cjs/model/ModelEditor.module.css.js +257 -5
  17. package/dist/cjs/model/ModelEditorLeftPanel.js +126 -47
  18. package/dist/cjs/model/ModelEditorMenuBar.js +359 -32
  19. package/dist/cjs/model/ModelEditorRightPanel.js +647 -131
  20. package/dist/cjs/model/ModelEditorSubComponents.js +277 -112
  21. package/dist/cjs/model/ModelEditorTimeline.js +232 -34
  22. package/dist/cjs/model/ModelEditorToolbar.js +170 -14
  23. package/dist/cjs/model/ModelEditorViewport.js +231 -39
  24. package/dist/cjs/model/ModelViewer.js +388 -63
  25. package/dist/cjs/model/ModelViewer.module.css.js +77 -5
  26. package/dist/cjs/model/NiceArmatureEditor.js +473 -274
  27. package/dist/cjs/model/NiceMorphTargetEditor.js +333 -227
  28. package/dist/cjs/model/NiceOctree.js +372 -320
  29. package/dist/cjs/model/NicePhysicsSimulation.js +528 -298
  30. package/dist/cjs/model/NiceProceduralGeometry.js +673 -300
  31. package/dist/cjs/model/NiceTerrainEditor.js +595 -385
  32. package/dist/cjs/model/NiceWeightPainter.js +384 -261
  33. package/dist/cjs/model/NiceXRPreview.js +385 -308
  34. package/dist/cjs/model/cadModeUtils.js +103 -106
  35. package/dist/cjs/model/editorShortcuts.js +171 -194
  36. package/dist/cjs/model/modelEditorTypes.js +100 -118
  37. package/dist/cjs/model/modelEditorUtils.js +1061 -1146
  38. package/dist/cjs/model/simsModeUtils.js +1023 -1053
  39. package/dist/cjs/model/useModelEditor.js +1977 -1933
  40. package/dist/cjs/model/useModelViewer.js +633 -665
  41. package/dist/cjs/nice2dev-ui-3d.css +1 -1
  42. package/dist/cjs/particle/NiceParticleEditor.js +1186 -571
  43. package/dist/cjs/particle/ParticleEditor.module.css.js +122 -5
  44. package/dist/cjs/particle/particleEditorTypes.js +62 -70
  45. package/dist/cjs/particle/particleEditorUtils.js +998 -1013
  46. package/dist/cjs/rendering/NiceCascadedShadows.js +381 -259
  47. package/dist/cjs/rendering/NiceRenderExport.js +424 -337
  48. package/dist/cjs/rendering/NiceSSAO.js +454 -337
  49. package/dist/cjs/rendering/NiceSSR.js +387 -261
  50. package/dist/cjs/rendering/NiceWebGPURenderer.js +359 -230
  51. package/dist/cjs/uv/NiceUVEditor.js +787 -550
  52. package/dist/cjs/uv/UVEditor.module.css.js +53 -5
  53. package/dist/cjs/uv/uvEditorTypes.js +59 -76
  54. package/dist/cjs/uv/uvEditorUtils.js +501 -575
  55. package/dist/esm/collaborative/collaborativeScene.js +163 -205
  56. package/dist/esm/core/editorAsk.js +53 -40
  57. package/dist/esm/core/i18n.js +15 -13
  58. package/dist/esm/core/threeCompat.js +18 -26
  59. package/dist/esm/dance/DanceBridge.js +151 -176
  60. package/dist/esm/dance/DanceScoreEngine.js +211 -220
  61. package/dist/esm/dance/PoseDetector.js +304 -274
  62. package/dist/esm/index.js +258 -36
  63. package/dist/esm/material/MaterialEditor.module.css.js +242 -3
  64. package/dist/esm/material/NiceMaterialEditor.js +1215 -756
  65. package/dist/esm/material/materialEditorTypes.js +45 -48
  66. package/dist/esm/material/materialEditorUtils.js +713 -746
  67. package/dist/esm/material/materialNodeDefinitions.js +1415 -1458
  68. package/dist/esm/model/ModelEditor.js +91 -39
  69. package/dist/esm/model/ModelEditor.module.css.js +257 -3
  70. package/dist/esm/model/ModelEditorLeftPanel.js +126 -45
  71. package/dist/esm/model/ModelEditorMenuBar.js +359 -30
  72. package/dist/esm/model/ModelEditorRightPanel.js +634 -114
  73. package/dist/esm/model/ModelEditorSubComponents.js +265 -94
  74. package/dist/esm/model/ModelEditorTimeline.js +232 -32
  75. package/dist/esm/model/ModelEditorToolbar.js +170 -12
  76. package/dist/esm/model/ModelEditorViewport.js +231 -37
  77. package/dist/esm/model/ModelViewer.js +389 -62
  78. package/dist/esm/model/ModelViewer.module.css.js +77 -3
  79. package/dist/esm/model/NiceArmatureEditor.js +462 -258
  80. package/dist/esm/model/NiceMorphTargetEditor.js +322 -211
  81. package/dist/esm/model/NiceOctree.js +361 -304
  82. package/dist/esm/model/NicePhysicsSimulation.js +517 -282
  83. package/dist/esm/model/NiceProceduralGeometry.js +667 -284
  84. package/dist/esm/model/NiceTerrainEditor.js +584 -369
  85. package/dist/esm/model/NiceWeightPainter.js +373 -245
  86. package/dist/esm/model/NiceXRPreview.js +374 -292
  87. package/dist/esm/model/cadModeUtils.js +95 -88
  88. package/dist/esm/model/editorShortcuts.js +172 -193
  89. package/dist/esm/model/modelEditorTypes.js +90 -100
  90. package/dist/esm/model/modelEditorUtils.js +1056 -1128
  91. package/dist/esm/model/simsModeUtils.js +1021 -1035
  92. package/dist/esm/model/useModelEditor.js +1946 -1920
  93. package/dist/esm/model/useModelViewer.js +619 -647
  94. package/dist/esm/nice2dev-ui-3d.css +1 -1
  95. package/dist/esm/particle/NiceParticleEditor.js +1188 -570
  96. package/dist/esm/particle/ParticleEditor.module.css.js +122 -3
  97. package/dist/esm/particle/particleEditorTypes.js +69 -69
  98. package/dist/esm/particle/particleEditorUtils.js +1027 -1012
  99. package/dist/esm/rendering/NiceCascadedShadows.js +370 -243
  100. package/dist/esm/rendering/NiceRenderExport.js +413 -321
  101. package/dist/esm/rendering/NiceSSAO.js +450 -328
  102. package/dist/esm/rendering/NiceSSR.js +383 -252
  103. package/dist/esm/rendering/NiceWebGPURenderer.js +326 -214
  104. package/dist/esm/uv/NiceUVEditor.js +788 -549
  105. package/dist/esm/uv/UVEditor.module.css.js +53 -3
  106. package/dist/esm/uv/uvEditorTypes.js +68 -75
  107. package/dist/esm/uv/uvEditorUtils.js +515 -557
  108. package/package.json +7 -12
@@ -1,343 +1,435 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { NiceIcon } from '@nice2dev/ui';
3
- import { useState, useCallback } from 'react';
4
- import * as THREE from 'three';
5
- import { useNiceTranslation } from '../core/i18n.js';
6
-
7
- /* ── EXR writer (minimal, uncompressed) ── */
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { NiceIcon } from "@nice2dev/ui";
3
+ import { useState, useCallback } from "react";
4
+ import * as THREE from "three";
5
+ import { useNiceTranslation } from "../core/i18n.js";
8
6
  function writeEXR(width, height, floatData) {
9
- var _a;
10
- // Minimal OpenEXR file: magic + version + header + scanlines
11
- const channels = 4; // RGBA
12
- const channelNames = ['A', 'B', 'G', 'R']; // EXR stores in alphabetical order
13
- // Helper to write string
14
- function stringBytes(s) {
15
- const bytes = new Uint8Array(s.length + 1);
16
- for (let i = 0; i < s.length; i++) {
17
- bytes[i] = s.charCodeAt(i);
18
- }
19
- bytes[s.length] = 0;
20
- return bytes;
7
+ const channels = 4;
8
+ const channelNames = ["A", "B", "G", "R"];
9
+ function stringBytes(s) {
10
+ const bytes = new Uint8Array(s.length + 1);
11
+ for (let i = 0; i < s.length; i++) {
12
+ bytes[i] = s.charCodeAt(i);
21
13
  }
22
- // Build the raw pixel data (scanline order, bottom-to-top for EXR)
23
- const scanlineDataSize = width * channels * 4; // per scanline, float32 per channel
24
- const totalPixelData = height * (scanlineDataSize + 8); // +8 for y-coord and size per scanline
25
- // Instead of building a full EXR, we'll use a simpler approach:
26
- // Create the EXR as a blob with proper header
27
- const headerSize = 512; // conservative estimate
28
- const buffer = new ArrayBuffer(headerSize + totalPixelData);
29
- const view = new DataView(buffer);
30
- let offset = 0;
31
- // Magic number
32
- view.setUint32(offset, 20000630, true);
33
- offset += 4;
34
- // Version (2, single-part, scanline)
14
+ bytes[s.length] = 0;
15
+ return bytes;
16
+ }
17
+ const scanlineDataSize = width * channels * 4;
18
+ const totalPixelData = height * (scanlineDataSize + 8);
19
+ const headerSize = 512;
20
+ const buffer = new ArrayBuffer(headerSize + totalPixelData);
21
+ const view = new DataView(buffer);
22
+ let offset = 0;
23
+ view.setUint32(offset, 20000630, true);
24
+ offset += 4;
25
+ view.setUint32(offset, 2, true);
26
+ offset += 4;
27
+ const channelsStr = stringBytes("channels");
28
+ new Uint8Array(buffer, offset, channelsStr.length).set(channelsStr);
29
+ offset += channelsStr.length;
30
+ const chlistStr = stringBytes("chlist");
31
+ new Uint8Array(buffer, offset, chlistStr.length).set(chlistStr);
32
+ offset += chlistStr.length;
33
+ const channelListStart = offset;
34
+ view.setUint32(offset, 0, true);
35
+ offset += 4;
36
+ const channelListDataStart = offset;
37
+ for (const name of channelNames) {
38
+ const nameBytes = stringBytes(name);
39
+ new Uint8Array(buffer, offset, nameBytes.length).set(nameBytes);
40
+ offset += nameBytes.length;
35
41
  view.setUint32(offset, 2, true);
36
42
  offset += 4;
37
- // Write attribute: channels
38
- const channelsStr = stringBytes('channels');
39
- new Uint8Array(buffer, offset, channelsStr.length).set(channelsStr);
40
- offset += channelsStr.length;
41
- const chlistStr = stringBytes('chlist');
42
- new Uint8Array(buffer, offset, chlistStr.length).set(chlistStr);
43
- offset += chlistStr.length;
44
- // Channel list size
45
- const channelListStart = offset;
46
- view.setUint32(offset, 0, true);
47
- offset += 4; // placeholder for size
48
- const channelListDataStart = offset;
49
- for (const name of channelNames) {
50
- const nameBytes = stringBytes(name);
51
- new Uint8Array(buffer, offset, nameBytes.length).set(nameBytes);
52
- offset += nameBytes.length;
53
- view.setUint32(offset, 2, true);
54
- offset += 4; // FLOAT pixel type
55
- view.setUint8(offset, 0);
56
- offset += 1; // pLinear
57
- offset += 3; // reserved
58
- view.setInt32(offset, 1, true);
59
- offset += 4; // xSampling
60
- view.setInt32(offset, 1, true);
61
- offset += 4; // ySampling
62
- }
63
- view.setUint8(offset, 0);
64
- offset += 1; // null terminate channel list
65
- // Update channel list size
66
- view.setUint32(channelListStart, offset - channelListDataStart, true);
67
- // compression attribute
68
- const compStr = stringBytes('compression');
69
- new Uint8Array(buffer, offset, compStr.length).set(compStr);
70
- offset += compStr.length;
71
- const compTypeStr = stringBytes('compression');
72
- new Uint8Array(buffer, offset, compTypeStr.length).set(compTypeStr);
73
- offset += compTypeStr.length;
74
- view.setUint32(offset, 1, true);
75
- offset += 4; // size
76
43
  view.setUint8(offset, 0);
77
- offset += 1; // NO_COMPRESSION
78
- // dataWindow attribute
79
- const dwStr = stringBytes('dataWindow');
80
- new Uint8Array(buffer, offset, dwStr.length).set(dwStr);
81
- offset += dwStr.length;
82
- const box2iStr = stringBytes('box2i');
83
- new Uint8Array(buffer, offset, box2iStr.length).set(box2iStr);
84
- offset += box2iStr.length;
85
- view.setUint32(offset, 16, true);
86
- offset += 4; // size
87
- view.setInt32(offset, 0, true);
88
- offset += 4; // xMin
89
- view.setInt32(offset, 0, true);
90
- offset += 4; // yMin
91
- view.setInt32(offset, width - 1, true);
92
- offset += 4; // xMax
93
- view.setInt32(offset, height - 1, true);
94
- offset += 4; // yMax
95
- // displayWindow attribute
96
- const dispStr = stringBytes('displayWindow');
97
- new Uint8Array(buffer, offset, dispStr.length).set(dispStr);
98
- offset += dispStr.length;
99
- new Uint8Array(buffer, offset, box2iStr.length).set(box2iStr);
100
- offset += box2iStr.length;
101
- view.setUint32(offset, 16, true);
102
- offset += 4;
103
- view.setInt32(offset, 0, true);
104
- offset += 4;
105
- view.setInt32(offset, 0, true);
106
- offset += 4;
107
- view.setInt32(offset, width - 1, true);
108
- offset += 4;
109
- view.setInt32(offset, height - 1, true);
44
+ offset += 1;
45
+ offset += 3;
46
+ view.setInt32(offset, 1, true);
110
47
  offset += 4;
111
- // lineOrder attribute
112
- const loStr = stringBytes('lineOrder');
113
- new Uint8Array(buffer, offset, loStr.length).set(loStr);
114
- offset += loStr.length;
115
- const loTypeStr = stringBytes('lineOrder');
116
- new Uint8Array(buffer, offset, loTypeStr.length).set(loTypeStr);
117
- offset += loTypeStr.length;
118
- view.setUint32(offset, 1, true);
48
+ view.setInt32(offset, 1, true);
119
49
  offset += 4;
120
- view.setUint8(offset, 0);
121
- offset += 1; // INCREASING_Y
122
- // pixelAspectRatio
123
- const parStr = stringBytes('pixelAspectRatio');
124
- new Uint8Array(buffer, offset, parStr.length).set(parStr);
125
- offset += parStr.length;
126
- const floatStr = stringBytes('float');
127
- new Uint8Array(buffer, offset, floatStr.length).set(floatStr);
128
- offset += floatStr.length;
129
- view.setUint32(offset, 4, true);
50
+ }
51
+ view.setUint8(offset, 0);
52
+ offset += 1;
53
+ view.setUint32(channelListStart, offset - channelListDataStart, true);
54
+ const compStr = stringBytes("compression");
55
+ new Uint8Array(buffer, offset, compStr.length).set(compStr);
56
+ offset += compStr.length;
57
+ const compTypeStr = stringBytes("compression");
58
+ new Uint8Array(buffer, offset, compTypeStr.length).set(compTypeStr);
59
+ offset += compTypeStr.length;
60
+ view.setUint32(offset, 1, true);
61
+ offset += 4;
62
+ view.setUint8(offset, 0);
63
+ offset += 1;
64
+ const dwStr = stringBytes("dataWindow");
65
+ new Uint8Array(buffer, offset, dwStr.length).set(dwStr);
66
+ offset += dwStr.length;
67
+ const box2iStr = stringBytes("box2i");
68
+ new Uint8Array(buffer, offset, box2iStr.length).set(box2iStr);
69
+ offset += box2iStr.length;
70
+ view.setUint32(offset, 16, true);
71
+ offset += 4;
72
+ view.setInt32(offset, 0, true);
73
+ offset += 4;
74
+ view.setInt32(offset, 0, true);
75
+ offset += 4;
76
+ view.setInt32(offset, width - 1, true);
77
+ offset += 4;
78
+ view.setInt32(offset, height - 1, true);
79
+ offset += 4;
80
+ const dispStr = stringBytes("displayWindow");
81
+ new Uint8Array(buffer, offset, dispStr.length).set(dispStr);
82
+ offset += dispStr.length;
83
+ new Uint8Array(buffer, offset, box2iStr.length).set(box2iStr);
84
+ offset += box2iStr.length;
85
+ view.setUint32(offset, 16, true);
86
+ offset += 4;
87
+ view.setInt32(offset, 0, true);
88
+ offset += 4;
89
+ view.setInt32(offset, 0, true);
90
+ offset += 4;
91
+ view.setInt32(offset, width - 1, true);
92
+ offset += 4;
93
+ view.setInt32(offset, height - 1, true);
94
+ offset += 4;
95
+ const loStr = stringBytes("lineOrder");
96
+ new Uint8Array(buffer, offset, loStr.length).set(loStr);
97
+ offset += loStr.length;
98
+ const loTypeStr = stringBytes("lineOrder");
99
+ new Uint8Array(buffer, offset, loTypeStr.length).set(loTypeStr);
100
+ offset += loTypeStr.length;
101
+ view.setUint32(offset, 1, true);
102
+ offset += 4;
103
+ view.setUint8(offset, 0);
104
+ offset += 1;
105
+ const parStr = stringBytes("pixelAspectRatio");
106
+ new Uint8Array(buffer, offset, parStr.length).set(parStr);
107
+ offset += parStr.length;
108
+ const floatStr = stringBytes("float");
109
+ new Uint8Array(buffer, offset, floatStr.length).set(floatStr);
110
+ offset += floatStr.length;
111
+ view.setUint32(offset, 4, true);
112
+ offset += 4;
113
+ view.setFloat32(offset, 1, true);
114
+ offset += 4;
115
+ view.setUint8(offset, 0);
116
+ offset += 1;
117
+ const tableOffset = offset;
118
+ offset += height * 8;
119
+ for (let y = 0; y < height; y++) {
120
+ const scanView = new DataView(buffer, tableOffset + y * 8, 8);
121
+ scanView.setUint32(0, offset, true);
122
+ scanView.setUint32(4, 0, true);
123
+ view.setInt32(offset, y, true);
130
124
  offset += 4;
131
- view.setFloat32(offset, 1.0, true);
125
+ view.setUint32(offset, scanlineDataSize, true);
132
126
  offset += 4;
133
- // End of header
134
- view.setUint8(offset, 0);
135
- offset += 1;
136
- // Scanline offset table
137
- const tableOffset = offset;
138
- offset += height * 8; // uint64 per scanline
139
- // Write scanlines (bottom to top not needed for INCREASING_Y)
140
- for (let y = 0; y < height; y++) {
141
- // Offset table entry
142
- const scanView = new DataView(buffer, tableOffset + y * 8, 8);
143
- // Write as two 32-bit values (little-endian uint64)
144
- scanView.setUint32(0, offset, true);
145
- scanView.setUint32(4, 0, true);
146
- // y-coordinate
147
- view.setInt32(offset, y, true);
148
- offset += 4;
149
- // pixel data size
150
- view.setUint32(offset, scanlineDataSize, true);
151
- offset += 4;
152
- // Pixel data (interleaved channels in EXR alphabetical order: A, B, G, R)
153
- for (let ch = 0; ch < channels; ch++) {
154
- const channelIdx = ch === 0 ? 3 : ch === 1 ? 2 : ch === 2 ? 1 : 0; // A,B,G,R → RGBA mapping
155
- for (let x = 0; x < width; x++) {
156
- const srcIdx = (y * width + x) * 4 + channelIdx;
157
- if (offset + 4 <= buffer.byteLength) {
158
- view.setFloat32(offset, (_a = floatData[srcIdx]) !== null && _a !== void 0 ? _a : 0, true);
159
- }
160
- offset += 4;
161
- }
127
+ for (let ch = 0; ch < channels; ch++) {
128
+ const channelIdx = ch === 0 ? 3 : ch === 1 ? 2 : ch === 2 ? 1 : 0;
129
+ for (let x = 0; x < width; x++) {
130
+ const srcIdx = (y * width + x) * 4 + channelIdx;
131
+ if (offset + 4 <= buffer.byteLength) {
132
+ view.setFloat32(offset, floatData[srcIdx] ?? 0, true);
162
133
  }
134
+ offset += 4;
135
+ }
163
136
  }
164
- return buffer.slice(0, offset);
137
+ }
138
+ return buffer.slice(0, offset);
165
139
  }
166
- /* ── Hook ── */
167
140
  const PRESET_SIZES = [
168
- { label: '720p', width: 1280, height: 720 },
169
- { label: '1080p', width: 1920, height: 1080 },
170
- { label: '2K', width: 2560, height: 1440 },
171
- { label: '4K', width: 3840, height: 2160 },
172
- { label: '8K', width: 7680, height: 4320 },
173
- { label: 'Square 1K', width: 1024, height: 1024 },
174
- { label: 'Square 2K', width: 2048, height: 2048 },
175
- { label: 'Square 4K', width: 4096, height: 4096 },
141
+ { label: "720p", width: 1280, height: 720 },
142
+ { label: "1080p", width: 1920, height: 1080 },
143
+ { label: "2K", width: 2560, height: 1440 },
144
+ { label: "4K", width: 3840, height: 2160 },
145
+ { label: "8K", width: 7680, height: 4320 },
146
+ { label: "Square 1K", width: 1024, height: 1024 },
147
+ { label: "Square 2K", width: 2048, height: 2048 },
148
+ { label: "Square 4K", width: 4096, height: 4096 }
176
149
  ];
177
150
  const DEFAULT_CONFIG = {
178
- width: 1920,
179
- height: 1080,
180
- format: 'png',
181
- quality: 0.92,
182
- // A THREE.js render target / image encoder, not CSS: var() is not a colour here and
183
- // resolves to nothing, so the exported frame needs the literal (cf. 344d1b2).
184
- backgroundColor: '#000000',
185
- backgroundMode: 'transparent',
186
- // audit-allow: literal-color +2 — the default sky gradient of the rendered image
187
- gradientTop: '#87CEEB',
188
- gradientBottom: '#2E4057',
189
- samples: 1,
190
- includeAlpha: true,
151
+ width: 1920,
152
+ height: 1080,
153
+ format: "png",
154
+ quality: 0.92,
155
+ // A THREE.js render target / image encoder, not CSS: var() is not a colour here and
156
+ // resolves to nothing, so the exported frame needs the literal (cf. 344d1b2).
157
+ backgroundColor: "#000000",
158
+ backgroundMode: "transparent",
159
+ // audit-allow: literal-color +2 — the default sky gradient of the rendered image
160
+ gradientTop: "#87CEEB",
161
+ gradientBottom: "#2E4057",
162
+ samples: 1,
163
+ includeAlpha: true
191
164
  };
192
165
  function useRenderExport(renderer, scene, camera) {
193
- const [config, setConfigState] = useState(DEFAULT_CONFIG);
194
- const [isRendering, setIsRendering] = useState(false);
195
- const setConfig = useCallback((partial) => {
196
- setConfigState((prev) => ({ ...prev, ...partial }));
197
- }, []);
198
- const render = useCallback(async () => {
199
- if (!renderer || !scene || !camera) {
200
- return null;
166
+ const [config, setConfigState] = useState(DEFAULT_CONFIG);
167
+ const [isRendering, setIsRendering] = useState(false);
168
+ const setConfig = useCallback((partial) => {
169
+ setConfigState((prev) => ({ ...prev, ...partial }));
170
+ }, []);
171
+ const render = useCallback(async () => {
172
+ if (!renderer || !scene || !camera) {
173
+ return null;
174
+ }
175
+ setIsRendering(true);
176
+ try {
177
+ const { width, height, format, samples } = config;
178
+ const renderWidth = width * samples;
179
+ const renderHeight = height * samples;
180
+ const originalSize = new THREE.Vector2();
181
+ renderer.getSize(originalSize);
182
+ const originalPixelRatio = renderer.getPixelRatio();
183
+ const originalBackground = scene.background;
184
+ const originalAspect = camera.aspect;
185
+ const renderTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight, {
186
+ minFilter: THREE.LinearFilter,
187
+ magFilter: THREE.LinearFilter,
188
+ format: THREE.RGBAFormat,
189
+ type: format === "exr" ? THREE.FloatType : THREE.UnsignedByteType,
190
+ colorSpace: THREE.SRGBColorSpace
191
+ });
192
+ camera.aspect = width / height;
193
+ camera.updateProjectionMatrix();
194
+ switch (config.backgroundMode) {
195
+ case "transparent":
196
+ scene.background = null;
197
+ break;
198
+ case "color":
199
+ scene.background = new THREE.Color(config.backgroundColor);
200
+ break;
201
+ case "gradient": {
202
+ scene.background = new THREE.Color(config.gradientTop);
203
+ break;
201
204
  }
202
- setIsRendering(true);
203
- try {
204
- const { width, height, format, samples } = config;
205
- const renderWidth = width * samples;
206
- const renderHeight = height * samples;
207
- // Save current state
208
- const originalSize = new THREE.Vector2();
209
- renderer.getSize(originalSize);
210
- const originalPixelRatio = renderer.getPixelRatio();
211
- const originalBackground = scene.background;
212
- const originalAspect = camera.aspect;
213
- // Setup render target
214
- const renderTarget = new THREE.WebGLRenderTarget(renderWidth, renderHeight, {
215
- minFilter: THREE.LinearFilter,
216
- magFilter: THREE.LinearFilter,
217
- format: THREE.RGBAFormat,
218
- type: format === 'exr' ? THREE.FloatType : THREE.UnsignedByteType,
219
- colorSpace: THREE.SRGBColorSpace,
220
- });
221
- // Set camera aspect
222
- camera.aspect = width / height;
223
- camera.updateProjectionMatrix();
224
- // Set background
225
- switch (config.backgroundMode) {
226
- case 'transparent':
227
- scene.background = null;
228
- break;
229
- case 'color':
230
- scene.background = new THREE.Color(config.backgroundColor);
231
- break;
232
- case 'gradient': {
233
- // Render gradient as a backdrop plane — simplified
234
- scene.background = new THREE.Color(config.gradientTop);
235
- break;
236
- }
237
- case 'scene':
238
- // Keep current background
239
- break;
205
+ case "scene":
206
+ break;
207
+ }
208
+ renderer.setRenderTarget(renderTarget);
209
+ renderer.setPixelRatio(1);
210
+ renderer.setSize(renderWidth, renderHeight, false);
211
+ renderer.setClearColor(0, config.backgroundMode === "transparent" ? 0 : 1);
212
+ renderer.clear();
213
+ renderer.render(scene, camera);
214
+ let blob = null;
215
+ if (format === "exr") {
216
+ const floatBuffer = new Float32Array(renderWidth * renderHeight * 4);
217
+ renderer.readRenderTargetPixels(renderTarget, 0, 0, renderWidth, renderHeight, floatBuffer);
218
+ const exrBuffer = writeEXR(renderWidth, renderHeight, floatBuffer);
219
+ blob = new Blob([exrBuffer], { type: "image/x-exr" });
220
+ } else {
221
+ const pixelBuffer = new Uint8Array(renderWidth * renderHeight * 4);
222
+ renderer.readRenderTargetPixels(renderTarget, 0, 0, renderWidth, renderHeight, pixelBuffer);
223
+ const exportCanvas = document.createElement("canvas");
224
+ exportCanvas.width = width;
225
+ exportCanvas.height = height;
226
+ const ctx = exportCanvas.getContext("2d");
227
+ const srcCanvas = document.createElement("canvas");
228
+ srcCanvas.width = renderWidth;
229
+ srcCanvas.height = renderHeight;
230
+ const srcCtx = srcCanvas.getContext("2d");
231
+ const imageData = srcCtx.createImageData(renderWidth, renderHeight);
232
+ for (let y = 0; y < renderHeight; y++) {
233
+ const srcRow = (renderHeight - 1 - y) * renderWidth * 4;
234
+ const dstRow = y * renderWidth * 4;
235
+ imageData.data.set(pixelBuffer.subarray(srcRow, srcRow + renderWidth * 4), dstRow);
236
+ }
237
+ srcCtx.putImageData(imageData, 0, 0);
238
+ ctx.drawImage(srcCanvas, 0, 0, renderWidth, renderHeight, 0, 0, width, height);
239
+ const mimeType = format === "jpeg" ? "image/jpeg" : "image/png";
240
+ blob = await new Promise((resolve) => {
241
+ exportCanvas.toBlob(
242
+ (b) => resolve(b),
243
+ mimeType,
244
+ format === "jpeg" ? config.quality : void 0
245
+ );
246
+ });
247
+ }
248
+ renderer.setRenderTarget(null);
249
+ renderer.setSize(originalSize.x, originalSize.y, false);
250
+ renderer.setPixelRatio(originalPixelRatio);
251
+ scene.background = originalBackground;
252
+ camera.aspect = originalAspect;
253
+ camera.updateProjectionMatrix();
254
+ renderTarget.dispose();
255
+ return blob;
256
+ } finally {
257
+ setIsRendering(false);
258
+ }
259
+ }, [renderer, scene, camera, config]);
260
+ return { config, setConfig, render, isRendering, presetSizes: PRESET_SIZES };
261
+ }
262
+ const NiceRenderExport = ({
263
+ renderer,
264
+ scene,
265
+ camera,
266
+ className = ""
267
+ }) => {
268
+ const { t } = useNiceTranslation();
269
+ const { config, setConfig, render, isRendering, presetSizes } = useRenderExport(
270
+ renderer,
271
+ scene,
272
+ camera
273
+ );
274
+ const handleRender = async () => {
275
+ const blob = await render();
276
+ if (!blob) {
277
+ return;
278
+ }
279
+ const ext = config.format === "exr" ? "exr" : config.format === "jpeg" ? "jpg" : "png";
280
+ const url = URL.createObjectURL(blob);
281
+ const link = document.createElement("a");
282
+ link.download = `render_${config.width}x${config.height}.${ext}`;
283
+ link.href = url;
284
+ link.click();
285
+ URL.revokeObjectURL(url);
286
+ };
287
+ return /* @__PURE__ */ jsxs(
288
+ "div",
289
+ {
290
+ className: `nice-render-export ${className}`,
291
+ style: { display: "grid", gap: 8, fontSize: 13 },
292
+ children: [
293
+ /* @__PURE__ */ jsxs("strong", { children: [
294
+ /* @__PURE__ */ jsx(NiceIcon, { name: "camera", size: 14 }),
295
+ " ",
296
+ t("renderExport.renderExport", "Render Export")
297
+ ] }),
298
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: 2 }, children: presetSizes.map((p) => /* @__PURE__ */ jsx(
299
+ "button",
300
+ {
301
+ type: "button",
302
+ onClick: () => setConfig({ width: p.width, height: p.height }),
303
+ style: {
304
+ padding: "2px 6px",
305
+ fontSize: 10,
306
+ cursor: "pointer",
307
+ borderRadius: 4,
308
+ border: "1px solid var(--nice-border, #e2e8f0)",
309
+ background: config.width === p.width && config.height === p.height ? "var(--nice-color-accent, #6366f1)" : "transparent",
310
+ color: config.width === p.width && config.height === p.height ? "var(--nice-color-white, #fff)" : "inherit"
311
+ },
312
+ children: p.label
313
+ },
314
+ p.label
315
+ )) }),
316
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 4, alignItems: "center" }, children: [
317
+ /* @__PURE__ */ jsx(
318
+ "input",
319
+ {
320
+ type: "number",
321
+ value: config.width,
322
+ min: 1,
323
+ max: 8192,
324
+ onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }),
325
+ style: { width: 70, padding: "3px 6px" }
240
326
  }
241
- // Render
242
- renderer.setRenderTarget(renderTarget);
243
- renderer.setPixelRatio(1);
244
- renderer.setSize(renderWidth, renderHeight, false);
245
- renderer.setClearColor(0x000000, config.backgroundMode === 'transparent' ? 0 : 1);
246
- renderer.clear();
247
- renderer.render(scene, camera);
248
- // Read pixels
249
- let blob = null;
250
- if (format === 'exr') {
251
- const floatBuffer = new Float32Array(renderWidth * renderHeight * 4);
252
- renderer.readRenderTargetPixels(renderTarget, 0, 0, renderWidth, renderHeight, floatBuffer);
253
- const exrBuffer = writeEXR(renderWidth, renderHeight, floatBuffer);
254
- blob = new Blob([exrBuffer], { type: 'image/x-exr' });
327
+ ),
328
+ /* @__PURE__ */ jsx("span", { children: "×" }),
329
+ /* @__PURE__ */ jsx(
330
+ "input",
331
+ {
332
+ type: "number",
333
+ value: config.height,
334
+ min: 1,
335
+ max: 8192,
336
+ onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }),
337
+ style: { width: 70, padding: "3px 6px" }
255
338
  }
256
- else {
257
- const pixelBuffer = new Uint8Array(renderWidth * renderHeight * 4);
258
- renderer.readRenderTargetPixels(renderTarget, 0, 0, renderWidth, renderHeight, pixelBuffer);
259
- // Create canvas for encoding
260
- const exportCanvas = document.createElement('canvas');
261
- exportCanvas.width = width;
262
- exportCanvas.height = height;
263
- const ctx = exportCanvas.getContext('2d');
264
- // If supersampled, draw to intermediate canvas first
265
- const srcCanvas = document.createElement('canvas');
266
- srcCanvas.width = renderWidth;
267
- srcCanvas.height = renderHeight;
268
- const srcCtx = srcCanvas.getContext('2d');
269
- const imageData = srcCtx.createImageData(renderWidth, renderHeight);
270
- // Flip Y (OpenGL → Canvas)
271
- for (let y = 0; y < renderHeight; y++) {
272
- const srcRow = (renderHeight - 1 - y) * renderWidth * 4;
273
- const dstRow = y * renderWidth * 4;
274
- imageData.data.set(pixelBuffer.subarray(srcRow, srcRow + renderWidth * 4), dstRow);
275
- }
276
- srcCtx.putImageData(imageData, 0, 0);
277
- ctx.drawImage(srcCanvas, 0, 0, renderWidth, renderHeight, 0, 0, width, height);
278
- const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png';
279
- blob = await new Promise((resolve) => {
280
- exportCanvas.toBlob((b) => resolve(b), mimeType, format === 'jpeg' ? config.quality : undefined);
281
- });
339
+ )
340
+ ] }),
341
+ /* @__PURE__ */ jsxs("label", { children: [
342
+ t("renderExport.format", "Format:"),
343
+ /* @__PURE__ */ jsxs(
344
+ "select",
345
+ {
346
+ value: config.format,
347
+ onChange: (e) => setConfig({ format: e.target.value }),
348
+ style: { width: "100%", marginTop: 2 },
349
+ children: [
350
+ /* @__PURE__ */ jsx("option", { value: "png", children: t("renderExport.pngLosslessAlpha", "PNG (lossless + alpha)") }),
351
+ /* @__PURE__ */ jsx("option", { value: "jpeg", children: t("renderExport.jpegLossyNoAlpha", "JPEG (lossy, no alpha)") }),
352
+ /* @__PURE__ */ jsx("option", { value: "exr", children: t("renderExport.exrHdrFloat", "EXR (HDR float)") })
353
+ ]
282
354
  }
283
- // Restore state
284
- renderer.setRenderTarget(null);
285
- renderer.setSize(originalSize.x, originalSize.y, false);
286
- renderer.setPixelRatio(originalPixelRatio);
287
- scene.background = originalBackground;
288
- camera.aspect = originalAspect;
289
- camera.updateProjectionMatrix();
290
- renderTarget.dispose();
291
- return blob;
292
- }
293
- finally {
294
- setIsRendering(false);
295
- }
296
- }, [renderer, scene, camera, config]);
297
- return { config, setConfig, render, isRendering, presetSizes: PRESET_SIZES };
298
- }
299
- /* ── Component ── */
300
- const NiceRenderExport = ({ renderer, scene, camera, className = '', }) => {
301
- const { t } = useNiceTranslation();
302
- const { config, setConfig, render, isRendering, presetSizes } = useRenderExport(renderer, scene, camera);
303
- const handleRender = async () => {
304
- const blob = await render();
305
- if (!blob) {
306
- return;
307
- }
308
- const ext = config.format === 'exr' ? 'exr' : config.format === 'jpeg' ? 'jpg' : 'png';
309
- const url = URL.createObjectURL(blob);
310
- const link = document.createElement('a');
311
- link.download = `render_${config.width}x${config.height}.${ext}`;
312
- link.href = url;
313
- link.click();
314
- URL.revokeObjectURL(url);
315
- };
316
- return (jsxs("div", { className: `nice-render-export ${className}`, style: { display: 'grid', gap: 8, fontSize: 13 }, children: [jsxs("strong", { children: [jsx(NiceIcon, { name: "camera", size: 14 }), " ", t('renderExport.renderExport', 'Render Export')] }), jsx("div", { style: { display: 'flex', flexWrap: 'wrap', gap: 2 }, children: presetSizes.map((p) => (jsx("button", { type: "button", onClick: () => setConfig({ width: p.width, height: p.height }), style: {
317
- padding: '2px 6px',
318
- fontSize: 10,
319
- cursor: 'pointer',
320
- borderRadius: 4,
321
- border: '1px solid var(--nice-border, #e2e8f0)',
322
- background: config.width === p.width && config.height === p.height
323
- ? 'var(--nice-color-accent, #6366f1)'
324
- : 'transparent',
325
- color: config.width === p.width && config.height === p.height
326
- ? 'var(--nice-color-white, #fff)'
327
- : 'inherit',
328
- }, children: p.label }, p.label))) }), jsxs("div", { style: { display: 'flex', gap: 4, alignItems: 'center' }, children: [jsx("input", { type: "number", value: config.width, min: 1, max: 8192, onChange: (e) => setConfig({ width: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } }), jsx("span", { children: "\u00D7" }), jsx("input", { type: "number", value: config.height, min: 1, max: 8192, onChange: (e) => setConfig({ height: parseInt(e.target.value, 10) || 1 }), style: { width: 70, padding: '3px 6px' } })] }), jsxs("label", { children: [t('renderExport.format', 'Format:'), jsxs("select", { value: config.format, onChange: (e) => setConfig({ format: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "png", children: t('renderExport.pngLosslessAlpha', 'PNG (lossless + alpha)') }), jsx("option", { value: "jpeg", children: t('renderExport.jpegLossyNoAlpha', 'JPEG (lossy, no alpha)') }), jsx("option", { value: "exr", children: t('renderExport.exrHdrFloat', 'EXR (HDR float)') })] })] }), jsxs("label", { children: [t('renderExport.background', 'Background:'), jsxs("select", { value: config.backgroundMode, onChange: (e) => setConfig({ backgroundMode: e.target.value }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: "transparent", children: t('renderExport.transparent', 'Transparent') }), jsx("option", { value: "color", children: t('renderExport.solidColor', 'Solid Color') }), jsx("option", { value: "scene", children: t('renderExport.sceneBackground', 'Scene Background') })] })] }), config.backgroundMode === 'color' && (jsxs("label", { style: { display: 'flex', alignItems: 'center', gap: 6 }, children: [t('renderExport.color', 'Color:'), jsx("input", { type: "color", value: config.backgroundColor, onChange: (e) => setConfig({ backgroundColor: e.target.value }) })] })), jsxs("label", { children: [t('renderExport.supersamplingX', 'Supersampling: {samples}x').replace('{samples}', String(config.samples)), jsxs("select", { value: config.samples, onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }), style: { width: '100%', marginTop: 2 }, children: [jsx("option", { value: 1, children: t('renderExport.n1xNone', '1x (none)') }), jsx("option", { value: 2, children: "2x" }), jsx("option", { value: 4, children: t('renderExport.n4xBestQuality', '4x (best quality)') })] })] }), jsx("button", { type: "button", onClick: handleRender, disabled: isRendering, style: {
329
- background: isRendering
330
- ? 'var(--nice-bg-tertiary, #94a3b8)'
331
- : 'var(--nice-color-accent, #6366f1)',
332
- color: 'var(--nice-color-white, #fff)',
333
- border: 'none',
334
- borderRadius: 4,
335
- padding: '8px 12px',
336
- cursor: isRendering ? 'wait' : 'pointer',
337
- }, children: isRendering ? (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "clock", size: 14 }), " ", t('renderExport.rendering', 'Rendering…')] })) : (jsxs(Fragment, { children: [jsx(NiceIcon, { name: "camera", size: 14 }), ' ', t('renderExport.render', 'Render {width}×{height} {format}')
338
- .replace('{width}', String(config.width))
339
- .replace('{height}', String(config.height))
340
- .replace('{format}', config.format.toUpperCase())] })) })] }));
355
+ )
356
+ ] }),
357
+ /* @__PURE__ */ jsxs("label", { children: [
358
+ t("renderExport.background", "Background:"),
359
+ /* @__PURE__ */ jsxs(
360
+ "select",
361
+ {
362
+ value: config.backgroundMode,
363
+ onChange: (e) => setConfig({ backgroundMode: e.target.value }),
364
+ style: { width: "100%", marginTop: 2 },
365
+ children: [
366
+ /* @__PURE__ */ jsx("option", { value: "transparent", children: t("renderExport.transparent", "Transparent") }),
367
+ /* @__PURE__ */ jsx("option", { value: "color", children: t("renderExport.solidColor", "Solid Color") }),
368
+ /* @__PURE__ */ jsx("option", { value: "scene", children: t("renderExport.sceneBackground", "Scene Background") })
369
+ ]
370
+ }
371
+ )
372
+ ] }),
373
+ config.backgroundMode === "color" && /* @__PURE__ */ jsxs("label", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
374
+ t("renderExport.color", "Color:"),
375
+ /* @__PURE__ */ jsx(
376
+ "input",
377
+ {
378
+ type: "color",
379
+ value: config.backgroundColor,
380
+ onChange: (e) => setConfig({ backgroundColor: e.target.value })
381
+ }
382
+ )
383
+ ] }),
384
+ /* @__PURE__ */ jsxs("label", { children: [
385
+ t("renderExport.supersamplingX", "Supersampling: {samples}x").replace(
386
+ "{samples}",
387
+ String(config.samples)
388
+ ),
389
+ /* @__PURE__ */ jsxs(
390
+ "select",
391
+ {
392
+ value: config.samples,
393
+ onChange: (e) => setConfig({ samples: parseInt(e.target.value, 10) }),
394
+ style: { width: "100%", marginTop: 2 },
395
+ children: [
396
+ /* @__PURE__ */ jsx("option", { value: 1, children: t("renderExport.n1xNone", "1x (none)") }),
397
+ /* @__PURE__ */ jsx("option", { value: 2, children: "2x" }),
398
+ /* @__PURE__ */ jsx("option", { value: 4, children: t("renderExport.n4xBestQuality", "4x (best quality)") })
399
+ ]
400
+ }
401
+ )
402
+ ] }),
403
+ /* @__PURE__ */ jsx(
404
+ "button",
405
+ {
406
+ type: "button",
407
+ onClick: handleRender,
408
+ disabled: isRendering,
409
+ style: {
410
+ background: isRendering ? "var(--nice-bg-tertiary, #94a3b8)" : "var(--nice-color-accent, #6366f1)",
411
+ color: "var(--nice-color-white, #fff)",
412
+ border: "none",
413
+ borderRadius: 4,
414
+ padding: "8px 12px",
415
+ cursor: isRendering ? "wait" : "pointer"
416
+ },
417
+ children: isRendering ? /* @__PURE__ */ jsxs(Fragment, { children: [
418
+ /* @__PURE__ */ jsx(NiceIcon, { name: "clock", size: 14 }),
419
+ " ",
420
+ t("renderExport.rendering", "Rendering…")
421
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
422
+ /* @__PURE__ */ jsx(NiceIcon, { name: "camera", size: 14 }),
423
+ " ",
424
+ t("renderExport.render", "Render {width}×{height} {format}").replace("{width}", String(config.width)).replace("{height}", String(config.height)).replace("{format}", config.format.toUpperCase())
425
+ ] })
426
+ }
427
+ )
428
+ ]
429
+ }
430
+ );
431
+ };
432
+ export {
433
+ NiceRenderExport,
434
+ useRenderExport
341
435
  };
342
-
343
- export { NiceRenderExport, useRenderExport };