1use crate::{
6 generate_three_js_html, EntityId, GizmoEvent, GizmoMode, GizmoSpace, GizmoTransform, HitInfo,
7 PointerEvent, ThreeViewProps, Vector2, Vector3,
8};
9use dioxus::document::eval;
10use dioxus::prelude::*;
11
12fn parse_hit_info(val: &serde_json::Value) -> Option<HitInfo> {
14 let hit = val.get("hit")?;
15 if hit.is_null() {
16 return None;
17 }
18 let entity_id = hit.get("entityId")?.as_u64()? as usize;
19 let point = parse_vec3(hit.get("point")?)?;
20 let normal = parse_vec3(hit.get("normal")?)?;
21 let uv = hit.get("uv").and_then(|uv| {
22 if uv.is_null() {
23 None
24 } else {
25 Some(Vector2::new(
26 uv.get("x")?.as_f64()? as f32,
27 uv.get("y")?.as_f64()? as f32,
28 ))
29 }
30 });
31 let distance = hit.get("distance")?.as_f64()? as f32;
32 let face_index = hit
33 .get("faceIndex")
34 .and_then(|v| v.as_u64())
35 .map(|v| v as usize);
36 let instance_id = hit
37 .get("instanceId")
38 .and_then(|v| v.as_u64())
39 .map(|v| v as usize);
40
41 Some(HitInfo {
42 entity_id: EntityId(entity_id),
43 point,
44 normal,
45 uv,
46 distance,
47 face_index,
48 instance_id,
49 })
50}
51
52fn parse_vec2(val: &serde_json::Value) -> Option<Vector2> {
53 Some(Vector2::new(
54 val.get("x")?.as_f64()? as f32,
55 val.get("y")?.as_f64()? as f32,
56 ))
57}
58
59fn parse_vec3(val: &serde_json::Value) -> Option<Vector3> {
60 Some(Vector3::new(
61 val.get("x")?.as_f64()? as f32,
62 val.get("y")?.as_f64()? as f32,
63 val.get("z")?.as_f64()? as f32,
64 ))
65}
66
67fn parse_pointer_event(data: &serde_json::Value) -> Option<PointerEvent> {
68 let hit = parse_hit_info(data);
69 let screen_position = parse_vec2(data.get("screenPosition")?)?;
70 let ndc_position = parse_vec2(data.get("ndcPosition")?)?;
71 let button = data.get("button").and_then(|b| match b.as_str()? {
72 "Left" => Some(crate::MouseButton::Left),
73 "Right" => Some(crate::MouseButton::Right),
74 "Middle" => Some(crate::MouseButton::Middle),
75 _ => None,
76 });
77 let shift_key = data
78 .get("shiftKey")
79 .and_then(|v| v.as_bool())
80 .unwrap_or(false);
81 let ctrl_key = data
82 .get("ctrlKey")
83 .and_then(|v| v.as_bool())
84 .unwrap_or(false);
85 let alt_key = data
86 .get("altKey")
87 .and_then(|v| v.as_bool())
88 .unwrap_or(false);
89
90 Some(PointerEvent {
91 hit,
92 screen_position,
93 ndc_position,
94 button,
95 shift_key,
96 ctrl_key,
97 alt_key,
98 })
99}
100
101fn parse_gizmo_event(data: &serde_json::Value) -> Option<GizmoEvent> {
102 let target = data.get("target")?.as_u64()? as usize;
103 let mode = data
104 .get("mode")
105 .and_then(|m| match m.as_str()?.to_lowercase().as_str() {
106 "translate" => Some(GizmoMode::Translate),
107 "rotate" => Some(GizmoMode::Rotate),
108 "scale" => Some(GizmoMode::Scale),
109 _ => Some(GizmoMode::Translate),
110 })
111 .unwrap_or(GizmoMode::Translate);
112 let space = data
113 .get("space")
114 .and_then(|s| match s.as_str()?.to_lowercase().as_str() {
115 "local" => Some(GizmoSpace::Local),
116 _ => Some(GizmoSpace::World),
117 })
118 .unwrap_or(GizmoSpace::World);
119
120 let transform_obj = data.get("transform")?;
121 let position = parse_vec3(transform_obj.get("position")?)?;
122 let rotation = parse_vec3(transform_obj.get("rotation")?)?;
123 let scale = parse_vec3(transform_obj.get("scale")?)?;
124 let is_finished = data
125 .get("isFinished")
126 .and_then(|v| v.as_bool())
127 .unwrap_or(false);
128
129 Some(GizmoEvent {
130 target: EntityId(target),
131 mode,
132 space,
133 transform: GizmoTransform {
134 position,
135 rotation,
136 scale,
137 },
138 is_finished,
139 })
140}
141
142#[component]
148pub fn ThreeView(mut props: ThreeViewProps) -> Element {
149 let mut html_signal = use_signal(|| generate_three_js_html(&props));
151 let mut prev_models_len = use_signal(|| props.models.len());
152
153 let on_pointer_down = props.on_pointer_down.take();
155 let on_pointer_up = props.on_pointer_up.take();
156 let on_pointer_move = props.on_pointer_move.take();
157 let on_gizmo_drag = props.on_gizmo_drag.take();
158 let on_selection_change = props.on_selection_change.take();
159
160 use_hook(move || {
162 spawn(async move {
163 let mut eval = eval(
164 r#"
165 window._dioxusThreeEvents = [];
166 window.addEventListener('message', function(e) {
167 // Only process messages from our Three.js iframe
168 var iframe = document.querySelector('iframe[srcdoc]');
169 if (iframe && iframe.contentWindow && e.source !== iframe.contentWindow) {
170 // Not from our iframe, but still allow if it looks like our event format
171 // (some WebView implementations don't set e.source correctly)
172 if (!e.data || !e.data.type) return;
173 }
174 if (e.data && (
175 e.data.type === 'gizmo-drag' ||
176 e.data.type === 'pointer-down' ||
177 e.data.type === 'pointer-up' ||
178 e.data.type === 'pointer-move' ||
179 e.data.type === 'selection-change'
180 )) {
181 console.log('[EVAL BRIDGE] received:', e.data.type);
182 window._dioxusThreeEvents.push(e.data);
183 dioxus.send(e.data);
184 }
185 });
186
187 while (true) {
188 await dioxus.recv();
189 }
190 "#,
191 );
192
193 loop {
194 match eval.recv::<serde_json::Value>().await {
195 Ok(event) => {
196 let event_type = event.get("type").and_then(|v| v.as_str());
197 let data = event.get("data");
198
199 println!("[DESKTOP BRIDGE] received event: {:?}", event_type);
200
201 match event_type {
202 Some("gizmo-drag") => {
203 println!("[DESKTOP BRIDGE] gizmo-drag event");
204 if let (Some(cb), Some(data)) = (&on_gizmo_drag, data) {
205 if let Some(gizmo_event) = parse_gizmo_event(data) {
206 cb.call(gizmo_event);
207 }
208 }
209 }
210 Some("pointer-down") => {
211 println!("[DESKTOP BRIDGE] pointer-down event");
212 if let (Some(cb), Some(data)) = (&on_pointer_down, data) {
213 if let Some(ptr_event) = parse_pointer_event(data) {
214 println!(
215 "[DESKTOP BRIDGE] parsed pointer-down: hit={:?}",
216 ptr_event.hit.as_ref().map(|h| h.entity_id)
217 );
218 cb.call(ptr_event);
219 } else {
220 println!("[DESKTOP BRIDGE] failed to parse pointer-down");
221 }
222 } else {
223 println!(
224 "[DESKTOP BRIDGE] no pointer-down callback registered"
225 );
226 }
227 }
228 Some("pointer-up") => {
229 if let (Some(cb), Some(data)) = (&on_pointer_up, data) {
230 if let Some(ptr_event) = parse_pointer_event(data) {
231 cb.call(ptr_event);
232 }
233 }
234 }
235 Some("pointer-move") => {
236 if let (Some(cb), Some(data)) = (&on_pointer_move, data) {
237 if let Some(ptr_event) = parse_pointer_event(data) {
238 cb.call(ptr_event);
239 }
240 }
241 }
242 Some("selection-change") => {
243 if let (Some(cb), Some(data)) = (&on_selection_change, data) {
244 if let Some(selection_ids) = data.get("selection") {
245 let ids: Vec<EntityId> = selection_ids
246 .as_array()
247 .unwrap_or(&vec![])
248 .iter()
249 .filter_map(|v| {
250 v.as_u64().map(|id| EntityId(id as usize))
251 })
252 .collect();
253 let mut selection =
254 crate::Selection::with_mode(props.selection_mode);
255 for id in ids {
256 selection.select(id);
257 }
258 cb.call(selection);
259 }
260 }
261 }
262 _ => {}
263 }
264 }
265 Err(e) => {
266 println!("[DESKTOP BRIDGE] eval recv error: {:?}", e);
267 break;
269 }
270 }
271 }
272 });
273 });
274
275 use_effect(use_reactive((&props,), move |(new_props,)| {
277 let old_len = prev_models_len();
278 let new_len = new_props.models.len();
279 if old_len != new_len {
280 html_signal.set(generate_three_js_html(&new_props));
281 prev_models_len.set(new_len);
282 return;
283 }
284
285 spawn(async move {
286 let selection_json = match &new_props.selection {
287 Some(s) => format!(
288 "[{}]",
289 s.iter()
290 .map(|e| e.0.to_string())
291 .collect::<Vec<_>>()
292 .join(",")
293 ),
294 None => "[]".to_string(),
295 };
296
297 let gizmo_json = match &new_props.gizmo {
298 Some(g) => format!(
299 r#"{{"target":{},"mode":"{:?}","space":"{:?}"}}"#,
300 g.target.0, g.mode, g.space
301 ),
302 None => "null".to_string(),
303 };
304
305 println!(
306 "[DESKTOP] Sending postMessage - selection: {}, gizmo: {}",
307 selection_json, gizmo_json
308 );
309
310 let js = format!(
311 r#"
312 (function() {{
313 const iframe = document.querySelector('iframe[srcdoc]');
314 if (iframe && iframe.contentWindow) {{
315 console.log('[POSTMSG] sending update-state');
316 iframe.contentWindow.postMessage({{
317 type: 'update-state',
318 camX: {},
319 camY: {},
320 camZ: {},
321 targetX: {},
322 targetY: {},
323 targetZ: {},
324 autoRotate: {},
325 rotSpeed: {},
326 scale: {},
327 color: '{}',
328 background: '{}',
329 showGrid: {},
330 showAxes: {},
331 wireframe: {},
332 selection: {},
333 gizmo: {}
334 }}, '*');
335 }} else {{
336 console.warn('[POSTMSG] iframe not found');
337 }}
338 }})();
339 "#,
340 new_props.cam_x,
341 new_props.cam_y,
342 new_props.cam_z,
343 new_props.target_x,
344 new_props.target_y,
345 new_props.target_z,
346 new_props.auto_rotate.to_string().to_lowercase(),
347 new_props.rot_speed,
348 new_props.scale,
349 new_props.color.replace('\\', "\\\\").replace('\'', "\\'"),
350 new_props
351 .background
352 .replace('\\', "\\\\")
353 .replace('\'', "\\'"),
354 new_props.show_grid.to_string().to_lowercase(),
355 new_props.show_axes.to_string().to_lowercase(),
356 new_props.wireframe.to_string().to_lowercase(),
357 selection_json,
358 gizmo_json
359 );
360 let _ = eval(&js).await;
361 println!("[DESKTOP] postMessage sent");
362 });
363 }));
364
365 rsx! {
366 iframe {
367 class: "{props.class}",
368 style: "width: 100%; height: 100%; border: none;",
369 srcdoc: "{html_signal}",
370 }
371 }
372}