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.
- package/IMPLEMENTATION_GUIDE.md +502 -0
- package/INTEGRATION-GUIDE.md +377 -0
- package/MODULES_PHASES_6_7.md +780 -0
- package/app/index.html +106 -2
- 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 +967 -0
- package/app/js/modules/assembly-module.js +47 -3
- package/app/js/modules/cam-module.js +1067 -0
- package/app/js/modules/collaboration-module.js +1102 -0
- package/app/js/modules/data-module.js +1656 -0
- package/app/js/modules/drawing-module.js +54 -8
- package/app/js/modules/formats-module.js +1173 -0
- package/app/js/modules/inspection-module.js +937 -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 +957 -0
- package/app/js/modules/rendering-module.js +1306 -0
- package/app/js/modules/scripting-module.js +955 -0
- package/app/js/modules/simulation-module.js +60 -3
- package/app/js/modules/sketch-module.js +1032 -90
- package/app/js/modules/step-module.js +47 -6
- package/app/js/modules/surface-module.js +728 -0
- package/app/js/modules/version-module.js +1410 -0
- package/app/js/modules/viewport-module.js +95 -8
- package/app/test-agent-v2.html +881 -1316
- 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/.github/scripts/cad-diff.js +0 -590
- 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
|
+
};
|