← All demos

Update! | SetSelf

Brick Wall Pattern

Quantified sequences and pattern repetition: bond rules place every brick, and the quantity takeoff falls out for free.

Parameters

3D View

View
ShadedWireframeHidden line

Quantity Takeoff

Snapped to whole modules: 91.7 in long, 45.6 in tall.

Courses16
Bricks per course11
Full bricks168
Cut half-bricks16
Wall area29.1 sq ft
Mortar volume (est.)1.56 cu ft

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 brick-wall ...)
(define-object brick-wall (base-object)

  :documentation
  (:description "Procedural brick wall: courses and bricks are Gendl
quantified sequences whose sizes derive from the wall and brick
dimensions.  Running bond staggers alternate courses by half a module
and closes each end with a cut half-brick; the quantity takeoff
(courses, full and half bricks, mortar volume) falls out of the same
rules that place the geometry."
   :author "Genworks International")

  :input-slots
  ((wall-length 96 :settable)
   (wall-height 48 :settable)
   (brick-length 8 :settable)
   (brick-height 5/2 :settable)
   (brick-depth 4 :settable)
   (mortar-joint 3/8 :settable)

   ("Keyword symbol, :running or :stack.  The bond pattern."
    bond :running
         :settable))

  :computed-slots
  ((module (+ (the brick-length) (the mortar-joint)))
   (course-module (+ (the brick-height) (the mortar-joint)))

   (bricks-per-course (max 1
                           (floor (+ (the wall-length) (the mortar-joint))
                                  (the module))))

   (number-of-courses (max 1
                           (floor (+ (the wall-height) (the mortar-joint))
                                  (the course-module))))

   (effective-length (- (* (the bricks-per-course) (the module))
                        (the mortar-joint)))

   (effective-height (- (* (the number-of-courses) (the course-module))
                        (the mortar-joint)))

   (half-brick-length (half (- (the brick-length) (the mortar-joint))))

   (offset-course-count (ecase (the bond)
                          (:stack 0)
                          (:running (floor (the number-of-courses) 2))))

   (aligned-course-count (- (the number-of-courses) (the offset-course-count)))

   (full-brick-count (+ (* (the aligned-course-count) (the bricks-per-course))
                        (* (the offset-course-count)
                           (max 0 (1- (the bricks-per-course))))))

   (half-brick-count (* 2 (the offset-course-count)))

   (wall-area-square-feet (/ (* (the effective-length) (the effective-height))
                             144.0))

   (mortar-volume-cubic-feet (let ((wall-volume
                                    (* (the effective-length)
                                       (the effective-height)
                                       (the brick-depth)))
                                   (brick-volume
                                    (+ (*
                                        (the full-brick-count)
                                        (*
                                         (the brick-length)
                                         (the brick-height)
                                         (the brick-depth)))
                                       (*
                                        (the half-brick-count)
                                        (*
                                         (the half-brick-length)
                                         (the brick-height)
                                         (the brick-depth))))))
                               (/ (- wall-volume brick-volume) 1728.0))))

  :objects
  ((mortar-backing :type 'box
                   :display-controls (list :color :grey-light)
                   :width (the effective-length)
                   :height (the effective-height)
                   :length (- (the brick-depth) 1/2)
                   :center (translate (the center) :rear 1/2))

   (courses :type 'wall-course
            :sequence (:size (the number-of-courses))
            :offset? (and (eq (the bond) :running) (oddp (the-child index)))
            :center (translate (the center)
                               :up
                               (+ (* (the-child index) (the course-module))
                                  (- (half (the effective-height)))
                                  (half (the brick-height))))
            :pass-down (brick-length brick-height brick-depth mortar-joint
                        module bricks-per-course half-brick-length
                        effective-length))))
(define-object wall-course ...)
(define-object wall-course (base-object)

  :documentation
  (:description "One course of bricks.  An offset (running-bond) course
starts and ends with a cut half-brick and holds one fewer full brick
than an aligned course."
   :author "Genworks International")

  :input-slots
  (offset?
   brick-length
   brick-height
   brick-depth
   mortar-joint
   module
   bricks-per-course
   half-brick-length
   effective-length)

  :computed-slots
  ((full-brick-count (if (the offset?)
                         (max 0 (1- (the bricks-per-course)))
                         (the bricks-per-course)))

   (start-x (- (half (the effective-length))))
   (brick-shades (list :firebrick :brown :salmon)))

  :objects
  ((bricks :type 'box
           :sequence (:size (the full-brick-count))
           :width (the brick-length)
           :height (the brick-height)
           :length (the brick-depth)
           :display-controls (list :color
                                   (nth (mod
                                         (+
                                          (the-child index)
                                          (the index)
                                          (if (the offset?) 2 0))
                                         (length (the brick-shades)))
                                        (the brick-shades)))
           :center (translate (the center)
                              :right
                              (+ (the start-x)
                                 (if (the offset?)
                                     (+ (the half-brick-length)
                                        (the mortar-joint))
                                     0)
                                 (* (the-child index) (the module))
                                 (half (the brick-length)))))

   (half-bricks :type 'box
                :sequence (:size (if (the offset?) 2 0))
                :width (the half-brick-length)
                :height (the brick-height)
                :length (the brick-depth)
                :display-controls (list :color
                                        (nth
                                         (mod
                                          (+ (the-child index) (the index))
                                          (length (the brick-shades)))
                                         (the brick-shades)))
                :center (translate (the center)
                                   :right
                                   (if (zerop (the-child index))
                                       (+
                                        (the start-x)
                                        (half (the half-brick-length)))
                                       (-
                                        (- (the start-x))
                                        (half (the half-brick-length))))))))
(define-object brick-wall-ui ...)
(define-object brick-wall-ui (demos-common:demo-ui-mixin session-control-mixin
                              base-html-page)

  :documentation
  (:description "Interactive brick-wall demo page: bond pattern and
dimensions drive quantified sequences of courses and bricks, with the
quantity takeoff computed from the same declarative rules."
   :author "Genworks International")

  :input-slots
  ((title "Brick Wall Demo")
   (demo-slug "brick-wall")
   (objects (list (the model)))
   (view-projection-key :wall-view)
   (view-projection-vector (unitize-vector (make-vector 0.45 -1 0.3)))
   (wall-length 96 :settable)
   (wall-height 48 :settable)
   (brick-length 8 :settable)
   (brick-height 2.5 :settable)
   (mortar-joint 0.375 :settable))

  :computed-slots
  ((source-pane-objects '(brick-wall wall-course brick-wall-ui
                          demos-common:demo-ui-mixin))

   (body (with-lhtml-string
           nil
           (:div :class "max-w-5xl mx-auto p-8"
            (str
             (the (page-intro "Brick Wall Pattern"
                   "Quantified sequences and pattern repetition: bond rules place every brick, and the quantity takeoff falls out for free.")))
            (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: 520px;"
                      (str (the viewport-area div)))))))
            (str (the takeoff-section div))
            (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)))))))

   (takeoff-card (the (card :title "Quantity Takeoff" :subtitle
                       (format nil
                               "Snapped to whole modules: ~,1f in long, ~,1f in tall."
                               (float (the model effective-length))
                               (float (the model effective-height)))
                       :content
                       (with-lhtml-string
                         nil
                         (:div :class "overflow-x-auto"
                          (str (the quantities-table)))))))

   (quantities-table (let ((model (the model)))
                       (with-lhtml-string
                         nil
                         (:table :class
                          "min-w-full table-fixed divide-y divide-gray-200"
                          (:tbody :class "divide-y divide-gray-100"
                           (dolist (row
                                    (list (list
                                           "Courses"
                                           (format
                                            nil
                                            "~d"
                                            (the-object
                                             model
                                             number-of-courses)))
                                          (list
                                           "Bricks per course"
                                           (format
                                            nil
                                            "~d"
                                            (the-object
                                             model
                                             bricks-per-course)))
                                          (list
                                           "Full bricks"
                                           (format
                                            nil
                                            "~d"
                                            (the-object
                                             model
                                             full-brick-count)))
                                          (list
                                           "Cut half-bricks"
                                           (format
                                            nil
                                            "~d"
                                            (the-object
                                             model
                                             half-brick-count)))
                                          (list
                                           "Wall area"
                                           (format
                                            nil
                                            "~,1f sq ft"
                                            (the-object
                                             model
                                             wall-area-square-feet)))
                                          (list
                                           "Mortar volume (est.)"
                                           (format
                                            nil
                                            "~,2f cu ft"
                                            (the-object
                                             model
                                             mortar-volume-cubic-feet)))))
                             (htm
                              (:tr :class "hover:bg-gray-50"
                               (:td :class
                                "px-6 py-3 text-sm font-medium text-gray-900"
                                (str (first row)))
                               (:td :class
                                "px-6 py-3 w-40 text-sm text-right tabular-nums text-gray-700"
                                (str (second row)))))))))))

   (control-form (with-form-string
                   (:class "grid grid-cols-2 md:grid-cols-4 gap-6")
                   (str
                    (the (param-field "Wall Length (in)"
                          (the wall-length-control))))
                   (str
                    (the (param-field "Wall Height (in)"
                          (the wall-height-control))))
                   (str
                    (the (param-field "Brick Length (in)"
                          (the brick-length-control))))
                   (str
                    (the (param-field "Brick Height (in)"
                          (the brick-height-control))))
                   (str
                    (the (param-field "Mortar Joint (in)"
                          (the mortar-joint-control))))
                   (str (the (param-field "Bond Pattern" (the bond-control)))))))

  :objects
  ((wall-length-control :type 'number-form-control
                        :default (the wall-length)
                        :ajax-submit-on-change? t
                        :domain :number
                        :min 24
                        :max 240
                        :step 8)

   (wall-height-control :type 'number-form-control
                        :default (the wall-height)
                        :ajax-submit-on-change? t
                        :domain :number
                        :min 12
                        :max 96
                        :step 4)

   (brick-length-control :type 'number-form-control
                         :default (the brick-length)
                         :ajax-submit-on-change? t
                         :domain :number
                         :min 6
                         :max 16
                         :step 1)

   (brick-height-control :type 'number-form-control
                         :default (the brick-height)
                         :ajax-submit-on-change? t
                         :domain :number
                         :min 2
                         :max 5
                         :step 0.25)

   (mortar-joint-control :type 'number-form-control
                         :default (the mortar-joint)
                         :ajax-submit-on-change? t
                         :domain :number
                         :min 0.25
                         :max 1
                         :step 0.125)

   (bond-control :type 'menu-form-control
                 :default :running
                 :size 1
                 :ajax-submit-on-change? t
                 :choice-plist '(:running "Running" :stack "Stack"))

   (model :type 'brick-wall
          :wall-length (the wall-length-control value)
          :wall-height (the wall-height-control value)
          :brick-length (the brick-length-control value)
          :brick-height (the brick-height-control value)
          :mortar-joint (the mortar-joint-control value)
          :bond (the bond-control value))

   (takeoff-section :type 'base-html-div :inner-html (the takeoff-card))))
(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)))))))