Home

Awesome

Clojars Project

Plexus

Plexus is a simple tool for defining cross sections and extruding them in a piece-wise fashion using egocentric reference frames.

You can specify 3D models by providing a series of extrusions to a set of cross sections, which are then composited using CSG operations.

This library is built on top of clj-manifold3d, a traditional CSG library in the style of OpenSCAD.

Try it Out

A beginner friendly introduction to developing with Plexus is available here.

DevEnv

Install

This library uses java bindings to the native library Manifold. The platform specific dependency needs to be included separately from plexus.

Projects Using Plexus

A Simple rapidly printable hydroponic tower: https://github.com/SovereignShop/spiralized-hydroponic-tower

Kossel delta printer: https://github.com/SovereignShop/kossel-printer/

Examples

In the following example, our outer cross section is a circle with radius of 6. The mask cross section is a circle of radius 4. We then specify a series of egocentric transformations to the outer and inner cross sections.

(require
 '[clj-manifold3d.core :as m]
 '[plexus.core
   :refer [result frame left right forward up down hull extrude set branch
           rotate translate difference union intersection points export insert
           loft trim-by-plane offset]])

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 20 :to [:body]) (set :curve-radius 20 :to [:mask])

     (left :angle (/ Math/PI 2) :to [:body])
     (left :angle (/ Math/PI 2) :to [:mask])

     (right :angle (/ Math/PI 2) :to [:body])
     (right :angle (/ Math/PI 2) :to [:mask])

     (forward :length 10 :to [:body])
     (forward :length 10 :to [:mask])

     (up :angle (/ Math/PI 2) :to [:body])
     (up :angle (/ Math/PI 2) :to [:mask]))
    (export "test.glb"))

Obviously there is a lot of code duplication here. After providing the cross section for the inner and outer forms, the transformations we apply to each are equivalent. We can get rid of that duplication by only providing one transforming both cross sections with each segment:

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 20 :to [:body :mask])

     (left :angle (/ Math/PI 2) :to [:body :mask])
     (right :angle (/ Math/PI 2) :to [:body :mask])
     (forward :length 10 :to [:body :mask])
     (up :angle (/ Math/PI 2) :to [:body :mask]))
    (export "pipes.glb"))

Pipe Example

This is equivalent to the one above, but we can still see there is a lot of duplication. The :to [:outer :inner] is repeated in each segment. We can elide this, as by default each segment will reply to every frame you have defined:

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 20)

     (left :angle (/ Math/PI 2))
     (right :angle (/ Math/PI 2))
     (forward :length 10)
     (up :angle (/ Math/PI 2)))
    (export "pipes.glb"))

This extrude is equivalent to the one above.

Hulls

Hulls are often a great way to transform between cross sections. You can wrap any sequence of extrusions in a hull form to make a convex hull out of those segments:

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 20)
     (hull
      (hull
       (forward :length 20)
       (set :cross-section (m/square 20 20 true) :to [:body])
       (set :cross-section (m/square 16 16 true) :to [:mask])
       (forward :length 20))
      (set :cross-section (m/circle 6) :to [:body])
      (set :cross-section (m/circle 4) :to [:mask])
      (forward :length 20)))
    (export "hull.glb"))

Hull Example

Lofts

You can loft between a sequence of cross-sections with loft. Edges are constructed between corresponding vertices of each cross-section.

(-> (extrude
     (result :name :pipes :expr :body)
     (frame :cross-section (m/difference (m/circle 20) (m/circle 18)) :name :body)
     (loft
      (forward :length 1)
      (for [i (range 3)]
        [(translate :x 8)
         (forward :length 20)
         (translate :x -8)
         (forward :length 20)])))
    (export "loft.glb"))

Loft Example

Lofted sections don't need to be isomorphic.

(-> (extrude
     (result :name :pipes :expr :body)
     (frame :cross-section (m/difference (m/circle 20) (m/circle 18)) :name :body)
     (loft
      (forward :length 1)
      (for [i (range 3)]
        [(translate :x 8)
         (set :cross-section (m/difference (m/square 30 30 true) (m/square 26 26 true)))
         (forward :length 20)
         (translate :x -8)
         (set :cross-section (m/difference (m/circle 20) (m/circle 18)))
         (forward :length 20)])))
    (export "monomorphic-loft.glb" (m/material :color [0 0.7 0.7 1.0] :metalness 0.2)))

Loft Example 2

Branching

Branches work as you'd expect.

(def pi|2 (/ Math/PI 2))

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 10)

     (branch :from :body (left :angle pi|2) (right :angle pi|2) (forward :length 20))
     (branch :from :body (right :angle pi|2) (left :angle pi|2) (forward :length 20)))
    (export "branch.glb"))

Branching Example

The body of the branch is just another extrude. The required ":from" property determines the starting coordinate frame of the branch. There's also an optional :with parameter that specifies which frames to include in the branch.

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :cross-section (m/circle 6) :name :body)
     (frame :cross-section (m/circle 4) :name :mask)
     (set :curve-radius 10)

     (branch :from :body (left :angle pi|2) (right :angle pi|2) (forward :length 20))
     (branch
      :from :body
      :with [:body]
      (right :angle pi|2)
      (left :angle pi|2)
      (forward :length 20)))
    (export "branch-with.glb" (m/material :color [0. 0.7 0.7 1.0] :metalness 0.2)))

Branching Example

Note that you can introduce new frames at any point. The starting transform of any new frame is inherited from the previously introduced frame.

Gaps

You can make any segment a gap with the :gap parameter:

(-> (extrude
     (frame :cross-section (m/circle 6) :name :body :curve-radius 10)
     (for [i (range 3)]
       [(left :angle (/ Math/PI 2) :gap true)
        (right :angle (/ Math/PI 2))]))
    (export "gaps.glb"))

Gap Example

You can also specify which subset of active frames should be a gap by supplying a vector frame names.

(-> (extrude
     (frame :cross-section (m/circle 6) :name :body :curve-radius 10)
     (for [i (range 3)]
       [(left :angle (/ Math/PI 2) :gap [:body])
        (right :angle (/ Math/PI 2))]))
    (export "gaps.glb"))

This is equivalent to above. :gap true is equivalent to gapping all active frames.

Insert

The easiest way to compose extrusions is with insert.

(let [pipe (extrude
            (frame :cross-section (m/circle 6) :name :outer :curve-radius 10)
            (frame :cross-section (m/circle 4) :name :inner)
            (forward :length 30))]
  (-> (extrude
       (result :name :pipes
               :expr (->> (difference :pipe/outer :pipe/inner)
                          (trim-by-plane :normal [-1 0 0])
                          (translate :z 30)))
       (frame :name :origin)

       (for [i (range 4)]
         (branch
          :from :origin
          (rotate :x (* i 1/2 Math/PI))
          (insert :extrusion pipe
                  :models [:outer :inner]
                  :ns :pipe
                  :end-frame :outer))))
      (export "insert.glb" (m/material :color [0 0.7 0.7 1.0] :metalness 0.2))))

Insert Example

Here we're inserting the models :outer and :inner at four different locations. We're also namespacing the inserted models with :pipe. :end-frame specifies the frame to continue on in the next segment.

Translate

You can "move" without extruding using translate.

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :name :body :cross-section (m/circle 6))
     (frame :name :mask :cross-section (m/circle 4))

     (forward :length 10)
     (translate :x 5 :z 10)
     (forward :length 10))
    (export "translate.glb" (m/material :color [0 0.7 0.7 1.0] :metalness 0.2)))

Translate Example

Rotate

You can rotate in place without extruding using rotate.

(-> (extrude
     (result :name :pipes
             :expr (difference :body :mask))

     (frame :name :body :cross-section (m/circle 6))
     (frame :name :mask :cross-section (m/circle 4))

     (forward :length 15)
     (rotate :x (/ Math/PI 2))
     (forward :length 15))
    (export "rotate.glb" (m/material :color [0 0.7 0.7 1.0] :metalness 0.2)))

Rotate Example

Result

Result expressions have been demonstrated in every example so far. As you can see, they represent an arbitrarily nested CSG expression. Result expressions can reference other results by name. the set of operations that can be appear in result expressions are: union, difference, intersection, hull, translate, rotate, mirror, and trim-by-plane.

Points

points is similar to extrude except you use it to define 2D polygons. Here's an example of how to define a circle.

(-> (m/cross-section
     (points
      :axes [:x :z]
      (frame :name :origin)
      (translate :x 50)
      (left :angle (* 2 Math/PI) :curve-radius 50 :cs 20)))
    (m/extrude 1)
    (export "circle.glb"))

Points Example

Using Extrusions

You can access extrusion models using get-model.

(m/difference (get-model extrusion :body) 
              (get-model extrusion :mask))