Arc AA Dimension#

Normal Angles

Right Angle

10_70

55_70

100_70

145_70

10_90

First Sector

Second Sector

Third Sector

Fourth Sector

First Sector

190_70

235_70

280_70

325_70

190_90

Fifth Sector

Sixth Sector

Seventh Sector

Eighth Sector

Fifth Sector

Arc AA Dimension Properties#

Show/Hide arc_dim_aa Attributes

PIL has a separate arc method with the following properties.

  • xy

    Four points to define the bounding box. Sequence of [(x0, y0), (x1, y1)] or [x0, y0, x1, y1].

  • start

    Starting angle, in degrees

  • end

    Ending angle, in degrees.

  • fill

    Arc colour

The arc dimension requires the following additional attributes.

  • im

    PIL image handle, link to the calling program

  • dr

    PIL drawing handle, link to the calling program

  • centre

    Arc's circle centre

  • radius

    Arc's circle radius

  • begin

    Starting angle, in degrees

  • end

    Ending angle, in degrees.

  • fill

    Arc colour, RGB tuple

  • text

    Text to be written next to the dimension

  • font

    Font of the text

  • arrowhead

    Three integer tuple describing the shape and size of the arrow

  • back

    background colour, RGB tuple


Create Arc AA Dimension#

The arcs used for dimensions are based on circles rather than ellipses, so use the arc centre, its radius and the start and end angles, rather than the box surrounding the circle containing the arc.

The arc dimension shows the angle directly, so no extenders are required, add the text straightaway. Use the PIL image reference, centre, radius, begin and end angles, followed by the text and its font and finally the arrow shape.

Once we have extracted the values from the tuples, calculate the coordinates of the start and end positions. Whether the arrows need to be inward or outward pointing depends on the arc size and is made automatically. Lastly position the text according to the text and arc sizes.

Add a conditional statement to draw a right angle square lines for 90° instead of an arc. If the angle becomes a straight line raise an error.

The utility function polar2cart from DimLinesPIL is particularly useful.

Show/Hide Code test_angle_aa_dim.py

import sys
sys.path.append('../dims')

from PIL import Image, ImageDraw, ImageFont
from collections import defaultdict
from math import radians, sqrt, sin, cos, tan
from DimLinesPIL import polar2cart, angled_text, int_up
from DimLinesAA import WideLineAA, findSect, LineAA, dimension_aa

def plot_sector_points(dr, xm, ym, x, y, sects, fill, all8=1):
    # plots all 8 sectors or only 4 sectors in the while loop
    if sects[0] < 0:
        ltemp = list(sects)
        ltemp[0] = -ltemp[0]
        sects = tuple(ltemp)
    if all8 == 1:
        if sects[0] == 1 or sects[1] == 1:
            dr.point((xm-x, ym+y), fill)               # I Octant
        elif sects[0] == 3 or sects[1] == 3:
            dr.point((xm-y, ym-x), fill)               # III. Octant
        elif sects[0] == 5 or sects[1] == 5:
            dr.point((xm+x, ym-y), fill)               # V  Octant
        elif sects[0] == 7 or sects[1] == 7:
            dr.point((xm+y, ym+x), fill)                #   VII. Octant

    if sects[0] == 4 or sects[1] == 4:
        dr.point((xm+x, ym+y), fill)               #  IV . Octant  +x +y
    elif sects[0] == 2 or sects[1] == 2:
        dr.point((xm+y, ym-x), fill)               # II Octant
    elif sects[0] == 8 or sects[1] == 8:
        dr.point((xm-x, ym-y), fill)               #  VIII. Octant  -x +y
    elif sects[0] == 6 or sects[1] == 6:
        dr.point((xm-y, ym+x), fill)               # VI Octant

def PartCircleAA(dr, xm, ym, r, start, finish, width, sects, fill=(0,0,0),
                    back=(255,255,221)):
    # xm, ym = centre
    # draw an antialiased circle on light background
    x = -r
    y = 0                                   # IV. Octant from left to bottom left

    err = 2 - 2 * r                         # initial difference

    sslope = abs((ym-start[1])/(xm-start[0]))
    fslope = abs((ym-finish[1])/(xm-finish[0]))
    # check sects
    ssect = 0
    fsect = 0
    plot = 0
    ssect, fsect = sects
    if sects[0] == sects[1]:
        # start, finish in one sector
        plot = 0
    elif (sects[0] == 0 and sects[1] in (1,3,5,7)) or \
            (sects[0] in (2,4,6,8) and sects[1] == 0):
        plot = 1

    if sects[0] < 0:
        plot = 1

    maxdi = [0]
    for n in range(0, width+1):
        maxdi.append(maxdi[n] + 2 * (r-n) -1)
    maxdi.remove(0)
    maxd = maxdi[0]
    # ensure inner aa working with conditions for single aa
    # find maxd of smallest main circle
    maxdsm = 2 * (r-width+1) - 1
    # thick factor used outer main lines
    thfact = (width-1)/2

    def errs(comp, size,j):
        return 255 if comp == 255 else int((255-comp) * j / size) + comp

    diffs = defaultdict(list)
    diffs = defaultdict(lambda:back, diffs)
    for i in range(maxd):
        if fill == (0,0,0):
            diffs[i] = tuple(int(255*i/maxd) for k in range(3))
        else:
            diffs[i] = tuple(errs(fill[k],maxd,i) for k in range(3))

    diffsm = defaultdict(list)
    diffsm = defaultdict(lambda:back, diffsm)
    for i in range(maxdsm):
        if fill == (0,0,0):
            diffsm[i] = tuple(int(255*i/maxdsm) for k in range(3))
        else:
            diffsm[i] = tuple(errs(fill[k],maxdsm,i) for k in range(3))

    while -x > y - 1:
        # actual and inverse slope
        aslope = abs((-y)/(-x))
        cslope = abs(-x/(y+0.2))

        if ssect in (1,5) and plot == 0:
            if aslope >= sslope:
                plot = 1
        elif fsect in (1,5) and plot == 1:
            if aslope >= fslope:
                plot = 0
        elif fsect in (2,6) and plot == 0: # and ssect > 0:
            if cslope <= fslope:
                plot = 1
        elif ssect in (2,6) and plot == 1:
            if cslope <= sslope:
                plot = 0

        elif ssect in (3,7) and plot == 0:
            if cslope <= sslope:
                plot = 1
        elif fsect in (3,7) and plot == 1:
            if cslope <= fslope:
                plot = 0
        elif fsect in (4,8) and plot == 0:
            if aslope >= fslope:
                plot = 1
        elif ssect in (4,8) and plot == 1:
            if aslope >= sslope:
                plot = 0

        err0 = err
        e2 = err-(2*y+1)-(2*x+1) # abs(err+2*(x+y)-2)
        ea = abs(e2)
        out = max(0,int(ea-thfact))
        if plot == 1:
            plot_sector_points(dr, xm, ym, x, y, sects, (diffs[out] if out > 0 else fill),
                    all8 = (1 if (xm+x, ym+y) != (xm-y, ym-x) \
                    or (x==-r and y == 0) else 0))

        # fill out diagonals
        x0 = -x
        eout = abs(e2 + 2*x0 + 2*y + 2)
        if eout < maxd: # and (width-1)//2 == 0
            if plot == 1:
                plot_sector_points(dr, xm, ym, x-1, y+1, sects, (diffs[eout] if eout > 0 else fill),
                    all8 = (1 if (xm+x, ym+y) != (xm-y, ym-x) else 0))

        ein = e2
        x0 = -x
        for n in range(0, width):
            ein = ein-(2*(x0-n)-1)
            e0 = -ein

            if n < width-2:
                fact = fill
            elif n == width-1:
                fact = diffs[abs(int(e0-maxd*thfact/10))] if n==0 else \
                        diffsm[e0-maxdi[n-1]]
            else:
                fact = diffsm[max(0,int(abs(e0-maxdi[n])-maxdsm*thfact/10))] # e2-maxdsm
            if plot == 1:
                plot_sector_points(dr, xm, ym, x+n+1, y, sects, fact,
                    all8 = (1 if (xm+x, ym+y) != (xm-y, ym-x) else 0))

        if (err0 <= y):
            y += 1
            err += y * 2 + 1            # e_xy+e_y < 0

        if (err0 > x or err > y):          # e_xy+e_x > 0 or no 2nd y-step
            x += 1
            # aa missed by diagonals
            eout = abs(e2 + 2*y - 1)
            if eout < maxd:
                if plot == 1:
                    plot_sector_points(dr, xm, ym, x-1, y, sects, (diffs[eout] if eout > 0 else fill),
                        all8 = (1 if (xm+x, ym+y) != (xm-y, ym-x) else 0))
            err += x * 2 + 1            # -> x-step now

def make_arc_aa(dr, centre, radius, start, finish, width=1, fill=(0,0,0), back = (255,255,221)):
    xm,ym = centre
    sq = findSect((xm, ym), (start[0], start[1]))
    fq = findSect((xm, ym), (finish[0], finish[1]))
    sects = ()

    diff_sect = fq[0] - sq[0]

    if diff_sect == 0:
        sects = sq[0],fq[0]
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

    elif (diff_sect == 1) or (sq[0] == 8 and fq[0] == 1):
        sects = sq[0],0
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)
        sects = 0,fq[0]
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

    elif (diff_sect == 2) or (sq[0] == 8 and fq[0] == 2) or (sq[0] == 7 and fq[0] == 1):
        sects = sq[0],0
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)
        if sq[0] < 8:
            sects = -sq[0]-1,-sq[0]-1
        else:
            sects = -1,-1
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)
        sects = 0,fq[0]
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

    elif (diff_sect == 3) or (sq[0] == 8 and fq[0] == 3) or (sq[0] == 7 and fq[0] == 2):
        sects = sq[0],0
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

        if sq[0] < 7:
            sects = -sq[0]-2,-sq[0]-2
        else:
            sects = -1,-1
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

        if sq[0] < 8:
            sects = -sq[0]-1,-sq[0]-1
        else:
            sects = -2,-2
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

        sects = 0,fq[0]
        PartCircleAA(dr, xm, ym, radius, start, finish, width, sects,
            fill=fill, back = back)

def arc_dim_aa(im,dr,centre,radius,begin,end,fill=(0,0,0),text=None,font=None,
                arrowhead=(8,10,3),back=(225,225,221),aall=0):
    # x,y,radius all relate to centre coords and radius, give 2 angles
    rtimes2 = max(60, radius*2)
    if isinstance(begin, int):
        beginp = polar2cart(centre, begin, rtimes2)
    if isinstance(end, int):
        endp = polar2cart(centre, end, rtimes2)
    if isinstance(begin, tuple):
        beginp = begin
        begin, rayb = cart2polar(centre, begin)
    if isinstance(end, tuple):
        endp = end
        end, raye = cart2polar(centre, end)

    beginr = radians(begin)
    endr = radians(end)

    bq = findSect(centre, beginp)
    eq = findSect(centre, endp)

    if bq[1] in (3,4) and eq[1] in (1,2):
        alpha = (360 - begin + end)/2
        diff = 360 - begin + end
    else:
        alpha = (begin + end)/2
        diff = end - begin

    if diff == 180:
        raise Exception('arc_dim_aa: angle is 180°, begin {} end {}' \
            ' difference {} cannot draw dimension'.format(begin, end, diff))

    if diff == 90:
        ax, ay = polar2cart(centre, begin, radius)
        bx, by = polar2cart(centre, end, radius)
        dx, dy = polar2cart(centre, alpha, radius*sqrt(2))

        LineAA(dr, (ax,ay), (dx,dy), fill=fill, back=back)
        LineAA(dr, (bx,by), (dx,dy), fill=fill, back=back)

    else:
        x, y = centre
        d1, d2, d3 = arrowhead

        # placement of arrows, 'first' point inwards
        order = 'last' if radians(diff) * radius > 4 * arrowhead[1] else 'first'

        # find begin and end arc
        start = int_up(x + radius * cos(beginr)), int_up(y + radius * sin(beginr))
        finish = int_up(x + radius * cos(endr)), int_up(y + radius * sin(endr))
        dimension_aa(im,dr, start, angle=(begin - 90), arrow=order,fill=fill,back=back)
        dimension_aa(im,dr, finish, angle=(end + 90), arrow=order,fill=fill,back=back)

        start_arc = polar2cart(start, (begin + 90), d1)
        finish_arc = polar2cart(finish, end - 90, d1)

        if order == 'first':
            make_arc_aa(dr, centre, radius, beginp, endp, fill=fill, back=back)
        else:
            make_arc_aa(dr, centre, radius, start_arc, finish_arc, fill=fill, back=back)

    # placement of text
    if text is None:
        text = str(diff) + '°'
    if font is None:
        font_size=15
        font = ImageFont.truetype('consola.ttf', font_size)
    # (wide, ht) = font.getsize(text)
    unused1, unused2, wide, ht = font.getbbox(text)

    # stop upside down text
    if bq[1] in (3,4) and eq[1] in (1,2):
        alpha = begin + alpha -360

    angle = alpha - 90
    da = 7

    if diff == 90:
        X, Y = polar2cart(centre, alpha, radius * sqrt(2) + da)
    else:
        t = tan(radians(diff/2))
        a = wide/ 2 / t
        size = max(radius, a)
        X, Y = polar2cart(centre, alpha, size + ht/2+da) # alpha

    angled_text(im, (X, Y), text, angle, font, aall=aall)


if __name__ == "__main__":

    Xm = 101 #w//2
    Ym = 101 # h//2
    Radius = 30

    w, h = 201, 201
    Back = (255,255,221)
    image = Image.new('RGB', (w,h), Back) #'#FFFFDD'
    drawl = ImageDraw.Draw(image)


    beg = 260
    Diff = 105
    e = beg + Diff if beg + Diff < 360 else beg + Diff - 360
    #e = 15

    left = polar2cart((Xm,Ym), beg, Radius*2)
    right = polar2cart((Xm,Ym), e, Radius*2)
    #print((beg+e)/2)
    #al = polar2cart((Xm,Ym), (beg+e)/2, Radius*2)
    #drawl.line([al,(Xm,Ym)], fill ='red')
    WideLineAA(drawl, left, [Xm,Ym], fill=(0,0,255), width=2, back=Back)
    WideLineAA(drawl, right, [Xm,Ym], fill=(0,0,255), width=2, back=Back)


    arc_dim_aa(image, drawl, (Xm, Ym), Radius, beg, e, fill=(0,0,0),
                back = Back)

    image.show()
    #image.save('../../temp/arc_dim_'+ str(beg)+'_'+str(diff)+'.png')