gizmo-renderer 0.9.1

A custom ECS and physics engine aimed for realistic simulations.
Documentation
use gizmo_math::Vec3;

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Terrain {
    pub heightmap_path: String,
    pub width: f32,
    pub depth: f32,
    pub max_height: f32,
}

impl Terrain {
    pub fn new(heightmap_path: String, width: f32, depth: f32, max_height: f32) -> Self {
        assert!(
            width > 0.0,
            "Kullanım hatası: Terrain width sıfırdan büyük olmalıdır."
        );
        assert!(
            depth > 0.0,
            "Kullanım hatası: Terrain depth sıfırdan büyük olmalıdır."
        );
        assert!(
            max_height > 0.0,
            "Kullanım hatası: Terrain max_height sıfırdan büyük olmalıdır."
        );
        Self {
            heightmap_path,
            width,
            depth,
            max_height,
        }
    }
}

#[derive(Clone)]
pub struct LodGroup {
    pub levels: Vec<LodLevel>,
}

#[derive(Clone)]
pub struct LodLevel {
    pub mesh: super::mesh::Mesh,
    pub max_distance: f32,
}

impl LodLevel {
    pub fn new(mesh: super::mesh::Mesh, max_distance: f32) -> Self {
        assert!(
            max_distance >= 0.0,
            "Kullanım hatası: LodLevel max_distance negatif olamaz."
        );
        Self { mesh, max_distance }
    }
}

impl LodGroup {
    pub fn new(levels: Vec<LodLevel>) -> Self {
        assert!(
            !levels.is_empty(),
            "Kullanım hatası: LodGroup en az 1 LodLevel içermelidir (Boş liste görünmezlik hatalarına yol açar)."
        );
        let mut levels = levels;
        levels.sort_by(|a, b| a.max_distance.total_cmp(&b.max_distance));
        Self { levels }
    }

    pub fn select_mesh(&self, distance: f32) -> Option<&super::mesh::Mesh> {
        self.select_level(distance).map(|i| &self.levels[i].mesh)
    }

    /// Which mesh an entity draws this frame: the LOD level for `distance` if it has a group,
    /// its own [`Mesh`](super::mesh::Mesh) if it does not, and `None` — cull — past the last
    /// level.
    ///
    /// Both render paths ask this question and each answered it inline, in eight lines that had
    /// to agree on all three cases: that a group **overrides** the entity's own mesh rather than
    /// competing with it, and that running off the end of the levels means "do not draw" rather
    /// than "draw the coarsest". They did agree, which is the good case and not a durable one —
    /// `LodGroup` was honoured only by the editor for a long time, so the engine's copy of this
    /// is younger than the feature.
    ///
    /// Takes the distance rather than the positions: the two paths reach the entity's world
    /// translation by different routes (a `GlobalTransform` here, the assembled model matrix
    /// there) and that part is genuinely theirs.
    pub fn pick<'a>(
        group: Option<&'a Self>,
        own_mesh: &'a super::mesh::Mesh,
        distance: f32,
    ) -> Option<&'a super::mesh::Mesh> {
        match group {
            Some(lod) => lod.select_mesh(distance),
            None => Some(own_mesh),
        }
    }

    /// Which level a distance falls in, or `None` past the last one.
    ///
    /// The policy on its own, separated from the mesh it picks. A [`super::mesh::Mesh`] owns
    /// `wgpu::Buffer`s and cannot be built without a device, so [`Self::select_mesh`] is not
    /// testable off a GPU — and this decision is worth testing, because the engine's own render
    /// pass now depends on it: the bounds decide what is drawn, and the `None` decides what is
    /// not drawn at all.
    #[must_use]
    pub fn select_level(&self, distance: f32) -> Option<usize> {
        Self::level_for(self.levels.iter().map(|l| l.max_distance), distance)
    }

    /// [`Self::select_level`] over bare bounds, so it can be tested without a GPU.
    ///
    /// The bounds are ascending — [`Self::new`] sorts them — so the first one the distance is
    /// within is the finest level that still covers it.
    fn level_for(bounds: impl Iterator<Item = f32>, distance: f32) -> Option<usize> {
        bounds
            .enumerate()
            .find(|(_, bound)| distance <= *bound)
            .map(|(i, _)| i)
    }
}

#[cfg(test)]
mod lod_tests {
    use super::*;

    fn pick(bounds: &[f32], distance: f32) -> Option<usize> {
        LodGroup::level_for(bounds.iter().copied(), distance)
    }

    /// Bands are closed at the top: a distance exactly on a bound belongs to that level, not the
    /// next one.
    #[test]
    fn a_distance_inside_a_band_picks_that_band() {
        let bands = [10.0, 50.0, 200.0];
        assert_eq!(pick(&bands, 0.0), Some(0));
        assert_eq!(pick(&bands, 9.9), Some(0));
        assert_eq!(pick(&bands, 10.0), Some(0), "a bound belongs to its own level");
        assert_eq!(pick(&bands, 10.1), Some(1));
        assert_eq!(pick(&bands, 200.0), Some(2));
    }

    #[test]
    fn past_the_last_band_is_culled_rather_than_drawn_coarsest() {
        assert_eq!(
            pick(&[10.0, 50.0, 200.0], 200.1),
            None,
            "past the last bound the object is culled — drawing the coarsest level instead would \
             make a LOD group a thing that can never disappear"
        );
    }
}

#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct ParticleEmitter {
    pub spawn_rate: f32,
    accumulator: f32,
    pub local_offset: Vec3,
    pub initial_velocity: Vec3,
    pub velocity_randomness: f32,
    pub lifespan: f32,
    pub lifespan_randomness: f32,
    pub size_start: f32,
    pub size_end: f32,
    pub color_start: gizmo_math::Vec4,
    pub color_end: gizmo_math::Vec4,
    pub texture_source: Option<String>,
    pub is_active: bool,
}

impl Default for ParticleEmitter {
    fn default() -> Self {
        Self::new()
    }
}

impl ParticleEmitter {
    pub fn new() -> Self {
        Self {
            spawn_rate: 10.0,
            accumulator: 0.0,
            local_offset: Vec3::ZERO,
            initial_velocity: Vec3::new(0.0, 1.0, 0.0),
            velocity_randomness: 0.5,
            lifespan: 2.0,
            lifespan_randomness: 0.5,
            size_start: 0.5,
            size_end: 0.1,
            color_start: gizmo_math::Vec4::new(1.0, 0.5, 0.1, 1.0),
            color_end: gizmo_math::Vec4::new(1.0, 0.2, 0.0, 0.0),
            texture_source: None,
            is_active: true,
        }
    }

    /// Yeni bir parçacık yayıcı oluşturur ve doğrulama yapar.
    pub fn with_rate(spawn_rate: f32) -> Self {
        assert!(
            spawn_rate > 0.0,
            "Kullanım hatası: ParticleEmitter spawn_rate sıfırdan büyük olmalıdır."
        );
        Self {
            spawn_rate,
            ..Self::default()
        }
    }

    pub fn get_accumulator(&self) -> f32 {
        self.accumulator
    }

    pub fn add_time(&mut self, dt: f32) {
        self.accumulator += dt;
    }

    pub fn consume_time(&mut self, dt: f32) {
        self.accumulator -= dt;
    }
}

#[derive(Clone)]
pub struct RenderTarget {
    pub view: std::sync::Arc<wgpu::TextureView>,
    pub width: u32,
    pub height: u32,
}

#[derive(Clone)]
pub struct EditorRenderTarget(pub RenderTarget);

#[derive(Clone)]
pub struct GameRenderTarget(pub RenderTarget);

/// What the last frame actually cost, published by whichever render path drew it.
///
/// The editor's viewport overlay reads this. It exists because the numbers it shows have to be
/// *measured*: `StudioState::draw_call_count` was a field nothing ever wrote, and an overlay that
/// prints a plausible-looking zero is worse than one that prints nothing.
///
/// Frame time is deliberately absent — that belongs to `gizmo_core::FrameProfiler`, which already
/// measures it for every configuration, editor or not.
// No `Eq`: the sample timestamp is a float. `PartialEq` is enough for the one comparison anyone
// makes of these — "did the frame change" in a test.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RenderStats {
    /// Draw calls recorded for the main pass — one per batch that reached the pass.
    pub draw_calls: u32,
    /// Triangles submitted, counting instances: `Σ (indices / 3) × instances`.
    pub triangles: u32,
    /// Instances uploaded this frame, across all batches.
    pub instances: u32,
    /// GPU memory wgpu has allocated, in bytes — `None` when the backend does not report it.
    ///
    /// This is the allocator's view (`Device::generate_allocator_report`), not the driver's total
    /// VRAM: it counts what this process has sub-allocated, which is the number an engine can
    /// actually act on. Labelled accordingly wherever it is shown, because "VRAM" would imply the
    /// card's usage including every other process.
    ///
    /// `None` is a real answer and must stay one: some backends never report, and a zero would
    /// read as "nothing allocated".
    pub gpu_allocated_bytes: Option<u64>,
    /// When the sample above was taken, in seconds of `Time::elapsed`.
    ///
    /// The report walks every live allocation, so it is sampled about once a second rather than
    /// per frame; this is what the sampler compares against.
    pub gpu_sampled_at: f32,
}


#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FluidPhaseType {
    Water,
    Foam,
    Bubble,
}

/// Bir entity'nin sıvı parçacığı olduğunu belirten ECS marker bileşenidir.
/// Simülasyonda FluidHandle, FluidPhase, FluidInteractor ile birlikte kullanılabilir.
#[derive(Clone)]
pub struct FluidParticle;

#[derive(Clone)]
pub struct FluidHandle {
    pub gpu_index: u32,
}

#[derive(Clone)]
pub struct FluidPhase {
    pub phase: FluidPhaseType,
}

#[derive(Clone)]
pub struct FluidInteractor {
    pub collider_gpu_index: u32,
    pub buoyancy_factor: f32,
    pub radius: f32,
    pub velocity: gizmo_math::Vec3,
}