use crate::{
generate_three_js_html, EntityId, GizmoEvent, GizmoMode, GizmoSpace, GizmoTransform, HitInfo,
PointerEvent, ThreeViewProps, Vector2, Vector3,
};
use dioxus::document::eval;
use dioxus::prelude::*;
fn parse_hit_info(val: &serde_json::Value) -> Option<HitInfo> {
let hit = val.get("hit")?;
if hit.is_null() {
return None;
}
let entity_id = hit.get("entityId")?.as_u64()? as usize;
let point = parse_vec3(hit.get("point")?)?;
let normal = parse_vec3(hit.get("normal")?)?;
let uv = hit.get("uv").and_then(|uv| {
if uv.is_null() {
None
} else {
Some(Vector2::new(
uv.get("x")?.as_f64()? as f32,
uv.get("y")?.as_f64()? as f32,
))
}
});
let distance = hit.get("distance")?.as_f64()? as f32;
let face_index = hit
.get("faceIndex")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let instance_id = hit
.get("instanceId")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
Some(HitInfo {
entity_id: EntityId(entity_id),
point,
normal,
uv,
distance,
face_index,
instance_id,
})
}
fn parse_vec2(val: &serde_json::Value) -> Option<Vector2> {
Some(Vector2::new(
val.get("x")?.as_f64()? as f32,
val.get("y")?.as_f64()? as f32,
))
}
fn parse_vec3(val: &serde_json::Value) -> Option<Vector3> {
Some(Vector3::new(
val.get("x")?.as_f64()? as f32,
val.get("y")?.as_f64()? as f32,
val.get("z")?.as_f64()? as f32,
))
}
fn parse_pointer_event(data: &serde_json::Value) -> Option<PointerEvent> {
let hit = parse_hit_info(data);
let screen_position = parse_vec2(data.get("screenPosition")?)?;
let ndc_position = parse_vec2(data.get("ndcPosition")?)?;
let button = data.get("button").and_then(|b| match b.as_str()? {
"Left" => Some(crate::MouseButton::Left),
"Right" => Some(crate::MouseButton::Right),
"Middle" => Some(crate::MouseButton::Middle),
_ => None,
});
let shift_key = data
.get("shiftKey")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ctrl_key = data
.get("ctrlKey")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let alt_key = data
.get("altKey")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(PointerEvent {
hit,
screen_position,
ndc_position,
button,
shift_key,
ctrl_key,
alt_key,
})
}
fn parse_gizmo_event(data: &serde_json::Value) -> Option<GizmoEvent> {
let target = data.get("target")?.as_u64()? as usize;
let mode = data
.get("mode")
.and_then(|m| match m.as_str()?.to_lowercase().as_str() {
"translate" => Some(GizmoMode::Translate),
"rotate" => Some(GizmoMode::Rotate),
"scale" => Some(GizmoMode::Scale),
_ => Some(GizmoMode::Translate),
})
.unwrap_or(GizmoMode::Translate);
let space = data
.get("space")
.and_then(|s| match s.as_str()?.to_lowercase().as_str() {
"local" => Some(GizmoSpace::Local),
_ => Some(GizmoSpace::World),
})
.unwrap_or(GizmoSpace::World);
let transform_obj = data.get("transform")?;
let position = parse_vec3(transform_obj.get("position")?)?;
let rotation = parse_vec3(transform_obj.get("rotation")?)?;
let scale = parse_vec3(transform_obj.get("scale")?)?;
let is_finished = data
.get("isFinished")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(GizmoEvent {
target: EntityId(target),
mode,
space,
transform: GizmoTransform {
position,
rotation,
scale,
},
is_finished,
})
}
#[component]
pub fn ThreeView(mut props: ThreeViewProps) -> Element {
let mut html_signal = use_signal(|| generate_three_js_html(&props));
let mut prev_models_len = use_signal(|| props.models.len());
let on_pointer_down = props.on_pointer_down.take();
let on_pointer_up = props.on_pointer_up.take();
let on_pointer_move = props.on_pointer_move.take();
let on_gizmo_drag = props.on_gizmo_drag.take();
let on_selection_change = props.on_selection_change.take();
use_hook(move || {
spawn(async move {
let mut eval = eval(
r#"
window._dioxusThreeEvents = [];
window.addEventListener('message', function(e) {
// Only process messages from our Three.js iframe
var iframe = document.querySelector('iframe[srcdoc]');
if (iframe && iframe.contentWindow && e.source !== iframe.contentWindow) {
// Not from our iframe, but still allow if it looks like our event format
// (some WebView implementations don't set e.source correctly)
if (!e.data || !e.data.type) return;
}
if (e.data && (
e.data.type === 'gizmo-drag' ||
e.data.type === 'pointer-down' ||
e.data.type === 'pointer-up' ||
e.data.type === 'pointer-move' ||
e.data.type === 'selection-change'
)) {
console.log('[EVAL BRIDGE] received:', e.data.type);
window._dioxusThreeEvents.push(e.data);
dioxus.send(e.data);
}
});
while (true) {
await dioxus.recv();
}
"#,
);
loop {
match eval.recv::<serde_json::Value>().await {
Ok(event) => {
let event_type = event.get("type").and_then(|v| v.as_str());
let data = event.get("data");
println!("[DESKTOP BRIDGE] received event: {:?}", event_type);
match event_type {
Some("gizmo-drag") => {
println!("[DESKTOP BRIDGE] gizmo-drag event");
if let (Some(cb), Some(data)) = (&on_gizmo_drag, data) {
if let Some(gizmo_event) = parse_gizmo_event(data) {
cb.call(gizmo_event);
}
}
}
Some("pointer-down") => {
println!("[DESKTOP BRIDGE] pointer-down event");
if let (Some(cb), Some(data)) = (&on_pointer_down, data) {
if let Some(ptr_event) = parse_pointer_event(data) {
println!(
"[DESKTOP BRIDGE] parsed pointer-down: hit={:?}",
ptr_event.hit.as_ref().map(|h| h.entity_id)
);
cb.call(ptr_event);
} else {
println!("[DESKTOP BRIDGE] failed to parse pointer-down");
}
} else {
println!(
"[DESKTOP BRIDGE] no pointer-down callback registered"
);
}
}
Some("pointer-up") => {
if let (Some(cb), Some(data)) = (&on_pointer_up, data) {
if let Some(ptr_event) = parse_pointer_event(data) {
cb.call(ptr_event);
}
}
}
Some("pointer-move") => {
if let (Some(cb), Some(data)) = (&on_pointer_move, data) {
if let Some(ptr_event) = parse_pointer_event(data) {
cb.call(ptr_event);
}
}
}
Some("selection-change") => {
if let (Some(cb), Some(data)) = (&on_selection_change, data) {
if let Some(selection_ids) = data.get("selection") {
let ids: Vec<EntityId> = selection_ids
.as_array()
.unwrap_or(&vec![])
.iter()
.filter_map(|v| {
v.as_u64().map(|id| EntityId(id as usize))
})
.collect();
let mut selection =
crate::Selection::with_mode(props.selection_mode);
for id in ids {
selection.select(id);
}
cb.call(selection);
}
}
}
_ => {}
}
}
Err(e) => {
println!("[DESKTOP BRIDGE] eval recv error: {:?}", e);
break;
}
}
}
});
});
use_effect(use_reactive((&props,), move |(new_props,)| {
let old_len = prev_models_len();
let new_len = new_props.models.len();
if old_len != new_len {
html_signal.set(generate_three_js_html(&new_props));
prev_models_len.set(new_len);
return;
}
spawn(async move {
let selection_json = match &new_props.selection {
Some(s) => format!(
"[{}]",
s.iter()
.map(|e| e.0.to_string())
.collect::<Vec<_>>()
.join(",")
),
None => "[]".to_string(),
};
let gizmo_json = match &new_props.gizmo {
Some(g) => format!(
r#"{{"target":{},"mode":"{:?}","space":"{:?}"}}"#,
g.target.0, g.mode, g.space
),
None => "null".to_string(),
};
println!(
"[DESKTOP] Sending postMessage - selection: {}, gizmo: {}",
selection_json, gizmo_json
);
let js = format!(
r#"
(function() {{
const iframe = document.querySelector('iframe[srcdoc]');
if (iframe && iframe.contentWindow) {{
console.log('[POSTMSG] sending update-state');
iframe.contentWindow.postMessage({{
type: 'update-state',
camX: {},
camY: {},
camZ: {},
targetX: {},
targetY: {},
targetZ: {},
autoRotate: {},
rotSpeed: {},
scale: {},
color: '{}',
background: '{}',
showGrid: {},
showAxes: {},
wireframe: {},
selection: {},
gizmo: {}
}}, '*');
}} else {{
console.warn('[POSTMSG] iframe not found');
}}
}})();
"#,
new_props.cam_x,
new_props.cam_y,
new_props.cam_z,
new_props.target_x,
new_props.target_y,
new_props.target_z,
new_props.auto_rotate.to_string().to_lowercase(),
new_props.rot_speed,
new_props.scale,
new_props.color.replace('\\', "\\\\").replace('\'', "\\'"),
new_props
.background
.replace('\\', "\\\\")
.replace('\'', "\\'"),
new_props.show_grid.to_string().to_lowercase(),
new_props.show_axes.to_string().to_lowercase(),
new_props.wireframe.to_string().to_lowercase(),
selection_json,
gizmo_json
);
let _ = eval(&js).await;
println!("[DESKTOP] postMessage sent");
});
}));
rsx! {
iframe {
class: "{props.class}",
style: "width: 100%; height: 100%; border: none;",
srcdoc: "{html_signal}",
}
}
}