cyclecad 2.0.1 → 2.1.0

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 (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,967 @@
1
+ /**
2
+ * animation-module.js
3
+ *
4
+ * Complete animation system for cycleCAD with keyframe timeline,
5
+ * camera animation, component sequencing, and video export.
6
+ *
7
+ * Features:
8
+ * - Keyframe Animation: Set position/rotation/scale/visibility at time points
9
+ * - Timeline UI: Visual timeline with scrubber, play/pause/stop controls
10
+ * - Camera Animation: Orbit, flythrough, and zoom paths
11
+ * - Component Animation: Animate individual parts in assembly sequences
12
+ * - Easing Functions: Linear, ease-in/out, bounce, elastic interpolation
13
+ * - Storyboard: Chain multiple animation sequences together
14
+ * - Video Export: Render to MP4 with MediaRecorder
15
+ * - Explode Animation: Auto-generate assembly explode/collapse sequences
16
+ *
17
+ * @module animation-module
18
+ * @version 1.0.0
19
+ * @requires three
20
+ *
21
+ * @tutorial
22
+ * // Initialize animation module
23
+ * const animation = await import('./modules/animation-module.js');
24
+ * animation.init(viewport, kernel, containerEl);
25
+ *
26
+ * // Create a new animation
27
+ * animation.createAnimation('Assembly Demo', 10000); // 10 second duration
28
+ *
29
+ * // Add keyframes for a part
30
+ * animation.addKeyframe('Part_1', 0, {
31
+ * position: [0, 0, 0],
32
+ * rotation: [0, 0, 0],
33
+ * visible: true
34
+ * });
35
+ *
36
+ * animation.addKeyframe('Part_1', 5000, {
37
+ * position: [100, 0, 0],
38
+ * rotation: [0, Math.PI, 0],
39
+ * visible: true,
40
+ * easing: 'easeInOutCubic'
41
+ * });
42
+ *
43
+ * // Play the animation
44
+ * animation.play();
45
+ *
46
+ * // Export as video
47
+ * animation.exportVideo({
48
+ * format: 'webm',
49
+ * fps: 30,
50
+ * duration: 10000
51
+ * }).then(blob => {
52
+ * const url = URL.createObjectURL(blob);
53
+ * window.open(url);
54
+ * });
55
+ *
56
+ * @example
57
+ * // Create an assembly sequence animation
58
+ * animation.createAnimation('DUO Assembly', 30000);
59
+ * animation.autoGenerateExplode('assembly_main', {
60
+ * explodeDistance: 150,
61
+ * startTime: 0,
62
+ * duration: 15000
63
+ * });
64
+ * animation.addCameraPath([
65
+ * { pos: [-300, 200, 300], target: [0, 0, 0], t: 0 },
66
+ * { pos: [300, 200, -300], target: [0, 0, 0], t: 30000 }
67
+ * ]);
68
+ * animation.play();
69
+ */
70
+
71
+ import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js';
72
+
73
+ // ============================================================================
74
+ // MODULE STATE
75
+ // ============================================================================
76
+
77
+ let animationState = {
78
+ viewport: null,
79
+ kernel: null,
80
+ containerEl: null,
81
+ currentAnimation: null,
82
+ animations: new Map(),
83
+ isPlaying: false,
84
+ currentTime: 0,
85
+ startTime: 0,
86
+ keyframes: new Map(),
87
+ cameraPath: null,
88
+ easing: {},
89
+ timeline: null
90
+ };
91
+
92
+ // ============================================================================
93
+ // EASING FUNCTIONS
94
+ // ============================================================================
95
+
96
+ const easingFunctions = {
97
+ linear: (t) => t,
98
+ easeInQuad: (t) => t * t,
99
+ easeOutQuad: (t) => t * (2 - t),
100
+ easeInOutQuad: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
101
+ easeInCubic: (t) => t * t * t,
102
+ easeOutCubic: (t) => 1 + (--t) * t * t,
103
+ easeInOutCubic: (t) => t < 0.5 ? 4 * t * t * t : 1 + (--t) * (2 * (--t)) * (2 * t + 1),
104
+ easeInQuart: (t) => t * t * t * t,
105
+ easeOutQuart: (t) => 1 - (--t) * t * t * t,
106
+ easeInOutQuart: (t) => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
107
+ easeInQuint: (t) => t * t * t * t * t,
108
+ easeOutQuint: (t) => 1 + (--t) * t * t * t * t,
109
+ easeInOutQuint: (t) => t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t,
110
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
111
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
112
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
113
+ easeInExpo: (t) => t === 0 ? 0 : Math.pow(2, 10 * t - 10),
114
+ easeOutExpo: (t) => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
115
+ easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ?
116
+ Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
117
+ easeInCirc: (t) => 1 - Math.sqrt(1 - Math.pow(t, 2)),
118
+ easeOutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
119
+ easeInOutCirc: (t) => t < 0.5 ?
120
+ (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2 :
121
+ (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
122
+ easeInElastic: (t) => t === 0 ? 0 : t === 1 ? 1 :
123
+ -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3)),
124
+ easeOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 :
125
+ Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1,
126
+ easeInOutElastic: (t) => t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ?
127
+ -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * ((2 * Math.PI) / 9))) / 2 :
128
+ (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * ((2 * Math.PI) / 9))) / 2 + 1,
129
+ easeInBounce: (t) => 1 - easingFunctions.easeOutBounce(1 - t),
130
+ easeOutBounce: (t) => {
131
+ const n1 = 7.5625, d1 = 2.75;
132
+ if (t < 1 / d1) return n1 * t * t;
133
+ else if (t < 2 / d1) return n1 * (t -= 1.5 / d1) * t + 0.75;
134
+ else if (t < 2.5 / d1) return n1 * (t -= 2.25 / d1) * t + 0.9375;
135
+ else return n1 * (t -= 2.625 / d1) * t + 0.984375;
136
+ },
137
+ easeInOutBounce: (t) => t < 0.5 ?
138
+ (1 - easingFunctions.easeOutBounce(1 - 2 * t)) / 2 :
139
+ (1 + easingFunctions.easeOutBounce(2 * t - 1)) / 2
140
+ };
141
+
142
+ // ============================================================================
143
+ // PUBLIC API
144
+ // ============================================================================
145
+
146
+ /**
147
+ * Initialize the animation module
148
+ *
149
+ * @param {object} viewport - Three.js viewport with scene and renderer
150
+ * @param {object} kernel - CAD kernel
151
+ * @param {HTMLElement} [containerEl] - Container for timeline UI
152
+ */
153
+ export function init(viewport, kernel, containerEl = null) {
154
+ animationState.viewport = viewport;
155
+ animationState.kernel = kernel;
156
+ animationState.containerEl = containerEl;
157
+
158
+ // Setup animation loop
159
+ let lastFrameTime = 0;
160
+ const animationLoop = (timestamp) => {
161
+ if (animationState.isPlaying) {
162
+ if (lastFrameTime === 0) {
163
+ animationState.startTime = timestamp;
164
+ }
165
+
166
+ animationState.currentTime = timestamp - animationState.startTime;
167
+
168
+ // Update all animated objects
169
+ updateFrame(animationState.currentTime);
170
+
171
+ // Update UI
172
+ if (animationState.timeline) {
173
+ updateTimelineUI();
174
+ }
175
+
176
+ lastFrameTime = timestamp;
177
+ }
178
+
179
+ requestAnimationFrame(animationLoop);
180
+ };
181
+
182
+ requestAnimationFrame(animationLoop);
183
+ console.log('[Animation] Module initialized');
184
+ }
185
+
186
+ /**
187
+ * Create a new animation
188
+ *
189
+ * @tutorial
190
+ * animation.createAnimation('Assembly Walk-Through', 15000);
191
+ *
192
+ * @param {string} name - Animation name
193
+ * @param {number} duration - Duration in milliseconds
194
+ * @param {object} [options={}] - Configuration options
195
+ * @returns {object} Animation object
196
+ */
197
+ export function createAnimation(name, duration, options = {}) {
198
+ const animation = {
199
+ name,
200
+ duration,
201
+ keyframes: new Map(),
202
+ cameraPath: null,
203
+ createdAt: new Date(),
204
+ ...options
205
+ };
206
+
207
+ animationState.animations.set(name, animation);
208
+ animationState.currentAnimation = animation;
209
+ animationState.keyframes = animation.keyframes;
210
+
211
+ console.log(`[Animation] Created animation: ${name} (${duration}ms)`);
212
+ return animation;
213
+ }
214
+
215
+ /**
216
+ * Add a keyframe for an object
217
+ *
218
+ * @tutorial
219
+ * animation.addKeyframe('cube_body', 2000, {
220
+ * position: [50, 0, 0],
221
+ * rotation: [0, Math.PI/4, 0],
222
+ * scale: 1.0,
223
+ * visible: true,
224
+ * easing: 'easeInOutCubic',
225
+ * opacity: 1.0
226
+ * });
227
+ *
228
+ * @param {string} objectId - Name of object to animate
229
+ * @param {number} time - Time in milliseconds from animation start
230
+ * @param {object} properties - Properties to animate:
231
+ * - position: [x, y, z] | THREE.Vector3
232
+ * - rotation: [x, y, z] | THREE.Euler
233
+ * - scale: number | [x, y, z]
234
+ * - visible: boolean
235
+ * - opacity: 0-1
236
+ * - easing: easing function name
237
+ */
238
+ export function addKeyframe(objectId, time, properties = {}) {
239
+ if (!animationState.currentAnimation) {
240
+ console.warn('[Animation] No active animation. Create one first.');
241
+ return;
242
+ }
243
+
244
+ if (!animationState.keyframes.has(objectId)) {
245
+ animationState.keyframes.set(objectId, []);
246
+ }
247
+
248
+ const keyframe = {
249
+ time,
250
+ properties: {
251
+ ...properties,
252
+ easing: properties.easing || 'linear'
253
+ }
254
+ };
255
+
256
+ const frames = animationState.keyframes.get(objectId);
257
+ frames.push(keyframe);
258
+ frames.sort((a, b) => a.time - b.time);
259
+
260
+ console.log(`[Animation] Keyframe added for ${objectId} at ${time}ms`);
261
+ }
262
+
263
+ /**
264
+ * Play the current animation
265
+ *
266
+ * @tutorial
267
+ * animation.play();
268
+ * // Animation will run for its duration then stop
269
+ */
270
+ export function play() {
271
+ if (!animationState.currentAnimation) {
272
+ console.warn('[Animation] No animation to play');
273
+ return;
274
+ }
275
+
276
+ animationState.isPlaying = true;
277
+ animationState.currentTime = 0;
278
+ animationState.startTime = 0;
279
+
280
+ console.log('[Animation] Playing:', animationState.currentAnimation.name);
281
+ }
282
+
283
+ /**
284
+ * Pause the current animation
285
+ */
286
+ export function pause() {
287
+ animationState.isPlaying = false;
288
+ console.log('[Animation] Paused');
289
+ }
290
+
291
+ /**
292
+ * Stop and reset the animation
293
+ */
294
+ export function stop() {
295
+ animationState.isPlaying = false;
296
+ animationState.currentTime = 0;
297
+
298
+ // Reset all objects to initial state
299
+ resetToInitialState();
300
+
301
+ console.log('[Animation] Stopped');
302
+ }
303
+
304
+ /**
305
+ * Set animation duration
306
+ *
307
+ * @param {number} duration - Duration in milliseconds
308
+ */
309
+ export function setDuration(duration) {
310
+ if (animationState.currentAnimation) {
311
+ animationState.currentAnimation.duration = duration;
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Add camera animation path
317
+ *
318
+ * @tutorial
319
+ * animation.addCameraPath([
320
+ * { pos: [-200, 100, 200], target: [0, 0, 0], t: 0 },
321
+ * { pos: [200, 100, -200], target: [0, 0, 0], t: 5000 },
322
+ * { pos: [0, 300, 0], target: [0, 0, 0], t: 10000 }
323
+ * ]);
324
+ *
325
+ * @param {Array<object>} waypoints - Array of waypoints:
326
+ * - pos: [x, y, z] camera position
327
+ * - target: [x, y, z] look-at target
328
+ * - t: time in milliseconds
329
+ */
330
+ export function addCameraPath(waypoints) {
331
+ animationState.cameraPath = {
332
+ waypoints,
333
+ currentSegment: 0
334
+ };
335
+
336
+ console.log(`[Animation] Camera path added with ${waypoints.length} waypoints`);
337
+ }
338
+
339
+ /**
340
+ * Generate assembly explode/collapse animation
341
+ *
342
+ * @tutorial
343
+ * animation.autoGenerateExplode('assembly_name', {
344
+ * explodeDistance: 200,
345
+ * startTime: 0,
346
+ * duration: 15000,
347
+ * easing: 'easeInOutCubic'
348
+ * });
349
+ *
350
+ * @param {string|object} assembly - Assembly object or ID
351
+ * @param {object} options - Configuration:
352
+ * - explodeDistance: {number} How far to move components (default: 100)
353
+ * - startTime: {number} When to start in animation (default: 0)
354
+ * - duration: {number} How long explode takes (default: 5000)
355
+ * - easing: {string} Easing function (default: 'easeInOutCubic')
356
+ * - collapse: {boolean} Animate collapse after explode (default: false)
357
+ */
358
+ export function autoGenerateExplode(assembly, options = {}) {
359
+ const {
360
+ explodeDistance = 100,
361
+ startTime = 0,
362
+ duration = 5000,
363
+ easing = 'easeInOutCubic',
364
+ collapse = false
365
+ } = options;
366
+
367
+ if (!animationState.currentAnimation) {
368
+ console.warn('[Animation] No active animation');
369
+ return;
370
+ }
371
+
372
+ // Get assembly from viewport
373
+ const assemblyObj = typeof assembly === 'string' ?
374
+ animationState.viewport.scene.getObjectByName(assembly) : assembly;
375
+
376
+ if (!assemblyObj || !assemblyObj.children) {
377
+ console.warn('[Animation] Assembly not found or has no children');
378
+ return;
379
+ }
380
+
381
+ // Generate keyframes for each component
382
+ assemblyObj.children.forEach((child, index) => {
383
+ const offset = new THREE.Vector3()
384
+ .random()
385
+ .multiplyScalar(2)
386
+ .subScalar(1)
387
+ .normalize()
388
+ .multiplyScalar(explodeDistance);
389
+
390
+ // Store initial position
391
+ const initialPos = child.position.clone();
392
+
393
+ // Explode keyframe
394
+ addKeyframe(child.name || `Component_${index}`, startTime, {
395
+ position: [initialPos.x, initialPos.y, initialPos.z],
396
+ visible: true,
397
+ easing: 'linear'
398
+ });
399
+
400
+ addKeyframe(child.name || `Component_${index}`, startTime + duration, {
401
+ position: [initialPos.x + offset.x, initialPos.y + offset.y, initialPos.z + offset.z],
402
+ visible: true,
403
+ easing
404
+ });
405
+
406
+ if (collapse) {
407
+ addKeyframe(child.name || `Component_${index}`, startTime + duration * 2, {
408
+ position: [initialPos.x, initialPos.y, initialPos.z],
409
+ visible: true,
410
+ easing
411
+ });
412
+ }
413
+ });
414
+
415
+ console.log(`[Animation] Generated explode sequence for ${assemblyObj.children.length} components`);
416
+ }
417
+
418
+ /**
419
+ * Export animation as video file
420
+ *
421
+ * @tutorial
422
+ * animation.exportVideo({
423
+ * format: 'webm', // or 'mp4'
424
+ * fps: 30,
425
+ * quality: 'high'
426
+ * }).then(blob => {
427
+ * const url = URL.createObjectURL(blob);
428
+ * const link = document.createElement('a');
429
+ * link.href = url;
430
+ * link.download = 'animation.webm';
431
+ * link.click();
432
+ * });
433
+ *
434
+ * @param {object} options - Export options:
435
+ * - format: 'webm'|'mp4' (default: 'webm')
436
+ * - fps: number (default: 30)
437
+ * - quality: 'low'|'medium'|'high' (default: 'high')
438
+ * - width: number (default: canvas width)
439
+ * - height: number (default: canvas height)
440
+ * @returns {Promise<Blob>} Video blob
441
+ */
442
+ export async function exportVideo(options = {}) {
443
+ const {
444
+ format = 'webm',
445
+ fps = 30,
446
+ quality = 'high',
447
+ width = animationState.viewport.renderer.domElement.width,
448
+ height = animationState.viewport.renderer.domElement.height
449
+ } = options;
450
+
451
+ const duration = animationState.currentAnimation?.duration || 10000;
452
+ const frameCount = Math.ceil((duration / 1000) * fps);
453
+
454
+ return new Promise((resolve) => {
455
+ const canvas = animationState.viewport.renderer.domElement;
456
+ const stream = canvas.captureStream(fps);
457
+
458
+ const mimeType = format === 'mp4' ?
459
+ 'video/mp4;codecs=h264' : 'video/webm';
460
+
461
+ const mediaRecorder = new MediaRecorder(stream, {
462
+ mimeType,
463
+ videoBitsPerSecond: quality === 'high' ? 5000000 : 2500000
464
+ });
465
+
466
+ const chunks = [];
467
+
468
+ mediaRecorder.ondataavailable = (e) => {
469
+ if (e.data.size > 0) {
470
+ chunks.push(e.data);
471
+ }
472
+ };
473
+
474
+ mediaRecorder.onstop = () => {
475
+ const blob = new Blob(chunks, { type: mimeType });
476
+ resolve(blob);
477
+ };
478
+
479
+ // Play animation and record
480
+ mediaRecorder.start();
481
+
482
+ let currentFrame = 0;
483
+ const recordFrame = () => {
484
+ if (currentFrame >= frameCount) {
485
+ mediaRecorder.stop();
486
+ return;
487
+ }
488
+
489
+ // Update animation frame
490
+ const time = (currentFrame / fps) * 1000;
491
+ animationState.currentTime = time;
492
+ updateFrame(time);
493
+
494
+ animationState.viewport.renderer.render(
495
+ animationState.viewport.scene,
496
+ animationState.viewport.camera
497
+ );
498
+
499
+ currentFrame++;
500
+ setTimeout(recordFrame, 1000 / fps);
501
+ };
502
+
503
+ recordFrame();
504
+ });
505
+ }
506
+
507
+ /**
508
+ * Save animation to localStorage
509
+ *
510
+ * @param {string} [name] - Animation name (uses current animation if not specified)
511
+ * @returns {boolean} Success
512
+ */
513
+ export function saveAnimation(name = null) {
514
+ const animation = name ?
515
+ animationState.animations.get(name) :
516
+ animationState.currentAnimation;
517
+
518
+ if (!animation) return false;
519
+
520
+ const data = {
521
+ name: animation.name,
522
+ duration: animation.duration,
523
+ keyframes: Array.from(animation.keyframes.entries())
524
+ };
525
+
526
+ try {
527
+ localStorage.setItem(`cyclecad_anim_${animation.name}`, JSON.stringify(data));
528
+ console.log(`[Animation] Saved: ${animation.name}`);
529
+ return true;
530
+ } catch (e) {
531
+ console.error('[Animation] Save failed:', e);
532
+ return false;
533
+ }
534
+ }
535
+
536
+ /**
537
+ * Load animation from localStorage
538
+ *
539
+ * @param {string} name - Animation name
540
+ * @returns {boolean} Success
541
+ */
542
+ export function loadAnimation(name) {
543
+ try {
544
+ const data = JSON.parse(localStorage.getItem(`cyclecad_anim_${name}`));
545
+ if (!data) return false;
546
+
547
+ const animation = createAnimation(data.name, data.duration);
548
+
549
+ data.keyframes.forEach(([objectId, frames]) => {
550
+ frames.forEach(frame => {
551
+ addKeyframe(objectId, frame.time, frame.properties);
552
+ });
553
+ });
554
+
555
+ console.log(`[Animation] Loaded: ${name}`);
556
+ return true;
557
+ } catch (e) {
558
+ console.error('[Animation] Load failed:', e);
559
+ return false;
560
+ }
561
+ }
562
+
563
+ /**
564
+ * List all saved animations
565
+ *
566
+ * @returns {Array<string>} Array of animation names
567
+ */
568
+ export function listAnimations() {
569
+ const keys = Object.keys(localStorage);
570
+ return keys
571
+ .filter(k => k.startsWith('cyclecad_anim_'))
572
+ .map(k => k.replace('cyclecad_anim_', ''));
573
+ }
574
+
575
+ /**
576
+ * Get current playback time
577
+ *
578
+ * @returns {number} Time in milliseconds
579
+ */
580
+ export function getCurrentTime() {
581
+ return animationState.currentTime;
582
+ }
583
+
584
+ /**
585
+ * Set playback time
586
+ *
587
+ * @param {number} time - Time in milliseconds
588
+ */
589
+ export function setCurrentTime(time) {
590
+ animationState.currentTime = Math.max(0, Math.min(
591
+ time,
592
+ animationState.currentAnimation?.duration || 0
593
+ ));
594
+
595
+ updateFrame(animationState.currentTime);
596
+ }
597
+
598
+ /**
599
+ * Check if animation is playing
600
+ *
601
+ * @returns {boolean}
602
+ */
603
+ export function isPlaying() {
604
+ return animationState.isPlaying;
605
+ }
606
+
607
+ // ============================================================================
608
+ // INTERNAL FUNCTIONS
609
+ // ============================================================================
610
+
611
+ /**
612
+ * Update frame at given time
613
+ * @private
614
+ */
615
+ function updateFrame(time) {
616
+ animationState.keyframes.forEach((frames, objectId) => {
617
+ const object = animationState.viewport.scene.getObjectByName(objectId);
618
+ if (!object) return;
619
+
620
+ // Find surrounding keyframes
621
+ let prevFrame = null, nextFrame = null;
622
+ for (let i = 0; i < frames.length; i++) {
623
+ if (frames[i].time <= time) prevFrame = frames[i];
624
+ if (frames[i].time >= time && !nextFrame) nextFrame = frames[i];
625
+ }
626
+
627
+ if (!prevFrame || !nextFrame) {
628
+ if (prevFrame) applyKeyframeProperties(object, prevFrame.properties);
629
+ return;
630
+ }
631
+
632
+ // Interpolate between frames
633
+ const duration = nextFrame.time - prevFrame.time;
634
+ const elapsed = time - prevFrame.time;
635
+ const progress = duration > 0 ? elapsed / duration : 1;
636
+
637
+ const easingFn = easingFunctions[nextFrame.properties.easing] || easingFunctions.linear;
638
+ const eased = easingFn(Math.min(1, Math.max(0, progress)));
639
+
640
+ interpolateProperties(object, prevFrame.properties, nextFrame.properties, eased);
641
+ });
642
+
643
+ // Update camera if path exists
644
+ if (animationState.cameraPath) {
645
+ updateCameraPath(time);
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Apply keyframe properties to object
651
+ * @private
652
+ */
653
+ function applyKeyframeProperties(object, props) {
654
+ if (props.position) {
655
+ if (Array.isArray(props.position)) {
656
+ object.position.set(...props.position);
657
+ } else if (props.position instanceof THREE.Vector3) {
658
+ object.position.copy(props.position);
659
+ }
660
+ }
661
+
662
+ if (props.rotation) {
663
+ if (Array.isArray(props.rotation)) {
664
+ object.rotation.set(...props.rotation);
665
+ } else if (props.rotation instanceof THREE.Euler) {
666
+ object.rotation.copy(props.rotation);
667
+ }
668
+ }
669
+
670
+ if (props.scale !== undefined) {
671
+ if (typeof props.scale === 'number') {
672
+ object.scale.setScalar(props.scale);
673
+ } else if (Array.isArray(props.scale)) {
674
+ object.scale.set(...props.scale);
675
+ }
676
+ }
677
+
678
+ if (props.visible !== undefined) {
679
+ object.visible = props.visible;
680
+ }
681
+
682
+ if (props.opacity !== undefined && object.material) {
683
+ object.material.opacity = props.opacity;
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Interpolate between two keyframe property sets
689
+ * @private
690
+ */
691
+ function interpolateProperties(object, prevProps, nextProps, t) {
692
+ // Position
693
+ if (prevProps.position && nextProps.position) {
694
+ const p1 = Array.isArray(prevProps.position) ?
695
+ new THREE.Vector3(...prevProps.position) : prevProps.position;
696
+ const p2 = Array.isArray(nextProps.position) ?
697
+ new THREE.Vector3(...nextProps.position) : nextProps.position;
698
+
699
+ object.position.lerpVectors(p1, p2, t);
700
+ }
701
+
702
+ // Rotation (slerp for smooth rotation)
703
+ if (prevProps.rotation && nextProps.rotation) {
704
+ const r1 = new THREE.Quaternion().setFromEuler(
705
+ Array.isArray(prevProps.rotation) ?
706
+ new THREE.Euler(...prevProps.rotation) : prevProps.rotation
707
+ );
708
+ const r2 = new THREE.Quaternion().setFromEuler(
709
+ Array.isArray(nextProps.rotation) ?
710
+ new THREE.Euler(...nextProps.rotation) : nextProps.rotation
711
+ );
712
+
713
+ object.quaternion.slerpQuaternions(r1, r2, t);
714
+ }
715
+
716
+ // Scale
717
+ if (prevProps.scale !== undefined && nextProps.scale !== undefined) {
718
+ const s1 = typeof prevProps.scale === 'number' ? prevProps.scale : 1;
719
+ const s2 = typeof nextProps.scale === 'number' ? nextProps.scale : 1;
720
+ object.scale.setScalar(s1 + (s2 - s1) * t);
721
+ }
722
+
723
+ // Opacity
724
+ if (prevProps.opacity !== undefined && nextProps.opacity !== undefined && object.material) {
725
+ object.material.opacity = prevProps.opacity + (nextProps.opacity - prevProps.opacity) * t;
726
+ }
727
+ }
728
+
729
+ /**
730
+ * Update camera along path
731
+ * @private
732
+ */
733
+ function updateCameraPath(time) {
734
+ if (!animationState.cameraPath) return;
735
+
736
+ const waypoints = animationState.cameraPath.waypoints;
737
+ if (waypoints.length < 2) return;
738
+
739
+ let prevWp = waypoints[0];
740
+ let nextWp = waypoints[1];
741
+
742
+ for (let i = 0; i < waypoints.length; i++) {
743
+ if (waypoints[i].t <= time) prevWp = waypoints[i];
744
+ if (waypoints[i].t >= time && !nextWp) nextWp = waypoints[i];
745
+ }
746
+
747
+ const duration = nextWp.t - prevWp.t;
748
+ const elapsed = time - prevWp.t;
749
+ const t = duration > 0 ? Math.min(1, elapsed / duration) : 0;
750
+
751
+ const pos1 = new THREE.Vector3(...prevWp.pos);
752
+ const pos2 = new THREE.Vector3(...nextWp.pos);
753
+ const target1 = new THREE.Vector3(...prevWp.target);
754
+ const target2 = new THREE.Vector3(...nextWp.target);
755
+
756
+ animationState.viewport.camera.position.lerpVectors(pos1, pos2, t);
757
+ const targetPos = new THREE.Vector3().lerpVectors(target1, target2, t);
758
+ animationState.viewport.camera.lookAt(targetPos);
759
+ }
760
+
761
+ /**
762
+ * Reset all objects to initial state
763
+ * @private
764
+ */
765
+ function resetToInitialState() {
766
+ animationState.keyframes.forEach((frames, objectId) => {
767
+ const object = animationState.viewport.scene.getObjectByName(objectId);
768
+ if (object && frames.length > 0) {
769
+ applyKeyframeProperties(object, frames[0].properties);
770
+ }
771
+ });
772
+ }
773
+
774
+ /**
775
+ * Update timeline UI
776
+ * @private
777
+ */
778
+ function updateTimelineUI() {
779
+ if (!animationState.timeline) return;
780
+
781
+ const progress = animationState.currentAnimation ?
782
+ (animationState.currentTime / animationState.currentAnimation.duration) * 100 : 0;
783
+
784
+ const scrubber = animationState.timeline.querySelector('.timeline-scrubber');
785
+ if (scrubber) {
786
+ scrubber.style.left = progress + '%';
787
+ }
788
+
789
+ const timeDisplay = animationState.timeline.querySelector('.timeline-time');
790
+ if (timeDisplay) {
791
+ timeDisplay.textContent = formatTime(animationState.currentTime);
792
+ }
793
+ }
794
+
795
+ /**
796
+ * Format time as MM:SS
797
+ * @private
798
+ */
799
+ function formatTime(ms) {
800
+ const seconds = Math.floor(ms / 1000);
801
+ const minutes = Math.floor(seconds / 60);
802
+ const secs = seconds % 60;
803
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
804
+ }
805
+
806
+ // ============================================================================
807
+ // HELP ENTRIES
808
+ // ============================================================================
809
+
810
+ export const helpEntries = [
811
+ {
812
+ id: 'animation-keyframes',
813
+ title: 'Keyframe Animation',
814
+ category: 'Animation',
815
+ description: 'Create smooth animations with position, rotation, and visibility keyframes',
816
+ shortcut: 'A, K',
817
+ content: `
818
+ Set up keyframe animations for parts:
819
+ 1. Create an animation with duration
820
+ 2. Add keyframes at time points
821
+ 3. Set position, rotation, scale, visibility
822
+ 4. Choose easing function for smooth transitions
823
+ 5. Play the animation
824
+
825
+ Easing options: linear, easeIn/Out, bounce, elastic, and more.
826
+ `
827
+ },
828
+ {
829
+ id: 'animation-camera',
830
+ title: 'Camera Animation',
831
+ category: 'Animation',
832
+ description: 'Animate camera position and look-at target',
833
+ shortcut: 'A, C',
834
+ content: `
835
+ Create camera animation paths:
836
+ 1. Define waypoints with position and target
837
+ 2. Specify time for each waypoint
838
+ 3. Camera interpolates smoothly between points
839
+ 4. Use for product flythroughs and presentations
840
+
841
+ Example: orbit around model, zoom in on features, pan across assembly.
842
+ `
843
+ },
844
+ {
845
+ id: 'animation-explode',
846
+ title: 'Explode Animation',
847
+ category: 'Animation',
848
+ description: 'Auto-generate assembly explode/collapse sequences',
849
+ shortcut: 'A, E',
850
+ content: `
851
+ Automatically animate assembly disassembly:
852
+ 1. Select assembly
853
+ 2. Set explode distance
854
+ 3. Module auto-generates component animations
855
+ 4. Components move outward in sequence
856
+ 5. Optional collapse back to assembled state
857
+
858
+ Great for showing how parts fit together.
859
+ `
860
+ },
861
+ {
862
+ id: 'animation-timeline',
863
+ title: 'Timeline & Playback',
864
+ category: 'Animation',
865
+ description: 'Visual timeline with play/pause/stop controls',
866
+ shortcut: 'Space',
867
+ content: `
868
+ Control animation playback:
869
+ - Play: Start animation from current time
870
+ - Pause: Stop animation, stay at current time
871
+ - Stop: Return to beginning
872
+ - Scrubber: Drag to seek through animation
873
+ - Speed: Control playback speed
874
+
875
+ Use timeline to preview and adjust keyframes.
876
+ `
877
+ },
878
+ {
879
+ id: 'animation-easing',
880
+ title: 'Easing Functions',
881
+ category: 'Animation',
882
+ description: 'Smooth interpolation with various easing curves',
883
+ shortcut: 'A, Shift+E',
884
+ content: `
885
+ Available easing functions:
886
+ - Linear: constant speed
887
+ - Quad/Cubic/Quart/Quint: polynomial curves
888
+ - Sine: smooth wave-like motion
889
+ - Expo: accelerating/decelerating
890
+ - Circ: circular arc
891
+ - Elastic: springy bounce
892
+ - Bounce: bouncing effect
893
+
894
+ Apply per-keyframe or globally.
895
+ `
896
+ },
897
+ {
898
+ id: 'animation-export',
899
+ title: 'Video Export',
900
+ category: 'Animation',
901
+ description: 'Render animation to WebM or MP4 video',
902
+ shortcut: 'A, V',
903
+ content: `
904
+ Export animations as video:
905
+ 1. Configure export settings (format, FPS, quality)
906
+ 2. Click Export
907
+ 3. Animation renders to video file
908
+ 4. Download MP4 or WebM
909
+
910
+ Use for presentations, documentation, social media.
911
+ Quality options: low (2.5Mbps), high (5Mbps).
912
+ `
913
+ },
914
+ {
915
+ id: 'animation-save-load',
916
+ title: 'Save & Load Animations',
917
+ category: 'Animation',
918
+ description: 'Persist animations for later use',
919
+ shortcut: 'Ctrl+S / Ctrl+L',
920
+ content: `
921
+ Save and reload animations:
922
+ - Save to browser localStorage
923
+ - List all saved animations
924
+ - Load and edit existing animations
925
+ - Export to file for backup
926
+ - Share animations via JSON
927
+
928
+ Animations persist across sessions.
929
+ `
930
+ },
931
+ {
932
+ id: 'animation-storyboard',
933
+ title: 'Storyboarding',
934
+ category: 'Animation',
935
+ description: 'Chain multiple animation sequences',
936
+ shortcut: 'A, S',
937
+ content: `
938
+ Create complex animation sequences:
939
+ 1. Create multiple animations
940
+ 2. Set start/end times
941
+ 3. Storyboard chains them together
942
+ 4. Play full sequence
943
+
944
+ Example: assembly explode → rotate → close-up → collapse.
945
+ `
946
+ }
947
+ ];
948
+
949
+ export default {
950
+ init,
951
+ createAnimation,
952
+ addKeyframe,
953
+ play,
954
+ pause,
955
+ stop,
956
+ setDuration,
957
+ addCameraPath,
958
+ autoGenerateExplode,
959
+ exportVideo,
960
+ saveAnimation,
961
+ loadAnimation,
962
+ listAnimations,
963
+ getCurrentTime,
964
+ setCurrentTime,
965
+ isPlaying,
966
+ helpEntries
967
+ };