compas icon indicating copy to clipboard operation
compas copied to clipboard

`DXF` reader enhancement

Open ZacZhangzhuo opened this issue 1 year ago • 3 comments

Feature Request

Currently, the DXF reader is not development. I had a bit of time looking at it and felt we could develop it. @tomvanmele

Details

.dxf is a fairly complex format containing many many datatypes, but general datatype like Line and Mesh support might be helpful. I found that ezdxf package is pretty lightweight and can be very helpful. shall we introduce this package?

Example code



import ezdxf

class DXF(object):
    """Class for working with DXF files.

    Parameters
    ----------
    filepath : path string | file-like object
        A path, a file-like object.
    precision : str, optional
        A precision specification.

    Attributes
    ----------
    reader : :class:`DXFReader`, read-only
        A DXF file reader.
    parser : :class:`DXFParser`, read-only
        A DXF data parser.

    References
    ----------
    * https://en.wikipedia.org/wiki/AutoCAD_DXF
    * http://paulbourke.net/dataformats/dxf/
    * http://paulbourke.net/dataformats/dxf/min3d.html
    * https://ezdxf.readthedocs.io/

    """

    def __init__(self, filepath, precision=None):
        self.filepath = filepath
        self.precision = precision
        self._is_parsed = False
        self._reader = None
        self._parser = None

    def read(self):
        """Read and parse the contents of the file."""
        self._reader = DXFReader(self.filepath)
        self._parser = DXFParser(self._reader, precision=self.precision)
        self._reader.open()
        self._reader.read()
        self._parser.parse()
        self._is_parsed = True

    @property
    def reader(self):
        if not self._is_parsed:
            self.read()
        return self._reader

    @property
    def parser(self):
        if not self._is_parsed:
            self.read()
        return self._parser

    @property
    def vertices(self):
        return self.parser.vertices

    @property
    def lines(self):
        return self.parser.lines

    @property
    def points(self):
        return self.parser.points

    @property
    def face3ds(self):
        return self.parser.face3ds

    @property
    def meshes(self):
        return self.parser.meshes

    @property
    def polyline2ds (self):
        return self.parser.polyline2ds

    @property
    def polyline3ds (self):
        return self.parser.polyline3ds



class DXFReader(object):
    """Class for reading data from DXF files.

    Parameters
    ----------
    filepath : path string | file-like object
        A path, a file-like object.

    """

    def __init__(self, filepath):
        self.filepath = filepath
        self.doc = None
        self.content = None

        self.lines = None
        self.points = None
        self.face3ds = None
        self.meshes = None
        self.polyline2ds = []
        self.polyline3ds = []

    def open(self):
        """Open the file and read its contents.

        Returns
        -------
        None
        """
        try:
            self.content = ezdxf.readfile(self.filepath)
        except IOError:
            print(f"Not a DXF file or a generic I/O error.")
        except ezdxf.DXFStructureError:
            print(f"Invalid or corrupted DXF file.")

    def read(self):
        """Read the contents of the file."""

        self.lines = self.content.modelspace().query("LINE")

        self.points = self.content.modelspace().query("POINT")

        # The 3DFACE entity is real 3D solid filled triangle or quadrilateral.
        self.face3ds = self.content.modelspace().query("3DFACE")

        # Not yet implemented, example file needed
        # self.meshes = self.content.modelspace().query("MESH")

        polyline_entities = self.content.modelspace().query("POLYLINE")
        
        for entity in polyline_entities:
            if entity.is_2d_polyline:
                self.polyline2ds.append(entity)
            elif entity.is_3d_polyline:
                self.polyline3ds.append(entity)



class DXFParser(object):
    """Class for parsing data from DXF files.

    The parser converts the raw geometric data of the file
    into corresponding geometry objects and data structures.

    Parameters
    ----------
    reader : :class:`DXFReader`
        A DXF file reader.
    precision : str
        Precision specification for parsing geometric data.

    """

    def __init__(self, reader, precision):
        self.precision = precision
        self.reader = reader
        self.vertices = None
        self.points = None
        self.face3ds = None
        self.lines = None
        self.polylines = None
        self.faces = None
        self.groups = None
        self.objects = None
        self.meshes = None
        self.polyline2ds = None
        self.polyline3ds = None

    def parse(self):
        """Parse the the data found by the reader."""

        self.lines = [
            [list(line.dxf.start), list(line.dxf.end)] for line in self.reader.lines
        ]

        self.points = [list(point.dxf.location) for point in self.reader.points]

        # self.meshes = [
        #     [mesh.MeshData.vertices, mesh.MeshData.faces] for mesh in self.reader.meshes
        # ]

        # self.parse_face3ds()

        self.face3ds = [[v.xyz for v in face.wcs_vertices()] for face in self.reader.face3ds]


        # The POLYLINE entity is very complex, it’s used to build 2D/3D polylines, 3D meshes and 3D polyfaces.
        # TODO Convert OCS to WCS
        self.polyline2ds = [[v.dxf.location for v in polyline.vertices] for polyline in self.reader.polyline2ds]
        self.polyline3ds = [[v.dxf.location for v in polyline.vertices] for polyline in self.reader.polyline3ds]



    def parse_face3ds_deprecated(self):
        # Not used for now
        # At the moment, the 3d faces are parsed as points

        if self.face3ds is None:
            self.face3ds = []

        for face3d in self.reader.face3ds:
            [f0, f1, f2, f3] = face3d.get_edges_visibility()

            if f0:
                self.face3ds.append([list(face3d.dxf.vtx0), list(face3d.dxf.vtx1)])
            if f1:
                self.face3ds.append([list(face3d.dxf.vtx1), list(face3d.dxf.vtx2)])
            if f2:
                self.face3ds.append([list(face3d.dxf.vtx2), list(face3d.dxf.vtx3)])
            if f3:
                self.face3ds.append([list(face3d.dxf.vtx3), list(face3d.dxf.vtx0)])

This code support:

.dxf
unit
coordinate system
vertices list[list[x,y,z]]
faces list[int]
normals
lines list[[x0,y0,z0],[x1,y1,z1]]
polyline list[list[x,y,z]]
points list[x,y,z]
groups
textures

ZacZhangzhuo avatar Nov 10 '23 16:11 ZacZhangzhuo

@tomvanmele || @gonzalocasas ezdxf is no longer a dependency, pls close

jf--- avatar Apr 10 '24 22:04 jf---

@jf--- actually i would like to keep this open because i see some value in supporting DXF files in the context of being able to read geometric data from older sources and formats. ezdxf was only removed because we never got around to writing an actual implementation...

tomvanmele avatar Apr 11 '24 07:04 tomvanmele

Gotcha, I assumed that with the dependency gone that this maybe have become irrelevant. Thanks for clarifying.

jf--- avatar Apr 11 '24 07:04 jf---