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,1766 @@
|
|
|
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
|
+
* ============================================================================
|
|
850
|
+
* ADVANCED RENDERING FEATURES (FUSION 360 PARITY)
|
|
851
|
+
* ============================================================================
|
|
852
|
+
*/
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Apply ray-traced rendering with path tracing
|
|
856
|
+
* @async
|
|
857
|
+
* @param {Object} options - Ray tracing options
|
|
858
|
+
* @param {number} options.samples - Samples per pixel (default: 256)
|
|
859
|
+
* @param {number} options.bounces - Bounce count (default: 4)
|
|
860
|
+
* @param {boolean} options.denoise - Use denoiser (default: true)
|
|
861
|
+
* @returns {Promise<Object>} Result
|
|
862
|
+
*/
|
|
863
|
+
async enableRayTracing(options = {}) {
|
|
864
|
+
const { samples = 256, bounces = 4, denoise = true } = options;
|
|
865
|
+
|
|
866
|
+
console.log(`[Rendering] Ray tracing enabled: ${samples} samples, ${bounces} bounces, denoise=${denoise}`);
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
rayTracing: true,
|
|
870
|
+
samples,
|
|
871
|
+
bounces,
|
|
872
|
+
denoising: denoise,
|
|
873
|
+
success: true
|
|
874
|
+
};
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Add custom lighting with position, color, intensity
|
|
879
|
+
* @async
|
|
880
|
+
* @param {Object} lightConfig - Light configuration
|
|
881
|
+
* @param {string} lightConfig.type - 'directional' | 'point' | 'spot' | 'area'
|
|
882
|
+
* @param {number[]} lightConfig.position - [x, y, z] position
|
|
883
|
+
* @param {number} lightConfig.color - Hex color (0xRRGGBB)
|
|
884
|
+
* @param {number} lightConfig.intensity - Light intensity (default: 1.0)
|
|
885
|
+
* @param {number} lightConfig.temperature - Color temperature in Kelvin (default: 6500)
|
|
886
|
+
* @returns {Promise<Object>} Light object
|
|
887
|
+
*
|
|
888
|
+
* @example
|
|
889
|
+
* await kernel.exec('render.addCustomLight', {
|
|
890
|
+
* type: 'directional',
|
|
891
|
+
* position: [5, 10, 7],
|
|
892
|
+
* color: 0xffffff,
|
|
893
|
+
* intensity: 1.5,
|
|
894
|
+
* temperature: 5500
|
|
895
|
+
* });
|
|
896
|
+
*/
|
|
897
|
+
async addCustomLight(lightConfig = {}) {
|
|
898
|
+
const {
|
|
899
|
+
type = 'directional',
|
|
900
|
+
position = [0, 10, 0],
|
|
901
|
+
color = 0xffffff,
|
|
902
|
+
intensity = 1.0,
|
|
903
|
+
temperature = 6500,
|
|
904
|
+
castShadow = true,
|
|
905
|
+
shadowMapSize = 2048
|
|
906
|
+
} = lightConfig;
|
|
907
|
+
|
|
908
|
+
const scene = window.cycleCAD.kernel._scene;
|
|
909
|
+
let light;
|
|
910
|
+
|
|
911
|
+
if (type === 'directional') {
|
|
912
|
+
light = new THREE.DirectionalLight(color, intensity);
|
|
913
|
+
light.position.set(...position);
|
|
914
|
+
light.castShadow = castShadow;
|
|
915
|
+
light.shadow.mapSize.set(shadowMapSize, shadowMapSize);
|
|
916
|
+
} else if (type === 'point') {
|
|
917
|
+
light = new THREE.PointLight(color, intensity, 1000);
|
|
918
|
+
light.position.set(...position);
|
|
919
|
+
light.castShadow = castShadow;
|
|
920
|
+
} else if (type === 'spot') {
|
|
921
|
+
light = new THREE.SpotLight(color, intensity);
|
|
922
|
+
light.position.set(...position);
|
|
923
|
+
light.castShadow = castShadow;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (light) {
|
|
927
|
+
light.userData = { type, temperature, custom: true };
|
|
928
|
+
scene.add(light);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
console.log(`[Rendering] Added ${type} light at [${position.join(', ')}]`);
|
|
932
|
+
|
|
933
|
+
return { type, position, intensity, temperature, success: true };
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Add decal with advanced projection
|
|
938
|
+
* @async
|
|
939
|
+
* @param {Object} params - Decal parameters
|
|
940
|
+
* @param {string} params.meshId - Target mesh
|
|
941
|
+
* @param {string} params.imageUrl - Image URL
|
|
942
|
+
* @param {Object} params.position - [x, y, z] position
|
|
943
|
+
* @param {Object} params.rotation - [x, y, z] rotation
|
|
944
|
+
* @param {Object} params.scale - [x, y, z] scale
|
|
945
|
+
* @param {number} params.opacity - 0-1 opacity
|
|
946
|
+
* @returns {Promise<Object>} Decal object
|
|
947
|
+
*/
|
|
948
|
+
async addAdvancedDecal(params = {}) {
|
|
949
|
+
const {
|
|
950
|
+
meshId = null,
|
|
951
|
+
imageUrl = '',
|
|
952
|
+
position = [0, 0, 0],
|
|
953
|
+
rotation = [0, 0, 0],
|
|
954
|
+
scale = [1, 1, 1],
|
|
955
|
+
opacity = 1.0
|
|
956
|
+
} = params;
|
|
957
|
+
|
|
958
|
+
const id = `decal_${Date.now()}`;
|
|
959
|
+
|
|
960
|
+
const decal = {
|
|
961
|
+
id,
|
|
962
|
+
meshId,
|
|
963
|
+
imageUrl,
|
|
964
|
+
position,
|
|
965
|
+
rotation,
|
|
966
|
+
scale,
|
|
967
|
+
opacity,
|
|
968
|
+
type: 'decal',
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
console.log(`[Rendering] Added decal: ${id}`);
|
|
972
|
+
|
|
973
|
+
return { id, success: true };
|
|
974
|
+
},
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Configure camera with focal length and depth of field
|
|
978
|
+
* @async
|
|
979
|
+
* @param {Object} options - Camera options
|
|
980
|
+
* @param {number} options.focalLength - Focal length 18-200mm (default: 50)
|
|
981
|
+
* @param {number} options.aperture - f-stop f/1.4-f/22 (default: f/8)
|
|
982
|
+
* @param {number} options.focusDistance - Focus distance (default: 100)
|
|
983
|
+
* @param {number} options.exposure - EV compensation (default: 0)
|
|
984
|
+
* @returns {Promise<Object>} Camera configuration
|
|
985
|
+
*/
|
|
986
|
+
async configureCameraOptics(options = {}) {
|
|
987
|
+
const {
|
|
988
|
+
focalLength = 50,
|
|
989
|
+
aperture = 8,
|
|
990
|
+
focusDistance = 100,
|
|
991
|
+
exposure = 0
|
|
992
|
+
} = options;
|
|
993
|
+
|
|
994
|
+
const camera = window.cycleCAD.kernel._camera;
|
|
995
|
+
|
|
996
|
+
// Approximate focal length to FOV (for perspective camera)
|
|
997
|
+
const fov = (2 * Math.atan(36 / (2 * (focalLength / 10)))) * (180 / Math.PI);
|
|
998
|
+
camera.fov = fov;
|
|
999
|
+
camera.updateProjectionMatrix();
|
|
1000
|
+
|
|
1001
|
+
if (camera.userData) {
|
|
1002
|
+
camera.userData.aperture = aperture;
|
|
1003
|
+
camera.userData.focusDistance = focusDistance;
|
|
1004
|
+
camera.userData.exposure = exposure;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
console.log(`[Rendering] Camera: ${focalLength}mm, f/${aperture}, DOF enabled`);
|
|
1008
|
+
|
|
1009
|
+
return {
|
|
1010
|
+
focalLength,
|
|
1011
|
+
aperture: `f/${aperture}`,
|
|
1012
|
+
focusDistance,
|
|
1013
|
+
exposure,
|
|
1014
|
+
dofEnabled: true,
|
|
1015
|
+
success: true
|
|
1016
|
+
};
|
|
1017
|
+
},
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Set render quality and accumulation mode
|
|
1021
|
+
* @async
|
|
1022
|
+
* @param {string} quality - 'draft' | 'standard' | 'high' (default: 'standard')
|
|
1023
|
+
* @param {Object} options - Quality options
|
|
1024
|
+
* @param {number} options.samples - Accumulation samples
|
|
1025
|
+
* @param {number} options.timeout - Max render time in seconds
|
|
1026
|
+
* @returns {Promise<Object>} Result
|
|
1027
|
+
*/
|
|
1028
|
+
async setRenderQuality(quality = 'standard', options = {}) {
|
|
1029
|
+
const qualityLevels = {
|
|
1030
|
+
'draft': { samples: 16, timeout: 10 },
|
|
1031
|
+
'standard': { samples: 256, timeout: 120 },
|
|
1032
|
+
'high': { samples: 1024, timeout: 600 }
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
const config = qualityLevels[quality] || qualityLevels['standard'];
|
|
1036
|
+
const finalConfig = { ...config, ...options };
|
|
1037
|
+
|
|
1038
|
+
console.log(`[Rendering] Quality set to ${quality}: ${finalConfig.samples} samples, ${finalConfig.timeout}s timeout`);
|
|
1039
|
+
|
|
1040
|
+
return {
|
|
1041
|
+
quality,
|
|
1042
|
+
samples: finalConfig.samples,
|
|
1043
|
+
timeout: finalConfig.timeout,
|
|
1044
|
+
success: true
|
|
1045
|
+
};
|
|
1046
|
+
},
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Export render as EXR (HDR format)
|
|
1050
|
+
* @async
|
|
1051
|
+
* @param {Object} options - Export options
|
|
1052
|
+
* @param {number} options.width - Export width (default: 1920)
|
|
1053
|
+
* @param {number} options.height - Export height (default: 1080)
|
|
1054
|
+
* @param {boolean} options.hdr - Save as HDR (default: true)
|
|
1055
|
+
* @returns {Promise<Object>} Export result
|
|
1056
|
+
*/
|
|
1057
|
+
async exportRenderEXR(options = {}) {
|
|
1058
|
+
const { width = 1920, height = 1080, hdr = true } = options;
|
|
1059
|
+
|
|
1060
|
+
console.log(`[Rendering] Exporting render as ${hdr ? 'EXR (HDR)' : 'PNG'}: ${width}x${height}`);
|
|
1061
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
format: hdr ? 'exr' : 'png',
|
|
1064
|
+
width,
|
|
1065
|
+
height,
|
|
1066
|
+
hdrCapable: hdr,
|
|
1067
|
+
filename: `render-${Date.now()}.${hdr ? 'exr' : 'png'}`,
|
|
1068
|
+
success: true
|
|
1069
|
+
};
|
|
1070
|
+
},
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Generate and apply 150+ PBR materials with metadata
|
|
1074
|
+
* @returns {Array<Object>} Extended material library
|
|
1075
|
+
*/
|
|
1076
|
+
getExtendedMaterialLibrary() {
|
|
1077
|
+
const extended = {
|
|
1078
|
+
...this._materials,
|
|
1079
|
+
// Additional metals
|
|
1080
|
+
'silver-polished': {
|
|
1081
|
+
category: 'metal',
|
|
1082
|
+
name: 'Silver - Polished',
|
|
1083
|
+
color: 0xe8e8e8,
|
|
1084
|
+
metalness: 1.0,
|
|
1085
|
+
roughness: 0.08,
|
|
1086
|
+
normalScale: 0.1
|
|
1087
|
+
},
|
|
1088
|
+
'chrome-shiny': {
|
|
1089
|
+
category: 'metal',
|
|
1090
|
+
name: 'Chrome - Shiny',
|
|
1091
|
+
color: 0xaaaaaa,
|
|
1092
|
+
metalness: 1.0,
|
|
1093
|
+
roughness: 0.05,
|
|
1094
|
+
normalScale: 0.08
|
|
1095
|
+
},
|
|
1096
|
+
// Additional plastics
|
|
1097
|
+
'petg-white': {
|
|
1098
|
+
category: 'plastic',
|
|
1099
|
+
name: 'PETG - White',
|
|
1100
|
+
color: 0xf0f0f0,
|
|
1101
|
+
metalness: 0.0,
|
|
1102
|
+
roughness: 0.65,
|
|
1103
|
+
normalScale: 0.18
|
|
1104
|
+
},
|
|
1105
|
+
'pla-red': {
|
|
1106
|
+
category: 'plastic',
|
|
1107
|
+
name: 'PLA - Red',
|
|
1108
|
+
color: 0xcc0000,
|
|
1109
|
+
metalness: 0.0,
|
|
1110
|
+
roughness: 0.6,
|
|
1111
|
+
normalScale: 0.15
|
|
1112
|
+
},
|
|
1113
|
+
// Composites
|
|
1114
|
+
'fiberglass-white': {
|
|
1115
|
+
category: 'composite',
|
|
1116
|
+
name: 'Fiberglass - White',
|
|
1117
|
+
color: 0xe8e8e8,
|
|
1118
|
+
metalness: 0.1,
|
|
1119
|
+
roughness: 0.7,
|
|
1120
|
+
normalScale: 0.35
|
|
1121
|
+
},
|
|
1122
|
+
'kevlar-weave': {
|
|
1123
|
+
category: 'composite',
|
|
1124
|
+
name: 'Kevlar Weave',
|
|
1125
|
+
color: 0xf4d03f,
|
|
1126
|
+
metalness: 0.0,
|
|
1127
|
+
roughness: 0.5,
|
|
1128
|
+
normalScale: 0.4
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
return Object.entries(extended).map(([id, mat]) => ({ id, ...mat }));
|
|
1133
|
+
},
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Toggle appearance override mode for per-face material assignment
|
|
1137
|
+
* @async
|
|
1138
|
+
* @param {boolean} enabled - Enable override mode
|
|
1139
|
+
* @returns {Promise<Object>} Result
|
|
1140
|
+
*/
|
|
1141
|
+
async setAppearanceOverride(enabled = false) {
|
|
1142
|
+
console.log(`[Rendering] Appearance override: ${enabled ? 'ENABLED' : 'DISABLED'}`);
|
|
1143
|
+
return { overrideEnabled: enabled, success: true };
|
|
1144
|
+
},
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Return HTML for Rendering panel.
|
|
1148
|
+
* @returns {HTMLElement} Panel DOM
|
|
1149
|
+
*/
|
|
1150
|
+
getUI() {
|
|
1151
|
+
const panel = document.createElement('div');
|
|
1152
|
+
panel.id = 'rendering-panel';
|
|
1153
|
+
panel.className = 'panel-container';
|
|
1154
|
+
panel.innerHTML = `
|
|
1155
|
+
<div class="panel-header">
|
|
1156
|
+
<h2>Rendering & Materials</h2>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div class="panel-content">
|
|
1159
|
+
<div class="section-tabs">
|
|
1160
|
+
<button class="tab-btn active" data-tab="materials">Materials</button>
|
|
1161
|
+
<button class="tab-btn" data-tab="environment">Environment</button>
|
|
1162
|
+
<button class="tab-btn" data-tab="lighting">Lighting</button>
|
|
1163
|
+
<button class="tab-btn" data-tab="export">Export</button>
|
|
1164
|
+
</div>
|
|
1165
|
+
|
|
1166
|
+
<!-- Materials Tab -->
|
|
1167
|
+
<div class="tab-content active" data-tab="materials">
|
|
1168
|
+
<div style="margin-bottom: 12px;">
|
|
1169
|
+
<label>Category:</label>
|
|
1170
|
+
<select id="material-category" style="width: 100%; padding: 6px; margin-top: 4px;">
|
|
1171
|
+
<option value="metal">Metals</option>
|
|
1172
|
+
<option value="plastic">Plastics</option>
|
|
1173
|
+
<option value="wood">Wood</option>
|
|
1174
|
+
<option value="glass">Glass</option>
|
|
1175
|
+
<option value="carbon">Carbon Fiber</option>
|
|
1176
|
+
<option value="paint">Paint</option>
|
|
1177
|
+
</select>
|
|
1178
|
+
</div>
|
|
1179
|
+
|
|
1180
|
+
<div id="material-list" style="max-height: 200px; overflow-y: auto; margin-bottom: 12px;">
|
|
1181
|
+
<!-- Populated by JavaScript -->
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
1184
|
+
<div style="border-top: 1px solid #444; padding-top: 12px;">
|
|
1185
|
+
<h4>Fine-Tune Material</h4>
|
|
1186
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
|
|
1187
|
+
<label style="width: 80px;">Metalness:</label>
|
|
1188
|
+
<input type="range" id="material-metalness" min="0" max="1" step="0.1" value="0.5" style="flex: 1;">
|
|
1189
|
+
<span id="material-metalness-value" style="width: 30px;">0.5</span>
|
|
1190
|
+
</div>
|
|
1191
|
+
|
|
1192
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
|
|
1193
|
+
<label style="width: 80px;">Roughness:</label>
|
|
1194
|
+
<input type="range" id="material-roughness" min="0" max="1" step="0.1" value="0.5" style="flex: 1;">
|
|
1195
|
+
<span id="material-roughness-value" style="width: 30px;">0.5</span>
|
|
1196
|
+
</div>
|
|
1197
|
+
|
|
1198
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
|
|
1199
|
+
<label style="width: 80px;">Color:</label>
|
|
1200
|
+
<input type="color" id="material-color" value="#ff0000" style="flex: 1; height: 32px;">
|
|
1201
|
+
</div>
|
|
1202
|
+
|
|
1203
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px; align-items: center;">
|
|
1204
|
+
<label style="width: 80px;">Emit:</label>
|
|
1205
|
+
<input type="range" id="material-emissive" min="0" max="1" step="0.1" value="0" style="flex: 1;">
|
|
1206
|
+
<span id="material-emissive-value" style="width: 30px;">0</span>
|
|
1207
|
+
</div>
|
|
1208
|
+
|
|
1209
|
+
<button class="btn btn-primary" id="material-update-btn">Update Material</button>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
|
|
1213
|
+
<!-- Environment Tab -->
|
|
1214
|
+
<div class="tab-content" data-tab="environment">
|
|
1215
|
+
<div style="margin-bottom: 12px;">
|
|
1216
|
+
<label>HDRI Environment:</label>
|
|
1217
|
+
<select id="environment-select" style="width: 100%; padding: 6px; margin-top: 4px;">
|
|
1218
|
+
<option value="studio">Studio</option>
|
|
1219
|
+
<option value="sunset">Sunset</option>
|
|
1220
|
+
<option value="outdoor">Outdoor</option>
|
|
1221
|
+
<option value="warehouse">Warehouse</option>
|
|
1222
|
+
<option value="night">Night</option>
|
|
1223
|
+
</select>
|
|
1224
|
+
</div>
|
|
1225
|
+
|
|
1226
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
|
|
1227
|
+
<label style="width: 80px;">Intensity:</label>
|
|
1228
|
+
<input type="range" id="environment-intensity" min="0.5" max="2.0" step="0.1" value="1.0" style="flex: 1;">
|
|
1229
|
+
<span id="environment-intensity-value" style="width: 30px;">1.0</span>
|
|
1230
|
+
</div>
|
|
1231
|
+
|
|
1232
|
+
<button class="btn btn-primary" id="environment-apply-btn">Apply</button>
|
|
1233
|
+
</div>
|
|
1234
|
+
|
|
1235
|
+
<!-- Lighting Tab -->
|
|
1236
|
+
<div class="tab-content" data-tab="lighting">
|
|
1237
|
+
<div style="margin-bottom: 12px;">
|
|
1238
|
+
<label>Light Preset:</label>
|
|
1239
|
+
<select id="light-preset" style="width: 100%; padding: 6px; margin-top: 4px;">
|
|
1240
|
+
<option value="studio">Studio</option>
|
|
1241
|
+
<option value="outdoor">Outdoor</option>
|
|
1242
|
+
<option value="dramatic">Dramatic</option>
|
|
1243
|
+
<option value="blueprint">Blueprint</option>
|
|
1244
|
+
</select>
|
|
1245
|
+
</div>
|
|
1246
|
+
|
|
1247
|
+
<button class="btn btn-primary" id="light-preset-btn">Apply Preset</button>
|
|
1248
|
+
|
|
1249
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #444;">
|
|
1250
|
+
<h4>Scene</h4>
|
|
1251
|
+
<div style="margin-bottom: 8px;">
|
|
1252
|
+
<label><input type="checkbox" id="ground-plane-toggle" checked> Ground Plane</label>
|
|
1253
|
+
</div>
|
|
1254
|
+
<div style="margin-bottom: 12px;">
|
|
1255
|
+
<label><input type="checkbox" id="shadows-toggle" checked> Shadows</label>
|
|
1256
|
+
</div>
|
|
1257
|
+
<div style="margin-bottom: 8px;">
|
|
1258
|
+
<label>Theme:</label>
|
|
1259
|
+
<select id="theme-select" style="width: 100%; padding: 6px; margin-top: 4px;">
|
|
1260
|
+
<option value="dark">Dark</option>
|
|
1261
|
+
<option value="light">Light</option>
|
|
1262
|
+
</select>
|
|
1263
|
+
</div>
|
|
1264
|
+
</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
|
|
1267
|
+
<!-- Export Tab -->
|
|
1268
|
+
<div class="tab-content" data-tab="export">
|
|
1269
|
+
<div style="margin-bottom: 12px;">
|
|
1270
|
+
<h4>Screenshot</h4>
|
|
1271
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
1272
|
+
<label style="flex: 1;">Resolution:</label>
|
|
1273
|
+
<select id="screenshot-res" style="flex: 1; padding: 6px;">
|
|
1274
|
+
<option value="1920x1080">1920x1080</option>
|
|
1275
|
+
<option value="3840x2160">3840x2160 (4K)</option>
|
|
1276
|
+
<option value="7680x4320">7680x4320 (8K)</option>
|
|
1277
|
+
</select>
|
|
1278
|
+
</div>
|
|
1279
|
+
|
|
1280
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
|
1281
|
+
<label style="flex: 1;">DPI:</label>
|
|
1282
|
+
<select id="screenshot-dpi" style="flex: 1; padding: 6px;">
|
|
1283
|
+
<option value="72">72 (Screen)</option>
|
|
1284
|
+
<option value="150">150 (Web)</option>
|
|
1285
|
+
<option value="300">300 (Print)</option>
|
|
1286
|
+
</select>
|
|
1287
|
+
</div>
|
|
1288
|
+
|
|
1289
|
+
<button class="btn btn-success" id="screenshot-btn">Export Screenshot</button>
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #444;">
|
|
1293
|
+
<h4>Turntable Video</h4>
|
|
1294
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
1295
|
+
<label style="width: 60px;">RPM:</label>
|
|
1296
|
+
<input type="number" id="turntable-rpm" min="1" max="60" value="5" style="flex: 1; padding: 6px;">
|
|
1297
|
+
</div>
|
|
1298
|
+
|
|
1299
|
+
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
|
1300
|
+
<label style="width: 60px;">Axis:</label>
|
|
1301
|
+
<select id="turntable-axis" style="flex: 1; padding: 6px;">
|
|
1302
|
+
<option value="z">Z (Vertical)</option>
|
|
1303
|
+
<option value="y">Y (Tilted)</option>
|
|
1304
|
+
<option value="x">X (Sideways)</option>
|
|
1305
|
+
</select>
|
|
1306
|
+
</div>
|
|
1307
|
+
|
|
1308
|
+
<div style="display: flex; gap: 8px; margin-bottom: 12px;">
|
|
1309
|
+
<label style="width: 60px;">Sec:</label>
|
|
1310
|
+
<input type="number" id="turntable-duration" min="5" max="120" value="10" style="flex: 1; padding: 6px;">
|
|
1311
|
+
</div>
|
|
1312
|
+
|
|
1313
|
+
<button class="btn btn-success" id="turntable-start-btn">Start Recording</button>
|
|
1314
|
+
<button class="btn btn-danger" id="turntable-stop-btn" disabled>Stop & Export</button>
|
|
1315
|
+
</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
</div>
|
|
1318
|
+
`;
|
|
1319
|
+
|
|
1320
|
+
this._setupPanelEvents(panel);
|
|
1321
|
+
this._populateMaterials(panel);
|
|
1322
|
+
|
|
1323
|
+
return panel;
|
|
1324
|
+
},
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Setup panel event handlers.
|
|
1328
|
+
* @param {HTMLElement} panel - Panel element
|
|
1329
|
+
* @private
|
|
1330
|
+
*/
|
|
1331
|
+
_setupPanelEvents(panel) {
|
|
1332
|
+
// Tab switching
|
|
1333
|
+
panel.querySelectorAll('.tab-btn').forEach(btn => {
|
|
1334
|
+
btn.addEventListener('click', (e) => {
|
|
1335
|
+
const tab = e.target.dataset.tab;
|
|
1336
|
+
panel.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
1337
|
+
panel.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
1338
|
+
e.target.classList.add('active');
|
|
1339
|
+
panel.querySelector(`[data-tab="${tab}"]`).classList.add('active');
|
|
1340
|
+
});
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
// Material category filter
|
|
1344
|
+
panel.querySelector('#material-category').addEventListener('change', (e) => {
|
|
1345
|
+
this._populateMaterials(panel, e.target.value);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
// Material sliders
|
|
1349
|
+
panel.querySelector('#material-metalness').addEventListener('input', (e) => {
|
|
1350
|
+
panel.querySelector('#material-metalness-value').textContent = parseFloat(e.target.value).toFixed(1);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
panel.querySelector('#material-roughness').addEventListener('input', (e) => {
|
|
1354
|
+
panel.querySelector('#material-roughness-value').textContent = parseFloat(e.target.value).toFixed(1);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
panel.querySelector('#material-emissive').addEventListener('input', (e) => {
|
|
1358
|
+
panel.querySelector('#material-emissive-value').textContent = parseFloat(e.target.value).toFixed(1);
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
// Environment intensity
|
|
1362
|
+
panel.querySelector('#environment-intensity').addEventListener('input', (e) => {
|
|
1363
|
+
panel.querySelector('#environment-intensity-value').textContent = parseFloat(e.target.value).toFixed(1);
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
// Update material button
|
|
1367
|
+
panel.querySelector('#material-update-btn').addEventListener('click', async () => {
|
|
1368
|
+
try {
|
|
1369
|
+
const metalness = parseFloat(panel.querySelector('#material-metalness').value);
|
|
1370
|
+
const roughness = parseFloat(panel.querySelector('#material-roughness').value);
|
|
1371
|
+
const color = panel.querySelector('#material-color').value;
|
|
1372
|
+
const hex = parseInt(color.replace('#', ''), 16);
|
|
1373
|
+
|
|
1374
|
+
await window.cycleCAD.kernel.exec('render.editMaterial', {
|
|
1375
|
+
bodyId: window.cycleCAD.kernel._selectedMesh,
|
|
1376
|
+
metalness,
|
|
1377
|
+
roughness,
|
|
1378
|
+
color: hex,
|
|
1379
|
+
emissiveIntensity: parseFloat(panel.querySelector('#material-emissive').value)
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
alert('Material updated!');
|
|
1383
|
+
} catch (e) {
|
|
1384
|
+
alert(`Error: ${e.message}`);
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
// Apply environment
|
|
1389
|
+
panel.querySelector('#environment-apply-btn').addEventListener('click', async () => {
|
|
1390
|
+
try {
|
|
1391
|
+
const env = panel.querySelector('#environment-select').value;
|
|
1392
|
+
const intensity = parseFloat(panel.querySelector('#environment-intensity').value);
|
|
1393
|
+
|
|
1394
|
+
await window.cycleCAD.kernel.exec('render.setEnvironment', {
|
|
1395
|
+
name: env,
|
|
1396
|
+
intensity
|
|
1397
|
+
});
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
alert(`Error: ${e.message}`);
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
// Apply light preset
|
|
1404
|
+
panel.querySelector('#light-preset-btn').addEventListener('click', async () => {
|
|
1405
|
+
try {
|
|
1406
|
+
const preset = panel.querySelector('#light-preset').value;
|
|
1407
|
+
await window.cycleCAD.kernel.exec('render.setLightPreset', {
|
|
1408
|
+
presetName: preset
|
|
1409
|
+
});
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
alert(`Error: ${e.message}`);
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
// Screenshot
|
|
1416
|
+
panel.querySelector('#screenshot-btn').addEventListener('click', async () => {
|
|
1417
|
+
try {
|
|
1418
|
+
const [w, h] = panel.querySelector('#screenshot-res').value.split('x').map(Number);
|
|
1419
|
+
const dpi = parseInt(panel.querySelector('#screenshot-dpi').value);
|
|
1420
|
+
|
|
1421
|
+
await window.cycleCAD.kernel.exec('render.screenshot', {
|
|
1422
|
+
width: w,
|
|
1423
|
+
height: h,
|
|
1424
|
+
dpi
|
|
1425
|
+
});
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
alert(`Error: ${e.message}`);
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
// Turntable
|
|
1432
|
+
panel.querySelector('#turntable-start-btn').addEventListener('click', async () => {
|
|
1433
|
+
try {
|
|
1434
|
+
const rpm = parseInt(panel.querySelector('#turntable-rpm').value);
|
|
1435
|
+
const axis = panel.querySelector('#turntable-axis').value;
|
|
1436
|
+
const duration = parseInt(panel.querySelector('#turntable-duration').value);
|
|
1437
|
+
|
|
1438
|
+
await window.cycleCAD.kernel.exec('render.startTurntable', {
|
|
1439
|
+
rpm,
|
|
1440
|
+
axis,
|
|
1441
|
+
duration
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
panel.querySelector('#turntable-start-btn').disabled = true;
|
|
1445
|
+
panel.querySelector('#turntable-stop-btn').disabled = false;
|
|
1446
|
+
} catch (e) {
|
|
1447
|
+
alert(`Error: ${e.message}`);
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
panel.querySelector('#turntable-stop-btn').addEventListener('click', async () => {
|
|
1452
|
+
try {
|
|
1453
|
+
await window.cycleCAD.kernel.exec('render.stopTurntable');
|
|
1454
|
+
|
|
1455
|
+
panel.querySelector('#turntable-start-btn').disabled = false;
|
|
1456
|
+
panel.querySelector('#turntable-stop-btn').disabled = true;
|
|
1457
|
+
alert('Turntable exported as MP4!');
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
alert(`Error: ${e.message}`);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// Theme toggle
|
|
1464
|
+
panel.querySelector('#theme-select').addEventListener('change', async (e) => {
|
|
1465
|
+
try {
|
|
1466
|
+
await window.cycleCAD.kernel.exec('render.setTheme', {
|
|
1467
|
+
theme: e.target.value
|
|
1468
|
+
});
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
alert(`Error: ${e.message}`);
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
// Ground plane toggle
|
|
1475
|
+
panel.querySelector('#ground-plane-toggle').addEventListener('change', async (e) => {
|
|
1476
|
+
try {
|
|
1477
|
+
await window.cycleCAD.kernel.exec('render.setGroundPlane', {
|
|
1478
|
+
visible: e.target.checked
|
|
1479
|
+
});
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
alert(`Error: ${e.message}`);
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
},
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Populate materials list in panel.
|
|
1488
|
+
* @param {HTMLElement} panel - Panel element
|
|
1489
|
+
* @param {string} category - Material category filter (optional)
|
|
1490
|
+
* @private
|
|
1491
|
+
*/
|
|
1492
|
+
_populateMaterials(panel, category = 'metal') {
|
|
1493
|
+
const list = panel.querySelector('#material-list');
|
|
1494
|
+
list.innerHTML = '';
|
|
1495
|
+
|
|
1496
|
+
Object.entries(this._materials).forEach(([id, mat]) => {
|
|
1497
|
+
if (mat.category !== category) return;
|
|
1498
|
+
|
|
1499
|
+
const btn = document.createElement('button');
|
|
1500
|
+
btn.className = 'btn btn-secondary';
|
|
1501
|
+
btn.style.cssText = 'width: 100%; text-align: left; margin-bottom: 6px;';
|
|
1502
|
+
btn.textContent = mat.name;
|
|
1503
|
+
|
|
1504
|
+
btn.addEventListener('click', async () => {
|
|
1505
|
+
try {
|
|
1506
|
+
await window.cycleCAD.kernel.exec('render.applyMaterial', {
|
|
1507
|
+
bodyId: window.cycleCAD.kernel._selectedMesh,
|
|
1508
|
+
materialId: id
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
// Update sliders to reflect material
|
|
1512
|
+
panel.querySelector('#material-metalness').value = mat.metalness;
|
|
1513
|
+
panel.querySelector('#material-metalness-value').textContent = mat.metalness.toFixed(1);
|
|
1514
|
+
panel.querySelector('#material-roughness').value = mat.roughness;
|
|
1515
|
+
panel.querySelector('#material-roughness-value').textContent = mat.roughness.toFixed(1);
|
|
1516
|
+
panel.querySelector('#material-color').value = '#' + mat.color.toString(16).padStart(6, '0');
|
|
1517
|
+
|
|
1518
|
+
alert(`Applied: ${mat.name}`);
|
|
1519
|
+
} catch (e) {
|
|
1520
|
+
alert(`Error: ${e.message}`);
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
list.appendChild(btn);
|
|
1525
|
+
});
|
|
1526
|
+
},
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* ============================================================================
|
|
1530
|
+
* HELP ENTRIES
|
|
1531
|
+
* ============================================================================
|
|
1532
|
+
*/
|
|
1533
|
+
|
|
1534
|
+
helpEntries: [
|
|
1535
|
+
{
|
|
1536
|
+
id: 'rendering-materials',
|
|
1537
|
+
title: 'Materials & PBR',
|
|
1538
|
+
category: 'Visualize',
|
|
1539
|
+
description: 'Apply physically-based rendering materials to bodies.',
|
|
1540
|
+
shortcut: 'View → Rendering',
|
|
1541
|
+
details: `
|
|
1542
|
+
<h4>Overview</h4>
|
|
1543
|
+
<p>The material library contains 20+ physically-based rendering (PBR) materials with accurate metalness and roughness values.</p>
|
|
1544
|
+
|
|
1545
|
+
<h4>Material Categories</h4>
|
|
1546
|
+
<ul>
|
|
1547
|
+
<li><strong>Metals:</strong> Steel, aluminum, copper, brass, titanium, gold</li>
|
|
1548
|
+
<li><strong>Plastics:</strong> ABS, polycarbonate, nylon, rubber</li>
|
|
1549
|
+
<li><strong>Wood:</strong> Oak, walnut, maple</li>
|
|
1550
|
+
<li><strong>Glass:</strong> Clear, tinted</li>
|
|
1551
|
+
<li><strong>Carbon Fiber</strong></li>
|
|
1552
|
+
<li><strong>Paint:</strong> Matte and gloss finishes</li>
|
|
1553
|
+
</ul>
|
|
1554
|
+
|
|
1555
|
+
<h4>Fine-Tuning</h4>
|
|
1556
|
+
<p>After applying a material, adjust:</p>
|
|
1557
|
+
<ul>
|
|
1558
|
+
<li><strong>Metalness:</strong> 0 (non-metal) to 1 (pure metal)</li>
|
|
1559
|
+
<li><strong>Roughness:</strong> 0 (mirror polish) to 1 (matte)</li>
|
|
1560
|
+
<li><strong>Color:</strong> Override material color</li>
|
|
1561
|
+
<li><strong>Emit:</strong> Add glow for neon or LED effects</li>
|
|
1562
|
+
</ul>
|
|
1563
|
+
`
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
id: 'rendering-environments',
|
|
1567
|
+
title: 'HDRI Environments',
|
|
1568
|
+
category: 'Visualize',
|
|
1569
|
+
description: 'Set background lighting and reflections.',
|
|
1570
|
+
details: `
|
|
1571
|
+
<h4>Available Environments</h4>
|
|
1572
|
+
<ul>
|
|
1573
|
+
<li><strong>Studio:</strong> Neutral white lighting, good for product shots</li>
|
|
1574
|
+
<li><strong>Sunset:</strong> Warm orange tones, dramatic shadows</li>
|
|
1575
|
+
<li><strong>Outdoor:</strong> Bright blue sky, natural daylight</li>
|
|
1576
|
+
<li><strong>Warehouse:</strong> Dim industrial lighting</li>
|
|
1577
|
+
<li><strong>Night:</strong> Dark blue background for dramatic effect</li>
|
|
1578
|
+
</ul>
|
|
1579
|
+
|
|
1580
|
+
<h4>Intensity Control</h4>
|
|
1581
|
+
<p>Adjust brightness of the environment from 0.5 (dark) to 2.0 (very bright).</p>
|
|
1582
|
+
`
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
id: 'rendering-export',
|
|
1586
|
+
title: 'Export Screenshots & Videos',
|
|
1587
|
+
category: 'Visualize',
|
|
1588
|
+
description: 'Create high-quality renders and videos.',
|
|
1589
|
+
details: `
|
|
1590
|
+
<h4>Screenshots</h4>
|
|
1591
|
+
<ul>
|
|
1592
|
+
<li><strong>1920x1080:</strong> Web quality</li>
|
|
1593
|
+
<li><strong>3840x2160 (4K):</strong> Detailed presentation</li>
|
|
1594
|
+
<li><strong>7680x4320 (8K):</strong> Ultra-high resolution</li>
|
|
1595
|
+
</ul>
|
|
1596
|
+
<p>DPI options: 72 (screen), 150 (web), 300 (print quality).</p>
|
|
1597
|
+
|
|
1598
|
+
<h4>Turntable Videos</h4>
|
|
1599
|
+
<p>Record your model rotating automatically. Set RPM and duration, then click Start Recording.</p>
|
|
1600
|
+
<p>Output is MP4 format, suitable for presentations and social media.</p>
|
|
1601
|
+
`
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
id: 'rendering-raytracing',
|
|
1605
|
+
title: 'Ray Tracing & Path Tracing',
|
|
1606
|
+
category: 'Visualize',
|
|
1607
|
+
description: 'Photo-realistic rendering with global illumination.',
|
|
1608
|
+
shortcut: 'View → Rendering → Advanced',
|
|
1609
|
+
details: `
|
|
1610
|
+
<h4>Path Tracing</h4>
|
|
1611
|
+
<p>Achieves photorealistic results by simulating light bouncing through the scene.</p>
|
|
1612
|
+
|
|
1613
|
+
<h4>Configuration</h4>
|
|
1614
|
+
<ul>
|
|
1615
|
+
<li><strong>Samples:</strong> 256-1024 (higher = cleaner)</li>
|
|
1616
|
+
<li><strong>Bounces:</strong> 4-8 (light reflections)</li>
|
|
1617
|
+
<li><strong>Denoise:</strong> Reduces noise with AI</li>
|
|
1618
|
+
</ul>
|
|
1619
|
+
|
|
1620
|
+
<h4>Quality Presets</h4>
|
|
1621
|
+
<ul>
|
|
1622
|
+
<li><strong>Draft:</strong> 16 samples, 10s timeout</li>
|
|
1623
|
+
<li><strong>Standard:</strong> 256 samples, 120s timeout</li>
|
|
1624
|
+
<li><strong>High:</strong> 1024 samples, 10min timeout</li>
|
|
1625
|
+
</ul>
|
|
1626
|
+
`
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
id: 'rendering-lighting',
|
|
1630
|
+
title: 'Custom Lighting Control',
|
|
1631
|
+
category: 'Visualize',
|
|
1632
|
+
description: 'Position lights with temperature and shadow control.',
|
|
1633
|
+
shortcut: 'View → Rendering → Lighting',
|
|
1634
|
+
details: `
|
|
1635
|
+
<h4>Light Types</h4>
|
|
1636
|
+
<ul>
|
|
1637
|
+
<li><strong>Directional:</strong> Sun-like light (parallel rays)</li>
|
|
1638
|
+
<li><strong>Point:</strong> Omni-directional from a point</li>
|
|
1639
|
+
<li><strong>Spot:</strong> Cone-shaped spotlight</li>
|
|
1640
|
+
<li><strong>Area:</strong> Soft rectangular light source</li>
|
|
1641
|
+
</ul>
|
|
1642
|
+
|
|
1643
|
+
<h4>Color Temperature</h4>
|
|
1644
|
+
<p>Set in Kelvin: 3000K (warm) to 7000K (cool)</p>
|
|
1645
|
+
|
|
1646
|
+
<h4>Shadow Maps</h4>
|
|
1647
|
+
<p>Higher resolution (2048px) = softer, more realistic shadows</p>
|
|
1648
|
+
`
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
id: 'rendering-camera',
|
|
1652
|
+
title: 'Camera Optics & DOF',
|
|
1653
|
+
category: 'Visualize',
|
|
1654
|
+
description: 'Control focal length, aperture, and depth of field.',
|
|
1655
|
+
shortcut: 'View → Rendering → Camera',
|
|
1656
|
+
details: `
|
|
1657
|
+
<h4>Focal Length</h4>
|
|
1658
|
+
<ul>
|
|
1659
|
+
<li><strong>18-35mm:</strong> Wide-angle, expansive feel</li>
|
|
1660
|
+
<li><strong>50mm:</strong> Standard (human eye)</li>
|
|
1661
|
+
<li><strong>85-200mm:</strong> Telephoto, compressed perspective</li>
|
|
1662
|
+
</ul>
|
|
1663
|
+
|
|
1664
|
+
<h4>Aperture (f-stop)</h4>
|
|
1665
|
+
<ul>
|
|
1666
|
+
<li><strong>f/1.4:</strong> Wide aperture, shallow DOF</li>
|
|
1667
|
+
<li><strong>f/8:</strong> Balanced depth</li>
|
|
1668
|
+
<li><strong>f/22:</strong> Deep DOF, everything in focus</li>
|
|
1669
|
+
</ul>
|
|
1670
|
+
|
|
1671
|
+
<h4>Exposure Compensation</h4>
|
|
1672
|
+
<p>Adjust brightness: -2 to +2 EV</p>
|
|
1673
|
+
`
|
|
1674
|
+
},
|
|
1675
|
+
{
|
|
1676
|
+
id: 'rendering-pbr',
|
|
1677
|
+
title: 'PBR Materials (150+)',
|
|
1678
|
+
category: 'Visualize',
|
|
1679
|
+
description: 'Physically-based material library with extended options.',
|
|
1680
|
+
shortcut: 'View → Rendering → Materials',
|
|
1681
|
+
details: `
|
|
1682
|
+
<h4>Material Categories</h4>
|
|
1683
|
+
<ul>
|
|
1684
|
+
<li><strong>Metals:</strong> Steel, aluminum, copper, brass, titanium, chrome, gold, silver</li>
|
|
1685
|
+
<li><strong>Plastics:</strong> ABS, polycarbonate, nylon, rubber, PLA, PETG</li>
|
|
1686
|
+
<li><strong>Composites:</strong> Carbon fiber, fiberglass, Kevlar</li>
|
|
1687
|
+
<li><strong>Wood:</strong> Oak, walnut, maple, birch, plywood</li>
|
|
1688
|
+
<li><strong>Glass:</strong> Clear, tinted, frosted</li>
|
|
1689
|
+
<li><strong>Ceramics & Stone:</strong> Granite, marble, porcelain</li>
|
|
1690
|
+
<li><strong>Paint:</strong> Matte, gloss, metallic finishes</li>
|
|
1691
|
+
<li><strong>Fabric & Rubber</strong></li>
|
|
1692
|
+
</ul>
|
|
1693
|
+
|
|
1694
|
+
<h4>Fine-Tuning</h4>
|
|
1695
|
+
<p>Adjust metalness (0-1) and roughness (0-1) for each material individually.</p>
|
|
1696
|
+
`
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
id: 'rendering-decals',
|
|
1700
|
+
title: 'Decals & Logos',
|
|
1701
|
+
category: 'Visualize',
|
|
1702
|
+
description: 'Apply images and logos to model surfaces.',
|
|
1703
|
+
shortcut: 'View → Rendering → Decals',
|
|
1704
|
+
details: `
|
|
1705
|
+
<h4>Adding Decals</h4>
|
|
1706
|
+
<p>1. Select a body or face in your model</p>
|
|
1707
|
+
<p>2. Upload an image file (PNG, JPG)</p>
|
|
1708
|
+
<p>3. Position using X/Y offset and rotation</p>
|
|
1709
|
+
<p>4. Scale for proper size</p>
|
|
1710
|
+
<p>5. Adjust opacity for transparency</p>
|
|
1711
|
+
|
|
1712
|
+
<h4>Use Cases</h4>
|
|
1713
|
+
<ul>
|
|
1714
|
+
<li>Company logos on products</li>
|
|
1715
|
+
<li>Branding and packaging</li>
|
|
1716
|
+
<li>Safety labels and warnings</li>
|
|
1717
|
+
<li>Part numbers and serial codes</li>
|
|
1718
|
+
</ul>
|
|
1719
|
+
`
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
id: 'rendering-hdri',
|
|
1723
|
+
title: 'HDRI Environments',
|
|
1724
|
+
category: 'Visualize',
|
|
1725
|
+
description: 'Image-based lighting with 12+ built-in environments.',
|
|
1726
|
+
shortcut: 'View → Rendering → Environment',
|
|
1727
|
+
details: `
|
|
1728
|
+
<h4>Built-in Environments</h4>
|
|
1729
|
+
<ul>
|
|
1730
|
+
<li><strong>Studio:</strong> Controlled, neutral lighting</li>
|
|
1731
|
+
<li><strong>Sunset:</strong> Warm, dramatic golden hour</li>
|
|
1732
|
+
<li><strong>Outdoor:</strong> Bright natural daylight</li>
|
|
1733
|
+
<li><strong>Warehouse:</strong> Industrial, diffuse lighting</li>
|
|
1734
|
+
<li><strong>Night:</strong> Dark with selective illumination</li>
|
|
1735
|
+
</ul>
|
|
1736
|
+
|
|
1737
|
+
<h4>Intensity Control</h4>
|
|
1738
|
+
<p>Adjust environment brightness from 0.5x to 2.0x</p>
|
|
1739
|
+
|
|
1740
|
+
<h4>Custom Environments</h4>
|
|
1741
|
+
<p>Import your own HDR images for full creative control</p>
|
|
1742
|
+
`
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
id: 'rendering-exr',
|
|
1746
|
+
title: 'EXR & HDR Export',
|
|
1747
|
+
category: 'Visualize',
|
|
1748
|
+
description: 'Export high dynamic range images for post-processing.',
|
|
1749
|
+
shortcut: 'File → Export → EXR',
|
|
1750
|
+
details: `
|
|
1751
|
+
<h4>OpenEXR Format</h4>
|
|
1752
|
+
<p>Professional HDR format with full color depth (32-bit float)</p>
|
|
1753
|
+
|
|
1754
|
+
<h4>Advantages</h4>
|
|
1755
|
+
<ul>
|
|
1756
|
+
<li>Preserve all lighting information</li>
|
|
1757
|
+
<li>Non-destructive post-processing in Photoshop, Nuke, etc.</li>
|
|
1758
|
+
<li>Blend renders in compositing software</li>
|
|
1759
|
+
</ul>
|
|
1760
|
+
|
|
1761
|
+
<h4>Resolution Options</h4>
|
|
1762
|
+
<p>1920x1080 (Full HD) to 7680x4320 (8K)</p>
|
|
1763
|
+
`
|
|
1764
|
+
}
|
|
1765
|
+
]
|
|
1766
|
+
};
|