bridge bevy and react-map-gl to exchange gps (#6)

* isolate ipc pattern

* shuffle logic in main for readability, remove unused webview message observer

* renders react map

---------

Co-authored-by: geoffsee <>
This commit is contained in:
Geoff Seemueller
2025-07-08 17:44:37 -04:00
committed by GitHub
parent 59c0474bf9
commit 92e5cfb21c
8 changed files with 366 additions and 49 deletions

View File

@@ -6,8 +6,6 @@ authors = ["seemueller-io <git@github.geoffsee>"]
edition = "2021"
exclude = ["dist", "build", "assets", "credits"]
[profile.dev.package."*"]
opt-level = 3
@@ -63,16 +61,8 @@ systems = { path = "../systems" }
components = { path = "../components" }
wasm-bindgen = "0.2"
web-sys = { version = "0.3.53", features = ["Document", "Element", "HtmlElement", "Window"] }
# Platform-specific tokio features
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] }
image = "0.25"
winit = "0.30"
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { version = "1.0", features = ["rt"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# keep the following in sync with Bevy's dependencies
winit = { version = "0.30", default-features = false }
@@ -80,5 +70,17 @@ image = { version = "0.25", default-features = false }
## This greatly improves WGPU's performance due to its heavy use of trace! calls
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
# Platform-specific tokio features
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] }
image = "0.25"
winit = "0.30"
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
bevy_flurx = "0.11"
bevy_flurx_ipc = "0.4.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { version = "1.0", features = ["rt"] }
[build-dependencies]
embed-resource = "1"

View File

@@ -14,31 +14,6 @@ use winit::window::Icon;
use bevy_webview_wry::WebviewWryPlugin;
fn main() {
#[cfg(target_arch = "wasm32")]
App::new()
.insert_resource(ClearColor(Color::NONE))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
// Bind to canvas included in `index.html`
canvas: Some("#yachtpit-canvas".to_owned()),
fit_canvas_to_parent: true,
// Tells wasm not to override default event handling, like F5 and Ctrl+R
prevent_default_event_handling: false,
..default()
}),
..default()
})
.set(AssetPlugin {
meta_check: AssetMetaCheck::Never,
..default()
}),
)
.add_plugins(GamePlugin)
.add_systems(Startup, set_window_icon)
.run();
#[cfg(not(target_arch = "wasm32"))]
App::new()
.insert_resource(ClearColor(Color::NONE))
@@ -65,7 +40,33 @@ fn main() {
.add_plugins(WebviewWryPlugin::default())
.run();
#[cfg(target_arch = "wasm32")]
App::new()
.insert_resource(ClearColor(Color::NONE))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
// Bind to canvas included in `index.html`
canvas: Some("#yachtpit-canvas".to_owned()),
fit_canvas_to_parent: true,
// Tells wasm not to override default event handling, like F5 and Ctrl+R
prevent_default_event_handling: false,
..default()
}),
..default()
})
.set(AssetPlugin {
meta_check: AssetMetaCheck::Never,
..default()
}),
)
.add_plugins(GamePlugin)
.add_systems(Startup, set_window_icon)
.run();
}
// Sets the icon on windows and X11
fn set_window_icon(
windows: NonSend<WinitWindows>,

View File

@@ -2,7 +2,9 @@ use bevy::prelude::*;
use bevy::render::view::RenderLayers;
use bevy::window::Window;
use std::collections::HashMap;
use bevy_flurx::prelude::*;
use bevy_webview_wry::prelude::*;
use serde::{Deserialize, Serialize};
/// Render layer for GPS map entities to isolate them from other cameras
@@ -12,6 +14,38 @@ use bevy_webview_wry::prelude::*;
/// Render layer for GPS map entities to isolate them from other cameras
const GPS_MAP_LAYER: usize = 1;
/// GPS position data
#[derive(Serialize, Debug, Clone)]
pub struct GpsPosition {
pub latitude: f64,
pub longitude: f64,
pub zoom: u8,
}
/// Vessel position and status data
#[derive(Serialize, Debug, Clone)]
pub struct VesselStatus {
pub latitude: f64,
pub longitude: f64,
pub heading: f64,
pub speed: f64,
}
/// Map view change parameters
#[derive(Deserialize, Debug, Clone)]
pub struct MapViewParams {
pub latitude: f64,
pub longitude: f64,
pub zoom: u8,
}
/// Authentication parameters
#[derive(Deserialize, Debug, Clone)]
pub struct AuthParams {
pub authenticated: bool,
pub token: Option<String>,
}
/// Component to mark the GPS map window
#[derive(Component)]
pub struct GpsMapWindow;
@@ -32,6 +66,10 @@ pub struct GpsMapState {
pub center_lon: f64,
pub zoom_level: u8,
pub tile_cache: HashMap<String, Handle<Image>>,
pub vessel_lat: f64,
pub vessel_lon: f64,
pub vessel_heading: f64,
pub vessel_speed: f64,
}
impl GpsMapState {
@@ -42,6 +80,10 @@ impl GpsMapState {
center_lon: -1.4497,
zoom_level: 10,
tile_cache: HashMap::new(),
vessel_lat: 43.6377, // Default vessel position
vessel_lon: -1.4497,
vessel_heading: 0.0,
vessel_speed: 0.0,
}
}
}
@@ -52,7 +94,11 @@ pub struct GpsMapPlugin;
impl Plugin for GpsMapPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<GpsMapState>()
.add_systems(Update, (handle_gps_map_window_events, update_map_tiles));
.add_systems(Update, (
handle_gps_map_window_events,
update_map_tiles,
send_periodic_gps_updates,
));
}
}
@@ -184,9 +230,128 @@ pub fn spawn_gps_map_window(commands: &mut Commands, gps_map_state: &mut ResMut<
#[cfg(not(target_arch = "wasm32"))]
fn spawn_gps_webview(commands: &mut Commands, gps_map_state: &mut ResMut<GpsMapState>) {
if let Some(win) = gps_map_state.window_id {
commands.entity(win).insert(Webview::Uri(WebviewUri::relative_local(
// Using the build output of the base-map package
"packages/base-map/dist/index.html",
)));
commands.entity(win).insert((
IpcHandlers::new([
navigation_clicked,
search_clicked,
map_view_changed,
auth_status_changed,
get_map_init,
get_vessel_status
]),
Webview::Uri(WebviewUri::relative_local(
// Using the build output of the base-map package
"packages/base-map/dist/index.html",
))
));
}
}
}
// GPS Map IPC Commands using bevy_flurx_ipc
/// Handle navigation button click
#[command]
fn navigation_clicked(
WebviewEntity(_entity): WebviewEntity,
) -> Action<(), ()> {
once::run(|_: In<()>| {
info!("Navigation button clicked in React");
// Handle navigation logic here
}).into()
}
/// Handle search button click
#[command]
fn search_clicked(
WebviewEntity(_entity): WebviewEntity,
) -> Action<(), ()> {
once::run(|_: In<()>| {
info!("Search button clicked in React");
// Handle search logic here
}).into()
}
/// Handle map view change
#[command]
fn map_view_changed(
In(params): In<MapViewParams>,
WebviewEntity(_entity): WebviewEntity,
) -> Action<(f64, f64, u8), ()> {
once::run(|In((latitude, longitude, zoom)): In<(f64, f64, u8)>, mut gps_map_state: ResMut<GpsMapState>| {
info!("Map view changed: lat={}, lon={}, zoom={}", latitude, longitude, zoom);
gps_map_state.center_lat = latitude;
gps_map_state.center_lon = longitude;
gps_map_state.zoom_level = zoom;
}).with((params.latitude, params.longitude, params.zoom)).into()
}
/// Handle authentication status change
#[command]
fn auth_status_changed(
In(params): In<AuthParams>,
WebviewEntity(_entity): WebviewEntity,
) -> Action<(bool, Option<String>), ()> {
once::run(|In((authenticated, token)): In<(bool, Option<String>)>| {
info!("Auth status changed: authenticated={}, token={:?}", authenticated, token);
// Handle authentication status change
}).with((params.authenticated, params.token)).into()
}
/// Get map initialization data
#[command]
async fn get_map_init(
WebviewEntity(_entity): WebviewEntity,
task: ReactorTask,
) -> GpsPosition {
task.will(Update, once::run(|gps_map_state: Res<GpsMapState>| {
GpsPosition {
latitude: gps_map_state.center_lat,
longitude: gps_map_state.center_lon,
zoom: gps_map_state.zoom_level,
}
})).await
}
/// Get current vessel status
#[command]
async fn get_vessel_status(
WebviewEntity(_entity): WebviewEntity,
task: ReactorTask,
) -> VesselStatus {
task.will(Update, once::run(|gps_map_state: Res<GpsMapState>| {
VesselStatus {
latitude: gps_map_state.vessel_lat,
longitude: gps_map_state.vessel_lon,
heading: gps_map_state.vessel_heading,
speed: gps_map_state.vessel_speed,
}
})).await
}
/// System to send periodic GPS updates for testing
fn send_periodic_gps_updates(
mut gps_map_state: ResMut<GpsMapState>,
time: Res<Time>,
) {
// Update vessel position every frame for testing
if time.delta_secs() > 0.0 {
// Simulate slight movement around Monaco
let base_lat = 43.6377;
let base_lon = -1.4497;
let offset = (time.elapsed_secs().sin() * 0.001) as f64;
gps_map_state.vessel_lat = base_lat + offset;
gps_map_state.vessel_lon = base_lon + offset * 0.5;
gps_map_state.vessel_speed = 5.0 + (time.elapsed_secs().cos() * 2.0) as f64;
gps_map_state.vessel_heading = ((time.elapsed_secs() * 10.0) % 360.0) as f64;
// React side can poll for updates using get_vessel_status command
if time.elapsed_secs() as u32 % 5 == 0 && time.delta_secs() < 0.1 {
info!("Vessel position updated: lat={:.4}, lon={:.4}, speed={:.1}, heading={:.1}",
gps_map_state.vessel_lat, gps_map_state.vessel_lon,
gps_map_state.vessel_speed, gps_map_state.vessel_heading);
}
}
}