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