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