1use dioxus::prelude::*;
10use std::collections::HashMap;
11
12#[derive(Clone, PartialEq, Debug, Default)]
14pub struct ShaderConfig {
15 pub vertex_shader: Option<String>,
17 pub fragment_shader: Option<String>,
19 pub uniforms: HashMap<String, f32>,
21 pub animated: bool,
23}
24
25#[derive(Clone, PartialEq, Debug)]
27pub enum ShaderPreset {
28 None,
30 Gradient,
32 Water,
34 Hologram,
36 Toon,
38 Heatmap,
40 Custom(ShaderConfig),
42}
43
44#[derive(Clone, PartialEq, Debug)]
46pub enum ModelFormat {
47 Obj,
49 Fbx,
51 Gltf,
53 Glb,
55 Stl,
57 Ply,
59 Dae,
61 Json,
63 Cube,
65}
66
67impl ModelFormat {
68 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 fn extra_scripts(&self) -> Vec<&'static str> {
123 match self {
124 ModelFormat::Fbx => vec!["https://cdn.jsdelivr.net/npm/fflate@0.8.0/umd/index.js"],
126 _ => vec![],
127 }
128 }
129}
130
131#[derive(Clone, PartialEq, Debug)]
133pub struct ModelConfig {
134 pub url: String,
136 pub format: ModelFormat,
138 pub pos_x: f32,
140 pub pos_y: f32,
142 pub pos_z: f32,
144 pub rot_x: f32,
146 pub rot_y: f32,
148 pub rot_z: f32,
150 pub scale: f32,
152 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 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 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 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 pub fn with_scale(mut self, scale: f32) -> Self {
201 self.scale = scale;
202 self
203 }
204
205 pub fn with_color(mut self, color: impl Into<String>) -> Self {
207 self.color = color.into();
208 self
209 }
210}
211
212#[derive(Props, Clone, PartialEq)]
214pub struct ThreeViewProps {
215 #[props(default = None)]
217 pub model_url: Option<String>,
218 #[props(default = ModelFormat::Cube)]
220 pub format: ModelFormat,
221 #[props(default = 0.0)]
223 pub pos_x: f32,
224 #[props(default = 0.0)]
226 pub pos_y: f32,
227 #[props(default = 0.0)]
229 pub pos_z: f32,
230 #[props(default = 0.0)]
232 pub rot_x: f32,
233 #[props(default = 0.0)]
235 pub rot_y: f32,
236 #[props(default = 0.0)]
238 pub rot_z: f32,
239 #[props(default = 1.0)]
241 pub scale: f32,
242 #[props(default = "#ff6b6b".to_string())]
244 pub color: String,
245 #[props(default = Vec::new())]
247 pub models: Vec<ModelConfig>,
248 #[props(default = true)]
250 pub auto_center: bool,
251 #[props(default = false)]
253 pub auto_scale: bool,
254 #[props(default = 5.0)]
256 pub cam_x: f32,
257 #[props(default = 5.0)]
259 pub cam_y: f32,
260 #[props(default = 5.0)]
262 pub cam_z: f32,
263 #[props(default = 0.0)]
265 pub target_x: f32,
266 #[props(default = 0.0)]
268 pub target_y: f32,
269 #[props(default = 0.0)]
271 pub target_z: f32,
272 #[props(default = true)]
274 pub auto_rotate: bool,
275 #[props(default = 1.0)]
277 pub rot_speed: f32,
278 #[props(default = true)]
280 pub show_grid: bool,
281 #[props(default = true)]
283 pub show_axes: bool,
284 #[props(default = "#1a1a2e".to_string())]
286 pub background: String,
287 #[props(default = String::new())]
289 pub class: String,
290 #[props(default = true)]
292 pub shadows: bool,
293 #[props(default = false)]
295 pub wireframe: bool,
296 #[props(default = ShaderPreset::None)]
298 pub shader: ShaderPreset,
299}
300
301impl ShaderPreset {
302 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 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 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#[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
353fn 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
382fn 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
402fn 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
458fn 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
489fn 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 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 let use_multiple_models = !props.models.is_empty();
504
505 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 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 let (shader_material_code, shader_uniforms, _shader_animated) =
527 build_shader_code(&props.shader);
528
529 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
728fn 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}