@chocozhang/three-model-render 1.0.3 → 1.0.5

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 (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +134 -97
  3. package/dist/camera/index.d.ts +59 -36
  4. package/dist/camera/index.js +83 -67
  5. package/dist/camera/index.js.map +1 -1
  6. package/dist/camera/index.mjs +83 -67
  7. package/dist/camera/index.mjs.map +1 -1
  8. package/dist/core/index.d.ts +81 -28
  9. package/dist/core/index.js +194 -104
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.mjs +194 -105
  12. package/dist/core/index.mjs.map +1 -1
  13. package/dist/effect/index.d.ts +47 -134
  14. package/dist/effect/index.js +287 -288
  15. package/dist/effect/index.js.map +1 -1
  16. package/dist/effect/index.mjs +287 -288
  17. package/dist/effect/index.mjs.map +1 -1
  18. package/dist/index.d.ts +432 -349
  19. package/dist/index.js +1399 -1228
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +1395 -1229
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/interaction/index.d.ts +85 -52
  24. package/dist/interaction/index.js +168 -142
  25. package/dist/interaction/index.js.map +1 -1
  26. package/dist/interaction/index.mjs +168 -142
  27. package/dist/interaction/index.mjs.map +1 -1
  28. package/dist/loader/index.d.ts +106 -58
  29. package/dist/loader/index.js +492 -454
  30. package/dist/loader/index.js.map +1 -1
  31. package/dist/loader/index.mjs +491 -455
  32. package/dist/loader/index.mjs.map +1 -1
  33. package/dist/setup/index.d.ts +26 -24
  34. package/dist/setup/index.js +125 -163
  35. package/dist/setup/index.js.map +1 -1
  36. package/dist/setup/index.mjs +124 -164
  37. package/dist/setup/index.mjs.map +1 -1
  38. package/dist/ui/index.d.ts +18 -7
  39. package/dist/ui/index.js +45 -37
  40. package/dist/ui/index.js.map +1 -1
  41. package/dist/ui/index.mjs +45 -37
  42. package/dist/ui/index.mjs.map +1 -1
  43. package/package.json +50 -22
@@ -22,163 +22,166 @@ function _interopNamespaceDefault(e) {
22
22
 
23
23
  var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
24
24
 
25
- /******************************************************************************
26
- Copyright (c) Microsoft Corporation.
27
-
28
- Permission to use, copy, modify, and/or distribute this software for any
29
- purpose with or without fee is hereby granted.
30
-
31
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
32
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
33
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
34
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
35
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
36
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
37
- PERFORMANCE OF THIS SOFTWARE.
38
- ***************************************************************************** */
39
- /* global Reflect, Promise, SuppressedError, Symbol, Iterator */
40
-
41
-
42
- function __awaiter(thisArg, _arguments, P, generator) {
43
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
44
- return new (P || (P = Promise))(function (resolve, reject) {
45
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
46
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
47
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
48
- step((generator = generator.apply(thisArg, _arguments || [])).next());
49
- });
50
- }
51
-
52
- typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
53
- var e = new Error(message);
54
- return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
25
+ let globalConfig = {
26
+ dracoDecoderPath: '/draco/',
27
+ ktx2TranscoderPath: '/basis/',
55
28
  };
29
+ /**
30
+ * Update global loader configuration (e.g., set path to CDN)
31
+ */
32
+ function setLoaderConfig(config) {
33
+ globalConfig = { ...globalConfig, ...config };
34
+ }
35
+ /**
36
+ * Get current global loader configuration
37
+ */
38
+ function getLoaderConfig() {
39
+ return globalConfig;
40
+ }
56
41
 
42
+ /**
43
+ * @file modelLoader.ts
44
+ * @description
45
+ * Utility to load 3D models (GLTF, FBX, OBJ, PLY, STL) from URLs.
46
+ *
47
+ * @best-practice
48
+ * - Use `loadModelByUrl` for a unified loading interface.
49
+ * - Supports Draco compression and KTX2 textures for GLTF.
50
+ * - Includes optimization options like geometry merging and texture downscaling.
51
+ */
57
52
  const DEFAULT_OPTIONS$1 = {
58
53
  useKTX2: false,
59
54
  mergeGeometries: false,
60
55
  maxTextureSize: null,
61
56
  useSimpleMaterials: false,
62
57
  skipSkinned: true,
58
+ useCache: true,
63
59
  };
64
- /** 自动根据扩展名决定启用哪些选项(智能判断) */
60
+ const modelCache = new Map();
61
+ /** Automatically determine which options to enable based on extension (smart judgment) */
65
62
  function normalizeOptions(url, opts) {
66
63
  const ext = (url.split('.').pop() || '').toLowerCase();
67
- const merged = Object.assign(Object.assign({}, DEFAULT_OPTIONS$1), opts);
64
+ const merged = { ...DEFAULT_OPTIONS$1, ...opts };
68
65
  if (ext === 'gltf' || ext === 'glb') {
69
- // gltf/glb 默认尝试 draco/ktx2,如果用户没填
66
+ const globalConfig = getLoaderConfig();
67
+ // gltf/glb defaults to trying draco/ktx2 if user didn't specify
70
68
  if (merged.dracoDecoderPath === undefined)
71
- merged.dracoDecoderPath = '/draco/';
69
+ merged.dracoDecoderPath = globalConfig.dracoDecoderPath;
72
70
  if (merged.useKTX2 === undefined)
73
71
  merged.useKTX2 = true;
74
72
  if (merged.ktx2TranscoderPath === undefined)
75
- merged.ktx2TranscoderPath = '/basis/';
73
+ merged.ktx2TranscoderPath = globalConfig.ktx2TranscoderPath;
76
74
  }
77
75
  else {
78
- // fbx/obj/ply/stl 等不需要 draco/ktx2
76
+ // fbx/obj/ply/stl etc. do not need draco/ktx2
79
77
  merged.dracoDecoderPath = null;
80
78
  merged.ktx2TranscoderPath = null;
81
79
  merged.useKTX2 = false;
82
80
  }
83
81
  return merged;
84
82
  }
85
- function loadModelByUrl(url_1) {
86
- return __awaiter(this, arguments, void 0, function* (url, options = {}) {
87
- var _a, _b;
88
- if (!url)
89
- throw new Error('url required');
90
- const ext = (url.split('.').pop() || '').toLowerCase();
91
- const opts = normalizeOptions(url, options);
92
- const manager = (_a = opts.manager) !== null && _a !== void 0 ? _a : new THREE__namespace.LoadingManager();
93
- let loader;
94
- if (ext === 'gltf' || ext === 'glb') {
95
- const { GLTFLoader } = yield import('three/examples/jsm/loaders/GLTFLoader.js');
96
- const gltfLoader = new GLTFLoader(manager);
97
- if (opts.dracoDecoderPath) {
98
- const { DRACOLoader } = yield import('three/examples/jsm/loaders/DRACOLoader.js');
99
- const draco = new DRACOLoader();
100
- draco.setDecoderPath(opts.dracoDecoderPath);
101
- gltfLoader.setDRACOLoader(draco);
102
- }
103
- if (opts.useKTX2 && opts.ktx2TranscoderPath) {
104
- const { KTX2Loader } = yield import('three/examples/jsm/loaders/KTX2Loader.js');
105
- const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
106
- gltfLoader.__ktx2Loader = ktx2Loader;
107
- }
108
- loader = gltfLoader;
109
- }
110
- else if (ext === 'fbx') {
111
- const { FBXLoader } = yield import('three/examples/jsm/loaders/FBXLoader.js');
112
- loader = new FBXLoader(manager);
113
- }
114
- else if (ext === 'obj') {
115
- const { OBJLoader } = yield import('three/examples/jsm/loaders/OBJLoader.js');
116
- loader = new OBJLoader(manager);
117
- }
118
- else if (ext === 'ply') {
119
- const { PLYLoader } = yield import('three/examples/jsm/loaders/PLYLoader.js');
120
- loader = new PLYLoader(manager);
121
- }
122
- else if (ext === 'stl') {
123
- const { STLLoader } = yield import('three/examples/jsm/loaders/STLLoader.js');
124
- loader = new STLLoader(manager);
83
+ async function loadModelByUrl(url, options = {}) {
84
+ if (!url)
85
+ throw new Error('url required');
86
+ const ext = (url.split('.').pop() || '').toLowerCase();
87
+ const opts = normalizeOptions(url, options);
88
+ const manager = opts.manager ?? new THREE__namespace.LoadingManager();
89
+ // Cache key includes URL and relevant optimization options
90
+ const cacheKey = `${url}_${opts.mergeGeometries}_${opts.maxTextureSize}_${opts.useSimpleMaterials}`;
91
+ if (opts.useCache && modelCache.has(cacheKey)) {
92
+ return modelCache.get(cacheKey).clone();
93
+ }
94
+ let loader;
95
+ if (ext === 'gltf' || ext === 'glb') {
96
+ const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
97
+ const gltfLoader = new GLTFLoader(manager);
98
+ if (opts.dracoDecoderPath) {
99
+ const { DRACOLoader } = await import('three/examples/jsm/loaders/DRACOLoader.js');
100
+ const draco = new DRACOLoader();
101
+ draco.setDecoderPath(opts.dracoDecoderPath);
102
+ gltfLoader.setDRACOLoader(draco);
125
103
  }
126
- else {
127
- throw new Error(`Unsupported model extension: .${ext}`);
104
+ if (opts.useKTX2 && opts.ktx2TranscoderPath) {
105
+ const { KTX2Loader } = await import('three/examples/jsm/loaders/KTX2Loader.js');
106
+ const ktx2Loader = new KTX2Loader().setTranscoderPath(opts.ktx2TranscoderPath);
107
+ gltfLoader.__ktx2Loader = ktx2Loader;
128
108
  }
129
- const object = yield new Promise((resolve, reject) => {
130
- loader.load(url, (res) => {
131
- var _a;
132
- if (ext === 'gltf' || ext === 'glb') {
133
- const sceneObj = res.scene || res;
134
- // --- 关键:把 animations 暴露到 scene.userData(或 scene.animations)上 ---
135
- // 这样调用方只要拿到 sceneObj,就能通过 sceneObj.userData.animations 读取到 clips
136
- sceneObj.userData = (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.userData) || {};
137
- sceneObj.userData.animations = (_a = res.animations) !== null && _a !== void 0 ? _a : [];
138
- resolve(sceneObj);
139
- }
140
- else {
141
- resolve(res);
142
- }
143
- }, undefined, (err) => reject(err));
144
- });
145
- // 优化
146
- object.traverse((child) => {
147
- var _a, _b, _c;
148
- const mesh = child;
149
- if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
150
- try {
151
- mesh.geometry = (_c = (_b = (_a = new THREE__namespace.BufferGeometry()).fromGeometry) === null || _b === void 0 ? void 0 : _b.call(_a, mesh.geometry)) !== null && _c !== void 0 ? _c : mesh.geometry;
152
- }
153
- catch (_d) { }
109
+ loader = gltfLoader;
110
+ }
111
+ else if (ext === 'fbx') {
112
+ const { FBXLoader } = await import('three/examples/jsm/loaders/FBXLoader.js');
113
+ loader = new FBXLoader(manager);
114
+ }
115
+ else if (ext === 'obj') {
116
+ const { OBJLoader } = await import('three/examples/jsm/loaders/OBJLoader.js');
117
+ loader = new OBJLoader(manager);
118
+ }
119
+ else if (ext === 'ply') {
120
+ const { PLYLoader } = await import('three/examples/jsm/loaders/PLYLoader.js');
121
+ loader = new PLYLoader(manager);
122
+ }
123
+ else if (ext === 'stl') {
124
+ const { STLLoader } = await import('three/examples/jsm/loaders/STLLoader.js');
125
+ loader = new STLLoader(manager);
126
+ }
127
+ else {
128
+ throw new Error(`Unsupported model extension: .${ext}`);
129
+ }
130
+ const object = await new Promise((resolve, reject) => {
131
+ loader.load(url, (res) => {
132
+ if (ext === 'gltf' || ext === 'glb') {
133
+ const sceneObj = res.scene || res;
134
+ // --- Critical: Expose animations to scene.userData (or scene.animations) ---
135
+ // So the caller can access clips simply by getting sceneObj.userData.animations
136
+ sceneObj.userData = sceneObj?.userData || {};
137
+ sceneObj.userData.animations = res.animations ?? [];
138
+ resolve(sceneObj);
154
139
  }
155
- });
156
- if (opts.maxTextureSize && opts.maxTextureSize > 0)
157
- downscaleTexturesInObject(object, opts.maxTextureSize);
158
- if (opts.useSimpleMaterials) {
159
- object.traverse((child) => {
160
- const m = child.material;
161
- if (!m)
162
- return;
163
- if (Array.isArray(m))
164
- child.material = m.map((mat) => toSimpleMaterial(mat));
165
- else
166
- child.material = toSimpleMaterial(m);
167
- });
168
- }
169
- if (opts.mergeGeometries) {
170
- try {
171
- yield tryMergeGeometries(object, { skipSkinned: (_b = opts.skipSkinned) !== null && _b !== void 0 ? _b : true });
140
+ else {
141
+ resolve(res);
172
142
  }
173
- catch (e) {
174
- console.warn('mergeGeometries failed', e);
143
+ }, undefined, (err) => reject(err));
144
+ });
145
+ // Optimize
146
+ object.traverse((child) => {
147
+ const mesh = child;
148
+ if (mesh.isMesh && mesh.geometry && !mesh.geometry.isBufferGeometry) {
149
+ try {
150
+ mesh.geometry = new THREE__namespace.BufferGeometry().fromGeometry?.(mesh.geometry) ?? mesh.geometry;
175
151
  }
152
+ catch { }
176
153
  }
177
- return object;
178
154
  });
155
+ if (opts.maxTextureSize && opts.maxTextureSize > 0)
156
+ await downscaleTexturesInObject(object, opts.maxTextureSize);
157
+ if (opts.useSimpleMaterials) {
158
+ object.traverse((child) => {
159
+ const m = child.material;
160
+ if (!m)
161
+ return;
162
+ if (Array.isArray(m))
163
+ child.material = m.map((mat) => toSimpleMaterial(mat));
164
+ else
165
+ child.material = toSimpleMaterial(m);
166
+ });
167
+ }
168
+ if (opts.mergeGeometries) {
169
+ try {
170
+ await tryMergeGeometries(object, { skipSkinned: opts.skipSkinned ?? true });
171
+ }
172
+ catch (e) {
173
+ console.warn('mergeGeometries failed', e);
174
+ }
175
+ }
176
+ if (opts.useCache) {
177
+ modelCache.set(cacheKey, object);
178
+ return object.clone();
179
+ }
180
+ return object;
179
181
  }
180
- /** 运行时下采样网格中的贴图到 maxSizecanvas drawImage)以节省 GPU 内存 */
181
- function downscaleTexturesInObject(obj, maxSize) {
182
+ /** Runtime downscale textures in mesh to maxSize (createImageBitmap or canvas) to save GPU memory */
183
+ async function downscaleTexturesInObject(obj, maxSize) {
184
+ const tasks = [];
182
185
  obj.traverse((ch) => {
183
186
  if (!ch.isMesh)
184
187
  return;
@@ -197,115 +200,128 @@ function downscaleTexturesInObject(obj, maxSize) {
197
200
  const max = maxSize;
198
201
  if (image.width <= max && image.height <= max)
199
202
  return;
200
- // downscale using canvas (sync, may be heavy for many textures)
201
- try {
202
- const scale = Math.min(max / image.width, max / image.height);
203
- const canvas = document.createElement('canvas');
204
- canvas.width = Math.floor(image.width * scale);
205
- canvas.height = Math.floor(image.height * scale);
206
- const ctx = canvas.getContext('2d');
207
- if (ctx) {
208
- ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
209
- const newTex = new THREE__namespace.Texture(canvas);
210
- newTex.needsUpdate = true;
211
- // copy common settings (encoding etc)
212
- newTex.encoding = tex.encoding;
213
- mat[p] = newTex;
203
+ tasks.push((async () => {
204
+ try {
205
+ const scale = Math.min(max / image.width, max / image.height);
206
+ const newWidth = Math.floor(image.width * scale);
207
+ const newHeight = Math.floor(image.height * scale);
208
+ let newSource;
209
+ if (typeof createImageBitmap !== 'undefined') {
210
+ newSource = await createImageBitmap(image, {
211
+ resizeWidth: newWidth,
212
+ resizeHeight: newHeight,
213
+ resizeQuality: 'high'
214
+ });
215
+ }
216
+ else {
217
+ // Fallback for environments without createImageBitmap
218
+ const canvas = document.createElement('canvas');
219
+ canvas.width = newWidth;
220
+ canvas.height = newHeight;
221
+ const ctx = canvas.getContext('2d');
222
+ if (ctx) {
223
+ ctx.drawImage(image, 0, 0, newWidth, newHeight);
224
+ newSource = canvas;
225
+ }
226
+ }
227
+ if (newSource) {
228
+ const newTex = new THREE__namespace.Texture(newSource);
229
+ newTex.needsUpdate = true;
230
+ newTex.encoding = tex.encoding;
231
+ mat[p] = newTex;
232
+ }
214
233
  }
215
- }
216
- catch (e) {
217
- console.warn('downscale texture failed', e);
218
- }
234
+ catch (e) {
235
+ console.warn('downscale texture failed', e);
236
+ }
237
+ })());
219
238
  });
220
239
  });
240
+ await Promise.all(tasks);
221
241
  }
222
242
  /**
223
- * 尝试合并 object 中的几何体(只合并:非透明、非 SkinnedMeshattribute 集合兼容的 BufferGeometry
224
- * - 合并前会把每个 mesh 的几何体应用 world matrixso merged geometry in world space
225
- * - 合并会按材质 UUID 分组(不同材质不能合并)
226
- * - 合并函数会兼容 BufferGeometryUtils 的常见导出名
243
+ * Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
244
+ * - Before merging, apply world matrix to each mesh's geometry (so merged geometry is in world space)
245
+ * - Merging will group by material UUID (different materials cannot be merged)
246
+ * - Merge function is compatible with common export names of BufferGeometryUtils
227
247
  */
228
- function tryMergeGeometries(root, opts) {
229
- return __awaiter(this, void 0, void 0, function* () {
230
- // collect meshes by material uuid
231
- const groups = new Map();
232
- root.traverse((ch) => {
233
- var _a;
234
- if (!ch.isMesh)
235
- return;
236
- const mesh = ch;
237
- if (opts.skipSkinned && mesh.isSkinnedMesh)
238
- return;
239
- const mat = mesh.material;
240
- // don't merge transparent or morph-enabled or skinned meshes
241
- if (!mesh.geometry || mesh.visible === false)
242
- return;
243
- if (mat && mat.transparent)
244
- return;
245
- const geom = mesh.geometry.clone();
246
- mesh.updateWorldMatrix(true, false);
247
- geom.applyMatrix4(mesh.matrixWorld);
248
- // ensure attributes compatible? we'll rely on merge function to return null if incompatible
249
- const key = (mat && mat.uuid) || 'default';
250
- const bucket = (_a = groups.get(key)) !== null && _a !== void 0 ? _a : { material: mat !== null && mat !== void 0 ? mat : new THREE__namespace.MeshStandardMaterial(), geoms: [] };
251
- bucket.geoms.push(geom);
252
- groups.set(key, bucket);
253
- // mark for removal (we'll remove meshes after)
254
- mesh.userData.__toRemoveForMerge = true;
255
- });
256
- if (groups.size === 0)
248
+ async function tryMergeGeometries(root, opts) {
249
+ // collect meshes by material uuid
250
+ const groups = new Map();
251
+ root.traverse((ch) => {
252
+ if (!ch.isMesh)
257
253
  return;
258
- // dynamic import BufferGeometryUtils and find merge function name
259
- const bufUtilsMod = yield import('three/examples/jsm/utils/BufferGeometryUtils.js');
260
- // use || chain (avoid mixing ?? with || without parentheses)
261
- const mergeFn = bufUtilsMod.mergeBufferGeometries ||
262
- bufUtilsMod.mergeGeometries ||
263
- bufUtilsMod.mergeBufferGeometries || // defensive duplicate
264
- bufUtilsMod.mergeGeometries;
265
- if (!mergeFn)
266
- throw new Error('No merge function found in BufferGeometryUtils');
267
- // for each group, try merge
268
- for (const [key, { material, geoms }] of groups) {
269
- if (geoms.length <= 1) {
270
- // nothing to merge
271
- continue;
272
- }
273
- // call merge function - signature typically mergeBufferGeometries(array, useGroups)
274
- const merged = mergeFn(geoms, false);
275
- if (!merged) {
276
- console.warn('merge returned null for group', key);
277
- continue;
278
- }
279
- // create merged mesh at root (world-space geometry already applied)
280
- const mergedMesh = new THREE__namespace.Mesh(merged, material);
281
- root.add(mergedMesh);
254
+ const mesh = ch;
255
+ if (opts.skipSkinned && mesh.isSkinnedMesh)
256
+ return;
257
+ const mat = mesh.material;
258
+ // don't merge transparent or morph-enabled or skinned meshes
259
+ if (!mesh.geometry || mesh.visible === false)
260
+ return;
261
+ if (mat && mat.transparent)
262
+ return;
263
+ const geom = mesh.geometry.clone();
264
+ mesh.updateWorldMatrix(true, false);
265
+ geom.applyMatrix4(mesh.matrixWorld);
266
+ // ensure attributes compatible? we'll rely on merge function to return null if incompatible
267
+ const key = (mat && mat.uuid) || 'default';
268
+ const bucket = groups.get(key) ?? { material: mat ?? new THREE__namespace.MeshStandardMaterial(), geoms: [] };
269
+ bucket.geoms.push(geom);
270
+ groups.set(key, bucket);
271
+ // mark for removal (we'll remove meshes after)
272
+ mesh.userData.__toRemoveForMerge = true;
273
+ });
274
+ if (groups.size === 0)
275
+ return;
276
+ // dynamic import BufferGeometryUtils and find merge function name
277
+ const bufUtilsMod = await import('three/examples/jsm/utils/BufferGeometryUtils.js');
278
+ // use || chain (avoid mixing ?? with || without parentheses)
279
+ const mergeFn = bufUtilsMod.mergeBufferGeometries ||
280
+ bufUtilsMod.mergeGeometries ||
281
+ bufUtilsMod.mergeBufferGeometries || // defensive duplicate
282
+ bufUtilsMod.mergeGeometries;
283
+ if (!mergeFn)
284
+ throw new Error('No merge function found in BufferGeometryUtils');
285
+ // for each group, try merge
286
+ for (const [key, { material, geoms }] of groups) {
287
+ if (geoms.length <= 1) {
288
+ // nothing to merge
289
+ continue;
282
290
  }
283
- // now remove original meshes flagged for removal
284
- const toRemove = [];
285
- root.traverse((ch) => {
286
- var _a;
287
- if ((_a = ch.userData) === null || _a === void 0 ? void 0 : _a.__toRemoveForMerge)
288
- toRemove.push(ch);
289
- });
290
- toRemove.forEach((m) => {
291
- if (m.parent)
292
- m.parent.remove(m);
293
- // free original resources (geometries already cloned/applied), but careful with shared materials
294
- if (m.isMesh) {
295
- const mm = m;
296
- try {
297
- mm.geometry.dispose();
298
- }
299
- catch (_a) { }
300
- // we do NOT dispose material because it may be reused by mergedMesh
291
+ // call merge function - signature typically mergeBufferGeometries(array, useGroups)
292
+ const merged = mergeFn(geoms, false);
293
+ if (!merged) {
294
+ console.warn('merge returned null for group', key);
295
+ continue;
296
+ }
297
+ // create merged mesh at root (world-space geometry already applied)
298
+ const mergedMesh = new THREE__namespace.Mesh(merged, material);
299
+ root.add(mergedMesh);
300
+ }
301
+ // now remove original meshes flagged for removal
302
+ const toRemove = [];
303
+ root.traverse((ch) => {
304
+ if (ch.userData?.__toRemoveForMerge)
305
+ toRemove.push(ch);
306
+ });
307
+ toRemove.forEach((m) => {
308
+ if (m.parent)
309
+ m.parent.remove(m);
310
+ // free original resources (geometries already cloned/applied), but careful with shared materials
311
+ if (m.isMesh) {
312
+ const mm = m;
313
+ try {
314
+ mm.geometry.dispose();
301
315
  }
302
- });
316
+ catch { }
317
+ // we do NOT dispose material because it may be reused by mergedMesh
318
+ }
303
319
  });
304
320
  }
305
321
  /* ---------------------
306
- 释放工具
322
+ Dispose Utils
307
323
  --------------------- */
308
- /** 彻底释放对象:几何体,材质和其贴图(危险:共享资源会被释放) */
324
+ /** Completely dispose object: geometry, material and its textures (Danger: shared resources will be disposed) */
309
325
  function disposeObject(obj) {
310
326
  if (!obj)
311
327
  return;
@@ -316,7 +332,7 @@ function disposeObject(obj) {
316
332
  try {
317
333
  m.geometry.dispose();
318
334
  }
319
- catch (_a) { }
335
+ catch { }
320
336
  }
321
337
  const mat = m.material;
322
338
  if (mat) {
@@ -328,7 +344,7 @@ function disposeObject(obj) {
328
344
  }
329
345
  });
330
346
  }
331
- /** 释放材质及其贴图 */
347
+ /** Dispose material and its textures */
332
348
  function disposeMaterial(mat) {
333
349
  if (!mat)
334
350
  return;
@@ -338,232 +354,244 @@ function disposeMaterial(mat) {
338
354
  try {
339
355
  mat[k].dispose();
340
356
  }
341
- catch (_a) { }
357
+ catch { }
342
358
  }
343
359
  });
344
360
  try {
345
361
  if (typeof mat.dispose === 'function')
346
362
  mat.dispose();
347
363
  }
348
- catch (_a) { }
364
+ catch { }
365
+ }
366
+ // Helper to convert to simple material (stub)
367
+ function toSimpleMaterial(mat) {
368
+ // Basic implementation, preserve color/map
369
+ const m = new THREE__namespace.MeshBasicMaterial();
370
+ if (mat.color)
371
+ m.color.copy(mat.color);
372
+ if (mat.map)
373
+ m.map = mat.map;
374
+ return m;
349
375
  }
350
376
 
351
- /** 默认值 */
377
+ /**
378
+ * @file skyboxLoader.ts
379
+ * @description
380
+ * Utility for loading skyboxes (CubeTexture or Equirectangular/HDR).
381
+ *
382
+ * @best-practice
383
+ * - Use `loadSkybox` for a unified interface.
384
+ * - Supports internal caching to avoid reloading the same skybox.
385
+ * - Can set background and environment map independently.
386
+ */
387
+ /** Default Values */
352
388
  const DEFAULT_OPTIONS = {
353
389
  setAsBackground: true,
354
390
  setAsEnvironment: true,
355
391
  useSRGBEncoding: true,
356
392
  cache: true
357
393
  };
358
- /** 内部缓存:key -> { handle, refCount } */
394
+ /** Internal Cache: key -> { handle, refCount } */
359
395
  const cubeCache = new Map();
360
396
  const equirectCache = new Map();
361
397
  /* -------------------------------------------
362
- 公共函数:加载 skybox(自动选 cube equirect
398
+ Public Function: Load skybox (Automatically choose cube or equirect)
363
399
  ------------------------------------------- */
364
400
  /**
365
- * 加载立方体贴图(6张)
366
- * @param renderer THREE.WebGLRenderer - 用于 PMREM 生成环境贴图
401
+ * Load Cube Texture (6 images)
402
+ * @param renderer THREE.WebGLRenderer - Used for PMREM generating environment map
367
403
  * @param scene THREE.Scene
368
- * @param paths string[] 6 张图片地址,顺序:[px, nx, py, ny, pz, nz]
404
+ * @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
369
405
  * @param opts SkyboxOptions
370
406
  */
371
- function loadCubeSkybox(renderer_1, scene_1, paths_1) {
372
- return __awaiter(this, arguments, void 0, function* (renderer, scene, paths, opts = {}) {
373
- var _a, _b;
374
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
375
- if (!Array.isArray(paths) || paths.length !== 6)
376
- throw new Error('cube skybox requires 6 image paths');
377
- const key = paths.join('|');
378
- // 缓存处理
379
- if (options.cache && cubeCache.has(key)) {
380
- const rec = cubeCache.get(key);
381
- rec.refCount += 1;
382
- // reapply to scene (in case it was removed)
383
- if (options.setAsBackground)
384
- scene.background = rec.handle.backgroundTexture;
385
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
386
- scene.environment = rec.handle.envRenderTarget.texture;
387
- return rec.handle;
388
- }
389
- // 加载立方体贴图
390
- const loader = new THREE__namespace.CubeTextureLoader();
391
- const texture = yield new Promise((resolve, reject) => {
392
- loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
393
- });
394
- // 设置编码与映射
395
- if (options.useSRGBEncoding)
396
- texture.encoding = THREE__namespace.sRGBEncoding;
397
- texture.mapping = THREE__namespace.CubeReflectionMapping;
398
- // apply as background if required
407
+ async function loadCubeSkybox(renderer, scene, paths, opts = {}) {
408
+ const options = { ...DEFAULT_OPTIONS, ...opts };
409
+ if (!Array.isArray(paths) || paths.length !== 6)
410
+ throw new Error('cube skybox requires 6 image paths');
411
+ const key = paths.join('|');
412
+ // Cache handling
413
+ if (options.cache && cubeCache.has(key)) {
414
+ const rec = cubeCache.get(key);
415
+ rec.refCount += 1;
416
+ // reapply to scene (in case it was removed)
399
417
  if (options.setAsBackground)
400
- scene.background = texture;
401
- // environment: use PMREM to produce a proper prefiltered env map for PBR
402
- let pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
403
- (_b = pmremGenerator.compileCubemapShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
404
- // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
405
- let envRenderTarget = null;
406
- if (pmremGenerator.fromCubemap) {
407
- envRenderTarget = pmremGenerator.fromCubemap(texture);
418
+ scene.background = rec.handle.backgroundTexture;
419
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
420
+ scene.environment = rec.handle.envRenderTarget.texture;
421
+ return rec.handle;
422
+ }
423
+ // Load cube texture
424
+ const loader = new THREE__namespace.CubeTextureLoader();
425
+ const texture = await new Promise((resolve, reject) => {
426
+ loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
427
+ });
428
+ // Set encoding and mapping
429
+ if (options.useSRGBEncoding)
430
+ texture.encoding = THREE__namespace.sRGBEncoding;
431
+ texture.mapping = THREE__namespace.CubeReflectionMapping;
432
+ // apply as background if required
433
+ if (options.setAsBackground)
434
+ scene.background = texture;
435
+ // environment: use PMREM to produce a proper prefiltered env map for PBR
436
+ let pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
437
+ pmremGenerator.compileCubemapShader?.( /* optional */);
438
+ // fromCubemap might be available in your three.js; fallback to fromEquirectangular approach if not
439
+ let envRenderTarget = null;
440
+ if (pmremGenerator.fromCubemap) {
441
+ envRenderTarget = pmremGenerator.fromCubemap(texture);
442
+ }
443
+ else {
444
+ // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
445
+ // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
446
+ envRenderTarget = null;
447
+ }
448
+ if (options.setAsEnvironment) {
449
+ if (envRenderTarget) {
450
+ scene.environment = envRenderTarget.texture;
408
451
  }
409
452
  else {
410
- // Fallback: render cube to env map by using generator.fromEquirectangular with a converted equirect if needed.
411
- // Simpler fallback: use the cube texture directly as environment (less correct for reflections).
412
- envRenderTarget = null;
453
+ // fallback: use cube texture directly (works but not prefiltered)
454
+ scene.environment = texture;
413
455
  }
414
- if (options.setAsEnvironment) {
456
+ }
457
+ const handle = {
458
+ key,
459
+ backgroundTexture: options.setAsBackground ? texture : null,
460
+ envRenderTarget: envRenderTarget,
461
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
462
+ setAsBackground: !!options.setAsBackground,
463
+ setAsEnvironment: !!options.setAsEnvironment,
464
+ dispose() {
465
+ // remove from scene
466
+ if (options.setAsBackground && scene.background === texture)
467
+ scene.background = null;
468
+ if (options.setAsEnvironment && scene.environment) {
469
+ // only clear if it's the same texture we set
470
+ if (envRenderTarget && scene.environment === envRenderTarget.texture)
471
+ scene.environment = null;
472
+ else if (scene.environment === texture)
473
+ scene.environment = null;
474
+ }
475
+ // dispose resources only if not cached/shared
415
476
  if (envRenderTarget) {
416
- scene.environment = envRenderTarget.texture;
477
+ try {
478
+ envRenderTarget.dispose();
479
+ }
480
+ catch { }
417
481
  }
418
- else {
419
- // fallback: use cube texture directly (works but not prefiltered)
420
- scene.environment = texture;
482
+ try {
483
+ texture.dispose();
421
484
  }
422
- }
423
- const handle = {
424
- key,
425
- backgroundTexture: options.setAsBackground ? texture : null,
426
- envRenderTarget: envRenderTarget,
427
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator, // only dispose if we created it
428
- setAsBackground: !!options.setAsBackground,
429
- setAsEnvironment: !!options.setAsEnvironment,
430
- dispose() {
431
- // remove from scene
432
- if (options.setAsBackground && scene.background === texture)
433
- scene.background = null;
434
- if (options.setAsEnvironment && scene.environment) {
435
- // only clear if it's the same texture we set
436
- if (envRenderTarget && scene.environment === envRenderTarget.texture)
437
- scene.environment = null;
438
- else if (scene.environment === texture)
439
- scene.environment = null;
440
- }
441
- // dispose resources only if not cached/shared
442
- if (envRenderTarget) {
443
- try {
444
- envRenderTarget.dispose();
445
- }
446
- catch (_a) { }
447
- }
485
+ catch { }
486
+ // dispose pmremGenerator we created
487
+ if (!options.pmremGenerator && pmremGenerator) {
448
488
  try {
449
- texture.dispose();
450
- }
451
- catch (_b) { }
452
- // dispose pmremGenerator we created
453
- if (!options.pmremGenerator && pmremGenerator) {
454
- try {
455
- pmremGenerator.dispose();
456
- }
457
- catch (_c) { }
489
+ pmremGenerator.dispose();
458
490
  }
491
+ catch { }
459
492
  }
460
- };
461
- if (options.cache)
462
- cubeCache.set(key, { handle, refCount: 1 });
463
- return handle;
464
- });
493
+ }
494
+ };
495
+ if (options.cache)
496
+ cubeCache.set(key, { handle, refCount: 1 });
497
+ return handle;
465
498
  }
466
499
  /**
467
- * 加载等距/单图(支持 HDR via RGBELoader
500
+ * Load Equirectangular/Single Image (Supports HDR via RGBELoader)
468
501
  * @param renderer THREE.WebGLRenderer
469
502
  * @param scene THREE.Scene
470
503
  * @param url string - *.hdr, *.exr, *.jpg, *.png
471
504
  * @param opts SkyboxOptions
472
505
  */
473
- function loadEquirectSkybox(renderer_1, scene_1, url_1) {
474
- return __awaiter(this, arguments, void 0, function* (renderer, scene, url, opts = {}) {
475
- var _a, _b;
476
- const options = Object.assign(Object.assign({}, DEFAULT_OPTIONS), opts);
477
- const key = url;
478
- if (options.cache && equirectCache.has(key)) {
479
- const rec = equirectCache.get(key);
480
- rec.refCount += 1;
481
- if (options.setAsBackground)
482
- scene.background = rec.handle.backgroundTexture;
483
- if (options.setAsEnvironment && rec.handle.envRenderTarget)
484
- scene.environment = rec.handle.envRenderTarget.texture;
485
- return rec.handle;
486
- }
487
- // 动态导入 RGBELoader(用于 .hdr/.exr),如果加载的是普通 jpg/png 可直接用 TextureLoader
488
- const isHDR = /\.hdr$|\.exr$/i.test(url);
489
- let hdrTexture;
490
- if (isHDR) {
491
- const { RGBELoader } = yield import('three/examples/jsm/loaders/RGBELoader.js');
492
- hdrTexture = yield new Promise((resolve, reject) => {
493
- new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
494
- });
495
- // RGBE textures typically use LinearEncoding
496
- hdrTexture.encoding = THREE__namespace.LinearEncoding;
497
- }
498
- else {
499
- // ordinary image - use TextureLoader
500
- const loader = new THREE__namespace.TextureLoader();
501
- hdrTexture = yield new Promise((resolve, reject) => {
502
- loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
503
- });
504
- if (options.useSRGBEncoding)
505
- hdrTexture.encoding = THREE__namespace.sRGBEncoding;
506
- }
507
- // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
508
- const pmremGenerator = (_a = options.pmremGenerator) !== null && _a !== void 0 ? _a : new THREE__namespace.PMREMGenerator(renderer);
509
- (_b = pmremGenerator.compileEquirectangularShader) === null || _b === void 0 ? void 0 : _b.call(pmremGenerator);
510
- const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
511
- // envTexture to use for scene.environment
512
- const envTexture = envRenderTarget.texture;
513
- // set background and/or environment
514
- if (options.setAsBackground) {
515
- // for background it's ok to use the equirect texture directly or the envTexture
516
- // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
517
- scene.background = envTexture;
518
- }
519
- if (options.setAsEnvironment) {
520
- scene.environment = envTexture;
521
- }
522
- // We can dispose the original hdrTexture (the PMREM target contains the needed data)
523
- try {
524
- hdrTexture.dispose();
525
- }
526
- catch (_c) { }
527
- const handle = {
528
- key,
529
- backgroundTexture: options.setAsBackground ? envTexture : null,
530
- envRenderTarget,
531
- pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
532
- setAsBackground: !!options.setAsBackground,
533
- setAsEnvironment: !!options.setAsEnvironment,
534
- dispose() {
535
- if (options.setAsBackground && scene.background === envTexture)
536
- scene.background = null;
537
- if (options.setAsEnvironment && scene.environment === envTexture)
538
- scene.environment = null;
506
+ async function loadEquirectSkybox(renderer, scene, url, opts = {}) {
507
+ const options = { ...DEFAULT_OPTIONS, ...opts };
508
+ const key = url;
509
+ if (options.cache && equirectCache.has(key)) {
510
+ const rec = equirectCache.get(key);
511
+ rec.refCount += 1;
512
+ if (options.setAsBackground)
513
+ scene.background = rec.handle.backgroundTexture;
514
+ if (options.setAsEnvironment && rec.handle.envRenderTarget)
515
+ scene.environment = rec.handle.envRenderTarget.texture;
516
+ return rec.handle;
517
+ }
518
+ // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
519
+ const isHDR = /\.hdr$|\.exr$/i.test(url);
520
+ let hdrTexture;
521
+ if (isHDR) {
522
+ const { RGBELoader } = await import('three/examples/jsm/loaders/RGBELoader.js');
523
+ hdrTexture = await new Promise((resolve, reject) => {
524
+ new RGBELoader().load(url, (tex) => resolve(tex), undefined, (err) => reject(err));
525
+ });
526
+ // RGBE textures typically use LinearEncoding
527
+ hdrTexture.encoding = THREE__namespace.LinearEncoding;
528
+ }
529
+ else {
530
+ // ordinary image - use TextureLoader
531
+ const loader = new THREE__namespace.TextureLoader();
532
+ hdrTexture = await new Promise((resolve, reject) => {
533
+ loader.load(url, (t) => resolve(t), undefined, (err) => reject(err));
534
+ });
535
+ if (options.useSRGBEncoding)
536
+ hdrTexture.encoding = THREE__namespace.sRGBEncoding;
537
+ }
538
+ // PMREMGenerator to convert equirectangular to prefiltered cubemap (good for PBR)
539
+ const pmremGenerator = options.pmremGenerator ?? new THREE__namespace.PMREMGenerator(renderer);
540
+ pmremGenerator.compileEquirectangularShader?.();
541
+ const envRenderTarget = pmremGenerator.fromEquirectangular(hdrTexture);
542
+ // envTexture to use for scene.environment
543
+ const envTexture = envRenderTarget.texture;
544
+ // set background and/or environment
545
+ if (options.setAsBackground) {
546
+ // for background it's ok to use the equirect texture directly or the envTexture
547
+ // envTexture is cubemap-like and usually better for reflections; using it as background creates cube-projected look
548
+ scene.background = envTexture;
549
+ }
550
+ if (options.setAsEnvironment) {
551
+ scene.environment = envTexture;
552
+ }
553
+ // We can dispose the original hdrTexture (the PMREM target contains the needed data)
554
+ try {
555
+ hdrTexture.dispose();
556
+ }
557
+ catch { }
558
+ const handle = {
559
+ key,
560
+ backgroundTexture: options.setAsBackground ? envTexture : null,
561
+ envRenderTarget,
562
+ pmremGenerator: options.pmremGenerator ? null : pmremGenerator,
563
+ setAsBackground: !!options.setAsBackground,
564
+ setAsEnvironment: !!options.setAsEnvironment,
565
+ dispose() {
566
+ if (options.setAsBackground && scene.background === envTexture)
567
+ scene.background = null;
568
+ if (options.setAsEnvironment && scene.environment === envTexture)
569
+ scene.environment = null;
570
+ try {
571
+ envRenderTarget.dispose();
572
+ }
573
+ catch { }
574
+ if (!options.pmremGenerator && pmremGenerator) {
539
575
  try {
540
- envRenderTarget.dispose();
541
- }
542
- catch (_a) { }
543
- if (!options.pmremGenerator && pmremGenerator) {
544
- try {
545
- pmremGenerator.dispose();
546
- }
547
- catch (_b) { }
576
+ pmremGenerator.dispose();
548
577
  }
578
+ catch { }
549
579
  }
550
- };
551
- if (options.cache)
552
- equirectCache.set(key, { handle, refCount: 1 });
553
- return handle;
554
- });
580
+ }
581
+ };
582
+ if (options.cache)
583
+ equirectCache.set(key, { handle, refCount: 1 });
584
+ return handle;
555
585
  }
556
- function loadSkybox(renderer_1, scene_1, params_1) {
557
- return __awaiter(this, arguments, void 0, function* (renderer, scene, params, opts = {}) {
558
- if (params.type === 'cube')
559
- return loadCubeSkybox(renderer, scene, params.paths, opts);
560
- return loadEquirectSkybox(renderer, scene, params.url, opts);
561
- });
586
+ async function loadSkybox(renderer, scene, params, opts = {}) {
587
+ if (params.type === 'cube')
588
+ return loadCubeSkybox(renderer, scene, params.paths, opts);
589
+ return loadEquirectSkybox(renderer, scene, params.url, opts);
562
590
  }
563
591
  /* -------------------------
564
- 缓存/引用计数 辅助方法
592
+ Cache / Reference Counting Helper Methods
565
593
  ------------------------- */
566
- /** 释放一个缓存的 skybox(会减少 refCountrefCount=0 时才真正 dispose) */
594
+ /** Release a cached skybox (decrements refCount, only truly disposes when refCount=0) */
567
595
  function releaseSkybox(handle) {
568
596
  // check cube cache
569
597
  if (cubeCache.has(handle.key)) {
@@ -588,85 +616,94 @@ function releaseSkybox(handle) {
588
616
  // handle.dispose()
589
617
  }
590
618
 
591
- // utils/BlueSkyManager.ts - 优化版
592
619
  /**
593
- * BlueSkyManager - 优化版
620
+ * @file blueSkyManager.ts
621
+ * @description
622
+ * Global singleton manager for loading and managing HDR/EXR blue sky environment maps.
623
+ *
624
+ * @best-practice
625
+ * - Call `init` once before use.
626
+ * - Use `loadAsync` to load skyboxes with progress tracking.
627
+ * - Automatically handles PMREM generation for realistic lighting.
628
+ */
629
+ /**
630
+ * BlueSkyManager - Optimized
594
631
  * ---------------------------------------------------------
595
- * 一个全局单例管理器,用于加载和管理基于 HDR/EXR 的蓝天白云环境贴图。
632
+ * A global singleton manager for loading and managing HDR/EXR based blue sky environment maps.
596
633
  *
597
- * ✨ 优化内容:
598
- * - 添加加载进度回调
599
- * - 支持加载取消
600
- * - 完善错误处理
601
- * - 返回 Promise 支持异步
602
- * - 添加加载状态管理
634
+ * Features:
635
+ * - Adds load progress callback
636
+ * - Supports load cancellation
637
+ * - Improved error handling
638
+ * - Returns Promise for async operation
639
+ * - Adds loading state management
603
640
  */
604
641
  class BlueSkyManager {
605
642
  constructor() {
606
- /** 当前环境贴图的 RenderTarget,用于后续释放 */
643
+ /** RenderTarget for current environment map, used for subsequent disposal */
607
644
  this.skyRT = null;
608
- /** 是否已经初始化 */
645
+ /** Whether already initialized */
609
646
  this.isInitialized = false;
610
- /** 当前加载器,用于取消加载 */
647
+ /** Current loader, used for cancelling load */
611
648
  this.currentLoader = null;
612
- /** 加载状态 */
649
+ /** Loading state */
613
650
  this.loadingState = 'idle';
614
651
  }
615
652
  /**
616
- * 初始化
653
+ * Initialize
617
654
  * ---------------------------------------------------------
618
- * 必须在使用 BlueSkyManager 之前调用一次。
619
- * @param renderer WebGLRenderer 实例
620
- * @param scene Three.js 场景
621
- * @param exposure 曝光度 (默认 1.0)
655
+ * Must be called once before using BlueSkyManager.
656
+ * @param renderer WebGLRenderer instance
657
+ * @param scene Three.js Scene
658
+ * @param exposure Exposure (default 1.0)
622
659
  */
623
660
  init(renderer, scene, exposure = 1.0) {
624
661
  if (this.isInitialized) {
625
- console.warn('BlueSkyManager: 已经初始化,跳过重复初始化');
662
+ console.warn('BlueSkyManager: Already initialized, skipping duplicate initialization');
626
663
  return;
627
664
  }
628
665
  this.renderer = renderer;
629
666
  this.scene = scene;
630
- // 使用 ACESFilmicToneMapping,效果更接近真实
667
+ // Use ACESFilmicToneMapping, effect is closer to reality
631
668
  this.renderer.toneMapping = THREE__namespace.ACESFilmicToneMapping;
632
669
  this.renderer.toneMappingExposure = exposure;
633
- // 初始化 PMREM 生成器(全局只需一个)
670
+ // Initialize PMREM generator (only one needed globally)
634
671
  this.pmremGen = new THREE__namespace.PMREMGenerator(renderer);
635
672
  this.pmremGen.compileEquirectangularShader();
636
673
  this.isInitialized = true;
637
674
  }
638
675
  /**
639
- * 加载蓝天 HDR/EXR 贴图并应用到场景(Promise 版本)
676
+ * Load blue sky HDR/EXR map and apply to scene (Promise version)
640
677
  * ---------------------------------------------------------
641
- * @param exrPath HDR/EXR 文件路径
642
- * @param options 加载选项
678
+ * @param exrPath HDR/EXR file path
679
+ * @param options Load options
643
680
  * @returns Promise<void>
644
681
  */
645
682
  loadAsync(exrPath, options = {}) {
646
683
  if (!this.isInitialized) {
647
684
  return Promise.reject(new Error('BlueSkyManager not initialized!'));
648
685
  }
649
- // 取消之前的加载
686
+ // Cancel previous load
650
687
  this.cancelLoad();
651
688
  const { background = true, onProgress, onComplete, onError } = options;
652
689
  this.loadingState = 'loading';
653
690
  this.currentLoader = new EXRLoader_js.EXRLoader();
654
691
  return new Promise((resolve, reject) => {
655
692
  this.currentLoader.load(exrPath,
656
- // 成功回调
693
+ // Success callback
657
694
  (texture) => {
658
695
  try {
659
- // 设置贴图为球面反射映射
696
+ // Set texture mapping to EquirectangularReflectionMapping
660
697
  texture.mapping = THREE__namespace.EquirectangularReflectionMapping;
661
- // 清理旧的环境贴图
698
+ // Clear old environment map
662
699
  this.dispose();
663
- // PMREM 生成高效的环境贴图
700
+ // Generate efficient environment map using PMREM
664
701
  this.skyRT = this.pmremGen.fromEquirectangular(texture);
665
- // 应用到场景:环境光照 & 背景
702
+ // Apply to scene: Environment Lighting & Background
666
703
  this.scene.environment = this.skyRT.texture;
667
704
  if (background)
668
705
  this.scene.background = this.skyRT.texture;
669
- // 原始 HDR/EXR 贴图用完即销毁,节省内存
706
+ // Dispose original HDR/EXR texture immediately to save memory
670
707
  texture.dispose();
671
708
  this.loadingState = 'loaded';
672
709
  this.currentLoader = null;
@@ -684,14 +721,14 @@ class BlueSkyManager {
684
721
  reject(error);
685
722
  }
686
723
  },
687
- // 进度回调
724
+ // Progress callback
688
725
  (xhr) => {
689
726
  if (onProgress && xhr.lengthComputable) {
690
727
  const progress = xhr.loaded / xhr.total;
691
728
  onProgress(progress);
692
729
  }
693
730
  },
694
- // 错误回调
731
+ // Error callback
695
732
  (err) => {
696
733
  this.loadingState = 'error';
697
734
  this.currentLoader = null;
@@ -703,10 +740,10 @@ class BlueSkyManager {
703
740
  });
704
741
  }
705
742
  /**
706
- * 加载蓝天 HDR/EXR 贴图并应用到场景(同步 API,保持向后兼容)
743
+ * Load blue sky HDR/EXR map and apply to scene (Sync API, for backward compatibility)
707
744
  * ---------------------------------------------------------
708
- * @param exrPath HDR/EXR 文件路径
709
- * @param background 是否应用为场景背景 (默认 true)
745
+ * @param exrPath HDR/EXR file path
746
+ * @param background Whether to apply as scene background (default true)
710
747
  */
711
748
  load(exrPath, background = true) {
712
749
  this.loadAsync(exrPath, { background }).catch((error) => {
@@ -714,32 +751,32 @@ class BlueSkyManager {
714
751
  });
715
752
  }
716
753
  /**
717
- * 取消当前加载
754
+ * Cancel current load
718
755
  */
719
756
  cancelLoad() {
720
757
  if (this.currentLoader) {
721
- // EXRLoader 本身没有 abort 方法,但我们可以清空引用
758
+ // EXRLoader itself does not have abort method, but we can clear the reference
722
759
  this.currentLoader = null;
723
760
  this.loadingState = 'idle';
724
761
  }
725
762
  }
726
763
  /**
727
- * 获取加载状态
764
+ * Get loading state
728
765
  */
729
766
  getLoadingState() {
730
767
  return this.loadingState;
731
768
  }
732
769
  /**
733
- * 是否正在加载
770
+ * Is loading
734
771
  */
735
772
  isLoading() {
736
773
  return this.loadingState === 'loading';
737
774
  }
738
775
  /**
739
- * 释放当前的天空贴图资源
776
+ * Release current sky texture resources
740
777
  * ---------------------------------------------------------
741
- * 仅清理 skyRT,不销毁 PMREM
742
- * 适用于切换 HDR/EXR 文件时调用
778
+ * Only cleans up skyRT, does not destroy PMREM
779
+ * Suitable for calling when switching HDR/EXR files
743
780
  */
744
781
  dispose() {
745
782
  if (this.skyRT) {
@@ -753,34 +790,35 @@ class BlueSkyManager {
753
790
  this.scene.environment = null;
754
791
  }
755
792
  /**
756
- * 完全销毁 BlueSkyManager
793
+ * Completely destroy BlueSkyManager
757
794
  * ---------------------------------------------------------
758
- * 包括 PMREMGenerator 的销毁
759
- * 通常在场景彻底销毁或应用退出时调用
795
+ * Includes destruction of PMREMGenerator
796
+ * Usually called when the scene is completely destroyed or the application exits
760
797
  */
761
798
  destroy() {
762
- var _a;
763
799
  this.cancelLoad();
764
800
  this.dispose();
765
- (_a = this.pmremGen) === null || _a === void 0 ? void 0 : _a.dispose();
801
+ this.pmremGen?.dispose();
766
802
  this.isInitialized = false;
767
803
  this.loadingState = 'idle';
768
804
  }
769
805
  }
770
806
  /**
771
- * 🌐 全局单例
807
+ * Global Singleton
772
808
  * ---------------------------------------------------------
773
- * 直接导出一个全局唯一的 BlueSkyManager 实例,
774
- * 保证整个应用中只用一个 PMREMGenerator,性能最佳。
809
+ * Directly export a globally unique BlueSkyManager instance,
810
+ * Ensuring only one PMREMGenerator is used throughout the application for best performance.
775
811
  */
776
812
  const BlueSky = new BlueSkyManager();
777
813
 
778
814
  exports.BlueSky = BlueSky;
779
815
  exports.disposeMaterial = disposeMaterial;
780
816
  exports.disposeObject = disposeObject;
817
+ exports.getLoaderConfig = getLoaderConfig;
781
818
  exports.loadCubeSkybox = loadCubeSkybox;
782
819
  exports.loadEquirectSkybox = loadEquirectSkybox;
783
820
  exports.loadModelByUrl = loadModelByUrl;
784
821
  exports.loadSkybox = loadSkybox;
785
822
  exports.releaseSkybox = releaseSkybox;
823
+ exports.setLoaderConfig = setLoaderConfig;
786
824
  //# sourceMappingURL=index.js.map