# Dining room: an oak table on splayed, tapered legs (each leg a loft between two squares), six
# chairs whose back legs curve through three sections and carry a bent back rest, places laid
# for dinner, a walnut sideboard with doors and drawers that open, an arc lamp over the table.
from math import *
units("cm")

oak, walnut, walnut2 = "#c9a27a", "#5e4231", "#77543e"
fabric, linen, china = "#a85c3b", "#e6dfd2", "#f5f3ee"
brass, black, steel = "#c9a35a", "#2b2b2e", "#c9ccd2"

def spline(pts, n=3):
    # a smooth line through the points (Catmull-Rom), n steps between each two
    out = []
    for i in range(len(pts) - 1):
        p0, p1, p2, p3 = pts[max(0, i - 1)], pts[i], pts[i + 1], pts[min(len(pts) - 1, i + 2)]
        for j in range(n):
            t = j / n
            out.append(tuple(0.5 * (2 * p1[k] + (p2[k] - p0[k]) * t + (2 * p0[k] - 5 * p1[k] + 4 * p2[k] - p3[k]) * t * t + (3 * p1[k] - p0[k] - 3 * p2[k] + p3[k]) * t * t * t) for k in range(2)))
    out.append(pts[-1])
    return out

def leg(top, bottom, z0, z1, name, color):
    # a tapered leg: a square at the floor, a bigger one under the top, lofted (offset centres splay it)
    (x0, y0, w0), (x1, y1, w1) = bottom, top
    return loft([rect(x0, y0, w0, w0, center=True, offset=z0), rect(x1, y1, w1, w1, center=True, offset=z1)], name=name, color=color)

FL = 1.1                                    # the rug's top: the furniture on it stands here

# ---------- the table ----------
TH = 75                                     # its height
legs = []                                   # the legs first: their outlines would land on the top's underside
for sx in (-1, 1):
    for sy in (-1, 1):
        legs.append(leg((86 * sx, 35.5 * sy, 5.5), (90 * sx, 38.5 * sy, 3.2), 0, TH - 3, "leg", oak))
top = box(200, 95, 3, at=(-100, -47.5, TH - 3), name="top", color=oak)
top.fillet(10, edges="vertical")
top.chamfer(1.2, edges="bottom")
top.fillet(0.6, edges="top")
aprons = [box(172, 2.5, 9, at=(-86, -38.2, TH - 12), name="apron", color=oak),
          box(172, 2.5, 9, at=(-86, 35.7, TH - 12), name="apron", color=oak),
          box(2.5, 66, 9, at=(-88.7, -33, TH - 12), name="apron", color=oak),
          box(2.5, 66, 9, at=(86.2, -33, TH - 12), name="apron", color=oak)]
table = group([top] + legs + aprons, name="table")
table.move(0, 0, FL)

# ---------- a chair, built at the origin facing +y, then placed six times ----------
def chair(name):
    parts = []
    for s in (-1, 1):
        parts.append(leg((19 * s, 17.5, 3.6), (20.5 * s, 19.5, 2.8), 0, 42, "front leg", oak))
        # the back leg bends: raked back at the floor, upright at the seat, raked back again at the top
        parts.append(loft([rect(20 * s, -22, 3, 3, center=True, offset=0),
                           rect(19.5 * s, -18.5, 3.6, 3.6, center=True, offset=42),
                           rect(19 * s, -23.5, 2.6, 2.6, center=True, offset=84)], name="back leg", color=oak))
        parts.append(pipe([(20 * s, 18.8, 14), (19.8 * s, -20.8, 14)], 0.9, name="stretcher", color=oak, sides=12))
    # the back rest: a band bent round the sitter, its ends running past the legs
    R, cy, a = 70, 45, 17.5
    inner = [(R * sin(radians(t)), cy - R * cos(radians(t))) for t in [-a + 2 * a * i / 16 for i in range(17)]]
    outer = [((R + 1.6) * sin(radians(t)), cy - (R + 1.6) * cos(radians(t))) for t in [a - 2 * a * i / 16 for i in range(17)]]
    parts.append(prism(inner + outer, 14, plane="xy", offset=68, name="back rest", color=oak))
    seat = box(44, 42, 3, at=(-22, -21, 42), name="seat", color=oak)
    seat.fillet(2.5, edges="vertical")
    cushion = box(42, 40, 3.5, at=(-21, -20, 45), name="cushion", color=fabric)
    cushion.fillet(3, edges="vertical")
    cushion.chamfer(1, edges="top")
    return group([seat, cushion] + parts, name=name)

SEAT_Y = 61                                 # from the table's middle to a chair's middle, along the sides
chairs = []
for i, (x, y, turn) in enumerate([(-45, -SEAT_Y, 0), (45, -SEAT_Y, 0), (-45, SEAT_Y, 180), (45, SEAT_Y, 180), (-113, 0, -90), (113, 0, 90)]):
    c = chair(f"chair {i + 1}")
    if turn:
        c.rotate(turn, axis="z", about=(0, 0, 0))
    c.move(x, y, FL)
    chairs.append(c)

# ---------- a place laid for each chair: plate, napkin, fork, knife, glass ----------
plate_profile = [(0, 0), (8.5, 0), (9, 0.4), (13.5, 1.7), (13.7, 2.0), (13.4, 2.1), (9.2, 0.9), (0, 0.7)]
glass_profile = [(0, 0), (3.6, 0), (3.6, 0.2), (0.7, 0.5), (0.35, 1.3), (0.35, 8.6)] + spline([(0.8, 9.3), (2.9, 10.6), (3.9, 13), (4.05, 15.5), (3.5, 19.5)], 1) + spline([(3.35, 19.5), (3.9, 15.5), (3.75, 13), (2.75, 10.8), (0.7, 9.8)], 1) + [(0, 9.7)]
fork_pts = [(-0.5, 0), (0.5, 0), (0.65, 1.5), (0.6, 8), (0.35, 11.6), (0.45, 12.6), (1.1, 13.6), (1.2, 15), (1.2, 19), (0.78, 19), (0.78, 15),
            (0.54, 15), (0.54, 19), (0.12, 19), (0.12, 15), (-0.12, 15), (-0.12, 19), (-0.54, 19), (-0.54, 15), (-0.78, 15), (-0.78, 19),
            (-1.2, 19), (-1.2, 15), (-1.1, 13.6), (-0.45, 12.6), (-0.35, 11.6), (-0.6, 8), (-0.65, 1.5)]
knife_pts = [(-0.55, 0), (0.55, 0), (0.6, 9), (0.45, 9.6), (0.45, 19), (0.2, 21), (-0.35, 21), (-0.9, 18), (-0.9, 10), (-0.45, 9.6), (-0.6, 9)]
def place(name):
    plate = polygon(plate_profile, plane="xz").revolve(axis="z", name="plate", color=china, sides=40)
    napkin = box(11, 18, 0.5, at=(-24, -9, 0), name="napkin", color="#a9b8a3")
    fork = prism([(x - 18.5, y - 9.5) for x, y in fork_pts], 0.25, plane="xy", offset=0.5, name="fork", color=steel)
    knife = prism([(x + 17, y - 10.5) for x, y in knife_pts], 0.25, plane="xy", offset=0, name="knife", color=steel)
    glass = polygon(glass_profile, plane="xz").revolve(axis="z", name="glass", color="glass", sides=28)
    glass.move(14, 16, 0)
    return group([plate, napkin, fork, knife, glass], name=name)
PY = 47.5 - 3 - 13.7                        # a plate's middle, in from the table's edge
places = []
for i, (x, y, turn) in enumerate([(-45, -PY, 0), (45, -PY, 0), (-45, PY, 180), (45, PY, 180), (-(100 - 16.7), 0, -90), (100 - 16.7, 0, 90)]):
    p = place(f"place {i + 1}")
    if turn:
        p.rotate(turn, axis="z", about=(0, 0, 0))
    p.move(x, y, FL + TH)
    places.append(p)

# ---------- in the middle: brass candle holders and a vase with a bare branch ----------
Z = FL + TH
centre = []
for x, y, h in [(-30, 4, 12), (-17, -3, 8), (26, 3, 15)]:
    holder = polygon(spline([(0, 0), (3.6, 0), (3.4, 0.8), (1.6, 1.6), (0.9, h * 0.5), (1.3, h - 1.2), (2.0, h - 0.4), (2.0, h), (0, h)], 1), plane="xz").revolve(axis="z", name="candle holder", color=brass, sides=28)
    holder.move(x, y, Z)
    candle = cylinder(1.05, 20 - h * 0.4, at=(x, y, Z + h), name="candle", color="#f4efe4", sides=20)
    centre += [holder, candle]
vase = polygon(spline([(0, 0), (4.5, 0), (6.5, 4), (7, 9), (5, 15), (2.4, 19), (2.2, 22)], 3) + [(0, 22)], plane="xz").revolve(axis="z", name="vase", color="#3f5a73", sides=40)
vase.move(3, 0, Z)
branch = pipe([(3, 0, Z + 18), (3.6, 0.5, Z + 32), (1.5, 1.5, Z + 44), (-4, 2.5, Z + 54), (-11, 3, Z + 59)], 0.35, smooth=True, name="branch", color="#5b4636", sides=8)
twig = pipe([(2.4, 1.3, Z + 41), (6, 1.6, Z + 49), (12, 1.0, Z + 54)], 0.22, smooth=True, name="branch", color="#5b4636", sides=8)
centre += [vase, branch, twig]
centrepiece = group(centre, name="centrepiece")

# ---------- the sideboard: walnut, three fronts that open, on splayed legs ----------
SX, SY, SW, SD = -95, 150, 190, 45          # its left-front corner, width, depth
LZ, CZ = 18, 74                             # the carcass from the top of its legs up to here
sb = []
for lx in (SX + 9, SX + SW - 9):
    for ly in (SY + 7, SY + SD - 7):
        sx, sy = (-1 if lx < 0 else 1), (-1 if ly < SY + SD / 2 else 1)
        sb.append(leg((lx, ly, 4.2), (lx + 2.5 * sx, ly + 1.8 * sy, 2.6), 0, LZ, "leg", walnut))
carcass = box(SW, SD, CZ - LZ, at=(SX, SY, LZ), name="carcass", color=walnut)
carcass.fillet(1.5, edges="vertical")
sb_top = box(SW + 2, SD + 1, 2.5, at=(SX - 1, SY - 1, CZ), name="top", color=walnut2)
sb_top.fillet(1.5, edges="vertical")
sb_top.fillet(0.5, edges="top")
sb += [carcass, sb_top]
W3 = SW / 3
def sb_front(x0, z0, z1, name, bar):
    p = box(W3 - 0.6, 2, z1 - z0 - 0.6, at=(x0 + 0.3, SY - 2, z0 + 0.3), name="front", color=walnut2)
    if bar == "v":
        h = box(1.2, 1.5, 16, at=(x0 + (W3 - 6 if x0 < SX + 1 else 4.8), SY - 3.5, (z0 + z1) / 2 - 8), name="handle", color=brass)
    else:
        h = box(18, 1.5, 1.2, at=(x0 + W3 / 2 - 9, SY - 3.5, z1 - 6), name="handle", color=brass)
    return group([p, h], name=name)
sb.append(sb_front(SX, LZ + 2, CZ - 2, "left door", "v"))
dz = (CZ - LZ - 4) / 3
for k in range(3):
    sb.append(sb_front(SX + W3, LZ + 2 + dz * k, LZ + 2 + dz * (k + 1), f"drawer {3 - k}", "h"))
sb.append(sb_front(SX + 2 * W3, LZ + 2, CZ - 2, "right door", "v"))

# on it: a lamp with a linen drum shade, a stack of books, a bowl
TZ = CZ + 2.5
lamp_base = polygon(spline([(0, 0), (5, 0), (8.5, 4), (10, 10), (8, 17), (3.5, 22), (2.2, 25)], 3) + [(0, 25)], plane="xz").revolve(axis="z", name="lamp base", color="#d9cfc0", sides=40)
lamp_base.move(55, SY + SD / 2, TZ)
stem = cylinder(0.6, 12, at=(55, SY + SD / 2, TZ + 25), name="lamp stem", color=brass, sides=12)
shade = cylinder(16, 20, at=(55, SY + SD / 2, TZ + 30), name="shade", color=linen, sides=48)
shade.shell(0.4, open=["top", "bottom"])
books = [box(24, 17, 3.2, at=(-70, SY + 14, TZ), name="book", color="#3e5c76"),
         box(22, 16, 2.6, at=(-69, SY + 14.5, TZ + 3.2), name="book", color="#c7b299"),
         box(20, 15, 3.6, at=(-68, SY + 15, TZ + 5.8), name="book", color="#8c3b2e")]
books[1].rotate(-6, axis="z")
books[2].rotate(4, axis="z")
bowl = polygon(spline([(0, 0), (4, 0), (9, 2.5), (12, 6.5), (13, 9)], 2) + [(0, 9)], plane="xz").revolve(axis="z", name="bowl", color="#a9b8a3", sides=48)
bowl.shell(0.6, open="top")
bowl.move(-25, SY + SD / 2, TZ)
sideboard = group(sb + [lamp_base, stem, shade, bowl] + books, name="sideboard")

# ---------- the arc lamp: a heavy round foot, a steel arc, a dome shade over the table ----------
foot = cone(19, 16.5, 4, at=(-170, 150, 0), name="lamp foot", color="#e8e6e1", sides=48)
arc = pipe([(-170, 150, 4), (-170, 150, 110), (-158, 128, 175), (-118, 88, 208), (-62, 40, 214), (-18, 8, 201), (0, 0, 188)], 1.1, smooth=True, name="arc", color=black, sides=14)
dome = polygon(spline([(23, 0), (22.2, 5), (19, 10.5), (12, 15), (3, 17)], 2) + [(0, 17), (0, 16.4)] + spline([(2.9, 16.4), (11.7, 14.4), (18.5, 10.2), (21.6, 4.9), (22.4, 0)], 2), plane="xz").revolve(axis="z", name="dome", color=black, sides=56)
dome.move(0, 0, 171)
bulb = sphere(4, at=(0, 0, 175), name="bulb", color="#fff3d6", sides=20)
arc_lamp = group([foot, arc, dome, bulb], name="arc lamp")

# ---------- the floor, laid last (outlines drawn on a face would split it): oak planks, a rug ----------
planks = []
for i in range(22):
    planks.append(box(520, 20, 2, at=(-260, -200 + 20 * i, -2), name="plank", color="#dccab0" if i % 2 else "#d3bfa3"))
floor_ = group(planks, name="floor")
rug_base = box(300, 230, 0.8, at=(-150, -115, 0), name="rug", color="#6f7d7b")
rug_field = box(284, 214, 0.3, at=(-142, -107, 0.8), name="rug field", color="#a9b3ae")
rug = group([rug_base, rug_field], name="rug")
