1use dioxus::prelude::*;
16use std::collections::HashMap;
17
18#[cfg(not(target_arch = "wasm32"))]
20mod desktop;
21#[cfg(target_arch = "wasm32")]
22mod web;
23
24#[cfg(not(target_arch = "wasm32"))]
26pub use desktop::ThreeView;
27#[cfg(target_arch = "wasm32")]
28pub use web::ThreeView;
29
30#[derive(Clone, PartialEq, Debug, Default)]
32pub struct ShaderConfig {
33 pub vertex_shader: Option<String>,
35 pub fragment_shader: Option<String>,
37 pub uniforms: HashMap<String, f32>,
39 pub animated: bool,
41}
42
43#[derive(Clone, PartialEq, Debug)]
45pub enum ShaderPreset {
46 None,
48 Gradient,
50 Water,
52 Hologram,
54 Toon,
56 Heatmap,
58 Custom(ShaderConfig),
60}
61
62#[derive(Clone, PartialEq, Debug)]
64pub enum ModelFormat {
65 Obj,
67 Fbx,
69 Gltf,
71 Glb,
73 Stl,
75 Ply,
77 Dae,
79 Json,
81 Cube,
83}
84
85impl ModelFormat {
86 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 fn extra_scripts(&self) -> Vec<&'static str> {
141 match self {
142 ModelFormat::Fbx => vec!["https://cdn.jsdelivr.net/npm/fflate@0.8.0/umd/index.js"],
144 _ => vec![],
145 }
146 }
147}
148
149#[derive(Clone, PartialEq, Debug)]
151pub struct ModelConfig {
152 pub url: String,
154 pub format: ModelFormat,
156 pub pos_x: f32,
158 pub pos_y: f32,
160 pub pos_z: f32,
162 pub rot_x: f32,
164 pub rot_y: f32,
166 pub rot_z: f32,
168 pub scale: f32,
170 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 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 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 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 pub fn with_scale(mut self, scale: f32) -> Self {
219 self.scale = scale;
220 self
221 }
222
223 pub fn with_color(mut self, color: impl Into<String>) -> Self {
225 self.color = color.into();
226 self
227 }
228}
229
230#[derive(Props, Clone, PartialEq)]
232pub struct ThreeViewProps {
233 #[props(default = None)]
235 pub model_url: Option<String>,
236 #[props(default = ModelFormat::Cube)]
238 pub format: ModelFormat,
239 #[props(default = 0.0)]
241 pub pos_x: f32,
242 #[props(default = 0.0)]
244 pub pos_y: f32,
245 #[props(default = 0.0)]
247 pub pos_z: f32,
248 #[props(default = 0.0)]
250 pub rot_x: f32,
251 #[props(default = 0.0)]
253 pub rot_y: f32,
254 #[props(default = 0.0)]
256 pub rot_z: f32,
257 #[props(default = 1.0)]
259 pub scale: f32,
260 #[props(default = "#ff6b6b".to_string())]
262 pub color: String,
263 #[props(default = Vec::new())]
265 pub models: Vec<ModelConfig>,
266 #[props(default = true)]
268 pub auto_center: bool,
269 #[props(default = false)]
271 pub auto_scale: bool,
272 #[props(default = 5.0)]
274 pub cam_x: f32,
275 #[props(default = 5.0)]
277 pub cam_y: f32,
278 #[props(default = 5.0)]
280 pub cam_z: f32,
281 #[props(default = 0.0)]
283 pub target_x: f32,
284 #[props(default = 0.0)]
286 pub target_y: f32,
287 #[props(default = 0.0)]
289 pub target_z: f32,
290 #[props(default = true)]
292 pub auto_rotate: bool,
293 #[props(default = 1.0)]
295 pub rot_speed: f32,
296 #[props(default = true)]
298 pub show_grid: bool,
299 #[props(default = true)]
301 pub show_axes: bool,
302 #[props(default = "#1a1a2e".to_string())]
304 pub background: String,
305 #[props(default = String::new())]
307 pub class: String,
308 #[props(default = true)]
310 pub shadows: bool,
311 #[props(default = false)]
313 pub wireframe: bool,
314 #[props(default = ShaderPreset::None)]
316 pub shader: ShaderPreset,
317}
318
319impl ShaderPreset {
320 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 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 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
357pub 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
386pub 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
406pub 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
462pub 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
493pub 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 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 let use_multiple_models = !props.models.is_empty();
508
509 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 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 let (shader_material_code, shader_uniforms, _shader_animated) =
531 build_shader_code(&props.shader);
532
533 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
732pub 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}