rustial-renderer-bevy 1.0.0

Bevy Engine renderer for the rustial 2.5D map engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
// ---------------------------------------------------------------------------
//! # Bevy ECS marker components for map entities
//!
//! Every visual entity managed by the rustial Bevy plugin carries one
//! of the marker components defined here.  The plugin's sync systems
//! use these components to query, reposition, and despawn entities as
//! the map state changes each frame.
//!
//! ## Component to system mapping
//!
//! ```text
//! Component        Spawned / consumed by            Schedule
//! ---------------------------------------------------------------
//! MapCamera        setup_from_config                Startup
//! TileEntity       sync_tiles                       Update
//! TerrainEntity    sync_terrain                     Update
//! VectorEntity     sync_vectors                     Update
//! ModelEntity      sync_models                      Update
//! MapEntity +      sync_geo_entities                Update
//!   GeoTransform     (user-spawned, system repositions)
//! ```
//!
//! ## Camera-relative positioning
//!
//! All map entities are rendered relative to the camera's world-space
//! origin ([`Camera::target_world`](rustial_engine::Camera::target_world))
//! so that large Web Mercator coordinates are reduced to small f32
//! values before reaching the GPU (avoids floating-point jitter).
//!
//! Two patterns are used depending on how the entity's mesh data is
//! produced:
//!
//! ### Pattern A -- `spawn_origin` offset (terrain, vectors)
//!
//! Vertex positions are baked into the mesh at spawn time as
//! `world_pos - camera_origin`.  The entity stores that camera
//! origin in a `spawn_origin` field.  Each frame the sync system
//! sets `Transform.translation = spawn_origin - current_camera_origin`,
//! so the net vertex position is always `world_pos - current_camera_origin`
//! without ever re-uploading mesh data.
//!
//! ### Pattern B -- per-frame world query (tiles, models)
//!
//! Tile quad meshes use small half-extent vertices centred on the
//! mesh origin; the tile's world centre is recomputed from
//! [`tile_bounds_world`](rustial_engine::tile_bounds_world) each frame.
//! Model entities reproject their `GeoCoord` each frame.  These
// entities do not need a `spawn_origin` because the world position is
// cheap to recompute and the mesh data is position-independent.
//
// ### Pattern C -- user entities (MapEntity + GeoTransform)
//
// User-spawned entities are repositioned each frame by
// `sync_geo_entities`, which projects the `GeoTransform` coordinate and
// writes `Transform.translation` directly.
// ---------------------------------------------------------------------------

use bevy::prelude::*;
use glam::DVec3;
use rustial_engine::{CameraProjection, GeoCoord, TileId, VectorRenderMode};

// ---------------------------------------------------------------------------
// Camera
// ---------------------------------------------------------------------------

/// Marker component for the Bevy camera controlled by the map plugin.
#[derive(Component)]
pub struct MapCamera;

// ---------------------------------------------------------------------------
// Deferred asset cleanup
// ---------------------------------------------------------------------------

/// Resource that holds mesh and material handles for one extra frame
/// after their entities are despawned.
///
/// When an entity is despawned, its `Handle<Mesh>` and
/// `Handle<TileFogMaterial>` may be the last strong references.  If
/// dropped immediately, Bevy frees the underlying GPU buffers -- but
/// the render world may have already extracted them for the current
/// frame's render pass.  Accessing a freed buffer triggers a wgpu
/// validation panic ("Buffer is invalid").
///
/// This resource acts as a one-frame keep-alive: handles are pushed
/// here on the frame of despawn and drained on the *next* frame,
/// giving the render pipeline time to finish using them.
#[derive(Resource, Default)]
pub struct DeferredAssetDrop {
    /// Mesh handles kept alive from the previous frame's despawns.
    prev_meshes: Vec<Handle<bevy::prelude::Mesh>>,
    /// Material handles kept alive from the previous frame's despawns.
    prev_materials: Vec<Handle<crate::tile_fog_material::TileFogMaterial>>,
    /// Hillshade material handles kept alive from the previous frame's despawns.
    prev_hillshade_materials: Vec<Handle<crate::hillshade_material::HillshadeMaterial>>,
    /// Grid scalar material handles kept alive from the previous frame's despawns.
    prev_grid_scalar_materials: Vec<Handle<crate::grid_scalar_material::GridScalarMaterial>>,
    /// Mesh handles from this frame's despawns (promoted to `prev` next frame).
    curr_meshes: Vec<Handle<bevy::prelude::Mesh>>,
    /// Material handles from this frame's despawns (promoted to `prev` next frame).
    curr_materials: Vec<Handle<crate::tile_fog_material::TileFogMaterial>>,
    /// Hillshade material handles from this frame's despawns (promoted to `prev` next frame).
    curr_hillshade_materials: Vec<Handle<crate::hillshade_material::HillshadeMaterial>>,
    /// Grid scalar material handles from this frame's despawns (promoted to `prev` next frame).
    curr_grid_scalar_materials: Vec<Handle<crate::grid_scalar_material::GridScalarMaterial>>,
}

impl DeferredAssetDrop {
    /// Stash a mesh handle to keep it alive until the next frame.
    pub fn keep_mesh(&mut self, handle: Handle<bevy::prelude::Mesh>) {
        self.curr_meshes.push(handle);
    }

    /// Stash a material handle to keep it alive until the next frame.
    pub fn keep_material(&mut self, handle: Handle<crate::tile_fog_material::TileFogMaterial>) {
        self.curr_materials.push(handle);
    }

    /// Stash a hillshade material handle to keep it alive until the next frame.
    pub fn keep_hillshade_material(
        &mut self,
        handle: Handle<crate::hillshade_material::HillshadeMaterial>,
    ) {
        self.curr_hillshade_materials.push(handle);
    }

    /// Stash a grid scalar material handle to keep it alive until the next frame.
    pub fn keep_grid_scalar_material(
        &mut self,
        handle: Handle<crate::grid_scalar_material::GridScalarMaterial>,
    ) {
        self.curr_grid_scalar_materials.push(handle);
    }

    /// Advance the deferred drop by one frame: drop `prev` handles
    /// (now two frames old) and promote `curr` to `prev`.
    pub fn advance_frame(&mut self) {
        self.prev_meshes.clear();
        self.prev_materials.clear();
        self.prev_hillshade_materials.clear();
        self.prev_grid_scalar_materials.clear();
        std::mem::swap(&mut self.prev_meshes, &mut self.curr_meshes);
        std::mem::swap(&mut self.prev_materials, &mut self.curr_materials);
        std::mem::swap(
            &mut self.prev_hillshade_materials,
            &mut self.curr_hillshade_materials,
        );
        std::mem::swap(
            &mut self.prev_grid_scalar_materials,
            &mut self.curr_grid_scalar_materials,
        );
    }
}

// ---------------------------------------------------------------------------
// Tile entities
// ---------------------------------------------------------------------------

/// Marker component for tile quad entities.
///
/// Each tile stores the camera origin and map projection used when its
/// projection-specialized quad was baked, allowing the mesh to be
/// repositioned cheaply as the camera moves and rebuilt when the active
/// planar projection changes.
///
/// The [`upload_textures`](crate::systems::texture_upload::upload_textures)
/// system later assigns raster imagery as `base_color_texture` on
/// the tile's [`StandardMaterial`].
#[derive(Component)]
pub struct TileEntity {
    /// The slippy-map tile this entity represents (`zoom/x/y`).
    pub tile_id: TileId,
    /// Camera world-space origin at the time the tile mesh was built.
    pub spawn_origin: DVec3,
    /// Active camera projection at the time the tile mesh was built.
    pub projection: CameraProjection,
    /// Whether the entity currently has the exact tile texture (not a
    /// cropped parent fallback). When `false`, the texture upload system
    /// continues checking each frame so it can replace the fallback with
    /// exact imagery as soon as it becomes available.
    pub has_exact_texture: bool,
}

// ---------------------------------------------------------------------------
// Terrain entities
// ---------------------------------------------------------------------------

/// Marker component for terrain mesh entities.
///
/// Tracks both the tile identity and the camera origin at which the
/// mesh vertex positions were baked.  The
/// [`sync_terrain`](crate::systems::terrain_sync::sync_terrain)
/// system uses `spawn_origin` to compute a `Transform` offset each
/// frame so that vertices remain correctly camera-relative as the
/// user pans, without needing to rebuild the mesh.
///
/// ## Repositioning formula
///
/// ```text
/// Transform.translation = spawn_origin - current_camera_origin
///
/// vertex_world = vertex_mesh + translation
///              = (world_pos - spawn_origin) + (spawn_origin - camera_origin)
///              = world_pos - camera_origin
/// ```
#[derive(Component)]
pub struct TerrainEntity {
    /// The tile this terrain mesh represents.
    pub tile_id: TileId,
    /// Camera world-space origin at the time the mesh was built.
    ///
    /// Vertex positions in the mesh asset are stored as
    /// `world_pos - spawn_origin`.  Each frame the system sets
    /// `Transform.translation = spawn_origin - current_camera_origin`
    /// so that the net position equals `world_pos - current_camera_origin`.
    pub spawn_origin: DVec3,
    /// Active camera projection at the time the terrain mesh was built.
    pub projection: CameraProjection,
    /// Whether the entity uses the shared reusable grid plus GPU height
    /// sampling path instead of CPU-materialized displaced geometry.
    pub gpu_displaced: bool,
    /// Elevation-cache generation at the time this entity was spawned.
    ///
    /// When the engine's `TerrainMeshData::generation` for this tile
    /// exceeds this value, the mesh must be rebuilt (e.g. a flat
    /// placeholder was replaced by real elevation data).
    pub mesh_generation: u64,
    /// Whether the entity currently has the exact tile texture (not a
    /// parent fallback).  When `false`, the texture upload system will
    /// try to replace the fallback with the exact texture each frame.
    pub has_exact_texture: bool,
}

/// Marker component for dedicated hillshade overlay entities.
#[derive(Component)]
pub struct HillshadeEntity {
    /// The tile this hillshade overlay represents.
    pub tile_id: TileId,
    /// Camera world-space origin at the time the mesh was built.
    pub spawn_origin: DVec3,
    /// Active camera projection at the time the terrain mesh was built.
    pub projection: CameraProjection,
    /// Whether the entity uses the shared reusable grid plus GPU height
    /// sampling path instead of CPU-materialized displaced geometry.
    pub gpu_displaced: bool,
    /// Elevation-cache generation at the time this entity was spawned.
    pub mesh_generation: u64,
}

// ---------------------------------------------------------------------------
// Vector entities
// ---------------------------------------------------------------------------

/// Marker component for vector geometry entities.
///
/// Tracks the layer name and the camera origin at which the mesh
/// vertex positions were baked.  The
/// [`sync_vectors`](crate::systems::vector_sync::sync_vectors)
/// system uses `spawn_origin` to compute a `Transform` offset each
/// frame -- the same repositioning formula as [`TerrainEntity`].
///
/// ## Rebuild strategy
///
/// Vector entities use a **rebuild-on-change** lifecycle: when the
/// engine's `vector_meshes` count changes all entities are despawned
/// and respawned.  When the count is stable, only the `Transform`
/// translation is updated (cheap reposition path).
#[derive(Component)]
pub struct VectorEntity {
    /// Name of the vector layer this entity belongs to.
    pub layer_name: String,
    /// Camera world-space origin at the time the mesh was built.
    ///
    /// Vertex positions in the mesh asset are stored as
    /// `world_pos - spawn_origin`.  Each frame the system sets
    /// `Transform.translation = spawn_origin - current_camera_origin`
    /// so that the net position equals `world_pos - current_camera_origin`.
    pub spawn_origin: DVec3,
    /// Render mode of the vector layer this entity belongs to.
    ///
    /// Used by the sync system to select mode-specific materials
    /// (e.g. additive blending for heatmaps, lit shading for
    /// fill-extrusions, unlit flat for 2-D fills / lines).
    pub render_mode: VectorRenderMode,
}

/// Marker component for image overlay entities.
///
/// Each image overlay is a textured quad spanning four geographic
/// corners. The sync system spawns one entity per
/// [`ImageOverlayData`](rustial_engine::layers::ImageOverlayData)
/// and uses `spawn_origin` for camera-relative repositioning.
#[derive(Component)]
pub struct ImageOverlayEntity {
    /// Engine layer id that produced this overlay.
    pub layer_id: rustial_engine::LayerId,
    /// Camera world-space origin at the time the mesh was built.
    pub spawn_origin: DVec3,
}

/// Marker component for grid scalar overlay entities.
#[derive(Component)]
pub struct GridScalarEntity {
    /// Layer identity from the engine visualization overlay.
    pub layer_id: rustial_engine::LayerId,
    /// Structural generation of the scalar field.
    pub generation: u64,
    /// Value-only generation of the scalar field.
    pub value_generation: u64,
    /// Fingerprint of the color ramp used by this entity.
    pub ramp_fingerprint: u64,
}

/// Marker component for extruded grid overlay entities.
#[derive(Component)]
pub struct GridExtrusionEntity {
    /// Layer identity from the engine visualization overlay.
    pub layer_id: rustial_engine::LayerId,
    /// Camera-relative origin at the time the mesh was baked.
    pub spawn_origin: DVec3,
    /// Value-only generation of the scalar field at the time the mesh was last updated.
    pub value_generation: u64,
    /// Active projection at the time the mesh was baked.
    pub projection: CameraProjection,
}

/// Marker component for instanced column overlay entities.
#[derive(Component)]
pub struct ColumnEntity {
    /// Layer identity from the engine visualization overlay.
    pub layer_id: rustial_engine::LayerId,
    /// Stable per-layer column index.
    pub column_index: usize,
}

/// Marker component for point-cloud overlay entities.
#[derive(Component)]
pub struct PointCloudEntity {
    /// Layer identity from the engine visualization overlay.
    pub layer_id: rustial_engine::LayerId,
    /// Stable per-layer point index.
    pub point_index: usize,
}

// ---------------------------------------------------------------------------
// Model entities
// ---------------------------------------------------------------------------

/// Marker component for 3D model entities.
///
/// Used by [`sync_models`](crate::systems::model_sync::sync_models)
/// to track which engine model instance each Bevy entity represents.
///
/// ## Rebuild strategy
///
/// Model entities use a **rebuild-on-change** lifecycle similar to
/// vectors.  When the instance count matches, only the `Transform` is
/// updated each frame (the model's `GeoCoord` is reprojected and the
/// camera origin subtracted -- no `spawn_origin` needed because the
/// transform is fully recomputed from the instance data).
///
/// When the instance count changes, all model entities are despawned
/// and respawned from scratch.
#[derive(Component)]
pub struct ModelEntity {
    /// Index of the model instance in the engine's
    /// [`model_instances`](rustial_engine::MapState::model_instances)
    /// list.  Used by the reposition path to find the corresponding
    /// instance without a full rebuild.
    pub instance_index: usize,
    /// Name of the model layer this entity belongs to.
    pub layer_name: String,
}

// ---------------------------------------------------------------------------
// User geographic entities
// ---------------------------------------------------------------------------

/// Tag component for user-placed entities that should be automatically
/// repositioned each frame relative to the map camera origin.
///
/// Attach this to any Bevy entity along with a [`GeoTransform`] to
/// have the plugin keep its `Transform.translation` in sync with the
/// map view.  User-supplied rotation and scale are preserved -- only
/// the translation is overwritten.
///
/// ## Required companion components
///
/// | Component | Purpose |
/// |-----------|---------|
/// | [`GeoTransform`] | Geographic position + altitude mode |
/// | [`Transform`] | Written by [`sync_geo_entities`](crate::systems::geo_entity_sync::sync_geo_entities) |
/// | [`Visibility`] | Standard Bevy visibility |
///
/// ## Example
///
/// ```rust,no_run
/// use bevy::prelude::*;
/// use rustial_renderer_bevy::components::{MapEntity, GeoTransform};
///
/// fn spawn_pin(mut commands: Commands) {
///     commands.spawn((
///         MapEntity,
///         GeoTransform::from_lat_lon(48.8566, 2.3522), // Paris
///         Transform::default(),
///         Visibility::default(),
///         // ... add your own mesh, sprite, etc.
///     ));
/// }
/// ```
#[derive(Component)]
pub struct MapEntity;

/// Geographic position and altitude mode for a user entity on the map.
///
/// Pair with [`MapEntity`] on a Bevy entity.  Each frame the
/// [`sync_geo_entities`](crate::systems::geo_entity_sync::sync_geo_entities)
/// system projects this coordinate to camera-relative Bevy
/// `Transform.translation`, keeping the entity aligned with the tile
/// layer regardless of panning, zooming, or rotation.
///
/// # Construction
///
/// | Method | Altitude | Mode |
/// |--------|----------|------|
/// | [`from_lat_lon`](Self::from_lat_lon) | 0 | `ClampToGround` |
/// | [`new`](Self::new) | from `GeoCoord.alt` | explicit |
///
/// # Example
///
/// ```rust,no_run
/// use bevy::prelude::*;
/// use rustial_renderer_bevy::components::{MapEntity, GeoTransform};
///
/// fn spawn_pin(mut commands: Commands) {
///     commands.spawn((
///         MapEntity,
///         GeoTransform::from_lat_lon(48.8566, 2.3522),
///         Transform::default(),
///         Visibility::default(),
///     ));
/// }
/// ```
#[derive(Component, Debug, Clone, Copy)]
pub struct GeoTransform {
    /// Geographic position (WGS-84, degrees + meters).
    pub coord: GeoCoord,
    /// How the entity's altitude is resolved relative to terrain.
    pub altitude_mode: GeoAltitudeMode,
}

/// How a [`GeoTransform`] entity's altitude is resolved.
///
/// Used by [`sync_geo_entities`](crate::systems::geo_entity_sync::sync_geo_entities)
/// to compute the Z component of `Transform.translation`.
///
/// When terrain is disabled or the elevation tile for the entity's
/// coordinate has not loaded yet, ground elevation is treated as 0 m.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GeoAltitudeMode {
    /// Place the entity on the ground surface (Z = terrain elevation,
    /// or 0 when terrain data is unavailable).
    #[default]
    ClampToGround,
    /// Altitude is meters **above the ground surface**
    /// (Z = terrain elevation + `GeoCoord.alt`).
    RelativeToGround,
    /// Altitude is the absolute Z value in world space
    /// (Z = `GeoCoord.alt`, terrain ignored).
    Absolute,
}

impl GeoTransform {
    /// Create a ground-clamped position from latitude and longitude.
    ///
    /// Altitude defaults to 0, mode defaults to
    /// [`ClampToGround`](GeoAltitudeMode::ClampToGround).
    #[inline]
    pub fn from_lat_lon(lat: f64, lon: f64) -> Self {
        Self {
            coord: GeoCoord::from_lat_lon(lat, lon),
            altitude_mode: GeoAltitudeMode::ClampToGround,
        }
    }

    /// Create a position with an explicit coordinate and altitude mode.
    #[inline]
    pub fn new(coord: GeoCoord, altitude_mode: GeoAltitudeMode) -> Self {
        Self {
            coord,
            altitude_mode,
        }
    }
}

// ---------------------------------------------------------------------------
// Placeholder entities
// ---------------------------------------------------------------------------

/// Marker component for loading-placeholder entities.
///
/// Placeholder entities are flat coloured quads displayed at tile
/// world bounds while tile data is still loading.  They are managed
/// by [`sync_placeholders`](crate::systems::placeholder_sync::sync_placeholders)
/// which despawns and respawns them every frame based on the engine's
/// [`LoadingPlaceholder`](rustial_engine::LoadingPlaceholder) list.
#[derive(Component)]
pub struct PlaceholderEntity {
    /// The tile this placeholder stands in for.
    pub tile_id: TileId,
}