Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better Loft #2

Merged
merged 3 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

:clj-dev
{:extra-deps {badigeon/badigeon {:mvn/version "1.7"}
org.clojars.cartesiantheatrics/manifold3d$linux-x86_64 {:mvn/version "1.0.78"}
org.clojars.cartesiantheatrics/manifold3d$linux-x86_64 {:mvn/version "1.0.79"}
;; org.clojars.cartesiantheatrics/manifold3d {:local/root "../manifold/bindings/java/target/manifold3d-1.0.39.jar"}
}
:extra-paths ["build" "test/cljc" "examples/"]}
Expand Down
44 changes: 33 additions & 11 deletions src/cljc/clj_manifold3d/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
Working with promises can be pretty annoying, especially without a type system that supports
them well. For this reason, the CLJS API generally also works on non-promise objects."
#?(:clj (:import
[manifold3d Manifold MeshUtils ManifoldVector]
[manifold3d Manifold MeshUtils MeshUtils$LoftAlgorithm ManifoldVector]
[manifold3d.pub DoubleMesh SmoothnessVector Smoothness SimplePolygon Polygons PolygonsVector OpType]
[manifold3d.manifold CrossSection CrossSectionVector Material ExportOptions MeshIO]
[manifold3d.glm DoubleVec3 DoubleVec2 DoubleMat4x3 DoubleMat3x2 IntegerVec4Vector DoubleMat4x3Vector
Expand Down Expand Up @@ -512,8 +512,8 @@ to the interpolated surface according to their barycentric coordinates."
(defn get-properties
([manifold]
(let [props (.getProperties ^Manifold manifold)]
{:surface-area (.surfaceArea props)
:volume (.volume props)}))))
{:surface-area (double (.surfaceArea props))
:volume (double (.volume props))}))))

(defn trim-by-plane
"Subtracts the halfspace defined by the `normal`, offset by `origin-offset` in the direction of the normal."
Expand Down Expand Up @@ -815,13 +815,29 @@ to the interpolated surface according to their barycentric coordinates."
(doseq [poly (if (number? (ffirst x)) [x] x)]
(.pushBack pv (SimplePolygon/FromBuffer (double-vec2-sequence-to-native-double-buffer poly))))
pv))))
#?(:clj
(defn- loft-algorithm->enum [name]
(case name
:eager-nearest-neighbor MeshUtils$LoftAlgorithm/EagerNearestNeighbor
:isomorphic MeshUtils$LoftAlgorithm/Isomorphic)))

#?(:clj
(defn loft
"Loft between isomorphic cross sections transformed by the associated 3D transform frames.
If a single cross-section is provided, lofts between copies of the cross section. cross-sections
do not need to be unique objects. The order and number of polygons and polygon vertices must be
equivalent for all cross-sections.
"Loft between cross sections transformed by the associated 3D transform frames.
If a single cross-section is provided, lofts between the same cross section at each frame position.
It also accepts a sequence of cross-sections, or a \"decomposed\" sequence of cross-sections,
i.e. a vector of vectors of polygons (which are vectors of points). Note there is a cannonical polygon
set for every cross section, with holes encoded by winding order.

There is also an optional `algorithm` argument. Options are :eager-nearest-neighbor (default) and
:isomorphic. :eager-nearest-neighbor cnostructs edges by eagerly adding the edge of minimum distance as it
zips around consecutive polygons. It can handle many cases of many-to-one and one-to-many vertex mappings.
However, all lofted cross sections must decompose into equal numbers of polygons. :isomorphic is the simplest and
maps vertices of consecutive polygons one-to-one. It requires that the order and number of polygons and polygon
vertices is equivalent for all cross-sections. It is slightly faster.

Note loft is *not* guaranteed to be well-defined for all input combinations. Users should roughy understand
how the the \"skinning\" of cross-sections works for a given algorithm type.

ex.
(let [c (difference (square 10 10 true) (square 8 8 true))]
Expand All @@ -841,7 +857,8 @@ to the interpolated surface according to their barycentric coordinates."

It also has a single arity operation that accepts a vector of {:cross-section ... :frame ...} maps.
It will automatically use the \"lastest\" cross-section if one or more is missing. :frame must be
defined in each map.
defined in each map. First map can optionally include a :algorithm keyword to specify the lofting
algorithm.

(loft
(reductions
Expand All @@ -853,23 +870,28 @@ to the interpolated surface according to their barycentric coordinates."
(MatrixTransforms/Yaw (/ 0.2 2)))))
{:cross-section (difference (square 4 4 true)
(square 2 2 true))
:algorithm :earger-nearest-neighbor ;; Optional algorithm specifier in first map.
:frame (frame 1)}
(cons (rem (* 2 Math/PI) 0.2) (range (quot (* 2 Math/PI) 0.2)))))"
([loft-segments]
(let [cv (PolygonsVector.)
fv (DoubleMat4x3Vector.)
last-cross-section (:cross-section (first loft-segments))]
first-seg (first loft-segments)
last-cross-section (:cross-section first-seg)
algorithm (:algorithm first-seg :eager-nearest-neighbor)]
(when (nil? last-cross-section)
(throw (IllegalArgumentException. "First loft segment must contain :cross-section")))
(loop [last-cross-section last-cross-section
[{:keys [frame cross-section] :as segment} & more] loft-segments]
(cond (nil? segment) (MeshUtils/Loft cv fv)
(cond (nil? segment) (MeshUtils/Loft cv fv (loft-algorithm->enum algorithm))
(nil? frame) (throw (IllegalArgumentException. "All loft segments must have :frame"))
:else (let [c (or cross-section last-cross-section)]
(.pushBack cv (>polygons c))
(.pushBack fv frame)
(recur c more))))))
([cross-sections frames]
(loft cross-sections frames :eager-nearest-neighbor))
([cross-sections frames algorithm]
(let [sections (if (cross-section? cross-sections)
cross-sections
(let [cv (PolygonsVector.)]
Expand All @@ -881,7 +903,7 @@ to the interpolated surface according to their barycentric coordinates."
tv (DoubleMat4x3Vector.)]
(doseq [^DoubleMat4x3 t frames]
(.pushBack tv t))
(MeshUtils/Loft sections ^DoubleMat4x3Vector tv)))))
(MeshUtils/Loft sections ^DoubleMat4x3Vector tv (loft-algorithm->enum algorithm))))))

#?(:clj
(defn simplify
Expand Down
56 changes: 54 additions & 2 deletions test/cljc/clj_manifold3d/core_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
[clojure.test :refer [deftest is testing]]
[clojure.java.io :as io]
[clj-manifold3d.core :refer [mesh cube get-mesh manifold get-properties mirror union scale
compose decompose translate get-mesh-gl get-mesh import-mesh
smooth sphere refine cylinder polyhedron export-mesh tetrahedron]]))
compose decompose translate get-mesh-gl get-mesh import-mesh loft
difference smooth sphere refine cylinder polyhedron export-mesh
tetrahedron circle frame rotate square]]))

(defn- glm-to-vectors [v]
(for [i (range (.size v))]
Expand All @@ -17,6 +18,12 @@
([x y eps]
(< (Math/abs (- x y)) eps)))

(defmacro test-props [props m]
`(let [e-props# (get-properties ~m)]
(doseq [[k# x#] e-props#]
(let [y# (get ~props k#)]
(is (about= (double x#) (double y#)))))))

(deftest test-mesh
(let [m (mesh
:tri-verts [[2 0 1] [0 5 1]
Expand Down Expand Up @@ -129,3 +136,48 @@
(:volume))))
(finally
(io/delete-file test-file-name)))))

(deftest test-loft
(test-props
{:surface-area 55850.77734375, :volume 54942.234375}
(loft
(union
(difference (circle 10 40)
(circle 8 40))
(difference (circle 20 40)
(circle 18 40)))
(let [n 30]
(for [i (range (inc n))]
(-> (frame 1)
(rotate [0 (- (* i (/ (/ Math/PI 1) n))) 0])
(translate [50 0 0]))))))

(test-props
{:surface-area 3723.955322265625, :volume 16952.8828125}
(loft [(circle 15 20)
(square 20 20 true)]
[(frame 1) (-> (frame 1) (translate [0 0 30]))]))

(test-props
{:surface-area 3723.955322265625, :volume 16952.8828125}
(loft [(circle 15 20)
(square 20 20 true)]
[(frame 1) (-> (frame 1) (translate [0 0 30]))]
:eager-nearest-neighbor) )

(test-props
{:surface-area 13741.2900390625, :volume 2407.7626953125}
(loft
(reductions
(fn [m _]
(assoc m
:frame (-> (:frame m)
(rotate [0 (/ 0.2 2) (/ Math/PI 6)])
(translate [0 0 10])
(rotate [0 (/ 0.2 2) 0]))))
{:cross-section (difference (square 8 8 true)
(square 6 6 true))
:algorithm :eager-nearest-neighbor
:frame (-> (frame 1)
(rotate [(+ (/ Math/PI 10)) 0 0]))}
(cons (rem (* 1.5 Math/PI) 0.2) (range (quot (* 1.5 Math/PI) 0.2)))))) )
Loading