Skip to main content

dioxus_three/
lib.rs

1//! Dioxus Three - A Three.js component for Dioxus
2//!
3//! Provides a simple component for embedding interactive 3D scenes
4//! using Three.js within Dioxus Desktop applications.
5//!
6//! Supports multiple 3D formats: OBJ, FBX, GLTF, GLB, STL, PLY, and more.
7//! Also supports custom GLSL shaders for advanced visual effects.
8
9use dioxus::prelude::*;
10use std::collections::HashMap;
11
12/// Custom shader configuration
13#[derive(Clone, PartialEq, Debug, Default)]
14pub struct ShaderConfig {
15    /// Vertex shader GLSL code (optional - uses default if not provided)
16    pub vertex_shader: Option<String>,
17    /// Fragment shader GLSL code (optional - uses default if not provided)
18    pub fragment_shader: Option<String>,
19    /// Uniform values to pass to shaders (float values)
20    pub uniforms: HashMap<String, f32>,
21    /// Time-based animation (automatically sets `u_time` uniform)
22    pub animated: bool,
23}
24
25/// Built-in shader presets
26#[derive(Clone, PartialEq, Debug)]
27pub enum ShaderPreset {
28    /// No custom shader (default StandardMaterial)
29    None,
30    /// Animated gradient
31    Gradient,
32    /// Water/wave effect
33    Water,
34    /// Hologram effect
35    Hologram,
36    /// Toon/cel shading
37    Toon,
38    /// Heat map visualization
39    Heatmap,
40    /// Custom shader with provided config
41    Custom(ShaderConfig),
42}
43
44/// 3D model format types
45#[derive(Clone, PartialEq, Debug)]
46pub enum ModelFormat {
47    /// Wavefront OBJ format
48    Obj,
49    /// Autodesk FBX format
50    Fbx,
51    /// glTF 2.0 format (JSON)
52    Gltf,
53    /// glTF 2.0 binary format
54    Glb,
55    /// STL format (StereoLithography)
56    Stl,
57    /// Stanford PLY format
58    Ply,
59    /// Collada DAE format
60    Dae,
61    /// Three.js JSON format
62    Json,
63    /// Default cube (no file)
64    Cube,
65}
66
67impl ModelFormat {
68    /// Get the format identifier string
69    pub fn as_str(&self) -> &'static str {
70        match self {
71            ModelFormat::Obj => "obj",
72            ModelFormat::Fbx => "fbx",
73            ModelFormat::Gltf => "gltf",
74            ModelFormat::Glb => "glb",
75            ModelFormat::Stl => "stl",
76            ModelFormat::Ply => "ply",
77            ModelFormat::Dae => "dae",
78            ModelFormat::Json => "json",
79            ModelFormat::Cube => "cube",
80        }
81    }
82
83    fn loader_js(&self) -> &'static str {
84        match self {
85            ModelFormat::Obj => "OBJLoader",
86            ModelFormat::Fbx => "FBXLoader",
87            ModelFormat::Gltf | ModelFormat::Glb => "GLTFLoader",
88            ModelFormat::Stl => "STLLoader",
89            ModelFormat::Ply => "PLYLoader",
90            ModelFormat::Dae => "ColladaLoader",
91            ModelFormat::Json => "ObjectLoader",
92            ModelFormat::Cube => "",
93        }
94    }
95
96    fn loader_url(&self) -> &'static str {
97        match self {
98            ModelFormat::Obj => {
99                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/OBJLoader.js"
100            }
101            ModelFormat::Fbx => {
102                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/FBXLoader.js"
103            }
104            ModelFormat::Gltf | ModelFormat::Glb => {
105                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/GLTFLoader.js"
106            }
107            ModelFormat::Stl => {
108                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/STLLoader.js"
109            }
110            ModelFormat::Ply => {
111                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/PLYLoader.js"
112            }
113            ModelFormat::Dae => {
114                "https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/loaders/ColladaLoader.js"
115            }
116            ModelFormat::Json => "",
117            ModelFormat::Cube => "",
118        }
119    }
120
121    /// Get additional dependency URLs required by this loader
122    fn extra_scripts(&self) -> Vec<&'static str> {
123        match self {
124            // FBXLoader requires fflate for decompression
125            ModelFormat::Fbx => vec!["https://cdn.jsdelivr.net/npm/fflate@0.8.0/umd/index.js"],
126            _ => vec![],
127        }
128    }
129}
130
131/// Configuration for a single model in a multi-model scene
132#[derive(Clone, PartialEq, Debug)]
133pub struct ModelConfig {
134    /// URL or path to the model file
135    pub url: String,
136    /// Format of the model
137    pub format: ModelFormat,
138    /// Position X
139    pub pos_x: f32,
140    /// Position Y
141    pub pos_y: f32,
142    /// Position Z
143    pub pos_z: f32,
144    /// Rotation X (degrees)
145    pub rot_x: f32,
146    /// Rotation Y (degrees)
147    pub rot_y: f32,
148    /// Rotation Z (degrees)
149    pub rot_z: f32,
150    /// Scale
151    pub scale: f32,
152    /// Color (hex string)
153    pub color: String,
154}
155
156impl Default for ModelConfig {
157    fn default() -> Self {
158        Self {
159            url: String::new(),
160            format: ModelFormat::Cube,
161            pos_x: 0.0,
162            pos_y: 0.0,
163            pos_z: 0.0,
164            rot_x: 0.0,
165            rot_y: 0.0,
166            rot_z: 0.0,
167            scale: 1.0,
168            color: "#ff6b6b".to_string(),
169        }
170    }
171}
172
173impl ModelConfig {
174    /// Create a new model config with just URL and format
175    pub fn new(url: impl Into<String>, format: ModelFormat) -> Self {
176        Self {
177            url: url.into(),
178            format,
179            ..Default::default()
180        }
181    }
182
183    /// Set position
184    pub fn with_position(mut self, x: f32, y: f32, z: f32) -> Self {
185        self.pos_x = x;
186        self.pos_y = y;
187        self.pos_z = z;
188        self
189    }
190
191    /// Set rotation
192    pub fn with_rotation(mut self, x: f32, y: f32, z: f32) -> Self {
193        self.rot_x = x;
194        self.rot_y = y;
195        self.rot_z = z;
196        self
197    }
198
199    /// Set scale
200    pub fn with_scale(mut self, scale: f32) -> Self {
201        self.scale = scale;
202        self
203    }
204
205    /// Set color
206    pub fn with_color(mut self, color: impl Into<String>) -> Self {
207        self.color = color.into();
208        self
209    }
210}
211
212/// Properties for the ThreeView component
213#[derive(Props, Clone, PartialEq)]
214pub struct ThreeViewProps {
215    /// Model file path or URL (optional - uses cube if not provided)
216    #[props(default = None)]
217    pub model_url: Option<String>,
218    /// Model format
219    #[props(default = ModelFormat::Cube)]
220    pub format: ModelFormat,
221    /// Model position X
222    #[props(default = 0.0)]
223    pub pos_x: f32,
224    /// Model position Y
225    #[props(default = 0.0)]
226    pub pos_y: f32,
227    /// Model position Z
228    #[props(default = 0.0)]
229    pub pos_z: f32,
230    /// Model rotation X (degrees)
231    #[props(default = 0.0)]
232    pub rot_x: f32,
233    /// Model rotation Y (degrees)
234    #[props(default = 0.0)]
235    pub rot_y: f32,
236    /// Model rotation Z (degrees)
237    #[props(default = 0.0)]
238    pub rot_z: f32,
239    /// Model scale
240    #[props(default = 1.0)]
241    pub scale: f32,
242    /// Model color/material (hex string like "#ff6b6b")
243    #[props(default = "#ff6b6b".to_string())]
244    pub color: String,
245    /// Multiple models to load (optional - if set, model_url/format are ignored)
246    #[props(default = Vec::new())]
247    pub models: Vec<ModelConfig>,
248    /// Auto-center the model
249    #[props(default = true)]
250    pub auto_center: bool,
251    /// Auto-scale to fit viewport
252    #[props(default = false)]
253    pub auto_scale: bool,
254    /// Camera position X
255    #[props(default = 5.0)]
256    pub cam_x: f32,
257    /// Camera position Y
258    #[props(default = 5.0)]
259    pub cam_y: f32,
260    /// Camera position Z
261    #[props(default = 5.0)]
262    pub cam_z: f32,
263    /// Camera target X
264    #[props(default = 0.0)]
265    pub target_x: f32,
266    /// Camera target Y
267    #[props(default = 0.0)]
268    pub target_y: f32,
269    /// Camera target Z
270    #[props(default = 0.0)]
271    pub target_z: f32,
272    /// Auto-rotate the model
273    #[props(default = true)]
274    pub auto_rotate: bool,
275    /// Auto-rotation speed
276    #[props(default = 1.0)]
277    pub rot_speed: f32,
278    /// Show grid helper
279    #[props(default = true)]
280    pub show_grid: bool,
281    /// Show axes helper
282    #[props(default = true)]
283    pub show_axes: bool,
284    /// Background color
285    #[props(default = "#1a1a2e".to_string())]
286    pub background: String,
287    /// Additional CSS class for the container
288    #[props(default = String::new())]
289    pub class: String,
290    /// Enable shadows
291    #[props(default = true)]
292    pub shadows: bool,
293    /// Wireframe mode
294    #[props(default = false)]
295    pub wireframe: bool,
296    /// Shader preset or custom shader
297    #[props(default = ShaderPreset::None)]
298    pub shader: ShaderPreset,
299}
300
301impl ShaderPreset {
302    /// Get the vertex shader code for this preset
303    fn vertex_shader(&self) -> Option<String> {
304        match self {
305            ShaderPreset::None => None,
306            ShaderPreset::Gradient => Some(include_str!("shaders/gradient.vert").to_string()),
307            ShaderPreset::Water => Some(include_str!("shaders/water.vert").to_string()),
308            ShaderPreset::Hologram => Some(include_str!("shaders/hologram.vert").to_string()),
309            ShaderPreset::Toon => Some(include_str!("shaders/toon.vert").to_string()),
310            ShaderPreset::Heatmap => Some(include_str!("shaders/heatmap.vert").to_string()),
311            ShaderPreset::Custom(config) => config.vertex_shader.clone(),
312        }
313    }
314
315    /// Get the fragment shader code for this preset
316    fn fragment_shader(&self) -> Option<String> {
317        match self {
318            ShaderPreset::None => None,
319            ShaderPreset::Gradient => Some(include_str!("shaders/gradient.frag").to_string()),
320            ShaderPreset::Water => Some(include_str!("shaders/water.frag").to_string()),
321            ShaderPreset::Hologram => Some(include_str!("shaders/hologram.frag").to_string()),
322            ShaderPreset::Toon => Some(include_str!("shaders/toon.frag").to_string()),
323            ShaderPreset::Heatmap => Some(include_str!("shaders/heatmap.frag").to_string()),
324            ShaderPreset::Custom(config) => config.fragment_shader.clone(),
325        }
326    }
327
328    /// Check if this shader uses time animation
329    fn is_animated(&self) -> bool {
330        match self {
331            ShaderPreset::None => false,
332            ShaderPreset::Gradient | ShaderPreset::Water | ShaderPreset::Hologram => true,
333            ShaderPreset::Custom(config) => config.animated,
334            _ => false,
335        }
336    }
337}
338
339/// A Three.js 3D viewer component for Dioxus
340#[component]
341pub fn ThreeView(props: ThreeViewProps) -> Element {
342    let html = generate_three_js_html(&props);
343
344    rsx! {
345        iframe {
346            class: "{props.class}",
347            style: "width: 100%; height: 100%; border: none;",
348            srcdoc: "{html}",
349        }
350    }
351}
352
353/// Build loader scripts for multiple models
354fn build_loader_scripts_for_models(models: &[ModelConfig]) -> String {
355    let mut scripts: Vec<String> = vec![];
356    let mut seen_formats: Vec<ModelFormat> = vec![];
357
358    for model in models {
359        if seen_formats.contains(&model.format) {
360            continue;
361        }
362        seen_formats.push(model.format.clone());
363
364        let loader_url = model.format.loader_url();
365        if loader_url.is_empty() {
366            continue;
367        }
368
369        for extra in model.format.extra_scripts() {
370            let script = format!(r#"<script src="{}"></script>"#, extra);
371            if !scripts.contains(&script) {
372                scripts.push(script);
373            }
374        }
375
376        scripts.push(format!(r#"<script src="{}"></script>"#, loader_url));
377    }
378
379    scripts.join("\n    ")
380}
381
382/// Build loader scripts for single model
383fn build_loader_scripts_single(format: &ModelFormat, model_url: &Option<String>) -> String {
384    let url = model_url.clone().unwrap_or_default();
385    let has_model = !url.is_empty() && *format != ModelFormat::Cube;
386    let loader_url = format.loader_url();
387
388    if !has_model || loader_url.is_empty() {
389        return String::new();
390    }
391
392    let mut scripts: Vec<String> = format
393        .extra_scripts()
394        .iter()
395        .map(|url| format!(r#"<script src="{}"></script>"#, url))
396        .collect();
397
398    scripts.push(format!(r#"<script src="{}"></script>"#, loader_url));
399    scripts.join("\n    ")
400}
401
402/// Build JavaScript code for loading multiple models
403fn build_multi_model_loading(models: &[ModelConfig], shadows: bool) -> String {
404    let shadows_str = shadows.to_string().to_lowercase();
405
406    let load_calls: Vec<String> = models.iter().enumerate().map(|(idx, model)| {
407        let loader_class = model.format.loader_js();
408        let is_geometry_loader = matches!(model.format, ModelFormat::Stl | ModelFormat::Ply);
409        let url = &model.url;
410        let pos_x = model.pos_x;
411        let pos_y = model.pos_y;
412        let pos_z = model.pos_z;
413        let rot_x = model.rot_x.to_radians();
414        let rot_y = model.rot_y.to_radians();
415        let rot_z = model.rot_z.to_radians();
416        let scale = model.scale;
417        let color = &model.color;
418        let default_color = "#ff6b6b";
419
420        if model.format == ModelFormat::Cube {
421            format!(
422                r#"(function() {{ const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshStandardMaterial({{ color: "{color}", roughness: 0.5, metalness: 0.3 }}); const mesh = new THREE.Mesh(geometry, material); mesh.position.set({pos_x}, {pos_y}, {pos_z}); mesh.rotation.set({rot_x}, {rot_y}, {rot_z}); mesh.scale.setScalar({scale}); mesh.castShadow = {shadows_str}; mesh.receiveShadow = {shadows_str}; modelContainer.add(mesh); }})();"#
423            )
424        } else if is_geometry_loader {
425            format!(
426                r#"(function() {{ const loader = new THREE.{loader_class}(); loader.load("{url}", function(geometry) {{ const material = new THREE.MeshStandardMaterial({{ color: "{color}", roughness: 0.5, metalness: 0.1, side: THREE.DoubleSide }}); const mesh = new THREE.Mesh(geometry, material); mesh.position.set({pos_x}, {pos_y}, {pos_z}); mesh.rotation.set({rot_x}, {rot_y}, {rot_z}); mesh.scale.setScalar({scale}); mesh.castShadow = {shadows_str}; mesh.receiveShadow = {shadows_str}; modelContainer.add(mesh); }}, undefined, function(err) {{ console.error('Failed to load model {idx}:', err); }}); }})();"#
427            )
428        } else {
429            let color_js = if color != default_color {
430                format!(
431                    r#"if (child.material) {{ if (Array.isArray(child.material)) {{ child.material.forEach(m => m.color.set("{color}")); }} else {{ child.material.color.set("{color}"); }} }}"#,
432                    color = color
433                )
434            } else {
435                String::new()
436            };
437            format!(
438                r#"(function() {{ const loader = new THREE.{loader_class}(); loader.load("{url}", function(object) {{ let model = object.scene || object.dae || object; model.position.set({pos_x}, {pos_y}, {pos_z}); model.rotation.set({rot_x}, {rot_y}, {rot_z}); model.scale.setScalar({scale}); model.traverse(function(child) {{ if (child.isMesh) {{ child.castShadow = {shadows_str}; child.receiveShadow = {shadows_str}; {color_js} }} }}); modelContainer.add(model); }}, undefined, function(err) {{ console.error('Failed to load model {idx}:', err); }}); }})();"#,
439                loader_class = loader_class,
440                url = url,
441                pos_x = pos_x,
442                pos_y = pos_y,
443                pos_z = pos_z,
444                rot_x = rot_x,
445                rot_y = rot_y,
446                rot_z = rot_z,
447                scale = scale,
448                shadows_str = shadows_str,
449                color_js = color_js,
450                idx = idx
451            )
452        }
453    }).collect();
454
455    format!("loadingEl.style.display = 'none'; {}", load_calls.join(" "))
456}
457
458/// Build JavaScript code for loading a single model
459fn build_single_model_loading(
460    format: &ModelFormat,
461    model_url: &Option<String>,
462    auto_center: bool,
463    auto_scale: bool,
464    shadows: bool,
465) -> String {
466    let url = model_url.clone().unwrap_or_default();
467    let has_model = !url.is_empty() && *format != ModelFormat::Cube;
468    let loader_class = format.loader_js();
469    let is_geometry_loader = matches!(format, ModelFormat::Stl | ModelFormat::Ply);
470    let auto_center_str = auto_center.to_string().to_lowercase();
471    let auto_scale_str = auto_scale.to_string().to_lowercase();
472    let shadows_str = shadows.to_string().to_lowercase();
473
474    if !has_model {
475        return "const geometry = new THREE.BoxGeometry(1, 1, 1); let material = new THREE.MeshStandardMaterial({ color: state.color, roughness: 0.5, metalness: 0.3, wireframe: state.wireframe }); model = new THREE.Mesh(geometry, material); model.castShadow = true; model.receiveShadow = true; modelContainer.add(model); loadingEl.style.display = 'none';".to_string();
476    }
477
478    if is_geometry_loader {
479        format!(
480            r#"const loader = new THREE.{loader_class}(); loader.load("{url}", function(geometry) {{ loadingEl.style.display = 'none'; const material = new THREE.MeshStandardMaterial({{ color: state.color, roughness: 0.5, metalness: 0.1, wireframe: state.wireframe, side: THREE.DoubleSide }}); model = new THREE.Mesh(geometry, material); model.castShadow = {shadows_str}; model.receiveShadow = {shadows_str}; if ({auto_center_str}) {{ const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); }} if ({auto_scale_str}) {{ const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); if (maxDim > 0) {{ const s = 2 / maxDim; model.scale.setScalar(s); }} }} modelContainer.add(model); updateTransform(); }}, function(xhr) {{ const percent = xhr.loaded / xhr.total * 100; loadingEl.textContent = 'Loading: ' + Math.round(percent) + '%'; }}, function(error) {{ console.error('Error loading model:', error); loadingEl.style.display = 'none'; errorEl.style.display = 'block'; errorEl.textContent = 'Failed to load model: ' + (error.message || 'Unknown error'); const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshStandardMaterial({{ color: 0xff6b6b }}); model = new THREE.Mesh(geometry, material); modelContainer.add(model); }});"#
481        )
482    } else {
483        format!(
484            r#"const loader = new THREE.{loader_class}(); loader.load("{url}", function(object) {{ loadingEl.style.display = 'none'; if (object.scene) {{ model = object.scene; }} else if (object.dae) {{ model = object.scene; }} else {{ model = object; }} model.traverse(function(child) {{ if (child.isMesh) {{ child.castShadow = {shadows_str}; child.receiveShadow = {shadows_str}; if (!child.material) {{ child.material = new THREE.MeshStandardMaterial({{ color: state.color, roughness: 0.5, metalness: 0.3 }}); }} const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach(m => {{ if (m.opacity !== undefined && m.opacity < 0.1) m.opacity = 1.0; if (m.transparent === true && m.opacity < 0.1) m.transparent = false; if (state.color !== '#ff6b6b' && m.color) {{ m.color.set(state.color); }} m.wireframe = state.wireframe; }}); }} }}); if ({auto_center_str}) {{ const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); }} if ({auto_scale_str}) {{ const box = new THREE.Box3().setFromObject(model); const size = box.getSize(new THREE.Vector3()); const maxDim = Math.max(size.x, size.y, size.z); if (maxDim > 0) {{ const s = 2 / maxDim; model.scale.setScalar(s); }} }} modelContainer.add(model); updateTransform(); }}, function(xhr) {{ const percent = xhr.loaded / xhr.total * 100; loadingEl.textContent = 'Loading: ' + Math.round(percent) + '%'; }}, function(error) {{ console.error('Error loading model:', error); loadingEl.style.display = 'none'; errorEl.style.display = 'block'; errorEl.textContent = 'Failed to load model: ' + (error.message || 'Unknown error'); const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshStandardMaterial({{ color: 0xff6b6b }}); model = new THREE.Mesh(geometry, material); modelContainer.add(model); }});"#
485        )
486    }
487}
488
489/// Generate the HTML with embedded Three.js
490fn generate_three_js_html(props: &ThreeViewProps) -> String {
491    let rot_x_rad = props.rot_x.to_radians();
492    let rot_y_rad = props.rot_y.to_radians();
493    let rot_z_rad = props.rot_z.to_radians();
494
495    // Legacy single-model variables (for backward compatibility with template)
496    let loader_url = props.format.loader_url();
497    let loader_class = props.format.loader_js();
498    let format_str = props.format.as_str();
499    let model_url = props.model_url.clone().unwrap_or_default();
500    let has_model = !model_url.is_empty() && props.format != ModelFormat::Cube;
501
502    // Check if using multiple models
503    let use_multiple_models = !props.models.is_empty();
504
505    // Build loader script tags
506    let loader_script = if use_multiple_models {
507        build_loader_scripts_for_models(&props.models)
508    } else {
509        build_loader_scripts_single(&props.format, &props.model_url)
510    };
511
512    // Build model loading JavaScript code
513    let model_loading_code = if use_multiple_models {
514        build_multi_model_loading(&props.models, props.shadows)
515    } else {
516        build_single_model_loading(
517            &props.format,
518            &props.model_url,
519            props.auto_center,
520            props.auto_scale,
521            props.shadows,
522        )
523    };
524
525    // Build shader code if needed
526    let (shader_material_code, shader_uniforms, _shader_animated) =
527        build_shader_code(&props.shader);
528
529    // Build the HTML
530    let html = format!(
531        r##"<!DOCTYPE html>
532<html>
533<head>
534    <meta charset="UTF-8">
535    <style>
536        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
537        html, body {{ width: 100%; height: 100%; overflow: hidden; background: {bg}; }}
538        #canvas-container {{ width: 100%; height: 100%; }}
539        canvas {{ display: block; }}
540        #loading {{ 
541            position: absolute; 
542            top: 50%; 
543            left: 50%; 
544            transform: translate(-50%, -50%); 
545            color: white; 
546            font-family: sans-serif;
547            font-size: 14px;
548        }}
549        #error {{
550            position: absolute;
551            top: 50%;
552            left: 50%;
553            transform: translate(-50%, -50%);
554            color: #ff6b6b;
555            font-family: sans-serif;
556            font-size: 14px;
557            text-align: center;
558            display: none;
559        }}
560    </style>
561</head>
562<body>
563    <div id="canvas-container"></div>
564    <div id="loading">Loading 3D model...</div>
565    <div id="error"></div>
566    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
567    {loader_script}
568    <script>
569        console.log("Dioxus Three: Initializing ({fmt})...");
570        
571        const container = document.getElementById('canvas-container');
572        const loadingEl = document.getElementById('loading');
573        const errorEl = document.getElementById('error');
574        const width = container.clientWidth || window.innerWidth;
575        const height = container.clientHeight || window.innerHeight;
576        
577        const scene = new THREE.Scene();
578        scene.background = new THREE.Color('{bg}');
579        
580        const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
581        camera.position.set({cam_x}, {cam_y}, {cam_z});
582        camera.lookAt({target_x}, {target_y}, {target_z});
583        
584        const renderer = new THREE.WebGLRenderer({{ antialias: true }});
585        renderer.setSize(width, height);
586        renderer.setPixelRatio(window.devicePixelRatio);
587        renderer.shadowMap.enabled = {shadows};
588        renderer.shadowMap.type = THREE.PCFSoftShadowMap;
589        container.appendChild(renderer.domElement);
590        
591        // Brighter ambient light for better material visibility
592        const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
593        scene.add(ambientLight);
594        
595        // Main directional light (sun)
596        const dirLight = new THREE.DirectionalLight(0xffffff, 1.2);
597        dirLight.position.set(10, 20, 10);
598        dirLight.castShadow = {shadows};
599        dirLight.shadow.mapSize.width = 2048;
600        dirLight.shadow.mapSize.height = 2048;
601        scene.add(dirLight);
602        
603        // Fill light from opposite side
604        const fillLight = new THREE.DirectionalLight(0xffffff, 0.4);
605        fillLight.position.set(-10, 10, -10);
606        scene.add(fillLight);
607        
608        // Back light for rim lighting
609        const backLight = new THREE.DirectionalLight(0xffffff, 0.3);
610        backLight.position.set(0, 5, -10);
611        scene.add(backLight);
612        
613        if ({show_grid}) {{
614            const gridHelper = new THREE.GridHelper(20, 20, 0x444444, 0x222222);
615            scene.add(gridHelper);
616        }}
617        if ({show_axes}) {{
618            const axesHelper = new THREE.AxesHelper(2);
619            scene.add(axesHelper);
620        }}
621        
622        let model = null;
623        let modelContainer = new THREE.Group();
624        scene.add(modelContainer);
625        
626        let state = {{
627            rotX: {rot_x},
628            rotY: {rot_y},
629            rotZ: {rot_z},
630            scale: {scale},
631            color: "{color}",
632            autoRotate: {auto_rotate},
633            rotSpeed: {rot_speed},
634            wireframe: {wireframe}
635        }};
636        let autoRotY = 0;
637        
638        async function loadModel() {{
639            try {{
640                {model_loading_code}
641            }} catch (e) {{
642                console.error('Error:', e);
643                loadingEl.style.display = 'none';
644                errorEl.style.display = 'block';
645                errorEl.textContent = 'Error: ' + e.message;
646            }}
647        }}
648        
649        function updateTransform() {{
650            if (!modelContainer) return;
651            modelContainer.position.set({pos_x}, {pos_y}, {pos_z});
652            modelContainer.scale.setScalar(state.scale);
653            if (!state.autoRotate) {{
654                modelContainer.rotation.set(state.rotX, state.rotY, state.rotZ);
655            }} else {{
656                modelContainer.rotation.x = state.rotX;
657                modelContainer.rotation.z = state.rotZ;
658            }}
659            modelContainer.traverse(function (child) {{
660                if (child.isMesh && child.material) {{
661                    child.material.wireframe = state.wireframe;
662                }}
663            }});
664        }}
665        
666        window.updateThreeView = function(params) {{
667            state = {{ ...state, ...params }};
668            if (params.camX !== undefined) camera.position.set(state.camX, state.camY, state.camZ);
669            if (params.targetX !== undefined) camera.lookAt(state.targetX, state.targetY, state.targetZ);
670            updateTransform();
671        }};
672        
673        function animate() {{
674            requestAnimationFrame(animate);
675            if (state.autoRotate && modelContainer) {{
676                autoRotY += state.rotSpeed * 0.01;
677                modelContainer.rotation.y = state.rotY + autoRotY;
678            }}
679            {shader_uniforms}
680            renderer.render(scene, camera);
681        }}
682        
683        window.addEventListener('resize', () => {{
684            const w = container.clientWidth || window.innerWidth;
685            const h = container.clientHeight || window.innerHeight;
686            camera.aspect = w / h;
687            camera.updateProjectionMatrix();
688            renderer.setSize(w, h);
689        }});
690        
691        loadModel();
692        updateTransform();
693        animate();
694        console.log("Dioxus Three: Running");
695    </script>
696</body>
697</html>"##,
698        bg = props.background,
699        loader_script = loader_script,
700        fmt = format_str,
701        cam_x = props.cam_x,
702        cam_y = props.cam_y,
703        cam_z = props.cam_z,
704        target_x = props.target_x,
705        target_y = props.target_y,
706        target_z = props.target_z,
707        shadows = props.shadows.to_string().to_lowercase(),
708        show_grid = props.show_grid.to_string().to_lowercase(),
709        show_axes = props.show_axes.to_string().to_lowercase(),
710        rot_x = rot_x_rad,
711        rot_y = rot_y_rad,
712        rot_z = rot_z_rad,
713        scale = props.scale,
714        color = props.color,
715        auto_rotate = props.auto_rotate.to_string().to_lowercase(),
716        rot_speed = props.rot_speed,
717        wireframe = props.wireframe.to_string().to_lowercase(),
718        pos_x = props.pos_x,
719        pos_y = props.pos_y,
720        pos_z = props.pos_z,
721        shader_uniforms = shader_uniforms,
722        model_loading_code = model_loading_code,
723    );
724
725    html
726}
727
728/// Build shader code for the Three.js scene
729fn build_shader_code(shader: &ShaderPreset) -> (String, String, bool) {
730    match shader {
731        ShaderPreset::None => (String::new(), String::new(), false),
732        _ => {
733            let vert = shader.vertex_shader().unwrap_or_default();
734            let frag = shader.fragment_shader().unwrap_or_default();
735            let animated = shader.is_animated();
736
737            let material_code = format!(
738                r#"
739            // Shader material
740            const shaderMaterial = new THREE.ShaderMaterial({{
741                uniforms: {{
742                    u_time: {{ value: 0 }},
743                    u_color: {{ value: new THREE.Color(state.color) }}
744                }},
745                vertexShader: `{}`,
746                fragmentShader: `{}`,
747                transparent: true,
748                side: THREE.DoubleSide
749            }});
750            material = shaderMaterial;
751            "#,
752                vert.replace("`", "\\`"),
753                frag.replace("`", "\\`")
754            );
755
756            let uniforms_code = r#"
757            // Update shader uniforms
758            if (material && material.uniforms) {
759                material.uniforms.u_time.value = performance.now() * 0.001;
760                material.uniforms.u_color.value.set(state.color);
761            }
762            "#
763            .to_string();
764
765            (material_code, uniforms_code, animated)
766        }
767    }
768}