Skip to content

Instantly share code, notes, and snippets.

@adam-james-v
Created August 21, 2023 19:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save adam-james-v/bd14903210295c9b5e488d9a176dfa7d to your computer and use it in GitHub Desktop.
Save adam-james-v/bd14903210295c9b5e488d9a176dfa7d to your computer and use it in GitHub Desktop.
Higher Order Functions can be used to capture geometric data and compile to other CAD contexts simultaneously.
(ns example.geom
(:require [clojure.string :as str]))
;; Utils
(defn normalize
"find the unit vector of the given vector `v`."
[v]
(when v
(let [m (Math/sqrt ^double (reduce + (mapv * v v)))]
(mapv / v (repeat m)))))
(def ^:dynamic *rounding-decimal-places*
"The number of decimal places the `round` funciton will round to."
3)
(defn round
"Rounds a non-integer number `num` to `places` decimal places."
([num]
(round num *rounding-decimal-places*))
([num places]
(if places
(let [d (bigdec (Math/pow 10 places))]
(double (/ (Math/round (* (double num) d)) d)))
num)))
(defn distance
"Computes the distance between two points `a` and `b`."
[a b]
(let [v (mapv - b a)
v2 (reduce + (mapv * v v))]
(round (Math/sqrt ^double v2))))
;; Primitives
(defn sphere
"Defines a sphere of radius `r` centered at the origin."
[r]
(fn [p]
(cond
(string? p)
(format "sdSphere(%s, %s)" (str p) (double r))
(every? number? p)
(fn [p]
(- (distance p [0 0 0]) r)))))
(defn box
"Defines a box with dimensions `l`, `w`, and `h` centered at the origin."
[l w h]
(fn [p]
(cond
(string? p)
(format "sdBox(%s, vec3(%s, %s, %s))" (str p) (double l) (double w) (double h))
(every? number? p)
(let [[x y z] p
[lh wh hh] (map #(/ % 2) [l w h])]
(max (- x lh) (- (- lh) x)
(- y wh) (- (- wh) y)
(- z hh) (- (- hh) z))))))
(defn circle
"Defines a circle of radius `r` on the XY plane centered at the origin."
[r]
(fn
[p]
(cond
(string? p) ;; string input means we want the fragmentShader code output
(format "sdCircle(%s, %s)" (str p) (double r))
(every? number? p) ;; vector input means we want the distance pt p is to the contour
(- (distance p (repeat 0)) r))))
(defn extrude
"Defines a 3D shape by 'pulling' the 2D `shape` along the Z axis up to the given height `h`."
[shape h]
(fn [p]
(cond
(string? p)
(format "opExtrude(%s, %s,%s)" (str p) (shape (format "%s.xy" p)) (double h))
(every? number? p)
(let [d (shape (drop-last p))
w (- (Math/abs ^long (- (last p) (/ h 2))) (/ h 2))]
(+ (min (max d w) 0)
(distance [0 0] [(max d 0) (max w 0)]))))))
;; Transforms
(defn translate
"Translates the given shape function `f` by [`x` `y` `z`] or by [`x` `y`] if the shape function is a 2D shape."
[f [x y z :as mv]]
(fn [p]
(cond
(string? p)
(if z
(f (format "opTranslate(%s, vec3(%s, %s, %s))" (str p) (double x) (double y) (double z)))
(f (format "opTranslate(%s, vec2(%s, %s))" (str p) (double x) (double y))))
(every? number? p)
(f (mapv + p )))))
(comment
;; Define some shape
(def my-shape
(-> (circle 20)
(extrude 100)
(translate [10 0 50])))
;; In the repl, you can call my-shape with real data (pts):
(my-shape [0 0 0]) ;=> 0.0
(my-shape [10 10 10]) ;=> -5.858
(my-shape [0 0 120]) ;=> 20.0
;; you can also call it with a string to 'compile' your shape:
(my-shape "p") ;=> "opExtrude(opTranslate(p, vec3(10.0, 0.0, 50.0)), sdCircle(opTranslate(p, vec3(10.0, 0.0, 50.0)).xy, 20.0),100.0)"
;; The same code can emit data or can compile to another context. With the right implementation per primitive and transform, you can compile to many different contexts. As long as you can match the semantics across implementations you're good to go!
;; This technique can fairly easily emit OpenSCAD code, for example.
;; The switching mechanism can be more intelligently designed here, perhaps using Clojure's multimethods, but the technique is quite fun to work with already!
;; I imagine being able to emit s-exprs directly would also be useful for 'compiling' to hylang, or to other lisp dialects too.
;; eg. imagine:
(box 20 30 40) ;=>
`(do
(setv box_gensym_001 (cq.Workplane "XY"))
(setv box_gensym_002 (box_gensym_001.box 20 30 40)))
;; Or whatever the correct code would be. Here you could start to build more advanced compilers to help massage
;; things if the semantics of your target context don't quite line up. You could build up a context map and use information
;; from that to make optimizations, etc.
;; The key 'win' I see doing it this way is that inside your repl you still have something tangible to work with. The function has real utility up front when you pass it proper data -> the math that defines an SDF is actually executed and you can use the function right away!
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment