1use crate::{components::{Component, DynamicMesh}, mesh::{MeshData, Vertex}};
3use glam::Vec3;
4use kvas_ecs::World;
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7
8#[derive(Clone, Default, Serialize, Deserialize)]
10pub struct EditableMesh {
11 pub vertices: Vec<Vertex>,
13 pub indices: Vec<u32>,
15 pub revision: u64,
17}
18impl Component for EditableMesh {}
19
20impl EditableMesh {
21 pub fn from_mesh(mesh: &MeshData) -> Result<Self, String> {
23 mesh.validate()?;
24 if mesh.vertices.len() > 100_000 || mesh.indices.len() > 600_000 { return Err("editable geometry limit: 100000 vertices / 600000 indices".into()); }
25 Ok(Self { vertices: mesh.vertices.clone(), indices: mesh.indices.clone(), revision: 1 })
26 }
27 pub fn mesh(&self) -> MeshData { MeshData { vertices: self.vertices.clone(), indices: self.indices.clone(), ..Default::default() } }
29 pub fn changed(&mut self) { self.revision = self.revision.saturating_add(1); }
31 pub fn recalculate_normals(&mut self) -> Result<(), String> {
33 let mut mesh = self.mesh(); mesh.recalculate_normals()?; self.vertices = mesh.vertices; self.changed(); Ok(())
34 }
35 pub fn box_uv(&mut self, scale: f32) -> Result<(), String> {
37 if !scale.is_finite() || scale <= 0.0 { return Err("UV scale must be positive".into()); }
38 for v in &mut self.vertices {
39 let n = Vec3::from(v.normal).abs(); let p = Vec3::from(v.pos) / scale;
40 v.uv = if n.y >= n.x && n.y >= n.z { [p.x, p.z] } else if n.x >= n.z { [p.z, p.y] } else { [p.x, p.y] };
41 }
42 self.changed(); Ok(())
43 }
44 pub fn sculpt(&mut self, center: Vec3, radius: f32, amount: f32) -> bool {
47 if !center.is_finite() || !radius.is_finite() || radius <= 0.0 || !amount.is_finite() { return false; }
48 let key = |p: [f32; 3]| p.map(|v| (v as f64 * 100000.0).round() as i64);
49 let mut normals = std::collections::HashMap::<[i64; 3], Vec3>::new();
50 for v in &self.vertices { *normals.entry(key(v.pos)).or_default() += Vec3::from(v.normal); }
51 let mut changed = false;
52 for v in &mut self.vertices {
53 let p = Vec3::from(v.pos); let distance = p.distance(center) / radius;
54 if distance < 1.0 {
55 let falloff = (1.0 - distance * distance).powi(2);
56 v.pos = (p + normals[&key(v.pos)].normalize_or_zero() * amount * falloff).to_array();
57 changed = true;
58 }
59 }
60 if changed { self.changed(); } changed
61 }
62 pub fn to_obj(&self) -> Result<String, String> {
64 use std::fmt::Write;
65 self.mesh().validate()?;
66 let mut text = String::from("# Kvas edited geometry; local coordinates\n");
67 for v in &self.vertices { writeln!(text, "v {} {} {}", v.pos[0], v.pos[1], v.pos[2]).unwrap(); }
68 for v in &self.vertices { writeln!(text, "vt {} {}", v.uv[0], v.uv[1]).unwrap(); }
69 for v in &self.vertices { writeln!(text, "vn {} {} {}", v.normal[0], v.normal[1], v.normal[2]).unwrap(); }
70 for t in self.indices.chunks_exact(3) { let a = t[0] + 1; let b = t[1] + 1; let c = t[2] + 1; writeln!(text, "f {a}/{a}/{a} {b}/{b}/{b} {c}/{c}/{c}").unwrap(); }
71 Ok(text)
72 }
73}
74
75pub fn update(world: &mut World) {
77 for e in world.entities_with::<EditableMesh>() {
78 let Some(edit) = world.get::<EditableMesh>(e) else { continue };
79 let version = edit.revision | (1u64 << 63);
81 if world.get::<DynamicMesh>(e).is_some_and(|d| d.version == version) { continue; }
82 let mesh = edit.mesh(); if mesh.validate().is_err() { continue; }
83 world.insert(e, DynamicMesh { data: Arc::new(mesh), version });
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 #[test]
91 fn sculpt_uv_export_and_scene_round_trip_preserve_geometry() {
92 let source = MeshData::primitive("cube").unwrap();
93 let mut editable = EditableMesh::from_mesh(&source).unwrap();
94 assert!(editable.sculpt(Vec3::new(0.5, 0.5, 0.5), 0.2, 0.1));
95 let a = Vec3::from(editable.vertices[0].pos);
97 assert!(a.is_finite());
98 editable.recalculate_normals().unwrap(); editable.box_uv(2.0).unwrap();
99 let json = serde_json::to_string(&editable).unwrap();
100 let restored: EditableMesh = serde_json::from_str(&json).unwrap();
101 assert_eq!(restored.to_obj().unwrap(), editable.to_obj().unwrap());
102 assert!(restored.to_obj().unwrap().contains("f "));
103 assert!(!editable.sculpt(Vec3::ZERO, 0.0, 1.0));
104 }
105}