Geometry checks

A face can only be made of an outline that closes, does not cross itself and has no edge twice. When Face() fails on a drawing, Diagnose shows why. It lists every fault of the drawn geometry with the ids of the elements and the place where it is, and show_diagnostics=True marks the faults in the plot. Both work on constraint sketches and quick sketches alike.

[1]:
import math

import planex as px

Five kinds of faults

Diagnose returns a dict with five lists, one per kind of fault:

  • open_ends: an end of a line, arc or open spline that no other end meets;

  • missing_coincident: two ends that nearly meet, closer than gap, but are not one point;

  • crossings: two elements that meet where not both of them end, such as a line crossing an outline, a line ending on another one, or a circle touching a line;

  • overlaps: two elements that share a stretch;

  • degenerate: a line too short to have a direction, or an arc or circle of almost no radius.

Each entry is a dict with the ids of the elements and the place at. Construction elements, points and the axes are not checked, since they never become part of a face.

A traced plate

The example is a plate of 60 × 40 with a bore of diameter 16, traced from a drawing with four mistakes that are easy to make. The left side ends 0.02 above the start of the bottom, the center line is drawn as an ordinary line, the right side is drawn a second time, and a stray line sticks out of the top:

[3]:
sk = px.Sketch()
sk.Line(60, id="bottom").Line((60, 40), id="right").Line((0, 40), id="top").Line((0, 0.02), id="left")
sk.Move((30, -5)).Line((30, 45), id="center_line")
sk.Move((60, 10)).Line((60, 30), id="right_again")
sk.Move((45, 40)).Line((45, 52), id="stray")
sk.Circle((30, 20), 8, id="bore")
try:
    sk.Face()
except ValueError as err:
    print(err)
Face(): the edges bound 2 regions; Shape() keeps them all
PlanexWarning: open wire with 5 open ends, inside and outside are not defined; consider px.ClosedOpenWires(wires)

Face() fails, and its message alone does not say which edges are at fault. Diagnose does:

[4]:
for kind, found in sk.Diagnose().items():
    for fault in found:
        print(kind, fault["ids"], "at", fault["at"])
open_ends ('bottom',) at (0.0, 0.0)
open_ends ('left',) at (0.0, 0.02)
open_ends ('center_line',) at (30.0, -5.0)
open_ends ('center_line',) at (30.0, 45.0)
open_ends ('right_again',) at (60.0, 10.0)
open_ends ('right_again',) at (60.0, 30.0)
open_ends ('stray',) at (45.0, 40.0)
open_ends ('stray',) at (45.0, 52.0)
missing_coincident ('bottom', 'left') at (0.0, 0.01)
crossings ('bottom', 'center_line') at (30.0, 0.0)
crossings ('top', 'center_line') at (30.0, 40.0)
crossings ('top', 'stray') at (45.0, 40.0)
crossings ('center_line', 'bore') at (30.0, 12.0)
crossings ('center_line', 'bore') at (30.0, 28.0)
overlaps ('right', 'right_again') at (60.0, 20.0)

The center line crosses the bottom, the top and the bore, the stray line ends on the top, and the doubled side overlaps the right side. The ends of these three lines are open ends, and so are the two ends at the gap, which also form a missing coincident. In the plot, each kind has its own color (Figure 1a).

Fixing the faults

Each fault has its own fix. The gap is closed by a coincident constraint, the center line becomes a construction line, and the doubled side and the stray line are removed. After that, Diagnose finds nothing, and the face has the area of the plate less the bore (Figure 1b):

[5]:
traced = sk.Copy()
sk.Constrain("left.end", "bottom.start", px.Coincident)
sk("center_line").construction = True
sk.Remove("right_again")
sk.Remove("stray")
print(sk.Diagnose())
print("area", sk.Face().mass, "exact", 60 * 40 - math.pi * 8**2)
{'open_ends': [], 'missing_coincident': [], 'crossings': [], 'overlaps': [], 'degenerate': []}
area 2198.938070170253 exact 2198.938070170253
The traced plate, its faults marked
(a) The traced plate, its faults marked
Fixed
(b) Fixed
Figure 1: A traced plate before and after its faults are fixed

How close counts as nearly

Two ends count as a missing coincident when they are closer than gap, and as two open ends when they are farther apart. By default, gap=px.Auto is a thousandth of the larger side of the drawing’s bounding box, here 0.06, so the gap of 0.02 is found. With a smaller gap, the two ends are open ends instead:

[7]:
for gap in (px.Auto, 0.01):
    found = traced.Diagnose(gap=gap)
    print(gap, "->", len(found["missing_coincident"]), "missing coincident,", len(found["open_ends"]), "open ends")
px.Auto -> 1 missing coincident, 8 open ends
0.01 -> 0 missing coincident, 8 open ends

Ends closer than the rounding are always one point. For a drawing read from a file, where every joint misses by about the same small amount, AutoConstrain closes them all at once. See From a drawing to a parametric sketch for details.