acadrust 0.6.7

A pure Rust library for reading and writing CAD files in DXF format (ASCII and Binary) and DWG format (Binary).
Documentation
use std::io::Cursor;

use acadrust::io::dwg::dwg_stream_readers::{
    handle_reader::read_handles, merged_reader::DwgMergedReader, object_reader::DwgObjectReader,
};
use acadrust::io::dwg::dwg_stream_writers::bit_writer::DwgBitWriter;
use acadrust::io::dwg::dwg_version::DwgVersion;
use acadrust::objects::{
    ClassObject, ClassObjectData, DataTable, DataTableCellType, DataTableColumn, DataTableValue,
    LayerFilter, ObjectType,
};
use acadrust::tables::Layer;
use acadrust::types::DxfVersion;
use acadrust::types::Vector3;
use acadrust::{CadDocument, DwgReader, DwgWriter, DxfReader, DxfWriter, Handle};

fn table(document: &CadDocument) -> &DataTable {
    document
        .objects
        .values()
        .find_map(|object| match object {
            ObjectType::ClassObject(ClassObject {
                data: ClassObjectData::DataTable(table),
                ..
            }) => Some(table),
            _ => None,
        })
        .expect("DATATABLE must remain a native object")
}

fn native_document(dwg: bool) -> CadDocument {
    if dwg {
        DwgReader::from_stream(Cursor::new(include_bytes!("datatable/point_object_id.dwg")))
            .read()
            .unwrap()
    } else {
        DxfReader::from_reader(Cursor::new(include_bytes!("datatable/point_object_id.dxf")))
            .unwrap()
            .read()
            .unwrap()
    }
}

fn table_handle(document: &CadDocument) -> Handle {
    *document
        .objects
        .iter()
        .find(|(_, object)| {
            matches!(
                object,
                ObjectType::ClassObject(ClassObject {
                    data: ClassObjectData::DataTable(_),
                    ..
                })
            )
        })
        .unwrap()
        .0
}

fn table_record(bytes: &[u8], handle: Handle) -> DwgMergedReader {
    let mut reader = DwgReader::from_stream(Cursor::new(bytes));
    let header = reader.read_file_header().unwrap();
    let handles =
        read_handles(&reader.get_section_buffer("AcDb:Handles", &header).unwrap()).unwrap();
    let offset = handles[&handle.value()];
    let objects = DwgObjectReader::new(
        reader
            .get_section_buffer("AcDb:AcDbObjects", &header)
            .unwrap(),
        DxfVersion::AC1032,
        handles,
    )
    .unwrap();
    let (code, mut record) = objects.read_record_at(offset as usize).unwrap();
    objects.read_common_non_entity_data(&mut record, code);
    record
}

fn read_table_prefix(record: &mut DwgMergedReader) {
    assert_eq!(record.read_bit_short(), 2);
    assert_eq!(record.read_bit_long(), 2);
    assert_eq!(record.read_bit_long(), 1);
    assert_eq!(record.read_variable_text(), "");
}

#[test]
fn reads_native_autocad_datatable_in_both_formats() {
    for dwg in [false, true] {
        let document = native_document(dwg);
        let table = table(&document);
        assert_eq!(table.row_count, 1);
        assert_eq!(table.columns.len(), 2);
        assert_eq!(table.columns[0].cell_type(), Some(DataTableCellType::Point));
        assert_eq!(table.columns[0].rows[0].point, Vector3::new(1.5, 2.5, 3.5));
        assert_eq!(
            table.columns[1].cell_type(),
            Some(DataTableCellType::ObjectId)
        );
        assert_eq!(table.columns[1].rows[0].handle, document.layers.handle());
        assert_eq!(table.columns[1].rows[0].handle, Handle::new(2));
    }
}

#[test]
fn edited_native_datatable_survives_dwg_and_both_dxf_encodings() {
    let mut document = native_document(true);
    for object in document.objects.values_mut() {
        if let ObjectType::ClassObject(ClassObject {
            data: ClassObjectData::DataTable(table),
            ..
        }) = object
        {
            table.columns[0].rows[0].point = Vector3::new(-8.25, 0.0, 12.5);
            table.columns[1].rows[0].handle = document.text_styles.handle();
        }
    }
    let expected = table(&document).clone();
    let dwg = DwgReader::from_stream(Cursor::new(DwgWriter::write_to_vec(&document).unwrap()))
        .read()
        .unwrap();
    assert_eq!(table(&dwg), &expected);
    for writer in [DxfWriter::new(&document), DxfWriter::new_binary(&document)] {
        let dxf = DxfReader::from_reader(Cursor::new(writer.write_to_vec().unwrap()))
            .unwrap()
            .read()
            .unwrap();
        assert_eq!(table(&dxf), &expected);
    }
}

#[test]
fn dwg_writer_uses_native_point_and_soft_pointer_encodings() {
    let document = native_document(true);
    let bytes = DwgWriter::write_to_vec(&document).unwrap();
    let mut record = table_record(&bytes, table_handle(&document));
    read_table_prefix(&mut record);
    assert_eq!(record.read_bit_long(), DataTableCellType::Point as i32);
    assert_eq!(record.read_variable_text(), "Point");
    assert_eq!(record.read_3bit_double(), Vector3::new(1.5, 2.5, 3.5));
    assert_eq!(record.read_bit_long(), DataTableCellType::ObjectId as i32);
    assert_eq!(record.read_variable_text(), "ObjectId");
    assert_eq!(
        record.read_typed_handle(),
        (2, acadrust::io::dwg::DwgReferenceType::SoftPointer)
    );
}

#[test]
fn object_id_cells_follow_handle_collision_repairs() {
    let mut document = CadDocument::new();
    let old_handle = Handle::new(0x1234);
    let mut layer = Layer::new("COLLISION");
    layer.handle = old_handle;
    document.layers.add(layer).unwrap();
    let mut target = ClassObject::new(ClassObjectData::LayerFilter(LayerFilter {
        names: vec!["target".into()],
    }));
    target.handle = old_handle;
    document
        .objects
        .insert(old_handle, ObjectType::ClassObject(target));
    let mut object = ClassObject::new(ClassObjectData::DataTable(DataTable {
        row_count: 1,
        columns: (5..=9)
            .map(|value_type| DataTableColumn {
                value_type,
                name: format!("Reference {value_type}"),
                rows: vec![DataTableValue {
                    handle: old_handle,
                    ..Default::default()
                }],
            })
            .collect(),
        ..Default::default()
    }));
    object.handle = Handle::new(0x1235);
    document
        .objects
        .insert(object.handle, ObjectType::ClassObject(object));

    document.resolve_references();
    let repaired = document
        .objects
        .iter()
        .find_map(|(handle, object)| match object {
            ObjectType::ClassObject(ClassObject {
                data: ClassObjectData::LayerFilter(_),
                ..
            }) => Some(*handle),
            _ => None,
        })
        .unwrap();
    assert_ne!(repaired, old_handle);
    for column in &table(&document).columns {
        assert_eq!(column.rows[0].handle, repaired);
    }
}

#[test]
fn writers_reject_unsupported_cells_and_inconsistent_row_counts() {
    for (value_type, row_count) in [(6, 1), (99, 1), (1, 2), (1, -1)] {
        let mut document = CadDocument::new();
        let mut object = ClassObject::new(ClassObjectData::DataTable(DataTable {
            row_count,
            columns: vec![DataTableColumn {
                value_type,
                name: "Invalid".into(),
                rows: vec![DataTableValue::default()],
            }],
            ..Default::default()
        }));
        object.handle = document.allocate_handle();
        document
            .objects
            .insert(object.handle, ObjectType::ClassObject(object));
        assert!(DwgWriter::write_to_vec(&document).is_err());
        assert!(DxfWriter::new(&document).write_to_vec().is_err());
    }
}

#[test]
fn unsupported_dwg_cell_types_preserve_the_complete_record() {
    for cell_type in [6, 99] {
        let mut document = native_document(true);
        let handle = table_handle(&document);
        let mut record = table_record(include_bytes!("datatable/point_object_id.dwg"), handle);
        read_table_prefix(&mut record);
        let type_position = record.position_in_bits();
        assert_eq!(record.read_bit_long(), 4);
        let old_end = record.position_in_bits();
        let raw = record.raw_merged_data();
        let mut patch = DwgBitWriter::new(DwgVersion::AC24, DxfVersion::AC1032);
        patch.write_bytes(&raw);
        patch.set_position_in_bits(type_position);
        patch.write_bit_long(cell_type);
        assert_eq!(
            patch.position_in_bits(),
            old_end,
            "replacement must preserve framing"
        );
        patch.set_position_in_bits(raw.len() as i64 * 8);
        let raw = patch.into_bytes();
        document.objects.insert(
            handle,
            ObjectType::Unknown {
                type_name: "DATATABLE".into(),
                handle,
                owner: document.header.named_objects_dict_handle,
                raw_dxf_codes: None,
                raw_dwg_data: Some(raw.clone()),
                raw_dwg_handle_bits: record.get_handle_bits(),
                raw_dwg_version: Some(DxfVersion::AC1032),
            },
        );
        for _ in 0..2 {
            let bytes = DwgWriter::write_to_vec(&document).unwrap();
            document = DwgReader::from_stream(Cursor::new(bytes)).read().unwrap();
            let ObjectType::Unknown {
                raw_dwg_data: Some(saved),
                ..
            } = &document.objects[&handle]
            else {
                panic!("unsupported table must remain opaque");
            };
            assert_eq!(saved, &raw);
            let ObjectType::Dictionary(root) =
                &document.objects[&document.header.named_objects_dict_handle]
            else {
                panic!("missing root dictionary");
            };
            assert_eq!(root.get("Review93DATATABLE"), Some(handle));
        }
    }
}