use std::slice::IterMut; use bevy::{ prelude::*, render::{mesh::Indices, render_resource::PrimitiveTopology, view::RenderLayers}, sprite::MaterialMesh2dBundle, }; use crate::global_state::GlobalState; #[derive(Default, Debug, PartialEq)] struct CollectionIDs { points: Vec, lines: Vec, } #[derive(Debug, PartialEq)] struct FinishedIDs { // 2d plane plane: Entity, // 3d object object: Entity, } #[derive(Debug, PartialEq)] enum PolygonState { Collection(CollectionIDs), Finished(FinishedIDs), } impl PolygonState { fn collection_mut(&mut self) -> &mut CollectionIDs { match self { PolygonState::Collection(c) => c, PolygonState::Finished(_) => panic!("called collection on finished state!"), } } } impl Default for PolygonState { fn default() -> Self { Self::Collection(CollectionIDs::default()) } } #[derive(Default, Debug, PartialEq)] pub struct Polygon { points: Vec, state: PolygonState, } impl Polygon { const PROXIMITY_THRESHOLD: f32 = 10.0; pub fn swap_to_3d(&mut self, global_state: &GlobalState, commands: &mut Commands) { match &self.state { PolygonState::Collection(collection_ids) => { // add render layer to all points for point in collection_ids.points.iter() { let mut handle = commands.entity(*point); handle.insert(global_state.offscreen_render_layer); } // also add render layer to all lines for line in collection_ids.points.iter() { let mut handle = commands.entity(*line); handle.insert(global_state.offscreen_render_layer); } } PolygonState::Finished(finished_ids) => { // add render layer to 2d plane commands .entity(finished_ids.plane) .insert(global_state.offscreen_render_layer); // remove render layer from 3d object commands .entity(finished_ids.object) .remove::(); } } } pub fn swap_to_2d(&mut self, global_state: &GlobalState, commands: &mut Commands) { match &self.state { PolygonState::Collection(collection_ids) => { // remove render layer from all points for point in collection_ids.points.iter() { let mut handle = commands.entity(*point); handle.remove::(); } // remove render layer from all lines for line in collection_ids.points.iter() { let mut handle = commands.entity(*line); handle.remove::(); } } PolygonState::Finished(finished_ids) => { // remove render layer from 2d plane commands.entity(finished_ids.plane).remove::(); // add render layer to 3d object commands .entity(finished_ids.object) .insert(global_state.offscreen_render_layer); } } } pub fn points(&self) -> &[Vec2] { &self.points } pub fn add_point( &mut self, global_state: &GlobalState, commands: &mut Commands, meshes: &mut ResMut>, color_materials: &mut ResMut>, standard_materials: &mut ResMut>, ) -> bool { debug_assert_eq!(self.finished(), false); let point = global_state.cursor_position; // polygon must be at least an triangle // and check if we can close up the polygon if self.points.len() >= 3 && self.check_first_for_proximity(point) { // remove points and lines from collection state { let collection_ids = self.state.collection_mut(); for point_id in collection_ids.points.iter() { commands.entity(*point_id).despawn(); } for line_id in collection_ids.lines.iter() { commands.entity(*line_id).despawn(); } } self.order_points(); // create triangle mesh let triangle_mesh = self.create_triangulated_mesh(); let triangle_id = commands .spawn(MaterialMesh2dBundle { mesh: meshes.add(triangle_mesh).into(), transform: Transform::default() .with_translation((-global_state.window_extent * 0.5).extend(0.0)), material: color_materials.add(ColorMaterial::from(Color::BLACK)), ..default() }) .id(); // create 3d object let vertices: Vec = self .points .iter() .map(|p| vec![p.extend(0.0), p.extend(global_state.extrusion_height)]) .flatten() .collect(); let indices = Indices::U32({ let mut v = Vec::new(); for i in 0..self.points.len() { let index = (i as u32) * 2; // create quads from 4 adjacent points v.push(index); v.push((index + 2) % vertices.len() as u32); v.push(index + 1); v.push(index + 1); v.push((index + 2) % vertices.len() as u32); v.push((index + 3) % vertices.len() as u32); } // TODO: proper ear cutting algo for i in 2..self.points.len() as u32 { let index = (i as u32) * 2 + 1; v.push(1); v.push(index - 2); v.push(index); } v }); let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); mesh.set_indices(Some(indices)); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices); let mesh_id = commands .spawn(( MaterialMeshBundle { mesh: meshes.add(mesh).into(), transform: Transform::default(), material: standard_materials.add(StandardMaterial::from(Color::BLACK)), ..default() }, global_state.offscreen_render_layer, )) .id(); // store finished entities in state self.state = PolygonState::Finished(FinishedIDs { plane: triangle_id, object: mesh_id, }); return true; } // deny too close points if !self.check_all_for_proximity(point) { let collection_ids = self.state.collection_mut(); // create point mesh at click position let point_id = commands .spawn(MaterialMesh2dBundle { mesh: meshes.add(Mesh::from(shape::Quad::default())).into(), transform: Transform::default() .with_scale(Vec3::splat(4.0)) .with_translation( (global_state.cursor_position - (global_state.window_extent * 0.5)) .extend(0.0), ), material: color_materials.add(ColorMaterial::from(Color::BLACK)), ..default() }) .id(); collection_ids.points.push(point_id); // draw a line between new point and last point if let Some(&last_point) = self.points.last() { let a = Vec2::X.angle_between((point - last_point).normalize()); let line_id = commands .spawn(MaterialMesh2dBundle { mesh: meshes.add(Self::create_line_mesh(last_point, point)).into(), transform: Transform::default() .with_translation( (last_point - (global_state.window_extent * 0.5)).extend(0.0), ) .with_rotation(Quat::from_rotation_z(a)), material: color_materials.add(ColorMaterial::from(Color::BLACK)), ..default() }) .id(); collection_ids.lines.push(line_id); } self.points.push(point); } false } pub fn finished(&self) -> bool { match &self.state { PolygonState::Collection(_) => false, PolygonState::Finished(_) => true, } } fn order_points(&mut self) { let n = Vec3::Z; let v1 = (self.points[0] - self.points[1]).normalize().extend(0.0); let v2 = (self.points[1] - self.points[2]).normalize().extend(0.0); let c1 = v1.cross(n); let d = c1.dot(v2); // dot product is bigger than 0 if normal and connection line are facing in the same direction // reverse the order of points, thus the face of the 3d object is directed to the outside if d >= 0.0 { self.points = self.points.iter().cloned().rev().collect(); } } fn check_all_for_proximity(&self, point: Vec2) -> bool { self.points .iter() .any(|p| p.distance(point) < Self::PROXIMITY_THRESHOLD) } fn check_first_for_proximity(&self, point: Vec2) -> bool { debug_assert!(!self.points.is_empty()); self.points.first().unwrap().distance(point) < Self::PROXIMITY_THRESHOLD } fn create_triangulated_mesh(&self) -> Mesh { let vertices: Vec<_> = self.points.iter().map(|p| p.extend(0.0)).collect(); // TODO: proper ear cutting algo let indices = Indices::U32({ let mut v = Vec::new(); for i in 2..self.points.len() as u32 { v.push(0); v.push(i - 1); v.push(i); } v }); let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); mesh.set_indices(Some(indices)); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, vertices); mesh } fn create_line_mesh(start: Vec2, end: Vec2) -> Mesh { let d = start.distance(end); let vertices = [[0.0, 0.0, 0.0], [d, 0.0, 0.0]]; let mut mesh = Mesh::new(PrimitiveTopology::LineList); mesh.insert_attribute( Mesh::ATTRIBUTE_POSITION, vertices.into_iter().map(|v| v).collect::>(), ); mesh } } #[derive(Resource, Default, Debug, PartialEq)] pub struct ObjectInfos { polygons: Vec, } impl ObjectInfos { pub fn active_polygon(&mut self) -> Option<&mut Polygon> { self.polygons.iter_mut().find(|polygon| !polygon.finished()) } pub fn add_polygon(&mut self) -> &mut Polygon { self.polygons.push(Polygon::default()); self.polygons.last_mut().unwrap() } pub fn iter_mut(&mut self) -> IterMut<'_, Polygon> { self.polygons.iter_mut() } }