← All demos

Update! | SetSelf

Robot Assembly

The classic GDL robot example, reborn in the 2026 demo framework -- pose it live.

Parameters

3D View

View
ShadedWireframeHidden line

About This Demo

This robot first appeared in 2002 as the introductory GDL training example: a handful of box primitives composed with declarative positioning and orientation rules.

The geometry definitions below are essentially the 2002 originals. Only the user interface is new -- form-control objects, an x3dom viewport, and session-controlled instances replace the old html-format lens.

Source Code

The complete Gendl source for this demo, reconstituted live from the in-memory object definitions serving this page.

Want to change it? Open this source in a live Emacs and rebuild your own copy of the page.

Hack this demo →
(define-object robot ...)
(define-object robot (base-object)

  :documentation
  (:description "The classic GDL robot: a poseable figure composed of
box primitives with declarative positioning and orientation rules."
   :author "Genworks International")

  :input-slots
  (head-angle
   body-angle
   arm-angle-right
   arm-angle-left
   pincer-distance-right
   pincer-distance-left
   shininess
   transparency
   (torso-type 'box :settable))

  :computed-slots
  ((display-controls (list :color
                           :orange
                           :line-thickness
                           2
                           :transparency
                           (the transparency)
                           :shininess
                           (the shininess))))

  :objects
  ((base :type 'robot-base
         :display-controls (merge-display-controls (list :color :orange))
         :height (* (the height) 0.4)
         :width (* (the width) 0.2)
         :length (* (the length) 0.2)
         :center (translate (the center) :down (* (the height) 0.3)))

   (body :type 'robot-body
         :height (* (the height) 0.6)
         :center (translate (the center) :up (* (the height) 0.2))
         :orientation (alignment :top
                                 (the (face-normal-vector :top))
                                 :right
                                 (rotate-vector-d
                                   (the (face-normal-vector :right))
                                   (the body-angle)
                                   (the (face-normal-vector :top))))
         :pass-down (torso-type head-angle arm-angle-right arm-angle-left
                     pincer-distance-left pincer-distance-right))))
(define-object robot-body ...)
(define-object robot-body (base-object)

  :input-slots
  (head-angle
   arm-angle-left
   arm-angle-right
   pincer-distance-left
   pincer-distance-right
   (torso-type 'box)
   (shoulder-height (* (the torso height) 0.1)))

  :objects
  ((torso :type (the torso-type)
          :height (* (the height) 0.85)
          :width (* (the width) 0.7)
          :radius (half (the-child width))
          :display-controls (merge-display-controls
                              (list :transparency 0 :color :blue))
          :center (translate (the center)
                             :down
                             (- (half (the height)) (half (the-child height)))))

   (head :type 'box
         :height (- (the height) (the torso height))
         :width (* (the width) 0.25)
         :length (half (the length))
         :center (translate (the center)
                            :up
                            (- (half (the height)) (half (the-child height))))
         :display-controls (merge-display-controls
                             (list :color :cyan :transparency 0))
         :orientation (alignment :right
                                 (rotate-vector-d
                                   (the (face-normal-vector :right))
                                   (the head-angle)
                                   (the (face-normal-vector :top)))))

   (arms :type 'robot-arm
         :sequence (:size 2)
         :side (ecase (the-child index) (0 :left) (1 :right))
         :display-controls (merge-display-controls
                             (list :transparency
                                   0
                                   :color
                                   (ecase (the-child index)
                                     (0 :green-spring-medium)
                                     (1 :red))))
         :width (half (- (the width) (the torso width)))
         :length (/ (the length) 3)
         :height (- (the torso height) (twice (the shoulder-height)))
         :center (translate-along-vector
                   (the-child shoulder-point)
                   (the-child (face-normal-vector :bottom))
                   (half (the-child height)))
         :orientation (alignment :bottom
                                 (rotate-vector-d
                                   (the (face-normal-vector :bottom))
                                   (the-child angle)
                                   (the (face-normal-vector :left)))
                                 :right
                                 (the (face-normal-vector :right)))
         :shoulder-point (translate (the torso
                                         (edge-center :top (the-child side)))
                                    (the-child side)
                                    (half (the-child width))
                                    :down
                                    (the shoulder-height))
         :angle (ecase (the-child side)
                  (:left (the arm-angle-left))
                  (:right (the arm-angle-right)))
         :pincer-distance (ecase (the-child side)
                            (:left (the pincer-distance-left))
                            (:right (the pincer-distance-right))))))
(define-object robot-arm ...)
(define-object robot-arm (base-object)

  :input-slots
  (side
   shoulder-point
   angle
   pincer-distance)

  :objects
  ((arm :type 'box)

   (thumb :type 'box
          :width (the hand width)
          :height (the hand height)
          :length (the hand length)
          :center (translate (the hand center) (the side) (the pincer-distance)))

   (hand :type 'box
         :display-controls (merge-display-controls
                             (list :color :pink :transparency 0))
         :center (translate (the center)
                            :down
                            (+ (half (the height)) (half (the-child height)))
                            (the side)
                            (- (- (half (the width)) (half (the-child width)))))
         :height (* (the height) 0.15)
         :width (* (the width) 0.2)
         :length (half (the length)))))
(define-object robot-base ...)
(define-object robot-base (base-object)

  :objects
  ((leg :type 'box
        :height (* 0.9 (the height))
        :center (translate (the center)
                           :up
                           (- (half (the height)) (half (the-child height)))))

   (foot :type 'box
         :height (* 0.1 (the height))
         :width (twice (twice (the width)))
         :length (twice (twice (twice (the length))))
         :center (translate (the center)
                            :down
                            (- (half (the height)) (half (the-child height)))))))
(define-object robot-ui ...)
(define-object robot-ui (demos-common:demo-ui-mixin session-control-mixin
                         base-html-page)

  :documentation
  (:description "Interactive robot demo page: the classic GDL robot
example (2002) hosted in the 2026 demo framework -- pose the robot
live with form controls."
   :author "Genworks International")

  :input-slots
  ((title "Robot Assembly Demo")
   (demo-slug "robot")
   (objects (list (the model)))
   (view-projection-key :robot-view)
   (view-projection-vector (unitize-vector (make-vector 0.7 -1 0.45)))
   (robot-height 10 :settable)
   (robot-width 5 :settable)
   (robot-length 2 :settable)
   (head-angle 0 :settable)
   (body-angle 20 :settable)
   (arm-angle-left 0 :settable)
   (arm-angle-right 0 :settable)
   (pincer-distance-left 0.45 :settable)
   (pincer-distance-right 0.45 :settable))

  :computed-slots
  ((source-pane-objects '(robot robot-body robot-arm robot-base robot-ui
                          demos-common:demo-ui-mixin))

   (body (with-lhtml-string
           nil
           (:div :class "max-w-5xl mx-auto p-8"
            (str
             (the (page-intro "Robot Assembly"
                   "The classic GDL robot example, reborn in the 2026 demo framework -- pose it live.")))
            (str
             (the (card :title "Parameters" :content
                   (with-lhtml-string
                     nil
                     (:div :class "p-6" (str (the control-form)))))))
            (str
             (the (card :title "3D View" :content
                   (with-lhtml-string
                     nil
                     (:div :class "p-6 relative" :style "height: 600px;"
                      (str (the viewport-area div)))))))
            (str
             (the (card :title "About This Demo" :content
                   (with-lhtml-string
                     nil
                     (:div :class "px-6 py-4 text-sm text-gray-700 space-y-2"
                      (:p
                       "This robot first appeared in 2002 as the introductory GDL
         training example: a handful of box primitives composed with declarative
         positioning and orientation rules.")
                      (:p
                       "The geometry definitions below are essentially the 2002
         originals.  Only the user interface is new -- form-control objects, an
         x3dom viewport, and session-controlled instances replace the old
         html-format lens."))))))
            (str
             (the (card :title "Source Code" :subtitle
                   "The complete Gendl source for this demo, reconstituted live from the in-memory object definitions serving this page."
                   :content (the source-code-panes)))))))

   (control-form (with-form-string
                   (:class "grid grid-cols-2 md:grid-cols-4 gap-6")
                   (str (the (param-field "Height" (the height-control))))
                   (str (the (param-field "Width" (the width-control))))
                   (str (the (param-field "Length" (the length-control))))
                   (str (the (param-field "Torso" (the torso-control))))
                   (str
                    (the (param-field "Head Angle (deg)"
                          (the head-angle-control))))
                   (str
                    (the (param-field "Body Angle (deg)"
                          (the body-angle-control))))
                   (str
                    (the (param-field "Left Arm (deg)"
                          (the arm-angle-left-control))))
                   (str
                    (the (param-field "Right Arm (deg)"
                          (the arm-angle-right-control))))
                   (str
                    (the (param-field "Left Grip" (the pincer-left-control))))
                   (str
                    (the (param-field "Right Grip"
                          (the pincer-right-control)))))))

  :objects
  ((height-control :type 'number-form-control
                   :default (the robot-height)
                   :ajax-submit-on-change? t
                   :domain :number
                   :min 4
                   :max 20
                   :step 1)

   (width-control :type 'number-form-control
                  :default (the robot-width)
                  :ajax-submit-on-change? t
                  :domain :number
                  :min 3
                  :max 10
                  :step 0.5)

   (length-control :type 'number-form-control
                   :default (the robot-length)
                   :ajax-submit-on-change? t
                   :domain :number
                   :min 1
                   :max 4
                   :step 0.5)

   (torso-control :type 'menu-form-control
                  :default :box
                  :size 1
                  :ajax-submit-on-change? t
                  :choice-plist '(:box "Box" :cylinder "Cylinder"))

   (head-angle-control :type 'number-form-control
                       :default (the head-angle)
                       :ajax-submit-on-change? t
                       :domain :number
                       :min -90
                       :max 90
                       :step 5)

   (body-angle-control :type 'number-form-control
                       :default (the body-angle)
                       :ajax-submit-on-change? t
                       :domain :number
                       :min -90
                       :max 90
                       :step 5)

   (arm-angle-left-control :type 'number-form-control
                           :default (the arm-angle-left)
                           :ajax-submit-on-change? t
                           :domain :number
                           :min -180
                           :max 180
                           :step 5)

   (arm-angle-right-control :type 'number-form-control
                            :default (the arm-angle-right)
                            :ajax-submit-on-change? t
                            :domain :number
                            :min -180
                            :max 180
                            :step 5)

   (pincer-left-control :type 'number-form-control
                        :default (the pincer-distance-left)
                        :ajax-submit-on-change? t
                        :domain :number
                        :min 0
                        :max 1
                        :step 0.05)

   (pincer-right-control :type 'number-form-control
                         :default (the pincer-distance-right)
                         :ajax-submit-on-change? t
                         :domain :number
                         :min 0
                         :max 1
                         :step 0.05)

   (model :type 'robot
          :height (the height-control value)
          :width (the width-control value)
          :length (the length-control value)
          :torso-type (ecase (the torso-control value)
                        (:box 'box)
                        (:cylinder 'cylinder))
          :head-angle (the head-angle-control value)
          :body-angle (the body-angle-control value)
          :arm-angle-left (the arm-angle-left-control value)
          :arm-angle-right (the arm-angle-right-control value)
          :pincer-distance-left (the pincer-left-control value)
          :pincer-distance-right (the pincer-right-control value)
          :transparency 0
          :shininess 0.8)))
(define-object demos-common:demo-ui-mixin ...)
(define-object demo-ui-mixin nil

  :documentation
  (:description "Chrome shared by every public demo page: Tailwind
stylesheet hookup, card and parameter-field helpers, source-code panes
reconstituted live from the in-memory definitions, and a standard
viewport driven by the demo's objects list -- one viewport-html-div
whose renderer follows graphics-format: shaded (x3dom), wireframe
(svg) or hidden-line (svg through base-view's hidden-line remover),
switched by the toggle the pane wears -- and whose projection is the
demo's own angle or one of Gendl's standard views, chosen from the
View menu beside it."
   :author "Genworks International")

  :input-slots
  (("List of define-object symbols shown in the Source Code card."
    source-pane-objects nil)

   ("List of supporting defun symbols shown after the object panes."
    source-pane-functions nil)

   ("String or nil.  The demo's slug under /demo/ (e.g. \"staircase\"),
which names it to the holodeck; nil renders no Hack-this-demo link."
    demo-slug nil)

   (view-projection-key :demo-view)
   (view-projection-vector (unitize-vector (make-vector 1 -1 0.5)))

   ("Keyword, :x3dom, :svg or :hidden-line.  The viewport's format when the
page opens: shaded (x3dom), wireframe (svg), or hidden-line (svg with the
hidden edges removed).  The toggle on the pane switches it."
    graphics-format-default :x3dom))

  :computed-slots
  ((use-svgpanzoom? t)
   (use-x3dom? t)
   (view-key (the view-control value))

   (view-vector (if (eql (the view-key) (the view-projection-key))
                    (the view-projection-vector)
                    (getf *standard-views* (the view-key))))

   (viewport-toolbar-html (with-lhtml-string
                            nil
                            (:div :class
                             "absolute left-1 top-1 z-10 flex items-center rounded bg-white/80 px-2 py-0.5 text-xs shadow backdrop-blur select-none"
                             (:span :class "text-gray-600" "View")
                             (str (the view-control form-control)))
                            (:div :class
                             "absolute right-1 top-1 z-10 flex overflow-hidden rounded bg-white/80 text-xs shadow backdrop-blur select-none"
                             (dolist (choice
                                      '((:x3dom "Shaded")
                                        (:svg "Wireframe")
                                        (:hidden-line "Hidden line")))
                               (let ((active?
                                      (eql (first choice)
                                           (the graphics-format))))
                                 (htm
                                  (:span :class
                                   (if active?
                                       "px-2 py-0.5 bg-indigo-600 text-white"
                                       "px-2 py-0.5 cursor-pointer text-gray-600 hover:text-indigo-700")
                                   :title
                                   (if active?
                                       "The current graphics format"
                                       "Switch the graphics format")
                                   :onclick
                                   (unless active?
                                     (the (gdl-ajax-call
                                           :function-key
                                           :set-graphics-format!
                                           :arguments
                                           (list (first choice)))))
                                   (str (second choice)))))))))

   (additional-header-content (string-append
                                (with-lhtml-string
                                  nil
                                  (:script
                                   (str (the viewport-renderers-js-baseline))))
                                (call-next-method)
                                (with-lhtml-string
                                  nil
                                  (:link :href
                                   (format nil
                                           "~a/demo/css/demos-style.css"
                                           *url-prefix*)
                                   :rel "stylesheet" :type "text/css")
                                  (:script (str (the busy-overlay-js))))))

   (busy-overlay-js "
(function () {
  var count = 0, overlay = null, nonQuiet = false;
  function ensureOverlay () {
    if (overlay) return overlay;
    overlay = document.createElement('div');
    overlay.id = 'demo-busy-overlay';
    overlay.setAttribute('style',
      'position:fixed;inset:0;z-index:9999;display:none;' +
      'background:rgba(249,250,251,0.55);backdrop-filter:blur(1px);' +
      'transition:opacity 100ms;opacity:0;');
    var pill = document.createElement('div');
    pill.setAttribute('style',
      'position:absolute;top:1.25rem;left:50%;transform:translateX(-50%);' +
      'background:#4f46e5;color:#fff;padding:0.4rem 1rem;' +
      'border-radius:9999px;font:500 0.875rem system-ui,sans-serif;' +
      'box-shadow:0 2px 8px rgba(0,0,0,0.25);');
    pill.textContent = 'Rebuilding\\u2026';
    overlay.appendChild(pill);
    document.body.appendChild(overlay);
    return overlay;
  }
  function show () {
    var el = ensureOverlay();
    if (document.activeElement && document.activeElement.blur)
      document.activeElement.blur();
    el.style.display = 'block';
    requestAnimationFrame(function () { el.style.opacity = '1'; });
  }
  function hide () {
    if (overlay) { overlay.style.opacity = '0'; overlay.style.display = 'none'; }
  }
  var origOpen = XMLHttpRequest.prototype.open;
  XMLHttpRequest.prototype.open = function (method, url) {
    this._gdlAjax = typeof url === 'string' && url.indexOf('/gdlAjax') === 0;
    return origOpen.apply(this, arguments);
  };
  var origSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.send = function () {
    if (this._gdlAjax) {
      count++;
      if (nonQuiet) { show(); nonQuiet = false; }
      this.addEventListener('loadend', function () {
        count--;
        if (count <= 0) { count = 0; hide(); }
      });
    }
    return origSend.apply(this, arguments);
  };
  window.addEventListener('DOMContentLoaded', function () {
    var orig = window.gdlAjax;
    if (!orig) return;
    window.gdlAjax = function (evt, params, asynch, quiet) {
      if (!quiet) nonQuiet = true;
      return orig.apply(this, arguments);
    };
  });
})();
")

   (source-code-panes (apply #'string-append
                             (the (hack-banner))
                             (append (mapcar #'(lambda
                                                (sym)
                                                (the (object-source-pane sym)))
                                             (the source-pane-objects))
                                     (mapcar #'(lambda
                                                (sym)
                                                (the (defun-source-pane sym)))
                                             (the source-pane-functions)))))

   (graphics-format (the graphics-format-default) :settable))

  :objects
  ((view-control :type 'menu-form-control
                 :default (the view-projection-key)
                 :size 1
                 :ajax-submit-on-change? t
                 :class "cursor-pointer text-xs text-gray-700"
                 :style "background:transparent;border:0;padding:0 0 0 0.25rem"
                 :choice-plist (list (the view-projection-key)
                                     "Default"
                                     :trimetric
                                     "Trimetric"
                                     :front
                                     "Front"
                                     :rear
                                     "Rear"
                                     :left
                                     "Left"
                                     :right
                                     "Right"
                                     :top
                                     "Top"
                                     :bottom
                                     "Bottom"))

   (viewport-area :type 'viewport-html-div
                  :image-format (if (eql (the graphics-format) :hidden-line)
                                    :svg
                                    (the graphics-format))
                  :hidden-lines (if (eql (the graphics-format) :hidden-line)
                                    :remove
                                    :draw)
                  :header-html (the viewport-toolbar-html)
                  :projection-key (the view-key)
                  :projection-vector (the view-vector)
                  :display-list-object-roots (the objects)))

  :functions
  ((set-graphics-format! (format) (the (set-slot! :graphics-format format)))

   (page-intro
    (heading blurb)
    (with-lhtml-string
      nil
      (:div :class "mb-4 flex items-center justify-between"
       (:a :href "/demos/index.html" :class
        "text-sm font-medium text-indigo-600 hover:text-indigo-500"
        "&larr; All demos")
       (when *developing?*
         (htm
          (:div :class
           "text-xs text-gray-500 [&_a]:text-indigo-600 [&_a]:font-medium [&_p]:m-0"
           (str (the development-links))))))
      (:div :class "text-center mb-8"
       (:h1 :class "text-4xl font-bold text-gray-900 mb-2" (str heading))
       (:p :class "text-lg text-gray-600" (str blurb)))))

   (card
    (&key title subtitle content)
    (with-lhtml-string
      nil
      (:div :class "bg-white rounded-lg shadow-lg overflow-hidden mb-8"
       (:div :class "px-6 py-4 bg-gray-50 border-b border-gray-200"
        (:h3 :class "text-lg font-medium text-gray-900" (str title))
        (when subtitle
          (htm (:p :class "text-sm text-gray-600" (str subtitle)))))
       (str content))))

   (param-field
    (label control)
    (with-lhtml-string
      nil
      (:div :class "space-y-2"
       (:label :class "block text-sm font-medium text-gray-700" :for
        (the-object control id) (str label))
       (:div :class
        "param-field block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm"
        (str (the-object control form-control))))))

   (hack-banner
    nil
    (if (and *hack-base* (the demo-slug))
        (with-lhtml-string
          nil
          (:div :class
           "px-6 py-3 bg-indigo-50 border-b border-indigo-100 flex items-center justify-between gap-4"
           (:p :class "text-sm text-indigo-900"
            "Want to change it?  Open this source in a live Emacs and rebuild your own copy of the page.")
           (:a :href (format nil "~a/?arg=~a" *hack-base* (the demo-slug))
            :target "_blank" :rel "noopener" :class
            "shrink-0 inline-flex items-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-700"
            "Hack this demo &rarr;")))
        ""))

   (object-source-pane
    (sym)
    (with-lhtml-string
      nil
      (:details :class "border-b border-gray-100"
       (:summary :class
        "px-6 py-3 cursor-pointer select-none font-mono text-sm font-medium text-indigo-700 hover:bg-gray-50"
        (fmt "(define-object ~a ...)" (the (pane-label sym))))
       (:div :class "src-pane"
        (wmd (format nil "```lisp~%~a```" (definition-source-string sym)))))))

   (defun-source-pane
    (sym)
    (let ((source (function-source-string sym)))
      (if source
          (with-lhtml-string
            nil
            (:details :class "border-b border-gray-100"
             (:summary :class
              "px-6 py-3 cursor-pointer select-none font-mono text-sm font-medium text-indigo-700 hover:bg-gray-50"
              (fmt "(defun ~a ...)" (the (pane-label sym))))
             (:div :class "src-pane"
              (wmd (format nil "```lisp~%~a```" source)))))
          "")))

   (pane-label
    (sym)
    (string-downcase
      (if (eq (symbol-package sym) (symbol-package (the type)))
          (symbol-name sym)
          (format nil
                  "~a:~a"
                  (package-name (symbol-package sym))
                  (symbol-name sym)))))))