# Fachwerk house: a modern timber-frame house among pines (docs/fachwerk/brief.md) — a dark glulam
# frame, the south gable 15 m wide and glazed floor to roof under a 35° roof with deep overhangs on
# open purlins, a living room open to the roof with a gallery over it, a terrace round the south and
# east. North = +y, the street to the north. Units cm.
from math import *
units("cm")

def one(parts, name):
    l = list(parts)
    while len(l) > 1:
        nl = [l[i] + l[i + 1] for i in range(0, len(l) - 1, 2)]
        if len(l) % 2:
            nl.append(l[-1])
        l = nl
    l[0].name = name
    return l[0]

frame_c, board, inner, roof_c, soffit = "#2a221d", "#2f2925 planks", "#efe7da", "#25272b", "#a8754a planks"
oak, steel, dark, linen = "#c49a6c", "#9aa0a8", "#2b2d31", "#f1ede4"
SX, SY = 3600, 3000
X0, X1, Y0, Y1 = 1000, 2500, 900, 1900       # the house: 15 m across the gable, 10 m deep
FL, UF = 50, 380                             # the floor; the upper floor (its slab 350..380)
RY0, RY1 = 650, 2050                         # the roof: 2.5 m over the south terrace, 1.5 m at the north
PITCH = tan(radians(35))
XM = (X0 + X1) / 2                           # the ridge
def zr(x):
    # the roof's underside over a point across the house: 35° up from the walls' tops (420) to the ridge
    return 420 + min(x - X0, X1 - x) * PITCH
RIDGE = zr(XM)

# ---------- the land ----------
box(SX, SY, 60, at=(0, 0, -60), name="lawn", color="#7fa35f")
MX, MY = 1500, 1500
meadow = box(SX + 2 * MX, SY + 2 * MY, 58, at=(-MX, -MY, -61), name="meadow", color="#a9bf8c") - box(SX, SY, 58, at=(0, 0, -61))
meadow.paint("#a9bf8c")
box(SX + 2 * MX, 240, 12, at=(-MX, SY, -3), name="pavement", color="#cfcac0")
box(SX + 2 * MX, 12, 14, at=(-MX, SY + 240, -3), name="kerb", color="#b9b4aa")
box(SX + 2 * MX, 700, 4, at=(-MX, SY + 252, -3), name="street", color="#7b7f86")
one([box(300, 15, 1, at=(x, SY + 595, 1), color="#f4f2ec") for x in range(-MX + 100, SX + MX - 300, 600)], "road marking")

# ---------- the house: its floor, the frame, the walls between ----------
floor_ = box(X1 - X0, Y1 - Y0, FL, at=(X0, Y0, 0), name="floor", color=dark)
floor_.top.paint("#c8a273 planks")

def to_roof(xa, xb, zb, y0, d, color, name, top=None):
    # a piece in a gable (x across, z up), from zb up to the roof's underside (or less), d deep from y0
    f = top or zr
    pts = [(xa, zb), (xb, zb), (xb, f(xb))]
    if xa < XM < xb:
        pts.append((XM, f(XM)))
    pts.append((xa, f(xa)))
    return prism(pts, d, plane="xz", offset=y0, name=name, color=color)

# purlins and the ridge beam run the roof's length under it
PUR = [(1235, 1265), (1485, 1515), (1985, 2015), (2235, 2265)]    # 30 wide, 40 deep
for a, b in PUR:
    lo = min(zr(a), zr(b)) - 40
    prism([(a, lo), (b, lo), (b, zr(b)), (a, zr(a))], RY1 - RY0, plane="xz", offset=RY0, name="purlin", color=frame_c)
RB = RIDGE - 55
prism([(XM - 15, RB), (XM + 15, RB), (XM + 15, zr(XM + 15)), (XM, RIDGE), (XM - 15, zr(XM - 15))], RY1 - RY0, plane="xz", offset=RY0, name="ridge beam", color=frame_c)
# the gables' posts: the corner ones up to the roof, those under a purlin or the ridge up to it
XP = [(1000, 1020)] + PUR[:2] + [(XM - 15, XM + 15)] + PUR[2:] + [(2480, 2500)]
def gable_posts(y0):
    for a, b in XP:
        if (a, b) in PUR:
            box(b - a, 20, min(zr(a), zr(b)) - 40 - FL, at=(a, y0, FL), name="post", color=frame_c)
        elif a == XM - 15:
            box(30, 20, RB - FL, at=(a, y0, FL), name="post", color=frame_c)
        else:
            to_roof(a, b, FL, y0, 20, frame_c, "post")
gable_posts(Y0)
gable_posts(Y1 - 20)
BAYS = [(XP[i][1], XP[i + 1][0]) for i in range(len(XP) - 1)]

# the south gable: sliding glass below, glass to the roof above, the floor's beam between
for a, b in BAYS:
    window(w=b - a, h=300, panes=2, color=frame_c, at=(a, Y0, FL))
    box(b - a, 20, 30, at=(a, Y0, 350), name="floor beam", color=frame_c)
    if a == XM + 15:                              # the door onto the balcony, glass over it
        window(w=b - a, h=210, panes=2, color=frame_c, at=(a, Y0, 380))
        box(b - a, 20, 10, at=(a, Y0, 590), name="transom", color=frame_c)
        to_roof(a, b, 600, Y0 + 9, 2, "glass", "glass")
    elif zr(a) > 700 and zr(b) > 700:
        box(b - a, 20, 15, at=(a, Y0, 640), name="transom", color=frame_c)
        prism([(a, 380), (b, 380), (b, 640), (a, 640)], 2, plane="xz", offset=Y0 + 9, name="glass", color="glass")
        to_roof(a, b, 655, Y0 + 9, 2, "glass", "glass")
    else:
        to_roof(a, b, 380, Y0 + 9, 2, "glass", "glass")

# the north gable: boards, the front door, windows
north = {}
for a, b in BAYS:
    p = to_roof(a, b, FL, Y1 - 15, 10, board, "wall")
    p.front.paint(inner)
    north[a] = p
t = door(w=100, h=230, turn=180, at=(0, 0, 0))
fx, fy = t.part("frame top").min[0], t.part("frame top").min[1]
t.delete()
door(w=100, h=230, turn=180, color="#5a4a3c", frame=frame_c, at=(1325 - fx, Y1 - 15 - fy, FL), wall=north[1265])
window(w=120, h=140, color=frame_c, turn=180, at=(1070, Y1 - 13, 140), wall=north[1020])
window(w=150, h=60, panes=2, color=frame_c, turn=180, at=(1800, Y1 - 13, 180), wall=north[1765])    # over the worktop
window(w=150, h=140, color=frame_c, turn=180, at=(2290, Y1 - 13, 130), wall=north[2265])
window(w=120, h=120, color=frame_c, turn=180, at=(1565, Y1 - 13, 470), wall=north[1515])
window(w=120, h=120, color=frame_c, turn=180, at=(2065, Y1 - 13, 470), wall=north[2015])

# the long walls: a post at each third, bays of glass or boards, the floor's beam, the knee wall under the eaves
YB = [(920, 1220), (1240, 1560), (1580, 1880)]
for x0, side in ((X0, -1), (X1 - 20, 1)):
    for y in (1220, 1560):
        box(20, 20, 300, at=(x0, y, FL), name="post", color=frame_c)
    box(20, Y1 - Y0 - 40, 30, at=(x0, Y0 + 20, 350), name="floor beam", color=frame_c)
    knee = prism([(x0, 380), (x0 + 20, 380), (x0 + 20, zr(x0 + 20)), (x0, zr(x0))], Y1 - Y0 - 40, plane="xz", offset=Y0 + 20, name="knee wall", color=board)
    if side < 0: knee.right.paint(inner)
    else: knee.left.paint(inner)
def boards(x0, ya, yb, side):
    w = box(20, yb - ya, 300, at=(x0, ya, FL), name="wall", color=board)
    if side < 0: w.right.paint(inner)
    else: w.left.paint(inner)
    return w
window(w=300, h=300, panes=2, color=frame_c, turn=90, at=(X0, 920, FL))
w2 = boards(X0, 1240, 1560, -1)
w3 = boards(X0, 1580, 1880, -1)
window(w=160, h=150, panes=2, color=frame_c, turn=90, at=(X0, 1320, 110), wall=w2)
window(w=60, h=60, panes=1, color=frame_c, turn=90, at=(X0, 1700, 220), wall=w3)
for ya, yb in YB[:2]:
    window(w=yb - ya, h=300, panes=2, color=frame_c, turn=270, at=(X1 - 8, ya, FL))
e3 = boards(X1 - 20, 1580, 1880, 1)
window(w=160, h=150, panes=2, color=frame_c, turn=270, at=(X1 - 8, 1650, 120), wall=e3)

# the roof: the wood soffit under, the black roofing over, two slopes meeting at the ridge
for xa, xb in ((880, XM), (XM, 2620)):
    za, zb = zr(xa), zr(xb)
    prism([(xa, za), (xb, zb), (xb, zb + 3), (xa, za + 3)], RY1 - RY0, plane="xz", offset=RY0, name="soffit", color=soffit)
    prism([(xa, za + 3), (xb, zb + 3), (xb, zb + 42), (xa, za + 42)], RY1 - RY0, plane="xz", offset=RY0, name="roof", color=roof_c)

# ---------- inside ----------
# the upper floor over the back 4.6 m, a landing at the stair's top; the front open to the roof
slab = box(X1 - X0 - 40, Y1 - 20 - 1420, 30, at=(X0 + 20, 1420, 350), name="upper floor", color=inner)
slab.top.paint("#c8a273 planks")
ext = box(X1 - 20 - (XM + 15), 1420 - Y0 - 20, 30, at=(XM + 15, Y0 + 20, 350), name="upper floor", color=inner)    # the east part reaches the gable
ext.top.paint("#c8a273 planks")
landing = box(100, 100, 30, at=(1280, 1320, 350), name="landing", color=inner)   # (clear of the purlin over 1235..1265)
landing.top.paint("#c8a273 planks")
# the stair along the gallery's edge, climbing west to the landing (where the roof is high enough)
n, going = 19, 26
L = n * going
r = (UF - FL) / n
stairs(w=100, h=UF - FL, steps=n, going=going, color=oak, at=(1380, 1320, FL), turn=90)
prism([(1380 + L, FL + r), (1380, UF), (1380, UF + 90), (1380 + L, FL + r + 90)], 1.2, plane="xz", offset=1318.8, name="glass", color="glass")
pipe([(1380 + L, 1319.4, FL + r + 93), (1380, 1319.4, UF + 93)], 2, name="handrail", color=frame_c, sides=10)
# frameless glass along the upper floor's open edges, no higher than the roof lets it
def gtop(x):
    return min(UF + 100, zr(x) - 2)
def balustrade(xa, xb, y0, name="glass"):
    pts = [(xa, UF), (xb, UF), (xb, gtop(xb))]
    for xk in (X0 + (100 - 2 + UF - 420 + 2) / PITCH, X1 - (100 + UF - 420) / PITCH):
        if xa < xk < xb: pts.append((xk, gtop(xk)))
    pts.append((xa, gtop(xa)))
    pts = [pts[0], pts[1]] + sorted(pts[2:], key=lambda q: -q[0])
    return prism(pts, 1.2, plane="xz", offset=y0, name=name, color="glass")
balustrade(X0 + 20, 1280, 1420)
balustrade(1280, 1380, 1320)
prism([(1321.2, UF), (1420, UF), (1420, UF + 100), (1321.2, UF + 100)], 1.2, plane="yz", offset=1280, name="glass", color="glass")
balustrade(1380, XM + 15, 1420)
prism([(Y0 + 20, UF), (1420, UF), (1420, UF + 100), (Y0 + 20, UF + 100)], 1.2, plane="yz", offset=XM + 15, name="glass", color="glass")
# the ground floor's back: a bathroom (west), the hall by the front door, the kitchen and table, a bedroom (east)
p1 = box(10, Y1 - 20 - 1420, 300, at=(1300, 1420, FL), name="partition", color=inner)
p2 = box(10, Y1 - 20 - 1420, 300, at=(2200, 1420, FL), name="partition", color=inner)
p3 = box(270, 10, 300, at=(2210, 1420, FL), name="partition", color=inner)
def door_at(wl, x, y, z, turn, w, open, hinge, swing):
    t = door(w=w, h=210, turn=turn, open=open, hinge=hinge, swing=swing, at=(0, 0, 0))
    fx, fy = t.part("frame top").min[0], t.part("frame top").min[1]
    t.delete()
    return door(w=w, h=210, turn=turn, open=open, hinge=hinge, swing=swing, color=inner, frame=inner, at=(x - fx, y - fy, z), wall=wl)
door_at(p1, 1300, 1600, FL, 90, 80, 0, "left", "in")              # the bathroom, from the hall
door_at(p3, 2300, 1420, FL, 0, 90, 0, "left", "in")               # the bedroom
wardrobe(w=120, d=60, h=240, color=inner, at=(1450, 1820, FL), turn=180)   # coats, beside the front door
kitchen_units(w=360, d=60, sink="middle", hob="right", color="#e9e5dd", top="#3b3a38", at=(1700, Y1 - 20 - 62.8, FL), turn=180)
fridge(w=60, d=65, h=185, color="#e9e5dd", at=(2070, Y1 - 20 - 70, FL), turn=180)
dining_table(w=200, d=100, color=oak, at=(1780, 1560, FL))
for x in (1800, 1880, 1960):
    chair(color=oak, pad="#8a8f99", at=(x - 2, 1665, FL))
    chair(color=oak, pad="#8a8f99", at=(x - 2, 1505, FL), turn=180)
toilet(at=(1150, 1813.5, FL))
washbasin(w=60, d=46, at=(1030, 1500, FL), turn=90)
bed(w=160, l=200, color="#8f9aa6", linen=linen, at=(2266, 1600, FL), turn=270)
# the living room under the glass: a sofa looking at the garden, armchairs, a rug, a coffee table
rug(w=360, d=220, color="#c9bda8", border="#e6dfd2", at=(1570, 960, FL))
sofa(w=280, d=95, color="#5e6670", at=(1610, 1110, FL + 1.2))
coffee_table(w=120, d=60, h=40, color=oak, at=(1690, 1000, FL + 1.2))
armchair(color="#b9a68c", at=(1470, 960, FL), turn=270)
armchair(color="#b9a68c", at=(1945, 960, FL), turn=90)
floor_lamp(at=(1940, 1160, FL))
# white curtains gathered by the gable's posts, on a rail under the floor beam
cur = [box(X1 - X0 - 40, 10, 5, at=(X0 + 20, 920, 345), color=frame_c)]
for a in (1022, 1200, 1450, 1700, 1950, 2200, 2440):
    b = a + 38
    m = 6
    front = [(a + (b - a) * i / m, 922 + (3 if i % 2 else 0)) for i in range(m + 1)]
    back = [(x, y + 1.5) for x, y in reversed(front)]
    cur.append(prism(front + back, 293, plane="xy", offset=FL + 2, color=linen))
one(cur, "curtains")
# upstairs: the gallery with armchairs looking out through the gable; a bedroom to the east
up = to_roof(2000, 2010, UF, 1420, Y1 - 20 - 1420, inner, "partition")
upf = to_roof(2010, X1 - 20, UF, 1420, 10, inner, "partition")
up = up - box(30, Y1 - 1420, 200, at=(1985, 1420, zr(2015) - 40))       # (round the purlins over them)
upf = upf - box(30, 10, 200, at=(2235, 1420, zr(2265) - 40)) - box(30, 10, 200, at=(1985, 1420, zr(2015) - 40))
up.paint(inner)
upf.paint(inner)
door_at(up, 2000, 1500, UF, 90, 90, 0, "left", "in")
for x in (2025, 2125):
    armchair(color="#7d9c88", at=(x, 960, UF), turn=180)      # looking out through the gable, beside the balcony door
# the balcony: its floor on beams run out of the house, a glass railing round it
# centred on the gable, between the posts under the purlins (its door in the bay east of the ridge post)
for x in (1515, XM - 15, 1955):
    box(30, 150, 40, at=(x, 750, 310), name="balcony beam", color=frame_c)
bal = box(470, 150, 30, at=(1515, 750, 350), name="balcony", color=frame_c)
bal.top.paint("#b98d5f planks")
railing(w=470, h=100, style="glass", color=frame_c, at=(1515, 750, UF))
railing(w=144, h=100, style="glass", color=frame_c, at=(1515, 756, UF), turn=90)
railing(w=144, h=100, style="glass", color=frame_c, at=(1979, 756, UF), turn=90)
for x in (1620, 1835):
    chair(color=frame_c, pad="#7d9c88", at=(x, 790, UF))
coffee_table(w=50, d=40, h=45, shelf=False, color=frame_c, at=(XM - 25, 795, UF))
bed(w=160, l=200, color="#a8b4a0", linen=linen, at=(2040, 1660, UF))

# ---------- the terrace ----------
deck(w=900, d=250, h=FL, color="#b98d5f", at=(850, 650, 0))
deck(w=900, d=250, h=FL, color="#b98d5f", at=(1750, 650, 0))
deck(w=700, d=250, h=FL, color="#b98d5f", at=(X1, Y0, 0), turn=90)
box(200, 30, 2 * FL / 3, at=(1650, 620, 0), name="step", color="#b98d5f")
box(200, 30, FL / 3, at=(1650, 590, 0), name="step", color="#b98d5f")

def egg_chair(cx, cy):
    # a hanging egg chair on its stand: a round base, a pole behind the basket arching over it,
    # a chain and three cords down to the basket's rim, the basket woven round a cushion
    parts = [cylinder(42, 4, at=(cx, cy + 20, FL), sides=24),
             pipe([(cx, cy + 56, FL + 4), (cx, cy + 58, FL + 150), (cx, cy + 40, FL + 205), (cx, cy, FL + 212)], 2.5, smooth=True, sides=10),
             cylinder(1, 45, at=(cx, cy, FL + 168), sides=8)]
    for a in (90, 210, 330):
        parts.append(pipe([(cx, cy, FL + 170), (cx + 31 * cos(radians(a)), cy + 31 * sin(radians(a)), FL + 141)], 0.6, sides=6))
    for p in parts: p.paint(dark)
    shell = sphere(45, at=(cx, cy, FL + 110), sides=24) - box(100, 60, 70, at=(cx - 50, cy - 70, FL + 100))
    shell = shell - box(100, 100, 30, at=(cx - 50, cy - 50, FL + 140))
    shell = shell - sphere(41, at=(cx, cy, FL + 110), sides=24)
    shell.paint("#6b5444")
    cushion = cylinder(32, 10, at=(cx, cy, FL + 82), sides=24)
    cushion.paint(linen)
    return one(parts + [shell, cushion], "egg chair")
egg_chair(1110, 760)
egg_chair(1250, 760)
# the lounge on the east deck
sofa(w=200, d=85, color="#3b4148", at=(X1 + 15, 1100, FL), turn=90)
coffee_table(w=100, d=55, h=38, color=oak, at=(X1 + 125, 1150, FL), turn=90)
armchair(color="#3b4148", at=(X1 + 120, 990, FL), turn=180)
armchair(color="#3b4148", at=(X1 + 120, 1330, FL))

def plant(cx, cy, z, h, r):
    pot = cone(r * 0.8, r, r * 1.6, at=(cx, cy, z), sides=20)
    pot.paint("#d8d2c8")
    parts = [pot, cylinder(r * 0.9, 1, at=(cx, cy, z + r * 1.6), color="#4a3828", sides=20),
             cylinder(1.8, h * 0.5, at=(cx, cy, z + r * 1.6 + 1), color="#6b4f3a", sides=8)]
    top = z + r * 1.6 + h * 0.5
    for dx, dy, dz, rr in ((0, 0, 0, r * 1.2), (r * 0.6, 0, -r * 0.5, r * 0.85), (-r * 0.5, r * 0.4, -r * 0.3, r * 0.9)):
        parts.append(sphere(rr, at=(cx + dx, cy + dy, top + dz), color="#4f7a45", sides=12))
    return one(parts, "plant")
plant(900, 845, FL, 120, 25)
plant(2600, 845, FL, 120, 25)
plant(2150, 960, FL, 110, 22)

# ---------- outside: the way in, the parking, the garden ----------
def car(x, y, color, z=4):
    # a hatchback, its nose to -y: a lower body with the bonnet, a glasshouse with raked screens
    body = box(180, 440, 62, at=(x, y, z + 28), name="car body", color=color)
    body.fillet(16, edges="vertical")
    body.fillet(9, edges="top")
    for wy in (y + 85, y + 355):                 # the wheel arches
        body = body - cylinder(37, 184, at=(x - 2, wy, z + 33), axis="x", sides=24)
    body.paint(color)
    body.name = "car body"
    # the glasshouse in side view (y, z): the windscreen raked back from the bonnet, the roof, the tailgate
    zb = z + 90
    glass_ = prism([(y + 140, zb), (y + 430, zb), (y + 420, zb + 46), (y + 214, zb + 50)], 166, plane="yz", offset=x + 7, name="car windows", color="#2a3440")
    shell = prism([(y + 214, zb + 50), (y + 420, zb + 46), (y + 418, zb + 54), (y + 216, zb + 58)], 170, plane="yz", offset=x + 5, name="car roof", color=color)
    parts = [body, shell, glass_]
    for wy in (y + 85, y + 355):
        for wx in (x - 2, x + 158):
            parts.append(cylinder(33, 24, at=(wx, wy, z + 33), axis="x", name="wheel", color="#202124", sides=24))
    for lx in (x + 18, x + 132):                 # headlamps
        parts.append(box(30, 2, 10, at=(lx, y - 2, z + 66), name="headlamp", color="#f4f2ec"))
    return group(parts, name="car")
box(140, 40, 25, at=(1305, Y1, 0), name="step", color="#9a948a")
one(paving_path(w=1400, d=120, slab=60, at=(1300, Y1 + 40, 0)).solids, "path")
for px in (2700, 3000):
    one(paving_path(w=SY - Y1 - 100, d=300, slab=60, color="#a7a196", at=(px, Y1 + 100, 0), turn=90).solids, "parking")
one(paving_path(w=490, d=120, slab=60, at=(1690, 100, 0), turn=90).solids, "path")
car(2760, 2250, "#e9ebee")
for x in range(900, 2650, 130):
    if x < 1600 or x > 1900:
        shrub(w=70, h=60, at=(x - 35, 560, 0))
for x, y in ((1650, 560), (1850, 560), (1300, 2080), (2650, 2080), (1650, 300), (1850, 300)):
    box(12, 12, 60, at=(x - 6, y - 6, 0), name="lamp post", color=dark)
    box(16, 16, 10, at=(x - 8, y - 8, 60), name="lamp", color="#fff6d8")
for x, y, h, w, crown in ((300, 300, 1300, 500, "cone"), (3300, 350, 900, 600, "round"), (300, 1500, 1400, 500, "cone"),
                          (3300, 1400, 1200, 450, "cone"), (450, 2650, 800, 500, "round"), (250, 800, 1500, 450, "cone")):
    tree(h=h, w=w, crown=crown, at=(x - w / 2, y - w / 2, 0))
