Can *probably* convert MicroWorld to STL

Code is written. It isn't tested.
This commit is contained in:
Simon Brooke 2024-04-12 22:29:16 +01:00
parent a0b2b93f6b
commit 4187b52c66
6 changed files with 102820 additions and 66411 deletions

2
.gitignore vendored
View file

@ -10,6 +10,8 @@ pom.xml.asc
*.svg
/.lein-*
/.nrepl-port
.clj-kondo
.lsp
.hgignore
.hg/

View file

@ -15,6 +15,9 @@
[hiccup "1.0.5"]
[macroz/search "0.3.0"]
[me.raynes/fs "1.4.6"]
[mw-engine "0.3.0-SNAPSHOT"]
[net.mikera/core.matrix "0.63.0"]
[net.mikera/vectorz-clj "0.48.0"]
[smee/binary "0.5.5"]]
:deploy-repositories [["releases" :clojars]
["snapshots" :clojars]]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,50 @@
(ns cc.journeyman.walkmap.mw-stl
"Convert from Microworld to STL format"
(:require [cc.journeyman.walkmap.ocean :refer [cull-ocean-facets]]
[cc.journeyman.walkmap.polygon :refer [polygon]]
[cc.journeyman.walkmap.vertex :refer [vertex]]
[mw-engine.utils :refer [get-cell map-world]]))
(defn mean-altitude
[cells]
(let [c (remove nil? cells)]
(if (= (count c) 1)
(:altitude (first c))
(/ (reduce + (map :altitude c)) (count c)))))
(defn cell->facets
"Convert a cell into facets.
All facets in an STL file must be triangles, so each MicroWorld cell needs
to be split into two. I am not at present seeing a rational rule for
splitting cells, so at this stage I'm going to do it randomly to prevent
parallel ridge artifacts appearing in the output.
This function is designed to be used with `mw-engint.utils/map-world`, q.v."
[world cell]
(let [;; bounds
n (:y cell)
e (:x cell)
s (inc n)
w (inc e)
;; corner altitudes
nea (mean-altitude [cell (get-cell world (dec n) (dec e))])
nwa (mean-altitude [cell (get-cell world (dec n) (inc e))])
swa (mean-altitude [cell (get-cell world (inc n) (inc e))])
sea (mean-altitude [cell (get-cell world (inc n) (dec e))])]
(if (rand-nth [true false])
[(polygon (vertex n e nea) (vertex n w nwa) (vertex s e sea))
(polygon (vertex n w nwa) (vertex s e sea) (vertex s w swa))]
[(polygon (vertex n e nea) (vertex n w nwa) (vertex s w sea))
(polygon (vertex n e nwa) (vertex s e sea) (vertex s w swa))])))
(defn mw->stl
"Return an STL structure representing the topology of this MicroWorld world
`mw`. If `title` is supplied use that as the title of the STL structure."
([mw]
(mw->stl mw nil))
([mw title]
(let [facets (cull-ocean-facets (flatten (map-world mw cell->facets)))
stl {:facets facets
:count (count facets)}]
(if title (assoc stl :title title) stl))))

View file

@ -4,9 +4,9 @@
[cc.journeyman.walkmap.edge :as e]
[cc.journeyman.walkmap.tag :as t]
[cc.journeyman.walkmap.utils :refer [check-kind-type
check-kind-type-seq
kind-type
not-yet-implemented]]
check-kind-type-seq
kind-type
not-yet-implemented]]
[cc.journeyman.walkmap.vertex :refer [check-vertex check-vertices vertex vertex?]]))
(defn polygon?
@ -15,13 +15,13 @@
of vertices."
[o]
(let
[v (:vertices o)]
[v (:vertices o)]
(and
(coll? v)
(> (count v) 2)
(every? vertex? v)
(:walkmap.id/id o)
(or (nil? (:kind o)) (= (:kind o) :polygon)))))
(coll? v)
(> (count v) 2)
(every? vertex? v)
(:walkmap.id/id o)
(or (nil? (:kind o)) (= (:kind o) :polygon)))))
(defmacro check-polygon
"If `o` is not a polygon, throw an `IllegalArgumentException` with an
@ -42,8 +42,8 @@
polygon with exactly three vertices."
[o]
(and
(coll? o)
(= (count (:vertices o)) 3)))
(coll? o)
(= (count (:vertices o)) 3)))
(defmacro check-triangle
"If `o` is not a triangle, throw an `IllegalArgumentException` with an
@ -52,16 +52,38 @@
[o]
`(check-kind-type ~o triangle? :triangle))
(def recognised-polygon-types #{:triangle
:quadrilateral
:pentagon
:hexagon
:heptagon
:octogon
:polygon})
(defn- poly-kind
"Return a keyword representing the kind of polygon which has this many
`vertices`."
[vertices]
(case (count vertices)
3 :triangle
4 :quadrilateral
5 :pentagon
6 :hexagon
7 :heptagon
8 :octogon
;;else
:polygon))
(defn polygon
"Return a polygon constructed from these `vertices`."
[& vertices]
(if
(> (count vertices) 2)
(> (count vertices) 2)
{:vertices (check-vertices vertices)
:walkmap.id/id (keyword (gensym "poly"))
:kind :polygon}
:kind (poly-kind vertices)}
(throw (IllegalArgumentException.
"A polygon must have at least 3 vertices."))))
"A polygon must have at least 3 vertices."))))
(defn rectangle
"Return a rectangle, with edges aligned east-west and north-south, whose
@ -78,15 +100,15 @@
(/ (reduce + (map #(or (:z %) 0) [vsw vne])) 2))
height-order (sort-by :z [vsw vne])]
(t/tag
(assoc
(polygon vsw vnw vne vse)
:gradient
(e/unit-vector (e/edge (first height-order) (last height-order)))
:centre
(vertex (+ (:x vsw) (/ (- (:x vne) (:x vsw)) 2))
(+ (:x vsw) (/ (- (:y vne) (:y vsw)) 2))
(:z vse)))
:rectangle)))
(assoc
(polygon vsw vnw vne vse)
:gradient
(e/unit-vector (e/edge (first height-order) (last height-order)))
:centre
(vertex (+ (:x vsw) (/ (- (:x vne) (:x vsw)) 2))
(+ (:y vsw) (/ (- (:y vne) (:y vsw)) 2))
(:z vse)))
:rectangle)))
;; (rectangle (vertex 1 2 3) (vertex 7 9 4))
@ -98,7 +120,7 @@
(:vertices (check-triangle triangle)))
highest (first order)
lowest (last order)]
(assoc triangle :gradient (e/unit-vector (e/edge lowest highest)))))
(assoc triangle :gradient (e/unit-vector (e/edge lowest highest)))))
(defn triangle-centre
"Return a canonicalised `facet` (i.e. a triangular polygon) with an added
@ -111,13 +133,13 @@
v1 (first vs)
opposite (e/edge (nth vs 1) (nth vs 2))
oc (e/centre opposite)]
(assoc
facet
:centre
(vertex
(+ (:x v1) (* (- (:x oc) (:x v1)) 2/3))
(+ (:y v1) (* (- (:y oc) (:y v1)) 2/3))
(+ (:z v1) (* (- (:z oc) (:z v1)) 2/3))))))
(assoc
facet
:centre
(vertex
(+ (:x v1) (* (- (:x oc) (:x v1)) 2/3))
(+ (:y v1) (* (- (:y oc) (:y v1)) 2/3))
(+ (:z v1) (* (- (:z oc) (:z v1)) 2/3))))))
(defn centre
[poly]
@ -125,8 +147,8 @@
3 (triangle-centre poly)
;; else
(throw
(UnsupportedOperationException.
"The general case of centre for polygons is not yet implemented."))))
(UnsupportedOperationException.
"The general case of centre for polygons is not yet implemented."))))
(defmacro on2dtriangle?
"Is the projection of this `vertex` on the x, y plane within the
@ -141,15 +163,15 @@
(let [xo (sort-by :x (:vertices rectangle))
yo (sort-by :y (:vertices rectangle))]
(and
(< (:x (first xo)) (:x vertex) (:x (last xo)))
(< (:y (first yo)) (:y vertex) (:y (last yo))))))
(< (:x (first xo)) (:x vertex) (:x (last xo)))
(< (:y (first yo)) (:y vertex) (:y (last yo))))))
(defmacro on2d?
"Is the projection of this `vertex` on the x, y plane within the
projection of this polygon `poly` on that plane?"
[vertex poly]
`(cond
(rectangle? ~poly) (on2drectangle? ~vertex ~poly)
(triangle? ~poly) (on2dtriangle? ~vertex ~poly)
:else
(not-yet-implemented "general case of on2d? for polygons.")))
(rectangle? ~poly) (on2drectangle? ~vertex ~poly)
(triangle? ~poly) (on2dtriangle? ~vertex ~poly)
:else
(not-yet-implemented "general case of on2d? for polygons.")))

View file

@ -1,19 +1,23 @@
(ns cc.journeyman.walkmap.stl
"Utility functions dealing with stereolithography (STL) files. Not a stable API yet!"
(:require
[cc.journeyman.walkmap.ocean :refer [ocean?]]
[cc.journeyman.walkmap.polygon :refer [centre gradient polygon?]]
[cc.journeyman.walkmap.superstructure :refer [store]]
[cc.journeyman.walkmap.tag :refer [tag]]
[cc.journeyman.walkmap.utils :refer [truncate]]
[cc.journeyman.walkmap.vertex :as v]
[clojure.lang.io :refer [input-stream]]
[clojure.string :as s]
[me.raynes.fs :refer [base-name split-ext]]
[org.clojars.smee.binary.core :as b]
[taoensso.timbre :refer [debug]])
(:require [cc.journeyman.walkmap.ocean :refer [ocean?]]
[cc.journeyman.walkmap.polygon :refer [centre gradient triangle?]]
[cc.journeyman.walkmap.superstructure :refer [store]]
[cc.journeyman.walkmap.tag :refer [tag]]
[cc.journeyman.walkmap.utils :refer [kind-type truncate]]
[cc.journeyman.walkmap.vertex :as v :refer [vertex]]
[clojure.core.matrix :refer [set-current-implementation sub]]
[clojure.java.io :refer [input-stream]]
[clojure.string :as s]
[me.raynes.fs :refer [base-name split-ext]]
[org.clojars.smee.binary.core :as b]
[taoensso.timbre :refer [debug]])
(:import [clojure.lang Keyword]))
;; We want to use the `[vectorz](https://github.com/mikera/vectorz-clj)`
;; implementation of core.matrix.
(set-current-implementation :vectorz)
(defn stl?
"True if `o` is recogniseable as an STL structure. An STL structure must
have a key `:facets`, whose value must be a sequence of polygons; and
@ -26,27 +30,27 @@
(stl? o false))
([o verify-count?]
(and
(map? o)
(:facets o)
(every? polygon? (:facets o))
(if (:header o) (string? (:header o)) true)
(if (:count o) (integer? (:count o)) true)
(or (nil? (:kind o)) (= (:kind o) :stl))
(if verify-count? (= (:count o) (count (:facets o))) true))))
(map? o)
(:facets o)
(every? triangle? (:facets o))
(if (:header o) (string? (:header o)) true)
(if (:count o) (integer? (:count o)) true)
(or (nil? (:kind o)) (= (:kind o) :stl))
(if verify-count? (= (:count o) (count (:facets o))) true))))
(def vect
"A codec for vectors within a binary STL file."
(b/ordered-map
:x :float-le
:y :float-le
:z :float-le))
:x :float-le
:y :float-le
:z :float-le))
(def facet
"A codec for a facet (triangle) within a binary STL file."
(b/ordered-map
:normal vect
:vertices [vect vect vect]
:abc :ushort-le))
:normal vect
:vertices [vect vect vect]
:abc :ushort-le))
(def binary-stl
"A codec for binary STL files"
@ -55,6 +59,50 @@
:count :uint-le
:facets (b/repeated facet)))
(defn vertex->array
[v]
[(:x v) (:y v) (or (:z v) 0)])
(defn surface-normal
"From https://www.khronos.org/opengl/wiki/Calculating_a_Surface_Normal
```
Begin Function CalculateSurfaceNormal (Input Triangle) Returns Vector
Set Vector U to (Triangle.p2 minus Triangle.p1)
Set Vector V to (Triangle.p3 minus Triangle.p1)
Set Normal.x to (multiply U.y by V.z) minus (multiply U.z by V.y)
Set Normal.y to (multiply U.z by V.x) minus (multiply U.x by V.z)
Set Normal.z to (multiply U.x by V.y) minus (multiply U.y by V.x)
Returning Normal
End Function
```"
[triangle]
(if (triangle? triangle)
(let
[vertices (:vertices triangle)
v1 (vertex->array (nth vertices 0))
u (sub (vertex->array (nth vertices 1)) v1)
v (sub (vertex->array (nth vertices 2)) v1)
x (- (* (nth u 1)(nth v 2)) (* (nth u 2) (nth v 1)))
y (- (* (nth u 2) (nth v 0)) (* (nth u 0) (nth v 2)))
z (- (* (nth u 0) (nth v 1)) (* (nth u 1 (nth v 0))))]
(debug (format "Calculating normal for triangle %s" triangle))
(vertex x y z))
(throw (IllegalArgumentException.
(format "Expected :triangle, found %s" (kind-type triangle))))))
(defn ensure-normal
"Ensure this `triangle` has a normal vector/"
[triangle]
(if (triangle? triangle)
(if (:normal triangle) triangle
(assoc triangle :normal (surface-normal triangle)))
(throw (IllegalArgumentException.
(format "Expected :triangle, found %s" (kind-type triangle))))))
(defn canonicalise
"Objects read in from STL won't have all the keys/values we need them to have.
`o` may be a map (representing a facet or a vertex), or a sequence of such maps;
@ -67,30 +115,34 @@
(canonicalise o map-kind (v/vertex 1 1 1)))
([o ^Keyword map-kind scale-vertex]
(when-not
(keyword? map-kind)
(keyword? map-kind)
(throw (IllegalArgumentException.
(truncate (str "Must be a keyword: " (or map-kind "nil")) 80))))
(truncate (str "Must be a keyword: " (or map-kind "nil")) 80))))
(cond
(and (coll? o) (not (map? o))) (map #(canonicalise % map-kind) o)
;; if it has :facets it's an STL structure, but it doesn't yet conform to `stl?`
(:facets o) (assoc o
:kind :stl
:walkmap.id/id (or (:walkmap.id/id o) (keyword (gensym "stl")))
:facets (canonicalise (:facets o) map-kind))
:kind :stl
:walkmap.id/id (or (:walkmap.id/id o)
(keyword (gensym "stl")))
:facets (canonicalise (:facets o) map-kind))
;; if it's a triangle it's almost a facet. All it needs now is a normal
;; vector
(triangle? o) (ensure-normal o)
;; if it has :vertices it's a polygon, but it may not yet conform to
;; `polygon?`
(:vertices o) (let [f (gradient
(centre
(tag
(assoc o
:walkmap.id/id (or
(:walkmap.id/id o)
(keyword (gensym "poly")))
:kind :polygon
:vertices (canonicalise
(:vertices o)
map-kind))
:facet map-kind)))]
(centre
(tag
(assoc o
:walkmap.id/id (or
(:walkmap.id/id o)
(keyword (gensym "poly")))
:kind :polygon
:vertices (canonicalise
(:vertices o)
map-kind))
:facet map-kind)))]
(if (ocean? f)
(tag f :ocean :no-traversal)
f))
@ -131,7 +183,7 @@
(let [in (input-stream filename)
stl (canonicalise (b/decode binary-stl in) map-kind scale-vertex)]
(if
(map? superstructure)
(map? superstructure)
(store stl superstructure)
stl))))
@ -140,13 +192,13 @@
(defn- facet2str [tri]
(str
(vect->str "facet normal" (:normal tri))
"outer loop\n"
(s/join
(map
#(vect->str "vertex" %)
(:vertices tri)))
"endloop\nendfacet\n"))
(vect->str "facet normal" (:normal tri))
"outer loop\n"
(s/join
(map
#(vect->str "vertex" %)
(:vertices tri)))
"endloop\nendfacet\n"))
(defn stl->ascii
"Return as a string an ASCII rendering of the `stl` structure."
@ -154,17 +206,17 @@
(stl->ascii stl "unknown"))
([stl solidname]
(str
"solid "
solidname
(s/trim (:header stl))
"\n"
(s/join
(map
facet2str
(:facets stl)))
"endsolid "
solidname
"\n")))
"solid "
solidname
(s/trim (:header stl))
"\n"
(s/join
(map
facet2str
(:facets stl)))
"endsolid "
solidname
"\n")))
(defn write-ascii-stl
"Write an `stl` structure as read by `decode-binary-stl` to this
@ -172,13 +224,13 @@
([filename stl]
(let [b (base-name filename true)]
(write-ascii-stl
filename stl
(subs b 0 (or (s/index-of b ".") (count b))))))
filename stl
(subs b 0 (or (s/index-of b ".") (count b))))))
([filename stl solidname]
(debug "Solid name is " solidname)
(spit
filename
(stl->ascii stl solidname))))
filename
(stl->ascii stl solidname))))
(defn binary-stl-to-ascii
"Convert the binary STL file indicated by `in-filename`, and write it to
@ -187,15 +239,15 @@
([in-filename]
(let [[_ ext] (split-ext in-filename)]
(binary-stl-to-ascii
in-filename
(str
(subs
in-filename
0
(or
(s/last-index-of in-filename ".")
(count in-filename)))
".ascii"
ext))))
in-filename
(str
(subs
in-filename
0
(or
(s/last-index-of in-filename ".")
(count in-filename)))
".ascii"
ext))))
([in-filename out-filename]
(write-ascii-stl out-filename (decode-binary-stl in-filename))))