# walkmap/stl.clj

001 (ns walkmap.stl
002 "Utility functions dealing with stereolithography (STL) files. Not a stable API yet!"
003 (:require [clojure.java.io :as io :refer [file output-stream input-stream]]
004 [clojure.string :as s]
005 [me.raynes.fs :as fs]
006 [org.clojars.smee.binary.core :as b]
007 [taoensso.timbre :as l :refer [info error spy]]
008 [walkmap.edge :as e]
009 [walkmap.polygon :refer [polygon?]]
010 [walkmap.tag :refer [tag]]
011 [walkmap.vertex :as v])
012 (:import org.clojars.smee.binary.core.BinaryIO
013 java.io.DataInput))
014
015 (defn stl?
016 "True if `o` is recogniseable as an STL structure. An STL structure must
017 have a key `:facets`, whose value must be a sequence of polygons; and
018 may have a key `:header` whose value should be a string, and/or a key
019 `:count`, whose value should be a positive integer.
020
021 If `verify-count?` is passed and is not `false`, verify that the value of
022 the `:count` header is equal to the number of facets."
023 ([o]
024 (stl? o false))
025 ([o verify-count?]
026 (and
027 (map? o)
028 (:facets o)
029 (every? polygon? (:facets o))
030 (if (:header o) (string? (:header o)) true)
031 (if (:count o) (integer? (:count o)) true)
032 (or (nil? (:kind o)) (= (:kind o) :stl))
033 (if verify-count? (= (:count o) (count (:facets o))) true))))
034
035 (def vect
036 "A codec for vectors within a binary STL file."
037 (b/ordered-map
038 :x :float-le
039 :y :float-le
040 :z :float-le))
041
042 (def facet
043 "A codec for a facet (triangle) within a binary STL file."
044 (b/ordered-map
045 :normal vect
046 :vertices [vect vect vect]
047 :abc :ushort-le))
048
049 (def binary-stl
050 "A codec for binary STL files"
051 (b/ordered-map
052 :header (b/string "ISO-8859-1" :length 80) ;; for the time being we neither know nor care what's in this.
053 :count :uint-le
054 :facets (b/repeated facet)))
055
056 (defn centre
057 "Return a canonicalised `facet` (i.e. a triangular polygon) with an added
058 key `:centre` whose value represents the centre of this facet in 3
059 dimensions. This only works for triangles, so is here not in
060 `walkmap.polygon`. It is an error (although no exception is currently
061 thrown) if the object past is not a triangular polygon."
062 [facet]
063 (let [vs (:vertices facet)
064 v1 (first vs)
065 opposite (e/edge (nth vs 1) (nth vs 2))
066 oc (e/centre opposite)]
067 (assoc
068 facet
069 :centre
070 (v/vertex
071 (+ (:x v1) (* (- (:x oc) (:x v1)) 2/3))
072 (+ (:y v1) (* (- (:y oc) (:y v1)) 2/3))
073 (+ (:z v1) (* (- (:z oc) (:z v1)) 2/3))))))
074
075 (defn canonicalise
076 "Objects read in from STL won't have all the keys/values we need them to have.
077 `o` may be a map (representing a facet or a vertex), or a sequence of such maps;
078 if it isn't recognised it is at present just returned unchanged. `map-kind`, if
079 passed, must be a keyword indicating the value represented by the `z` axis
080 (defaults to `:height`). It is an error, and an exception will be thrown, if
081 `map-kind` is not a keyword."
082 ([o] (canonicalise o :height))
083 ([o map-kind]
084 (when-not
085 (keyword? map-kind)
086 (throw (IllegalArgumentException.
087 (subs (str "Must be a keyword: " (or map-kind "nil")) 0 80))))
088 (cond
089 (and (coll? o) (not (map? o))) (map #(canonicalise % map-kind) o)
090 ;; if it has :facets it's an STL structure, but it doesn't yet conform to `stl?`
091 (:facets o) (assoc o
092 :kind :stl
093 :id (or (:id o) (keyword (gensym "stl")))
094 :facets (canonicalise (:facets o) map-kind))
095 ;; if it has :vertices it's a polygon, but it doesn't yet conform to `polygon?`
096 (:vertices o) (centre
097 (tag
098 (assoc o
099 :id (or (:id o) (keyword (gensym "poly")))
100 :kind :polygon
101 :vertices (canonicalise (:vertices o) map-kind))
102 :facet map-kind))
103 ;; if it has a value for :x it's a vertex, but it doesn't yet conform to `vertex?`
104 (:x o) (v/canonicalise o)
105 ;; shouldn't happen
106 :else o)))
107
108 (defn decode-binary-stl
109 "Parse a binary STL file from this `filename` and return an STL structure
110 representing its contents. `map-kind`, if passed, must be a keyword
111 indicating the value represented by the `z` axis (defaults to `:height`).
112 It is an error, and an exception will be thrown, if `map-kind` is not a
113 keyword.
114
115 **NOTE** that we've no way of verifying that the input file is binary STL
116 data, if it is not this will run but will return garbage."
117 ([filename]
118 (decode-binary-stl filename :height))
119 ([filename map-kind]
120 (when-not
121 (keyword? map-kind)
122 (throw (IllegalArgumentException.
123 (subs (str "Must be a keyword: " (or map-kind "nil")) 0 80))))
124 (let [in (io/input-stream filename)]
125 (canonicalise (b/decode binary-stl in) map-kind))))
126
127 (defn- vect->str [prefix v]
128 (str prefix " " (:x v) " " (:y v) " " (:z v) "\n"))
129
130 (defn- facet2str [tri]
131 (str
132 (vect->str "facet normal" (:normal tri))
133 "outer loop\n"
134 (apply str
135 (map
136 #(vect->str "vertex" %)
137 (:vertices tri)))
138 "endloop\nendfacet\n"))
139
140 (defn stl->ascii
141 "Return as a string an ASCII rendering of the `stl` structure."
142 ([stl]
143 (stl->ascii stl "unknown"))
144 ([stl solidname]
145 (str
146 "solid "
147 solidname
148 (s/trim (:header stl))
149 "\n"
150 (apply
151 str
152 (map
153 facet2str
154 (:facets stl)))
155 "endsolid "
156 solidname
157 "\n")))
158
159 (defn write-ascii-stl
160 "Write an `stl` structure as read by `decode-binary-stl` to this
161 `filename` as ASCII encoded STL."
162 ([filename stl]
163 (let [b (fs/base-name filename true)]
164 (write-ascii-stl
165 filename stl
166 (subs b 0 (or (s/index-of b ".") (count b))))))
167 ([filename stl solidname]
168 (l/debug "Solid name is " solidname)
169 (spit
170 filename
171 (stl->ascii stl solidname))))
172
173 (defn binary-stl-to-ascii
174 "Convert the binary STL file indicated by `in-filename`, and write it to
175 `out-filename`, if specified; otherwise, to a file with the same basename
176 as `in-filename` but the extension `.ascii.stl`."
177 ([in-filename]
178 (let [[_ ext] (fs/split-ext in-filename)]
179 (binary-stl-to-ascii
180 in-filename
181 (str
182 (subs
183 in-filename
184 0
185 (or
186 (s/last-index-of in-filename ".")
187 (count in-filename)))
188 ".ascii"
189 ext))))
190 ([in-filename out-filename]
191 (write-ascii-stl out-filename (decode-binary-stl in-filename))))