Walldye

Engine section

A single-cylinder engine in hatched section at top dead centre. A spray of dots lights the chamber under the spark plug.

Made with Claude Opus 5.5

Technique
technical drawing, stippling and hatching
Shape
Any screen
Added
27 September 2026

Colours

  • #1C1B1Abackground
  • #DAD8CEforeground
  • #CF6A4Caccent

Export

Format
Shape
Size

Notes

Drawn to drafting conventions: surfaces the cut passes through are hatched, hidden edges such as the head studs and the main journal are dashed, and axes are dash-dotted. The valves, connecting rod and crank web lie behind the cut and are left unhatched. A phantom line marks where the piston crown sits at the bottom of its stroke, closing the stroke dimension.

Sources

  1. ISO 128.

Source code

wallpapers/engine-section/design.py, 234 lines

"""A single-cylinder engine in drafted section at top dead centre: ruled hatching on the cut parts, a stippled burn in the combustion chamber."""

import math

import numpy as np
import shapely
from shapely import LineString, Point, Polygon, box
from shapely.ops import unary_union

from walldye import ACCENT, ACCENT_1, BG, UI, UI_ALT, UI_HI, Canvas, P, Rect, Vec, design, polar
from walldye.geom import Affine, bezier_points, hatch, poisson_disk

# The figure is drawn with the bore axis at x = 0 in a 1080-tall box, then placed by one
# transform that puts (0, MID) on the placement point.
MID = 540
YH = 380  # head face / gasket plane
BORE, WALL, FIN = 100, 24, 208  # half bore, liner wall, fin reach from the axis
DOME_H = 50
RC = (BORE**2 + DOME_H**2) / (2 * DOME_H)  # dome radius: the arc meets the bore at the gasket
DOME_C = YH - DOME_H + RC
DOME_HALF = math.degrees(math.asin(BORE / RC))  # half the dome's arc, in degrees
HEAD_TOP, HEAD_W = YH - 172, 190
YB = YH + 262  # barrel foot / crankcase flange
THROW, ROD, PIN_DROP = 80, 270, 62
CROWN_TDC = YH + 6
CKY = CROWN_TDC + PIN_DROP + ROD + THROW
CRANK = 0.0  # crank bearing; 0 is top dead centre, the rod on the bore axis
VALVE = 24  # valve axes lean this many degrees either side of the bore axis
R_TOP = (DOME_C - HEAD_TOP) / math.cos(math.radians(VALVE))  # where a stem leaves the head
STEM = R_TOP + 83 - RC
# a valve, head at y = 0 and stem up the axis (-y)
POPPET = ((-27, 0), (27, 0), (22, -7), (6, -24), (6, -STEM), (-6, -STEM), (-6, -24), (-22, -7))
DASH = (10, 6)
DASHDOT = (26, 6, 3, 6)
PHANTOM = (22, 5, 4, 5, 4, 5)


def valve_axis(sgn: int) -> Affine:
    """Local frame of one valve: the axis runs up local -y from the dome centre, and local +x
    points across it, clockwise of up."""
    return Affine.frame((0, DOME_C), deg=sgn * VALVE)


def along(sgn: int, r: float, side: float = 0.0) -> Vec:
    """The point `r` out along a valve axis from the dome centre, `side` across it."""
    return valve_axis(sgn)((side, -r))


@design(aspects="any")
def draw(s: Canvas) -> None:
    # right of centre on landscape screens; centred and larger on width-bound portrait ones
    at = s.pick(landscape=(0.6458, 0.5), portrait=(0.49, 0.53))
    k = 0.88 if s.landscape else 1.35
    place = Affine.translate(at.x, at.y) @ Affine.scale(k) @ Affine.translate(0, -MID)

    main = Vec(0, CKY)
    cp = polar(main, THROW, bearing=CRANK)
    small = Vec(0, cp.y - math.sqrt(ROD**2 - cp.x**2))  # gudgeon pin, on the bore axis
    crown = small.y - PIN_DROP

    # --- sectioned parts -------------------------------------------------
    dome = Point(0, DOME_C).buffer(RC, quad_segs=48)
    chamber = unary_union(
        [dome.intersection(box(-BORE, YH - DOME_H - 2, BORE, YH)), box(-BORE, YH, BORE, crown)]
    )

    voids = [box(-13, HEAD_TOP - 1, 13, YH - DOME_H + 4)]  # spark plug bore
    for sgn in (-1, 1):
        # port: up the valve axis, then sweeping out through the side of the head
        exit_ = Vec(sgn * (HEAD_W + 2), YH - 104)
        bend = bezier_points([along(sgn, RC - 4), along(sgn, RC + 70), exit_], 40)
        port = LineString([*bend.tolist(), exit_ + (sgn * 20, 0)])
        voids.append(port.buffer(21, cap_style="flat"))
        guide = LineString([along(sgn, RC + 20), along(sgn, R_TOP + 10)])
        voids.append(guide.buffer(6, cap_style="flat"))
    head = box(-HEAD_W, HEAD_TOP, HEAD_W, YH).difference(dome).difference(unary_union(voids))

    walls = []
    for sgn in (-1, 1):
        a, b = sorted((sgn * BORE, sgn * (BORE + WALL)))
        walls.append(box(a, YH, b, YB))
        r0 = BORE + WALL - 2
        for y in range(YH + 14, YB - 18, 25):
            fin = [(r0, y), (FIN, y + 2), (FIN, y + 8), (r0, y + 10)]
            walls.append(Polygon([(sgn * x, fy) for x, fy in fin]))
    barrel = unary_union(walls)

    outer = unary_union([Point(main).buffer(196, quad_segs=48), box(-168, YB, 168, CKY)])
    inner = unary_union(
        [
            Point(main).buffer(172, quad_segs=48),
            box(-144, YB + 22, 144, CKY),
            box(-BORE, YB - 1, BORE, YB + 30),
        ]
    )
    case = outer.difference(inner).intersection(box(-300, YB, 300, CKY + 300))

    piston = box(-BORE + 2, crown, BORE - 2, crown + 150).difference(
        box(-BORE + 14, crown + 24, BORE - 14, crown + 151)
    )
    for j in range(3):  # ring grooves
        y = crown + 10 + j * 13
        piston = piston.difference(box(-BORE + 1, y, -BORE + 9, y + 5))
        piston = piston.difference(box(BORE - 9, y, BORE - 1, y + 5))
    pins = unary_union([Point(small).buffer(19, quad_segs=24), Point(cp).buffer(30, quad_segs=24)])

    # --- behind the section plane: crank web, then the rod ----------------
    arc = [polar(main, 124, bearing=CRANK + 180 + a) for a in range(-76, 77, 2)]
    web = (
        unary_union([Polygon([*arc, main]), Point(cp).buffer(48, quad_segs=32)])
        .convex_hull.buffer(-14, quad_segs=16)
        .buffer(14, quad_segs=16)
    )
    u = (cp - small).unit()
    n = u.perp()
    shank = Polygon([small + n * 14, cp + n * 26, cp - n * 26, small - n * 14])
    rod = unary_union(
        [shank, Point(small).buffer(31, quad_segs=24), Point(cp).buffer(52, quad_segs=32)]
    )

    with s.group(transform=place, stroke_linejoin="round"):
        # centre lines sit beneath everything so parts interrupt them
        cl = P()
        cl.M(0, HEAD_TOP - 170).V(CKY + 270)
        cl.M(-270, CKY).H(270)
        cl.M(-44, small.y).H(44)
        cl.M(cp.x - 44, cp.y).H(cp.x + 44).M(cp.x, cp.y - 44).V(cp.y + 44)
        cl.circle(main, THROW)
        for sgn in (-1, 1):
            cl.M(along(sgn, RC + 4)).L(along(sgn, R_TOP + 110))
        s.stroke(cl, UI_ALT, 1.3, dash=DASHDOT)

        s.path(P().shape(web), fill=BG, stroke=UI_HI, stroke_width=2)
        s.path(P().shape(rod), fill=BG, stroke=UI_HI, stroke_width=2)
        thin, hidden = P(), P()
        for sgn in (-1, 1):
            # rod I-beam flanges and the big-end cap joint
            thin.M(small + u * 36 + n * (6 * sgn)).L(cp - u * 58 + n * (12 * sgn))
            joint = cp + n * (40 * sgn)
            hidden.M(joint - u * 28).L(joint + u * 34)
            thin.M(cp + n * (31 * sgn)).L(cp + n * (52 * sgn))
            hidden.M(sgn * 158, HEAD_TOP + 10).V(YB + 16)  # head studs
        hidden.circle(main, 40)  # main journal

        s.fill(P().shape(piston), BG)
        # section lining, one pitch and direction per part
        lined = (
            (unary_union([head, case]), 6.5, 45),
            (barrel, 6.5, -45),
            (piston, 4.5, -45),
            (pins, 4.2, 45),
        )
        lining = P()
        for region, pitch, ang in lined:
            for seg in hatch(region, pitch, deg=ang):
                lining.poly(seg)
        s.stroke(lining, UI, 1.15)

        edge = P()
        for g in (head, barrel, case, piston, pins):
            edge.shape(g)
        s.stroke(hidden, UI_ALT, 1.5, dash=DASH)
        s.stroke(thin, UI_ALT, 1.5)
        s.stroke(edge, UI_HI, 2)
        gasket = P()
        for sgn in (-1, 1):
            a, b = sorted((sgn * BORE, sgn * HEAD_W))
            gasket.rect(a, YH - 1.8, b - a, 3.6)
        s.fill(gasket, UI_ALT)

        # valves (not sectioned), their springs on the head, and the spark plug
        vp, wire = P(), P()
        for sgn in (-1, 1):
            ax = valve_axis(sgn)
            vp.poly(ax.apply(np.asarray(POPPET) + (0, 1 - RC)), closed=True)
            # spring in section: two staggered columns of cut-wire circles, seat washer below,
            # cap above
            base, top = R_TOP + 13, R_TOP + 68
            for side, phase in ((-1, 0.0), (1, 0.5)):
                for i in range(5):
                    wire.circle(along(sgn, base + 8 + (i + phase) * 9, side * 16), 4.2)
            for rr, w, h in ((base, 24, 3), (top, 23, 6)):
                cap = [(-w, -rr), (w, -rr), (w - 3, -rr - h), (-w + 3, -rr - h)]
                vp.poly(ax.apply(cap), closed=True)
        plug = P()
        plug.M(-11, YH - DOME_H - 2).V(HEAD_TOP).M(11, YH - DOME_H - 2).V(HEAD_TOP)
        plug.M(-22, HEAD_TOP).V(HEAD_TOP - 26).H(22).V(HEAD_TOP).Z()
        plug.M(-13, HEAD_TOP - 26).L(-10, HEAD_TOP - 88).H(10).L(13, HEAD_TOP - 26)
        plug.M(-5, HEAD_TOP - 88).V(HEAD_TOP - 102).H(5).V(HEAD_TOP - 88)
        for j in range(1, 9):  # thread
            y = YH - DOME_H - 2 - j * (YH - DOME_H - 2 - HEAD_TOP) / 9
            plug.M(-11, y).L(-6, y - 4).M(11, y).L(6, y - 4)
        s.path(vp, fill=BG, stroke=UI_HI, stroke_width=1.8)
        s.path(wire, fill=UI, stroke=UI_HI, stroke_width=1.5)
        s.stroke(plug, UI_HI, 1.8)

        # dimensions: stroke on the right, head width across the top
        ext, dl, heads = P(), P(), P()
        xd, bdc = 300, CROWN_TDC + 2 * THROW
        for y in (CROWN_TDC, bdc):
            ext.M(FIN + 14, y).H(xd + 14)
        dl.M(xd, CROWN_TDC).V(bdc)
        heads.arrowhead((xd, CROWN_TDC), 13, bearing=0, width=4.2)
        heads.arrowhead((xd, bdc), 13, bearing=180, width=4.2)
        yd = HEAD_TOP - 132
        for sgn in (-1, 1):
            ext.M(sgn * HEAD_W, HEAD_TOP - 12).V(yd - 14)
            heads.arrowhead((sgn * HEAD_W, yd), 13, bearing=sgn * 90, width=4.2)
        dl.M(-HEAD_W, yd).H(HEAD_W)
        s.stroke(ext, UI, 1.3)
        s.stroke(dl, UI_ALT, 1.3)
        s.fill(heads, UI_ALT)

        # BDC phantom of the crown, so the stroke dimension's lower extension lands on something
        s.stroke(P().M(-BORE + 4, bdc).H(BORE - 4), UI_ALT, 1.5, dash=PHANTOM)

        # combustion chamber: one glow of stipple, dense at the plug tip and dying out at the
        # dome rim
        spark = Vec(0, YH - DOME_H + 6)
        x0, y0, x1, y1 = chamber.bounds
        rng = s.rng(5)
        pts = poisson_disk(Rect(x0, y0, x1 - x0, y1 - y0), 2.7, rng)
        inside = shapely.contains_xy(chamber.buffer(-3.5), pts[:, 0], pts[:, 1])
        with s.buckets((ACCENT_1, ACCENT), "fill") as glow:
            for (x, y), ok in zip(pts.tolist(), inside.tolist(), strict=True):
                if not ok:
                    continue
                d = math.hypot(x - spark.x, (y - spark.y) * 1.25)
                if rng.random() > 0.012 + 0.988 * math.exp(-((d / 30) ** 1.8)):
                    continue
                glow[int(rng.random() < math.exp(-d / 45))].circle((x, y), 1.4)
        # lit only along the dome; the band down to the crown stays plain liner
        rim = P().arc((0, DOME_C), RC, bearing=(-DOME_HALF, DOME_HALF))
        s.stroke(rim, ACCENT, 2.2, cap="round")

Run it yourself

$ git clone https://github.com/nickolaj-jepsen/walldye && cd walldye$ uv run walldye render engine-section --theme fireproof -o engine-section-fireproof-16x9.svg