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