mozman / ezdxf

Python interface to DXF
https://ezdxf.mozman.at
MIT License
901 stars 189 forks source link

Polyline Transform Bug + Viewer Inconsistency #989

Closed cm107 closed 8 months ago

cm107 commented 8 months ago

Describe the bug Suppose that we have a transformation matrix like this:

xReflectMat = Matrix44(
    [-1, 0, 0, 0],
    [0, 1, 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1]
)

and we want to apply it to several polylines that were loaded from a dxf file that was exported from an external application (e.g. CATIA). Suppose that some polylines are

polyline.is_2d_polyline == False

and other polylines are

polyline.is_2d_polyline == True

(It just ended up like this for some reason. Don't ask me why.)

If we use ezdxf to apply the transformation to the polylines:

polyline.transform(xReflectMat)

Save the result to a new dxf file, and open it using:

ezdxf view result.dxf

we will see the transformed result that we were expecting. However, if we open the dxf in a different viewer, such as LibreCAD:

librecad result.dxf

we will see that the polylines are not transformed as expected. I have been told that the same problem also occurs if you try to open the same result.dxf with a commercial CAD editor as well. Inspecting the contents of polyline.points() shows that the vertices of the polyline haven't been transformed correctly either.

Expected behavior Even though the polyline vertices aren't actually being transformed correctly, the result drawn in the ezdxf viewer misleads you into thinking that they are. We need to apply a fix that ensures that the polyline vertices are transformed correctly, but we also need to make sure that the ezdxf viewer doesn't show misleading results as well.

To Reproduce I cannot provide my dxf file that was exported from CATIA which yielded both 2D and 3D polylines, but I can provide a sample script that reproduces the same problem.

Show Sample Script ```python import math from typing import Callable import ezdxf from ezdxf.math import Vec3, Matrix44 from ezdxf.entities.dxfgfx import DXFGraphic usePolyline2D = True RED = 0xFF002B GREEN = 0x00FF00 doc = ezdxf.new() msp = doc.modelspace() # Create Original Entities doc.layers.new("original") polylinePoints = [ (2, 5), (3, 2), (4, 5) ] add_polyline = msp.add_polyline2d \ if usePolyline2D else msp.add_polyline3d add_polyline( points=[ Vec3(*p) for p in polylinePoints ], dxfattribs={ "layer": "original", "flags": 0, "true_color": RED } ) boundingLinePoints = [ (1, 1), (5, 1), (5, 6), (1, 6) ] for i in range(len(boundingLinePoints)): j = i + 1 if i + 1 < len(boundingLinePoints) else 0 p0 = boundingLinePoints[i] p1 = boundingLinePoints[j] msp.add_line( start=Vec3(*p0), end=Vec3(*p1), dxfattribs={ "layer": "original", "true_color": RED } ) # Create Transformed Entities doc.layers.new("transformed") rotateMat: Callable[[Vec3, float], Matrix44] = lambda point, angle: Matrix44.chain( Matrix44.translate(-point.x, -point.y, -point.z), Matrix44.z_rotate(math.radians(angle)), Matrix44.translate(point.x, point.y, point.z), ) xReflectMat = Matrix44( [-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ) m = Matrix44.chain( rotateMat(Vec3(3, 3.5, 0), 180), xReflectMat, ) for _entity in msp.query(f'*[layer=="original"]'): from ezdxf.entities.polyline import Polyline if type(_entity) is Polyline: print(f"{_entity.is_2d_polyline=}") entity: DXFGraphic = _entity.copy() entity.dxf.layer = "transformed" entity.dxf.true_color = GREEN entity.transform(m) if type(entity) is Polyline and any([ point.x > 0 for point in entity.points() ]): print("Encountered +x point in polyline. Something is wrong!") msp.add_entity(entity) # Save Result doc.saveas("polyline-debug.dxf") ```

Notice that the polylines are not transformed correctly when using msp.add_polyline2d (polyline.is_2d_polyline == True), but they are transformed correctly when using msp.add_polyline3d (polyline.is_2d_polyline == False). That indicates that this part of the code seems to be causing the problem. It looks like there is some special processing that happens when polyline.is_2d_polyline == True, but that doesn't seem to be working as expected.

python version: 3.10.11 ezdxf version: 1.1.1b0 OS: Ubuntu 20.04.6 LTS

Screenshots

add_polyline2d: ezdxf Viewer vs LibreCAD Viewer ![image](https://github.com/mozman/ezdxf/assets/48467759/c11a76e5-9cd7-421a-81fb-421962d0c00c) ![image](https://github.com/mozman/ezdxf/assets/48467759/8e21d9d2-7044-4d69-ae27-810d01f52073)
add_polyline3d: ezdxf Viewer vs LibreCAD Viewer ![image](https://github.com/mozman/ezdxf/assets/48467759/50fc2581-ab93-4dab-8528-4baea82e9d5b) ![image](https://github.com/mozman/ezdxf/assets/48467759/9ef7f487-bd1d-4763-9aea-2036454c3c8b)
mozman commented 8 months ago

I stopped reading at LibreCAD. LibreCAD is not a reliable DXF viewer and I don't waste my time with bug reports based on observations in LibreCAD.

Come back if you can reproduce the issue with DWG Trueview or another reliable DXF viewer, sadly the are not open source one.

cm107 commented 8 months ago

As a workaround, you could force ezdxf to treat your 2D polylines as 3D polylines like this:

if type(entity) is Polyline:
    entity.dxf.flags = Polyline.POLYLINE_3D

or

if entity.dxf.dxftype == "POLYLINE":
    entity.dxf.flags = 8

This works because of the way that polyline.is_2d_polyline is defined. https://github.com/mozman/ezdxf/blob/41d16dda6d09f68edda4ed43616b70d1d5919ec9/src/ezdxf/entities/polyline.py#L241-L244

Just make sure that you do this before you call entity.transform.

mozman commented 8 months ago

Also: post code or a DXF file to reproduce the bug.

cm107 commented 8 months ago

@mozman

Come back if you can reproduce the issue with DWG Trueview or another reliable DXF viewer, sadly the are not open source one.

Unfortunately, it seems that DWG Trueview is not supported on Linux. Unless you have an alternative free solution for reliably viewing the DXF file, all I can use right now is LibreCAD. I have been told that the same problem was observed using a commercial CAD viewer, though.

I'll share the file so that you can check if it is happening in DWG Trueview. polyline-debug.zip

Also: post code or a DXF file to reproduce the bug.

I have already provided the code that produces the problematic DXF file. Refer to the sample script in the original post.

mozman commented 8 months ago

BricsCAD displays the same as ezdxf:

image

ezdxf: image

Conclusion: LibreCAD is not a reliable DXF viewer!

mozman commented 8 months ago

@cm107 you formatted the post in a nice way - but the collapsed sections are easy to overlook

cm107 commented 8 months ago

@mozman Thank you for checking. That answers one of my questions I guess.

I do have one more question though: why are the polyline.points() incorrect after being transformed? Based on how I set up the transform, all of the points should have been moved to x < 0, but the print statement seems to indicate that the points are in the x > 0 region where the original polyline was located.

# (omitted)

for _entity in msp.query(f'*[layer=="original"]'):
    from ezdxf.entities.polyline import Polyline
    if type(_entity) is Polyline:
        print(f"{_entity.is_2d_polyline=}")
    entity: DXFGraphic = _entity.copy()
    entity.dxf.layer = "transformed"
    entity.dxf.true_color = GREEN
    entity.transform(m)
    if type(entity) is Polyline and any([
        point.x > 0
        for point in entity.points()
    ]):
        print("Encountered +x point in polyline. Something is wrong!")
    msp.add_entity(entity)

# (omitted)
python polyline_test.py
_entity.is_2d_polyline=True
Encountered +x point in polyline. Something is wrong!
mozman commented 8 months ago

2D entities are located in the OCS. When mirroring entities, the extrusion vector, which establishes the OCS, can be inverted (0, 0, -1) and that confuses most developers.

You can use the upright module to transform (some) entities so that the extrusion vector is restored to (0, 0, 1), which aligns the OCS with the WCS. The upright module does not work for text based entities like TEXT and ATTRIB.

You can use the OCS Class of the entity to transform the coordinates by yourself from OCS to WCS:

ocs = polyline.ocs()
for p in polyline.points():
    print(ocs.to_wcs(p))
cm107 commented 8 months ago

@mozman Thank you for the explanation.

I tried converting the points from OCS coordinates to WCS coordinates as you suggested, and everything checked out.

# (omitted)

for _entity in msp.query(f'*[layer=="original"]'):
    from ezdxf.entities.polyline import Polyline
    if type(_entity) is Polyline:
        print(f"{_entity.is_2d_polyline=}")
    entity: DXFGraphic = _entity.copy()
    entity.dxf.layer = "transformed"
    entity.dxf.true_color = GREEN
    entity.transform(m)
    if type(entity) is Polyline:
        if not entity.is_2d_polyline:
            if any([
                point.x > 0
                for point in entity.points()
            ]):
                print("Encountered +x point in polyline. Something is wrong!")
        else:
            ocs = entity.ocs()
            if any([
                ocs.to_wcs(point).x > 0
                for point in entity.points()
            ]):
                print("Encountered +x point in polyline. Something is wrong!")

    msp.add_entity(entity)

# (omitted)
python polyline_test.py
_entity.is_2d_polyline=True

I'll close the issue since that answers all of my questions. Thanks again!