dxfscan 0.1.0

Binary DXF parser with typed entity data and lookup indices
Documentation
use dxfbin::{BinarySink, convert_all};

fn text_to_binary(text: &[u8]) -> Vec<u8> {
    let mut out = Vec::new();
    let mut sink = BinarySink::new(&mut out);
    convert_all(text, &mut sink).expect("dxfbin conversion failed");
    out
}

#[test]
fn header_variables() {
    let text = b"\
  0\nSECTION\n\
  2\nHEADER\n\
  9\n$ACADVER\n\
  1\nAC1027\n\
  9\n$INSUNITS\n\
 70\n4\n\
  9\n$LTSCALE\n\
 40\n2.5\n\
  9\n$INSBASE\n\
 10\n100.0\n\
 20\n200.0\n\
 30\n0.0\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    // String variable.
    match drawing.header("$ACADVER") {
        Some(dxfscan::GroupValue::String(s)) => assert_eq!(*s, b"AC1027"),
        other => panic!("expected String, got {other:?}"),
    }

    // i16 variable.
    match drawing.header("$INSUNITS") {
        Some(dxfscan::GroupValue::Int16(v)) => assert_eq!(*v, 4),
        other => panic!("expected Int16, got {other:?}"),
    }

    // Double variable.
    match drawing.header("$LTSCALE") {
        Some(dxfscan::GroupValue::Double(v)) => assert_eq!(*v, 2.5),
        other => panic!("expected Double, got {other:?}"),
    }

    // Multi-component variable stores only first component.
    match drawing.header("$INSBASE") {
        Some(dxfscan::GroupValue::Double(v)) => assert_eq!(*v, 100.0),
        other => panic!("expected Double (first component), got {other:?}"),
    }

    // Missing variable.
    assert!(drawing.header("$NOSUCH").is_none());
}

#[test]
fn minimal_dxf_sections() {
    let text = b"\
  0\nSECTION\n\
  2\nTABLES\n\
  0\nTABLE\n\
  2\nLAYER\n\
 70\n1\n\
  0\nLAYER\n\
  5\nA\n\
  2\nWalls\n\
 70\n0\n\
 62\n7\n\
370\n50\n\
  0\nENDTAB\n\
  0\nENDSEC\n\
  0\nSECTION\n\
  2\nBLOCKS\n\
  0\nBLOCK\n\
  2\nDoor\n\
 10\n0.0\n\
 20\n0.0\n\
  0\nLINE\n\
  5\n10\n\
  8\nWalls\n\
 10\n0.0\n\
 20\n0.0\n\
 11\n1.0\n\
 21\n0.0\n\
  0\nENDBLK\n\
  0\nENDSEC\n\
  0\nSECTION\n\
  2\nENTITIES\n\
  0\nLINE\n\
  5\n20\n\
  8\nWalls\n\
 62\n1\n\
 10\n0.0\n\
 20\n0.0\n\
 11\n10.0\n\
 21\n5.0\n\
  0\nCIRCLE\n\
  5\n21\n\
  8\nWalls\n\
 10\n5.0\n\
 20\n5.0\n\
 40\n2.5\n\
  0\nINSERT\n\
  5\n22\n\
  8\nWalls\n\
  2\nDoor\n\
 10\n3.0\n\
 20\n4.0\n\
 41\n1.0\n\
 42\n1.0\n\
 50\n90.0\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    // Layers
    assert_eq!(drawing.layers.len(), 1);
    assert_eq!(drawing.layers[0].name, b"Walls");
    assert_eq!(drawing.layers[0].color, 7);
    assert_eq!(drawing.layers[0].lineweight, 50);
    assert!(drawing.layer_by_name(b"Walls").is_some());

    // Blocks
    assert_eq!(drawing.blocks.len(), 1);
    assert_eq!(drawing.blocks[0].name, b"Door");
    assert_eq!(drawing.blocks[0].entities.len(), 1);
    assert!(drawing.block_by_name(b"Door").is_some());

    // Entities
    assert_eq!(drawing.entities.len(), 3);

    // LINE
    match &drawing.entities[0] {
        dxfscan::Entity::Line(common, line) => {
            assert_eq!(common.handle, b"20");
            assert_eq!(common.layer, b"Walls");
            assert_eq!(common.color, 1);
            assert_eq!(line.p1.x, 0.0);
            assert_eq!(line.p2.x, 10.0);
            assert_eq!(line.p2.y, 5.0);
        }
        other => panic!("expected Line, got {other:?}"),
    }

    // CIRCLE
    match &drawing.entities[1] {
        dxfscan::Entity::Circle(common, circle) => {
            assert_eq!(common.handle, b"21");
            assert_eq!(circle.center.x, 5.0);
            assert_eq!(circle.center.y, 5.0);
            assert_eq!(circle.radius, 2.5);
        }
        other => panic!("expected Circle, got {other:?}"),
    }

    // INSERT
    match &drawing.entities[2] {
        dxfscan::Entity::Insert(common, insert) => {
            assert_eq!(common.handle, b"22");
            assert_eq!(insert.name, b"Door");
            assert_eq!(insert.location.x, 3.0);
            assert_eq!(insert.location.y, 4.0);
            assert_eq!(insert.rotation, 90.0);
        }
        other => panic!("expected Insert, got {other:?}"),
    }

    // Handle lookup
    assert!(drawing.entity_by_handle(b"20").is_some());
    assert!(drawing.entity_by_handle(b"21").is_some());
    assert!(drawing.entity_by_handle(b"99").is_none());
}

#[test]
fn numeric_entity_types() {
    let text = b"\
  0\nSECTION\n\
  2\nENTITIES\n\
  0\nARC\n\
  5\n1\n\
  8\n0\n\
 10\n1.0\n\
 20\n2.0\n\
 40\n5.0\n\
 50\n0.0\n\
 51\n180.0\n\
  0\nELLIPSE\n\
  5\n2\n\
  8\n0\n\
 10\n0.0\n\
 20\n0.0\n\
 11\n3.0\n\
 21\n0.0\n\
 40\n0.5\n\
 41\n0.0\n\
 42\n6.283185307\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    assert_eq!(drawing.entities.len(), 2);

    match &drawing.entities[0] {
        dxfscan::Entity::Arc(_, arc) => {
            assert_eq!(arc.center.x, 1.0);
            assert_eq!(arc.center.y, 2.0);
            assert_eq!(arc.radius, 5.0);
            assert_eq!(arc.start_angle, 0.0);
            assert_eq!(arc.end_angle, 180.0);
        }
        other => panic!("expected Arc, got {other:?}"),
    }

    match &drawing.entities[1] {
        dxfscan::Entity::Ellipse(_, ellipse) => {
            assert_eq!(ellipse.major_axis.x, 3.0);
            assert_eq!(ellipse.minor_axis_ratio, 0.5);
        }
        other => panic!("expected Ellipse, got {other:?}"),
    }
}

#[test]
fn lwpolyline_vertices() {
    let text = b"\
  0\nSECTION\n\
  2\nENTITIES\n\
  0\nLWPOLYLINE\n\
  5\n1\n\
  8\n0\n\
 70\n1\n\
 10\n0.0\n\
 20\n0.0\n\
 42\n0.5\n\
 10\n10.0\n\
 20\n0.0\n\
 10\n10.0\n\
 20\n10.0\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    match &drawing.entities[0] {
        dxfscan::Entity::LwPolyline(_, lw) => {
            assert!(lw.is_closed());
            assert_eq!(lw.vertices.len(), 3);
            assert_eq!(lw.vertices[0].x, 0.0);
            assert_eq!(lw.vertices[0].bulge, 0.5);
            assert_eq!(lw.vertices[1].x, 10.0);
            assert_eq!(lw.vertices[1].bulge, 0.0); // default
            assert_eq!(lw.vertices[2].y, 10.0);
        }
        other => panic!("expected LwPolyline, got {other:?}"),
    }
}

#[test]
fn attrib_with_embedded_mtext() {
    // Simulate an ATTRIB entity that has an embedded AcDbMText subclass.
    // Group code 1 appears twice: once in AcDbText (empty), once in AcDbMText (real text).
    let text = b"\
  0\nSECTION\n\
  2\nENTITIES\n\
  0\nINSERT\n\
  5\n100\n\
  8\n0\n\
100\nAcDbEntity\n\
100\nAcDbBlockReference\n\
  2\nMyBlock\n\
 10\n0.0\n\
 20\n0.0\n\
 66\n1\n\
  0\nATTRIB\n\
  5\n101\n\
  8\n0\n\
100\nAcDbEntity\n\
100\nAcDbText\n\
 10\n1.0\n\
 20\n2.0\n\
 40\n5.0\n\
  1\n\n\
100\nAcDbAttribute\n\
  2\nTAG1\n\
 70\n0\n\
 74\n0\n\
100\nAcDbMText\n\
  1\nHello from MTEXT\n\
  0\nSEQEND\n\
  5\n102\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    assert_eq!(drawing.entities.len(), 1);
    match &drawing.entities[0] {
        dxfscan::Entity::Insert(_, ins) => {
            assert_eq!(ins.attributes.len(), 1);
            let a = &ins.attributes[0];
            // Primary value is empty.
            assert_eq!(a.value, b"");
            // Embedded MTEXT has the real text.
            let mt = a.mtext.as_ref().unwrap();
            assert_eq!(mt.text, b"Hello from MTEXT");
            assert_eq!(a.tag, b"TAG1");
        }
        other => panic!("expected Insert, got {other:?}"),
    }
}

#[test]
fn attrib_justification_and_second_alignment_point() {
    // ATTRIB with Center horizontal justification + Middle vertical.
    let text = b"\
  0\nSECTION\n\
  2\nENTITIES\n\
  0\nINSERT\n\
  5\n200\n\
  8\n0\n\
100\nAcDbEntity\n\
100\nAcDbBlockReference\n\
  2\nTestBlock\n\
 10\n0.0\n\
 20\n0.0\n\
 66\n1\n\
  0\nATTRIB\n\
  5\n201\n\
  8\nLayer1\n\
100\nAcDbEntity\n\
100\nAcDbText\n\
 10\n100.0\n\
 20\n200.0\n\
 40\n8.0\n\
  1\n6046\n\
 72\n1\n\
 11\n110.0\n\
 21\n205.0\n\
100\nAcDbAttribute\n\
  2\nNUMBER\n\
 70\n0\n\
 74\n2\n\
  0\nSEQEND\n\
  5\n202\n\
  0\nENDSEC\n\
  0\nEOF\n";

    let binary = text_to_binary(text);
    let drawing = dxfscan::scan(&binary).expect("scan failed");

    match &drawing.entities[0] {
        dxfscan::Entity::Insert(_, ins) => {
            let a = &ins.attributes[0];
            assert_eq!(a.value, b"6046");
            assert_eq!(a.h_justify, 1); // Center
            assert_eq!(a.v_justify, 2); // Middle
            assert_eq!(a.second_alignment_point.x, 110.0);
            assert_eq!(a.second_alignment_point.y, 205.0);
            assert_eq!(a.location.x, 100.0);
            assert_eq!(a.location.y, 200.0);
        }
        other => panic!("expected Insert, got {other:?}"),
    }
}