summaryrefslogtreecommitdiff
path: root/openscad/Graphics/OpenSCAD.hs
blob: e4efe7b73952723e992fc6137ca212a537db4006 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}

{- |
Module      : Graphics.OpenSCAD
Description : Type-checked wrappers for the OpenSCAD primitives.
Copyright   : © Mike Meyer, 2014
License     : BSD4
Maintainer  : mwm@mired.org
Stability   : experimental

= Overview

The Graphics.OpenSCAD module provides abstract data types for creating
OpenSCAD model definitions calls, along with a function to render it
as a string, and some utilities. The primary goal is that the output
should always be valid OpenSCAD. If you manage to generate OpenSCAD
source that causes OpenSCAD to complain, please open an issue.

The primary effect of this is that Graphics.OpenSCAD distinguishes
between 2d and 3d 'Model's. If you want to mix them, you must
explicitly convert between them.  While two-dimensional model creation
could be polymorphic functions that create either, so that such models
could be treated as either 2d or 3d, you'd still have to explicitly
convert models whose type was fixed as 2d by a transformation, and
'render' wouldn't work if the type was still ambiguous, ala @render $
square 2@.

= Usage

Standard usage is to have a @main@ function that looks like:

@
main = draw $ /Solid/
@
or
@
main = drawL $ [/Solid/]
@

and then set your IDE's compile command to use @runhaskell@ or
equivalent to run your code and send the output to a .scad file. Open
that file in OpenSCAD, and set it to automatically reload if the file
changes. Recompiling your program will cause the model to be loaded
and displayed by OpenSCAD.

The type constructors are not exported, with functions being exported
in their stead.  This allows extra checking to be done on those that
need it.  It also provides consistency, as otherwise you'd have to
remember whether 'box' is a constructor or a convenience function,
etc.

Because of this, the constructors are not documented, the exported
functions are. The documentation is generally just the corresponding
OpenSCAD function name, along with the names of the arguments from the
OpenSCAD documentation. If no OpenSCAD function name is given, then
it's the same as the 'Graphics.OpenSCAD' function. You should check
the OpenSCAD documentation for usage information.

= Oddities

'importFile' has been left polymorphic. I couldn't find a sane way to
check that you're importing the right file type, so detecting such
errors - including importing a 3d file and trying to extrude it - have
to be left up to OpenSCAD in any case.  So for now, there's just
'importFile'. This does create the oddity that if you import a file
and try and render it without doing something to indicate how many
dimensions it has (one of the transformations, an extrusion or
projection, or 'solid') you'll get a compile error because the type is
ambiguous. Later, this may turn into @import2d@ and @import3d@.

The interfaces for 'polygon's and 'polyhedron's is seriously different
from the OpenSCAD interface. Rather than expecting you to enter a list
of points and then references to them, you just enter the points
directly. If you really want to do it the OpenSCAD way, you can do
something like:

@
draw $ polyhedron [[(p 0, p 1, p 2), (p 0, p 2, p 3), ... ]]
where points = [.....]
      p i = points !! i
@

Also, the OpenSCAD polyedron code recently changed. The old version
requires that the faces all be triangles, the new version allows for
them to be arbitrary polygons. 'Graphics.OpenSCAD' supports both: if
all your faces are triangles, it will use the old version. If some
have more points, the new version will be used. If any have fewer than
three points you get an error. At this time, no tests are done on the
faces. That will probably change in the future.

Finally, polygon and polyhedron can generate errors on input that
seems to generate proper solids. If you turn on 'View->Thrown
Together', you'll see it highlighting errors in the object.

Offset is missing even though it's documented, as it isn't supported
by a released version of OpenSCAD, so presumably subject to change. It
is implemented, but untested as yet. You can add it to the module's
export lists if you want to play with it.

-}

module Graphics.OpenSCAD (
  -- * Types
  -- ** A 'Model' to be rendered, and a 'Vector' that fixes the
  -- number of dimensions it has.
  Model, Vector,
  -- ** Types aliases with fixed dimensions
  Model2d, Model3d, Vector2d, Vector3d,
  --  ** Other type aliases
  Facet, TransMatrix,
  -- ** Type for 'unsafePolyhedron' 'Sides' argument
  Sides(..),
  -- * Primitive creation
  -- ** 'Model2d's
  rectangle, square, circle, polygon, unsafePolygon, projection, importFile,
  -- ** 'Model3d's
  sphere, box, cube, cylinder, obCylinder, polyhedron, unsafePolyhedron,
  multMatrix, linearExtrude, rotateExtrude, surface, solid,
  -- * Functions
  -- ** Combinations
  union, intersection, difference, minkowski, hull,
  -- ** Transformations
  scale, resize, rotate, translate, mirror, color, transparent, up,
  -- ** Rendering
  render, renderL,
  -- ** 'Facet's.
  var, fn, fs, fa, def,
  -- ** General convenience functions
  diam, draw, drawL, (#),
  module Colours)

where

import Data.Colour (Colour, AlphaColour, alphaChannel, darken, over, black)
import Data.Colour.Names as Colours
import Data.Colour.SRGB (channelRed, channelBlue, channelGreen, toSRGB)
import Data.List (elemIndices, nub, intercalate)
import qualified Data.List.NonEmpty as NE
import Data.Semigroup (Semigroup((<>), sconcat))
import qualified Data.Set as Set
import System.FilePath (FilePath)

-- A vector in 2 or 3-space. They are used in transformations of
-- 'Model's of their type.
class Eq a => Vector a where
  rVector :: a -> String
  toList :: a -> [Double]
  (#*) :: a -> a -> a      -- cross product
  (#-) :: a -> a -> a      -- difference between two vectors

  (#.) :: a -> a -> Double -- dot product
  v1 #. v2 = sum $ zipWith (*) (toList v1) (toList v2)

  isZero :: a -> Bool      -- is a zero vector. Arguably should use eps.
  isZero = all (== 0) . toList

  collinear :: [a] -> Bool -- are all points collinear?
  collinear [] = False
  collinear [_] = False
  collinear [v1, v2] = v1 /= v2
  collinear (v1:v2:vs)
    | v1 /= v2  = all (\v -> isZero $ (v2 #- v1) #* (v1 #- v)) vs
    | otherwise = collinear (v2:vs)

-- | 'Vector2d' is used where 'Graphics.OpenSCAD' expects an OpenSCAD
-- @vector@ of length 2.
type Vector2d = (Double, Double)
instance Vector Vector2d where
  rVector (x, y) = "[" ++ show x ++ "," ++ show y ++ "]"
  toList (x, y) = [x, y]
  (x1, y1) #- (x2, y2) = (x1 - x2, y1 - y2)
  (x1, y1) #* (x2, y2) = (0, x1 * y2 - y1 * x2) -- for purposes of collinear

-- | 'Vector3d' is used where 'Graphics.OpenSCAD' expects an OpenSCAD
-- @vector@ of length 3.
type Vector3d = (Double, Double, Double)
instance Vector Vector3d where
  rVector (x, y, z) = "[" ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]"
  toList (x, y, z) = [x, y, z]
  (x1, y1, z1) #- (x2, y2, z2) = (x1 - x2, y1 - y2, z1 - z2)
  (x1, y1, z1) #* (x2, y2, z2) = (y1 * z2 - z1 * y2,
                                  z1 * x2 - x1 * z2,
                                  x1 * y2 - y1 * x2)

-- Coplanar only makes sense for R3, so it's not part of the Vector class
coplanar :: [Vector3d] -> Bool
coplanar vs | length vs <= 3        = True -- by definition
            | collinear $ take 3 vs = coplanar $ tail vs
            | otherwise =
                all (\v -> (v3 #- v1) #. ((v2 #- v1) #* (v #- v3)) == 0) vs'
                where (v1:v2:v3:vs') = vs


-- | A 4x4 transformation matrix specifying a complete 3-space
-- transform of a 'Model3d'.
type TransMatrix =
  ((Double, Double, Double, Double), (Double, Double, Double, Double),
   (Double, Double, Double, Double), (Double, Double, Double, Double))


-- While it's tempting to add more options to Solid, Shape or Model,
-- don't do it. Instead, add functions that add that functionality,
-- by building the appropriate structure, like cube vs. box.

-- | A 'Facet' is used to set one of the special variables that
-- control the mesh used during generation of circular objects. They
-- appear as arguments to various constructors, as well as in the
-- 'var' function to set them for the argument objects.
data Facet = Fa Double | Fs Double | Fn Int | Def deriving Show

-- | A 'Join' controls how edges in a 'polygon' are joined by the
-- 'offset' operation.
data Join = Bevel | Round | Miter Double deriving Show

-- A 'Shape' is a 2-dimensional primitive to be used in a 'Model2d'.
data Shape = Rectangle Double Double
           | Circle Double Facet
           | Polygon Int [Vector2d] [[Int]]
           | Projection Bool Model3d
           | Offset Double Join Shape
           deriving Show

-- | The third argument to unsafePolyhedron is a 'Sides'.
data Sides = Faces [[Int]] | Triangles [[Int]] deriving Show

-- A 'Solid' is a 3-dimensional primitive to be used in a 'Model3d'.
data Solid = Sphere Double Facet
           | Box Double Double Double
           | Cylinder Double Double Facet
           | ObCylinder Double Double Double Facet
           | Polyhedron Int [Vector3d] Sides
           | MultMatrix TransMatrix Model3d
           | LinearExtrude Double Double Vector2d Int Int Facet Model2d
           | RotateExtrude Int Facet Model2d
           | Surface FilePath Bool Int
           | ToSolid Model2d
           deriving Show

-- | A 'Model' is either a 'Model2d', a 'Model3d', a transformation of
-- a 'Model', a combination of 'Model's, or a 'Model' with it's
-- rendering tweaked by a 'Facet'. 'Model's can be rendered.
data Model v = Shape Shape
             | Solid Solid
             | Scale v (Model v)
             | Resize v (Model v)
             | Rotate v (Model v)
             | Translate v (Model v)
             | Mirror v (Model v)
             | Color (Colour Double) (Model v)
             | Transparent (AlphaColour Double) (Model v)
             -- and combinations
             | Union [Model v]
             | Intersection [Model v]
             | Minkowski [Model v]
             | Hull [Model v]
             | Difference (Model v) (Model v)
             -- And oddball stuff control
             | Import FilePath
             | Var Facet [Model v]
             deriving Show

-- | A two-dimensional model. Note that the types do not mix
-- implicitly. You must turn a 'Model2d' into a 'Model3d' using one of
-- 'linearExtrude', 'rotateExtrude', or 'solid'.
type Model2d = Model Vector2d

-- | A three-dimensional model. You can create a 'Model2d' from a
-- 'Model3d' using 'projection'.
type Model3d = Model Vector3d

-- Tools for creating 'Model2d's.
-- | Create a rectangular 'Model2d' with @rectangle /x-size y-size/@.
rectangle :: Double -> Double -> Model2d
rectangle w h = Shape $ Rectangle w h

-- | 'square' is a 'rectangle' with both sides the same size.
square :: Double -> Model2d
square s = rectangle s s

-- | Create a circular 'Model' with @circle /radius/ 'Facet'@.
circle :: Double -> Facet -> Model2d
circle r f = Shape $ Circle r f

-- | Project a 'Model3d' into a 'Model' with @projection /cut 'Model3d'/@.
projection :: Bool -> Model3d -> Model2d
projection c s = Shape $ Projection c s

-- | Turn a list of lists of 'Vector2d's and an Int into @polygon
-- /convexity points path/@. The argument to polygon is the list of
-- paths that is the second argument to the OpenSCAD polygon function,
-- except the points are 'Vector2d's, not references to 'Vector2d's in
-- that functions points argument.  If you were just going to pass in
-- the points, it now needs to be in an extra level of 'List'.
polygon ::  Int -> [[Vector2d]] -> Model2d
polygon convexity paths
  | any ((< 3) . length) paths = error "Polygon has fewer than 3 points."
  | any collinear paths = error "Points in polygon are collinear."
  | otherwise = let points = nub $ concat paths
                in Shape . Polygon convexity points
                   $ map (concatMap (`elemIndices` points)) paths

-- | This provides direct access to the OpenScad @polygon@ command for
-- performance reasons. This version uses the OpenSCAD arguments:
-- @polygon /convexity points path/@ to allow client code to save
-- space.  However, it bypasses all the checks done by
-- 'polygon', which need the other representation.
unsafePolygon :: Int -> [Vector2d] -> [[Int]] -> Model2d
unsafePolygon convexity points paths = Shape $ Polygon convexity points paths

-- | 'offset' a 'Model2d's edges by @offset /delta join/@.
offset :: Double -> Join -> Model2d -> Model2d
offset d j (Shape s) = Shape $ Offset d j s

-- Tools for creating Model3ds
-- | Create a sphere with @sphere /radius 'Facet'/@.
sphere :: Double -> Facet -> Model3d
sphere r f = Solid $ Sphere r f

-- | Create a box with @cube /x-size y-size z-size/@
box :: Double -> Double -> Double -> Model3d
box x y z = Solid $ Box x y z

-- | A convenience function for creating a cube as a 'box' with all
-- sides the same length.
cube :: Double -> Model3d
cube x = box x x x

-- | Create a cylinder with @cylinder /radius height 'Facet'/@.
cylinder :: Double -> Double -> Facet -> Model3d
cylinder h r f = Solid $ Cylinder h r f

-- | Create an oblique cylinder with @cylinder /radius1 height radius2 'Facet'/@.
obCylinder :: Double -> Double -> Double -> Facet -> Model Vector3d
obCylinder r1 h r2 f= Solid $ ObCylinder r1 h r2 f

-- | Turn a list of list of 'Vector3d's and an int into @polyhedron
-- /convexity points 'Sides'/@. The argument to polyhedron is the list
-- of paths that is the second argument to the OpenSCAD polyhedron
-- function, except the points are 'Vector3d's, not the references to
-- 'Vector3d's used in that functions @points@ argument.  The function
-- will build the appropriate function call, using @faces@ if you pass
-- in a side that uses more than 3 points, or @triangles@ if not. Note
-- that @faces@ doesn't work in older versions of OpenSCAD, and
-- @triangles@ is depreciated. Until a mechanism to set the version of
-- OpenSCAD is provided, generating the @faces@ version will cause an
-- error.
--
-- Passing in 'Sides' that have fewer than three points, have
-- collinear points or have points that aren't in the same plane is an
-- error that is caught by the library.
polyhedron ::  Int -> [[Vector3d]] -> Model3d
polyhedron convexity paths
  | any ((< 3) . length) paths = error "Some face has fewer than 3 points."
  | any collinear paths = error "Some face has collinear points."
  | any (not . coplanar) paths = error "Some face isn't coplanar."
  | length vectors /= length (nub vectors) =
    error "Some faces have different orientation."
  | 2 * length edges /= length vectors = error "Some edges are not in two faces."
  | xCross headMax xMax tailMax > 0 =
    error "Face orientations are counterclockwise."
  | otherwise = Solid . Polyhedron convexity points $ sides sidesIn
  where vectors = concatMap (\p -> zip p (tail p ++ [head p])) paths
        edges = nub $ map (Set.fromList . \(a, b) -> [a, b]) vectors
        points = nub $ concat paths
        xMax = maximum points
        faceMax = head $ filter (elem xMax) paths
        (maxFirst, maxLast) = break (== xMax) faceMax
        (headMax, tailMax)  = (if null maxFirst
                                  then last maxLast
                                  else last maxFirst,
                               if null (tail maxLast)
                                  then head maxFirst
                                  else head (tail maxLast))
        xCross a b c  = (\(a, b, c) -> a) $ (a #- b) #* (b #- c)
        sidesIn = map (concatMap (`elemIndices` points)) paths
        sides ss | any ((> 3) . length) ss  = Faces ss
                 | all ((== 3) . length) ss = Triangles ss
                 | otherwise = error "Some faces have fewer than 3 points."

-- | This provides direct access to the OpenSCAD @polyhedron@ command
-- for performance reasons.  This version uses the OpenSCAD arguments:
-- @polyhedron /convexity points 'Sides'/@ to allow client code to
-- save space. However, it bypasses all the checks done by
-- 'polyhedron', which needs the other representation.
unsafePolyhedron :: Int -> [Vector3d] -> Sides -> Model3d
unsafePolyhedron convexity points sides = Solid $ Polyhedron convexity points sides


-- | Transform a 'Model3d' with a 'TransMatrix'
multMatrix :: TransMatrix -> Model3d -> Model3d
multMatrix t m = Solid $ MultMatrix t m

-- | Turn a 'Model2d' into a 'Model3d' exactly as is.
solid :: Model2d -> Model3d
solid = Solid . ToSolid

-- | Extrude a 'Model2d' along a line with @linear_extrude@.
linearExtrude :: Double   -- ^ height
              -> Double   -- ^ twist
              -> Vector2d -- ^ scale
              -> Int      -- ^ slices
              -> Int      -- ^ convexity
              -> Facet
              -> Model2d  -- ^ to extrude
              -> Model3d
linearExtrude h t sc sl c f m = Solid $ LinearExtrude h t sc sl c f m

-- | Rotate a 'Model2d' around the origin with @rotate_extrude
-- /convexity 'Facet' 'Model'/@
rotateExtrude ::  Int -> Facet -> Model2d -> Model3d
rotateExtrude c f m = Solid $ RotateExtrude c f m

-- | Load a height map from a file with @surface /FilePath Invert Convexity/@.
surface :: FilePath -> Bool -> Int -> Model3d
surface f i c = Solid $ Surface f i c

-- And the one polymorphic function we have.
-- | 'importFile' is @import /filename/@.
importFile :: Vector v => FilePath -> Model v
importFile = Import


-- Transformations
-- | Scale a 'Model', the vector specifying the scale factor for each axis.
scale :: Vector v => v -> Model v -> Model v
scale = Scale

-- | Resize a 'Model' to occupy the dimensions given by the vector. Note that
-- this does nothing prior to the 2014 versions of OpenSCAD.
resize :: Vector v => v -> Model v -> Model v
resize = Resize

-- | Rotate a 'Model' by different amounts around each of the three axis.
rotate :: Vector v => v -> Model v -> Model v
rotate = Rotate

-- | Translate a 'Model' along a 'Vector'.
translate :: Vector v => v -> Model v -> Model v
translate = Translate

-- | Mirror a 'Model' across a plane intersecting the origin.
mirror :: Vector v => v -> Model v -> Model v
mirror = Mirror

-- | Render a 'Model' in a specific color. This doesn't use the
-- OpenSCAD color model, but instead uses the 'Data.Colour' model. The
-- 'Graphics.OpenSCAD' module rexports 'Data.Colour.Names' so you can
-- conveniently say @'color' 'red' /'Model'/@.
color :: Vector v => Colour Double -> Model v -> Model v
color = Color

-- | Render a 'Model' in a transparent color. This uses the
-- 'Data.Colour.AlphaColour' color model.
transparent :: Vector v => AlphaColour Double -> Model v -> Model v
transparent = Transparent

-- | A 'translate' that just goes up, since those seem to be common.
up :: Double -> Model3d -> Model3d
up f = translate (0, 0, f)


-- Combinations
-- | Create the union of a list of 'Model's.
union :: Vector v => [Model v] -> Model v
union = Union

-- | Create the intersection of a list of 'Model's.
intersection :: Vector v => [Model v] -> Model v
intersection = Intersection

-- | The difference between two 'Model's.
difference :: Vector v => Model v -> Model v -> Model v
difference = Difference

-- | The Minkowski sum of a list of 'Model's.
minkowski :: Vector v => [Model v] -> Model v
minkowski = Minkowski

-- | The convex hull of a list of 'Model's.
hull :: Vector v => [Model v] -> Model v
hull = Hull


-- | 'render' does all the real work. It will walk the AST for a 'Model',
-- returning an OpenSCAD program in a 'String'.
render :: Vector v => Model v -> String
render (Shape s) = rShape s
render (Solid s) = rSolid s
render (Union ss) = rList "union()" ss
render (Intersection ss) = rList "intersection()" ss
render (Difference s1 s2) = "difference(){" ++ render s1 ++ render s2 ++ "}\n"
render (Minkowski ss) = rList "minkowski()" ss
render (Hull ss) = rList "hull()" ss
render (Scale v s) = rVecSolid "scale" v s
render (Resize v s) = rVecSolid "resize" v s
render (Translate v s) = rVecSolid "translate" v s
render (Rotate v s) = "rotate(" ++ rVector v ++ ")" ++ render s
render (Mirror v s) = rVecSolid "mirror" v s
render (Import f) = "import(\"" ++ f ++ "\");\n"
render (Color c s) = let r = toSRGB c in
    "color(" ++ rVector (channelRed r, channelGreen r, channelBlue r) ++ ")\n"
    ++ render s
render (Transparent c s) =
    "color(" ++ rQuad (channelRed r, channelGreen r, channelBlue r, a) ++ ")"
    ++ render s
    where r = toSRGB $ toPure c
          a = alphaChannel c
          toPure ac = if a > 0 then darken (recip a) (ac `over` black) else black
render (Var (Fa f) ss) = rList ("assign($fa=" ++ show f ++ ")") ss
render (Var (Fs f) ss) = rList ("assign($fs=" ++ show f ++ ")") ss
render (Var (Fn n) ss) = rList ("assign($fn=" ++ show n ++ ")") ss

-- utility for rendering Shapes.
rShape :: Shape -> String
rShape (Rectangle r f) = "square([" ++ show r ++ "," ++ show f ++ "]);\n"
rShape (Circle r f) = "circle(" ++ show r ++ rFacet f ++ ");\n"
rShape (Projection c s) =
  "projection(cut=" ++ (if c then "true)" else "false)") ++ render s
rShape (Polygon c points paths) = "polygon(points=" ++ rVectorL points ++
 ",paths=" ++ show paths ++ ",convexity=" ++ show c ++ ");\n"
rShape (Offset d j s) =
  "offset(delta=" ++ show d ++ "," ++ rJoin j ++ ")" ++ rShape s

-- utility for rendering Joins
rJoin :: Join -> String
rJoin Bevel = "join_type=bevel"
rJoin Round = "join_type=round"
rJoin (Miter l) = "miter_limit=" ++ show l

-- utilities for rendering Solids.
rSolid :: Solid -> String
rSolid (Sphere x f) = "sphere(" ++ show x ++ rFacet f ++ ");\n"
rSolid (Box x y z) =
  "cube([" ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]);\n"
rSolid (Cylinder r h f) =
  "cylinder(r=" ++ show r ++ ",h=" ++ show h ++ rFacet f ++ ");\n"
rSolid (ObCylinder r1 h r2 f) =
    "cylinder(r1=" ++ show r1 ++ ",h=" ++ show h ++ ",r2=" ++ show r2 ++ rFacet f
    ++ ");\n"
rSolid (Polyhedron c ps ss) = "polyhedron(points=" ++  rVectorL ps ++ rSides ss
                              ++ ",convexity=" ++ show c ++ ");\n"
rSolid (MultMatrix (a, b, c, d) s) =
    "multmatrix([" ++ rQuad a ++ "," ++ rQuad b ++ "," ++ rQuad c ++ ","
    ++ rQuad d ++"])\n" ++ render s
rSolid (LinearExtrude h t sc sl c f sh) =
    "linear_extrude(height=" ++ show h ++ ",twist=" ++ show t ++ ",scale="
    ++ rVector sc ++ ",slices=" ++ show sl ++ ",convexity=" ++ show c
    ++ rFacet f ++ ")" ++ render sh
rSolid (RotateExtrude c f sh) =
  "rotate_extrude(convexity=" ++ show c ++ rFacet f ++ ")" ++ render sh
rSolid (Surface f i c) =
  "surface(file=\"" ++ f ++ "\"," ++ (if i then "invert=true," else "")
  ++ "convexity=" ++ show c ++ ");\n"
rSolid (ToSolid s) = render s

-- render a list of vectors as an Openscad vector of vectors.
rVectorL vs = "[" ++ intercalate "," (map rVector vs) ++ "]"

-- render a Sides.
rSides (Faces vs) = ",faces=" ++ rListL vs
rSides (Triangles vs) = ",triangles=" ++ rListL vs
rListL vs = "[" ++ intercalate "," (map show vs) ++ "]"

-- | A convenience function to render a list of 'Model's by taking
-- their union.
renderL :: Vector v => [Model v] -> String
renderL = render . union

-- | A convenience function to write the rendered 'Model' to
-- standard output.
draw :: Vector v => Model v -> IO ()
draw = putStrLn . render

-- | A convenience function to write a 'union' of 'Model's to
-- standard output.
drawL :: Vector v => [Model v] -> IO ()
drawL = draw . Union

-- And some misc. rendering utilities.
rList n ss = n ++ "{\n" ++  concatMap render ss ++ "}"
rVecSolid n v s = n ++ "(" ++ rVector v ++ ")\n" ++ render s
rQuad (w, x, y, z) =
  "[" ++ show w ++ "," ++ show x ++ "," ++ show y ++ "," ++ show z ++ "]"
rFacet Def = ""
rFacet f = "," ++ showFacet f

-- render a facet setting.
showFacet :: Facet -> String
showFacet (Fa f) = "$fa=" ++ show f
showFacet (Fs f) = "$fs=" ++ show f
showFacet (Fn n) = "$fn=" ++ show n
showFacet Def    = ""

-- Convenience functions for Facets.
-- | 'var' uses @assign@ to set a 'Facet' variable for it's 'Model's.
var :: Facet -> [Model v] -> Model v
var = Var

-- | 'fa' is used to set the @$fa@ variable in a 'Facet' or 'var'.
fa :: Double -> Facet
fa = Fa

-- | 'fs' is used to set the @$fs@ variable in a 'Facet' or 'var'.
fs :: Double -> Facet
fs = Fs

-- | 'fn' is used to set the @$fn@ variable in a 'Facet' or 'var'.
fn :: Int -> Facet
fn = Fn

-- | 'def' is used where a 'Facet' is needed but we don't want to change
-- any of the values.
def :: Facet
def = Def

-- And one last convenience function.
-- | Use 'diam' to turn a diameter into a radius for circles, spheres, etc.
diam :: Double -> Double
diam = (/ 2)
-- Now, let Haskell work it's magic
instance Vector v => Semigroup (Model v) where
  a <> b = union [a, b]
  sconcat = union . NE.toList

instance Vector v => Monoid (Model v) where
  mempty = Solid $ Box 0 0 0
  mappend (Solid (Box 0 0 0)) b = b
  mappend a (Solid (Box 0 0 0)) = a
  mappend a b = union [a, b]
  mconcat [a] = a
  mconcat as = union as


-- | You can use '(#)' to write transformations in a more readable postfix form,
--   cube 3 # color red # translate (-3, -3, -3)
infixl 8 #
(#) = flip ($)