1#[cfg(test)]
18#[path = "kmz_tests.rs"]
19mod tests;
20
21#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
23pub enum AltitudeMode {
24 #[default]
29 ClampToGround,
30 RelativeToGround,
32 Absolute,
34}
35
36impl AltitudeMode {
37 fn as_kml(self) -> &'static str {
38 match self {
39 AltitudeMode::ClampToGround => "clampToGround",
40 AltitudeMode::RelativeToGround => "relativeToGround",
41 AltitudeMode::Absolute => "absolute",
42 }
43 }
44}
45
46#[derive(Default)]
51pub struct KmzOptions {
52 pub latitude: f64,
54 pub longitude: f64,
56 pub altitude: f64,
59 pub altitude_mode: AltitudeMode,
63 pub x_axis_abscissa: Option<f64>,
66 pub x_axis_ordinate: Option<f64>,
68 pub name: Option<String>,
70}
71
72pub fn ifc_angle_to_kml_heading(x_abscissa: Option<f64>, x_ordinate: Option<f64>) -> f64 {
79 match (x_abscissa, x_ordinate) {
80 (Some(x), Some(y)) if x == 0.0 && y == 0.0 => 0.0,
83 (Some(x), Some(y)) => {
84 let angle_from_east_ccw = y.atan2(x).to_degrees();
85 (-angle_from_east_ccw).rem_euclid(360.0) + 0.0
89 }
90 _ => 0.0,
91 }
92}
93
94fn build_kml(opts: &KmzOptions, heading: f64, model_href: &str) -> String {
95 let name = crate::xml_text::escape(opts.name.as_deref().unwrap_or("IFC Model"));
96 format!(
97 r#"<?xml version="1.0" encoding="UTF-8"?>
98<kml xmlns="http://www.opengis.net/kml/2.2">
99 <Document>
100 <name>{name}</name>
101 <Placemark>
102 <name>{name}</name>
103 <Model id="model">
104 <altitudeMode>{altitude_mode}</altitudeMode>
105 <Location>
106 <longitude>{lon}</longitude>
107 <latitude>{lat}</latitude>
108 <altitude>{alt}</altitude>
109 </Location>
110 <Orientation>
111 <heading>{heading}</heading>
112 <tilt>0</tilt>
113 <roll>0</roll>
114 </Orientation>
115 <Scale>
116 <x>1</x>
117 <y>1</y>
118 <z>1</z>
119 </Scale>
120 <Link>
121 <href>{href}</href>
122 </Link>
123 </Model>
124 </Placemark>
125 </Document>
126</kml>"#,
127 name = name,
128 altitude_mode = opts.altitude_mode.as_kml(),
129 lon = opts.longitude,
130 lat = opts.latitude,
131 alt = opts.altitude,
132 heading = heading,
133 href = model_href,
134 )
135}
136
137#[allow(clippy::too_many_arguments)]
148pub fn try_export_kmz_collada_from_meshes(
149 positions: &[f32],
150 normals: &[f32],
151 indices: &[u32],
152 vertex_counts: &[u32],
153 index_counts: &[u32],
154 colors: &[f32],
155 origins: &[f64],
156 opts: &KmzOptions,
157) -> Result<Vec<u8>, crate::error::ExportError> {
158 let dae = crate::try_export_collada_from_meshes(
159 positions,
160 normals,
161 indices,
162 vertex_counts,
163 index_counts,
164 colors,
165 origins,
166 )?;
167 try_pack_kmz(&dae, "model.dae", opts)
168}
169
170const ZIP_LOCAL_HEADER_LEN: u64 = 30;
171const ZIP_CENTRAL_HEADER_LEN: u64 = 46;
172const ZIP_END_RECORD_LEN: u64 = 22;
173
174fn zip32_error(detail: impl Into<String>) -> crate::error::ExportError {
175 crate::error::ExportError::Serialization {
176 stage: "KMZ ZIP32",
177 detail: detail.into(),
178 }
179}
180
181fn validate_zip32_layout(entries: &[(&str, u64)]) -> Result<u64, crate::error::ExportError> {
186 if entries.len() > u16::MAX as usize {
187 return Err(zip32_error(format!(
188 "{} entries exceed ZIP32's {}-entry limit",
189 entries.len(),
190 u16::MAX
191 )));
192 }
193
194 let mut local_size = 0u64;
195 let mut central_size = 0u64;
196 for (name, data_size) in entries {
197 let name_size = u64::try_from(name.len()).map_err(|_| zip32_error("entry name length overflow"))?;
198 if name_size > u16::MAX as u64 {
199 return Err(zip32_error(format!("entry name is {name_size} bytes, over ZIP32's 65535-byte limit")));
200 }
201 if *data_size > u32::MAX as u64 {
202 return Err(zip32_error(format!(
203 "entry {name:?} is {data_size} bytes, over ZIP32's 4 GiB entry limit"
204 )));
205 }
206 if local_size > u32::MAX as u64 {
207 return Err(zip32_error(format!(
208 "entry {name:?} starts at offset {local_size}, over ZIP32's 32-bit offset limit"
209 )));
210 }
211 local_size = local_size
212 .checked_add(ZIP_LOCAL_HEADER_LEN)
213 .and_then(|size| size.checked_add(name_size))
214 .and_then(|size| size.checked_add(*data_size))
215 .ok_or_else(|| zip32_error("local-entry layout overflow"))?;
216 central_size = central_size
217 .checked_add(ZIP_CENTRAL_HEADER_LEN)
218 .and_then(|size| size.checked_add(name_size))
219 .ok_or_else(|| zip32_error("central-directory layout overflow"))?;
220 }
221
222 if local_size > u32::MAX as u64 {
223 return Err(zip32_error(format!(
224 "central directory starts at offset {local_size}, over ZIP32's 32-bit offset limit"
225 )));
226 }
227 if central_size > u32::MAX as u64 {
228 return Err(zip32_error(format!(
229 "central directory is {central_size} bytes, over ZIP32's 32-bit size limit"
230 )));
231 }
232 let total = local_size
233 .checked_add(central_size)
234 .and_then(|size| size.checked_add(ZIP_END_RECORD_LEN))
235 .ok_or_else(|| zip32_error("archive layout overflow"))?;
236 if total > u32::MAX as u64 {
237 return Err(zip32_error(format!(
238 "archive is {total} bytes, over this ZIP32 writer's 4 GiB limit"
239 )));
240 }
241 Ok(total)
242}
243
244fn try_pack_kmz(
245 model: &[u8],
246 href: &str,
247 opts: &KmzOptions,
248) -> Result<Vec<u8>, crate::error::ExportError> {
249 let heading = ifc_angle_to_kml_heading(opts.x_axis_abscissa, opts.x_axis_ordinate);
250 let kml = build_kml(opts, heading, href);
251 validate_zip32_layout(&[
252 ("doc.kml", kml.len() as u64),
253 (href, model.len() as u64),
254 ])?;
255
256 let mut zip = StoredZip::new();
257 zip.add("doc.kml", kml.as_bytes());
258 zip.add(href, model);
259 Ok(zip.finish())
260}
261
262struct StoredZip {
267 out: Vec<u8>,
268 central: Vec<u8>,
269 count: u16,
270}
271
272const DOS_TIME: u16 = 0;
275const DOS_DATE: u16 = 0x0021;
276
277impl StoredZip {
278 fn new() -> Self {
279 StoredZip { out: Vec::new(), central: Vec::new(), count: 0 }
280 }
281
282 fn add(&mut self, name: &str, data: &[u8]) {
283 let crc = crc32(data);
284 let offset = self.out.len() as u32;
285 let name_bytes = name.as_bytes();
286 let size = data.len() as u32;
287
288 self.out.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
290 self.out.extend_from_slice(&20u16.to_le_bytes()); self.out.extend_from_slice(&0u16.to_le_bytes()); self.out.extend_from_slice(&0u16.to_le_bytes()); self.out.extend_from_slice(&DOS_TIME.to_le_bytes());
294 self.out.extend_from_slice(&DOS_DATE.to_le_bytes());
295 self.out.extend_from_slice(&crc.to_le_bytes());
296 self.out.extend_from_slice(&size.to_le_bytes()); self.out.extend_from_slice(&size.to_le_bytes()); self.out.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
299 self.out.extend_from_slice(&0u16.to_le_bytes()); self.out.extend_from_slice(name_bytes);
301 self.out.extend_from_slice(data);
302
303 self.central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
305 self.central.extend_from_slice(&20u16.to_le_bytes()); self.central.extend_from_slice(&20u16.to_le_bytes()); self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&DOS_TIME.to_le_bytes());
310 self.central.extend_from_slice(&DOS_DATE.to_le_bytes());
311 self.central.extend_from_slice(&crc.to_le_bytes());
312 self.central.extend_from_slice(&size.to_le_bytes());
313 self.central.extend_from_slice(&size.to_le_bytes());
314 self.central.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
315 self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&0u16.to_le_bytes()); self.central.extend_from_slice(&0u32.to_le_bytes()); self.central.extend_from_slice(&offset.to_le_bytes());
321 self.central.extend_from_slice(name_bytes);
322
323 self.count += 1;
324 }
325
326 fn finish(mut self) -> Vec<u8> {
327 let cd_offset = self.out.len() as u32;
328 let cd_size = self.central.len() as u32;
329 self.out.extend_from_slice(&self.central);
330
331 self.out.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
333 self.out.extend_from_slice(&0u16.to_le_bytes()); self.out.extend_from_slice(&0u16.to_le_bytes()); self.out.extend_from_slice(&self.count.to_le_bytes()); self.out.extend_from_slice(&self.count.to_le_bytes()); self.out.extend_from_slice(&cd_size.to_le_bytes());
338 self.out.extend_from_slice(&cd_offset.to_le_bytes());
339 self.out.extend_from_slice(&0u16.to_le_bytes()); self.out
341 }
342}
343
344fn crc32(data: &[u8]) -> u32 {
346 let mut crc = 0xFFFF_FFFFu32;
347 for &byte in data {
348 crc ^= byte as u32;
349 for _ in 0..8 {
350 let mask = (crc & 1).wrapping_neg();
351 crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
352 }
353 }
354 !crc
355}