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.
- package/DELIVERABLES.txt +296 -445
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/app/index.html +1345 -4930
- package/app/js/app.js +1312 -514
- package/app/js/brep-kernel.js +1353 -455
- package/app/js/help-module.js +1437 -0
- package/app/js/kernel.js +364 -40
- package/app/js/modules/animation-module.js +1461 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1572 -0
- package/app/js/modules/collaboration-module.js +1615 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +1054 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +873 -0
- package/app/js/modules/inspection-module.js +1330 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/mesh-module.js +968 -0
- package/app/js/modules/operations-module.js +40 -7
- package/app/js/modules/plugin-module.js +1554 -0
- package/app/js/modules/rendering-module.js +1766 -0
- package/app/js/modules/scripting-module.js +1073 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +2029 -91
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +1040 -0
- package/app/js/modules/version-module.js +1830 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docs/ARCHITECTURE.html +838 -1408
- package/docs/DEVELOPER-GUIDE.md +1504 -0
- package/docs/TUTORIAL.md +740 -0
- package/package.json +1 -1
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
- package/.github/scripts/cad-diff.js +0 -590
- 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
|
+
};
|