Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a DXF importer #817

Open
gumyr opened this issue Dec 15, 2024 · 2 comments
Open

Add a DXF importer #817

gumyr opened this issue Dec 15, 2024 · 2 comments
Assignees
Labels
enhancement New feature or request

Comments

@gumyr
Copy link
Owner

gumyr commented Dec 15, 2024

Here is an initial version that needs to be finished:

"""
build123d import dxf

name: import_dxf.py
by:   Gumyr
date: November 10th, 2024

desc:
    This python module imports a DXF file as build123d objects.

license:

    Copyright 2024 Gumyr

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

"""

import math
import warnings
import ezdxf
from build123d.objects_curve import (
    CenterArc,
    EllipticalCenterArc,
    Line,
    Polyline,
    SagittaArc,
    Spline,
    ThreePointArc,
)
from build123d.drafting import Arrow
from build123d.build_enums import Align
from build123d.objects_sketch import Circle, Polygon, Text
from build123d.geometry import Axis, Pos, TOLERANCE, Vector
from build123d.operations_generic import scale
from build123d.topology import ShapeList, Vertex, Wire


def process_arc(entity):
    """Convert ARC"""
    start, mid, end = entity.angles(3)
    arc_center = Vector(*entity.dxf.center)
    radius_vec = Vector(entity.dxf.radius, 0, 0)
    pnts = [arc_center + radius_vec.rotate(Axis.Z, a) for a in [start, mid, end]]
    return ThreePointArc(*pnts)


def process_circle(entity):
    """Convert CIRCLE"""
    return Pos(*entity.dxf.center) * Circle(entity.dxf.radius).edge()


def process_ellipse(entity):
    """Convert ELLIPSE"""
    center = entity.dxf.center
    major_axis = entity.dxf.major_axis
    x_radius = (major_axis[0] ** 2 + major_axis[1] ** 2) ** 0.5
    y_radius = x_radius * entity.dxf.ratio
    rotation = math.degrees(math.atan2(major_axis[1], major_axis[0]))
    start_angle = math.degrees(entity.dxf.start_param)
    end_angle = math.degrees(entity.dxf.end_param)
    return EllipticalCenterArc(
        center=center,
        x_radius=x_radius,
        y_radius=y_radius,
        start_angle=start_angle,
        end_angle=end_angle,
        rotation=rotation,
    )


def process_insert(entity, doc):
    """Process INSERT by referencing block definition and applying transformations."""
    block_name = entity.dxf.name
    # insert_point = (entity.dxf.insert.x, entity.dxf.insert.y, entity.dxf.insert.z)
    insert_point = entity.dxf.insert
    scale_factors = (entity.dxf.xscale, entity.dxf.yscale, entity.dxf.zscale)
    rotation_angle = entity.dxf.rotation

    # Retrieve the block definition
    block = doc.blocks.get(block_name)
    transformed_entities = []

    # Process each entity in the block definition
    for block_entity in block:
        dxftype = block_entity.dxftype()
        if dxftype in entity_dispatch:
            # Process the entity and apply transformations
            entity_object = entity_dispatch[dxftype](block_entity)
            transformed_entity = scale(entity_object, scale_factors)
            transformed_entity = transformed_entity.rotate(Axis.Z, rotation_angle)
            transformed_entity.position = insert_point
            transformed_entities.append(transformed_entity)
        else:
            warnings.warn(f"Unhandled block entity type: {dxftype}")

    return ShapeList(transformed_entities)


def process_leader(entity):
    """Convert LEADER entity to a Wire with an Arrow at the endpoint."""
    # Extract the vertices of the LEADER as (x, y) points
    vertices = [Vector(x, y) for x, y, *_ in entity.vertices]

    # Create a series of lines for the leader segments
    edges = [
        Line(start=vertices[i], end=vertices[i + 1]) for i in range(len(vertices) - 1)
    ]

    # Calculate arrow size based on leader length or use a default
    leader_length = sum(
        Line(start=vertices[i], end=vertices[i + 1]).length()
        for i in range(len(vertices) - 1)
    )
    arrow_size = (
        leader_length * 0.05 if leader_length > 0 else 1.0
    )  # Default size if leader is very short

    # Create an arrow at the end of the leader
    direction = vertices[-1] - vertices[-2]
    # arrow = Pos(*vertices[-1]) * Arrow(
    #     direction=direction.normalize(), size=arrow_size
    # )

    # Return the combined Wire (leader line) and Arrow (arrowhead)
    # return Wire(edges=edges), arrow
    return Wire(edges)


def process_line(entity):
    """Convert LINE"""
    start, end = Vector(*entity.dxf.start), Vector(*entity.dxf.end)
    if (start - end).length < TOLERANCE:
        warnings.warn("Skipping degenerate LINE")
    else:
        return Line(start, end)


def process_lwpolyline(entity):
    """Convert LWPOLYLINE"""
    # (LWPolyline.dxf.elevation is the z-axis value).
    # Can contain arcs
    return Polyline(*entity.get_points("xy"))


def process_point(entity):
    """Convert POINT"""
    point = entity.dxf.location
    return Vertex(point[0], point[1], point[2])


def process_polyline(entity):
    """Convert POLYLINE - a collection of LINE and ARC segments."""
    edges = []
    points = entity.get_points("xyb")  # Extracts x, y, and bulge (if available)

    for i in range(len(points) - 1):
        start_point = points[i][:2]
        end_point = points[i + 1][:2]
        bulge = points[i][2] if len(points[i]) > 2 else 0

        if bulge == 0:
            # Straight segment: create a Line
            edge = Line(start_point, end_point)
        else:
            # Curved segment: create a SagittaArc using the bulge as the sagitta
            sagitta = bulge * math.dist(start_point, end_point) / 2
            edge = SagittaArc(start_point, end_point, sagitta)

        edges.append(edge)

    return Wire(edges=edges)


def process_solid_trace_3dface(entity):
    """Convert filled objects - i.e. Faces"""
    # Gather vertices as a list of (x, y, z) tuples
    vertices = []
    for i in range(4):
        # Some entities like SOLID or TRACE may define only 3 vertices, repeating the last one
        # if the fourth vertex is not defined.
        try:
            vertex = entity.dxf.get(f"v{i}")
            vertices.append((vertex.x, vertex.y, vertex.z))
        except AttributeError:
            break

    # Create the Polygon object
    polygon_obj = Polygon(*vertices)
    return polygon_obj


def process_spline(entity):
    """Convert SPLINE"""
    # Get the control points as a list of (x, y) tuples
    control_points = [(point[0], point[1]) for point in entity.control_points]

    # Retrieve start and end tangents if available
    start_tangent = entity.dxf.get("start_tangent")  # May return None if not defined
    end_tangent = entity.dxf.get("end_tangent")  # May return None if not defined

    if any(t is None for t in [start_tangent, end_tangent]):
        tangents = ()
    else:
        tangents = (start_tangent, end_tangent)

    # Create the Spline object
    spline_obj = Spline(*control_points, tangents=tangents)
    return spline_obj


def process_text(entity):
    """Convert TEXT"""
    # Convert alignments
    v_alignment = {0: None, 1: Align.MIN, 2: Align.CENTER, 3: Align.MAX}
    h_alignment = {0: Align.MIN, 1: None, 4: Align.CENTER, 2: Align.MAX}

    # Extract common attributes for both TEXT and MTEXT
    position = entity.dxf.insert  # Starting position
    content = (
        entity.dxf.text if entity.dxftype() == "TEXT" else entity.text
    )  # Text content
    height = (
        entity.dxf.height if entity.dxftype() == "TEXT" else entity.dxf.char_height
    )  # Text height
    rotation = (
        entity.dxf.rotation
        if entity.dxftype() == "TEXT"
        else entity.dxf.get("rotation", 0)
    )  # Rotation angle

    # Create the Text object
    text_obj = Pos(*position) * Text(
        content,
        font_size=height,
        rotation=rotation,
        align=(h_alignment[entity.dxf.halign], v_alignment[entity.dxf.valign]),
    )
    return text_obj


# Dispatch dictionary mapping entity types to processing functions
entity_dispatch = {
    "3DFACE": process_solid_trace_3dface,
    "ARC": process_arc,
    "CIRCLE": process_circle,
    "ELLIPSE": process_ellipse,
    # "INSERT": process_insert,
    # "LEADER": process_leader,
    "LINE": process_line,
    "LWPOLYLINE": process_lwpolyline,
    # "MTEXT": process_text,
    "POINT": process_point,
    "POLYLINE": process_polyline,
    "SOLID": process_solid_trace_3dface,
    "SPLINE": process_spline,
    "TEXT": process_text,
    "TRACE": process_solid_trace_3dface,
}


def import_dxf(filename: str):
    """Import shapes from a DXF file

    Args:
        filename (str): dxf file

    Raises:
        DXFStructureError: file not found

    Returns:
        ShapeList: build123d objects
    """
    try:
        doc = ezdxf.readfile(filename)
    except ezdxf.DXFStructureError:
        raise ValueError(f"Failed to read {filename}")
    build123d_objects = []

    # Iterate over all entities in the model space
    for entity in doc.modelspace():
        dxftype = entity.dxftype()
        print(f"{dxftype=}")
        if dxftype in entity_dispatch:
            new_object = entity_dispatch[dxftype](entity)
            print(f"{new_object=}")
            if isinstance(new_object, list):
                build123d_objects.extend(new_object)
            else:
                build123d_objects.append(new_object)
        else:
            warnings.warn(f"Unable to convert {dxftype}")

    return ShapeList(build123d_objects)
@gumyr gumyr added the enhancement New feature or request label Dec 15, 2024
@gumyr gumyr added this to the Not Gating Release 1.0.0 milestone Dec 15, 2024
@gumyr gumyr self-assigned this Dec 15, 2024
@petejohanson
Copy link

Looking forward to this. Just added this locally to a new project I'm working on, and this properly imported the DXF files exported by ergogen just fine for some case generation work.

@gumyr
Copy link
Owner Author

gumyr commented Dec 23, 2024

Thanks for trying it out - good to know it works at least somewhat. IIRC DXF polyline may have a "bump" which needs to be added along with tests etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants