use crate::components::VectorEntity;
use crate::plugin::MapStateResource;
use crate::systems::frame_change_detection::{frame_unchanged, FrameChangeDetection};
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use glam::DVec3;
use rustial_engine::{VectorMeshData, VectorRenderMode};
fn quantise_origin(origin: DVec3) -> [i64; 3] {
[
(origin.x * 100.0) as i64,
(origin.y * 100.0) as i64,
(origin.z * 100.0) as i64,
]
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
struct VectorFingerprint {
layers: Vec<(usize, usize, u8, u32)>,
}
impl VectorFingerprint {
fn from_meshes(meshes: &[VectorMeshData]) -> Self {
Self {
layers: meshes
.iter()
.map(|m| {
(
m.positions.len(),
m.indices.len(),
m.render_mode as u8,
m.fill_opacity.to_bits(),
)
})
.collect(),
}
}
}
#[derive(Resource, Default)]
pub struct VectorSyncState {
fingerprint: VectorFingerprint,
entity_count: usize,
last_origin: [i64; 3],
}
pub fn sync_vectors(
mut commands: Commands,
state: Res<MapStateResource>,
mut existing: Query<(Entity, &VectorEntity, &mut Transform)>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut sync_state: ResMut<VectorSyncState>,
detection: Res<FrameChangeDetection>,
) {
if frame_unchanged(&detection, &state.0) {
return;
}
let camera_origin: DVec3 = state.0.scene_world_origin();
let vector_data = &state.0.vector_meshes();
if vector_data.is_empty() {
for (entity, _, _) in existing.iter() {
commands.entity(entity).despawn();
log::trace!("vector_sync: despawned (vector data empty)");
}
sync_state.fingerprint = VectorFingerprint::default();
sync_state.entity_count = 0;
sync_state.last_origin = [0; 3];
return;
}
let new_fingerprint = VectorFingerprint::from_meshes(vector_data);
if new_fingerprint == sync_state.fingerprint && sync_state.entity_count == vector_data.len() {
let origin_key = quantise_origin(camera_origin);
if origin_key == sync_state.last_origin {
return;
}
for (_, vec_entity, mut transform) in existing.iter_mut() {
let offset = vec_entity.spawn_origin - camera_origin;
transform.translation =
Vec3::new(offset.x as f32, offset.y as f32, offset.z as f32 + 0.1);
}
sync_state.last_origin = origin_key;
return;
}
for (entity, _, _) in existing.iter() {
commands.entity(entity).despawn();
log::trace!("vector_sync: despawned (structural change)");
}
let mut spawned = 0usize;
for (idx, vmesh) in vector_data.iter().enumerate() {
if vmesh.positions.is_empty() || vmesh.indices.is_empty() {
log::trace!(
"vector_sync: skipping degenerate mesh at index {} ({} verts, {} indices)",
idx,
vmesh.positions.len(),
vmesh.indices.len(),
);
continue;
}
let positions: Vec<[f32; 3]> = vmesh
.positions
.iter()
.map(|p| {
[
(p[0] - camera_origin.x) as f32,
(p[1] - camera_origin.y) as f32,
(p[2] - camera_origin.z) as f32,
]
})
.collect();
let vertex_count = positions.len();
let normals = if vmesh.has_normals() {
vmesh.normals.clone()
} else {
compute_vertex_normals(&positions, &vmesh.indices)
};
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, vmesh.colors.clone());
mesh.insert_indices(Indices::U32(vmesh.indices.clone()));
let mesh_handle = meshes.add(mesh);
let material_handle = material_for_mode(vmesh, &mut materials);
commands.spawn((
Mesh3d(mesh_handle),
MeshMaterial3d(material_handle),
Transform::from_xyz(0.0, 0.0, 0.1), Visibility::default(),
VectorEntity {
layer_name: format!("vector_{idx}"),
spawn_origin: camera_origin,
render_mode: vmesh.render_mode,
},
));
spawned += 1;
log::trace!(
"vector_sync: spawned mesh index {} ({} verts, {} tris)",
idx,
vertex_count,
vmesh.indices.len() / 3,
);
}
sync_state.fingerprint = new_fingerprint;
sync_state.entity_count = spawned;
sync_state.last_origin = quantise_origin(camera_origin);
}
fn material_for_mode(
vmesh: &VectorMeshData,
materials: &mut Assets<StandardMaterial>,
) -> Handle<StandardMaterial> {
match vmesh.render_mode {
VectorRenderMode::FillExtrusion => {
materials.add(StandardMaterial {
base_color: Color::WHITE,
alpha_mode: AlphaMode::Blend,
double_sided: false,
depth_bias: 0.0,
perceptual_roughness: 0.95,
..Default::default()
})
}
VectorRenderMode::Fill => {
materials.add(StandardMaterial {
base_color: Color::linear_rgba(1.0, 1.0, 1.0, vmesh.fill_opacity),
alpha_mode: AlphaMode::Blend,
double_sided: true,
depth_bias: 1000.0,
perceptual_roughness: 0.95,
unlit: true,
..Default::default()
})
}
VectorRenderMode::Heatmap => {
materials.add(StandardMaterial {
base_color: Color::WHITE,
alpha_mode: AlphaMode::Add,
double_sided: true,
depth_bias: 1000.0,
perceptual_roughness: 0.95,
unlit: true,
..Default::default()
})
}
_ => materials.add(StandardMaterial {
base_color: Color::WHITE,
alpha_mode: AlphaMode::Blend,
double_sided: true,
depth_bias: 1000.0,
perceptual_roughness: 0.95,
unlit: true,
..Default::default()
}),
}
}
fn compute_vertex_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let mut accum = vec![Vec3::ZERO; positions.len()];
for tri in indices.chunks_exact(3) {
let ia = tri[0] as usize;
let ib = tri[1] as usize;
let ic = tri[2] as usize;
if ia >= positions.len() || ib >= positions.len() || ic >= positions.len() {
continue;
}
let a = Vec3::from_array(positions[ia]);
let b = Vec3::from_array(positions[ib]);
let c = Vec3::from_array(positions[ic]);
let normal = (b - a).cross(c - a);
if normal.length_squared() > 1e-12 {
accum[ia] += normal;
accum[ib] += normal;
accum[ic] += normal;
}
}
accum
.into_iter()
.map(|normal| {
let normal = if normal.length_squared() > 1e-12 {
normal.normalize()
} else {
Vec3::Z
};
[normal.x, normal.y, normal.z]
})
.collect()
}