Awesome
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.
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"))
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"))
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"))
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)))
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"))
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)))
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"))
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))))
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)))
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)))
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"))
Using Extrusions
You can access extrusion models using get-model
.
(m/difference (get-model extrusion :body)
(get-model extrusion :mask))