diff options
Diffstat (limited to 'openscad/Graphics/OpenSCAD.hs')
-rw-r--r-- | openscad/Graphics/OpenSCAD.hs | 638 |
1 files changed, 638 insertions, 0 deletions
diff --git a/openscad/Graphics/OpenSCAD.hs b/openscad/Graphics/OpenSCAD.hs new file mode 100644 index 0000000..e4efe7b --- /dev/null +++ b/openscad/Graphics/OpenSCAD.hs | |||
@@ -0,0 +1,638 @@ | |||
1 | {-# LANGUAGE FlexibleContexts #-} | ||
2 | {-# LANGUAGE FlexibleInstances #-} | ||
3 | |||
4 | {- | | ||
5 | Module : Graphics.OpenSCAD | ||
6 | Description : Type-checked wrappers for the OpenSCAD primitives. | ||
7 | Copyright : © Mike Meyer, 2014 | ||
8 | License : BSD4 | ||
9 | Maintainer : mwm@mired.org | ||
10 | Stability : experimental | ||
11 | |||
12 | = Overview | ||
13 | |||
14 | The Graphics.OpenSCAD module provides abstract data types for creating | ||
15 | OpenSCAD model definitions calls, along with a function to render it | ||
16 | as a string, and some utilities. The primary goal is that the output | ||
17 | should always be valid OpenSCAD. If you manage to generate OpenSCAD | ||
18 | source that causes OpenSCAD to complain, please open an issue. | ||
19 | |||
20 | The primary effect of this is that Graphics.OpenSCAD distinguishes | ||
21 | between 2d and 3d 'Model's. If you want to mix them, you must | ||
22 | explicitly convert between them. While two-dimensional model creation | ||
23 | could be polymorphic functions that create either, so that such models | ||
24 | could be treated as either 2d or 3d, you'd still have to explicitly | ||
25 | convert models whose type was fixed as 2d by a transformation, and | ||
26 | 'render' wouldn't work if the type was still ambiguous, ala @render $ | ||
27 | square 2@. | ||
28 | |||
29 | = Usage | ||
30 | |||
31 | Standard usage is to have a @main@ function that looks like: | ||
32 | |||
33 | @ | ||
34 | main = draw $ /Solid/ | ||
35 | @ | ||
36 | or | ||
37 | @ | ||
38 | main = drawL $ [/Solid/] | ||
39 | @ | ||
40 | |||
41 | and then set your IDE's compile command to use @runhaskell@ or | ||
42 | equivalent to run your code and send the output to a .scad file. Open | ||
43 | that file in OpenSCAD, and set it to automatically reload if the file | ||
44 | changes. Recompiling your program will cause the model to be loaded | ||
45 | and displayed by OpenSCAD. | ||
46 | |||
47 | The type constructors are not exported, with functions being exported | ||
48 | in their stead. This allows extra checking to be done on those that | ||
49 | need it. It also provides consistency, as otherwise you'd have to | ||
50 | remember whether 'box' is a constructor or a convenience function, | ||
51 | etc. | ||
52 | |||
53 | Because of this, the constructors are not documented, the exported | ||
54 | functions are. The documentation is generally just the corresponding | ||
55 | OpenSCAD function name, along with the names of the arguments from the | ||
56 | OpenSCAD documentation. If no OpenSCAD function name is given, then | ||
57 | it's the same as the 'Graphics.OpenSCAD' function. You should check | ||
58 | the OpenSCAD documentation for usage information. | ||
59 | |||
60 | = Oddities | ||
61 | |||
62 | 'importFile' has been left polymorphic. I couldn't find a sane way to | ||
63 | check that you're importing the right file type, so detecting such | ||
64 | errors - including importing a 3d file and trying to extrude it - have | ||
65 | to be left up to OpenSCAD in any case. So for now, there's just | ||
66 | 'importFile'. This does create the oddity that if you import a file | ||
67 | and try and render it without doing something to indicate how many | ||
68 | dimensions it has (one of the transformations, an extrusion or | ||
69 | projection, or 'solid') you'll get a compile error because the type is | ||
70 | ambiguous. Later, this may turn into @import2d@ and @import3d@. | ||
71 | |||
72 | The interfaces for 'polygon's and 'polyhedron's is seriously different | ||
73 | from the OpenSCAD interface. Rather than expecting you to enter a list | ||
74 | of points and then references to them, you just enter the points | ||
75 | directly. If you really want to do it the OpenSCAD way, you can do | ||
76 | something like: | ||
77 | |||
78 | @ | ||
79 | draw $ polyhedron [[(p 0, p 1, p 2), (p 0, p 2, p 3), ... ]] | ||
80 | where points = [.....] | ||
81 | p i = points !! i | ||
82 | @ | ||
83 | |||
84 | Also, the OpenSCAD polyedron code recently changed. The old version | ||
85 | requires that the faces all be triangles, the new version allows for | ||
86 | them to be arbitrary polygons. 'Graphics.OpenSCAD' supports both: if | ||
87 | all your faces are triangles, it will use the old version. If some | ||
88 | have more points, the new version will be used. If any have fewer than | ||
89 | three points you get an error. At this time, no tests are done on the | ||
90 | faces. That will probably change in the future. | ||
91 | |||
92 | Finally, polygon and polyhedron can generate errors on input that | ||
93 | seems to generate proper solids. If you turn on 'View->Thrown | ||
94 | Together', you'll see it highlighting errors in the object. | ||
95 | |||
96 | Offset is missing even though it's documented, as it isn't supported | ||
97 | by a released version of OpenSCAD, so presumably subject to change. It | ||
98 | is implemented, but untested as yet. You can add it to the module's | ||
99 | export lists if you want to play with it. | ||
100 | |||
101 | -} | ||
102 | |||
103 | module Graphics.OpenSCAD ( | ||
104 | -- * Types | ||
105 | -- ** A 'Model' to be rendered, and a 'Vector' that fixes the | ||
106 | -- number of dimensions it has. | ||
107 | Model, Vector, | ||
108 | -- ** Types aliases with fixed dimensions | ||
109 | Model2d, Model3d, Vector2d, Vector3d, | ||
110 | -- ** Other type aliases | ||
111 | Facet, TransMatrix, | ||
112 | -- ** Type for 'unsafePolyhedron' 'Sides' argument | ||
113 | Sides(..), | ||
114 | -- * Primitive creation | ||
115 | -- ** 'Model2d's | ||
116 | rectangle, square, circle, polygon, unsafePolygon, projection, importFile, | ||
117 | -- ** 'Model3d's | ||
118 | sphere, box, cube, cylinder, obCylinder, polyhedron, unsafePolyhedron, | ||
119 | multMatrix, linearExtrude, rotateExtrude, surface, solid, | ||
120 | -- * Functions | ||
121 | -- ** Combinations | ||
122 | union, intersection, difference, minkowski, hull, | ||
123 | -- ** Transformations | ||
124 | scale, resize, rotate, translate, mirror, color, transparent, up, | ||
125 | -- ** Rendering | ||
126 | render, renderL, | ||
127 | -- ** 'Facet's. | ||
128 | var, fn, fs, fa, def, | ||
129 | -- ** General convenience functions | ||
130 | diam, draw, drawL, (#), | ||
131 | module Colours) | ||
132 | |||
133 | where | ||
134 | |||
135 | import Data.Colour (Colour, AlphaColour, alphaChannel, darken, over, black) | ||
136 | import Data.Colour.Names as Colours | ||
137 | import Data.Colour.SRGB (channelRed, channelBlue, channelGreen, toSRGB) | ||
138 | import Data.List (elemIndices, nub, intercalate) | ||
139 | import qualified Data.List.NonEmpty as NE | ||
140 | import Data.Semigroup (Semigroup((<>), sconcat)) | ||
141 | import qualified Data.Set as Set | ||
142 | import System.FilePath (FilePath) | ||
143 | |||
144 | -- A vector in 2 or 3-space. They are used in transformations of | ||
145 | -- 'Model's of their type. | ||
146 | class Eq a => Vector a where | ||
147 | rVector :: a -> String | ||
148 | toList :: a -> [Double] | ||
149 | (#*) :: a -> a -> a -- cross product | ||
150 | (#-) :: a -> a -> a -- difference between two vectors | ||
151 | |||
152 | (#.) :: a -> a -> Double -- dot product | ||
153 | v1 #. v2 = sum $ zipWith (*) (toList v1) (toList v2) | ||
154 | |||
155 | isZero :: a -> Bool -- is a zero vector. Arguably should use eps. | ||
156 | isZero = all (== 0) . toList | ||
157 | |||
158 | collinear :: [a] -> Bool -- are all points collinear? | ||
159 | collinear [] = False | ||
160 | collinear [_] = False | ||
161 | collinear [v1, v2] = v1 /= v2 | ||
162 | collinear (v1:v2:vs) | ||
163 | | v1 /= v2 = all (\v -> isZero $ (v2 #- v1) #* (v1 #- v)) vs | ||
164 | | otherwise = collinear (v2:vs) | ||
165 | |||
166 | -- | 'Vector2d' is used where 'Graphics.OpenSCAD' expects an OpenSCAD | ||
167 | -- @vector@ of length 2. | ||
168 | type Vector2d = (Double, Double) | ||
169 | instance Vector Vector2d where | ||
170 | rVector (x, y) = "[" ++ show x ++ "," ++ show y ++ "]" | ||
171 | toList (x, y) = [x, y] | ||
172 | (x1, y1) #- (x2, y2) = (x1 - x2, y1 - y2) | ||
173 | (x1, y1) #* (x2, y2) = (0, x1 * y2 - y1 * x2) -- for purposes of collinear | ||
174 | |||
175 | -- | 'Vector3d' is used where 'Graphics.OpenSCAD' expects an OpenSCAD | ||
176 | -- @vector@ of length 3. | ||
177 | type Vector3d = (Double, Double, Double) | ||
178 | instance Vector Vector3d where | ||
179 | rVector (x, y, z) = "[" ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]" | ||
180 | toList (x, y, z) = [x, y, z] | ||
181 | (x1, y1, z1) #- (x2, y2, z2) = (x1 - x2, y1 - y2, z1 - z2) | ||
182 | (x1, y1, z1) #* (x2, y2, z2) = (y1 * z2 - z1 * y2, | ||
183 | z1 * x2 - x1 * z2, | ||
184 | x1 * y2 - y1 * x2) | ||
185 | |||
186 | -- Coplanar only makes sense for R3, so it's not part of the Vector class | ||
187 | coplanar :: [Vector3d] -> Bool | ||
188 | coplanar vs | length vs <= 3 = True -- by definition | ||
189 | | collinear $ take 3 vs = coplanar $ tail vs | ||
190 | | otherwise = | ||
191 | all (\v -> (v3 #- v1) #. ((v2 #- v1) #* (v #- v3)) == 0) vs' | ||
192 | where (v1:v2:v3:vs') = vs | ||
193 | |||
194 | |||
195 | -- | A 4x4 transformation matrix specifying a complete 3-space | ||
196 | -- transform of a 'Model3d'. | ||
197 | type TransMatrix = | ||
198 | ((Double, Double, Double, Double), (Double, Double, Double, Double), | ||
199 | (Double, Double, Double, Double), (Double, Double, Double, Double)) | ||
200 | |||
201 | |||
202 | -- While it's tempting to add more options to Solid, Shape or Model, | ||
203 | -- don't do it. Instead, add functions that add that functionality, | ||
204 | -- by building the appropriate structure, like cube vs. box. | ||
205 | |||
206 | -- | A 'Facet' is used to set one of the special variables that | ||
207 | -- control the mesh used during generation of circular objects. They | ||
208 | -- appear as arguments to various constructors, as well as in the | ||
209 | -- 'var' function to set them for the argument objects. | ||
210 | data Facet = Fa Double | Fs Double | Fn Int | Def deriving Show | ||
211 | |||
212 | -- | A 'Join' controls how edges in a 'polygon' are joined by the | ||
213 | -- 'offset' operation. | ||
214 | data Join = Bevel | Round | Miter Double deriving Show | ||
215 | |||
216 | -- A 'Shape' is a 2-dimensional primitive to be used in a 'Model2d'. | ||
217 | data Shape = Rectangle Double Double | ||
218 | | Circle Double Facet | ||
219 | | Polygon Int [Vector2d] [[Int]] | ||
220 | | Projection Bool Model3d | ||
221 | | Offset Double Join Shape | ||
222 | deriving Show | ||
223 | |||
224 | -- | The third argument to unsafePolyhedron is a 'Sides'. | ||
225 | data Sides = Faces [[Int]] | Triangles [[Int]] deriving Show | ||
226 | |||
227 | -- A 'Solid' is a 3-dimensional primitive to be used in a 'Model3d'. | ||
228 | data Solid = Sphere Double Facet | ||
229 | | Box Double Double Double | ||
230 | | Cylinder Double Double Facet | ||
231 | | ObCylinder Double Double Double Facet | ||
232 | | Polyhedron Int [Vector3d] Sides | ||
233 | | MultMatrix TransMatrix Model3d | ||
234 | | LinearExtrude Double Double Vector2d Int Int Facet Model2d | ||
235 | | RotateExtrude Int Facet Model2d | ||
236 | | Surface FilePath Bool Int | ||
237 | | ToSolid Model2d | ||
238 | deriving Show | ||
239 | |||
240 | -- | A 'Model' is either a 'Model2d', a 'Model3d', a transformation of | ||
241 | -- a 'Model', a combination of 'Model's, or a 'Model' with it's | ||
242 | -- rendering tweaked by a 'Facet'. 'Model's can be rendered. | ||
243 | data Model v = Shape Shape | ||
244 | | Solid Solid | ||
245 | | Scale v (Model v) | ||
246 | | Resize v (Model v) | ||
247 | | Rotate v (Model v) | ||
248 | | Translate v (Model v) | ||
249 | | Mirror v (Model v) | ||
250 | | Color (Colour Double) (Model v) | ||
251 | | Transparent (AlphaColour Double) (Model v) | ||
252 | -- and combinations | ||
253 | | Union [Model v] | ||
254 | | Intersection [Model v] | ||
255 | | Minkowski [Model v] | ||
256 | | Hull [Model v] | ||
257 | | Difference (Model v) (Model v) | ||
258 | -- And oddball stuff control | ||
259 | | Import FilePath | ||
260 | | Var Facet [Model v] | ||
261 | deriving Show | ||
262 | |||
263 | -- | A two-dimensional model. Note that the types do not mix | ||
264 | -- implicitly. You must turn a 'Model2d' into a 'Model3d' using one of | ||
265 | -- 'linearExtrude', 'rotateExtrude', or 'solid'. | ||
266 | type Model2d = Model Vector2d | ||
267 | |||
268 | -- | A three-dimensional model. You can create a 'Model2d' from a | ||
269 | -- 'Model3d' using 'projection'. | ||
270 | type Model3d = Model Vector3d | ||
271 | |||
272 | -- Tools for creating 'Model2d's. | ||
273 | -- | Create a rectangular 'Model2d' with @rectangle /x-size y-size/@. | ||
274 | rectangle :: Double -> Double -> Model2d | ||
275 | rectangle w h = Shape $ Rectangle w h | ||
276 | |||
277 | -- | 'square' is a 'rectangle' with both sides the same size. | ||
278 | square :: Double -> Model2d | ||
279 | square s = rectangle s s | ||
280 | |||
281 | -- | Create a circular 'Model' with @circle /radius/ 'Facet'@. | ||
282 | circle :: Double -> Facet -> Model2d | ||
283 | circle r f = Shape $ Circle r f | ||
284 | |||
285 | -- | Project a 'Model3d' into a 'Model' with @projection /cut 'Model3d'/@. | ||
286 | projection :: Bool -> Model3d -> Model2d | ||
287 | projection c s = Shape $ Projection c s | ||
288 | |||
289 | -- | Turn a list of lists of 'Vector2d's and an Int into @polygon | ||
290 | -- /convexity points path/@. The argument to polygon is the list of | ||
291 | -- paths that is the second argument to the OpenSCAD polygon function, | ||
292 | -- except the points are 'Vector2d's, not references to 'Vector2d's in | ||
293 | -- that functions points argument. If you were just going to pass in | ||
294 | -- the points, it now needs to be in an extra level of 'List'. | ||
295 | polygon :: Int -> [[Vector2d]] -> Model2d | ||
296 | polygon convexity paths | ||
297 | | any ((< 3) . length) paths = error "Polygon has fewer than 3 points." | ||
298 | | any collinear paths = error "Points in polygon are collinear." | ||
299 | | otherwise = let points = nub $ concat paths | ||
300 | in Shape . Polygon convexity points | ||
301 | $ map (concatMap (`elemIndices` points)) paths | ||
302 | |||
303 | -- | This provides direct access to the OpenScad @polygon@ command for | ||
304 | -- performance reasons. This version uses the OpenSCAD arguments: | ||
305 | -- @polygon /convexity points path/@ to allow client code to save | ||
306 | -- space. However, it bypasses all the checks done by | ||
307 | -- 'polygon', which need the other representation. | ||
308 | unsafePolygon :: Int -> [Vector2d] -> [[Int]] -> Model2d | ||
309 | unsafePolygon convexity points paths = Shape $ Polygon convexity points paths | ||
310 | |||
311 | -- | 'offset' a 'Model2d's edges by @offset /delta join/@. | ||
312 | offset :: Double -> Join -> Model2d -> Model2d | ||
313 | offset d j (Shape s) = Shape $ Offset d j s | ||
314 | |||
315 | -- Tools for creating Model3ds | ||
316 | -- | Create a sphere with @sphere /radius 'Facet'/@. | ||
317 | sphere :: Double -> Facet -> Model3d | ||
318 | sphere r f = Solid $ Sphere r f | ||
319 | |||
320 | -- | Create a box with @cube /x-size y-size z-size/@ | ||
321 | box :: Double -> Double -> Double -> Model3d | ||
322 | box x y z = Solid $ Box x y z | ||
323 | |||
324 | -- | A convenience function for creating a cube as a 'box' with all | ||
325 | -- sides the same length. | ||
326 | cube :: Double -> Model3d | ||
327 | cube x = box x x x | ||
328 | |||
329 | -- | Create a cylinder with @cylinder /radius height 'Facet'/@. | ||
330 | cylinder :: Double -> Double -> Facet -> Model3d | ||
331 | cylinder h r f = Solid $ Cylinder h r f | ||
332 | |||
333 | -- | Create an oblique cylinder with @cylinder /radius1 height radius2 'Facet'/@. | ||
334 | obCylinder :: Double -> Double -> Double -> Facet -> Model Vector3d | ||
335 | obCylinder r1 h r2 f= Solid $ ObCylinder r1 h r2 f | ||
336 | |||
337 | -- | Turn a list of list of 'Vector3d's and an int into @polyhedron | ||
338 | -- /convexity points 'Sides'/@. The argument to polyhedron is the list | ||
339 | -- of paths that is the second argument to the OpenSCAD polyhedron | ||
340 | -- function, except the points are 'Vector3d's, not the references to | ||
341 | -- 'Vector3d's used in that functions @points@ argument. The function | ||
342 | -- will build the appropriate function call, using @faces@ if you pass | ||
343 | -- in a side that uses more than 3 points, or @triangles@ if not. Note | ||
344 | -- that @faces@ doesn't work in older versions of OpenSCAD, and | ||
345 | -- @triangles@ is depreciated. Until a mechanism to set the version of | ||
346 | -- OpenSCAD is provided, generating the @faces@ version will cause an | ||
347 | -- error. | ||
348 | -- | ||
349 | -- Passing in 'Sides' that have fewer than three points, have | ||
350 | -- collinear points or have points that aren't in the same plane is an | ||
351 | -- error that is caught by the library. | ||
352 | polyhedron :: Int -> [[Vector3d]] -> Model3d | ||
353 | polyhedron convexity paths | ||
354 | | any ((< 3) . length) paths = error "Some face has fewer than 3 points." | ||
355 | | any collinear paths = error "Some face has collinear points." | ||
356 | | any (not . coplanar) paths = error "Some face isn't coplanar." | ||
357 | | length vectors /= length (nub vectors) = | ||
358 | error "Some faces have different orientation." | ||
359 | | 2 * length edges /= length vectors = error "Some edges are not in two faces." | ||
360 | | xCross headMax xMax tailMax > 0 = | ||
361 | error "Face orientations are counterclockwise." | ||
362 | | otherwise = Solid . Polyhedron convexity points $ sides sidesIn | ||
363 | where vectors = concatMap (\p -> zip p (tail p ++ [head p])) paths | ||
364 | edges = nub $ map (Set.fromList . \(a, b) -> [a, b]) vectors | ||
365 | points = nub $ concat paths | ||
366 | xMax = maximum points | ||
367 | faceMax = head $ filter (elem xMax) paths | ||
368 | (maxFirst, maxLast) = break (== xMax) faceMax | ||
369 | (headMax, tailMax) = (if null maxFirst | ||
370 | then last maxLast | ||
371 | else last maxFirst, | ||
372 | if null (tail maxLast) | ||
373 | then head maxFirst | ||
374 | else head (tail maxLast)) | ||
375 | xCross a b c = (\(a, b, c) -> a) $ (a #- b) #* (b #- c) | ||
376 | sidesIn = map (concatMap (`elemIndices` points)) paths | ||
377 | sides ss | any ((> 3) . length) ss = Faces ss | ||
378 | | all ((== 3) . length) ss = Triangles ss | ||
379 | | otherwise = error "Some faces have fewer than 3 points." | ||
380 | |||
381 | -- | This provides direct access to the OpenSCAD @polyhedron@ command | ||
382 | -- for performance reasons. This version uses the OpenSCAD arguments: | ||
383 | -- @polyhedron /convexity points 'Sides'/@ to allow client code to | ||
384 | -- save space. However, it bypasses all the checks done by | ||
385 | -- 'polyhedron', which needs the other representation. | ||
386 | unsafePolyhedron :: Int -> [Vector3d] -> Sides -> Model3d | ||
387 | unsafePolyhedron convexity points sides = Solid $ Polyhedron convexity points sides | ||
388 | |||
389 | |||
390 | -- | Transform a 'Model3d' with a 'TransMatrix' | ||
391 | multMatrix :: TransMatrix -> Model3d -> Model3d | ||
392 | multMatrix t m = Solid $ MultMatrix t m | ||
393 | |||
394 | -- | Turn a 'Model2d' into a 'Model3d' exactly as is. | ||
395 | solid :: Model2d -> Model3d | ||
396 | solid = Solid . ToSolid | ||
397 | |||
398 | -- | Extrude a 'Model2d' along a line with @linear_extrude@. | ||
399 | linearExtrude :: Double -- ^ height | ||
400 | -> Double -- ^ twist | ||
401 | -> Vector2d -- ^ scale | ||
402 | -> Int -- ^ slices | ||
403 | -> Int -- ^ convexity | ||
404 | -> Facet | ||
405 | -> Model2d -- ^ to extrude | ||
406 | -> Model3d | ||
407 | linearExtrude h t sc sl c f m = Solid $ LinearExtrude h t sc sl c f m | ||
408 | |||
409 | -- | Rotate a 'Model2d' around the origin with @rotate_extrude | ||
410 | -- /convexity 'Facet' 'Model'/@ | ||
411 | rotateExtrude :: Int -> Facet -> Model2d -> Model3d | ||
412 | rotateExtrude c f m = Solid $ RotateExtrude c f m | ||
413 | |||
414 | -- | Load a height map from a file with @surface /FilePath Invert Convexity/@. | ||
415 | surface :: FilePath -> Bool -> Int -> Model3d | ||
416 | surface f i c = Solid $ Surface f i c | ||
417 | |||
418 | -- And the one polymorphic function we have. | ||
419 | -- | 'importFile' is @import /filename/@. | ||
420 | importFile :: Vector v => FilePath -> Model v | ||
421 | importFile = Import | ||
422 | |||
423 | |||
424 | -- Transformations | ||
425 | -- | Scale a 'Model', the vector specifying the scale factor for each axis. | ||
426 | scale :: Vector v => v -> Model v -> Model v | ||
427 | scale = Scale | ||
428 | |||
429 | -- | Resize a 'Model' to occupy the dimensions given by the vector. Note that | ||
430 | -- this does nothing prior to the 2014 versions of OpenSCAD. | ||
431 | resize :: Vector v => v -> Model v -> Model v | ||
432 | resize = Resize | ||
433 | |||
434 | -- | Rotate a 'Model' by different amounts around each of the three axis. | ||
435 | rotate :: Vector v => v -> Model v -> Model v | ||
436 | rotate = Rotate | ||
437 | |||
438 | -- | Translate a 'Model' along a 'Vector'. | ||
439 | translate :: Vector v => v -> Model v -> Model v | ||
440 | translate = Translate | ||
441 | |||
442 | -- | Mirror a 'Model' across a plane intersecting the origin. | ||
443 | mirror :: Vector v => v -> Model v -> Model v | ||
444 | mirror = Mirror | ||
445 | |||
446 | -- | Render a 'Model' in a specific color. This doesn't use the | ||
447 | -- OpenSCAD color model, but instead uses the 'Data.Colour' model. The | ||
448 | -- 'Graphics.OpenSCAD' module rexports 'Data.Colour.Names' so you can | ||
449 | -- conveniently say @'color' 'red' /'Model'/@. | ||
450 | color :: Vector v => Colour Double -> Model v -> Model v | ||
451 | color = Color | ||
452 | |||
453 | -- | Render a 'Model' in a transparent color. This uses the | ||
454 | -- 'Data.Colour.AlphaColour' color model. | ||
455 | transparent :: Vector v => AlphaColour Double -> Model v -> Model v | ||
456 | transparent = Transparent | ||
457 | |||
458 | -- | A 'translate' that just goes up, since those seem to be common. | ||
459 | up :: Double -> Model3d -> Model3d | ||
460 | up f = translate (0, 0, f) | ||
461 | |||
462 | |||
463 | -- Combinations | ||
464 | -- | Create the union of a list of 'Model's. | ||
465 | union :: Vector v => [Model v] -> Model v | ||
466 | union = Union | ||
467 | |||
468 | -- | Create the intersection of a list of 'Model's. | ||
469 | intersection :: Vector v => [Model v] -> Model v | ||
470 | intersection = Intersection | ||
471 | |||
472 | -- | The difference between two 'Model's. | ||
473 | difference :: Vector v => Model v -> Model v -> Model v | ||
474 | difference = Difference | ||
475 | |||
476 | -- | The Minkowski sum of a list of 'Model's. | ||
477 | minkowski :: Vector v => [Model v] -> Model v | ||
478 | minkowski = Minkowski | ||
479 | |||
480 | -- | The convex hull of a list of 'Model's. | ||
481 | hull :: Vector v => [Model v] -> Model v | ||
482 | hull = Hull | ||
483 | |||
484 | |||
485 | -- | 'render' does all the real work. It will walk the AST for a 'Model', | ||
486 | -- returning an OpenSCAD program in a 'String'. | ||
487 | render :: Vector v => Model v -> String | ||
488 | render (Shape s) = rShape s | ||
489 | render (Solid s) = rSolid s | ||
490 | render (Union ss) = rList "union()" ss | ||
491 | render (Intersection ss) = rList "intersection()" ss | ||
492 | render (Difference s1 s2) = "difference(){" ++ render s1 ++ render s2 ++ "}\n" | ||
493 | render (Minkowski ss) = rList "minkowski()" ss | ||
494 | render (Hull ss) = rList "hull()" ss | ||
495 | render (Scale v s) = rVecSolid "scale" v s | ||
496 | render (Resize v s) = rVecSolid "resize" v s | ||
497 | render (Translate v s) = rVecSolid "translate" v s | ||
498 | render (Rotate v s) = "rotate(" ++ rVector v ++ ")" ++ render s | ||
499 | render (Mirror v s) = rVecSolid "mirror" v s | ||
500 | render (Import f) = "import(\"" ++ f ++ "\");\n" | ||
501 | render (Color c s) = let r = toSRGB c in | ||
502 | "color(" ++ rVector (channelRed r, channelGreen r, channelBlue r) ++ ")\n" | ||
503 | ++ render s | ||
504 | render (Transparent c s) = | ||
505 | "color(" ++ rQuad (channelRed r, channelGreen r, channelBlue r, a) ++ ")" | ||
506 | ++ render s | ||
507 | where r = toSRGB $ toPure c | ||
508 | a = alphaChannel c | ||
509 | toPure ac = if a > 0 then darken (recip a) (ac `over` black) else black | ||
510 | render (Var (Fa f) ss) = rList ("assign($fa=" ++ show f ++ ")") ss | ||
511 | render (Var (Fs f) ss) = rList ("assign($fs=" ++ show f ++ ")") ss | ||
512 | render (Var (Fn n) ss) = rList ("assign($fn=" ++ show n ++ ")") ss | ||
513 | |||
514 | -- utility for rendering Shapes. | ||
515 | rShape :: Shape -> String | ||
516 | rShape (Rectangle r f) = "square([" ++ show r ++ "," ++ show f ++ "]);\n" | ||
517 | rShape (Circle r f) = "circle(" ++ show r ++ rFacet f ++ ");\n" | ||
518 | rShape (Projection c s) = | ||
519 | "projection(cut=" ++ (if c then "true)" else "false)") ++ render s | ||
520 | rShape (Polygon c points paths) = "polygon(points=" ++ rVectorL points ++ | ||
521 | ",paths=" ++ show paths ++ ",convexity=" ++ show c ++ ");\n" | ||
522 | rShape (Offset d j s) = | ||
523 | "offset(delta=" ++ show d ++ "," ++ rJoin j ++ ")" ++ rShape s | ||
524 | |||
525 | -- utility for rendering Joins | ||
526 | rJoin :: Join -> String | ||
527 | rJoin Bevel = "join_type=bevel" | ||
528 | rJoin Round = "join_type=round" | ||
529 | rJoin (Miter l) = "miter_limit=" ++ show l | ||
530 | |||
531 | -- utilities for rendering Solids. | ||
532 | rSolid :: Solid -> String | ||
533 | rSolid (Sphere x f) = "sphere(" ++ show x ++ rFacet f ++ ");\n" | ||
534 | rSolid (Box x y z) = | ||
535 | "cube([" ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]);\n" | ||
536 | rSolid (Cylinder r h f) = | ||
537 | "cylinder(r=" ++ show r ++ ",h=" ++ show h ++ rFacet f ++ ");\n" | ||
538 | rSolid (ObCylinder r1 h r2 f) = | ||
539 | "cylinder(r1=" ++ show r1 ++ ",h=" ++ show h ++ ",r2=" ++ show r2 ++ rFacet f | ||
540 | ++ ");\n" | ||
541 | rSolid (Polyhedron c ps ss) = "polyhedron(points=" ++ rVectorL ps ++ rSides ss | ||
542 | ++ ",convexity=" ++ show c ++ ");\n" | ||
543 | rSolid (MultMatrix (a, b, c, d) s) = | ||
544 | "multmatrix([" ++ rQuad a ++ "," ++ rQuad b ++ "," ++ rQuad c ++ "," | ||
545 | ++ rQuad d ++"])\n" ++ render s | ||
546 | rSolid (LinearExtrude h t sc sl c f sh) = | ||
547 | "linear_extrude(height=" ++ show h ++ ",twist=" ++ show t ++ ",scale=" | ||
548 | ++ rVector sc ++ ",slices=" ++ show sl ++ ",convexity=" ++ show c | ||
549 | ++ rFacet f ++ ")" ++ render sh | ||
550 | rSolid (RotateExtrude c f sh) = | ||
551 | "rotate_extrude(convexity=" ++ show c ++ rFacet f ++ ")" ++ render sh | ||
552 | rSolid (Surface f i c) = | ||
553 | "surface(file=\"" ++ f ++ "\"," ++ (if i then "invert=true," else "") | ||
554 | ++ "convexity=" ++ show c ++ ");\n" | ||
555 | rSolid (ToSolid s) = render s | ||
556 | |||
557 | -- render a list of vectors as an Openscad vector of vectors. | ||
558 | rVectorL vs = "[" ++ intercalate "," (map rVector vs) ++ "]" | ||
559 | |||
560 | -- render a Sides. | ||
561 | rSides (Faces vs) = ",faces=" ++ rListL vs | ||
562 | rSides (Triangles vs) = ",triangles=" ++ rListL vs | ||
563 | rListL vs = "[" ++ intercalate "," (map show vs) ++ "]" | ||
564 | |||
565 | -- | A convenience function to render a list of 'Model's by taking | ||
566 | -- their union. | ||
567 | renderL :: Vector v => [Model v] -> String | ||
568 | renderL = render . union | ||
569 | |||
570 | -- | A convenience function to write the rendered 'Model' to | ||
571 | -- standard output. | ||
572 | draw :: Vector v => Model v -> IO () | ||
573 | draw = putStrLn . render | ||
574 | |||
575 | -- | A convenience function to write a 'union' of 'Model's to | ||
576 | -- standard output. | ||
577 | drawL :: Vector v => [Model v] -> IO () | ||
578 | drawL = draw . Union | ||
579 | |||
580 | -- And some misc. rendering utilities. | ||
581 | rList n ss = n ++ "{\n" ++ concatMap render ss ++ "}" | ||
582 | rVecSolid n v s = n ++ "(" ++ rVector v ++ ")\n" ++ render s | ||
583 | rQuad (w, x, y, z) = | ||
584 | "[" ++ show w ++ "," ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]" | ||
585 | rFacet Def = "" | ||
586 | rFacet f = "," ++ showFacet f | ||
587 | |||
588 | -- render a facet setting. | ||
589 | showFacet :: Facet -> String | ||
590 | showFacet (Fa f) = "$fa=" ++ show f | ||
591 | showFacet (Fs f) = "$fs=" ++ show f | ||
592 | showFacet (Fn n) = "$fn=" ++ show n | ||
593 | showFacet Def = "" | ||
594 | |||
595 | -- Convenience functions for Facets. | ||
596 | -- | 'var' uses @assign@ to set a 'Facet' variable for it's 'Model's. | ||
597 | var :: Facet -> [Model v] -> Model v | ||
598 | var = Var | ||
599 | |||
600 | -- | 'fa' is used to set the @$fa@ variable in a 'Facet' or 'var'. | ||
601 | fa :: Double -> Facet | ||
602 | fa = Fa | ||
603 | |||
604 | -- | 'fs' is used to set the @$fs@ variable in a 'Facet' or 'var'. | ||
605 | fs :: Double -> Facet | ||
606 | fs = Fs | ||
607 | |||
608 | -- | 'fn' is used to set the @$fn@ variable in a 'Facet' or 'var'. | ||
609 | fn :: Int -> Facet | ||
610 | fn = Fn | ||
611 | |||
612 | -- | 'def' is used where a 'Facet' is needed but we don't want to change | ||
613 | -- any of the values. | ||
614 | def :: Facet | ||
615 | def = Def | ||
616 | |||
617 | -- And one last convenience function. | ||
618 | -- | Use 'diam' to turn a diameter into a radius for circles, spheres, etc. | ||
619 | diam :: Double -> Double | ||
620 | diam = (/ 2) | ||
621 | -- Now, let Haskell work it's magic | ||
622 | instance Vector v => Semigroup (Model v) where | ||
623 | a <> b = union [a, b] | ||
624 | sconcat = union . NE.toList | ||
625 | |||
626 | instance Vector v => Monoid (Model v) where | ||
627 | mempty = Solid $ Box 0 0 0 | ||
628 | mappend (Solid (Box 0 0 0)) b = b | ||
629 | mappend a (Solid (Box 0 0 0)) = a | ||
630 | mappend a b = union [a, b] | ||
631 | mconcat [a] = a | ||
632 | mconcat as = union as | ||
633 | |||
634 | |||
635 | -- | You can use '(#)' to write transformations in a more readable postfix form, | ||
636 | -- cube 3 # color red # translate (-3, -3, -3) | ||
637 | infixl 8 # | ||
638 | (#) = flip ($) | ||