--- jupyter: description: 'Cleaning up an imported drawing in Python with planex2d: find open ends, close small gaps, fit arcs back to tessellated outlines, check the deviation and write a clean DXF.' --- # Cleaning up imported drawings Drawings exported by plotting, laser and graphics software often carry their curves as many short lines, their straight sides in pieces and their ends a hair apart. planex2d reads such a drawing, closes the gaps, fits arcs back to the tessellated curves and checks how far the result moved, so that the part is ready for Boolean operations, meshing or a CAM program. The same cleanup works on a face from a DXF drawing, a planar STEP or BREP file, a sketch from a .CATPart file, a circuit board, a Shapely polygon or a list of measured points. The first cell is the complete workflow, ready to copy and run, and the section Step by step runs it in parts, with the plots one looks at along the way. The example is a pump-cover gasket of 100 × 70 mm with corners of radius 10, an opening of diameter 40 and four bolt holes of diameter 6.6, in the file [gasket.svg](gasket.svg). Every curve in it is a polyline, the holes 72-gons and the corners 12 segments each, its straight sides come in four pieces, its coordinates are rounded to 0.01 mm, and its outline stops 0.005 mm short of its start. ```python nbsphinx="hidden" from docs_figures import Row, Vertices ``` ```python import planex as px raw = px.Face(px.FromSVG("gasket.svg", tol=1e-5)) gasket = raw.Simplified(fit_arcs=dict(tol=2e-5)) print(len(raw.edges), "edges read |", len(gasket.edges), "edges after the cleanup") print("radii", sorted({round(e.edge.radius, 7) for e in gasket.edges if hasattr(e.edge, "radius")})) print("largest deviation", gasket.Hausdorff(raw)) px.WriteDXF(gasket.wires, "gasket-clean.dxf", unit=px.mm) gasket ``` ## Step by step ### Finding what is wrong Read without a tolerance, the drawing gives six wires, and the outline is the one that does not close. `OpenEnds` lists its loose ends by edge and end, here the start of its first edge and the end of its last (see [Small gaps](../core/defeaturing/gaps.md)). `tol` joins ends that lie closer together than it, in meters, since the reader takes the unit from the file's width and height, millimeters here, and returns meters (see [SVG files](../core/files/svg.md)). The face then holds 412 edges, all of them lines, with a vertex at every point of the curves' polylines (Figure 1). The reader has already joined the four pieces of each straight side, since they lie exactly on one line. ```python import planex as px print("closed", [w.IsClosed() for w in px.FromSVG("gasket.svg")]) print("open ends", [w.OpenEnds() for w in px.FromSVG("gasket.svg") if not w.IsClosed()]) raw = px.Face(px.FromSVG("gasket.svg", tol=1e-5)) print(len(raw.edges), "edges, all lines") # px.Plot(raw) ``` ```python nbsphinx="hide-input" Row((raw, "The gasket as read: a vertex at every point", {"marks": Vertices(raw), "names": False}), caption="The gasket read from its SVG file") ``` ### Cleaning up `Simplified` returns a cleaned copy and leaves the face as read. `fit_arcs` fits arcs to runs of short lines that follow a circle, here within 0.02 mm of the drawing's points, twice the rounding of its coordinates (see [Fitting arcs](../core/defeaturing/arcs.md)). The gasket then has 18 edges, which are four straight sides, four corner arcs of radius 10 and five holes of two arcs each. The corners come out 0.0008 mm larger than drawn, because the drawing's points are rounded to 0.01 mm (Figure 2). ```python gasket = raw.Simplified(fit_arcs=dict(tol=2e-5)) print(len(gasket.edges), "edges after the cleanup") print("radii", sorted({round(e.edge.radius, 7) for e in gasket.edges if hasattr(e.edge, "radius")})) # px.Plot(gasket) ``` ```python nbsphinx="hide-input" Row((gasket, "After the cleanup: lines and arcs", {"marks": Vertices(gasket), "names": False}), caption="The gasket cleaned up") ``` Other drawings need the other steps of `Simplified` as well. `merge_collinear_closer` joins straight pieces that lie nearly but not exactly on one line (see [Collinear lines](../core/defeaturing/collinear.md)), and `remove_edges_smaller` removes edges far shorter than the part (see [Small edges](../core/defeaturing/small-edges.md)). See [Defeaturing and simplification](../core/defeaturing/index.md) for every step, its defaults and `Simplify`, which cleans the face in place. ### Checking the result The Hausdorff distance between the cleaned face and the face as read is the largest distance from a point of one outline to the other (see [Part analysis](../core/measuring/analysis.md)). It is 0.023 mm, the height of the polylines' chords over the arcs they cut, so the cleanup moved nothing that the drawing actually holds: ```python print("largest deviation", gasket.Hausdorff(raw)) ``` ### Writing the clean drawing `px.WriteDXF` of the face's wires writes the outlines alone, without a fill, for a CAM program or a laser cutter. `unit=px.mm` writes millimeters, converted from the meters of the geometry, and the drawing read back is 100 × 70 mm again, 0.1 × 0.07 in meters. See [Files](../core/files.md) for the other writers. ```python px.WriteDXF(gasket.wires, "gasket-clean.dxf", unit=px.mm) print(px.Compound(px.FromDXF("gasket-clean.dxf")).bounding_box) ``` The clean drawing is [gasket-clean.dxf](gasket-clean.dxf). ## Other formats Every reader returns wires that the same cleanup takes: | Format | Reader | See | |---|---|---| | SVG | `px.FromSVG(path, unit=..., tol=...)` | [SVG files](../core/files/svg.md) | | DXF | `px.FromDXF(path, layers=..., unit=..., tol=...)` | [Files](../core/files.md) | | STEP, BREP (planar) | `px.FromStep(path)`, `px.FromBrep(path)` | [STEP and BREP](../core/files/step-brep.md) | | KiCad, Altium, Eagle and other boards | `px.FromKiCad(path)` | [KiCad, Altium, Eagle and more](../integrations/pcb/index.md) | | Shapely | `px.FromShapely(geometry)` | [Shapely](../integrations/shapely.md) | | .CATPart sketches | `px.FromCATPart(path)` | [CATIA V5](../integrations/catia.md) | | Measured or traced points | `px.FromPolyline(points, closed=True)` | [Fitting arcs](../core/defeaturing/arcs.md) | ## Pitfalls - **The fit's tolerance.** It must be at least as large as the rounding of the drawing's coordinates, or no arc fits. A much larger one fits arcs to runs of short lines that the part does not have as arcs. Each fit is checked against the edges it replaces and rejected if it strays further. - **Real small features.** `tol` and `remove_edges_smaller` must stay below the smallest real feature of the part, such as a narrow slot or a short chamfer. - **Units.** An SVG file whose width names no unit is read as CSS pixels of 1/96 inch, with a warning, and `unit=` sets the unit instead. All tolerances are in meters, as the geometry is. - **Free-form curves.** A curve that is no circle, such as an airfoil or a cam, takes `fit_splines` instead of `fit_arcs`. See [Fitting splines](../core/defeaturing/splines.md). ## In the browser The [contour checker](https://planex2d.org/contour-checker/) finds open contours, gaps, doubled lines and small edges in a DXF, SVG or STEP file in the browser and repairs them, with the same calls. The [converters](https://planex2d.org/converters/) turn DXF, SVG and STEP files into each other.