three-cad-viewer 4.3.4 → 4.3.6

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 (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
@@ -1,329 +0,0 @@
1
- /**
2
- * Triplanar texture mapping for MeshPhysicalMaterial.
3
- *
4
- * Replaces standard UV-based texture sampling with model-space triplanar
5
- * projection via `material.onBeforeCompile`. Eliminates seams on curved
6
- * surfaces (cylinders, cones, etc.) and maintains uniform texture scale
7
- * regardless of object proportions.
8
- *
9
- * All coordinates are in **model space** to match the geometry's bounding box.
10
- * `transformed` (model-space position) and `objectNormal` (model-space normal)
11
- * are used — NOT the view-space `transformedNormal`.
12
- *
13
- * NOTE: `onBeforeCompile` receives shaders BEFORE `#include` resolution.
14
- * Therefore we replace `#include <chunk_name>` directives with inline GLSL
15
- * that uses triplanar sampling, rather than replacing expanded texture2D calls.
16
- *
17
- * Handles: map, normalMap, roughnessMap, metalnessMap, emissiveMap, aoMap,
18
- * clearcoatNormalMap, alphaMap.
19
- */
20
-
21
- import * as THREE from "three";
22
-
23
- // ---------------------------------------------------------------------------
24
- // GLSL: Varyings (shared between vertex & fragment)
25
- // ---------------------------------------------------------------------------
26
-
27
- const TRIPLANAR_VARYINGS = /* glsl */ `
28
- varying vec3 vTriplanarPos;
29
- varying vec3 vTriplanarNormal;
30
- `;
31
-
32
- // ---------------------------------------------------------------------------
33
- // GLSL: Fragment header (uniforms + helper functions)
34
- // ---------------------------------------------------------------------------
35
-
36
- const TRIPLANAR_FRAGMENT_HEADER = /* glsl */ `
37
- varying vec3 vTriplanarPos;
38
- varying vec3 vTriplanarNormal;
39
- uniform vec3 triplanarOffset;
40
- uniform float triplanarScale;
41
- uniform vec2 triplanarRepeat;
42
-
43
- // normalMatrix is only declared in the fragment shader for object-space
44
- // normal maps. We need it for triplanar tangent-space normal mapping too.
45
- #ifndef USE_NORMALMAP_OBJECTSPACE
46
- uniform mat3 normalMatrix;
47
- #endif
48
-
49
- // --- Global triplanar state (computed once, reused by all samples) ---
50
- vec2 tri_uvX, tri_uvY, tri_uvZ;
51
- vec3 tri_blend;
52
-
53
- // Initialize global triplanar UVs and blend weights from varyings.
54
- // Must be called before any texture sampling.
55
- void initTriplanarUVs() {
56
- tri_blend = abs(vTriplanarNormal);
57
- tri_blend = pow(tri_blend, vec3(3.0));
58
- tri_blend /= (tri_blend.x + tri_blend.y + tri_blend.z);
59
- vec3 p = (vTriplanarPos - triplanarOffset) * triplanarScale;
60
- vec2 r = triplanarRepeat;
61
- tri_uvX = p.yz * r;
62
- tri_uvY = p.xz * r;
63
- tri_uvZ = p.xy * r;
64
- }
65
-
66
- // Sample a texture using the global triplanar UVs and blend weights.
67
- vec4 triplanarSample(sampler2D tex) {
68
- return texture2D(tex, tri_uvX) * tri_blend.x
69
- + texture2D(tex, tri_uvY) * tri_blend.y
70
- + texture2D(tex, tri_uvZ) * tri_blend.z;
71
- }
72
-
73
- // Surface-gradient triplanar normal mapping.
74
- // Instead of constructing full 3D normals per axis (which creates faceted
75
- // shading at axis boundaries), we extract the tangent-space perturbation (xy)
76
- // from each axis sample, project it into model space as a gradient, blend
77
- // the gradients, and add to the geometric normal. Gradients blend smoothly.
78
- // Ref: Morten Mikkelsen, "Surface Gradient Based Bump Mapping Framework"
79
- //
80
- // normalScale is declared in normalmap_pars_fragment (after this header),
81
- // so we accept it as a parameter to avoid forward-reference errors.
82
- vec3 triplanarNormal(sampler2D normalTex, vec2 nScale) {
83
- vec3 N = vTriplanarNormal;
84
-
85
- // Sample tangent-space normals for each projection axis
86
- vec3 tnX = texture2D(normalTex, tri_uvX).xyz * 2.0 - 1.0;
87
- vec3 tnY = texture2D(normalTex, tri_uvY).xyz * 2.0 - 1.0;
88
- vec3 tnZ = texture2D(normalTex, tri_uvZ).xyz * 2.0 - 1.0;
89
-
90
- // Apply normal scale to perturbation components
91
- tnX.xy *= nScale;
92
- tnY.xy *= nScale;
93
- tnZ.xy *= nScale;
94
-
95
- // Project each tangent-space perturbation (xy) into model space.
96
- // X proj: UV=(y,z) -> tangent=(0,1,0), bitangent=(0,0,1) -> grad=(0, tx, ty)
97
- // Y proj: UV=(x,z) -> tangent=(1,0,0), bitangent=(0,0,1) -> grad=(tx, 0, ty)
98
- // Z proj: UV=(x,y) -> tangent=(1,0,0), bitangent=(0,1,0) -> grad=(tx, ty, 0)
99
- vec3 surfGrad =
100
- vec3(0.0, tnX.x, tnX.y) * tri_blend.x +
101
- vec3(tnY.x, 0.0, tnY.y) * tri_blend.y +
102
- vec3(tnZ.x, tnZ.y, 0.0) * tri_blend.z;
103
-
104
- return normalize(N + surfGrad);
105
- }
106
- `;
107
-
108
- // ---------------------------------------------------------------------------
109
- // GLSL: UV initialization injection
110
- // ---------------------------------------------------------------------------
111
-
112
- /**
113
- * Injected after #include <logdepthbuf_fragment>.
114
- * This is the earliest reliable point in the fragment shader before any
115
- * texture sampling. Initializes global triplanar UVs.
116
- */
117
- const UV_INIT = /* glsl */ `
118
- #include <logdepthbuf_fragment>
119
- initTriplanarUVs();
120
- `;
121
-
122
- // ---------------------------------------------------------------------------
123
- // GLSL: Replacement chunks (inline GLSL replacing #include directives)
124
- // ---------------------------------------------------------------------------
125
-
126
- /** Replaces #include <map_fragment> */
127
- const MAP_FRAGMENT = /* glsl */ `
128
- #ifdef USE_MAP
129
- vec4 sampledDiffuseColor = triplanarSample( map );
130
- #ifdef DECODE_VIDEO_TEXTURE
131
- sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor );
132
- #endif
133
- diffuseColor *= sampledDiffuseColor;
134
- #endif
135
- `;
136
-
137
- /** Replaces #include <roughnessmap_fragment> */
138
- const ROUGHNESSMAP_FRAGMENT = /* glsl */ `
139
- float roughnessFactor = roughness;
140
- #ifdef USE_ROUGHNESSMAP
141
- vec4 texelRoughness = triplanarSample( roughnessMap );
142
- roughnessFactor *= texelRoughness.g;
143
- #endif
144
- `;
145
-
146
- /** Replaces #include <metalnessmap_fragment> */
147
- const METALNESSMAP_FRAGMENT = /* glsl */ `
148
- float metalnessFactor = metalness;
149
- #ifdef USE_METALNESSMAP
150
- vec4 texelMetalness = triplanarSample( metalnessMap );
151
- metalnessFactor *= texelMetalness.b;
152
- #endif
153
- `;
154
-
155
- /** Replaces #include <emissivemap_fragment> */
156
- const EMISSIVEMAP_FRAGMENT = /* glsl */ `
157
- #ifdef USE_EMISSIVEMAP
158
- vec4 emissiveColor = triplanarSample( emissiveMap );
159
- #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE
160
- emissiveColor = sRGBTransferEOTF( emissiveColor );
161
- #endif
162
- totalEmissiveRadiance *= emissiveColor.rgb;
163
- #endif
164
- `;
165
-
166
- /** Replaces #include <aomap_fragment> */
167
- const AOMAP_FRAGMENT = /* glsl */ `
168
- #ifdef USE_AOMAP
169
- float ambientOcclusion = ( triplanarSample( aoMap ).r - 1.0 ) * aoMapIntensity + 1.0;
170
- reflectedLight.indirectDiffuse *= ambientOcclusion;
171
- #if defined( USE_CLEARCOAT )
172
- clearcoatSpecularIndirect *= ambientOcclusion;
173
- #endif
174
- #if defined( USE_SHEEN )
175
- sheenSpecularIndirect *= ambientOcclusion;
176
- #endif
177
- #if defined( USE_ENVMAP ) && defined( STANDARD )
178
- float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
179
- reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
180
- #endif
181
- #endif
182
- `;
183
-
184
- /**
185
- * Replaces #include <normal_fragment_maps>.
186
- * Object-space and bumpmap paths kept as-is.
187
- * Tangent-space path: triplanar normal mapping samples the normal map 3x
188
- * (one per projection axis), swizzles each to model space, blends, and
189
- * transforms to view space via normalMatrix.
190
- */
191
- const NORMAL_FRAGMENT_MAPS = /* glsl */ `
192
- #ifdef USE_NORMALMAP_OBJECTSPACE
193
- normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0;
194
- #ifdef FLIP_SIDED
195
- normal = - normal;
196
- #endif
197
- #ifdef DOUBLE_SIDED
198
- normal = normal * faceDirection;
199
- #endif
200
- normal = normalize( normalMatrix * normal );
201
-
202
- #elif defined( USE_NORMALMAP_TANGENTSPACE )
203
- // Triplanar normal mapping: sample normal map 3x (one per projection axis),
204
- // swizzle each to model space, blend, and transform to view space.
205
- normal = normalize(normalMatrix * triplanarNormal(normalMap, normalScale));
206
-
207
- #elif defined( USE_BUMPMAP )
208
- normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection );
209
-
210
- #endif
211
- `;
212
-
213
- /**
214
- * Replaces #include <clearcoat_normal_fragment_maps>.
215
- * Triplanar clearcoat normal mapping using surface gradient blending.
216
- */
217
- const CLEARCOAT_NORMAL_FRAGMENT_MAPS = /* glsl */ `
218
- #ifdef USE_CLEARCOAT_NORMALMAP
219
- // Reuse triplanarNormal with clearcoat normal map and scale
220
- vec3 clearcoatModelNormal = triplanarNormal(clearcoatNormalMap, clearcoatNormalScale);
221
- clearcoatNormal = normalize(normalMatrix * clearcoatModelNormal);
222
- #endif
223
- `;
224
-
225
- /** Replaces #include <alphamap_fragment> */
226
- const ALPHAMAP_FRAGMENT = /* glsl */ `
227
- #ifdef USE_ALPHAMAP
228
- diffuseColor.a *= triplanarSample( alphaMap ).g;
229
- #endif
230
- `;
231
-
232
- // ---------------------------------------------------------------------------
233
- // Chunk replacement table
234
- // ---------------------------------------------------------------------------
235
-
236
- /** Fragment shader chunk replacements */
237
- const FRAGMENT_CHUNK_REPLACEMENTS: [string, string][] = [
238
- ["#include <logdepthbuf_fragment>", UV_INIT],
239
- ["#include <map_fragment>", MAP_FRAGMENT],
240
- ["#include <roughnessmap_fragment>", ROUGHNESSMAP_FRAGMENT],
241
- ["#include <metalnessmap_fragment>", METALNESSMAP_FRAGMENT],
242
- ["#include <emissivemap_fragment>", EMISSIVEMAP_FRAGMENT],
243
- ["#include <aomap_fragment>", AOMAP_FRAGMENT],
244
- ["#include <normal_fragment_maps>", NORMAL_FRAGMENT_MAPS],
245
- ["#include <clearcoat_normal_fragment_maps>", CLEARCOAT_NORMAL_FRAGMENT_MAPS],
246
- ["#include <alphamap_fragment>", ALPHAMAP_FRAGMENT],
247
- ];
248
-
249
- // ---------------------------------------------------------------------------
250
- // Public API
251
- // ---------------------------------------------------------------------------
252
-
253
- /**
254
- * Apply triplanar texture mapping to a MeshPhysicalMaterial.
255
- *
256
- * Modifies the material's shader via `onBeforeCompile` so that all texture
257
- * lookups use model-space triplanar projection instead of UV coordinates.
258
- * The material should be a **clone** (not shared) because `onBeforeCompile`
259
- * and `customProgramCacheKey` are set on it.
260
- *
261
- * Bounding box of the geometry determines coordinate normalization: the
262
- * texture tiles once across the largest dimension with uniform scale,
263
- * preserving aspect ratio. `textureRepeat` on the material's textures
264
- * (if set) is respected via the triplanarRepeat uniform.
265
- *
266
- * @param material - Material clone to modify
267
- * @param geometry - Geometry for bounding box computation
268
- */
269
- export function applyTriplanarMapping(
270
- material: THREE.MeshPhysicalMaterial,
271
- geometry: THREE.BufferGeometry,
272
- ): void {
273
- // Compute bounding box for coordinate normalization (model space)
274
- geometry.computeBoundingBox();
275
- const bb = geometry.boundingBox!;
276
- const size = new THREE.Vector3();
277
- bb.getSize(size);
278
- const maxDim = Math.max(size.x, size.y, size.z, 1e-6);
279
-
280
- // Uniform values (captured by closure, per-object)
281
- const offset = bb.min.clone();
282
- const scale = 1.0 / maxDim;
283
-
284
- // Read texture repeat from the material's map (all textures share same repeat)
285
- const repeat = material.map?.repeat?.clone() ?? new THREE.Vector2(1, 1);
286
-
287
- material.onBeforeCompile = (shader) => {
288
- // Custom uniforms
289
- shader.uniforms.triplanarOffset = { value: offset };
290
- shader.uniforms.triplanarScale = { value: scale };
291
- shader.uniforms.triplanarRepeat = { value: repeat };
292
-
293
- // --- Vertex shader ---
294
- // Declare varyings
295
- shader.vertexShader = shader.vertexShader.replace(
296
- "#include <common>",
297
- `#include <common>\n${TRIPLANAR_VARYINGS}`,
298
- );
299
-
300
- // Pass model-space position and normal to fragment shader.
301
- // `transformed` = model-space position (after morphing, before view transform)
302
- // `objectNormal` = model-space normal (before normalMatrix)
303
- // Both match the geometry's bounding box coordinate space.
304
- shader.vertexShader = shader.vertexShader.replace(
305
- "#include <worldpos_vertex>",
306
- `#include <worldpos_vertex>
307
- vTriplanarPos = transformed;
308
- vTriplanarNormal = normalize(objectNormal);`,
309
- );
310
-
311
- // --- Fragment shader ---
312
- // Inject varyings, uniforms, and the triplanarSample helper
313
- shader.fragmentShader = shader.fragmentShader.replace(
314
- "#include <common>",
315
- `#include <common>\n${TRIPLANAR_FRAGMENT_HEADER}`,
316
- );
317
-
318
- // Replace texture-sampling #include chunks with triplanar versions
319
- for (const [from, to] of FRAGMENT_CHUNK_REPLACEMENTS) {
320
- shader.fragmentShader = shader.fragmentShader.replace(from, to);
321
- }
322
- };
323
-
324
- // All triplanar materials share the same compiled WebGL program
325
- // (uniform values differ per instance). Appended to Three.js's standard
326
- // program cache key, so materials with different maps/flags still get
327
- // separate programs.
328
- material.customProgramCacheKey = () => "triplanar";
329
- }
@@ -1,343 +0,0 @@
1
- import * as THREE from "three";
2
- import { AXIS_VECTORS } from "../utils/utils.js";
3
- import { logger } from "../utils/logger.js";
4
- import type { Axis } from "../core/types.js";
5
-
6
- /**
7
- * Create a quaternion from an axis and angle in degrees.
8
- */
9
- function fromAxisAngle(axis: Axis, angle: number): THREE.Quaternion {
10
- const q = new THREE.Quaternion();
11
- q.setFromAxisAngle(AXIS_VECTORS[axis], (angle / 180) * Math.PI);
12
- return q;
13
- }
14
-
15
- interface AnimationBackup {
16
- tracks: THREE.KeyframeTrack[];
17
- root: THREE.Object3D | null;
18
- duration: number | null;
19
- speed: number | null;
20
- repeat: boolean | null;
21
- }
22
-
23
- /**
24
- * Manages keyframe animations for CAD objects.
25
- * Supports translation (t, tx, ty, tz) and rotation (q, rx, ry, rz) transforms.
26
- */
27
- class Animation {
28
- delim: string;
29
- tracks: THREE.KeyframeTrack[];
30
- mixer: THREE.AnimationMixer | null;
31
- clip: THREE.AnimationClip | null;
32
- clipAction: THREE.AnimationAction | null;
33
- clock: THREE.Timer;
34
- duration: number | null;
35
- speed: number | null;
36
- repeat: boolean | null;
37
- root: THREE.Object3D | null;
38
- _backup: AnimationBackup | null;
39
-
40
- /**
41
- * Create an Animation manager.
42
- * @param delim - Path delimiter used in object selectors.
43
- */
44
- constructor(delim: string) {
45
- this.delim = delim;
46
- this.tracks = [];
47
- this.mixer = null;
48
- this.clip = null;
49
- this.clipAction = null;
50
- this.clock = new THREE.Timer();
51
- this.duration = null;
52
- this._backup = null;
53
- this.root = null;
54
- this.speed = null;
55
- this.repeat = null;
56
- }
57
-
58
- /**
59
- * Prepare selector by replacing path delimiter.
60
- */
61
- private _prepareSelector(selector: string): string {
62
- return selector.replaceAll("/", this.delim);
63
- }
64
-
65
- /**
66
- * Validate that times and values arrays have the same length.
67
- */
68
- private _validateArrayLengths(times: number[], values: unknown[]): boolean {
69
- if (times.length !== values.length) {
70
- logger.error("times and values arrays need to have the same length");
71
- return false;
72
- }
73
- return true;
74
- }
75
-
76
- /**
77
- * Add a position track (full 3D translation).
78
- * @param selector - Object path selector (using "/" delimiter).
79
- * @param group - The object to animate.
80
- * @param times - Array of keyframe times.
81
- * @param positions - Array of [x, y, z] position offsets.
82
- */
83
- addPositionTrack(
84
- selector: string,
85
- group: THREE.Object3D,
86
- times: number[],
87
- positions: number[][],
88
- ): void {
89
- if (!this._validateArrayLengths(times, positions)) return;
90
-
91
- const basePosition = group.position;
92
- const newValues = positions.map((v) =>
93
- basePosition
94
- .clone()
95
- .add(new THREE.Vector3(...v))
96
- .toArray(),
97
- );
98
-
99
- this.tracks.push(
100
- new THREE.VectorKeyframeTrack(
101
- this._prepareSelector(selector) + ".position",
102
- times,
103
- newValues.flat(),
104
- ),
105
- );
106
- }
107
-
108
- /**
109
- * Add a single-axis translation track.
110
- * @param selector - Object path selector (using "/" delimiter).
111
- * @param group - The object to animate.
112
- * @param axis - Which axis to translate along ("x", "y", or "z").
113
- * @param times - Array of keyframe times.
114
- * @param values - Array of translation values along the axis.
115
- */
116
- addTranslationTrack(
117
- selector: string,
118
- group: THREE.Object3D,
119
- axis: Axis,
120
- times: number[],
121
- values: number[],
122
- ): void {
123
- if (!this._validateArrayLengths(times, values)) return;
124
-
125
- const basePosition = group.position;
126
- const offsets: Record<Axis, (v: number) => THREE.Vector3> = {
127
- x: (v) => new THREE.Vector3(v, 0, 0),
128
- y: (v) => new THREE.Vector3(0, v, 0),
129
- z: (v) => new THREE.Vector3(0, 0, v),
130
- };
131
-
132
- const newValues = values.map((v) =>
133
- basePosition.clone().add(offsets[axis](v)).toArray(),
134
- );
135
-
136
- this.tracks.push(
137
- new THREE.VectorKeyframeTrack(
138
- this._prepareSelector(selector) + ".position",
139
- times,
140
- newValues.flat(),
141
- ),
142
- );
143
- }
144
-
145
- /**
146
- * Add a quaternion rotation track.
147
- * @param selector - Object path selector (using "/" delimiter).
148
- * @param group - The object to animate.
149
- * @param times - Array of keyframe times.
150
- * @param quaternions - Array of [x, y, z, w] quaternion values.
151
- */
152
- addQuaternionTrack(
153
- selector: string,
154
- group: THREE.Object3D,
155
- times: number[],
156
- quaternions: number[][],
157
- ): void {
158
- if (!this._validateArrayLengths(times, quaternions)) return;
159
-
160
- const baseQuaternion = group.quaternion;
161
- const newValues = quaternions.map((q) =>
162
- baseQuaternion
163
- .clone()
164
- .multiply(new THREE.Quaternion(...q))
165
- .toArray(),
166
- );
167
-
168
- this.tracks.push(
169
- new THREE.QuaternionKeyframeTrack(
170
- this._prepareSelector(selector) + ".quaternion",
171
- times,
172
- newValues.flat(),
173
- ),
174
- );
175
- }
176
-
177
- /**
178
- * Add a single-axis rotation track.
179
- * @param selector - Object path selector (using "/" delimiter).
180
- * @param group - The object to animate.
181
- * @param axis - Which axis to rotate around ("x", "y", or "z").
182
- * @param times - Array of keyframe times.
183
- * @param angles - Array of rotation angles in degrees.
184
- */
185
- addRotationTrack(
186
- selector: string,
187
- group: THREE.Object3D,
188
- axis: Axis,
189
- times: number[],
190
- angles: number[],
191
- ): void {
192
- if (!this._validateArrayLengths(times, angles)) return;
193
-
194
- const baseQuaternion = group.quaternion;
195
- const newValues = angles.map((angle) =>
196
- baseQuaternion.clone().multiply(fromAxisAngle(axis, angle)).toArray(),
197
- );
198
-
199
- this.tracks.push(
200
- new THREE.QuaternionKeyframeTrack(
201
- this._prepareSelector(selector) + ".quaternion",
202
- times,
203
- newValues.flat(),
204
- ),
205
- );
206
- }
207
-
208
- /**
209
- * Store current animation state for later restoration.
210
- */
211
- backup(): void {
212
- this._backup = {
213
- tracks: this.tracks,
214
- root: this.root,
215
- duration: this.duration,
216
- speed: this.speed,
217
- repeat: this.repeat,
218
- };
219
- }
220
-
221
- /**
222
- * Restore previously backed up animation state.
223
- */
224
- restore(): {
225
- duration: number | null;
226
- speed: number | null;
227
- repeat: boolean | null;
228
- } {
229
- if (this._backup === null) {
230
- return { duration: null, speed: null, repeat: null };
231
- }
232
- this.tracks = this._backup.tracks;
233
- return {
234
- duration: this._backup.duration,
235
- speed: this._backup.speed,
236
- repeat: this._backup.repeat,
237
- };
238
- }
239
-
240
- /**
241
- * Clear the backup state.
242
- */
243
- cleanBackup(): void {
244
- this._backup = null;
245
- }
246
-
247
- /**
248
- * Check if any animation tracks have been added.
249
- */
250
- hasTracks(): boolean {
251
- return this.tracks != null && this.tracks.length > 0;
252
- }
253
-
254
- /**
255
- * Check if a backup exists.
256
- */
257
- hasBackup(): boolean {
258
- return this._backup !== null;
259
- }
260
-
261
- /**
262
- * Create and start the animation.
263
- * @param root - Root object containing animated children.
264
- * @param duration - Animation duration in seconds.
265
- * @param speed - Playback speed multiplier.
266
- * @param repeat - Whether to loop (true) or ping-pong (false).
267
- * @returns The created animation action.
268
- */
269
- animate(
270
- root: THREE.Object3D,
271
- duration: number,
272
- speed: number,
273
- repeat: boolean = true,
274
- ): THREE.AnimationAction {
275
- this.root = root;
276
- this.duration = duration;
277
- this.speed = speed;
278
- this.repeat = repeat;
279
-
280
- this.clip = new THREE.AnimationClip("track", duration, this.tracks);
281
- this.mixer = new THREE.AnimationMixer(root);
282
- this.mixer.timeScale = speed;
283
-
284
- this.clipAction = this.mixer.clipAction(this.clip);
285
- this.clipAction.setLoop(
286
- repeat ? THREE.LoopRepeat : THREE.LoopPingPong,
287
- Infinity,
288
- );
289
- return this.clipAction;
290
- }
291
-
292
- /**
293
- * Set the animation to a specific relative time (0-1).
294
- * Pauses the animation at that point.
295
- */
296
- setRelativeTime(fraction: number): void {
297
- if (!this.clipAction || !this.duration) return;
298
- this.clipAction.play();
299
- this.clipAction.paused = true;
300
- const currentTime = this.duration * fraction;
301
- this.clipAction.time = currentTime;
302
- }
303
-
304
- /**
305
- * Get the current relative time (0-1).
306
- */
307
- getRelativeTime(): number {
308
- if (!this.clipAction || !this.duration) return 0;
309
- return this.clipAction.time / this.duration;
310
- }
311
-
312
- /**
313
- * Dispose of animation resources.
314
- */
315
- dispose(): void {
316
- if (this.mixer) {
317
- this.mixer.stopAllAction();
318
- if (this.clip) {
319
- this.mixer.uncacheClip(this.clip);
320
- }
321
- if (this.root) {
322
- this.mixer.uncacheRoot(this.root);
323
- }
324
- }
325
- this.mixer = null;
326
- this.clipAction = null;
327
- this.clip = null;
328
- this.tracks = [];
329
- this.root = null;
330
- }
331
-
332
- /**
333
- * Update the animation mixer (call each frame when animating).
334
- */
335
- update(): void {
336
- if (this.mixer) {
337
- this.clock.update();
338
- this.mixer.update(this.clock.getDelta());
339
- }
340
- }
341
- }
342
-
343
- export { Animation };