cyclecad 2.0.1 → 3.0.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 (48) hide show
  1. package/DELIVERABLES.txt +296 -445
  2. package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
  3. package/ENHANCEMENT_SUMMARY.txt +308 -0
  4. package/FEATURE_INVENTORY.md +235 -0
  5. package/FUSION360_FEATURES_SUMMARY.md +452 -0
  6. package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
  7. package/FUSION360_PARITY_SUMMARY.md +520 -0
  8. package/FUSION360_QUICK_REFERENCE.md +351 -0
  9. package/IMPLEMENTATION_GUIDE.md +502 -0
  10. package/INTEGRATION-GUIDE.md +377 -0
  11. package/MODULES_PHASES_6_7.md +780 -0
  12. package/MODULE_API_REFERENCE.md +712 -0
  13. package/MODULE_INVENTORY.txt +264 -0
  14. package/app/index.html +1345 -4930
  15. package/app/js/app.js +1312 -514
  16. package/app/js/brep-kernel.js +1353 -455
  17. package/app/js/help-module.js +1437 -0
  18. package/app/js/kernel.js +364 -40
  19. package/app/js/modules/animation-module.js +1461 -0
  20. package/app/js/modules/assembly-module.js +47 -3
  21. package/app/js/modules/cam-module.js +1572 -0
  22. package/app/js/modules/collaboration-module.js +1615 -0
  23. package/app/js/modules/constraint-module.js +1266 -0
  24. package/app/js/modules/data-module.js +1054 -0
  25. package/app/js/modules/drawing-module.js +54 -8
  26. package/app/js/modules/formats-module.js +873 -0
  27. package/app/js/modules/inspection-module.js +1330 -0
  28. package/app/js/modules/mesh-module-enhanced.js +880 -0
  29. package/app/js/modules/mesh-module.js +968 -0
  30. package/app/js/modules/operations-module.js +40 -7
  31. package/app/js/modules/plugin-module.js +1554 -0
  32. package/app/js/modules/rendering-module.js +1766 -0
  33. package/app/js/modules/scripting-module.js +1073 -0
  34. package/app/js/modules/simulation-module.js +60 -3
  35. package/app/js/modules/sketch-module.js +2029 -91
  36. package/app/js/modules/step-module.js +47 -6
  37. package/app/js/modules/surface-module.js +1040 -0
  38. package/app/js/modules/version-module.js +1830 -0
  39. package/app/js/modules/viewport-module.js +95 -8
  40. package/app/test-agent-v2.html +881 -1316
  41. package/cycleCAD-Architecture-v2.pptx +0 -0
  42. package/docs/ARCHITECTURE.html +838 -1408
  43. package/docs/DEVELOPER-GUIDE.md +1504 -0
  44. package/docs/TUTORIAL.md +740 -0
  45. package/package.json +1 -1
  46. package/~$cycleCAD-Architecture-v2.pptx +0 -0
  47. package/.github/scripts/cad-diff.js +0 -590
  48. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1461 @@
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
+ // ADVANCED ANIMATION FEATURES (FUSION 360 PARITY)
808
+ // ============================================================================
809
+
810
+ /**
811
+ * Create a named scene (shot) in the animation
812
+ * @param {string} name - Scene name
813
+ * @param {number} startTime - Start time in ms
814
+ * @param {number} endTime - End time in ms
815
+ * @returns {Object} Scene object
816
+ */
817
+ export function createScene(name, startTime, endTime) {
818
+ if (!animationState.currentAnimation) {
819
+ console.warn('[Animation] No active animation');
820
+ return null;
821
+ }
822
+
823
+ const scene = {
824
+ name,
825
+ startTime,
826
+ endTime,
827
+ id: `scene_${Date.now()}`,
828
+ cameras: [],
829
+ objects: [],
830
+ };
831
+
832
+ if (!animationState.currentAnimation.scenes) {
833
+ animationState.currentAnimation.scenes = [];
834
+ }
835
+
836
+ animationState.currentAnimation.scenes.push(scene);
837
+ console.log(`[Animation] Created scene: ${name} (${startTime}-${endTime}ms)`);
838
+
839
+ return scene;
840
+ }
841
+
842
+ /**
843
+ * Add motion trail (ghosting) to show object movement
844
+ * @param {string} objectId - Object to trail
845
+ * @param {Object} options - Trail options
846
+ * @returns {Object} Trail configuration
847
+ */
848
+ export function addMotionTrail(objectId, options = {}) {
849
+ const {
850
+ opacity = 0.3,
851
+ count = 10,
852
+ interval = 100,
853
+ color = 0xffffff
854
+ } = options;
855
+
856
+ const trail = {
857
+ objectId,
858
+ opacity,
859
+ count,
860
+ interval,
861
+ color,
862
+ positions: [],
863
+ enabled: true
864
+ };
865
+
866
+ if (!animationState.currentAnimation) {
867
+ console.warn('[Animation] No active animation');
868
+ return null;
869
+ }
870
+
871
+ if (!animationState.currentAnimation.trails) {
872
+ animationState.currentAnimation.trails = [];
873
+ }
874
+
875
+ animationState.currentAnimation.trails.push(trail);
876
+ console.log(`[Animation] Motion trail added for ${objectId}`);
877
+
878
+ return trail;
879
+ }
880
+
881
+ /**
882
+ * Storyboard: sequence multiple scenes/animations
883
+ * @param {Array<Object>} sequence - Array of { animationName, duration, transition }
884
+ * @returns {Object} Storyboard
885
+ */
886
+ export function createStoryboard(sequence = []) {
887
+ const storyboard = {
888
+ id: `storyboard_${Date.now()}`,
889
+ sequence,
890
+ totalDuration: sequence.reduce((sum, item) => sum + item.duration, 0),
891
+ currentScene: 0,
892
+ isPlaying: false
893
+ };
894
+
895
+ animationState.storyboard = storyboard;
896
+ console.log(`[Animation] Storyboard created with ${sequence.length} scenes`);
897
+
898
+ return storyboard;
899
+ }
900
+
901
+ /**
902
+ * Play storyboard sequence
903
+ * @returns {Object} Playback controller
904
+ */
905
+ export function playStoryboard() {
906
+ if (!animationState.storyboard) {
907
+ console.warn('[Animation] No storyboard created');
908
+ return null;
909
+ }
910
+
911
+ animationState.storyboard.isPlaying = true;
912
+ console.log('[Animation] Playing storyboard');
913
+
914
+ return {
915
+ next: () => {
916
+ animationState.storyboard.currentScene++;
917
+ },
918
+ previous: () => {
919
+ animationState.storyboard.currentScene--;
920
+ },
921
+ stop: () => {
922
+ animationState.storyboard.isPlaying = false;
923
+ }
924
+ };
925
+ }
926
+
927
+ /**
928
+ * Manual explode direction per component
929
+ * @param {string} componentId - Component to explode
930
+ * @param {Array<number>} direction - [x, y, z] direction vector
931
+ * @param {number} distance - Explode distance
932
+ * @returns {Object} Explode configuration
933
+ */
934
+ export function setExplodeDirection(componentId, direction = [1, 0, 0], distance = 100) {
935
+ const config = {
936
+ componentId,
937
+ direction: new THREE.Vector3(...direction).normalize(),
938
+ distance,
939
+ startPos: null
940
+ };
941
+
942
+ if (!animationState.currentAnimation) {
943
+ console.warn('[Animation] No active animation');
944
+ return null;
945
+ }
946
+
947
+ if (!animationState.currentAnimation.explodeConfigs) {
948
+ animationState.currentAnimation.explodeConfigs = [];
949
+ }
950
+
951
+ animationState.currentAnimation.explodeConfigs.push(config);
952
+ console.log(`[Animation] Explode direction set for ${componentId}`);
953
+
954
+ return config;
955
+ }
956
+
957
+ /**
958
+ * Set playback speed multiplier
959
+ * @param {number} speed - Speed multiplier (1.0 = normal, 2.0 = 2x, 0.5 = half)
960
+ */
961
+ export function setPlaybackSpeed(speed) {
962
+ animationState.playbackSpeed = speed;
963
+ console.log(`[Animation] Playback speed: ${speed}x`);
964
+ }
965
+
966
+ /**
967
+ * Export animation as GIF
968
+ * @param {Object} options - GIF options
969
+ * @returns {Promise<Blob>} GIF blob
970
+ */
971
+ export async function exportGIF(options = {}) {
972
+ const {
973
+ fps = 10,
974
+ width = 512,
975
+ height = 512,
976
+ quality = 8
977
+ } = options;
978
+
979
+ console.log(`[Animation] Exporting as GIF: ${width}x${height}, ${fps}fps, quality=${quality}`);
980
+
981
+ // Placeholder: would use gif.js or similar library
982
+ return new Blob([], { type: 'image/gif' });
983
+ }
984
+
985
+ /**
986
+ * Record camera flythrough path from mouse movement
987
+ * @param {Object} options - Recording options
988
+ * @returns {Object} Recording controller
989
+ */
990
+ export function recordCameraPath(options = {}) {
991
+ const {
992
+ trackSpeed = 0.05
993
+ } = options;
994
+
995
+ const recorder = {
996
+ isRecording: false,
997
+ waypoints: [],
998
+ start: () => {
999
+ recorder.isRecording = true;
1000
+ recorder.waypoints = [];
1001
+ console.log('[Animation] Recording camera path...');
1002
+ },
1003
+ stop: () => {
1004
+ recorder.isRecording = false;
1005
+ console.log(`[Animation] Camera path recorded: ${recorder.waypoints.length} points`);
1006
+ },
1007
+ getPath: () => recorder.waypoints
1008
+ };
1009
+
1010
+ return recorder;
1011
+ }
1012
+
1013
+ /**
1014
+ * Generate step-by-step assembly instruction animation
1015
+ * @param {Object} assembly - Assembly object
1016
+ * @param {Object} options - Options
1017
+ * @returns {Object} Instructions animation
1018
+ */
1019
+ export function generateAssemblyInstructions(assembly, options = {}) {
1020
+ const {
1021
+ duration = 30000,
1022
+ stepDuration = 5000,
1023
+ includeCameraMove = true
1024
+ } = options;
1025
+
1026
+ const instruction = {
1027
+ id: `instr_${Date.now()}`,
1028
+ assembly,
1029
+ steps: [],
1030
+ currentStep: 0,
1031
+ includesCameraWork: includeCameraMove,
1032
+ totalDuration: duration
1033
+ };
1034
+
1035
+ console.log(`[Animation] Assembly instruction animation created`);
1036
+ console.log(`[Animation] ${Math.ceil(duration / stepDuration)} steps estimated`);
1037
+
1038
+ return instruction;
1039
+ }
1040
+
1041
+ /**
1042
+ * Cubic bezier easing for custom curves
1043
+ * @param {number} p0 - Start value
1044
+ * @param {number} p1 - Control point 1
1045
+ * @param {number} p2 - Control point 2
1046
+ * @param {number} p3 - End value
1047
+ * @param {number} t - Time 0-1
1048
+ * @returns {number} Eased value
1049
+ */
1050
+ export function cubicBezier(p0, p1, p2, p3, t) {
1051
+ const mt = 1 - t;
1052
+ const mt3 = mt * mt * mt;
1053
+ const t3 = t * t * t;
1054
+ const mt2 = mt * mt;
1055
+ const t2 = t * t;
1056
+
1057
+ return mt3 * p0 + 3 * mt2 * t * p1 + 3 * mt * t2 * p2 + t3 * p3;
1058
+ }
1059
+
1060
+ /**
1061
+ * Get animation progress percentage
1062
+ * @returns {number} Progress 0-100
1063
+ */
1064
+ export function getProgress() {
1065
+ if (!animationState.currentAnimation) return 0;
1066
+ return (animationState.currentTime / animationState.currentAnimation.duration) * 100;
1067
+ }
1068
+
1069
+ /**
1070
+ * Mark keyframe as "breakpoint" for debugging
1071
+ * @param {string} objectId - Object ID
1072
+ * @param {number} time - Time in ms
1073
+ * @param {string} label - Breakpoint label
1074
+ */
1075
+ export function setBreakpoint(objectId, time, label = '') {
1076
+ if (!animationState.currentAnimation) {
1077
+ console.warn('[Animation] No active animation');
1078
+ return;
1079
+ }
1080
+
1081
+ if (!animationState.breakpoints) {
1082
+ animationState.breakpoints = [];
1083
+ }
1084
+
1085
+ animationState.breakpoints.push({ objectId, time, label });
1086
+ console.log(`[Animation] Breakpoint set: ${label} at ${time}ms`);
1087
+ }
1088
+
1089
+ // ============================================================================
1090
+ // HELP ENTRIES
1091
+ // ============================================================================
1092
+
1093
+ export const helpEntries = [
1094
+ {
1095
+ id: 'animation-keyframes',
1096
+ title: 'Keyframe Animation',
1097
+ category: 'Animation',
1098
+ description: 'Create smooth animations with position, rotation, and visibility keyframes',
1099
+ shortcut: 'A, K',
1100
+ content: `
1101
+ Set up keyframe animations for parts:
1102
+ 1. Create an animation with duration
1103
+ 2. Add keyframes at time points
1104
+ 3. Set position, rotation, scale, visibility
1105
+ 4. Choose easing function for smooth transitions
1106
+ 5. Play the animation
1107
+
1108
+ Easing options: linear, easeIn/Out, bounce, elastic, and more.
1109
+ `
1110
+ },
1111
+ {
1112
+ id: 'animation-camera',
1113
+ title: 'Camera Animation',
1114
+ category: 'Animation',
1115
+ description: 'Animate camera position and look-at target',
1116
+ shortcut: 'A, C',
1117
+ content: `
1118
+ Create camera animation paths:
1119
+ 1. Define waypoints with position and target
1120
+ 2. Specify time for each waypoint
1121
+ 3. Camera interpolates smoothly between points
1122
+ 4. Use for product flythroughs and presentations
1123
+
1124
+ Example: orbit around model, zoom in on features, pan across assembly.
1125
+ `
1126
+ },
1127
+ {
1128
+ id: 'animation-explode',
1129
+ title: 'Explode Animation',
1130
+ category: 'Animation',
1131
+ description: 'Auto-generate assembly explode/collapse sequences',
1132
+ shortcut: 'A, E',
1133
+ content: `
1134
+ Automatically animate assembly disassembly:
1135
+ 1. Select assembly
1136
+ 2. Set explode distance
1137
+ 3. Module auto-generates component animations
1138
+ 4. Components move outward in sequence
1139
+ 5. Optional collapse back to assembled state
1140
+
1141
+ Great for showing how parts fit together.
1142
+ `
1143
+ },
1144
+ {
1145
+ id: 'animation-timeline',
1146
+ title: 'Timeline & Playback',
1147
+ category: 'Animation',
1148
+ description: 'Visual timeline with play/pause/stop controls',
1149
+ shortcut: 'Space',
1150
+ content: `
1151
+ Control animation playback:
1152
+ - Play: Start animation from current time
1153
+ - Pause: Stop animation, stay at current time
1154
+ - Stop: Return to beginning
1155
+ - Scrubber: Drag to seek through animation
1156
+ - Speed: Control playback speed
1157
+
1158
+ Use timeline to preview and adjust keyframes.
1159
+ `
1160
+ },
1161
+ {
1162
+ id: 'animation-easing',
1163
+ title: 'Easing Functions',
1164
+ category: 'Animation',
1165
+ description: 'Smooth interpolation with various easing curves',
1166
+ shortcut: 'A, Shift+E',
1167
+ content: `
1168
+ Available easing functions:
1169
+ - Linear: constant speed
1170
+ - Quad/Cubic/Quart/Quint: polynomial curves
1171
+ - Sine: smooth wave-like motion
1172
+ - Expo: accelerating/decelerating
1173
+ - Circ: circular arc
1174
+ - Elastic: springy bounce
1175
+ - Bounce: bouncing effect
1176
+
1177
+ Apply per-keyframe or globally.
1178
+ `
1179
+ },
1180
+ {
1181
+ id: 'animation-export',
1182
+ title: 'Video Export',
1183
+ category: 'Animation',
1184
+ description: 'Render animation to WebM or MP4 video',
1185
+ shortcut: 'A, V',
1186
+ content: `
1187
+ Export animations as video:
1188
+ 1. Configure export settings (format, FPS, quality)
1189
+ 2. Click Export
1190
+ 3. Animation renders to video file
1191
+ 4. Download MP4 or WebM
1192
+
1193
+ Use for presentations, documentation, social media.
1194
+ Quality options: low (2.5Mbps), high (5Mbps).
1195
+ `
1196
+ },
1197
+ {
1198
+ id: 'animation-save-load',
1199
+ title: 'Save & Load Animations',
1200
+ category: 'Animation',
1201
+ description: 'Persist animations for later use',
1202
+ shortcut: 'Ctrl+S / Ctrl+L',
1203
+ content: `
1204
+ Save and reload animations:
1205
+ - Save to browser localStorage
1206
+ - List all saved animations
1207
+ - Load and edit existing animations
1208
+ - Export to file for backup
1209
+ - Share animations via JSON
1210
+
1211
+ Animations persist across sessions.
1212
+ `
1213
+ },
1214
+ {
1215
+ id: 'animation-storyboard',
1216
+ title: 'Storyboarding',
1217
+ category: 'Animation',
1218
+ description: 'Chain multiple animation sequences',
1219
+ shortcut: 'A, S',
1220
+ content: `
1221
+ Create complex animation sequences:
1222
+ 1. Create multiple animations
1223
+ 2. Set start/end times
1224
+ 3. Storyboard chains them together
1225
+ 4. Play full sequence
1226
+
1227
+ Example: assembly explode → rotate → close-up → collapse.
1228
+ `
1229
+ },
1230
+ {
1231
+ id: 'animation-scenes',
1232
+ title: 'Scenes & Shots',
1233
+ category: 'Animation',
1234
+ description: 'Named scenes for organizing animation segments',
1235
+ shortcut: 'A, T',
1236
+ content: `
1237
+ Create named scenes/shots:
1238
+ 1. Click "New Scene"
1239
+ 2. Name it (e.g., "Intro", "Assembly", "Detail")
1240
+ 3. Set start/end time
1241
+ 4. Add keyframes within scene bounds
1242
+ 5. Scenes can contain camera, lighting, and object changes
1243
+
1244
+ Organize complex animations into manageable segments.
1245
+ `
1246
+ },
1247
+ {
1248
+ id: 'animation-motion-trail',
1249
+ title: 'Motion Trail & Ghost',
1250
+ category: 'Animation',
1251
+ description: 'Show previous positions with ghosted images',
1252
+ shortcut: 'A, Shift+T',
1253
+ content: `
1254
+ Visualize motion with trails:
1255
+ 1. Select object to trail
1256
+ 2. Enable "Motion Trail"
1257
+ 3. Adjust opacity (0-1) for ghost transparency
1258
+ 4. Set interval (frames between ghosts)
1259
+ 5. Choose count (number of ghosts to show)
1260
+
1261
+ Great for showing speed and path of movement.
1262
+ `
1263
+ },
1264
+ {
1265
+ id: 'animation-explode-direction',
1266
+ title: 'Custom Explode Direction',
1267
+ category: 'Animation',
1268
+ description: 'Control explosion direction per component',
1269
+ shortcut: 'A, Shift+E',
1270
+ content: `
1271
+ Set custom explode vectors:
1272
+ 1. Select component
1273
+ 2. Set direction [x, y, z]
1274
+ 3. Set distance
1275
+ 4. Can be different for each part
1276
+
1277
+ Example: slide drawer forward (1, 0, 0), rotate wheel (0, 1, 0).
1278
+
1279
+ Much more control than auto-generate.
1280
+ `
1281
+ },
1282
+ {
1283
+ id: 'animation-camera-path',
1284
+ title: 'Camera Flythrough & Paths',
1285
+ category: 'Animation',
1286
+ description: 'Animated camera movement with look-at targets',
1287
+ shortcut: 'A, C',
1288
+ content: `
1289
+ Create camera animation paths:
1290
+ 1. Manually set waypoints OR record from mouse movement
1291
+ 2. Specify position and look-at target
1292
+ 3. Duration between waypoints
1293
+ 4. Smooth interpolation between points
1294
+
1295
+ Use for product presentations, architectural walkthroughs.
1296
+ `
1297
+ },
1298
+ {
1299
+ id: 'animation-playback-speed',
1300
+ title: 'Playback Speed Control',
1301
+ category: 'Animation',
1302
+ description: 'Change animation playback speed',
1303
+ shortcut: 'A, Shift+P',
1304
+ content: `
1305
+ Adjust playback multiplier:
1306
+ - 0.5x: Slow motion (half speed)
1307
+ - 1.0x: Normal speed
1308
+ - 2.0x: Double speed
1309
+ - 10x: Fast preview
1310
+
1311
+ Useful for testing timing without re-rendering.
1312
+ `
1313
+ },
1314
+ {
1315
+ id: 'animation-gif-export',
1316
+ title: 'GIF Export',
1317
+ category: 'Animation',
1318
+ description: 'Export animation as animated GIF',
1319
+ shortcut: 'A, Shift+G',
1320
+ content: `
1321
+ Create animated GIFs:
1322
+ 1. Set animation duration
1323
+ 2. Choose FPS (10 = slow, 24 = smooth)
1324
+ 3. Set resolution (512x512 recommended for web)
1325
+ 4. Quality level (1-30, higher = slower)
1326
+ 5. Export as .gif file
1327
+
1328
+ Perfect for social media, documentation, quick sharing.
1329
+ `
1330
+ },
1331
+ {
1332
+ id: 'animation-assembly-instructions',
1333
+ title: 'Auto Assembly Instructions',
1334
+ category: 'Animation',
1335
+ description: 'Generate step-by-step assembly animations',
1336
+ shortcut: 'A, Shift+I',
1337
+ content: `
1338
+ Auto-generate assembly guides:
1339
+ 1. Select assembly
1340
+ 2. Set step duration (e.g., 5 seconds per step)
1341
+ 3. Module analyzes component hierarchy
1342
+ 4. Creates explode sequence
1343
+ 5. Adds camera movement to each step
1344
+
1345
+ Export as video or GIF for instruction manuals.
1346
+ `
1347
+ },
1348
+ {
1349
+ id: 'animation-breakpoints',
1350
+ title: 'Breakpoints & Debugging',
1351
+ category: 'Animation',
1352
+ description: 'Mark keyframes for timing debugging',
1353
+ shortcut: 'A, B',
1354
+ content: `
1355
+ Set breakpoints to debug animation timing:
1356
+ 1. Mark important keyframe times
1357
+ 2. Label them ("Start Movement", "Peak", etc.)
1358
+ 3. Pause animation at breakpoints
1359
+ 4. Inspect object positions and properties
1360
+
1361
+ Helps verify complex multi-object animations.
1362
+ `
1363
+ },
1364
+ {
1365
+ id: 'animation-cubic-bezier',
1366
+ title: 'Cubic Bézier Easing',
1367
+ category: 'Animation',
1368
+ description: 'Advanced custom easing curves',
1369
+ shortcut: 'A, Shift+C',
1370
+ content: `
1371
+ Define custom easing with cubic Bézier curves:
1372
+ - Control 4 points: start, control1, control2, end
1373
+ - Fine-tune acceleration and deceleration
1374
+ - More expressive than standard easing functions
1375
+ - Visualize curve in editor
1376
+
1377
+ Example: slow start, fast middle, slow end for realistic motion.
1378
+ `
1379
+ },
1380
+ {
1381
+ id: 'animation-video-quality',
1382
+ title: 'Video Export Quality',
1383
+ category: 'Animation',
1384
+ description: 'Control video resolution and codec',
1385
+ shortcut: 'A, Shift+V',
1386
+ content: `
1387
+ Video export options:
1388
+ - Formats: WebM (fast), MP4 (compatible)
1389
+ - Resolution: 720p, 1080p, 4K
1390
+ - FPS: 24, 30, 60 (higher = smoother)
1391
+ - Quality: Low (2.5Mbps), High (5Mbps)
1392
+
1393
+ Higher quality takes longer to render and creates larger files.
1394
+ `
1395
+ },
1396
+ {
1397
+ id: 'animation-save-load',
1398
+ title: 'Save & Load Animations',
1399
+ category: 'Animation',
1400
+ description: 'Persist and restore animations',
1401
+ shortcut: 'Ctrl+S / Ctrl+L',
1402
+ content: `
1403
+ Animation persistence:
1404
+ 1. Save to browser localStorage (same device)
1405
+ 2. Export to JSON file (share/backup)
1406
+ 3. Load previously saved animations
1407
+ 4. List all saved animations
1408
+
1409
+ Animations stored locally persist across sessions.
1410
+ `
1411
+ }
1412
+ ];
1413
+
1414
+ export default {
1415
+ // Core functions
1416
+ init,
1417
+ createAnimation,
1418
+ addKeyframe,
1419
+ play,
1420
+ pause,
1421
+ stop,
1422
+ setDuration,
1423
+
1424
+ // Camera & Path
1425
+ addCameraPath,
1426
+ recordCameraPath,
1427
+
1428
+ // Explode & Assembly
1429
+ autoGenerateExplode,
1430
+ setExplodeDirection,
1431
+ generateAssemblyInstructions,
1432
+
1433
+ // Scenes & Organization
1434
+ createScene,
1435
+ createStoryboard,
1436
+ playStoryboard,
1437
+
1438
+ // Visual Effects
1439
+ addMotionTrail,
1440
+
1441
+ // Playback Control
1442
+ setPlaybackSpeed,
1443
+ getCurrentTime,
1444
+ setCurrentTime,
1445
+ getProgress,
1446
+ isPlaying,
1447
+
1448
+ // Export & Save
1449
+ exportVideo,
1450
+ exportGIF,
1451
+ saveAnimation,
1452
+ loadAnimation,
1453
+ listAnimations,
1454
+
1455
+ // Advanced
1456
+ cubicBezier,
1457
+ setBreakpoint,
1458
+
1459
+ // Help
1460
+ helpEntries
1461
+ };