diff --git a/nusamai-citygml/src/geometry.rs b/nusamai-citygml/src/geometry.rs index 1f868a3d0..b06eb7b02 100644 --- a/nusamai-citygml/src/geometry.rs +++ b/nusamai-citygml/src/geometry.rs @@ -100,15 +100,13 @@ impl GeometryCollector { &mut self, iter: impl IntoIterator, ring_id: Option, - ) -> usize { + ) { self.ring_ids.push(ring_id); self.multipolygon.add_exterior(iter.into_iter().map(|v| { let vbits = [v[0].to_bits(), v[1].to_bits(), v[2].to_bits()]; let (index, _) = self.vertices.insert_full(vbits); [index as u32] })); - - self.multipolygon.len() - 1 } pub fn add_interior_ring( diff --git a/nusamai-citygml/src/parser.rs b/nusamai-citygml/src/parser.rs index f1630ca74..3ead59091 100644 --- a/nusamai-citygml/src/parser.rs +++ b/nusamai-citygml/src/parser.rs @@ -875,6 +875,7 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { .fp_buf .chunks_exact(3) .map(|c| [c[0], c[1], c[2]]); + if is_exterior { // add a new polygon self.state @@ -1040,6 +1041,11 @@ impl<'b, R: BufRead> SubTreeReader<'_, 'b, R> { { self.state.fp_buf.pop(); self.state.fp_buf.pop(); + } else { + return Err(ParseError::InvalidValue(format!( + "The last UV coord must be the same as the first: {:?}", + self.state.fp_buf + ))); } } diff --git a/nusamai-citygml/src/values.rs b/nusamai-citygml/src/values.rs index ee76a66a0..7ddcc7720 100644 --- a/nusamai-citygml/src/values.rs +++ b/nusamai-citygml/src/values.rs @@ -381,6 +381,12 @@ impl Color { } } +impl From for [f32; 4] { + fn from(c: Color) -> [f32; 4] { + [c.r as f32, c.g as f32, c.b as f32, 1.] + } +} + impl std::hash::Hash for Color { fn hash(&self, state: &mut H) { self.r.to_bits().hash(state); diff --git a/nusamai-geometry/src/compact/multi_polygon.rs b/nusamai-geometry/src/compact/multi_polygon.rs index 5316dc202..8fb553652 100644 --- a/nusamai-geometry/src/compact/multi_polygon.rs +++ b/nusamai-geometry/src/compact/multi_polygon.rs @@ -179,7 +179,7 @@ impl<'a, const D: usize, T: CoordNum> MultiPolygon<'a, D, T> { } // Adds a polygon to the multipolygon. - pub fn push(&mut self, poly: Polygon) { + pub fn push(&mut self, poly: &Polygon) { self.add_exterior(&poly.exterior()); for hole in poly.interiors() { self.add_interior(&hole); @@ -296,7 +296,7 @@ mod tests { poly1.add_ring([[0., 0.], [5., 0.], [5., 5.], [0., 5.]]); // exterior poly1.add_ring([[1., 1.], [2., 1.], [2., 2.], [1., 2.]]); // interior poly1.add_ring([[3., 3.], [4., 3.], [4., 4.], [3., 4.]]); // interior - mpoly.push(poly1); + mpoly.push(&poly1); assert!(!mpoly.is_empty()); assert_eq!(mpoly.len(), 1); @@ -304,13 +304,13 @@ mod tests { let mut poly2 = Polygon2::new(); poly2.add_ring([[4., 0.], [7., 0.], [7., 3.], [4., 3.]]); // exterior poly2.add_ring([[5., 1.], [6., 1.], [6., 2.], [5., 2.]]); // interior - mpoly.push(poly2); + mpoly.push(&poly2); assert_eq!(mpoly.len(), 2); // 3rd polygon let mut poly3 = Polygon2::new(); poly3.add_ring([[4., 0.], [7., 0.], [7., 3.], [4., 3.]]); // exterior - mpoly.push(poly3); + mpoly.push(&poly3); assert_eq!(mpoly.len(), 3); for (i, poly) in mpoly.iter().enumerate() { diff --git a/nusamai-plateau/src/appearance.rs b/nusamai-plateau/src/appearance.rs index 2e8628781..a99ae8f92 100644 --- a/nusamai-plateau/src/appearance.rs +++ b/nusamai-plateau/src/appearance.rs @@ -19,6 +19,7 @@ pub struct Material { pub specular_color: Color, pub ambient_intensity: f64, // TOOD: other parameters + // Note: Adjust the Hash implementation if you add a new field } impl From for Material { diff --git a/nusamai-projection/Cargo.toml b/nusamai-projection/Cargo.toml index 5618e7fb0..fa2179e5d 100644 --- a/nusamai-projection/Cargo.toml +++ b/nusamai-projection/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -japan-geoid = { git = "https://github.com/MIERUNE/japan-geoid" } +japan-geoid = "0.4.0" thiserror = "1.0.57" diff --git a/nusamai/src/sink/cesiumtiles/material.rs b/nusamai/src/sink/cesiumtiles/material.rs new file mode 100644 index 000000000..0e2566b22 --- /dev/null +++ b/nusamai/src/sink/cesiumtiles/material.rs @@ -0,0 +1,19 @@ +//! Material mangement + +use std::hash::Hash; + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Clone, PartialEq, Deserialize)] +pub struct Material { + pub base_color: [f32; 4], + // NOTE: Adjust the hash implementation if you add more fields +} + +impl Eq for Material {} + +impl Hash for Material { + fn hash(&self, state: &mut H) { + self.base_color.iter().for_each(|c| c.to_bits().hash(state)); + } +} diff --git a/nusamai/src/sink/cesiumtiles/mod.rs b/nusamai/src/sink/cesiumtiles/mod.rs index 7ae5eaae3..52defb973 100644 --- a/nusamai/src/sink/cesiumtiles/mod.rs +++ b/nusamai/src/sink/cesiumtiles/mod.rs @@ -2,10 +2,13 @@ use std::fs; mod gltf; +mod material; mod slice; mod sort; mod tiling; +use indexmap::IndexSet; +use itertools::Itertools; use std::io::BufWriter; use std::path::{Path, PathBuf}; use std::sync::{mpsc, Arc, Mutex}; @@ -14,15 +17,12 @@ use ahash::RandomState; use earcut_rs::utils3d::project3d_to_2d; use earcut_rs::Earcut; use ext_sort::{buffer::mem::MemoryLimitedBufferBuilder, ExternalSorter, ExternalSorterBuilder}; -use indexmap::IndexSet; -use itertools::Itertools; use nusamai_mvt::TileZXY; use nusamai_projection::cartesian::geographic_to_geocentric; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use nusamai_citygml::schema::Schema; -use nusamai_geometry::MultiPolygon; use nusamai_mvt::tileid::TileIdMethod; use crate::parameters::*; @@ -30,7 +30,7 @@ use crate::pipeline::{Feedback, PipelineError, Receiver, Result}; use crate::sink::cesiumtiles::gltf::write_gltf_glb; use crate::sink::{DataSink, DataSinkProvider, SinkInfo}; use crate::{get_parameter_value, transformer}; -use slice::slice_cityobj_geoms; +use slice::{slice_to_tiles, SlicedFeature}; use sort::BincodeExternalChunk; use tiling::{TileSpec, TileTree}; @@ -83,12 +83,6 @@ struct SerializedSlicedFeature { body: Vec, } -#[derive(Serialize, Deserialize)] -struct SlicedFeature<'a> { - geometry: MultiPolygon<'a, 3>, - properties: nusamai_citygml::object::Value, -} - impl DataSink for CesiumTilesSink { fn make_transform_requirements(&self) -> transformer::Requirements { transformer::Requirements { @@ -158,18 +152,14 @@ fn geometry_slicing_stage( upstream.into_iter().par_bridge().try_for_each(|parcel| { feedback.ensure_not_canceled()?; - slice_cityobj_geoms(&parcel.entity, 7, 17, |(z, x, y), mpoly| { - let feature = SlicedFeature { - geometry: mpoly, - properties: parcel.entity.root.clone(), - }; + // TODO: zoom level from parameters + slice_to_tiles(&parcel.entity, 7, 17, |(z, x, y), feature| { let bytes = bincode::serialize(&feature).unwrap(); - let sfeat = SerializedSlicedFeature { + let serialized_feature = SerializedSlicedFeature { tile_id: tile_id_conv.zxy_to_id(z, x, y), body: bytes, }; - - if sender_sliced.send(sfeat).is_err() { + if sender_sliced.send(serialized_feature).is_err() { return Err(PipelineError::Canceled); }; Ok(()) @@ -224,7 +214,7 @@ fn tile_writing_stage( receiver_sorted .into_iter() .par_bridge() - .try_for_each(|(tile_id, sfeats)| { + .try_for_each(|(tile_id, serialized_feats)| { feedback.ensure_not_canceled()?; let mut tilespec = TileSpec { @@ -238,43 +228,51 @@ fn tile_writing_stage( }; let mut earcutter = Earcut::new(); - let mut buf3d: Vec = Vec::new(); - let mut buf2d: Vec = Vec::new(); + let mut buf5d: Vec = Vec::new(); // [x, y, z, u, v] + let mut buf2d: Vec = Vec::new(); // 2d-projected [x, y] let mut triangles_buf: Vec = Vec::new(); let mut triangles = Vec::new(); - for ser_feat in sfeats { - let mut feat: SlicedFeature = bincode::deserialize(&ser_feat.body).unwrap(); - - feat.geometry.transform_inplace(|&[lng, lat, height]| { - tilespec.min_lng = tilespec.min_lng.min(lng); - tilespec.max_lng = tilespec.max_lng.max(lng); - tilespec.min_lat = tilespec.min_lat.min(lat); - tilespec.max_lat = tilespec.max_lat.max(lat); - tilespec.min_height = tilespec.min_height.min(height); - tilespec.max_height = tilespec.max_height.max(height); - - let (x, y, z) = geographic_to_geocentric(&ellipsoid, lng, lat, height); - [x, z, -y] - }); - - for poly in &feat.geometry { + for serialized_feat in serialized_feats { + let mut feature: SlicedFeature = bincode::deserialize(&serialized_feat.body) + .map_err(|err| { + PipelineError::Other(format!( + "Failed to deserialize a sliced feature: {:?}", + err + )) + })?; + + feature + .polygons + .transform_inplace(|&[lng, lat, height, u, v]| { + tilespec.min_lng = tilespec.min_lng.min(lng); + tilespec.max_lng = tilespec.max_lng.max(lng); + tilespec.min_lat = tilespec.min_lat.min(lat); + tilespec.max_lat = tilespec.max_lat.max(lat); + tilespec.min_height = tilespec.min_height.min(height); + tilespec.max_height = tilespec.max_height.max(height); + + let (x, y, z) = geographic_to_geocentric(&ellipsoid, lng, lat, height); + [x, z, -y, u, v] + }); + + for poly in &feature.polygons { let num_outer = match poly.hole_indices().first() { Some(&v) => v as usize, - None => poly.coords().len() / 3, + None => poly.coords().len() / 5, }; - buf3d.clear(); - buf3d.extend(poly.coords()); + buf5d.clear(); + buf5d.extend(poly.coords()); - if project3d_to_2d(&buf3d, num_outer, 3, &mut buf2d) { + if project3d_to_2d(&buf5d, num_outer, 5, &mut buf2d) { // earcut earcutter.earcut(&buf2d, poly.hole_indices(), 2, &mut triangles_buf); triangles.extend(triangles_buf.iter().map(|idx| { [ - buf3d[*idx as usize * 3], - buf3d[*idx as usize * 3 + 1], - buf3d[*idx as usize * 3 + 2], + buf5d[*idx as usize * 5], + buf5d[*idx as usize * 5 + 1], + buf5d[*idx as usize * 5 + 2], ] })); } @@ -342,21 +340,17 @@ fn tile_writing_stage( let (min_y, max_y) = tiling::y_slice_range(zoom, y); let xs = tiling::x_step(zoom, y); let (min_x, max_x) = tiling::x_slice_range(zoom, x as i32, xs); - println!( - "tile: z={}, x={}, y={} (lng: {} -> {}, lat: {} -> {})", - zoom, x, y, min_x, max_x, min_y, max_y + log::info!( + "tile: z={zoom}, x={x}, y={y} (lng: [{min_x} => {max_x}], lat: [{min_y} => {max_y})" ); - println!("{:?} {:?}", vertices.len(), indices.len()); // write to file - let path_glb = output_path.join(Path::new(&format!("{}/{}/{}.glb", zoom, x, y))); + let path_glb = output_path.join(Path::new(&format!("{zoom}/{x}/{y}.glb"))); if let Some(dir) = path_glb.parent() { - if let Err(e) = fs::create_dir_all(dir) { - panic!("Fatal error: {:?}", e); // FIXME - } + fs::create_dir_all(dir)?; } - let mut file = std::fs::File::create(path_glb).unwrap(); + let mut file = std::fs::File::create(path_glb)?; let mut writer = BufWriter::new(&mut file); write_gltf_glb( &mut writer, diff --git a/nusamai/src/sink/cesiumtiles/slice.rs b/nusamai/src/sink/cesiumtiles/slice.rs index 096b5b56a..0742caddd 100644 --- a/nusamai/src/sink/cesiumtiles/slice.rs +++ b/nusamai/src/sink/cesiumtiles/slice.rs @@ -1,52 +1,105 @@ //! Polygon slicing algorithm based on [geojson-vt](https://github.com/mapbox/geojson-vt). use hashbrown::HashMap; +use indexmap::IndexSet; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use super::material::Material; use super::tiling; use nusamai_citygml::{ geometry::GeometryType, object::{ObjectStereotype, Value}, }; -use nusamai_geometry::{LineString3, MultiPolygon3, Polygon3}; +use nusamai_geometry::{MultiPolygon, Polygon, Polygon2, Polygon3}; use nusamai_mvt::TileZXY; -use nusamai_plateau::Entity; +use nusamai_plateau::{appearance, Entity}; + +#[derive(Serialize, Deserialize)] +pub struct SlicedFeature { + // polygons [x, y, z, u, v] + pub polygons: MultiPolygon<'static, 5>, + // material ids for each polygon + pub polygon_material_ids: Vec, + // materials + pub materials: IndexSet, + // attribute values + pub attributes: nusamai_citygml::object::Value, +} -pub fn slice_cityobj_geoms( - obj: &Entity, +pub fn slice_to_tiles( + entity: &Entity, min_z: u8, max_z: u8, - f: impl Fn(TileZXY, MultiPolygon3) -> Result<(), E>, + send_feature: impl Fn(TileZXY, SlicedFeature) -> Result<(), E>, ) -> Result<(), E> { assert!( max_z >= min_z, "max_z must be greater than or equal to min_z" ); - let geom_store = obj.geometry_store.read().unwrap(); - if geom_store.multipolygon.is_empty() { - return Ok(()); - } - - let mut tiled_mpolys = HashMap::new(); - - let Value::Object(obj) = &obj.root else { + // entity must be a Feature + let Value::Object(obj) = &entity.root else { return Ok(()); }; let ObjectStereotype::Feature { geometries, .. } = &obj.stereotype else { return Ok(()); }; + let geom_store = entity.geometry_store.read().unwrap(); + if geom_store.multipolygon.is_empty() { + return Ok(()); + } + let appearance_store = entity.appearance_store.read().unwrap(); + + let mut materials: IndexSet = IndexSet::new(); + let mut sliced_tiles: HashMap<(u8, u32, u32), SlicedFeature> = HashMap::new(); + let default_mat = appearance::Material::default(); + geometries.iter().for_each(|entry| match entry.ty { GeometryType::Solid | GeometryType::Surface | GeometryType::Triangle => { - for idx_poly in geom_store + // for each polygon + for ((idx_poly, poly_uv), poly_mat) in geom_store .multipolygon .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) + .zip_eq( + geom_store + .polygon_uvs + .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize), + ) + .zip_eq( + geom_store.polygon_materials + [entry.pos as usize..(entry.pos + entry.len) as usize] + .iter(), + ) { let poly = idx_poly.transform(|c| geom_store.vertices[c[0] as usize]); + let orig_mat = poly_mat + .and_then(|idx| appearance_store.materials.get(idx as usize)) + .unwrap_or(&default_mat) + .clone(); + + let mat = Material { + base_color: orig_mat.diffuse_color.into(), + }; + let (mat_idx, _) = materials.insert_full(mat); - // Slice for each zoom level + // Slice polygon for each zoom level for zoom in min_z..=max_z { - slice_polygon(zoom, &poly, &mut tiled_mpolys); + slice_polygon(zoom, &poly, &poly_uv, |(z, x, y), poly| { + let sliced_feature = + sliced_tiles + .entry((z, x, y)) + .or_insert_with(|| SlicedFeature { + polygons: MultiPolygon::new(), + attributes: entity.root.clone(), + polygon_material_ids: Default::default(), + materials: Default::default(), + }); + + sliced_feature.polygons.push(poly); + sliced_feature.polygon_material_ids.push(mat_idx as u32); + }); } } } @@ -58,24 +111,27 @@ pub fn slice_cityobj_geoms( } }); - for ((z, x, y), mpoly) in tiled_mpolys { - if mpoly.is_empty() { - continue; - } - f((z, x, y), mpoly)?; + for ((z, x, y), mut sliced_feature) in sliced_tiles { + sliced_feature.materials = materials.clone(); + send_feature((z, x, y), sliced_feature)?; } - Ok(()) // TODO: linestring, point } -fn slice_polygon(zoom: u8, poly: &Polygon3, out: &mut HashMap<(u8, u32, u32), MultiPolygon3>) { +/// Slice a polygon into tiles. The slicing algorithm is based on [geojson-vt](https://github.com/mapbox/geojson-vt). +fn slice_polygon( + zoom: u8, + poly: &Polygon3, + poly_uv: &Polygon2, + mut send_polygon: impl FnMut(TileZXY, &Polygon<'static, 5>), +) { if poly.exterior().is_empty() { return; } - let mut new_ring_buffer: Vec<[f64; 3]> = Vec::with_capacity(poly.exterior().len() + 1); + let mut ring_buffer: Vec<[f64; 5]> = Vec::with_capacity(poly.exterior().len() + 1); // Slice along Y-axis let y_range = { @@ -88,62 +144,77 @@ fn slice_polygon(zoom: u8, poly: &Polygon3, out: &mut HashMap<(u8, u32, u32), Mu tiling::iter_y_slice(zoom, min_y, max_y) }; - let mut y_sliced_polys = Vec::with_capacity(y_range.len()); + let mut y_sliced_polys = MultiPolygon::new(); for yi in y_range.clone() { let (k1, k2) = tiling::y_slice_range(zoom, yi); - let mut y_sliced_poly = Polygon3::new(); // todo?: check interior bbox to optimize - for ring in poly.rings() { + for (ri, (ring, uv_ring)) in poly.rings().zip_eq(poly_uv.rings()).enumerate() { if ring.coords().is_empty() { continue; } - new_ring_buffer.clear(); + ring_buffer.clear(); ring.iter_closed() + .zip_eq(uv_ring.iter_closed()) .fold(None, |a, b| { - let Some(a) = a else { return Some(b) }; + let Some((a, a_uv)) = a else { return Some(b) }; + let (b, b_uv) = b; if a[1] < k1 { if b[1] > k1 { - let x = (b[0] - a[0]) * (k1 - a[1]) / (b[1] - a[1]) + a[0]; - let z = (b[2] - a[2]) * (k1 - a[1]) / (b[1] - a[1]) + a[2]; - new_ring_buffer.push([x, k1, z]) + let t = (k1 - a[1]) / (b[1] - a[1]); + let x = (b[0] - a[0]) * t + a[0]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b_uv[0] - a_uv[0]) * t + a_uv[0]; + let v = (b_uv[1] - a_uv[1]) * t + a_uv[1]; + ring_buffer.push([x, k1, z, u, v]) } } else if a[1] > k2 { if b[1] < k2 { - let x = (b[0] - a[0]) * (k2 - a[1]) / (b[1] - a[1]) + a[0]; - let z = (b[2] - a[2]) * (k2 - a[1]) / (b[1] - a[1]) + a[2]; - new_ring_buffer.push([x, k2, z]) + let t = (k2 - a[1]) / (b[1] - a[1]); + let x = (b[0] - a[0]) * t + a[0]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b_uv[0] - a_uv[0]) * t + a_uv[0]; + let v = (b_uv[1] - a_uv[1]) * t + a_uv[1]; + ring_buffer.push([x, k2, z, u, v]) } } else { - new_ring_buffer.push(a) + ring_buffer.push([a[0], a[1], a[2], a_uv[0], a_uv[1]]) } if b[1] < k1 && a[1] > k1 { - let x = (b[0] - a[0]) * (k1 - a[1]) / (b[1] - a[1]) + a[0]; - let z = (b[2] - a[2]) * (k1 - a[1]) / (b[1] - a[1]) + a[2]; - new_ring_buffer.push([x, k1, z]) + let t = (k1 - a[1]) / (b[1] - a[1]); + let x = (b[0] - a[0]) * t + a[0]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b_uv[0] - a_uv[0]) * t + a_uv[0]; + let v = (b_uv[1] - a_uv[1]) * t + a_uv[1]; + ring_buffer.push([x, k1, z, u, v]) } else if b[1] > k2 && a[1] < k2 { - let x = (b[0] - a[0]) * (k2 - a[1]) / (b[1] - a[1]) + a[0]; - let z = (b[2] - a[2]) * (k2 - a[1]) / (b[1] - a[1]) + a[2]; - new_ring_buffer.push([x, k2, z]) + let t = (k2 - a[1]) / (b[1] - a[1]); + let x = (b[0] - a[0]) * t + a[0]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b_uv[0] - a_uv[0]) * t + a_uv[0]; + let v = (b_uv[1] - a_uv[1]) * t + a_uv[1]; + ring_buffer.push([x, k2, z, u, v]) } - Some(b) + Some((b, b_uv)) }) .unwrap(); - y_sliced_poly.add_ring(new_ring_buffer.iter().copied()); + match ri { + 0 => y_sliced_polys.add_exterior(ring_buffer.drain(..)), + _ => y_sliced_polys.add_interior(ring_buffer.drain(..)), + } } - - y_sliced_polys.push(y_sliced_poly); } // Slice along X-axis - for (yi, y_sliced_poly) in y_range.zip(y_sliced_polys.iter()) { + let mut poly_buf: Polygon<5> = Polygon::new(); + for (yi, y_sliced_poly) in y_range.zip_eq(y_sliced_polys.iter()) { let x_iter = { let (min_x, max_x) = y_sliced_poly .exterior() @@ -165,56 +236,64 @@ fn slice_polygon(zoom: u8, poly: &Polygon3, out: &mut HashMap<(u8, u32, u32), Mu xi.rem_euclid(1 << zoom) as u32, // handling geometry crossing the antimeridian yi, ); - let tile_mpoly = out.entry(key).or_default(); + poly_buf.clear(); - for (ri, ring) in y_sliced_poly.rings().enumerate() { + for ring in y_sliced_poly.rings() { if ring.coords().is_empty() { continue; } - new_ring_buffer.clear(); + ring_buffer.clear(); ring.iter_closed() .fold(None, |a, b| { let Some(a) = a else { return Some(b) }; if a[0] < k1 { if b[0] > k1 { - let y = (b[1] - a[1]) * (k1 - a[0]) / (b[0] - a[0]) + a[1]; - let z = (b[2] - a[2]) * (k1 - a[0]) / (b[0] - a[0]) + a[2]; - new_ring_buffer.push([k1, y, z]) + let t = (k1 - a[0]) / (b[0] - a[0]); + let y = (b[1] - a[1]) * t + a[1]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b[3] - a[3]) * t + a[3]; + let v = (b[4] - a[4]) * t + a[4]; + ring_buffer.push([k1, y, z, u, v]) } } else if a[0] > k2 { if b[0] < k2 { - let y = (b[1] - a[1]) * (k2 - a[0]) / (b[0] - a[0]) + a[1]; - let z = (b[2] - a[2]) * (k2 - a[0]) / (b[0] - a[0]) + a[2]; - new_ring_buffer.push([k2, y, z]) + let t = (k2 - a[0]) / (b[0] - a[0]); + let y = (b[1] - a[1]) * t + a[1]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b[3] - a[3]) * t + a[3]; + let v = (b[4] - a[4]) * t + a[4]; + ring_buffer.push([k2, y, z, u, v]) } } else { - new_ring_buffer.push(a) + ring_buffer.push(a) } if b[0] < k1 && a[0] > k1 { - let y = (b[1] - a[1]) * (k1 - a[0]) / (b[0] - a[0]) + a[1]; - let z = (b[2] - a[2]) * (k1 - a[0]) / (b[0] - a[0]) + a[2]; - new_ring_buffer.push([k1, y, z]) + let t = (k1 - a[0]) / (b[0] - a[0]); + let y = (b[1] - a[1]) * t + a[1]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b[3] - a[3]) * t + a[3]; + let v = (b[4] - a[4]) * t + a[4]; + ring_buffer.push([k1, y, z, u, v]) } else if b[0] > k2 && a[0] < k2 { - let y = (b[1] - a[1]) * (k2 - a[0]) / (b[0] - a[0]) + a[1]; - let z = (b[2] - a[2]) * (k2 - a[0]) / (b[0] - a[0]) + a[2]; - new_ring_buffer.push([k2, y, z]) + let t = (k2 - a[0]) / (b[0] - a[0]); + let y = (b[1] - a[1]) * t + a[1]; + let z = (b[2] - a[2]) * t + a[2]; + let u = (b[3] - a[3]) * t + a[3]; + let v = (b[4] - a[4]) * t + a[4]; + ring_buffer.push([k2, y, z, u, v]) } Some(b) }) .unwrap(); - let ring = - LineString3::from_raw(new_ring_buffer.iter().flatten().copied().collect()); - - match ri { - 0 => tile_mpoly.add_exterior(ring.iter()), - _ => tile_mpoly.add_interior(ring.iter()), - }; + poly_buf.add_ring(ring_buffer.drain(..)) } + + send_polygon(key, &poly_buf); } } } diff --git a/nusamai/src/sink/czml/mod.rs b/nusamai/src/sink/czml/mod.rs index 010832db1..c88a34e6b 100644 --- a/nusamai/src/sink/czml/mod.rs +++ b/nusamai/src/sink/czml/mod.rs @@ -174,7 +174,7 @@ pub fn entity_to_packet(entity: &Entity, single_part: bool) -> Vec { .multipolygon .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) { - mpoly.push(idx_poly); + mpoly.push(&idx_poly); } } GeometryType::Curve => unimplemented!(), diff --git a/nusamai/src/sink/gpkg/mod.rs b/nusamai/src/sink/gpkg/mod.rs index 14e47afa6..5f387f24e 100644 --- a/nusamai/src/sink/gpkg/mod.rs +++ b/nusamai/src/sink/gpkg/mod.rs @@ -125,7 +125,7 @@ impl GpkgSink { for idx_poly in geom_store.multipolygon.iter_range( entry.pos as usize..(entry.pos + entry.len) as usize, ) { - mpoly.push(idx_poly); + mpoly.push(&idx_poly); } } GeometryType::Curve => unimplemented!(), diff --git a/nusamai/src/sink/kml/mod.rs b/nusamai/src/sink/kml/mod.rs index babfee474..de83030e6 100644 --- a/nusamai/src/sink/kml/mod.rs +++ b/nusamai/src/sink/kml/mod.rs @@ -262,7 +262,7 @@ pub fn entity_to_kml_polygons(entity: &Entity) -> Vec { .multipolygon .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) { - mpoly.push(idx_poly); + mpoly.push(&idx_poly); } } diff --git a/nusamai/src/sink/shapefile/mod.rs b/nusamai/src/sink/shapefile/mod.rs index 0fae609a2..fd18b9f9b 100644 --- a/nusamai/src/sink/shapefile/mod.rs +++ b/nusamai/src/sink/shapefile/mod.rs @@ -159,7 +159,7 @@ pub fn entity_to_shapes(entity: &Entity) -> Vec { .multipolygon .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) { - mpoly.push(idx_poly); + mpoly.push(&idx_poly); } } GeometryType::Curve => unimplemented!(), diff --git a/nusamai/src/source/citygml.rs b/nusamai/src/source/citygml.rs index 5131529db..f7a02ab5a 100644 --- a/nusamai/src/source/citygml.rs +++ b/nusamai/src/source/citygml.rs @@ -49,6 +49,8 @@ impl DataSource for CityGmlSource { let code_resolver = nusamai_plateau::codelist::Resolver::new(); self.filenames.par_iter().try_for_each(|filename| { + feedback.ensure_not_canceled()?; + log::info!("Parsing CityGML file: {:?} ...", filename); let file = std::fs::File::open(filename)?; let reader = std::io::BufReader::with_capacity(1024 * 1024, file); @@ -61,6 +63,7 @@ impl DataSource for CityGmlSource { let mut st = citygml_reader.start_root(&mut xml_reader)?; match toplevel_dispatcher(&mut st, &downstream, feedback) { Ok(_) => Ok::<(), PipelineError>(()), + Err(ParseError::Canceled) => Err(PipelineError::Canceled), Err(e) => Err(e.into()), } })?; @@ -81,7 +84,7 @@ fn toplevel_dispatcher( st.parse_children(|st| { if feedback.is_canceled() { - return Ok(()); + return Err(ParseError::Canceled); } match st.current_path() { @@ -135,6 +138,10 @@ fn toplevel_dispatcher( })?; for entity in entities { + if feedback.is_canceled() { + break; + } + // merge global appearances into the entity's local appearance store { let geom_store = entity.geometry_store.read().unwrap(); diff --git a/nusamai/src/transformer/transform/appearance.rs b/nusamai/src/transformer/transform/appearance.rs index 8817c104c..b9fb71093 100644 --- a/nusamai/src/transformer/transform/appearance.rs +++ b/nusamai/src/transformer/transform/appearance.rs @@ -1,5 +1,8 @@ +//! Apply appearance to geometries + use crate::transformer::Transform; +use itertools::Itertools; use nusamai_citygml::schema::Schema; use nusamai_geometry::MultiPolygon; use nusamai_plateau::Entity; @@ -17,9 +20,9 @@ impl Transform for ApplyAppearanceTransform { .or_else(|| app.themes.get("FMETheme")) }; - if let Some(theme) = theme { - let mut geoms = entity.geometry_store.write().unwrap(); + let mut geoms = entity.geometry_store.write().unwrap(); + if let Some(theme) = theme { // find and apply materials { let mut poly_materials = vec![None; geoms.multipolygon.len()]; @@ -49,8 +52,8 @@ impl Transform for ApplyAppearanceTransform { .unwrap() .and_then(|ring_id| theme.ring_id_to_texture.get(&ring_id)); - let mut add_dummy = || { - let uv = [[0.0, 0.0]].into_iter().cycle().take(ring.len()); + let mut add_dummy_texture = || { + let uv = [[0.0, 0.0]].into_iter().cycle().take(ring.len() + 1); if i == 0 { poly_textures.push(None); poly_uvs.add_exterior(uv); @@ -60,23 +63,27 @@ impl Transform for ApplyAppearanceTransform { }; match tex { - Some((idx, uv)) if uv.len() == ring.len() => { + Some((idx, uv)) if ring.len() == uv.len() => { // texture found if i == 0 { poly_textures.push(Some(*idx)); - poly_uvs.add_exterior(uv); + poly_uvs.add_exterior(uv.iter_closed()); } else { - poly_uvs.add_interior(uv); + poly_uvs.add_interior(uv.iter_closed()); } } Some((_, uv)) if uv.len() != ring.len() => { // invalid texture found - log::warn!("Length of UVs does not match length of ring"); - add_dummy(); + log::warn!( + "Length of UVs does not match length of ring: {:?} {:?}", + ring, + uv + ); + add_dummy_texture(); } _ => { // no texture found - add_dummy(); + add_dummy_texture(); } }; } @@ -88,6 +95,35 @@ impl Transform for ApplyAppearanceTransform { geoms.polygon_textures = poly_textures; geoms.polygon_uvs = poly_uvs; } + } else { + // set 'null' appearance if no theme found + geoms.polygon_materials = vec![None; geoms.multipolygon.len()]; + geoms.polygon_textures = vec![None; geoms.multipolygon.len()]; + let mut poly_uvs = MultiPolygon::new(); + for poly in &geoms.multipolygon { + for (i, ring) in poly.rings().enumerate() { + let uv = [[0.0, 0.0]].into_iter().cycle().take(ring.len() + 1); + if i == 0 { + poly_uvs.add_exterior(uv); + } else { + poly_uvs.add_interior(uv); + } + } + } + geoms.polygon_uvs = poly_uvs; + } + + // REMOVE ME: + for (poly, poly_uv) in geoms.multipolygon.iter().zip_eq(geoms.polygon_uvs.iter()) { + for (ring, uv_ring) in poly.rings().zip_eq(poly_uv.rings()) { + assert_eq!( + ring.len(), + uv_ring.len(), + "mismatch {:?} {:?}", + ring, + uv_ring + ); + } } }