Skip to main content

copc_writer/
writer.rs

1use std::fs::File;
2use std::io::{BufWriter, Cursor, Seek, SeekFrom, Write};
3use std::path::Path;
4
5use byteorder::{LittleEndian, WriteBytesExt};
6use copc_core::{
7    Bounds, CancelCheck, CopcInfo, Entry, Error, LasPointRecord, NeverCancel, Result,
8    StreamingLayout, VoxelKey,
9};
10use las::Read as _;
11use laz::{LasZipCompressor, LazVlrBuilder};
12
13use crate::spill::{SpillReader, SpillWriter};
14
15const CANCEL_POLL_STRIDE: usize = 4_096;
16
17/// Normalized point fields consumed by the COPC writer.
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct CopcPointFields {
20    pub x: f64,
21    pub y: f64,
22    pub z: f64,
23    pub intensity: u16,
24    pub return_number: u8,
25    pub number_of_returns: u8,
26    pub synthetic: u8,
27    pub key_point: u8,
28    pub withheld: u8,
29    pub overlap: u8,
30    pub scan_channel: u8,
31    pub scan_direction_flag: u8,
32    pub edge_of_flight_line: u8,
33    pub classification: u8,
34    pub user_data: u8,
35    pub scan_angle_rank: i16,
36    pub point_source_id: u16,
37    pub gps_time: f64,
38    pub red: u16,
39    pub green: u16,
40    pub blue: u16,
41}
42
43/// Abstract point-data source for COPC emission.
44pub trait CopcPointSource {
45    fn len(&self) -> usize;
46    fn xyz(&self, index: usize) -> (f64, f64, f64);
47    fn fields(&self, index: usize) -> Result<CopcPointFields>;
48
49    fn is_empty(&self) -> bool {
50        self.len() == 0
51    }
52}
53
54struct SpillSource<'a> {
55    reader: &'a SpillReader,
56}
57
58impl CopcPointSource for SpillSource<'_> {
59    fn len(&self) -> usize {
60        self.reader.len()
61    }
62
63    #[inline]
64    fn xyz(&self, index: usize) -> (f64, f64, f64) {
65        self.reader.xyz_at(index)
66    }
67
68    fn fields(&self, index: usize) -> Result<CopcPointFields> {
69        let record = self.reader.record_at(index)?;
70        Ok(CopcPointFields {
71            x: record.x,
72            y: record.y,
73            z: record.z,
74            intensity: record.intensity,
75            return_number: record.return_number,
76            number_of_returns: record.number_of_returns,
77            synthetic: u8::from(record.synthetic),
78            key_point: u8::from(record.key_point),
79            withheld: u8::from(record.withheld),
80            overlap: u8::from(record.overlap),
81            scan_channel: record.scan_channel,
82            scan_direction_flag: u8::from(record.scan_direction_flag),
83            edge_of_flight_line: u8::from(record.edge_of_flight_line),
84            classification: record.classification,
85            user_data: record.user_data,
86            scan_angle_rank: record.scan_angle,
87            point_source_id: record.point_source_id,
88            gps_time: record.gps_time,
89            red: record.red,
90            green: record.green,
91            blue: record.blue,
92        })
93    }
94}
95
96#[derive(Debug, Clone, Copy)]
97pub struct CopcWriterParams {
98    pub max_points_per_node: u32,
99    pub max_depth: u32,
100}
101
102impl Default for CopcWriterParams {
103    fn default() -> Self {
104        Self {
105            max_points_per_node: 100_000,
106            max_depth: 8,
107        }
108    }
109}
110
111pub fn write_source<S: CopcPointSource>(
112    path: &Path,
113    source: &S,
114    has_color: bool,
115    bounds: Bounds,
116    params: &CopcWriterParams,
117) -> Result<()> {
118    write_source_with_cancel(path, source, has_color, bounds, params, &NeverCancel)
119}
120
121pub fn write_source_with_cancel<S: CopcPointSource>(
122    path: &Path,
123    source: &S,
124    has_color: bool,
125    bounds: Bounds,
126    params: &CopcWriterParams,
127    cancel: &dyn CancelCheck,
128) -> Result<()> {
129    cancel.check()?;
130    if source.is_empty() {
131        return Err(Error::InvalidInput(
132            "cannot write empty cloud to COPC".into(),
133        ));
134    }
135    write_copc_inner(path, source, has_color, bounds, params, cancel)
136}
137
138pub fn write_streaming_with_cancel<I>(
139    path: &Path,
140    layout: StreamingLayout,
141    points: I,
142    params: &CopcWriterParams,
143    spill_dir: &Path,
144    cancel: &dyn CancelCheck,
145) -> Result<()>
146where
147    I: IntoIterator<Item = Result<LasPointRecord>>,
148{
149    cancel.check()?;
150    let mut spill = SpillWriter::create(spill_dir, layout)?;
151    for (index, item) in points.into_iter().enumerate() {
152        if index % CANCEL_POLL_STRIDE == 0 {
153            cancel.check()?;
154        }
155        spill.push(&item?)?;
156    }
157    cancel.check()?;
158    let reader = spill.finalize()?;
159    write_copc_from_spill(path, reader, params, cancel)
160}
161
162pub fn convert_las_to_copc_streaming(
163    las_path: &Path,
164    copc_path: &Path,
165    params: &CopcWriterParams,
166    spill_dir: &Path,
167    cancel: &dyn CancelCheck,
168) -> Result<()> {
169    cancel.check()?;
170    let mut reader = las::Reader::from_path(las_path).map_err(|e| Error::Las(e.to_string()))?;
171    let layout = StreamingLayout::from_las_format(*reader.header().point_format());
172    let mut spill = SpillWriter::create(spill_dir, layout)?;
173    for (index, result) in reader.points().enumerate() {
174        if index % CANCEL_POLL_STRIDE == 0 {
175            cancel.check()?;
176        }
177        let point = result.map_err(|e| Error::Las(e.to_string()))?;
178        let record = LasPointRecord::from_las_point(&point);
179        spill.push(&record)?;
180    }
181    cancel.check()?;
182    let reader = spill.finalize()?;
183    write_copc_from_spill(copc_path, reader, params, cancel)
184}
185
186fn write_copc_from_spill(
187    path: &Path,
188    reader: SpillReader,
189    params: &CopcWriterParams,
190    cancel: &dyn CancelCheck,
191) -> Result<()> {
192    cancel.check()?;
193    if reader.is_empty() {
194        return Err(Error::InvalidInput(
195            "cannot write empty cloud to COPC".into(),
196        ));
197    }
198    let has_color = reader.layout().has_color;
199    let bounds = reader.bounds();
200    let source = SpillSource { reader: &reader };
201    write_copc_inner(path, &source, has_color, bounds, params, cancel)
202}
203
204fn write_copc_inner<S: CopcPointSource>(
205    path: &Path,
206    source: &S,
207    has_color: bool,
208    bounds: Bounds,
209    params: &CopcWriterParams,
210    cancel: &dyn CancelCheck,
211) -> Result<()> {
212    cancel.check()?;
213    let point_format_id = if has_color { 7u8 } else { 6u8 };
214    let point_record_length = if has_color { 36u16 } else { 30u16 };
215
216    let (center, halfsize) = cube_from_bounds(&bounds);
217    let (scale_x, scale_y, scale_z) = (0.001, 0.001, 0.001);
218    let (offset_x, offset_y, offset_z) = (bounds.min.0, bounds.min.1, bounds.min.2);
219
220    let nodes = build_lod_nodes(source, center, halfsize, params, cancel)?;
221    cancel.check()?;
222
223    let var_vlr = LazVlrBuilder::default()
224        .with_point_format(point_format_id, 0)
225        .map_err(|e| Error::Las(format!("laz items: {e}")))?
226        .with_variable_chunk_size()
227        .build();
228    let mut var_vlr_bytes = Vec::new();
229    var_vlr
230        .write_to(&mut var_vlr_bytes)
231        .map_err(|e| Error::Las(format!("variable chunk LAZ VLR: {e}")))?;
232
233    let copc_info_vlr_size = 160u16;
234    let las_header_size = 375u32;
235    let total_vlr_bytes =
236        (54u32 + u32::from(copc_info_vlr_size)) + (54u32 + var_vlr_bytes.len() as u32);
237    let offset_to_point_data = las_header_size + total_vlr_bytes;
238
239    let file = File::create(path).map_err(|e| Error::io("create COPC file", e))?;
240    let mut writer = BufWriter::new(file);
241
242    let header = LasHeader {
243        point_data_format: point_format_id | 0x80,
244        point_record_length,
245        offset_to_point_data,
246        number_of_vlrs: 2,
247        scale: (scale_x, scale_y, scale_z),
248        offset: (offset_x, offset_y, offset_z),
249        bounds,
250        legacy_point_count: 0,
251        total_point_count: source.len() as u64,
252        offset_to_first_evlr: 0,
253        number_of_evlrs: 1,
254    };
255    header.write(&mut writer)?;
256
257    write_vlr_header(&mut writer, "copc", 1, copc_info_vlr_size, "COPC info")?;
258    let copc_info_payload_start = writer
259        .stream_position()
260        .map_err(|e| Error::io("record COPC info payload offset", e))?;
261    writer
262        .write_all(&[0u8; 160])
263        .map_err(|e| Error::io("write COPC info placeholder", e))?;
264
265    write_vlr_header(
266        &mut writer,
267        "laszip encoded",
268        22204,
269        var_vlr_bytes.len() as u16,
270        "http://laszip.org",
271    )?;
272    writer
273        .write_all(&var_vlr_bytes)
274        .map_err(|e| Error::io("write LAZ VLR", e))?;
275
276    let point_data_actual_start = writer
277        .stream_position()
278        .map_err(|e| Error::io("record point data offset", e))?;
279    if point_data_actual_start as u32 != offset_to_point_data {
280        return Err(Error::InvalidInput(format!(
281            "VLR size accounting mismatch: at {point_data_actual_start}, expected {offset_to_point_data}"
282        )));
283    }
284
285    let mut compressor = LasZipCompressor::new(&mut writer, var_vlr.clone())
286        .map_err(|e| Error::Las(format!("compressor: {e}")))?;
287    let mut hierarchy: Vec<Entry> = Vec::with_capacity(nodes.len());
288    let mut point_buf = vec![0u8; point_record_length as usize];
289    let mut chunk_start_file_offset = compressor
290        .get_mut()
291        .stream_position()
292        .map_err(|e| Error::io("record chunk start", e))?;
293    chunk_start_file_offset += 8;
294
295    for (key, indices) in &nodes {
296        cancel.check()?;
297        for (point_index, &source_index) in indices.iter().enumerate() {
298            if point_index % CANCEL_POLL_STRIDE == 0 {
299                cancel.check()?;
300            }
301            let fields = source.fields(source_index as usize)?;
302            encode_point_record(
303                &mut point_buf,
304                &fields,
305                (scale_x, scale_y, scale_z),
306                (offset_x, offset_y, offset_z),
307                point_format_id,
308                has_color,
309            )?;
310            compressor
311                .compress_one(&point_buf)
312                .map_err(|e| Error::Las(format!("compress point: {e}")))?;
313        }
314        compressor
315            .finish_current_chunk()
316            .map_err(|e| Error::Las(format!("finish chunk: {e}")))?;
317        let after = compressor
318            .get_mut()
319            .stream_position()
320            .map_err(|e| Error::io("record chunk end", e))?;
321        hierarchy.push(Entry {
322            key: *key,
323            offset: chunk_start_file_offset,
324            byte_size: (after - chunk_start_file_offset) as i32,
325            point_count: indices.len() as i32,
326        });
327        chunk_start_file_offset = after;
328    }
329
330    cancel.check()?;
331    compressor
332        .done()
333        .map_err(|e| Error::Las(format!("finish compressor: {e}")))?;
334    drop(compressor);
335
336    let evlr_start = writer
337        .stream_position()
338        .map_err(|e| Error::io("record EVLR start", e))?;
339    let hierarchy_body_size = (hierarchy.len() * 32) as u64;
340    write_evlr_header(
341        &mut writer,
342        "copc",
343        1000,
344        hierarchy_body_size,
345        "COPC hierarchy",
346    )?;
347    let root_hier_offset = writer
348        .stream_position()
349        .map_err(|e| Error::io("record root hierarchy offset", e))?;
350    let mut entry_buf = [0u8; 32];
351    for entry in &hierarchy {
352        entry.write_le(&mut entry_buf)?;
353        writer
354            .write_all(&entry_buf)
355            .map_err(|e| Error::io("write hierarchy entry", e))?;
356    }
357
358    writer
359        .seek(SeekFrom::Start(copc_info_payload_start))
360        .map_err(|e| Error::io("seek COPC info payload", e))?;
361    let info = CopcInfo {
362        center,
363        halfsize,
364        spacing: halfsize / 128.0,
365        root_hier_offset,
366        root_hier_size: hierarchy_body_size,
367        gpstime_min: 0.0,
368        gpstime_max: 0.0,
369    };
370    writer
371        .write_all(&info.write_le_bytes())
372        .map_err(|e| Error::io("patch COPC info", e))?;
373
374    writer
375        .seek(SeekFrom::Start(235))
376        .map_err(|e| Error::io("seek first EVLR offset", e))?;
377    writer
378        .write_u64::<LittleEndian>(evlr_start)
379        .map_err(|e| Error::io("patch first EVLR offset", e))?;
380
381    writer
382        .flush()
383        .map_err(|e| Error::io("flush COPC file", e))?;
384    Ok(())
385}
386
387fn build_lod_nodes<S: CopcPointSource>(
388    source: &S,
389    center: (f64, f64, f64),
390    halfsize: f64,
391    params: &CopcWriterParams,
392    cancel: &dyn CancelCheck,
393) -> Result<Vec<(VoxelKey, Vec<u32>)>> {
394    cancel.check()?;
395    let total_points = u32::try_from(source.len()).map_err(|_| {
396        Error::InvalidInput("COPC writer supports at most u32::MAX points per file".into())
397    })?;
398    let max_points_per_node = params.max_points_per_node.max(1) as usize;
399    let max_depth = params.max_depth.min(30);
400    let mut builder = LodNodeBuilder {
401        source,
402        max_points_per_node,
403        max_depth,
404        cancel,
405        nodes: Vec::new(),
406    };
407    builder.assign(
408        VoxelKey::root(),
409        (0..total_points).collect(),
410        Bounds::cube(center, halfsize),
411    )?;
412    let mut nodes = builder.nodes;
413    nodes.sort_by_key(|(key, _)| *key);
414    Ok(nodes)
415}
416
417struct LodNodeBuilder<'a, S: CopcPointSource> {
418    source: &'a S,
419    max_points_per_node: usize,
420    max_depth: u32,
421    cancel: &'a dyn CancelCheck,
422    nodes: Vec<(VoxelKey, Vec<u32>)>,
423}
424
425impl<S: CopcPointSource> LodNodeBuilder<'_, S> {
426    fn assign(&mut self, key: VoxelKey, indices: Vec<u32>, bounds: Bounds) -> Result<()> {
427        self.cancel.check()?;
428        if indices.is_empty() {
429            return Ok(());
430        }
431        if indices.len() <= self.max_points_per_node || key.level as u32 >= self.max_depth {
432            self.nodes.push((key, indices));
433            return Ok(());
434        }
435
436        let mut children: [Vec<u32>; 8] = std::array::from_fn(|_| Vec::new());
437        for (partition_index, index) in indices.into_iter().enumerate() {
438            if partition_index % 16_384 == 0 {
439                self.cancel.check()?;
440            }
441            let (px, py, pz) = self.source.xyz(index as usize);
442            children[child_octant(bounds, px, py, pz)].push(index);
443        }
444        for child in &mut children {
445            child.reverse();
446        }
447
448        let mut selected = Vec::with_capacity(self.max_points_per_node);
449        while selected.len() < self.max_points_per_node {
450            let mut progressed = false;
451            for child in &mut children {
452                if let Some(index) = child.pop() {
453                    selected.push(index);
454                    progressed = true;
455                    if selected.len() == self.max_points_per_node {
456                        break;
457                    }
458                }
459            }
460            if !progressed {
461                break;
462            }
463        }
464        self.nodes.push((key, selected));
465
466        for (octant, child_indices) in children.into_iter().enumerate() {
467            if child_indices.is_empty() {
468                continue;
469            }
470            self.assign(
471                key.child(octant as u8),
472                child_indices,
473                bounds.octant(octant as u8),
474            )?;
475        }
476        Ok(())
477    }
478}
479
480fn child_octant(bounds: Bounds, x: f64, y: f64, z: f64) -> usize {
481    let center = bounds.center();
482    usize::from(x >= center.0)
483        | (usize::from(y >= center.1) << 1)
484        | (usize::from(z >= center.2) << 2)
485}
486
487fn cube_from_bounds(bounds: &Bounds) -> ((f64, f64, f64), f64) {
488    let center = bounds.center();
489    let dx = bounds.max.0 - bounds.min.0;
490    let dy = bounds.max.1 - bounds.min.1;
491    let dz = bounds.max.2 - bounds.min.2;
492    let halfsize = (dx.max(dy).max(dz) * 0.5).max(1e-6);
493    (center, halfsize)
494}
495
496struct LasHeader {
497    point_data_format: u8,
498    point_record_length: u16,
499    offset_to_point_data: u32,
500    number_of_vlrs: u32,
501    scale: (f64, f64, f64),
502    offset: (f64, f64, f64),
503    bounds: Bounds,
504    legacy_point_count: u32,
505    total_point_count: u64,
506    offset_to_first_evlr: u64,
507    number_of_evlrs: u32,
508}
509
510impl LasHeader {
511    fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
512        writer
513            .write_all(b"LASF")
514            .map_err(|e| Error::io("write LAS signature", e))?;
515        writer
516            .write_u16::<LittleEndian>(0)
517            .map_err(|e| Error::io("write file source id", e))?;
518        writer
519            .write_u16::<LittleEndian>(0)
520            .map_err(|e| Error::io("write global encoding", e))?;
521        writer
522            .write_u32::<LittleEndian>(0)
523            .map_err(|e| Error::io("write GUID1", e))?;
524        writer
525            .write_u16::<LittleEndian>(0)
526            .map_err(|e| Error::io("write GUID2", e))?;
527        writer
528            .write_u16::<LittleEndian>(0)
529            .map_err(|e| Error::io("write GUID3", e))?;
530        writer
531            .write_all(&[0u8; 8])
532            .map_err(|e| Error::io("write GUID4", e))?;
533        writer
534            .write_u8(1)
535            .map_err(|e| Error::io("write version major", e))?;
536        writer
537            .write_u8(4)
538            .map_err(|e| Error::io("write version minor", e))?;
539        writer
540            .write_all(&pad(b"copc-rust", 32))
541            .map_err(|e| Error::io("write system id", e))?;
542        writer
543            .write_all(&pad(b"copc-writer", 32))
544            .map_err(|e| Error::io("write generating software", e))?;
545        writer
546            .write_u16::<LittleEndian>(0)
547            .map_err(|e| Error::io("write creation day", e))?;
548        writer
549            .write_u16::<LittleEndian>(2026)
550            .map_err(|e| Error::io("write creation year", e))?;
551        writer
552            .write_u16::<LittleEndian>(375)
553            .map_err(|e| Error::io("write header size", e))?;
554        writer
555            .write_u32::<LittleEndian>(self.offset_to_point_data)
556            .map_err(|e| Error::io("write point data offset", e))?;
557        writer
558            .write_u32::<LittleEndian>(self.number_of_vlrs)
559            .map_err(|e| Error::io("write VLR count", e))?;
560        writer
561            .write_u8(self.point_data_format)
562            .map_err(|e| Error::io("write point format", e))?;
563        writer
564            .write_u16::<LittleEndian>(self.point_record_length)
565            .map_err(|e| Error::io("write point record length", e))?;
566        writer
567            .write_u32::<LittleEndian>(self.legacy_point_count)
568            .map_err(|e| Error::io("write legacy point count", e))?;
569        for _ in 0..5 {
570            writer
571                .write_u32::<LittleEndian>(0)
572                .map_err(|e| Error::io("write legacy returns", e))?;
573        }
574        writer
575            .write_f64::<LittleEndian>(self.scale.0)
576            .map_err(|e| Error::io("write x scale", e))?;
577        writer
578            .write_f64::<LittleEndian>(self.scale.1)
579            .map_err(|e| Error::io("write y scale", e))?;
580        writer
581            .write_f64::<LittleEndian>(self.scale.2)
582            .map_err(|e| Error::io("write z scale", e))?;
583        writer
584            .write_f64::<LittleEndian>(self.offset.0)
585            .map_err(|e| Error::io("write x offset", e))?;
586        writer
587            .write_f64::<LittleEndian>(self.offset.1)
588            .map_err(|e| Error::io("write y offset", e))?;
589        writer
590            .write_f64::<LittleEndian>(self.offset.2)
591            .map_err(|e| Error::io("write z offset", e))?;
592        writer
593            .write_f64::<LittleEndian>(self.bounds.max.0)
594            .map_err(|e| Error::io("write max x", e))?;
595        writer
596            .write_f64::<LittleEndian>(self.bounds.min.0)
597            .map_err(|e| Error::io("write min x", e))?;
598        writer
599            .write_f64::<LittleEndian>(self.bounds.max.1)
600            .map_err(|e| Error::io("write max y", e))?;
601        writer
602            .write_f64::<LittleEndian>(self.bounds.min.1)
603            .map_err(|e| Error::io("write min y", e))?;
604        writer
605            .write_f64::<LittleEndian>(self.bounds.max.2)
606            .map_err(|e| Error::io("write max z", e))?;
607        writer
608            .write_f64::<LittleEndian>(self.bounds.min.2)
609            .map_err(|e| Error::io("write min z", e))?;
610        writer
611            .write_u64::<LittleEndian>(0)
612            .map_err(|e| Error::io("write waveform packet start", e))?;
613        writer
614            .write_u64::<LittleEndian>(self.offset_to_first_evlr)
615            .map_err(|e| Error::io("write first EVLR offset", e))?;
616        writer
617            .write_u32::<LittleEndian>(self.number_of_evlrs)
618            .map_err(|e| Error::io("write EVLR count", e))?;
619        writer
620            .write_u64::<LittleEndian>(self.total_point_count)
621            .map_err(|e| Error::io("write total point count", e))?;
622        for _ in 0..15 {
623            writer
624                .write_u64::<LittleEndian>(0)
625                .map_err(|e| Error::io("write extended returns", e))?;
626        }
627        Ok(())
628    }
629}
630
631fn pad(value: &[u8], len: usize) -> Vec<u8> {
632    let mut out = Vec::with_capacity(len);
633    let take = value.len().min(len);
634    out.extend_from_slice(&value[..take]);
635    out.resize(len, 0);
636    out
637}
638
639fn write_vlr_header<W: Write>(
640    writer: &mut W,
641    user_id: &str,
642    record_id: u16,
643    body_size: u16,
644    description: &str,
645) -> Result<()> {
646    writer
647        .write_u16::<LittleEndian>(0)
648        .map_err(|e| Error::io("write VLR reserved", e))?;
649    writer
650        .write_all(&pad(user_id.as_bytes(), 16))
651        .map_err(|e| Error::io("write VLR user id", e))?;
652    writer
653        .write_u16::<LittleEndian>(record_id)
654        .map_err(|e| Error::io("write VLR record id", e))?;
655    writer
656        .write_u16::<LittleEndian>(body_size)
657        .map_err(|e| Error::io("write VLR body size", e))?;
658    writer
659        .write_all(&pad(description.as_bytes(), 32))
660        .map_err(|e| Error::io("write VLR description", e))?;
661    Ok(())
662}
663
664fn write_evlr_header<W: Write>(
665    writer: &mut W,
666    user_id: &str,
667    record_id: u16,
668    body_size: u64,
669    description: &str,
670) -> Result<()> {
671    writer
672        .write_u16::<LittleEndian>(0)
673        .map_err(|e| Error::io("write EVLR reserved", e))?;
674    writer
675        .write_all(&pad(user_id.as_bytes(), 16))
676        .map_err(|e| Error::io("write EVLR user id", e))?;
677    writer
678        .write_u16::<LittleEndian>(record_id)
679        .map_err(|e| Error::io("write EVLR record id", e))?;
680    writer
681        .write_u64::<LittleEndian>(body_size)
682        .map_err(|e| Error::io("write EVLR body size", e))?;
683    writer
684        .write_all(&pad(description.as_bytes(), 32))
685        .map_err(|e| Error::io("write EVLR description", e))?;
686    Ok(())
687}
688
689fn encode_point_record(
690    buf: &mut [u8],
691    fields: &CopcPointFields,
692    scale: (f64, f64, f64),
693    offset: (f64, f64, f64),
694    format_id: u8,
695    has_color: bool,
696) -> Result<()> {
697    let mut cursor = Cursor::new(buf);
698    let ix = ((fields.x - offset.0) / scale.0).round() as i32;
699    let iy = ((fields.y - offset.1) / scale.1).round() as i32;
700    let iz = ((fields.z - offset.2) / scale.2).round() as i32;
701    cursor
702        .write_i32::<LittleEndian>(ix)
703        .map_err(|e| Error::io("write point x", e))?;
704    cursor
705        .write_i32::<LittleEndian>(iy)
706        .map_err(|e| Error::io("write point y", e))?;
707    cursor
708        .write_i32::<LittleEndian>(iz)
709        .map_err(|e| Error::io("write point z", e))?;
710    cursor
711        .write_u16::<LittleEndian>(fields.intensity)
712        .map_err(|e| Error::io("write intensity", e))?;
713    let rn = fields.return_number & 0x0F;
714    let nr = fields.number_of_returns & 0x0F;
715    cursor
716        .write_u8(rn | (nr << 4))
717        .map_err(|e| Error::io("write return flags", e))?;
718    let flags = (fields.synthetic & 1)
719        | ((fields.key_point & 1) << 1)
720        | ((fields.withheld & 1) << 2)
721        | ((fields.overlap & 1) << 3);
722    let chan = fields.scan_channel & 0x03;
723    let sd = fields.scan_direction_flag & 1;
724    let eof = fields.edge_of_flight_line & 1;
725    cursor
726        .write_u8(flags | (chan << 4) | (sd << 6) | (eof << 7))
727        .map_err(|e| Error::io("write classification flags", e))?;
728    cursor
729        .write_u8(fields.classification)
730        .map_err(|e| Error::io("write classification", e))?;
731    cursor
732        .write_u8(fields.user_data)
733        .map_err(|e| Error::io("write user data", e))?;
734    let scan_angle = (fields.scan_angle_rank as f32 / 0.006) as i16;
735    cursor
736        .write_i16::<LittleEndian>(scan_angle)
737        .map_err(|e| Error::io("write scan angle", e))?;
738    cursor
739        .write_u16::<LittleEndian>(fields.point_source_id)
740        .map_err(|e| Error::io("write point source id", e))?;
741    cursor
742        .write_f64::<LittleEndian>(fields.gps_time)
743        .map_err(|e| Error::io("write gps time", e))?;
744    if format_id == 7 && has_color {
745        cursor
746            .write_u16::<LittleEndian>(fields.red)
747            .map_err(|e| Error::io("write red", e))?;
748        cursor
749            .write_u16::<LittleEndian>(fields.green)
750            .map_err(|e| Error::io("write green", e))?;
751        cursor
752            .write_u16::<LittleEndian>(fields.blue)
753            .map_err(|e| Error::io("write blue", e))?;
754    }
755    Ok(())
756}