Gallery Istanbul Marble Machine
Istanbul Marble Machine
A kinetic desk piece: turn the crank and a screw lift carries a marble up the Galata Tower, over the Bosphorus Bridge, round the Maiden's Tower and home along the aqueduct, while four cats spin on their gears.
Needs a project open in ArgilCAD. The CAD engine starts when a project is open, and it builds the model from the code. It runs on a computer; phones and tablets open ArgilCAD as a viewer.
- Open ArgilCAD and create or open a project.
- Paste the code into the prompt box or the Code Editor.
- Run it — then change any dimension, or ask the AI for a change. Running code is free.
- Size
- 250 × 180 × 217.9 mm
- Parts
- 17
- Category
- Toys
- How it was made
- The user's own AI
- Licence
- CC BY-NC-SA 4.0
Code model.py · 469 lines
# Istanbul Marble Machine — a kinetic desk piece.
#
# Turn the crank on the star gear: four low-poly Istanbul cats spin on their
# gears, and the gear train winds the screw lift inside the Galata Tower. A
# marble rises inside the tower, rolls out over the Bosphorus Bridge,
# spirals down round the Maiden's Tower on its rock and comes home along
# the aqueduct.
#
# Seen from above the marble runs a belt round two pulleys: counter-
# clockwise round the lift and counter-clockwise down the spiral, the bridge
# and the aqueduct being the belt's two straight runs.
import math
from build123d import *
from bd_warehouse.gear import SpurGear
# ---------------------------------------------------------------- parameters
MARBLE_D = 16.0 # a standard glass marble
MOD = 1.5 # one module for every gear
GEAR_T = 6.0 # gear thickness
GEAR_Z = GEAR_T / 2 + 0.5 # gear centre height: 0.5 mm clear of the base
GEAR_TOP = GEAR_Z + GEAR_T / 2
PEG_R, BORE_R = 3.0, 3.2 # base pegs, gear bores
SLOPE = math.radians(4.0) # chute slope
CHUTE_W = MARBLE_D + 3 # channel width between the lips
LIP, FLOOR = 7.0, 2.5 # channel lip height, floor thickness
TOWER = Vector(-85, 30, 0) # Galata Tower centre
MAIDEN = Vector(80, 30, 0) # Maiden's Tower centre
CENTRE = Vector(-25, -45, 0) # crank gear centre
TOWER_R, TOWER_IN = 27.0, 23.0 # tower wall
LIFT_SHAFT_R, FLIGHT_R = 5.0, 16.5 # screw lift
LIFT_PITCH = 24.0
MARBLE_RUN_R = TOWER_IN - MARBLE_D / 2 # the marble's radius in the lift
RIB_IN = 17.5 # the stop rib's inner edge
INNER_FLOOR = 10.0 # lift floor top, the intake level
DOOR_W, DOOR_H = 22.0, MARBLE_D + 5
SPIRAL_R = 30.0 # marble path radius round the Maiden's Tower
SPIRAL_PITCH = 24.0
CORE = 11.0 # the Maiden's Tower's half-width
SPIRAL_OUT = SPIRAL_R + CHUTE_W / 2 + 2 # outer edge of the spiral floor
Z_CAT, Z_CENTRE, Z_IDLER, Z_LIFT = 18, 36, 16, 26 # tooth counts
# ------------------------------------------------------------------- helpers
def pitch_r(teeth):
return MOD * teeth / 2
def unit(v):
return v / v.length
def angle_of(v):
return math.degrees(math.atan2(v.Y, v.X))
def radial(angle_deg, r, z=0.0):
a = math.radians(angle_deg)
return Vector(r * math.cos(a), r * math.sin(a), z)
def up(r, h):
"""A cylinder standing on z = 0."""
return Cylinder(r, h, align=(Align.CENTER, Align.CENTER, Align.MIN))
def arch_cutter(width, height, depth):
"""An arched opening, its sill on z = 0, cutting along X."""
body = Box(depth, width, height - width / 2, align=(Align.CENTER, Align.CENTER, Align.MIN))
top = Pos(0, 0, height - width / 2) * Rot(0, 90, 0) * Cylinder(width / 2, depth)
# Nothing below the sill, however squat the arch.
return (body + top) & Box(depth, width, height, align=(Align.CENTER, Align.CENTER, Align.MIN))
def eight_point_star(size, height):
"""The Seljuk star: two squares, one turned 45 degrees."""
sq = Rectangle(size, size)
return extrude(sq + Rot(0, 0, 45) * sq, height)
# The Studio finds hinges and gears from round faces on a common axis. Only
# the moving parts may have them there, so the tower's bearings are
# polygons, and so are the cuts that fit the chutes round other parts.
def bearing(clearance, height):
"""A 16-sided hole for the lift shaft."""
r = (LIFT_SHAFT_R + clearance) / math.cos(math.pi / 16)
return extrude(RegularPolygon(r, 16), height)
def round_cut(r, height):
"""A 64-sided prism standing in for a cylinder of radius r."""
return extrude(RegularPolygon(r / math.cos(math.pi / 64), 64), height)
def ring(cx, cy, rx, ry, z, n=6, turn=0.0):
"""A low-poly section: an n-gon on an ellipse, level at height z."""
return Wire.make_polygon([Vector(cx + rx * math.cos(a), cy + ry * math.sin(a), z)
for a in (math.radians(turn + 360 * i / n) for i in range(n))], close=True)
def facets(*sections):
"""A faceted solid through low-poly sections, flat face to flat face."""
return Solid.make_loft(list(sections), ruled=True)
def gear(teeth, turn=0.0):
g = SpurGear(module=MOD, tooth_count=teeth, pressure_angle=20, thickness=GEAR_T)
return Rot(0, 0, turn) * (g - Cylinder(BORE_R, 2 * GEAR_T))
def mesh_turn(at, driver_at, driver_teeth, driver_turn, teeth):
"""The turn that puts a gap of this gear on the driver's tooth."""
g = angle_of(at - driver_at)
f_driver = (g - driver_turn) * driver_teeth / 360
return (g + 180) - (0.5 - f_driver) * 360 / teeth
def chute(a, b, za, zb):
"""A straight channel from a to b, its floor top at za and zb."""
run = b - a
length = run.length
pitch = math.degrees(math.atan2(zb - za, length))
profile = (Box(length, CHUTE_W + 4, FLOOR + LIP, align=(Align.MIN, Align.CENTER, Align.MIN))
- Pos(0, 0, FLOOR) * Box(length, CHUTE_W, LIP, align=(Align.MIN, Align.CENTER, Align.MIN)))
return Pos(a.X, a.Y, za) * Rot(0, 0, angle_of(run)) * Rot(0, -pitch, 0) * Pos(0, 0, -FLOOR) * profile
# ------------------------------------------------------------ marble circuit
to_m = MAIDEN - TOWER
D = to_m.length
u = unit(to_m)
north = Vector(-u.Y, u.X, 0)
# The bridge leaves the lift radially and meets the spiral's south side.
half = math.degrees(math.asin(SPIRAL_R / D))
EXIT_ANGLE = angle_of(to_m) - half
e = radial(EXIT_ANGLE, 1)
bridge_start = TOWER + e * (TOWER_R + 0.3)
bridge_end = TOWER + e * math.sqrt(D * D - SPIRAL_R * SPIRAL_R)
# The aqueduct is the belt's other run: tangent to the spiral's north side
# and to the marble's circle in the tower, entering it running the way the
# lift turns.
k = (SPIRAL_R - MARBLE_RUN_R) / D
n = u * (-k) + north * math.sqrt(1 - k * k)
aqueduct_end = MAIDEN + n * SPIRAL_R # leaves the spiral here
touch = TOWER + n * MARBLE_RUN_R # would touch the lift circle here
a_dir = unit(touch - aqueduct_end)
# Where the run crosses the tower's outer wall.
rel = aqueduct_end - TOWER
bq = rel.dot(a_dir)
aqueduct_start = aqueduct_end + a_dir * (-bq - math.sqrt(bq * bq - (rel.dot(rel) - (TOWER_R + 0.3) ** 2)))
INTAKE_ANGLE = angle_of(aqueduct_start - TOWER)
bridge_len = (bridge_end - bridge_start).length
aqueduct_len = (aqueduct_start - aqueduct_end).length
# Down the spiral, counter-clockwise, from the bridge to the aqueduct.
ang_top = angle_of(bridge_end - MAIDEN)
ang_bot = angle_of(aqueduct_end - MAIDEN)
spiral_turn = 3 * 360 + (ang_bot - ang_top) % 360
# Heights, from the intake up.
Z_INTAKE = INNER_FLOOR + 0.5
Z_SPIRAL_BOT = Z_INTAKE + aqueduct_len * math.tan(SLOPE)
Z_SPIRAL_TOP = Z_SPIRAL_BOT + SPIRAL_PITCH * spiral_turn / 360
Z_EXIT = Z_SPIRAL_TOP + bridge_len * math.tan(SLOPE)
BODY_TOP = Z_EXIT + DOOR_H + 8
# The marble rides the lift against one stop rib, just counter-clockwise of
# the exit: the lift's turning floor carries it round until the rib stops
# it, and the flight lifts it straight up to the exit door. 33 degrees puts
# a resting marble's centre on the exit's line.
RIB_ANGLE = EXIT_ANGLE + 33
# ---------------------------------------------------------------- gear layout
cat_at = [CENTRE + radial(a, pitch_r(Z_CENTRE) + pitch_r(Z_CAT)) for a in (45, 135, 225, 315)]
cat_turn = [mesh_turn(p, CENTRE, Z_CENTRE, 0.0, Z_CAT) for p in cat_at]
# The idler joins the back-left cat's gear to the lift gear under the tower.
d2 = cat_at[1]
r_a = pitch_r(Z_CAT) + pitch_r(Z_IDLER)
r_b = pitch_r(Z_LIFT) + pitch_r(Z_IDLER)
span = TOWER - d2
dist = span.length
along = (r_a ** 2 - r_b ** 2 + dist ** 2) / (2 * dist)
off = math.sqrt(max(r_a ** 2 - along ** 2, 0))
idler_at = d2 + span / dist * along - Vector(-span.Y, span.X, 0) / dist * off
idler_turn = mesh_turn(idler_at, d2, Z_CAT, cat_turn[1], Z_IDLER)
lift_turn = mesh_turn(TOWER, idler_at, Z_IDLER, idler_turn, Z_LIFT)
# ------------------------------------------------------------------- the base
base = Pos(0, -5, -8) * Box(250, 180, 8, align=(Align.CENTER, Align.CENTER, Align.MIN))
base = chamfer(base.edges().group_by(Axis.Z)[-1], 2)
# Every gear's bore is capped, and hangs on its peg's tip, clear of the base.
for p in [CENTRE, idler_at, *cat_at]:
base += Pos(p.X, p.Y, 0) * up(PEG_R, GEAR_TOP)
# The lift's pin is tapered: a round one on the tower's axis would read as a
# hinge between the base and the tower.
base += Pos(TOWER.X, TOWER.Y, 0) * Cone(PEG_R, PEG_R - 0.6, GEAR_TOP,
align=(Align.CENTER, Align.CENTER, Align.MIN))
# A Seljuk star inlaid in the front right corner.
STAR = Vector(85, -65, -1)
base -= Pos(STAR) * eight_point_star(34, 2)
base += Pos(STAR) * Rot(0, 0, 22.5) * eight_point_star(16, 1)
base.label = "base"
# ------------------------------------------------------------ the Galata Tower
T = Pos(TOWER.X, TOWER.Y, 0)
tower = T * (up(TOWER_R, BODY_TOP) - up(TOWER_IN, BODY_TOP))
# The stop rib, standing just clear of the lift's turning floor.
tower += T * Pos(0, 0, INNER_FLOOR + 0.3) * Rot(0, 0, RIB_ANGLE) * Pos(RIB_IN, 0, 0) * Box(
TOWER_IN + 0.5 - RIB_IN, 3, Z_EXIT + DOOR_H - INNER_FLOOR - 0.3, align=(Align.MIN, Align.CENTER, Align.MIN))
# Roof slab with the lift's top bearing, and the cornice round it.
tower += T * Pos(0, 0, BODY_TOP - 4) * (up(TOWER_R + 2.5, 4) - bearing(0.4, 4))
# The arcaded gallery, the conical cap and its finial.
gallery = T * Pos(0, 0, BODY_TOP) * (up(TOWER_R - 1, 16) - up(TOWER_R - 4, 16))
for i in range(16):
gallery -= T * Pos(0, 0, BODY_TOP + 3) * Rot(0, 0, i * 22.5) * Pos(TOWER_R - 2.5, 0, 0) * arch_cutter(5, 10, 8)
tower += gallery
tower += T * Pos(0, 0, BODY_TOP + 16) * (up(TOWER_R + 2, 3) - up(TOWER_R - 4, 3))
CAP_Z = BODY_TOP + 19
tower += T * Pos(0, 0, CAP_Z) * Cone(TOWER_R + 1, 1.5, 38, align=(Align.CENTER, Align.CENTER, Align.MIN))
tower += T * Pos(0, 0, CAP_Z + 37) * Cone(1.6, 0.8, 7, align=(Align.CENTER, Align.CENTER, Align.MIN))
tower += T * Pos(0, 0, CAP_Z + 45) * Sphere(2)
def in_wall(angle, z):
return T * Pos(0, 0, z) * Rot(0, 0, angle) * Pos(TOWER_R - 2, 0, 0)
# The intake: the marble comes in along the wall, so it is a long gate cut
# along its run, through the wall and the rib's outer end (the marble stops
# against its inner edge). Then the exit door above the rib, and slit
# windows between them to watch the marble climb.
gate_len = (touch - aqueduct_start).length + 6
gate_mid = aqueduct_start + a_dir * (gate_len / 2 - 6)
gate = Pos(gate_mid.X, gate_mid.Y, INNER_FLOOR) * Rot(0, 0, angle_of(a_dir)) * arch_cutter(DOOR_W, DOOR_H, gate_len)
tower -= gate & T * (up(TOWER_R + 1, BODY_TOP) - up(RIB_IN + 1.5, BODY_TOP))
tower -= in_wall(EXIT_ANGLE, Z_EXIT) * arch_cutter(DOOR_W, DOOR_H, 12)
for z in range(int(INNER_FLOOR + DOOR_H + 6), int(Z_EXIT - 20), 26):
tower -= in_wall(EXIT_ANGLE, z) * arch_cutter(7, 18, 12)
# Arched windows round the rest of the body, clear of the rib and the slits.
for level in (40, 70, 100):
for i in range(8):
a = EXIT_ANGLE + 22.5 + i * 45
near = min(abs((a - b + 180) % 360 - 180) for b in (EXIT_ANGLE, RIB_ANGLE))
if near > 30 and level + 16 < BODY_TOP - 6:
tower -= in_wall(a, level) * arch_cutter(6, 14, 10)
# The idler reaches the lift gear through a slot at the foot of the wall.
tower -= Pos(idler_at.X, idler_at.Y, 0) * round_cut(pitch_r(Z_IDLER) + MOD + 1.5, INNER_FLOOR - 2.5)
tower.label = "galata_tower"
# ------------------------------------------------------------------ the lift
# One piece: gear, turning floor, shaft and flight. The tower slides down
# over it, and its roof bearing takes the shaft's top.
#
# A left-hand screw: turning counter-clockwise (seen from above) it lifts a
# marble that the rib holds still. The flight leans outward, so the marble
# rides against the wall and rolls out at the door by itself. It is lofted
# through radial sections rather than swept: a swept helix is a B-spline
# whose poles stand far off the surface, and the Studio reads a gear's size
# from its part's box. Its leading edge is thin, to slip under a marble.
flight_h = Z_EXIT - Z_INTAKE - 2.5
flight_turns = flight_h / LIFT_PITCH
flight_n = int(flight_turns * 24)
sections = []
for i in range(flight_n + 1):
th = -2 * math.pi * flight_turns * i / flight_n # clockwise as it climbs
z = flight_h * i / flight_n
t = min(3.0, 0.8 + 2.2 * i / 4) # the lead-in thickens
sections.append(Wire.make_polygon(
[Vector(r * math.cos(th), r * math.sin(th), z + dz)
for r, dz in ((4.5, 1.7), (FLIGHT_R, 0), (FLIGHT_R, t), (4.5, 1.7 + t))], close=True))
lift = Pos(TOWER.X, TOWER.Y, GEAR_Z) * gear(Z_LIFT, lift_turn)
lift += T * Pos(0, 0, GEAR_TOP) * up(LIFT_SHAFT_R, BODY_TOP - 0.5 - GEAR_TOP)
# The floor: no wider than the gear, whose size the Studio reads the ratio by.
lift += T * Pos(0, 0, INNER_FLOOR - 2) * up(pitch_r(Z_LIFT) + MOD, 2)
lift += T * Pos(0, 0, Z_INTAKE) * Solid.make_loft(sections, ruled=True)
lift.label = "screw_lift"
# ----------------------------------------------------------------- gears, cats
idler = Pos(idler_at.X, idler_at.Y, GEAR_Z) * gear(Z_IDLER, idler_turn)
idler += Pos(idler_at.X, idler_at.Y, GEAR_TOP) * up(5, 1) # caps the bore
idler.label = "idler_gear"
C = Pos(CENTRE.X, CENTRE.Y, 0)
crank = Pos(CENTRE.X, CENTRE.Y, GEAR_Z) * gear(Z_CENTRE)
crank += C * Pos(0, 0, GEAR_TOP) * up(8, 3) # caps the bore
crank += C * Pos(0, 0, GEAR_TOP) * eight_point_star(26, 3)
crank -= C * Pos(0, 0, GEAR_TOP + 1.5) * Rot(0, 0, 22.5) * eight_point_star(12, 2)
crank += C * Pos(19, 0, GEAR_TOP) * up(3.5, 16)
crank += C * Pos(19, 0, GEAR_TOP + 16) * Sphere(4.5)
crank.label = "crank_gear"
def cat(at, turn, pose):
"""A low-poly Istanbul street cat sitting up on a gear, its tail curled
round its front paws. Every face is flat; it stays inside the gear's
tips, whose size the Studio reads the ratio by."""
c = facets(ring(-1.5, 0, 7, 6, 0), ring(-1.2, 0, 6.6, 5.6, 6),
ring(0.3, 0, 4.8, 4.4, 13), ring(1.8, 0, 3, 3, 18)) # body
for y in (-2.3, 2.3): # front legs
c += facets(ring(5.2, y, 1.5, 1.5, 0, 4, 45), ring(3.6, y, 1.2, 1.2, 13, 4, 45))
c += facets(ring(2.4, 0, 2.6, 2.6, 17), ring(3.4, 0, 4.6, 5.0, 21),
ring(3.6, 0, 4.2, 4.6, 25), ring(2.8, 0, 2.2, 2.8, 28)) # head
for y in (-1, 1): # ears
c += facets(ring(2.6, 2.6 * y, 2.3, 1.9, 26, 3, 90), ring(2.4, 3.1 * y, 0.35, 0.35, 30.5, 3, 90))
tail = [(-7.8, 0), (-6.4, 4.8), (-2, 7.6), (3, 7.2), (7, 4.6)]
for (x0, y0), (x1, y1) in zip(tail, tail[1:]):
run = Vector(x1 - x0, y1 - y0, 0)
c += Pos((x0 + x1) / 2, (y0 + y1) / 2, 1.3) * Rot(0, 0, angle_of(run)) * Box(run.length + 2.6, 2.6, 2.6)
return Pos(at.X, at.Y, GEAR_Z) * gear(Z_CAT, turn) + Pos(at.X, at.Y, GEAR_TOP) * Rot(0, 0, pose) * c
cats = []
for i, (p, t) in enumerate(zip(cat_at, cat_turn)):
c = cat(p, t, pose=i * 90 + 30)
c.label = f"cat_{i + 1}"
cats.append(c)
# ------------------------------------------------- the Maiden's Tower, its rock
# Every face here is flat or conical: a round face on its axis would offer
# the tower as a hinge or a gear in the Studio.
Mk = Pos(MAIDEN.X, MAIDEN.Y, 0)
ISLAND_H = 12.0 # under the spiral's lowest turn
# A low-poly rock: an uneven nine-sided slab with sloping sides, and three
# boulders at its foot.
rock = zip(range(0, 360, 40), (40, 43, 38, 42, 39, 44, 37, 41, 40))
island = Mk * extrude(Polygon(*[(r * math.cos(math.radians(a)), r * math.sin(math.radians(a))) for a, r in rock],
align=None), ISLAND_H, taper=35)
for a, r, size in ((20, 40, 7), (150, 40, 6), (265, 39, 8)):
island += Mk * Pos(radial(a, r)) * extrude(RegularPolygon(size, 5, rotation=a), size * 0.7, taper=35)
island.label = "island"
def chamfered_square(half, cut):
return Polygon((half, cut - half), (half, half - cut), (half - cut, half), (cut - half, half),
(-half, half - cut), (-half, cut - half), (cut - half, -half), (half - cut, -half), align=None)
# The square stone tower, the spiral wound round it, then above the
# spiral's head its windows, the gallery, the lantern and the lead cap.
BODY_Z = Z_SPIRAL_TOP + LIP + 20
maiden = Mk * Pos(0, 0, ISLAND_H) * extrude(chamfered_square(CORE, 3), BODY_Z - ISLAND_H)
for a in (0, 90, 180, 270):
for z in (BODY_Z - 16, BODY_Z - 30):
maiden -= Mk * Pos(0, 0, z) * Rot(0, 0, a) * Pos(CORE, 0, 0) * arch_cutter(4, 10, 4)
maiden += Mk * Pos(0, 0, BODY_Z) * extrude(chamfered_square(CORE + 3, 4), 3) # cornice
maiden += Mk * Pos(0, 0, BODY_Z + 3) * (extrude(chamfered_square(CORE + 3, 4), 3)
- extrude(chamfered_square(CORE + 1.8, 3.4), 3)) # parapet
DRUM_Z = BODY_Z + 3
maiden += Mk * Pos(0, 0, DRUM_Z) * extrude(RegularPolygon(9, 8), 16) # lantern
for i in range(4):
maiden -= Mk * Pos(0, 0, DRUM_Z + 4) * Rot(0, 0, 22.5 + 90 * i) * Pos(9 * math.cos(math.pi / 8), 0, 0) * arch_cutter(3.5, 9, 3)
maiden += Mk * Pos(0, 0, DRUM_Z + 16) * extrude(RegularPolygon(10.5, 8), 2)
CAP_TOP = DRUM_Z + 18
maiden += Mk * Pos(0, 0, CAP_TOP) * Cone(9.5, 0.6, 22, align=(Align.CENTER, Align.CENTER, Align.MIN)) # lead cap
maiden += Mk * Pos(0, 0, CAP_TOP + 21) * Box(1.2, 1.2, 14, align=(Align.CENTER, Align.CENTER, Align.MIN)) # flagpole
maiden += Mk * Pos(0, 0, CAP_TOP + 35) * Sphere(1.2)
maiden += Mk * Pos(0.6, 0, CAP_TOP + 28) * Box(10, 0.8, 6.5, align=(Align.MIN, Align.CENTER, Align.MIN)) # flag
# The spiral chute, climbing clockwise from its foot (where the aqueduct
# leaves) to its head (where the bridge arrives): the marble runs it
# counter-clockwise, downward.
spiral_path = Helix(SPIRAL_PITCH, SPIRAL_PITCH * spiral_turn / 360, SPIRAL_R, lefthand=True)
spiral_profile = Plane.XZ * Polygon(
(CORE - 0.5, -FLOOR), (SPIRAL_OUT, -FLOOR), (SPIRAL_OUT, LIP), (SPIRAL_OUT - 2, LIP),
(SPIRAL_OUT - 2, 0), (CORE - 0.5, 0), align=None)
maiden += Mk * Pos(0, 0, Z_SPIRAL_BOT) * Rot(0, 0, ang_bot) * sweep(spiral_profile, path=spiral_path, is_frenet=True)
maiden.label = "maidens_tower"
# ---------------------------------------------------------- the Bosphorus Bridge
# The deck is the chute; two portal towers carry it and the main cables.
# Both chutes stop 1 mm short of the spiral, whose section meets them at a
# slant.
b_dir = unit(bridge_end - bridge_start)
bridge = chute(bridge_start, bridge_end - b_dir * 1.0, Z_EXIT, Z_SPIRAL_TOP)
b_side = Vector(-b_dir.Y, b_dir.X, 0)
b_heading = angle_of(b_dir)
LEG = CHUTE_W / 2 + 2 + 3
PYLON_H = 55
def deck_z(s):
return Z_EXIT + (Z_SPIRAL_TOP - Z_EXIT) * s / bridge_len
pylons = [36.0, bridge_len - 44]
for s in pylons:
at = bridge_start + b_dir * s
under = deck_z(s) - FLOOR - 0.3
for side in (-1, 1):
leg = at + b_side * (side * LEG)
bridge += Pos(leg.X, leg.Y, 0) * Rot(0, 0, b_heading) * Box(
5, 5, under + PYLON_H, align=(Align.CENTER, Align.CENTER, Align.MIN))
for z, h in ((under - 5, 5), (under + PYLON_H / 2, 4), (under + PYLON_H - 5, 4)):
bridge += Pos(at.X, at.Y, z) * Rot(0, 0, b_heading) * Box(
4, 2 * LEG + 5, h, align=(Align.CENTER, Align.CENTER, Align.MIN))
# Main cables: straight side spans, a sagging main span, and hangers.
tops = [deck_z(s) - FLOOR + PYLON_H - 1 for s in pylons]
mid = sum(pylons) / 2
low = deck_z(mid) + LIP + 5
for side in (-1, 1):
def on_cable(s, z):
p = bridge_start + b_dir * s + b_side * (side * LEG)
return Vector(p.X, p.Y, z)
pts = [on_cable(0, deck_z(0) + LIP), on_cable(pylons[0], tops[0])]
for i in range(1, 8):
f = i / 8
s = pylons[0] + (pylons[1] - pylons[0]) * f
z = (1 - f) * tops[0] + f * tops[1] - 4 * f * (1 - f) * ((tops[0] + tops[1]) / 2 - low)
pts.append(on_cable(s, z))
if i % 2 == 0: # a hanger down to the deck lip
bottom = deck_z(s) + LIP - 0.5
bridge += Pos(on_cable(s, bottom)) * up(0.6, z - bottom)
pts += [on_cable(pylons[1], tops[1]), on_cable(bridge_len, deck_z(bridge_len) + LIP)]
for a, b in zip(pts, pts[1:]):
bridge += Pos(a) * Solid.make_cylinder(0.9, (b - a).length, Plane((0, 0, 0), z_dir=b - a))
bridge -= T * round_cut(TOWER_R + 0.2, BODY_TOP) # the deck ends on the tower
bridge -= island # and its legs stand on the rock
bridge.label = "bosphorus_bridge"
# ------------------------------------------------------------- the aqueduct
# The return chute on a row of arches, like Bozdogan's.
aqueduct = chute(aqueduct_end + a_dir * 1.0, aqueduct_start, Z_SPIRAL_BOT, Z_INTAKE)
steps = int(aqueduct_len // 12)
for i in range(steps):
s0, s1 = i * aqueduct_len / steps, (i + 1) * aqueduct_len / steps
z_top = Z_SPIRAL_BOT + (Z_INTAKE - Z_SPIRAL_BOT) * s1 / aqueduct_len - FLOOR - 0.3
pier = Box(s1 - s0 + 0.2, CHUTE_W - 2, z_top, align=(Align.CENTER, Align.CENTER, Align.MIN))
if z_top > 9:
pier -= Rot(0, 0, 90) * arch_cutter(s1 - s0 - 4, z_top - 3, CHUTE_W)
p = aqueduct_end + a_dir * ((s0 + s1) / 2)
aqueduct += Pos(p.X, p.Y, 0) * Rot(0, 0, angle_of(a_dir)) * pier
aqueduct -= T * round_cut(TOWER_R + 0.2, BODY_TOP)
aqueduct -= island # its piers stand on the rock
aqueduct.label = "aqueduct"
# The Bosphorus round the rock: a thin low-poly sheet to colour as water,
# inside the base's chamfer and holed for the piers and the bridge's legs.
water = zip(range(0, 360, 30), (56, 60, 54, 58, 52, 57, 55, 60, 53, 58, 56, 54))
sea = Mk * extrude(Polygon(*[(r * math.cos(math.radians(a)), r * math.sin(math.radians(a))) for a, r in water],
align=None), 1.2)
sea &= Pos(0, -5, 0) * Box(244, 174, 1.2, align=(Align.CENTER, Align.CENTER, Align.MIN))
sea -= island
sea -= bridge
sea -= aqueduct
sea.label = "sea"
# ---------------------------------------------------------------- the marbles
def marble(p, z_floor):
return Pos(p.X, p.Y, z_floor + MARBLE_D / 2 + 0.2) * Sphere(MARBLE_D / 2)
m1 = marble(bridge_start + b_dir * (bridge_len * 0.62), deck_z(bridge_len * 0.62))
m2 = marble(MAIDEN + radial(ang_bot + 150, SPIRAL_R), Z_SPIRAL_BOT + SPIRAL_PITCH * 210 / 360)
m3 = marble(aqueduct_end + a_dir * (aqueduct_len * 0.55), Z_SPIRAL_BOT + (Z_INTAKE - Z_SPIRAL_BOT) * 0.55)
marbles = [m1, m2, m3]
for i, m in enumerate(marbles):
m.label = f"marble_{i + 1}"
result = Compound(label="istanbul_marble_machine",
children=[base, tower, lift, idler, crank, *cats, sea, island, maiden, bridge, aqueduct, *marbles])
© 2026 Argil Design · CC BY-NC-SA 4.0 — share and remix with credit; no commercial use or sale; remixes under the same licence.