← Ecky CAD/ Chapters

CHAPTER 06 / 06

Chapter 6: Open the gates, check the farm

Welcome to the complete Worm Farm: rake, mushroom house, garden, snap-together tractor, and articulated farmer. Every object uses the same geometry as its own chapter. We move those familiar pieces onto one map.

Arrange the familiar pieces

The articulated worm in front of a mushroom house, rake, assembled tractor, fence and carrot
The articulated worm in front of a mushroom house, rake, assembled tractor, fence and carrot

The complete source is assembled from the chapter files when the book is built. Downloads are self-contained: you do not need a hidden component library. A source check catches an old tractor or rigid worm accidentally left in the farm.

Each part gains an outer translate. All tractor pieces share one placement; both house pieces share another. Their local dimensions and fits stay the same. The worm is longer than the rake because it has nine links at a 23 mm pitch.

The farm starts in assembled inspection mode: print_layout and house_print_layout are off. For printing, start with the individual chapter downloads and the two small joint samples. The scene is not a promise that every posed piece is resting on the bed. There is no exported ground slab.

Peek under the roof

The named view called peek_inside lifts the roof for inspection. A view offset is preview-only: STL and STEP keep their normal geometry. By comparison, translate inside a part and the kit's print-layout switches change exports.

The illustration shows the assembled exported positions. A preview effect must never quietly become extra material in the printed object.

Reference: view.

Count what must stay separate

Before pressing Verify, predict the result:

ObjectConnected pieces
Rake1
House and roof2
Fence and carrot2
Tractor kit11
Worm9
Total25

The farm has twelve named part groups, but 25 connected solids. One group can contain several wheels or links. Names and connected pieces are different counts.

The source checks two things: zero non-manifold mesh edges, and 25 pieces. A broken rivet, detached decoration, or accidentally fused worm link should change that count. Do not “repair” moving parts by joining everything together.

verify writes the question. Rendering builds the geometry. Running model verification measures it. A declared check is not evidence that it ran. Closed meshes and a correct count still do not prove printed fit, wheel friction, or joint strength; keep the results from your small samples too.

Open the complete farm

Start at the part names and their outer placements. The longer component recipes are the same ones you have already inspected.

; Generated by scripts/worm_farm_sources.ts from the chapter models.
(define-component worm-link
  ((number joint-clearance 0.6mm)
   (number joint-yaw-limit 35deg)
   (number joint-pitch-limit 20deg)
   (toggle has-socket true)
   (toggle has-ball true))
  (let* ((ball-radius 4mm)
         (joint-height 5mm)
         (socket-wall 2mm)
         (cavity-radius (+ ball-radius joint-clearance))
         (socket-outer-radius (+ cavity-radius socket-wall))
         (socket-height 12mm)
         (socket-throat-radius 3mm)
         (socket-top-lever (- socket-height joint-height))
         (body-relief-radius (+ joint-clearance
           (* socket-outer-radius (cos (deg->rad joint-pitch-limit)))
           (* socket-top-lever (sin (deg->rad joint-pitch-limit)))))
         (ball-offset 11mm)
         (socket-offset -12mm)
         (neck-start 1.2mm)
         (neck-length (- ball-offset neck-start))
         (neck-width 4.8mm)
         (neck-height 2.8mm)
         (window-bottom 1.6mm)
         (window-top (- socket-height window-bottom))
         (window-height (- window-top window-bottom))
         (window-center (/ (+ window-top window-bottom) 2))
         (window-corner-radius 1mm)
         (neck-motion-clearance 1.2mm)
         (passage-half-width (+ (/ neck-width 2) neck-motion-clearance))
         (ball-bed-pedestal-radius 1.4mm)
         (ball-bed-pedestal-height 1.3mm)
         (ball-bed-flat-height 0.6mm)
         (pedestal-motion-clearance 0.3mm)
         (bottom-throat-radius (+ pedestal-motion-clearance
           (* ball-bed-pedestal-radius (cos (deg->rad joint-pitch-limit)))
           (* joint-height (sin (deg->rad joint-pitch-limit)))))
         (cutter-length 16mm))
    (build
      (shape rounded-body
        (clip-plane
          (translate 0 0 4.8mm (scale 1.15 1.12 1 (sphere 7.2mm)))
          :origin '(0 0 0) :normal '(0 0 1) :keep "positive"))
      (shape body-with-joint-relief
        (if has-ball
          (difference rounded-body
            (translate ball-offset 0 -1mm (cylinder body-relief-radius 16mm)))
          rounded-body))
      (shape socket-housing
        (if has-socket
          (union body-with-joint-relief
            (translate socket-offset 0 0 (cylinder socket-outer-radius socket-height)))
          body-with-joint-relief))
      (shape socket-cavity
        (union
          (translate socket-offset 0 joint-height (sphere cavity-radius))
          (translate socket-offset 0 joint-height
            (cylinder socket-throat-radius socket-height))
          (translate socket-offset 0 -1mm
            (cylinder bottom-throat-radius (+ window-bottom 1mm)))))
      (shape neck-passage-base
        (translate (- cutter-length) 0 window-center
          (rotate 90deg 0 90deg
            (extrude
              (rounded-rect (* passage-half-width 2) window-height window-corner-radius)
              cutter-length))))
      (shape neck-motion-envelope
        (translate socket-offset 0 0
          (apply union
            (map (lambda (yaw) (rotate 0 0 yaw neck-passage-base))
              (linspace (- joint-yaw-limit) joint-yaw-limit 7)))))
      (shape captive-socket
        (if has-socket
          (difference socket-housing socket-cavity neck-motion-envelope)
          socket-housing))
      (shape grounded-ball
        (union
          (clip-plane
            (translate ball-offset 0 joint-height (sphere ball-radius))
            :origin (list 0 0 ball-bed-flat-height) :normal '(0 0 1) :keep "positive")
          (translate ball-offset 0 0
            (cylinder ball-bed-pedestal-radius ball-bed-pedestal-height))))
      (shape central-neck-bridge
        (translate neck-start 0 window-center
          (rotate 90deg 0 90deg
            (extrude (slot-overall neck-width neck-height) neck-length))))
      (result
        (if has-ball
          (union captive-socket central-neck-bridge grounded-ball)
          captive-socket)))))

(define-component tractor-chassis ((number axle-radius 3.2mm) (number body-clearance 0.3mm))
  (let* ((axle-height 5mm)
         (body (box 30mm 50mm 10mm))
         (engine (translate 0 -11mm 10mm (box 22mm 23mm 10mm)))
         (chimney (translate 0 -15mm 20mm (cylinder 2.5mm 12mm)))
         (cabin (translate 0 14mm 10mm
           (difference (box 24mm 18mm 23mm)
             (translate 0 0 3mm (box 26mm 12mm 16mm)))))
         (axle-bores (repeat-compound i 2
           (translate 0 (+ -15mm (* i 30mm)) axle-height
             (rotate 0 90deg 0
               (cylinder (+ axle-radius body-clearance) 34mm :align '(center center center)))))))
    (difference (union body engine chimney cabin) axle-bores)))

(define-component tractor-axle
  ((number axle-radius 3.2mm) (number axle-length 47.2mm)
   (number mouth-radius 1.8mm) (number cavity-radius 2.35mm)
   (number lip-depth 2.2mm) (number socket-depth 5.5mm))
  (difference
    (cylinder axle-radius axle-length)
    (translate 0 0 -1mm (cylinder mouth-radius (+ axle-length 2mm)))
    (translate 0 0 lip-depth (cylinder cavity-radius (- socket-depth lip-depth)))
    (translate 0 0 (- axle-length socket-depth)
      (cylinder cavity-radius (- socket-depth lip-depth)))))

(define-component tractor-wheel
  ((number radius 10mm) (number width 6mm)
   (number axle-radius 3.2mm) (number running-clearance 0.4mm))
  (difference (cylinder radius width)
    (translate 0 0 -1mm
      (cylinder (+ axle-radius running-clearance) (+ width 2mm)))))

(define-component tractor-rivet
  ((number mouth-radius 1.8mm) (number snap-interference 0.25mm)
   (number head-radius 4.8mm) (number head-thickness 1.6mm))
  (let* ((stem-radius (- mouth-radius 0.2mm))
         (barb-radius (+ mouth-radius snap-interference))
         (barb-base 4.3mm)
         (slot-bottom (+ head-thickness 0.8mm)))
    (difference
      (union
        (cylinder head-radius head-thickness)
        (translate 0 0 (- head-thickness 0.2mm) (cylinder stem-radius 3.1mm))
        (translate 0 0 barb-base (cone barb-radius 1mm 1.9mm)))
      (translate 0 0 slot-bottom (box 1.1mm 8mm 5mm)))))

(define-component fence_post ((number x 0mm) (number height 22mm))
  (translate x 0 0
    (union
      (cylinder 2mm height)
      (translate 0 0 height
        (cone 3mm 0mm 5mm)))))

(model
  (meta :title "Worm Farm")
  (params
    (number body-count 7 :label "Body links" :min 2 :max 10 :step 1 :frozen true)
    (number joint-yaw-limit 35deg :label "Side motion envelope" :min 25deg :max 40deg :step 5deg)
    (number joint-pitch-limit 20deg :label "Vertical motion envelope" :min 15deg :max 25deg :step 5deg)
    (number joint-clearance 0.6mm :label "Joint clearance" :min 0.4mm :max 0.8mm :step 0.05mm)
    (number wall 2mm :label "House wall" :min 1.5 :max 3 :step 0.5)
    (number roof_clearance 0.3mm :label "Roof clearance" :min 0.2 :max 0.6 :step 0.1)
    (toggle house_print_layout false :label "House print layout (changes export)")
    (number tooth_count 5 :label "Rake teeth" :min 3 :max 7 :step 1)
    (number wheel_radius 10mm :label "Wheel radius" :min 8 :max 12 :step 1)
    (number wheel_clearance 0.4mm :label "Wheel running clearance" :min 0.2 :max 0.6 :step 0.05)
    (number snap_interference 0.25mm :label "Rivet snap interference" :min 0.1 :max 0.35 :step 0.05)
    (toggle print_layout false :label "Print layout (changes export)")
    (number posts 6 :label "Fence posts" :min 3 :max 9 :step 1))
  (let* ((link-pitch 23mm))
    (part worm-head
      (translate -100mm -35mm 0mm
(build
        (shape head-body
          (worm-link :joint-yaw-limit joint-yaw-limit :joint-pitch-limit joint-pitch-limit :joint-clearance joint-clearance :has-socket false :has-ball true))
        (shape face-eyes
          (apply union (map (lambda (side)
              (translate -4.8mm (* side 3mm) 10.6mm (sphere 1.9mm))) '(-1 1))))
        (shape face-with-nose
          (union head-body face-eyes
            (translate -6.5mm 0 7mm (sphere 2.2mm))))
        (shape pupil-recesses
          (apply union (map (lambda (side)
              (translate -5.3mm (* side 3mm) 11.7mm (cylinder 0.8mm 3mm))) '(-1 1))))
        (shape rounded-horns
          (apply union
            (map (lambda (side)
              (translate -2.8mm (* side 4.8mm) 9mm
                (rotate (* side -10deg) 0 -8deg
                  (union
                    (cone 1.7mm 0.65mm 5.5mm)
                    (translate 0 0 5.3mm (sphere 0.8mm))))))
              '(-1 1))))
        (result (difference (union face-with-nose rounded-horns) pupil-recesses)))))
    (part worm-body-links
      (translate -100mm -35mm 0mm
(translate link-pitch 0 0
        (linear-array body-count link-pitch 0 0
          (worm-link :joint-yaw-limit joint-yaw-limit :joint-pitch-limit joint-pitch-limit :joint-clearance joint-clearance)))))
    (part worm-tail
      (translate -100mm -35mm 0mm
(translate (* (+ body-count 1) link-pitch) 0 0
        (build
          (shape tail-root
            (worm-link :joint-yaw-limit joint-yaw-limit :joint-pitch-limit joint-pitch-limit :joint-clearance joint-clearance :has-ball false))
          (shape tapered-tail-tip
            (clip-plane
              (hull
                (translate 5mm 0 2.4mm (sphere 3.4mm))
                (translate 10mm 0.8mm 1.8mm (sphere 2.7mm))
                (translate 14mm 1.5mm 1.4mm (sphere 2mm)))
              :origin '(0 0 0) :normal '(0 0 1) :keep "positive"))
          (result (union tail-root tapered-tail-tip)))))))

  (part mushroom-house
      (translate -65mm 40mm 0mm
(let* ((outside (cylinder 18mm 28mm))
           (room (translate 0 0 2mm (cylinder (- 18mm wall) 27mm)))
           (doorway (translate 0 -16mm 9mm
             (rotate 90deg 0 0 (cylinder 8.5mm 18mm :align '(center center center))))))
      (difference outside room doorway))))

  (part mushroom-roof
      (translate -65mm 40mm 0mm
(let* ((rim-radius (+ 18mm roof_clearance))
           (skirt-height 3mm)
           (cap (cone 26mm 2mm 26mm))
           (rim-opening (translate 0 0 -1mm (cylinder rim-radius (+ skirt-height 1mm))))
           (inside (translate 0 0 skirt-height (cone rim-radius 0mm rim-radius))))
      (translate (if house_print_layout 52mm 0mm) 0 (if house_print_layout 0mm 25mm)
        (difference cap rim-opening inside)))))

  (part tool-rake
      (translate -20mm 75mm 0mm
(let* ((tooth-length 12mm)
           (tooth-radius 1.3mm)
           (head-height tooth-length)
           (head-radius 2mm)
           (tooth-spacing 4mm)
           (head-length (+ (* (- tooth_count 1) tooth-spacing) (* head-radius 2)))
           (handle (translate 0 0 head-height
             (rotate -35deg 0 0 (cylinder 2mm 44mm))))
           (crossbar (translate 0 0 head-height
             (rotate 0 90deg 0
               (cylinder head-radius head-length :align '(center center center)))))
           (teeth (repeat-union i tooth_count
             (translate (* (- i (/ (- tooth_count 1) 2)) tooth-spacing) 0 0
               (cylinder tooth-radius tooth-length)))))
      (union handle crossbar teeth))))

  (let* ((axle-radius 3.2mm)
         (body-half-width 15mm)
         (wheel-width 6mm)
         (wheel-side-gap 2mm)
         (wheel-end-play 0.6mm)
         (wheel-center (+ body-half-width wheel-side-gap (/ wheel-width 2)))
         (axle-half-length (+ body-half-width wheel-side-gap wheel-width wheel-end-play))
         (axle-length (* 2 axle-half-length))
         (rivet-head-thickness 1.6mm)
         (body-lift (- wheel_radius 5mm)))
    (part tractor-chassis
      (translate 40mm 35mm 0mm
(translate 0 0 (if print_layout 0mm body-lift)
        (tractor-chassis :axle-radius axle-radius))))
    (part tractor-axles
      (translate 40mm 35mm 0mm
(repeat-compound i 2
        (let* ((axle (tractor-axle :axle-radius axle-radius :axle-length axle-length)))
          (if print_layout
            (translate (+ -28mm (* i 56mm)) 46mm 0 axle)
            (translate (- axle-half-length) (+ -15mm (* i 30mm)) wheel_radius
              (rotate 0 90deg 0 axle)))))))
    (part tractor-wheels
      (translate 40mm 35mm 0mm
(repeat-compound side 2
        (repeat-compound i 2
          (let* ((sign (- (* side 2) 1))
                 (wheel (tractor-wheel :radius wheel_radius :width wheel-width
                   :axle-radius axle-radius :running-clearance wheel_clearance)))
            (if print_layout
              (translate (* sign 32mm) (+ -15mm (* i 30mm)) 0 wheel)
              (translate (* sign (- wheel-center (/ wheel-width 2)))
                (+ -15mm (* i 30mm)) wheel_radius
                (rotate 0 (* sign 90deg) 0 wheel))))))))
    (part tractor-rivets
      (translate 40mm 35mm 0mm
(repeat-compound side 2
        (repeat-compound i 2
          (let* ((sign (- (* side 2) 1))
                 (rivet (tractor-rivet :snap-interference snap_interference
                   :head-thickness rivet-head-thickness)))
            (if print_layout
              (translate (+ -18mm (* side 24mm) (* i 12mm)) 46mm 0 rivet)
              (translate (* sign (+ axle-half-length rivet-head-thickness))
                (+ -15mm (* i 30mm)) wheel_radius
                (rotate 0 (* sign -90deg) 0 rivet)))))))))

  (part garden-fence
      (translate -15mm 100mm 0mm
(let* ((post_row
             (repeat-union i posts
               (fence_post :x (* i 9mm))))
           (rail_length (+ (* (- posts 1) 9mm) 4mm))
           (rail_center (/ (* (- posts 1) 9mm) 2))
           (bottom_rail
             (translate rail_center 0 8mm
               (box rail_length 2mm 3mm)))
           (top_rail
             (translate rail_center 0 16mm
               (box rail_length 2mm 3mm))))
      (union post_row bottom_rail top_rail))))

  (part garden-carrot
      (translate -15mm 100mm 0mm
(translate 20mm -12mm 0
      (loft 18mm
        (circle 1mm)
        (regular-polygon 6 5mm)))))
  (view peek_inside (offset-part mushroom-roof 0 0 24mm))
  (verify (tag closed-farm) (metric bad-edges (stl non-manifold-edge-count)) (expect bad-edges (= 0)))
  (verify (tag separate-mechanisms) (metric pieces (stl connected-component-count)) (expect pieces (= 25))))

Download the complete farm

Change one thing and explain it

  1. Choose a change: seven rake teeth, eight fence posts, larger wheels, or a wider joint gap.
  2. Predict what should change and what should stay fixed.
  3. Render, inspect the changed piece, and run verification. Still expect 25 pieces unless you deliberately changed their number.
  4. Open the relevant chapter's print layout and export STL. In your slicer, separate shells when needed; check scale, orientation, bridges, and small gaps. Export STEP only when the rendered artifact offers it.
  5. Print a fit sample before a whole mechanism. Keep the measured setting with your model.

From a prepared repository, the CLI can compile and render the farm:

src-tauri/target/debug/ecky check farm.ecky
src-tauri/target/debug/ecky render --backend native farm.ecky --stl farm.stl --step farm.step --json

These commands do not execute authored runtime verification. Run that separately in the app or a verification harness.

You can now: assemble familiar parts, distinguish print placement from inspection, count moving pieces, and explain what your checks actually establish. For the next project, start with one new idea and one small sample.