# Flanged pipe joint: two weld-neck flanges turned from one profile and bolted together through a
# gasket — bolt holes drilled in a circle, hex bolts and nuts with chamfered corners, weld seams —
# on two stands.
from math import *
units("mm")

PIPE_R, BORE_R = 57.15, 51.1        # 114.3 mm pipe, 6 mm wall (DN100)
DISC_R, RF_R = 110, 79              # the flange 220 mm across, its raised face 158 mm
PCD, HOLE_R = 90, 9                 # 8 holes of 18 mm on a 180 mm circle
GAP = 1.5                           # half the gasket's thickness
H = 160                             # the pipe's centre line above the floor
SPOOL = 260                         # the pipe beyond each flange
steel, pipe_blue, dark, zinc, yellow = "steel", "#5d7f9e", "#4b4e54", "#9a9da3", "#d9a21b"

def arc(cx, cz, r, a0, a1, n=5):
    return [(cx + r * cos(radians(a0 + (a1 - a0) * i / n)), cz + r * sin(radians(a0 + (a1 - a0) * i / n))) for i in range(n + 1)]

def on_circle(r, k):
    a = radians(22.5 + 45 * k)
    return r * cos(a), H + r * sin(a)

def flange(s, name):
    # half its cross-section as (depth behind the face, radius); s = 1: on the +x side
    outline = [(0, BORE_R), (0, RF_R), (2, RF_R), (2, DISC_R - 2), (4, DISC_R), (20, DISC_R), (22, DISC_R - 2)]
    outline += arc(28, 74, 6, 180, 90)[1:-1]                # a fillet where the disc meets the hub
    outline += [(28, 68), (62, PIPE_R), (62, BORE_R)]
    f = polygon([(s * (GAP + d), H + r) for d, r in outline], plane="xz").revolve(axis="x", about=(0, 0, H), name=name, color=steel, sides=96)
    holes = []
    for k in range(8):
        y, z = on_circle(PCD, k)
        holes.append(cylinder(HOLE_R, 30, at=(s * (GAP - 1), y, z), axis=(s, 0, 0), sides=32))
    f = subtract(f, *holes)
    f.chamfer(1, edges=f.face((-s, 0, 0)))
    return f

def spool(s, n):
    f = flange(s, "flange " + n)
    x0 = s * (GAP + 62)
    p = cylinder(PIPE_R, SPOOL, at=(x0, 0, H), axis=(s, 0, 0), name="pipe " + n, color=pipe_blue, sides=96)
    p = p - cylinder(BORE_R, SPOOL, at=(x0, 0, H), axis=(s, 0, 0), sides=96)
    p.paint(pipe_blue)
    # the weld seam: a bead round the pipe where it meets the hub (half a round section, turned)
    bead = [(x0 + s * 3.4 * cos(radians(a)), H + PIPE_R + 3.4 * sin(radians(a))) for a in range(-90, 91, 30)]
    w = polygon(bead, plane="xz").revolve(axis="x", about=(0, 0, H), name="weld seam", color="#8a8f97", sides=96)
    return group([f, p, w], name="spool " + n)

spool(-1, "A")
spool(1, "B")

# the gasket between the raised faces
gasket = cylinder(RF_R - 1, 2 * GAP, at=(-GAP, 0, H), axis="x", name="gasket", color="#2f3033", sides=96)
gasket = gasket - cylinder(BORE_R + 2, 2 * GAP, at=(-GAP, 0, H), axis="x", sides=96)
gasket.paint("#2f3033")
group([gasket], name="gasket")

# eight M16 bolts: a hex head and a washer on one side, a washer and a nut on the other
HEX_R = 24 / sqrt(3)                # 24 mm across the flats
back = GAP + 22                     # the back of each flange's disc
bolts = []
for k in range(8):
    y, z = on_circle(PCD, k)
    head = ngon(y, z, HEX_R, sides=6, rotation=30, plane="yz", offset=-back - 13).pull(10, name="head", color=dark)
    head.chamfer(1.2, edges="left")
    shank = cylinder(8, 2 * back + 26, at=(-back - 3, y, z), axis="x", name="bolt", color=dark, sides=24)
    shank.chamfer(1, edges="right")
    w1 = cylinder(15, 3, at=(-back - 3, y, z), axis="x", name="washer", color=zinc, sides=32) - cylinder(8.5, 3, at=(-back - 3, y, z), axis="x", sides=24)
    w2 = cylinder(15, 3, at=(back, y, z), axis="x", name="washer", color=zinc, sides=32) - cylinder(8.5, 3, at=(back, y, z), axis="x", sides=24)
    nut = ngon(y, z, HEX_R, sides=6, rotation=30, plane="yz", offset=back + 3).pull(13, name="nut", color=dark)
    nut.chamfer(1.2, edges="right")
    nut = nut - cylinder(8, 13, at=(back + 3, y, z), axis="x", sides=24)
    for part, c in [(w1, zinc), (w2, zinc), (nut, dark)]:
        part.paint(c)
    bolts.append(group([head, shank, w1, w2, nut], name=f"bolt {k + 1}"))
group(bolts, name="bolts")

# two stands, a cradle under each pipe
for x, n in [(-235, 1), (235, 2)]:
    base = box(170, 130, 12, at=(x - 85, -65, 0), color=yellow)
    base.fillet(4, edges="vertical")
    web = box(14, 110, H - PIPE_R - 12 + 10, at=(x - 7, -55, 12), color=yellow)
    cradle = box(44, 150, 34, at=(x - 22, -75, H - PIPE_R - 14), color=yellow)
    cradle = cradle - cylinder(PIPE_R + 0.5, 44, at=(x - 22, 0, H), axis="x", sides=96)
    cradle.fillet(3, edges="x")
    stand = base + web + cradle
    stand.paint(yellow)
    stand.name = "stand"
    group([stand], name=f"stand {n}")

L = GAP + 62 + SPOOL
dim((-back - 13, -DISC_R, H - DISC_R), (-back - 13, -DISC_R, H + DISC_R), offset=(0, -40, 0))
dim((-L, -PIPE_R, H - PIPE_R), (L, -PIPE_R, H - PIPE_R), offset=(0, -170, 0))
label("DN100 PN16 weld-neck flanges, 8 × M16", at=(-170, 0, H + 185), to=(-12, 0, H + DISC_R))
label("gasket 3 mm", at=(120, -120, H + 165), to=(0, -60, H + RF_R - 6))
