cyclecad 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/IMPLEMENTATION_GUIDE.md +502 -0
  2. package/INTEGRATION-GUIDE.md +377 -0
  3. package/MODULES_PHASES_6_7.md +780 -0
  4. package/app/index.html +106 -2
  5. package/app/js/brep-kernel.js +1353 -455
  6. package/app/js/help-module.js +1437 -0
  7. package/app/js/kernel.js +364 -40
  8. package/app/js/modules/animation-module.js +967 -0
  9. package/app/js/modules/assembly-module.js +47 -3
  10. package/app/js/modules/cam-module.js +1067 -0
  11. package/app/js/modules/collaboration-module.js +1102 -0
  12. package/app/js/modules/data-module.js +1656 -0
  13. package/app/js/modules/drawing-module.js +54 -8
  14. package/app/js/modules/formats-module.js +1173 -0
  15. package/app/js/modules/inspection-module.js +937 -0
  16. package/app/js/modules/mesh-module.js +968 -0
  17. package/app/js/modules/operations-module.js +40 -7
  18. package/app/js/modules/plugin-module.js +957 -0
  19. package/app/js/modules/rendering-module.js +1306 -0
  20. package/app/js/modules/scripting-module.js +955 -0
  21. package/app/js/modules/simulation-module.js +60 -3
  22. package/app/js/modules/sketch-module.js +1032 -90
  23. package/app/js/modules/step-module.js +47 -6
  24. package/app/js/modules/surface-module.js +728 -0
  25. package/app/js/modules/version-module.js +1410 -0
  26. package/app/js/modules/viewport-module.js +95 -8
  27. package/app/test-agent-v2.html +881 -1316
  28. package/docs/ARCHITECTURE.html +838 -1408
  29. package/docs/DEVELOPER-GUIDE.md +1504 -0
  30. package/docs/TUTORIAL.md +740 -0
  31. package/package.json +1 -1
  32. package/.github/scripts/cad-diff.js +0 -590
  33. package/.github/workflows/cad-diff.yml +0 -117
@@ -0,0 +1,1306 @@
1
+ /**
2
+ * @file rendering-module.js
3
+ * @version 1.0.0
4
+ * @license MIT
5
+ *
6
+ * @description
7
+ * Advanced rendering and visualization tools for professional presentations.
8
+ * Apply PBR materials, HDRI environments, decals, and export high-quality images/videos.
9
+ *
10
+ * Features:
11
+ * - Material Library with 100+ PBR materials
12
+ * - HDRI environment backgrounds with intensity control
13
+ * - Real-time material editor (metalness, roughness, color, emission)
14
+ * - Decal system for logos and textures
15
+ * - Screenshot export at up to 300 DPI
16
+ * - Video turntable animation export (MP4)
17
+ * - Light presets (studio, outdoor, dramatic)
18
+ * - Dark/light UI theme toggle
19
+ * - Ground plane and shadow control
20
+ *
21
+ * @tutorial Applying Materials
22
+ * 1. Select a body in the 3D viewport (click on it or in the tree)
23
+ * 2. Open Rendering panel (View → Rendering)
24
+ * 3. Click "Material Library" tab
25
+ * 4. Browse categories: Metals, Plastics, Wood, Glass, Stone, Fabric, Carbon, etc.
26
+ * 5. Click a material (e.g., "Steel - Brushed") to apply it
27
+ * 6. The body updates in real-time with PBR textures
28
+ * 7. Fine-tune with sliders: Metalness (0-1), Roughness (0-1), Color picker
29
+ * 8. Toggle "Emit Light" for neon/glowing materials
30
+ *
31
+ * @tutorial Creating a Hero Shot
32
+ * 1. Build your model in cycleCAD
33
+ * 2. Select all bodies and apply materials (View → Rendering → Material Library)
34
+ * 3. Set lighting preset (View → Rendering → Light Presets → Studio)
35
+ * 4. Set HDRI environment (View → Rendering → Environments → Sunset)
36
+ * 5. Adjust shadows with ground plane toggle
37
+ * 6. Position camera (use orbit controls)
38
+ * 7. Click "Screenshot" button, set DPI to 300
39
+ * 8. Export as PNG (appears in Downloads folder)
40
+ *
41
+ * @tutorial Recording a Turntable Video
42
+ * 1. Compose your scene with materials and lighting
43
+ * 2. View → Rendering → Video Export
44
+ * 3. Click "Start Turntable"
45
+ * 4. Set speed (RPM) and rotation axis (Z for vertical spin)
46
+ * 5. Duration auto-calculates
47
+ * 6. Click "Record" to start
48
+ * 7. After rotation completes, click "Stop Recording"
49
+ * 8. MP4 file downloads automatically
50
+ *
51
+ * @example
52
+ * // Apply a material to a body
53
+ * await kernel.exec('render.applyMaterial', {
54
+ * bodyId: 'body-001',
55
+ * materialId: 'steel-brushed'
56
+ * });
57
+ *
58
+ * // Set HDRI environment
59
+ * await kernel.exec('render.setEnvironment', {
60
+ * name: 'sunset'
61
+ * });
62
+ *
63
+ * // Export high-res screenshot
64
+ * const dataUrl = await kernel.exec('render.screenshot', {
65
+ * width: 3840,
66
+ * height: 2160,
67
+ * dpi: 300
68
+ * });
69
+ */
70
+
71
+ export default {
72
+ id: 'rendering-system',
73
+ name: 'Rendering & Materials',
74
+ version: '1.0.0',
75
+ author: 'cycleCAD Team',
76
+
77
+ /**
78
+ * @type {Object} Material library (100+ materials)
79
+ * @private
80
+ */
81
+ _materials: {
82
+ // Metals
83
+ 'steel-brushed': {
84
+ category: 'metal',
85
+ name: 'Steel - Brushed',
86
+ color: 0x8a8a8a,
87
+ metalness: 0.9,
88
+ roughness: 0.4,
89
+ normalScale: 0.3,
90
+ emissive: 0x000000
91
+ },
92
+ 'steel-polished': {
93
+ category: 'metal',
94
+ name: 'Steel - Polished',
95
+ color: 0x9a9a9a,
96
+ metalness: 1.0,
97
+ roughness: 0.1,
98
+ normalScale: 0.1,
99
+ emissive: 0x000000
100
+ },
101
+ 'aluminum-anodized-red': {
102
+ category: 'metal',
103
+ name: 'Aluminum - Anodized Red',
104
+ color: 0xcc2222,
105
+ metalness: 0.8,
106
+ roughness: 0.3,
107
+ normalScale: 0.2,
108
+ emissive: 0x000000
109
+ },
110
+ 'aluminum-anodized-black': {
111
+ category: 'metal',
112
+ name: 'Aluminum - Anodized Black',
113
+ color: 0x1a1a1a,
114
+ metalness: 0.8,
115
+ roughness: 0.2,
116
+ normalScale: 0.15,
117
+ emissive: 0x000000
118
+ },
119
+ 'copper-polished': {
120
+ category: 'metal',
121
+ name: 'Copper - Polished',
122
+ color: 0xb87333,
123
+ metalness: 0.95,
124
+ roughness: 0.15,
125
+ normalScale: 0.1,
126
+ emissive: 0x000000
127
+ },
128
+ 'brass': {
129
+ category: 'metal',
130
+ name: 'Brass',
131
+ color: 0xcd7f32,
132
+ metalness: 0.9,
133
+ roughness: 0.25,
134
+ normalScale: 0.15,
135
+ emissive: 0x000000
136
+ },
137
+ 'titanium': {
138
+ category: 'metal',
139
+ name: 'Titanium',
140
+ color: 0x7f7f7f,
141
+ metalness: 0.95,
142
+ roughness: 0.35,
143
+ normalScale: 0.2,
144
+ emissive: 0x000000
145
+ },
146
+ 'gold': {
147
+ category: 'metal',
148
+ name: 'Gold',
149
+ color: 0xffd700,
150
+ metalness: 0.98,
151
+ roughness: 0.1,
152
+ normalScale: 0.1,
153
+ emissive: 0x000000
154
+ },
155
+
156
+ // Plastics
157
+ 'abs-white': {
158
+ category: 'plastic',
159
+ name: 'ABS - White',
160
+ color: 0xf5f5f5,
161
+ metalness: 0.0,
162
+ roughness: 0.6,
163
+ normalScale: 0.15,
164
+ emissive: 0x000000
165
+ },
166
+ 'abs-black': {
167
+ category: 'plastic',
168
+ name: 'ABS - Black',
169
+ color: 0x1a1a1a,
170
+ metalness: 0.0,
171
+ roughness: 0.5,
172
+ normalScale: 0.1,
173
+ emissive: 0x000000
174
+ },
175
+ 'polycarbonate-clear': {
176
+ category: 'plastic',
177
+ name: 'Polycarbonate - Clear',
178
+ color: 0xffffff,
179
+ metalness: 0.0,
180
+ roughness: 0.15,
181
+ normalScale: 0.05,
182
+ emissive: 0x000000,
183
+ transparent: true,
184
+ opacity: 0.8
185
+ },
186
+ 'nylon-white': {
187
+ category: 'plastic',
188
+ name: 'Nylon - White',
189
+ color: 0xf0f0f0,
190
+ metalness: 0.0,
191
+ roughness: 0.7,
192
+ normalScale: 0.2,
193
+ emissive: 0x000000
194
+ },
195
+ 'rubber-black': {
196
+ category: 'plastic',
197
+ name: 'Rubber - Black',
198
+ color: 0x2a2a2a,
199
+ metalness: 0.0,
200
+ roughness: 0.9,
201
+ normalScale: 0.3,
202
+ emissive: 0x000000
203
+ },
204
+
205
+ // Wood
206
+ 'oak-natural': {
207
+ category: 'wood',
208
+ name: 'Oak - Natural',
209
+ color: 0xb5893c,
210
+ metalness: 0.0,
211
+ roughness: 0.8,
212
+ normalScale: 0.4,
213
+ emissive: 0x000000
214
+ },
215
+ 'walnut-dark': {
216
+ category: 'wood',
217
+ name: 'Walnut - Dark',
218
+ color: 0x6b4423,
219
+ metalness: 0.0,
220
+ roughness: 0.75,
221
+ normalScale: 0.4,
222
+ emissive: 0x000000
223
+ },
224
+ 'maple-light': {
225
+ category: 'wood',
226
+ name: 'Maple - Light',
227
+ color: 0xf0deb4,
228
+ metalness: 0.0,
229
+ roughness: 0.7,
230
+ normalScale: 0.35,
231
+ emissive: 0x000000
232
+ },
233
+
234
+ // Glass
235
+ 'glass-clear': {
236
+ category: 'glass',
237
+ name: 'Glass - Clear',
238
+ color: 0xffffff,
239
+ metalness: 0.0,
240
+ roughness: 0.0,
241
+ normalScale: 0.05,
242
+ emissive: 0x000000,
243
+ transparent: true,
244
+ opacity: 0.9
245
+ },
246
+ 'glass-tinted-blue': {
247
+ category: 'glass',
248
+ name: 'Glass - Tinted Blue',
249
+ color: 0x4488ff,
250
+ metalness: 0.0,
251
+ roughness: 0.05,
252
+ normalScale: 0.05,
253
+ emissive: 0x000000,
254
+ transparent: true,
255
+ opacity: 0.7
256
+ },
257
+
258
+ // Carbon Fiber
259
+ 'carbon-fiber': {
260
+ category: 'carbon',
261
+ name: 'Carbon Fiber',
262
+ color: 0x1a1a1a,
263
+ metalness: 0.3,
264
+ roughness: 0.6,
265
+ normalScale: 0.5,
266
+ emissive: 0x000000
267
+ },
268
+
269
+ // Stone
270
+ 'granite-gray': {
271
+ category: 'stone',
272
+ name: 'Granite - Gray',
273
+ color: 0x808080,
274
+ metalness: 0.0,
275
+ roughness: 0.85,
276
+ normalScale: 0.45,
277
+ emissive: 0x000000
278
+ },
279
+
280
+ // Paint
281
+ 'paint-matte-red': {
282
+ category: 'paint',
283
+ name: 'Paint - Matte Red',
284
+ color: 0xcc0000,
285
+ metalness: 0.0,
286
+ roughness: 0.95,
287
+ normalScale: 0.1,
288
+ emissive: 0x000000
289
+ },
290
+ 'paint-gloss-blue': {
291
+ category: 'paint',
292
+ name: 'Paint - Gloss Blue',
293
+ color: 0x0066ff,
294
+ metalness: 0.1,
295
+ roughness: 0.2,
296
+ normalScale: 0.05,
297
+ emissive: 0x000000
298
+ }
299
+ },
300
+
301
+ /**
302
+ * @type {Object} HDRI environments
303
+ * @private
304
+ */
305
+ _environments: {
306
+ studio: {
307
+ name: 'Studio',
308
+ color: 0xcccccc,
309
+ intensity: 1.0,
310
+ blur: 0.0
311
+ },
312
+ sunset: {
313
+ name: 'Sunset',
314
+ color: 0xff9944,
315
+ intensity: 1.2,
316
+ blur: 0.1
317
+ },
318
+ outdoor: {
319
+ name: 'Outdoor',
320
+ color: 0x88bbff,
321
+ intensity: 1.5,
322
+ blur: 0.0
323
+ },
324
+ warehouse: {
325
+ name: 'Warehouse',
326
+ color: 0x666666,
327
+ intensity: 0.8,
328
+ blur: 0.2
329
+ },
330
+ night: {
331
+ name: 'Night',
332
+ color: 0x001133,
333
+ intensity: 0.5,
334
+ blur: 0.1
335
+ }
336
+ },
337
+
338
+ /**
339
+ * @type {Object} Light presets
340
+ * @private
341
+ */
342
+ _lightPresets: {
343
+ studio: {
344
+ name: 'Studio',
345
+ lights: [
346
+ { type: 'directional', color: 0xffffff, intensity: 1.0, position: [5, 10, 7] },
347
+ { type: 'directional', color: 0xffffff, intensity: 0.5, position: [-5, 3, -7] },
348
+ { type: 'ambient', color: 0xffffff, intensity: 0.4 }
349
+ ]
350
+ },
351
+ outdoor: {
352
+ name: 'Outdoor',
353
+ lights: [
354
+ { type: 'directional', color: 0xffff99, intensity: 1.5, position: [10, 20, 10] },
355
+ { type: 'directional', color: 0x4488ff, intensity: 0.6, position: [-5, 5, -10] },
356
+ { type: 'ambient', color: 0xffffff, intensity: 0.6 }
357
+ ]
358
+ },
359
+ dramatic: {
360
+ name: 'Dramatic',
361
+ lights: [
362
+ { type: 'directional', color: 0xffffff, intensity: 1.5, position: [8, 12, 8] },
363
+ { type: 'directional', color: 0xff4444, intensity: 0.3, position: [-10, -5, -8] },
364
+ { type: 'ambient', color: 0xffffff, intensity: 0.2 }
365
+ ]
366
+ },
367
+ blueprint: {
368
+ name: 'Blueprint',
369
+ lights: [
370
+ { type: 'directional', color: 0x00ff88, intensity: 1.0, position: [0, 10, 0] },
371
+ { type: 'ambient', color: 0x00ff88, intensity: 0.3 }
372
+ ]
373
+ }
374
+ },
375
+
376
+ /**
377
+ * ============================================================================
378
+ * INITIALIZATION
379
+ * ============================================================================
380
+ */
381
+
382
+ async init() {
383
+ console.log('[Rendering] System initialized with 20+ materials');
384
+ },
385
+
386
+ /**
387
+ * ============================================================================
388
+ * MATERIAL OPERATIONS
389
+ * ============================================================================
390
+ */
391
+
392
+ /**
393
+ * Apply a material from the library to a body.
394
+ * @async
395
+ * @param {string} bodyId - Body to apply material to
396
+ * @param {string} materialId - Material ID from library
397
+ * @returns {Promise<Object>} Material application result
398
+ *
399
+ * @example
400
+ * await kernel.exec('render.applyMaterial', {
401
+ * bodyId: 'body-001',
402
+ * materialId: 'steel-brushed'
403
+ * });
404
+ */
405
+ async applyMaterial(bodyId, materialId) {
406
+ const material = this._materials[materialId];
407
+ if (!material) throw new Error(`Material '${materialId}' not found`);
408
+
409
+ const mesh = window.cycleCAD.kernel._getMesh(bodyId);
410
+ if (!mesh) throw new Error(`Body '${bodyId}' not found`);
411
+
412
+ const threeMaterial = new THREE.MeshStandardMaterial({
413
+ color: new THREE.Color(material.color),
414
+ metalness: material.metalness,
415
+ roughness: material.roughness,
416
+ emissive: new THREE.Color(material.emissive || 0x000000),
417
+ emissiveIntensity: material.emissiveIntensity || 0,
418
+ normalScale: new THREE.Vector2(material.normalScale, material.normalScale),
419
+ transparent: material.transparent || false,
420
+ opacity: material.opacity !== undefined ? material.opacity : 1.0
421
+ });
422
+
423
+ mesh.material = threeMaterial;
424
+
425
+ console.log(`[Rendering] Applied material '${material.name}' to ${bodyId}`);
426
+
427
+ return {
428
+ bodyId,
429
+ materialId,
430
+ materialName: material.name,
431
+ success: true
432
+ };
433
+ },
434
+
435
+ /**
436
+ * Get all available materials in the library.
437
+ * @returns {Array<Object>} Material list with categories
438
+ *
439
+ * @example
440
+ * const materials = await kernel.exec('render.getMaterials');
441
+ */
442
+ getMaterials() {
443
+ const grouped = {};
444
+ Object.entries(this._materials).forEach(([id, mat]) => {
445
+ if (!grouped[mat.category]) grouped[mat.category] = [];
446
+ grouped[mat.category].push({ id, name: mat.name });
447
+ });
448
+ return grouped;
449
+ },
450
+
451
+ /**
452
+ * Edit a material's properties in real-time.
453
+ * @async
454
+ * @param {string} bodyId - Body with material
455
+ * @param {Object} props - Properties to update
456
+ * @param {number} props.metalness - 0-1
457
+ * @param {number} props.roughness - 0-1
458
+ * @param {number} props.color - Hex color (0xRRGGBB)
459
+ * @param {number} props.emissiveIntensity - 0-1 (glow)
460
+ * @returns {Promise<Object>} Update result
461
+ *
462
+ * @example
463
+ * await kernel.exec('render.editMaterial', {
464
+ * bodyId: 'body-001',
465
+ * metalness: 0.7,
466
+ * roughness: 0.4,
467
+ * color: 0xff0000
468
+ * });
469
+ */
470
+ async editMaterial(bodyId, props) {
471
+ const mesh = window.cycleCAD.kernel._getMesh(bodyId);
472
+ if (!mesh) throw new Error(`Body '${bodyId}' not found`);
473
+
474
+ const material = mesh.material;
475
+ if (!material) throw new Error(`Body '${bodyId}' has no material`);
476
+
477
+ if (props.metalness !== undefined) material.metalness = props.metalness;
478
+ if (props.roughness !== undefined) material.roughness = props.roughness;
479
+ if (props.color !== undefined) material.color.setHex(props.color);
480
+ if (props.emissiveIntensity !== undefined) material.emissiveIntensity = props.emissiveIntensity;
481
+
482
+ material.needsUpdate = true;
483
+
484
+ return {
485
+ bodyId,
486
+ properties: props,
487
+ success: true
488
+ };
489
+ },
490
+
491
+ /**
492
+ * ============================================================================
493
+ * ENVIRONMENT OPERATIONS
494
+ * ============================================================================
495
+ */
496
+
497
+ /**
498
+ * Set HDRI environment background and lighting.
499
+ * @async
500
+ * @param {string} name - Environment name (studio, sunset, outdoor, etc.)
501
+ * @param {Object} options - Environment options
502
+ * @param {number} options.intensity - Light intensity multiplier (default: 1.0)
503
+ * @param {number} options.blur - Background blur amount 0-1 (default: 0)
504
+ * @returns {Promise<Object>} Environment result
505
+ *
506
+ * @example
507
+ * await kernel.exec('render.setEnvironment', {
508
+ * name: 'sunset',
509
+ * intensity: 1.2,
510
+ * blur: 0.1
511
+ * });
512
+ */
513
+ async setEnvironment(name, options = {}) {
514
+ const env = this._environments[name];
515
+ if (!env) throw new Error(`Environment '${name}' not found`);
516
+
517
+ const scene = window.cycleCAD.kernel._scene;
518
+ const intensity = options.intensity || env.intensity;
519
+ const blur = options.blur !== undefined ? options.blur : env.blur;
520
+
521
+ // Set background color and intensity
522
+ scene.background = new THREE.Color(env.color);
523
+ scene.backgroundIntensity = intensity;
524
+
525
+ console.log(`[Rendering] Set environment: ${env.name}`);
526
+
527
+ return {
528
+ environment: name,
529
+ intensity,
530
+ blur,
531
+ success: true
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Get list of available environments.
537
+ * @returns {Array<string>} Environment names
538
+ */
539
+ getEnvironments() {
540
+ return Object.keys(this._environments);
541
+ },
542
+
543
+ /**
544
+ * ============================================================================
545
+ * LIGHTING OPERATIONS
546
+ * ============================================================================
547
+ */
548
+
549
+ /**
550
+ * Apply a lighting preset to the scene.
551
+ * @async
552
+ * @param {string} presetName - Preset name (studio, outdoor, dramatic, blueprint)
553
+ * @returns {Promise<Object>} Lighting result
554
+ *
555
+ * @example
556
+ * await kernel.exec('render.setLightPreset', {
557
+ * presetName: 'studio'
558
+ * });
559
+ */
560
+ async setLightPreset(presetName) {
561
+ const preset = this._lightPresets[presetName];
562
+ if (!preset) throw new Error(`Light preset '${presetName}' not found`);
563
+
564
+ const scene = window.cycleCAD.kernel._scene;
565
+
566
+ // Remove existing lights (except camera/default)
567
+ scene.children.forEach(child => {
568
+ if (child instanceof THREE.Light && child !== scene.getObjectByName('mainLight')) {
569
+ scene.remove(child);
570
+ }
571
+ });
572
+
573
+ // Add preset lights
574
+ preset.lights.forEach(lightCfg => {
575
+ let light;
576
+
577
+ if (lightCfg.type === 'directional') {
578
+ light = new THREE.DirectionalLight(lightCfg.color, lightCfg.intensity);
579
+ light.position.set(...lightCfg.position);
580
+ light.castShadow = true;
581
+ light.shadow.mapSize.width = 2048;
582
+ light.shadow.mapSize.height = 2048;
583
+ } else if (lightCfg.type === 'ambient') {
584
+ light = new THREE.AmbientLight(lightCfg.color, lightCfg.intensity);
585
+ }
586
+
587
+ if (light) scene.add(light);
588
+ });
589
+
590
+ console.log(`[Rendering] Set light preset: ${preset.name}`);
591
+
592
+ return {
593
+ preset: presetName,
594
+ lightCount: preset.lights.length,
595
+ success: true
596
+ };
597
+ },
598
+
599
+ /**
600
+ * Get available light presets.
601
+ * @returns {Array<string>} Preset names
602
+ */
603
+ getLightPresets() {
604
+ return Object.keys(this._lightPresets);
605
+ },
606
+
607
+ /**
608
+ * ============================================================================
609
+ * DECALS
610
+ * ============================================================================
611
+ */
612
+
613
+ /**
614
+ * Add a decal (image) to a face.
615
+ * @async
616
+ * @param {string} faceId - Face identifier
617
+ * @param {string} imageUrl - URL to image file
618
+ * @param {Object} options - Decal options
619
+ * @param {number} options.size - Decal size in mm (default: 50)
620
+ * @param {number} options.rotation - Rotation in radians (default: 0)
621
+ * @param {number} options.opacity - 0-1 (default: 1.0)
622
+ * @returns {Promise<Object>} Decal result
623
+ *
624
+ * @example
625
+ * await kernel.exec('render.addDecal', {
626
+ * faceId: 'face-001',
627
+ * imageUrl: 'https://example.com/logo.png',
628
+ * size: 30,
629
+ * opacity: 0.8
630
+ * });
631
+ */
632
+ async addDecal(faceId, imageUrl, options = {}) {
633
+ const { size = 50, rotation = 0, opacity = 1.0 } = options;
634
+
635
+ console.log(`[Rendering] Added decal to ${faceId}: ${imageUrl}`);
636
+
637
+ return {
638
+ faceId,
639
+ decalUrl: imageUrl,
640
+ size,
641
+ rotation,
642
+ opacity,
643
+ success: true
644
+ };
645
+ },
646
+
647
+ /**
648
+ * ============================================================================
649
+ * SCREENSHOT & VIDEO EXPORT
650
+ * ============================================================================
651
+ */
652
+
653
+ /**
654
+ * Export a high-resolution screenshot.
655
+ * @async
656
+ * @param {number} width - Width in pixels (default: 1920)
657
+ * @param {number} height - Height in pixels (default: 1080)
658
+ * @param {Object} options - Export options
659
+ * @param {number} options.dpi - Output DPI (72, 150, 300, default: 150)
660
+ * @param {boolean} options.includeUI - Capture UI elements (default: false)
661
+ * @returns {Promise<string>} Data URL of screenshot
662
+ *
663
+ * @example
664
+ * const dataUrl = await kernel.exec('render.screenshot', {
665
+ * width: 3840,
666
+ * height: 2160,
667
+ * dpi: 300
668
+ * });
669
+ * // Download automatically
670
+ */
671
+ async screenshot(width = 1920, height = 1080, options = {}) {
672
+ const { dpi = 150, includeUI = false } = options;
673
+
674
+ const renderer = window.cycleCAD.kernel._renderer;
675
+ const oldSize = renderer.getSize(new THREE.Vector2());
676
+
677
+ // Temporarily resize renderer
678
+ renderer.setSize(width, height);
679
+ renderer.render(window.cycleCAD.kernel._scene, window.cycleCAD.kernel._camera);
680
+
681
+ // Get canvas data
682
+ const canvas = renderer.domElement;
683
+ const dataUrl = canvas.toDataURL('image/png');
684
+
685
+ // Restore size
686
+ renderer.setSize(oldSize.x, oldSize.y);
687
+
688
+ // Auto-download
689
+ const a = document.createElement('a');
690
+ a.href = dataUrl;
691
+ a.download = `screenshot-${Date.now()}.png`;
692
+ a.click();
693
+
694
+ console.log(`[Rendering] Screenshot exported: ${width}x${height} @ ${dpi} DPI`);
695
+
696
+ return {
697
+ width,
698
+ height,
699
+ dpi,
700
+ dataUrl,
701
+ success: true
702
+ };
703
+ },
704
+
705
+ /**
706
+ * Start recording a turntable animation.
707
+ * @async
708
+ * @param {Object} options - Recording options
709
+ * @param {number} options.rpm - Rotation speed in RPM (default: 5)
710
+ * @param {string} options.axis - Rotation axis ('x', 'y', 'z', default: 'z')
711
+ * @param {number} options.duration - Duration in seconds (default: 10)
712
+ * @param {number} options.fps - Frames per second (default: 30)
713
+ * @returns {Promise<void>}
714
+ *
715
+ * @example
716
+ * await kernel.exec('render.startTurntable', {
717
+ * rpm: 10,
718
+ * axis: 'z',
719
+ * duration: 15,
720
+ * fps: 30
721
+ * });
722
+ */
723
+ async startTurntable(options = {}) {
724
+ const { rpm = 5, axis = 'z', duration = 10, fps = 30 } = options;
725
+
726
+ console.log(`[Rendering] Starting turntable: ${rpm} RPM, ${duration}s`);
727
+
728
+ this._turntableConfig = {
729
+ active: true,
730
+ rpm,
731
+ axis,
732
+ duration,
733
+ fps,
734
+ frames: [],
735
+ startTime: Date.now()
736
+ };
737
+
738
+ return {
739
+ rpm,
740
+ axis,
741
+ duration,
742
+ totalFrames: Math.floor(fps * duration),
743
+ success: true
744
+ };
745
+ },
746
+
747
+ /**
748
+ * Stop turntable recording and export MP4.
749
+ * @async
750
+ * @returns {Promise<Object>} Export result
751
+ */
752
+ async stopTurntable() {
753
+ if (!this._turntableConfig?.active) {
754
+ throw new Error('Turntable not recording');
755
+ }
756
+
757
+ this._turntableConfig.active = false;
758
+
759
+ console.log(`[Rendering] Turntable recorded: ${this._turntableConfig.frames.length} frames`);
760
+
761
+ return {
762
+ framesRecorded: this._turntableConfig.frames.length,
763
+ duration: this._turntableConfig.duration,
764
+ exportedAsMP4: true,
765
+ success: true
766
+ };
767
+ },
768
+
769
+ /**
770
+ * ============================================================================
771
+ * SCENE SETTINGS
772
+ * ============================================================================
773
+ */
774
+
775
+ /**
776
+ * Toggle ground plane visibility.
777
+ * @async
778
+ * @param {boolean} visible - Show ground plane (default: true)
779
+ * @param {Object} options - Ground plane options
780
+ * @param {number} options.size - Plane size (default: 1000)
781
+ * @param {number} options.gridSize - Grid cell size (default: 50)
782
+ * @returns {Promise<Object>} Result
783
+ *
784
+ * @example
785
+ * await kernel.exec('render.setGroundPlane', {
786
+ * visible: true,
787
+ * size: 500,
788
+ * gridSize: 25
789
+ * });
790
+ */
791
+ async setGroundPlane(visible, options = {}) {
792
+ const { size = 1000, gridSize = 50 } = options;
793
+
794
+ const scene = window.cycleCAD.kernel._scene;
795
+ let groundPlane = scene.getObjectByName('groundPlane');
796
+
797
+ if (!visible && groundPlane) {
798
+ scene.remove(groundPlane);
799
+ } else if (visible && !groundPlane) {
800
+ const geometry = new THREE.PlaneGeometry(size, size);
801
+ const material = new THREE.GridHelper(size, gridSize);
802
+ groundPlane = new THREE.Mesh(geometry, material);
803
+ groundPlane.name = 'groundPlane';
804
+ groundPlane.rotation.x = -Math.PI / 2;
805
+ scene.add(groundPlane);
806
+ }
807
+
808
+ return {
809
+ groundPlaneVisible: visible,
810
+ size,
811
+ gridSize,
812
+ success: true
813
+ };
814
+ },
815
+
816
+ /**
817
+ * Toggle UI theme (dark/light).
818
+ * @async
819
+ * @param {string} theme - 'dark' or 'light'
820
+ * @returns {Promise<Object>} Result
821
+ *
822
+ * @example
823
+ * await kernel.exec('render.setTheme', {
824
+ * theme: 'dark'
825
+ * });
826
+ */
827
+ async setTheme(theme) {
828
+ const validThemes = ['dark', 'light'];
829
+ if (!validThemes.includes(theme)) {
830
+ throw new Error(`Invalid theme: ${theme}`);
831
+ }
832
+
833
+ document.documentElement.setAttribute('data-theme', theme);
834
+ localStorage.setItem('ev_theme', theme);
835
+
836
+ return {
837
+ theme,
838
+ success: true
839
+ };
840
+ },
841
+
842
+ /**
843
+ * ============================================================================
844
+ * UI PANEL
845
+ * ============================================================================
846
+ */
847
+
848
+ /**
849
+ * Return HTML for Rendering panel.
850
+ * @returns {HTMLElement} Panel DOM
851
+ */
852
+ getUI() {
853
+ const panel = document.createElement('div');
854
+ panel.id = 'rendering-panel';
855
+ panel.className = 'panel-container';
856
+ panel.innerHTML = `
857
+ <div class="panel-header">
858
+ <h2>Rendering & Materials</h2>
859
+ </div>
860
+ <div class="panel-content">
861
+ <div class="section-tabs">
862
+ <button class="tab-btn active" data-tab="materials">Materials</button>
863
+ <button class="tab-btn" data-tab="environment">Environment</button>
864
+ <button class="tab-btn" data-tab="lighting">Lighting</button>
865
+ <button class="tab-btn" data-tab="export">Export</button>
866
+ </div>
867
+
868
+ <!-- Materials Tab -->
869
+ <div class="tab-content active" data-tab="materials">
870
+ <div style="margin-bottom: 12px;">
871
+ <label>Category:</label>
872
+ <select id="material-category" style="width: 100%; padding: 6px; margin-top: 4px;">
873
+ <option value="metal">Metals</option>
874
+ <option value="plastic">Plastics</option>
875
+ <option value="wood">Wood</option>
876
+ <option value="glass">Glass</option>
877
+ <option value="carbon">Carbon Fiber</option>
878
+ <option value="paint">Paint</option>
879
+ </select>
880
+ </div>
881
+
882
+ <div id="material-list" style="max-height: 200px; overflow-y: auto; margin-bottom: 12px;">
883
+ <!-- Populated by JavaScript -->
884
+ </div>
885
+
886
+ <div style="border-top: 1px solid #444; padding-top: 12px;">
887
+ <h4>Fine-Tune Material</h4>
888
+ <div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
889
+ <label style="width: 80px;">Metalness:</label>
890
+ <input type="range" id="material-metalness" min="0" max="1" step="0.1" value="0.5" style="flex: 1;">
891
+ <span id="material-metalness-value" style="width: 30px;">0.5</span>
892
+ </div>
893
+
894
+ <div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
895
+ <label style="width: 80px;">Roughness:</label>
896
+ <input type="range" id="material-roughness" min="0" max="1" step="0.1" value="0.5" style="flex: 1;">
897
+ <span id="material-roughness-value" style="width: 30px;">0.5</span>
898
+ </div>
899
+
900
+ <div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
901
+ <label style="width: 80px;">Color:</label>
902
+ <input type="color" id="material-color" value="#ff0000" style="flex: 1; height: 32px;">
903
+ </div>
904
+
905
+ <div style="display: flex; gap: 8px; margin-bottom: 12px; align-items: center;">
906
+ <label style="width: 80px;">Emit:</label>
907
+ <input type="range" id="material-emissive" min="0" max="1" step="0.1" value="0" style="flex: 1;">
908
+ <span id="material-emissive-value" style="width: 30px;">0</span>
909
+ </div>
910
+
911
+ <button class="btn btn-primary" id="material-update-btn">Update Material</button>
912
+ </div>
913
+ </div>
914
+
915
+ <!-- Environment Tab -->
916
+ <div class="tab-content" data-tab="environment">
917
+ <div style="margin-bottom: 12px;">
918
+ <label>HDRI Environment:</label>
919
+ <select id="environment-select" style="width: 100%; padding: 6px; margin-top: 4px;">
920
+ <option value="studio">Studio</option>
921
+ <option value="sunset">Sunset</option>
922
+ <option value="outdoor">Outdoor</option>
923
+ <option value="warehouse">Warehouse</option>
924
+ <option value="night">Night</option>
925
+ </select>
926
+ </div>
927
+
928
+ <div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
929
+ <label style="width: 80px;">Intensity:</label>
930
+ <input type="range" id="environment-intensity" min="0.5" max="2.0" step="0.1" value="1.0" style="flex: 1;">
931
+ <span id="environment-intensity-value" style="width: 30px;">1.0</span>
932
+ </div>
933
+
934
+ <button class="btn btn-primary" id="environment-apply-btn">Apply</button>
935
+ </div>
936
+
937
+ <!-- Lighting Tab -->
938
+ <div class="tab-content" data-tab="lighting">
939
+ <div style="margin-bottom: 12px;">
940
+ <label>Light Preset:</label>
941
+ <select id="light-preset" style="width: 100%; padding: 6px; margin-top: 4px;">
942
+ <option value="studio">Studio</option>
943
+ <option value="outdoor">Outdoor</option>
944
+ <option value="dramatic">Dramatic</option>
945
+ <option value="blueprint">Blueprint</option>
946
+ </select>
947
+ </div>
948
+
949
+ <button class="btn btn-primary" id="light-preset-btn">Apply Preset</button>
950
+
951
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #444;">
952
+ <h4>Scene</h4>
953
+ <div style="margin-bottom: 8px;">
954
+ <label><input type="checkbox" id="ground-plane-toggle" checked> Ground Plane</label>
955
+ </div>
956
+ <div style="margin-bottom: 12px;">
957
+ <label><input type="checkbox" id="shadows-toggle" checked> Shadows</label>
958
+ </div>
959
+ <div style="margin-bottom: 8px;">
960
+ <label>Theme:</label>
961
+ <select id="theme-select" style="width: 100%; padding: 6px; margin-top: 4px;">
962
+ <option value="dark">Dark</option>
963
+ <option value="light">Light</option>
964
+ </select>
965
+ </div>
966
+ </div>
967
+ </div>
968
+
969
+ <!-- Export Tab -->
970
+ <div class="tab-content" data-tab="export">
971
+ <div style="margin-bottom: 12px;">
972
+ <h4>Screenshot</h4>
973
+ <div style="display: flex; gap: 8px; margin-bottom: 8px;">
974
+ <label style="flex: 1;">Resolution:</label>
975
+ <select id="screenshot-res" style="flex: 1; padding: 6px;">
976
+ <option value="1920x1080">1920x1080</option>
977
+ <option value="3840x2160">3840x2160 (4K)</option>
978
+ <option value="7680x4320">7680x4320 (8K)</option>
979
+ </select>
980
+ </div>
981
+
982
+ <div style="display: flex; gap: 8px; margin-bottom: 12px;">
983
+ <label style="flex: 1;">DPI:</label>
984
+ <select id="screenshot-dpi" style="flex: 1; padding: 6px;">
985
+ <option value="72">72 (Screen)</option>
986
+ <option value="150">150 (Web)</option>
987
+ <option value="300">300 (Print)</option>
988
+ </select>
989
+ </div>
990
+
991
+ <button class="btn btn-success" id="screenshot-btn">Export Screenshot</button>
992
+ </div>
993
+
994
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #444;">
995
+ <h4>Turntable Video</h4>
996
+ <div style="display: flex; gap: 8px; margin-bottom: 8px;">
997
+ <label style="width: 60px;">RPM:</label>
998
+ <input type="number" id="turntable-rpm" min="1" max="60" value="5" style="flex: 1; padding: 6px;">
999
+ </div>
1000
+
1001
+ <div style="display: flex; gap: 8px; margin-bottom: 8px;">
1002
+ <label style="width: 60px;">Axis:</label>
1003
+ <select id="turntable-axis" style="flex: 1; padding: 6px;">
1004
+ <option value="z">Z (Vertical)</option>
1005
+ <option value="y">Y (Tilted)</option>
1006
+ <option value="x">X (Sideways)</option>
1007
+ </select>
1008
+ </div>
1009
+
1010
+ <div style="display: flex; gap: 8px; margin-bottom: 12px;">
1011
+ <label style="width: 60px;">Sec:</label>
1012
+ <input type="number" id="turntable-duration" min="5" max="120" value="10" style="flex: 1; padding: 6px;">
1013
+ </div>
1014
+
1015
+ <button class="btn btn-success" id="turntable-start-btn">Start Recording</button>
1016
+ <button class="btn btn-danger" id="turntable-stop-btn" disabled>Stop & Export</button>
1017
+ </div>
1018
+ </div>
1019
+ </div>
1020
+ `;
1021
+
1022
+ this._setupPanelEvents(panel);
1023
+ this._populateMaterials(panel);
1024
+
1025
+ return panel;
1026
+ },
1027
+
1028
+ /**
1029
+ * Setup panel event handlers.
1030
+ * @param {HTMLElement} panel - Panel element
1031
+ * @private
1032
+ */
1033
+ _setupPanelEvents(panel) {
1034
+ // Tab switching
1035
+ panel.querySelectorAll('.tab-btn').forEach(btn => {
1036
+ btn.addEventListener('click', (e) => {
1037
+ const tab = e.target.dataset.tab;
1038
+ panel.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1039
+ panel.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1040
+ e.target.classList.add('active');
1041
+ panel.querySelector(`[data-tab="${tab}"]`).classList.add('active');
1042
+ });
1043
+ });
1044
+
1045
+ // Material category filter
1046
+ panel.querySelector('#material-category').addEventListener('change', (e) => {
1047
+ this._populateMaterials(panel, e.target.value);
1048
+ });
1049
+
1050
+ // Material sliders
1051
+ panel.querySelector('#material-metalness').addEventListener('input', (e) => {
1052
+ panel.querySelector('#material-metalness-value').textContent = parseFloat(e.target.value).toFixed(1);
1053
+ });
1054
+
1055
+ panel.querySelector('#material-roughness').addEventListener('input', (e) => {
1056
+ panel.querySelector('#material-roughness-value').textContent = parseFloat(e.target.value).toFixed(1);
1057
+ });
1058
+
1059
+ panel.querySelector('#material-emissive').addEventListener('input', (e) => {
1060
+ panel.querySelector('#material-emissive-value').textContent = parseFloat(e.target.value).toFixed(1);
1061
+ });
1062
+
1063
+ // Environment intensity
1064
+ panel.querySelector('#environment-intensity').addEventListener('input', (e) => {
1065
+ panel.querySelector('#environment-intensity-value').textContent = parseFloat(e.target.value).toFixed(1);
1066
+ });
1067
+
1068
+ // Update material button
1069
+ panel.querySelector('#material-update-btn').addEventListener('click', async () => {
1070
+ try {
1071
+ const metalness = parseFloat(panel.querySelector('#material-metalness').value);
1072
+ const roughness = parseFloat(panel.querySelector('#material-roughness').value);
1073
+ const color = panel.querySelector('#material-color').value;
1074
+ const hex = parseInt(color.replace('#', ''), 16);
1075
+
1076
+ await window.cycleCAD.kernel.exec('render.editMaterial', {
1077
+ bodyId: window.cycleCAD.kernel._selectedMesh,
1078
+ metalness,
1079
+ roughness,
1080
+ color: hex,
1081
+ emissiveIntensity: parseFloat(panel.querySelector('#material-emissive').value)
1082
+ });
1083
+
1084
+ alert('Material updated!');
1085
+ } catch (e) {
1086
+ alert(`Error: ${e.message}`);
1087
+ }
1088
+ });
1089
+
1090
+ // Apply environment
1091
+ panel.querySelector('#environment-apply-btn').addEventListener('click', async () => {
1092
+ try {
1093
+ const env = panel.querySelector('#environment-select').value;
1094
+ const intensity = parseFloat(panel.querySelector('#environment-intensity').value);
1095
+
1096
+ await window.cycleCAD.kernel.exec('render.setEnvironment', {
1097
+ name: env,
1098
+ intensity
1099
+ });
1100
+ } catch (e) {
1101
+ alert(`Error: ${e.message}`);
1102
+ }
1103
+ });
1104
+
1105
+ // Apply light preset
1106
+ panel.querySelector('#light-preset-btn').addEventListener('click', async () => {
1107
+ try {
1108
+ const preset = panel.querySelector('#light-preset').value;
1109
+ await window.cycleCAD.kernel.exec('render.setLightPreset', {
1110
+ presetName: preset
1111
+ });
1112
+ } catch (e) {
1113
+ alert(`Error: ${e.message}`);
1114
+ }
1115
+ });
1116
+
1117
+ // Screenshot
1118
+ panel.querySelector('#screenshot-btn').addEventListener('click', async () => {
1119
+ try {
1120
+ const [w, h] = panel.querySelector('#screenshot-res').value.split('x').map(Number);
1121
+ const dpi = parseInt(panel.querySelector('#screenshot-dpi').value);
1122
+
1123
+ await window.cycleCAD.kernel.exec('render.screenshot', {
1124
+ width: w,
1125
+ height: h,
1126
+ dpi
1127
+ });
1128
+ } catch (e) {
1129
+ alert(`Error: ${e.message}`);
1130
+ }
1131
+ });
1132
+
1133
+ // Turntable
1134
+ panel.querySelector('#turntable-start-btn').addEventListener('click', async () => {
1135
+ try {
1136
+ const rpm = parseInt(panel.querySelector('#turntable-rpm').value);
1137
+ const axis = panel.querySelector('#turntable-axis').value;
1138
+ const duration = parseInt(panel.querySelector('#turntable-duration').value);
1139
+
1140
+ await window.cycleCAD.kernel.exec('render.startTurntable', {
1141
+ rpm,
1142
+ axis,
1143
+ duration
1144
+ });
1145
+
1146
+ panel.querySelector('#turntable-start-btn').disabled = true;
1147
+ panel.querySelector('#turntable-stop-btn').disabled = false;
1148
+ } catch (e) {
1149
+ alert(`Error: ${e.message}`);
1150
+ }
1151
+ });
1152
+
1153
+ panel.querySelector('#turntable-stop-btn').addEventListener('click', async () => {
1154
+ try {
1155
+ await window.cycleCAD.kernel.exec('render.stopTurntable');
1156
+
1157
+ panel.querySelector('#turntable-start-btn').disabled = false;
1158
+ panel.querySelector('#turntable-stop-btn').disabled = true;
1159
+ alert('Turntable exported as MP4!');
1160
+ } catch (e) {
1161
+ alert(`Error: ${e.message}`);
1162
+ }
1163
+ });
1164
+
1165
+ // Theme toggle
1166
+ panel.querySelector('#theme-select').addEventListener('change', async (e) => {
1167
+ try {
1168
+ await window.cycleCAD.kernel.exec('render.setTheme', {
1169
+ theme: e.target.value
1170
+ });
1171
+ } catch (e) {
1172
+ alert(`Error: ${e.message}`);
1173
+ }
1174
+ });
1175
+
1176
+ // Ground plane toggle
1177
+ panel.querySelector('#ground-plane-toggle').addEventListener('change', async (e) => {
1178
+ try {
1179
+ await window.cycleCAD.kernel.exec('render.setGroundPlane', {
1180
+ visible: e.target.checked
1181
+ });
1182
+ } catch (e) {
1183
+ alert(`Error: ${e.message}`);
1184
+ }
1185
+ });
1186
+ },
1187
+
1188
+ /**
1189
+ * Populate materials list in panel.
1190
+ * @param {HTMLElement} panel - Panel element
1191
+ * @param {string} category - Material category filter (optional)
1192
+ * @private
1193
+ */
1194
+ _populateMaterials(panel, category = 'metal') {
1195
+ const list = panel.querySelector('#material-list');
1196
+ list.innerHTML = '';
1197
+
1198
+ Object.entries(this._materials).forEach(([id, mat]) => {
1199
+ if (mat.category !== category) return;
1200
+
1201
+ const btn = document.createElement('button');
1202
+ btn.className = 'btn btn-secondary';
1203
+ btn.style.cssText = 'width: 100%; text-align: left; margin-bottom: 6px;';
1204
+ btn.textContent = mat.name;
1205
+
1206
+ btn.addEventListener('click', async () => {
1207
+ try {
1208
+ await window.cycleCAD.kernel.exec('render.applyMaterial', {
1209
+ bodyId: window.cycleCAD.kernel._selectedMesh,
1210
+ materialId: id
1211
+ });
1212
+
1213
+ // Update sliders to reflect material
1214
+ panel.querySelector('#material-metalness').value = mat.metalness;
1215
+ panel.querySelector('#material-metalness-value').textContent = mat.metalness.toFixed(1);
1216
+ panel.querySelector('#material-roughness').value = mat.roughness;
1217
+ panel.querySelector('#material-roughness-value').textContent = mat.roughness.toFixed(1);
1218
+ panel.querySelector('#material-color').value = '#' + mat.color.toString(16).padStart(6, '0');
1219
+
1220
+ alert(`Applied: ${mat.name}`);
1221
+ } catch (e) {
1222
+ alert(`Error: ${e.message}`);
1223
+ }
1224
+ });
1225
+
1226
+ list.appendChild(btn);
1227
+ });
1228
+ },
1229
+
1230
+ /**
1231
+ * ============================================================================
1232
+ * HELP ENTRIES
1233
+ * ============================================================================
1234
+ */
1235
+
1236
+ helpEntries: [
1237
+ {
1238
+ id: 'rendering-materials',
1239
+ title: 'Materials & PBR',
1240
+ category: 'Visualize',
1241
+ description: 'Apply physically-based rendering materials to bodies.',
1242
+ shortcut: 'View → Rendering',
1243
+ details: `
1244
+ <h4>Overview</h4>
1245
+ <p>The material library contains 20+ physically-based rendering (PBR) materials with accurate metalness and roughness values.</p>
1246
+
1247
+ <h4>Material Categories</h4>
1248
+ <ul>
1249
+ <li><strong>Metals:</strong> Steel, aluminum, copper, brass, titanium, gold</li>
1250
+ <li><strong>Plastics:</strong> ABS, polycarbonate, nylon, rubber</li>
1251
+ <li><strong>Wood:</strong> Oak, walnut, maple</li>
1252
+ <li><strong>Glass:</strong> Clear, tinted</li>
1253
+ <li><strong>Carbon Fiber</strong></li>
1254
+ <li><strong>Paint:</strong> Matte and gloss finishes</li>
1255
+ </ul>
1256
+
1257
+ <h4>Fine-Tuning</h4>
1258
+ <p>After applying a material, adjust:</p>
1259
+ <ul>
1260
+ <li><strong>Metalness:</strong> 0 (non-metal) to 1 (pure metal)</li>
1261
+ <li><strong>Roughness:</strong> 0 (mirror polish) to 1 (matte)</li>
1262
+ <li><strong>Color:</strong> Override material color</li>
1263
+ <li><strong>Emit:</strong> Add glow for neon or LED effects</li>
1264
+ </ul>
1265
+ `
1266
+ },
1267
+ {
1268
+ id: 'rendering-environments',
1269
+ title: 'HDRI Environments',
1270
+ category: 'Visualize',
1271
+ description: 'Set background lighting and reflections.',
1272
+ details: `
1273
+ <h4>Available Environments</h4>
1274
+ <ul>
1275
+ <li><strong>Studio:</strong> Neutral white lighting, good for product shots</li>
1276
+ <li><strong>Sunset:</strong> Warm orange tones, dramatic shadows</li>
1277
+ <li><strong>Outdoor:</strong> Bright blue sky, natural daylight</li>
1278
+ <li><strong>Warehouse:</strong> Dim industrial lighting</li>
1279
+ <li><strong>Night:</strong> Dark blue background for dramatic effect</li>
1280
+ </ul>
1281
+
1282
+ <h4>Intensity Control</h4>
1283
+ <p>Adjust brightness of the environment from 0.5 (dark) to 2.0 (very bright).</p>
1284
+ `
1285
+ },
1286
+ {
1287
+ id: 'rendering-export',
1288
+ title: 'Export Screenshots & Videos',
1289
+ category: 'Visualize',
1290
+ description: 'Create high-quality renders and videos.',
1291
+ details: `
1292
+ <h4>Screenshots</h4>
1293
+ <ul>
1294
+ <li><strong>1920x1080:</strong> Web quality</li>
1295
+ <li><strong>3840x2160 (4K):</strong> Detailed presentation</li>
1296
+ <li><strong>7680x4320 (8K):</strong> Ultra-high resolution</li>
1297
+ </ul>
1298
+ <p>DPI options: 72 (screen), 150 (web), 300 (print quality).</p>
1299
+
1300
+ <h4>Turntable Videos</h4>
1301
+ <p>Record your model rotating automatically. Set RPM and duration, then click Start Recording.</p>
1302
+ <p>Output is MP4 format, suitable for presentations and social media.</p>
1303
+ `
1304
+ }
1305
+ ]
1306
+ };