diff --git a/src/indexing/sample_data.rs b/src/indexing/sample_data.rs index 59a6fc9..753af6c 100644 --- a/src/indexing/sample_data.rs +++ b/src/indexing/sample_data.rs @@ -288,6 +288,18 @@ async fn insert_sample_ldraw_resource(conn: &mut SqliteConnection) -> anyhow::Re .bind(SAMPLE_TIME_MILLIS) .execute(&mut *conn) .await?; + sqlx::query!( + "INSERT INTO ldraw_resource_memberships \ + (resource_uri, project_uri, root, canonical_path, provenance) \ + VALUES (?, ?, ?, ?, ?)", + resource_uri, + SAMPLE_THING_URI, + root, + path, + "sample", + ) + .execute(&mut *conn) + .await?; sqlx::query( "INSERT INTO ldraw_verification (resource_uri, source_identity, state, diagnostic, updated_at) VALUES (?, ?, 'verified', NULL, ?)", ) diff --git a/src/viewer.rs b/src/viewer.rs index 76fbb53..f18a75d 100644 --- a/src/viewer.rs +++ b/src/viewer.rs @@ -348,6 +348,8 @@ pub fn ViewerPage(initial_model: DemoModel) -> Element { let active_step_object_url: Signal> = use_signal(|| None); let canvas_mounted = use_signal(|| false); let canvas_mount_generation = use_signal(|| 0_u32); + let canvas_mount_epoch = use_signal(|| None::); + let canvas_mount_requested = use_signal(|| true); let resize_count = use_signal(|| 0_u64); let context_acquisitions = use_signal(|| 0_u64); let renderer_failed = use_signal(|| false); @@ -426,6 +428,8 @@ pub fn ViewerPage(initial_model: DemoModel) -> Element { status, canvas_mounted, canvas_mount_generation, + canvas_mount_epoch, + canvas_mount_requested, resize_count, context_acquisitions, renderer_failed, @@ -454,6 +458,8 @@ pub fn AssetViewer( let step_report = use_signal(StepConversionReport::idle); let canvas_mounted = use_signal(|| false); let canvas_mount_generation = use_signal(|| 0_u32); + let canvas_mount_epoch = use_signal(|| None::); + let canvas_mount_requested = use_signal(|| true); let resize_count = use_signal(|| 0_u64); let context_acquisitions = use_signal(|| 0_u64); let renderer_failed = use_signal(|| false); @@ -554,6 +560,8 @@ pub fn AssetViewer( status, canvas_mounted, canvas_mount_generation, + canvas_mount_epoch, + canvas_mount_requested, resize_count, context_acquisitions, renderer_failed, @@ -862,6 +870,8 @@ pub struct ModelViewerProps { compound_intent: Signal>, compound_boundary: Signal, canvas_mount_generation: Signal, + canvas_mount_epoch: Signal>, + canvas_mount_requested: Signal, fallback_to_demo: bool, mesh_stats: Signal>, status: Signal, @@ -879,6 +889,8 @@ pub struct ModelViewerProps { #[component] pub fn ModelViewer(props: ModelViewerProps) -> Element { let canvas_mount_generation = props.canvas_mount_generation; + let canvas_mount_epoch = props.canvas_mount_epoch; + let canvas_mount_requested = props.canvas_mount_requested; let canvas_mounted = props.canvas_mounted; let resize_count = props.resize_count; let context_acquisitions = props.context_acquisitions; @@ -934,18 +946,26 @@ pub fn ModelViewer(props: ModelViewerProps) -> Element { "data-compound-boundary": "{compound_boundary.read()}", div { class: "model-viewer-stage", - canvas { - key: "viewer-canvas-{restart_epoch.read()}", - id: "polymodel-viewer-canvas", - "data-mounted": "{canvas_mounted.read()}", - "data-renderer-kind": renderer_kind, - class: "model-viewer-canvas", - aria_label: "Polymodel model viewer canvas", - onmounted: move |event| { - mark_canvas_mounted(&event, canvas_mounted, canvas_mount_generation) - }, - onwheel: move |event| event.stop_propagation(), - onpointerdown: move |event| event.stop_propagation(), + if *canvas_mount_requested.read() { + canvas { + key: "viewer-canvas-{restart_epoch.read()}", + id: "polymodel-viewer-canvas", + "data-mounted": "{canvas_mounted.read()}", + "data-renderer-kind": renderer_kind, + class: "model-viewer-canvas", + aria_label: "Polymodel model viewer canvas", + onmounted: move |event| { + mark_canvas_mounted( + &event, + canvas_mounted, + canvas_mount_generation, + canvas_mount_epoch, + restart_epoch, + ) + }, + onwheel: move |event| event.stop_propagation(), + onpointerdown: move |event| event.stop_propagation(), + } } if !renderer_active { if let Some(fallback) = fallback { @@ -968,6 +988,8 @@ pub fn ModelViewer(props: ModelViewerProps) -> Element { compound_intent, compound_boundary, canvas_mount_generation, + canvas_mount_epoch, + canvas_mount_requested, fallback_to_demo, status, canvas_mounted, @@ -1021,6 +1043,8 @@ fn WorkerRendererSession( compound_intent: Signal>, compound_boundary: Signal, canvas_mount_generation: Signal, + canvas_mount_epoch: Signal>, + canvas_mount_requested: Signal, fallback_to_demo: bool, status: Signal, canvas_mounted: Signal, @@ -1041,6 +1065,8 @@ fn WorkerRendererSession( compound_intent, compound_boundary, canvas_mount_generation, + canvas_mount_epoch, + canvas_mount_requested, fallback_to_demo, status, canvas_mounted, @@ -1065,6 +1091,8 @@ fn WorkerSessionInner( compound_intent: Signal>, compound_boundary: Signal, canvas_mount_generation: Signal, + canvas_mount_epoch: Signal>, + canvas_mount_requested: Signal, fallback_to_demo: bool, status: Signal, canvas_mounted: Signal, @@ -1084,6 +1112,8 @@ fn WorkerSessionInner( compound_intent, compound_boundary, canvas_mount_generation, + canvas_mount_epoch, + canvas_mount_requested, fallback_to_demo, status, canvas_mounted, @@ -1108,6 +1138,8 @@ fn WorkerSessionInner( compound_intent: Signal>, compound_boundary: Signal, canvas_mount_generation: Signal, + canvas_mount_epoch: Signal>, + canvas_mount_requested: Signal, fallback_to_demo: bool, status: Signal, canvas_mounted: Signal, @@ -1131,16 +1163,29 @@ fn WorkerSessionInner( let bridge = use_hook(|| Rc::new(RefCell::new(WorkerBridgeState::new()))); let worker_ready = use_signal(|| false); + // A restart first removes the transferred canvas for one render. Re-enable + // the slot on the following reactive pass so Dioxus must mount a fresh node. + let mut canvas_mount_requested_for_effect = canvas_mount_requested; + let canvas_mount_epoch_for_effect = canvas_mount_epoch; + use_effect(move || { + let epoch = *restart_epoch.read(); + if !*canvas_mount_requested_for_effect.read() + && *canvas_mount_epoch_for_effect.read() != Some(epoch) + { + canvas_mount_requested_for_effect.set(true); + } + }); + // Spawn only after the canvas for the current restart epoch has mounted. let bridge_for_spawn = bridge.clone(); use_effect(move || { let mounted_generation = *canvas_mount_generation.read(); + let mounted_epoch = *canvas_mount_epoch.read(); let mounted = *canvas_mounted.read(); let epoch = *restart_epoch.read(); - if !mounted || mounted_generation == 0 { + if !mounted || mounted_generation == 0 || mounted_epoch != Some(epoch) { return; } - let _ = epoch; let status_clone = status; let context_acquisitions_clone = context_acquisitions; let renderer_failed_clone = renderer_failed; @@ -1166,6 +1211,8 @@ fn WorkerSessionInner( cancelled_loads: cancelled_loads_clone, compound_boundary: compound_boundary_clone, canvas_mounted, + canvas_mount_epoch, + canvas_mount_requested, restart_epoch, }, ); @@ -1188,7 +1235,8 @@ fn WorkerSessionInner( // when the bridge becomes able to send protocol commands. let _ = *worker_session_id.read(); - let canvas_present = *canvas_mounted.read(); + let canvas_present = + *canvas_mounted.read() && *canvas_mount_epoch.read() == Some(*restart_epoch.read()); let mut bt = bridge_for_canvas.borrow_mut(); // Transfer canvas on first mount. @@ -1430,6 +1478,8 @@ struct WorkerSpawnSignals { cancelled_loads: Signal, compound_boundary: Signal, canvas_mounted: Signal, + canvas_mount_epoch: Signal>, + canvas_mount_requested: Signal, restart_epoch: Signal, } @@ -1481,6 +1531,8 @@ impl WorkerBridgeState { cancelled_loads, compound_boundary, canvas_mounted, + canvas_mount_epoch, + canvas_mount_requested, restart_epoch, } = signals; let mut renderer_failed = renderer_failed; @@ -1500,29 +1552,29 @@ impl WorkerBridgeState { // Set up onmessage handler. Both fatal event paths use the same reset so // a worker cannot leave a transferred canvas or stale session behind. - let worker_ready_for_restart = Rc::new(RefCell::new(worker_ready)); - let canvas_mounted_for_restart = Rc::new(RefCell::new(canvas_mounted)); - let renderer_failed_for_restart = Rc::new(RefCell::new(renderer_failed)); - let restart_epoch_for_restart = Rc::new(RefCell::new(restart_epoch)); let restart: Rc = Rc::new({ - let worker_ready = worker_ready_for_restart.clone(); - let canvas_mounted = canvas_mounted_for_restart.clone(); - let renderer_failed = renderer_failed_for_restart.clone(); - let restart_epoch = restart_epoch_for_restart.clone(); - let worker_session_id = Rc::new(RefCell::new(worker_session_id)); move || { let next_epoch = { let mut bt = bridge.borrow_mut(); bt.reset_for_restart(); bt.restart_epoch }; - worker_ready.borrow_mut().set(false); - // Force Dioxus to unmount the transferred node before the new epoch - // can be mounted. The spawn effect observes the next mount event. - canvas_mounted.borrow_mut().set(false); - renderer_failed.borrow_mut().set(true); - restart_epoch.borrow_mut().set(next_epoch); - worker_session_id.borrow_mut().set(None); + let mut worker_ready = worker_ready; + let mut canvas_mounted = canvas_mounted; + let mut canvas_mount_epoch = canvas_mount_epoch; + let mut canvas_mount_requested = canvas_mount_requested; + let mut renderer_failed = renderer_failed; + let mut restart_epoch = restart_epoch; + let mut worker_session_id = worker_session_id; + worker_ready.set(false); + // Remove the transferred node for one render before the new epoch + // can mount. The mount callback is the only source of mounted=true. + canvas_mounted.set(false); + canvas_mount_epoch.set(None); + canvas_mount_requested.set(false); + renderer_failed.set(true); + restart_epoch.set(next_epoch); + worker_session_id.set(None); } }); let restart_for_message = restart.clone(); @@ -2271,6 +2323,8 @@ fn mark_canvas_mounted( event: &MountedEvent, mut mounted: Signal, mut generation: Signal, + mut mounted_epoch: Signal>, + current_epoch: Signal, ) { use dioxus::web::WebEventExt; use wasm_bindgen::JsCast; @@ -2279,13 +2333,22 @@ fn mark_canvas_mounted( mounted.set(true); let next_generation = generation.read().saturating_add(1); generation.set(next_generation); + mounted_epoch.set(Some(*current_epoch.read())); } else { mounted.set(false); + mounted_epoch.set(None); } } #[cfg(not(target_arch = "wasm32"))] -fn mark_canvas_mounted(_event: &MountedEvent, _mounted: Signal, _generation: Signal) {} +fn mark_canvas_mounted( + _event: &MountedEvent, + _mounted: Signal, + _generation: Signal, + _mounted_epoch: Signal>, + _current_epoch: Signal, +) { +} // --------------------------------------------------------------------------- // Tests