compas-dev / compas

Core packages of the COMPAS framework.
https://compas.dev/compas/
MIT License
307 stars 104 forks source link

`DXF` reader enhancement #1210

Open ZacZhangzhuo opened 10 months ago

ZacZhangzhuo commented 10 months ago

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
jf--- commented 5 months ago

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

tomvanmele commented 5 months ago

@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...

jf--- commented 5 months ago

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