@chocozhang/three-model-render 1.0.2 → 1.0.4

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 (42) hide show
  1. package/README.md +149 -530
  2. package/dist/camera/index.d.ts +59 -36
  3. package/dist/camera/index.js +77 -57
  4. package/dist/camera/index.js.map +1 -1
  5. package/dist/camera/index.mjs +77 -57
  6. package/dist/camera/index.mjs.map +1 -1
  7. package/dist/core/index.d.ts +60 -27
  8. package/dist/core/index.js +124 -95
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +124 -95
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/effect/index.d.ts +47 -134
  13. package/dist/effect/index.js +109 -65
  14. package/dist/effect/index.js.map +1 -1
  15. package/dist/effect/index.mjs +109 -65
  16. package/dist/effect/index.mjs.map +1 -1
  17. package/dist/index.d.ts +397 -341
  18. package/dist/index.js +651 -472
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +651 -472
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/interaction/index.d.ts +85 -52
  23. package/dist/interaction/index.js +161 -133
  24. package/dist/interaction/index.js.map +1 -1
  25. package/dist/interaction/index.mjs +161 -133
  26. package/dist/interaction/index.mjs.map +1 -1
  27. package/dist/loader/index.d.ts +89 -56
  28. package/dist/loader/index.js +115 -76
  29. package/dist/loader/index.js.map +1 -1
  30. package/dist/loader/index.mjs +115 -76
  31. package/dist/loader/index.mjs.map +1 -1
  32. package/dist/setup/index.d.ts +28 -18
  33. package/dist/setup/index.js +33 -24
  34. package/dist/setup/index.js.map +1 -1
  35. package/dist/setup/index.mjs +33 -24
  36. package/dist/setup/index.mjs.map +1 -1
  37. package/dist/ui/index.d.ts +18 -7
  38. package/dist/ui/index.js +32 -22
  39. package/dist/ui/index.js.map +1 -1
  40. package/dist/ui/index.mjs +32 -22
  41. package/dist/ui/index.mjs.map +1 -1
  42. package/package.json +1 -1
@@ -1,141 +1,36 @@
1
1
  import * as THREE from 'three';
2
2
 
3
3
  /**
4
- * GroupExploder - 基于 Three.js 的模型爆炸效果工具(支持 Vue3 + TS)
4
+ * @file exploder.ts
5
+ * @description
6
+ * GroupExploder - Three.js based model explosion effect tool (Vue3 + TS Support)
5
7
  * ----------------------------------------------------------------------
6
- * 该工具用于对指定 Mesh 的集合进行“爆炸 / 还原”动画:
7
- * - 仅初始化一次(onMounted
8
- * - 支持动态切换模型并自动还原上一个模型的爆炸状态
9
- * - 支持多种排列模式(ring / spiral / grid / radial
10
- * - 支持非爆炸对象自动透明化(dimOthers
11
- * - 支持摄像机自动前置定位到最佳观察点
12
- * - 所有动画均采用原生 requestAnimationFrame 实现
13
- *
14
- * ----------------------------------------------------------------------
15
- * 🔧 构造参数
16
- * ----------------------------------------------------------------------
17
- * @param scene Three.js 场景实例
18
- * @param camera Three.js 相机(一般为 PerspectiveCamera)
19
- * @param controls OrbitControls 控件实例(必须绑定 camera)
20
- *
21
- * ----------------------------------------------------------------------
22
- * 🔥 爆炸参数 ExplodeOptions
23
- * ----------------------------------------------------------------------
24
- * 所有参数均可在 explode() 调用时指定,也可设置默认值。
25
- *
26
- * type ArrangeMode = 'ring' | 'spiral' | 'grid' | 'radial'
27
- *
28
- * @param mode?: ArrangeMode
29
- * 爆炸排列方式:
30
- * - 'ring' 环形排列(默认)
31
- * - 'spiral' 螺旋上升排列
32
- * - 'grid' 平面网格排列(规则整齐)
33
- * - 'radial' 从中心点向外扩散
34
- *
35
- * @param spacing?: number
36
- * 相邻爆炸对象之间的间距(默认:2.5)
37
- *
38
- * @param duration?: number
39
- * 爆炸动画时长(ms),原生 rAF 完成(默认:1000)
40
- *
41
- * @param lift?: number
42
- * 爆炸对象整体抬升的高度因子,用于让爆炸看起来更立体(默认:0.6)
43
- *
44
- * @param cameraPadding?: number
45
- * 摄像机贴合爆炸后包围球时的额外安全距离(默认:1.2)
46
- *
47
- * @param autoRestorePrev?: boolean
48
- * 当切换模型时,是否自动 restore 上一个模型的爆炸元素(默认:true)
49
- *
50
- * @param dimOthers?: { enabled: boolean; opacity?: number }
51
- * 非爆炸对象透明化配置:
52
- * - enabled: true 开启
53
- * - opacity: number 指定非爆炸对象透明度(默认:0.15)
54
- *
55
- * @param debug?: boolean
56
- * 是否开启调试日志,输出所有内部状态(默认 false)
57
- *
58
- *
59
- * ----------------------------------------------------------------------
60
- * 📌 方法说明
61
- * ----------------------------------------------------------------------
62
- *
63
- * ◆ setMeshes(meshSet: Set<Mesh>, contextId?: string)
64
- * 设置当前模型的爆炸 Mesh 集合:
65
- * - 会记录 Mesh 的初始 transform
66
- * - 根据 autoRestorePrev 自动还原上次爆炸
67
- * - 第二个参数 contextId 可选,用于区分业务场景
68
- *
69
- *
70
- * ◆ explode(options?: ExplodeOptions)
71
- * 对当前 meshSet 执行爆炸动画:
72
- * - 根据 mode 生成爆炸布局
73
- * - 相机先自动飞向最佳观察点
74
- * - 执行 mesh 位移动画
75
- * - 按需将非爆炸模型透明化
76
- *
77
- *
78
- * ◆ restore(duration?: number)
79
- * 还原所有爆炸 Mesh 到爆炸前的 transform:
80
- * - 支持平滑动画
81
- * - 自动取消透明化
82
- *
83
- *
84
- * ◆ dispose()
85
- * 移除事件监听、取消动画、清理引用(在组件销毁时调用)
86
- *
87
- *
88
- * ----------------------------------------------------------------------
89
- * 🎨 排列模式说明
90
- * ----------------------------------------------------------------------
91
- *
92
- * 1. Ring(环形)
93
- * - 按圆均匀分布
94
- * - spacing 控制半径
95
- * - lift 控制整体抬起高度
96
- *
97
- * 2. Spiral(螺旋)
98
- * - 在环形基础上添加高度递增(y++)
99
- * - 数量大时视觉效果最强
100
- *
101
- * 3. Grid(网格)
102
- * - 类似棋盘布局
103
- * - spacing 控制网格大小
104
- * - z 不变或小幅度变化
105
- *
106
- * 4. Radial(径向扩散)
107
- * - 从中心向外 “爆炸式” 发散
108
- * - 对于大型组件分解展示非常适合
109
- *
110
- *
111
- * ----------------------------------------------------------------------
112
- * 📌 使用示例(业务层 Vue)
113
- * ----------------------------------------------------------------------
114
- *
115
- * const exploder = new GroupExploder(scene, camera, controls);
116
- *
117
- * onMounted(() => {
118
- * exploder.setMeshes(new Set([meshA, meshB, meshC]));
119
- * });
120
- *
121
- * const triggerExplode = () => {
122
- * exploder.explode({
123
- * mode: 'ring',
124
- * spacing: 3,
125
- * duration: 1200,
126
- * lift: 0.8,
127
- * cameraPadding: 1.3,
128
- * dimOthers: { enabled: true, opacity: 0.2 },
129
- * });
130
- * };
131
- *
132
- * const triggerRestore = () => {
133
- * exploder.restore(600);
134
- * };
135
- *
8
+ * This tool is used to perform "explode / restore" animations on a set of specified Meshes:
9
+ * - Initialize only once (onMounted)
10
+ * - Supports dynamic switching of models and automatically restores the explosion state of the previous model
11
+ * - Supports multiple arrangement modes (ring / spiral / grid / radial)
12
+ * - Supports automatic transparency for non-exploded objects (dimOthers)
13
+ * - Supports automatic camera positioning to the best observation point
14
+ * - All animations use native requestAnimationFrame
15
+ *
16
+ * @best-practice
17
+ * - Initialize in `onMounted`.
18
+ * - Use `setMeshes` to update the active set of meshes to explode.
19
+ * - Call `explode()` to trigger the effect and `restore()` to reset.
136
20
  */
137
21
 
138
22
  type ArrangeMode = 'ring' | 'spiral' | 'grid' | 'radial';
23
+ /**
24
+ * Explosion Parameters
25
+ * @param mode Explosion arrangement mode: 'ring' | 'spiral' | 'grid' | 'radial'
26
+ * @param spacing Spacing between adjacent exploded objects (default: 2.5)
27
+ * @param duration Animation duration in ms (default: 1000)
28
+ * @param lift Lift factor for exploded objects (default: 0.6)
29
+ * @param cameraPadding Extra safety distance for camera framing (default: 1.2)
30
+ * @param autoRestorePrev Whether to automatically restore the previous model's explosion when switching models (default: true)
31
+ * @param dimOthers Configuration for dimming non-exploded objects
32
+ * @param debug Enable debug logs (default: false)
33
+ */
139
34
  type ExplodeOptions = {
140
35
  mode?: ArrangeMode;
141
36
  spacing?: number;
@@ -165,6 +60,12 @@ declare class GroupExploder {
165
60
  private isExploded;
166
61
  private isInitialized;
167
62
  onLog?: (s: string) => void;
63
+ /**
64
+ * Constructor
65
+ * @param scene Three.js Scene instance
66
+ * @param camera Three.js Camera (usually PerspectiveCamera)
67
+ * @param controls OrbitControls instance (must be bound to camera)
68
+ */
168
69
  constructor(scene: THREE.Scene, camera: THREE.PerspectiveCamera | THREE.Camera, controls?: {
169
70
  target?: THREE.Vector3;
170
71
  update?: () => void;
@@ -172,10 +73,12 @@ declare class GroupExploder {
172
73
  private log;
173
74
  init(): void;
174
75
  /**
175
- * setMeshes(newSet):
76
+ * Set the current set of meshes for explosion.
176
77
  * - Detects content-level changes even if same Set reference is used.
177
78
  * - Preserves prevSet/stateMap to allow async restore when needed.
178
79
  * - Ensures stateMap contains snapshots for *all meshes in the new set*.
80
+ * @param newSet The new set of meshes
81
+ * @param contextId Optional context ID to distinguish business scenarios
179
82
  */
180
83
  setMeshes(newSet: Set<THREE.Mesh> | null, options?: {
181
84
  autoRestorePrev?: boolean;
@@ -191,6 +94,11 @@ declare class GroupExploder {
191
94
  * animate camera to that targetBound, then animate meshes to targets.
192
95
  */
193
96
  explode(opts?: ExplodeOptions): Promise<void>;
97
+ /**
98
+ * Restore all exploded meshes to their original transform:
99
+ * - Supports smooth animation
100
+ * - Automatically cancels transparency
101
+ */
194
102
  restore(duration?: number): Promise<void>;
195
103
  /**
196
104
  * restoreSet: reparent and restore transforms using provided stateMap.
@@ -205,9 +113,14 @@ declare class GroupExploder {
205
113
  private computeBoundingSphereForPositionsAndMeshes;
206
114
  private computeTargetsByMode;
207
115
  private animateCameraToFit;
208
- private getCameraLookAtPoint;
116
+ /**
117
+ * Cancel all running animations
118
+ */
209
119
  private cancelAnimations;
210
- dispose(restoreBefore?: boolean): Promise<void>;
120
+ /**
121
+ * Dispose: remove listener, cancel animation, clear references
122
+ */
123
+ dispose(): void;
211
124
  }
212
125
 
213
126
  export { GroupExploder };
@@ -53,10 +53,34 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
53
53
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
54
54
  };
55
55
 
56
+ /**
57
+ * @file exploder.ts
58
+ * @description
59
+ * GroupExploder - Three.js based model explosion effect tool (Vue3 + TS Support)
60
+ * ----------------------------------------------------------------------
61
+ * This tool is used to perform "explode / restore" animations on a set of specified Meshes:
62
+ * - Initialize only once (onMounted)
63
+ * - Supports dynamic switching of models and automatically restores the explosion state of the previous model
64
+ * - Supports multiple arrangement modes (ring / spiral / grid / radial)
65
+ * - Supports automatic transparency for non-exploded objects (dimOthers)
66
+ * - Supports automatic camera positioning to the best observation point
67
+ * - All animations use native requestAnimationFrame
68
+ *
69
+ * @best-practice
70
+ * - Initialize in `onMounted`.
71
+ * - Use `setMeshes` to update the active set of meshes to explode.
72
+ * - Call `explode()` to trigger the effect and `restore()` to reset.
73
+ */
56
74
  function easeInOutQuad(t) {
57
75
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
58
76
  }
59
77
  class GroupExploder {
78
+ /**
79
+ * Constructor
80
+ * @param scene Three.js Scene instance
81
+ * @param camera Three.js Camera (usually PerspectiveCamera)
82
+ * @param controls OrbitControls instance (must be bound to camera)
83
+ */
60
84
  constructor(scene, camera, controls) {
61
85
  // sets and snapshots
62
86
  this.currentSet = null;
@@ -88,10 +112,12 @@ class GroupExploder {
88
112
  this.log('init() called');
89
113
  }
90
114
  /**
91
- * setMeshes(newSet):
115
+ * Set the current set of meshes for explosion.
92
116
  * - Detects content-level changes even if same Set reference is used.
93
117
  * - Preserves prevSet/stateMap to allow async restore when needed.
94
118
  * - Ensures stateMap contains snapshots for *all meshes in the new set*.
119
+ * @param newSet The new set of meshes
120
+ * @param contextId Optional context ID to distinguish business scenarios
95
121
  */
96
122
  setMeshes(newSet, options) {
97
123
  return __awaiter(this, void 0, void 0, function* () {
@@ -293,6 +319,11 @@ class GroupExploder {
293
319
  return;
294
320
  });
295
321
  }
322
+ /**
323
+ * Restore all exploded meshes to their original transform:
324
+ * - Supports smooth animation
325
+ * - Automatically cancels transparency
326
+ */
296
327
  restore(duration = 400) {
297
328
  if (!this.currentSet || this.currentSet.size === 0) {
298
329
  this.log('restore: no currentSet to restore');
@@ -649,99 +680,112 @@ class GroupExploder {
649
680
  return targets;
650
681
  }
651
682
  animateCameraToFit(targetCenter, targetRadius, opts) {
652
- var _a, _b, _c;
683
+ var _a, _b, _c, _d;
653
684
  const duration = (_a = opts === null || opts === void 0 ? void 0 : opts.duration) !== null && _a !== void 0 ? _a : 600;
654
685
  const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
655
686
  if (!(this.camera instanceof THREE__namespace.PerspectiveCamera)) {
656
687
  if (this.controls && this.controls.target) {
657
- this.controls.target.copy(targetCenter);
658
- if (typeof this.controls.update === 'function')
659
- this.controls.update();
688
+ // Fallback for non-PerspectiveCamera
689
+ const startTarget = this.controls.target.clone();
690
+ const startPos = this.camera.position.clone();
691
+ const endTarget = targetCenter.clone();
692
+ const dir = startPos.clone().sub(startTarget).normalize();
693
+ const dist = startPos.distanceTo(startTarget);
694
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
695
+ const startTime = performance.now();
696
+ const tick = (now) => {
697
+ var _a;
698
+ const t = Math.min(1, (now - startTime) / duration);
699
+ const k = easeInOutQuad(t);
700
+ if (this.controls && this.controls.target) {
701
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
702
+ }
703
+ this.camera.position.lerpVectors(startPos, endPos, k);
704
+ if ((_a = this.controls) === null || _a === void 0 ? void 0 : _a.update)
705
+ this.controls.update();
706
+ if (t < 1) {
707
+ this.cameraAnimId = requestAnimationFrame(tick);
708
+ }
709
+ else {
710
+ this.cameraAnimId = null;
711
+ }
712
+ };
713
+ this.cameraAnimId = requestAnimationFrame(tick);
660
714
  }
661
715
  return Promise.resolve();
662
716
  }
663
- const cam = this.camera;
664
- const fov = (cam.fov * Math.PI) / 180;
665
- const safeRadius = isFinite(targetRadius) && targetRadius > 0 ? targetRadius : 1;
666
- const desiredDistance = Math.min(1e6, (safeRadius * ((_c = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _c !== void 0 ? _c : padding)) / Math.sin(fov / 2));
667
- const camPos = cam.position.clone();
668
- const dir = camPos.clone().sub(targetCenter);
669
- if (dir.length() === 0)
717
+ // PerspectiveCamera logic
718
+ const fov = THREE__namespace.MathUtils.degToRad(this.camera.fov);
719
+ const aspect = this.camera.aspect;
720
+ // Calculate distance needed to fit the sphere
721
+ // tan(fov/2) = radius / distance => distance = radius / tan(fov/2)
722
+ // We also consider aspect ratio for horizontal fit
723
+ const distV = targetRadius / Math.sin(fov / 2);
724
+ const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
725
+ const dist = Math.max(distV, distH) * padding;
726
+ const startPos = this.camera.position.clone();
727
+ const startTarget = ((_c = this.controls) === null || _c === void 0 ? void 0 : _c.target) ? this.controls.target.clone() : new THREE__namespace.Vector3(); // assumption
728
+ if (!((_d = this.controls) === null || _d === void 0 ? void 0 : _d.target)) {
729
+ this.camera.getWorldDirection(startTarget);
730
+ startTarget.add(startPos);
731
+ }
732
+ // Determine end position: keep current viewing direction relative to center
733
+ const dir = startPos.clone().sub(startTarget).normalize();
734
+ if (dir.lengthSq() < 0.001)
670
735
  dir.set(0, 0, 1);
671
- else
672
- dir.normalize();
673
- const newCamPos = targetCenter.clone().add(dir.multiplyScalar(desiredDistance));
674
- const startPos = cam.position.clone();
675
- const startTarget = (this.controls && this.controls.target) ? (this.controls.target.clone()) : this.getCameraLookAtPoint();
676
736
  const endTarget = targetCenter.clone();
677
- const startTime = performance.now();
737
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
678
738
  return new Promise((resolve) => {
739
+ const startTime = performance.now();
679
740
  const tick = (now) => {
680
- const t = Math.min(1, (now - startTime) / Math.max(1, duration));
681
- const eased = easeInOutQuad(t);
682
- cam.position.lerpVectors(startPos, newCamPos, eased);
683
- if (this.controls && this.controls.target)
684
- this.controls.target.lerpVectors(startTarget, endTarget, eased);
685
- cam.updateProjectionMatrix();
686
- if (this.controls && typeof this.controls.update === 'function')
687
- this.controls.update();
688
- if (t < 1)
741
+ var _a, _b;
742
+ const t = Math.min(1, (now - startTime) / duration);
743
+ const k = easeInOutQuad(t);
744
+ this.camera.position.lerpVectors(startPos, endPos, k);
745
+ if (this.controls && this.controls.target) {
746
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
747
+ (_b = (_a = this.controls).update) === null || _b === void 0 ? void 0 : _b.call(_a);
748
+ }
749
+ else {
750
+ this.camera.lookAt(endTarget); // simple lookAt if no controls
751
+ }
752
+ if (t < 1) {
689
753
  this.cameraAnimId = requestAnimationFrame(tick);
754
+ }
690
755
  else {
691
756
  this.cameraAnimId = null;
692
- this.log(`animateCameraToFit: done. center=${targetCenter.toArray().map((n) => n.toFixed(2))}, radius=${targetRadius.toFixed(2)}`);
693
757
  resolve();
694
758
  }
695
759
  };
696
760
  this.cameraAnimId = requestAnimationFrame(tick);
697
761
  });
698
762
  }
699
- getCameraLookAtPoint() {
700
- const dir = new THREE__namespace.Vector3();
701
- this.camera.getWorldDirection(dir);
702
- return this.camera.position.clone().add(dir.multiplyScalar(10));
703
- }
763
+ /**
764
+ * Cancel all running animations
765
+ */
704
766
  cancelAnimations() {
705
- if (this.animId) {
767
+ if (this.animId !== null) {
706
768
  cancelAnimationFrame(this.animId);
707
769
  this.animId = null;
708
770
  }
709
- if (this.cameraAnimId) {
771
+ if (this.cameraAnimId !== null) {
710
772
  cancelAnimationFrame(this.cameraAnimId);
711
773
  this.cameraAnimId = null;
712
774
  }
713
775
  }
776
+ /**
777
+ * Dispose: remove listener, cancel animation, clear references
778
+ */
714
779
  dispose() {
715
- return __awaiter(this, arguments, void 0, function* (restoreBefore = true) {
716
- this.cancelAnimations();
717
- if (restoreBefore && this.isExploded) {
718
- try {
719
- yield this.restore(200);
720
- }
721
- catch (_a) { }
722
- }
723
- // force restore of materials
724
- for (const [mat, ctxs] of Array.from(this.materialContexts.entries())) {
725
- const snap = this.materialSnaps.get(mat);
726
- if (snap) {
727
- mat.transparent = snap.transparent;
728
- mat.opacity = snap.opacity;
729
- if (typeof snap.depthWrite !== 'undefined')
730
- mat.depthWrite = snap.depthWrite;
731
- mat.needsUpdate = true;
732
- }
733
- this.materialContexts.delete(mat);
734
- this.materialSnaps.delete(mat);
735
- }
736
- this.contextMaterials.clear();
737
- this.stateMap.clear();
738
- this.prevStateMap.clear();
739
- this.currentSet = null;
740
- this.prevSet = null;
741
- this.isInitialized = false;
742
- this.isExploded = false;
743
- this.log('dispose: cleaned up');
744
- });
780
+ this.cancelAnimations();
781
+ this.currentSet = null;
782
+ this.prevSet = null;
783
+ this.stateMap.clear();
784
+ this.prevStateMap.clear();
785
+ this.materialContexts.clear();
786
+ this.materialSnaps.clear();
787
+ this.contextMaterials.clear();
788
+ this.log('dispose() called, resources cleaned up');
745
789
  }
746
790
  }
747
791