1# Programming
2
3The Functional Geometry system (FG) provides both a low-level and a high-level API.
4* The high level API includes familiar operations like sphere(), translate()
5  and intersection(),
6  plus new operations like shell(), morph() and perimeter_extrude().
7* The low level API allows users to directly define new primitive operations
8  using functional representation.
9
10The low level API is quite powerful. Most of the OpenSCAD geometry
11primitives can be defined in a few hundred lines of OpenSCAD2.
12That's very impressive, compared to the amount of code required to implement
13these operations on a mesh.
14The low level API is
15also subtle and tricky. So you can define `sphere()`, `union()`, etc, with 3
16lines of code per primitive, but they can be subtle and tricky lines of code.
17
18This document is a tutorial introduction to the low level API,
19which is illustrated by showing you how to define a lot of interesting
20high level operations.
21There's enough detail so that you can really wrap your head around
22the idioms of creating geometry using Functional Representation.
23
24## Benefits and Limitations
25
26When compared to the mesh-based programming model of OpenSCAD,
27the Functional Geometry API has both benefits and limitations.
28
29* **expressive power** <br>
30  A primary benefit of Functional Geometry is that you can define powerful new
31  geometric primitives with only a few lines of OpenSCAD2 code.
32  Most of OpenSCAD's geometry kernel can be implemented, quite simply,
33  at user level. By contrast, programming these same primitives using a mesh
34  is notoriously difficult, with lots of edge conditions to deal with,
35  and a lot of additional complexity if you use floating point (as opposed
36  to the slow and memory-inefficient rational numbers used by OpenSCAD).
37
38* **direct access to the representation of shapes** <br>
39  In OpenSCAD, shapes are opaque: you can't access their representation;
40  you can't even query the bounding box, let alone access the vertexes and faces.
41  This restriction is due to an engineering tradeoff: we want preview to be fast,
42  and we don't generate the mesh representation of CSG operations during preview.
43  If we were to provide access to the mesh during preview, then preview would become as slow as rendering.
44  Functional Geometry has no such tradeoff, and much of the programming power is due
45  to the fact that we provide full access to the underlying functional representation.
46
47* **curved surfaces** <br>
48  It's extremely easy to work with curved surfaces in Functional Geometry,
49  something that is a major weakness of OpenSCAD.
50  * A curved object is represented by its mathematical formula,
51    which makes curved surfaces and organic forms easy to define with very little code.
52  * Internally, a curved object is represented exactly. It's like programming in
53    OpenSCAD with `$fn=∞`. Curved objects can be exactly rendered in the preview window at full resolution,
54    even for complex models. You can zoom in many orders of magnitude without the abstraction
55    breaking down, limited only by floating point resolution.
56    By contrast, in OpenSCAD, curved surfaces are represented by polygonal
57    approximations that are chosen when the object is created, and errors accumulate as
58    these curved surfaces are further transformed. In OpenSCAD, you must find
59    a value of `$fn` that's high enough to achieve the print quality you want
60    while still low enough to prevent OpenSCAD from getting too slow or crashing while
61    you are working on the design.
62
63* **complex objects with micro-fine detail** <br>
64  Using Functional Geometry, it is possible to create models that have huge
65  amounts of procedurally generated detail, without making preview too slow
66  or exceeding your memory limit. For example, fractals or digital fabrics.
67  You can create complex models that would be impossible in OpenSCAD, because
68  too many triangles would be required. To accomplish this, you need to use
69  either the (new) high level spatial repetition and patterning operators, or the low level API,
70  since unioning a million objects is just as problematic in FG as it is in OpenSCAD.
71
72* **no "non-manifold objects"** <br>
73  The OpenSCAD rendering engine, based on CGAL, is very picky about "non-manifold objects",
74  so you have to use tricks to perturb your model in ways to avoid these errors.
75  The problem doesn't occur in preview mode, and it's not something you worry about
76  in Functional Geometry either.
77
78* **high level annoyances** <br>
79  Of course, Functional Geometry has its own limitations.
80  We will discover that the FG high level API has its own annoyances that affect
81  the programming model.
82
83* **low level annoyances** <br>
84  The FG low level API has an ease of use problem, similar to `polyhedron` in OpenSCAD.
85  It's possible to write a bad distance function for a primitive shape,
86  which could cause error messages or rendering problems later, and it's hard to
87  automatically detect and report these problems when the shape is constructed.
88
89* **no `minkowski` or `hull`** <br>
90  There's no efficient implementation of Minkowski Sum and Convex Hull.
91  Of course, we could convert functional representation to a mesh, and run the
92  mesh versions of these operations, but that's even slower than OpenSCAD.
93  Even in OpenSCAD, these are slow operations that kill preview performance.
94  Fortunately, there are good alternatives to the OpenSCAD idioms that
95  use these operations, which preview quickly.
96
97* **not a boundary representation** <br>
98  Functional representation does not directly represent the boundary of an object in the
99  same way that a mesh does. This may lead to compromises when what you really want is
100  direct control over the mesh, for example in STL export. It may require you to
101  make additional decisions to configure space/time/accuracy tradeoffs during mesh generation.
102  If necessary, I'll investigate a hybrid approach to mitigate this problem,
103  but using mesh features will take away from the simplicity of Functional Geometry programming.
104
105## Functional Representation (F-Rep)
106
107F-Rep (functional representation) is a leading alternative to B-Rep (boundary representation)
108as a general purpose representation for geometric objects. Examples of B-Rep include the
109OpenSCAD polyhedral mesh, and spline based representations.
110Like voxel representations, F-Rep is considered a volumetric representation.
111
112In F-Rep, a shape is represented by a distance field:
113a scalar field that specifies the minimum distance to a shape.
114The distance is signed: negative inside the shape, zero on the boundary,
115and positive outside the shape.
116
117A distance field is a function that maps every point in space (specified as [x,y,z])
118to a distance value. A distance function can be derived from the mathematical equation
119for a shape. For example, a sphere of radius `r` has the equation
120```
121x^2 + y^2 + z^2 = r^2
122```
123This equation is true for all [x,y,z] points on the surface of the sphere.
124
125We can rewrite this as `x^2 + y^2 + z^2 - r^2 = 0`,
126and then derive a scalar field
127```
128f[x,y,z] = x^2 + y^2 + z^2 - r^2
129```
130which has the value zero for points on the surface of the sphere.
131
132To convert this to a distance field, we need one more transformation:
133```
134f[x,y,z] = sqrt(x^2 + y^2 + z^2) - r
135```
136
137### Constraints on Distance Functions
138We need to constrain distance functions so that the algorithms
139used to render shapes and export them to other data structures
140will still work.
141
142A distance function returns a *minimum distance* `d`
143between the point argument and the nearest object boundary.
144* If `d` is 0,
145  then the point must be on an object boundary.
146* Otherwise, the nearest object boundary
147  must be at least `abs(d)` units away,
148  but it could be farther. There is room for flexibility
149  because of isosurfaces, discussed below.
150* If this constraint is violated, then the ray-marching algorithm
151  for directly rendering a shape on a GPU will not work.
152
153A distance function must be [strictly increasing](http://mathworld.wolfram.com/StrictlyIncreasingFunction.html)
154(for points outside of objects) or strictly decreasing
155(for points inside of objects) as the point gets
156increasingly farther away from the nearest object boundary.
157This paragraph is speculative.
158
159### Isosurfaces
160The isosurface at distance d of a distance field f
161is the set of all points p such that f(p)=d.
162The isosurface at distance 0 is the boundary of the shape.
163The isosurface at distance 1 is another boundary, a shell
164that encloses the shape, and is separated from it by a minimum distance of 1.
165Isosurfaces are important because they are used to compute shells.
166
167When you design a distance function, you aren't just specifying
168the boundary of the shape, you are also specifying an infinite family
169of isosurfaces that enclose or are enclosed by the shape.
170It's important to consider what all of these isosurfaces look like,
171because that will determine what shells look like, as computed by
172the `shell` and `inflate` operations.
173
174### Infinite Shapes
175Distance fields are not restricted to representing finite geometrical objects.
176They can also represent infinite space-filling patterns.
177For examples, try a Google image search on
178[k3dsurf periodic lattice](https://www.google.ca/search?q=k3dsurf+periodic+lattice&tbm=isch).
179These infinite patterns are useful in 3D modelling:
180you can intersect them or subtract them from a finite 3D object.
181
182### Materials and Colours
183A material field or a colour field can be associated with a shape,
184assigning a material or colour to every point within the volume of a shape.
185This is an aspect of F-Rep that will be added to FG in a later revision.
186
187### Other Resources
188
189This topic is hard to google, because so many different terms are used.
190Try F-Rep, FRep, functional representation,
191On the Representation of Shapes Using Implicit Functions,
192distance field, distance function, signed distance function,
193isosurface, procedural modeling.
194
195Try this essay on
196[the mathematical basis of F-Rep](https://christopherolah.wordpress.com/2011/11/06/manipulation-of-implicit-functions-with-an-eye-on-cad/)
197by Christopher Olah, inventor of ImplicitCAD.
198
199## The Representation of Shapes in FG
200In the Functional Geometry system, every shape has two fields:
201* `dist`, a distance function.
202* `bbox`, a function which maps a distance value `d`
203  onto the bounding box of the isosurface of `d`.
204
205A bounding box has the representation `[[xmin,ymin,zmin],[xmax,ymax,zmax]]`.
206A bounding box may be larger than is strictly necessary to enclose the
207isosurface, but it must not be smaller. This flexibility is permitted
208due to the difficulty in computing an exact bounding box for some shapes.
209
210Empty shapes are supported, but the bounding box must also be empty.
211A bounding box has zero width along the X axis if xmin >= xmax,
212which means the bounding box is empty. Likewise for the Y and Z axes.
213
214A bbox function should handle a distance argument outside the range of
215its associated dist function by returning the empty bounding box.
216This can happen when computing shells. If you are lucky, then an out of range
217distance argument will result in your bbox function returning a bounding box
218where xmin > xmax, or ymin > ymax, or zmin > zmax, just because the arithmetic
219works out that way, and not because you wrote special case code.
220
221Both finite and infinite shapes are supported.
222If a shape extends to infinity along any of the 6 cardinal directions,
223then this is indicated in the bounding box by setting xmin, ymin or zmin
224to -inf, or by setting xmax, ymax or zmax to inf.
225
226A distance field can, in principle, specify a degenerate shape that has zero thickness
227across some or all of its extension. Which classes of degenerate shapes that we
228actually support is an open question. We use 64 bit IEEE floats for geometry
229computations, so shapes could become degenerate as a result of floating point underflow.
230This suggests that the FG geometry engine should be tolerant of degeneracy.
231
232(In OpenSCAD, a mesh computation with valid inputs can produce a non-manifold
233output, even though CGAL is being used throughout. And then you get CGAL
234exceptions. It's the same for all other mesh libraries. How hard is it to make
235FG robust against this kind of problem?)
236
237### Rationale
238Why do we need to specify a bounding box?
239Because the geometry kernel needs it to render STL, and to pick a bounding box
240for the preview window.
241
242There are two kinds of F-Rep geometry systems:
243* Systems like Hyperfun or ShapeJS require the user to specify a global bounding
244  box for all the geometry in a project. But primitive operations can be coded
245  without specifying a bounding box, since that's the user's problem.
246* Systems like ImplicitCAD, Antimony and FG automatically compute a bounding box
247  for each shape and operator, so that the user doesn't have to, but this moves
248  the complexity into the definition of each primitive.
249
250Why do we need to specify a bounding box *function*, instead of just a single
251bounding box for the isosurface at value 0?
252Because it's required by `inflate`, and other operations that compute shells.
253In the general case, you can't predict the bounding box of a shell from
254the bounding box of the isosurface at 0. A few primitives
255have "weird" isosurfaces with unusual bounding boxes, including
256anisotropic `scale`.
257* NOTE: I've changed my mind. Inflate will now assert that its input needs to
258  be a Euclidean distance field, and primitives lower in the tree will switch
259  to a more expensive algorithm (if required) to ensure this.
260
261## Low Level API
262
263`3dshape(dist([x,y,z])=..., bbox(d)=...)`
264> Returns a functional 3D shape, specified by a distance function
265> and a bounding box function.
266
267`2dshape(dist([x,y])=..., bbox(d)=...)`
268> Returns a functional 2D shape, specified by a distance function
269> and a bounding box function.
270
271The low level API contains utility operations that are used
272to define new operations that map shapes onto shapes.
273Shapes can be queried at run-time for their distance
274and bounding box functions.
275
276`shape.dist`
277> the distance function of a shape
278
279`shape.bbox`
280> the bounding box function of a shape
281
282## Circle and Sphere
283A circle of radius `r`, centred on the origin:
284```
285circle(r) = 2dshape(
286  dist(p) = norm(p) - r,
287  bbox(d)=[[-r,-r]-d,[r,r]+d]);
288```
289
290New style:
291```
292circle(r:is_num) = make_shape{
293  dist(req)(p:is_vec3) = norm(p) - r,
294  bbox=[[-r,-r],[r,r]]};
295```
296
297The `dist` function is derived from
298[the mathematical equation for a circle](https://en.wikipedia.org/wiki/Circle#Equations):
299```
300   x^2 + y^2 = r^2
301-> sqrt(x^2 + y^2) = r
302-> sqrt(x^2 + y^2) - r = 0
303-> dist[x,y] = sqrt(x^2 + y^2) - r
304-> dist(p) = norm(p) - r
305```
306This particular `dist` function returns the shortest euclidean distance
307from the point to the perimeter of the circle.
308
309Every `dist` function defines an infinite family of isosurfaces,
310or in the 2D case, isolines.
311For this particular `dist` function, all of the isolines are circles,
312so the shell of a circle is another circle.
313
314Most primitive operations, including `circle`, have isosurfaces that behave in a standard way:
315the bbox expands outwards by 1 unit for each unit increase in d.
316For these operations, the bbox function has the form:
317```
318bbox(d) = [[...]-d, [...]+d]
319```
320
321The range of our `dist` function is infinity to `-r`.
322
323A `bbox` function is required to return the empty bbox
324if it is passed an argument outside the range of the `dist` function.
325In this case, the math works out so that this happens automatically,
326without using an `if` expression. Eg,
327```
328  c = circle(2);
329  c.bbox(-3) == [[1,1],[-1,-1]]
330```
331And this bbox is empty since xmin > xmax, and ymin > ymax.
332
333The definition of `sphere` is very similar:
334```
335sphere(r) = 3dshape(
336  dist(p) = norm(p) - r,
337  bbox(d)=[[-r,-r,-r]-d,[r,r,r]+d]);
338```
339
340## CSG Operations
341```
342union(s1,s2) = 3dshape(
343  dist(p) = min(s1.dist(p), s2.dist(p)),
344  bbox(d)=[ min(s1.bbox(d)[0],s2.bbox(d)[0]), max(s1.bbox(d)[1],s2.bbox(d)[1]) ]);
345```
346```
347intersection(s1,s2) = 3dshape(
348  dist(p) = max(s1.dist(p), s2.dist(p)),
349  bbox(d)=[ max(s1.bbox(d)[0],s2.bbox(d)[0]), min(s1.bbox(d)[1],s2.bbox(d)[1]) ]);
350```
351Union and intersection are implemented as the minimum and maximum
352of the shape argument distance functions. This computes the union or
353intersection of all of the isosurfaces in the distance functions.
354Whenever you see `min` or `max` of a distance, in a distance function,
355a union or intersection is being computed.
356Also, it's likely that a sharp angle is being introduced into the shape.
357
358```
359complement(s) = 3dshape(
360  dist(p) = -s.dist(p),
361  bbox(d) = [[-inf,-inf,-inf],[inf,inf,inf]] );
362```
363The `complement` operation reverses the inside and outside of a shape.
364The result is infinite.
365It's useful when working with infinite space filling patterns,
366and for defining `difference`.
367
368```
369difference(a,b) = intersection(a, complement(b));
370```
371Subtract `b` from `a`.
372
373## Rectangle and Cuboid
374
375Consider an axis-aligned rectangle, specified by `[[xmin,ymin],[xmax,ymax]]`.
376This is a convex 4-sided polygon, which we can compute by
377[intersecting 4 half-planes](https://en.wikipedia.org/wiki/Convex_polytope#Intersection_of_half-spaces).
378
379Consider the half-plane containing all points such that x >= xmin.
380Rewrite this as xmin <= x.
381The corresponding distance function is `xmin - x`.
382
383Here are the 4 half-planes, and their distance functions:
384```
385x >= xmin    xmin - x
386x <= xmax    x - xmax
387y >= ymin    ymin - y
388y <= ymax    y - ymax
389```
390Here is their intersection:
391```
392max(xmin - x, x - xmax, ymin - y, y - ymax)
393```
394And here's the code:
395```
396box2([[xmin,ymin],[xmax,ymax]]) = 2dshape(
397  dist([x,y]) = max(xmin - x, x - xmax, ymin - y, y - ymax),
398  bbox(d)=[ [xmin,ymin]-d, [xmax,ymax]+d ]);
399```
400
401Now let's design a centred rectangle primitive, where the width and height are specified by `[dx,dy]`.
402
403The distance function is `max(-dx/2 - x, x - dx/2, -dy/2 - y, y - dy/2)`.
404
405We can simplify and speed it up: `max(abs(x) - dx/2, abs(y) - dy/2)`,
406exploiting the fact that the distance is symmetric around the origin.
407
408Here's the code:
409```
410rectangle([dx,dy]) = 2dshape(
411  dist([x,y,z]) = max(abs(x) - dx/2, abs(y) - dy/2),
412  bbox(d)=[ [-dx/2,-dy/2]-d, [dx/2,dy/2]+d ]);
413```
414
415Cuboid is similar; conceptually it works by
416[intersecting 6 half-spaces to make the 6 faces of the cuboid](https://en.wikipedia.org/wiki/Convex_polytope#Intersection_of_half-spaces):
417```
418cuboid([dx,dy,dz]) = 3dshape(
419  dist([x,y,z]) = max(abs(x)-dx/2, abs(y)-dy/2, abs(z)-dz/2),
420  bbox(d)=[ [-dx/2,-dy/2,-dz/2]-d, [dx/2,dy/2,dz/2]+d ]);
421```
422
423This technique can be generalized to model any convex polygon/polyhedron,
424by intersecting one half-plane/half-space for each edge/face.
425It can be further generalized to non-convex polytopes by using
426[Nef polygons/polyhedra](https://en.wikipedia.org/wiki/Nef_polygon),
427where both intersection and complement operations are used.
428
429However, this technique requires each edge/face to be examined in each
430call to the distance function. It will become inefficient for sufficently large
431numbers of edges/faces; for that, another approach should be found.
432
433## Cylinder and Linear_extrude
434
435We will construct a centred cylinder by intersecting 3 infinite shapes:
436two half-spaces (one for each end cap), and an infinite cylinder
437for the body. This is similar to the construction of a cuboid
438from six half-spaces.
439
440We'll first construct an infinite cylinder centred on the Z-axis.
441To do this, we just take the distance function for a centred circle,
442and put that into a 3D distance function:
443```
444dist([x,y,z]) = norm([x,y]) - r
445```
446We get infinite extension along the Z axis by ignoring the `z` parameter.
447This way, every slice through the distance field that is parallel to the X-Y plane
448looks exactly the same.
449
450The two half-spaces follow from our construction for cuboid.
451```
452z <= h/2    z - h/2
453z >= -h/2   -h/2 - z
454```
455The intersection of these three infinite solids is
456```
457max(norm([x,y]) - r, z - h/2, -h/2 - z)
458```
459We'll apply the same optimization for a centred object that we used for cuboid:
460```
461max(norm([x,y]) - r, abs(z) - h/2)
462```
463And here's the code:
464```
465cylinder(h,r) = 3dshape(
466  dist([x,y,z]) = max(norm([x,y]) - r, abs(z) - h/2),
467  bbox(d)=[ [-r,-r,-h/2]-d, [r,r,h/2]+d ]);
468```
469This can be generalized to `linear_extrude`:
470```
471linear_extrude(h)(s) = 3dshape(
472  dist([x,y,z]) = max(s.dist([x,y]), abs(z) - h/2),
473  bbox(d)=[ [s.bbox(d)[0].x, s.bbox(d)[0].y, -h/2-d],
474            [s.bbox(d)[1].x, s.bbox(d)[1].y, h/2+d] ]);
475```
476Now cylinder can be defined like this:
477```
478cylinder(h,r) = linear_extrude(h) circle(r);
479```
480
481## Rotate_extrude
482
483(Torus from Olah's essay):
484We can use the output of one of these functions as the input for another. For example, if we feed r = \sqrt{x^2+y^2} - k_1, the outwardness of a circle on the x,y-plane, into a new circle which we can think of as being on the r,z-plane, \sqrt{r^2+z^2}-k_2, we get a torus.
485That is, \sqrt{(\sqrt{x^2+y^2} - k_1)^2+z^2}-k_2 = 0 results in a torus.
486
487
488rotate_extrude from ImplicitCAD
489```
490getImplicit3 (RotateExtrude totalRotation round translate rotate symbObj) =
491    let
492        tau = 2 * pi
493        k   = tau / 360
494        totalRotation' = totalRotation*k
495        obj = getImplicit2 symbObj
496        capped = Maybe.isJust round
497        round' = Maybe.fromMaybe 0 round
498        translate' :: ℝ -> ℝ2
499        translate' = Either.either
500                (\(a,b) -> \θ -> (a*θ/totalRotation', b*θ/totalRotation'))
501                (. (/k))
502                translate
503        rotate' :: ℝ -> ℝ
504        rotate' = Either.either
505                (\t -> \θ -> t*θ/totalRotation' )
506                (. (/k))
507                rotate
508        twists = case rotate of
509                   Left 0  -> True
510                   _       -> False
511    in
512        \(x,y,z) -> minimum $ do
513
514            let
515                r = sqrt (x^2 + y^2)
516                θ = atan2 y x
517                ns :: [Int]
518                ns =
519                    if capped
520                    then -- we will cap a different way, but want leeway to keep the function cont
521                        [-1 .. (ceiling (totalRotation'  / tau) :: Int) + (1 :: Int)]
522                    else
523                        [0 .. floor $ (totalRotation' - θ) /tau]
524            n <- ns
525            let
526                θvirt = fromIntegral n * tau + θ
527                (rshift, zshift) = translate' θvirt
528                twist = rotate' θvirt
529                rz_pos = if twists
530                        then let
531                            (c,s) = (cos(twist*k), sin(twist*k))
532                            (r',z') = (r-rshift, z-zshift)
533                        in
534                            (c*r' - s*z', c*z' + s*r')
535                        else (r - rshift, z - zshift)
536            return $
537                if capped
538                then MathUtil.rmax round'
539                    (abs (θvirt - (totalRotation' / 2)) - (totalRotation' / 2))
540                    (obj rz_pos)
541                else obj rz_pos
542```
543
544ExtrudeOnEdgeOf from ImplicitCAD
545```
546getImplicit3 (ExtrudeOnEdgeOf symbObj1 symbObj2) =
547    let
548        obj1 = getImplicit2 symbObj1
549        obj2 = getImplicit2 symbObj2
550    in
551        \(x,y,z) -> obj1 (obj2 (x,y), z)
552getBox3 (ExtrudeOnEdgeOf symbObj1 symbObj2) =
553    let
554        ((ax1,ay1),(ax2,ay2)) = getBox2 symbObj1
555        ((bx1,by1),(bx2,by2)) = getBox2 symbObj2
556    in
557        ((bx1+ax1, by1+ax1, ay1), (bx2+ax2, by2+ax2, ay2))
558```
559```
560perimeter_extrude(a,b) = 3dshape(
561  dist([x,y,z]) = a.dist([b.dist([x,y]), z]),
562  bbox(d)=let ([[ax1,ay1],[ax2,ay2]] = a.bbox(d),
563               [[bx1,by1],[bx2,by2]] = b.bbox(d))
564          [[bx1+ax1, by1+ax1, ay1], [bx2+ax2, by2+ax2, ay2]] );
565```
566Extrude 2D object `a` around the perimeter of 2D object `b` to get a 3D object.
567The perimeter of 'b' lies on the X-Y plane. The (0,0) point of `a` is aligned with
568the perimeter as `a` is swept around the perimeter.
569
570If `a` and `b` are both circles, then you get a torus.
571
572This is ExtrudeOnEdgeOf from ImplicitCAD.
573Based on a few rendered examples, figure `a` is normal to the perimeter at each
574point on the perimeter, which is what you want. But how does it work?
575
576```
577rotate_extrude(r)(s) = perimeter_extrude(s, circle(r));
578torus(r1,r2) = rotate_extrude(r1) circle(r2);
579```
580
581## Shells
582
583There are many different ways to write a distance function that produce the same visible result.
584The main requirement is that the isosurface of the distance function at 0
585(the set of all points such that f[x,y,z] == 0)
586is the boundary of the desired object, and there are infinitely many functions that satisfy this
587requirement for any given shape.
588
589But the isosurface at 0 is not the only thing you need to consider.
590A distance function defines an infinite family of isosurfaces,
591and they are all important, particular when you use the `shell` primitive.
592Consider
593```
594c = cuboid([10,20,30]);
595```
596`c.dist` is the distance function. Because of the way we defined `cuboid`,
597the isosurface of `c.dist` at -1 is cuboid(8,18,28]), the isosurface at 1
598is cuboid([12,22,32]), and so on. All of the isosurfaces of `c` are cuboids,
599although with varying aspect ratios.
600As a result, `shell(1) sp` will return a spherical shell with outer radius 10
601and inner radius 9. The `shell` operator provides direct access to the other isosurfaces.
602(See the Shell section, which is later.)
603
604I have similar comments for `cuboid`.
605The distance function is designed so that all of the isosurfaces of a given
606cuboid are nested cuboids, and `shell` works as expected.
607
608Here's a thought experiment. Consider defining the distance function for `cuboid`
609so the the distance to the boundary is measured in millimeters along a vector that points to the centre.
610With this definition, only the isosurface at 0 would be a true cuboid. The isosurfaces at positive values
611would have increasingly convex sides, so that at large values, the surface would approach a sphere.
612The isosurfaces at negative values would have increasingly concave sides.
613This behaviour would be visible using `shell`.
614
615
616## Primitive Shapes
617Here I'll define 3 primitive shapes: sphere, cuboid and cylinder.
618The main lesson will be learning how to write a distance function.
619
620```
621sphere(r) = 3dshape(
622    f([x,y,z]) = sqrt(x^2 + y^2 + z^2) - r,
623    bbox=[[-r,-r,-r],[r,r,r]]);
624```
625
626In the F-Rep section, I said that `f([x,y,z]) = x^2 + y^2 + z^2 - r^2`
627is a distance function for a sphere, so why did we change it?
628
629There are many different ways to write a distance function that produce the same visible result.
630The main requirement is that the isosurface of the distance function at 0
631(the set of all points such that f[x,y,z] == 0)
632is the boundary of the desired object, and there are infinitely many functions that satisfy this
633requirement for any given shape.
634
635But the isosurface at 0 is not the only thing you need to consider.
636A distance function defines an infinite family of isosurfaces,
637and they are all important, particular when you use the `shell` primitive.
638Consider
639```
640sp = sphere(10);
641```
642`sp.f` is the distance function. Because of the way we defined `sphere`,
643the isosurface of `sp.f` at -1 is a sphere of radius 9, the isosurface at 1
644is a sphere of radius 11, and so on. All of the isosurfaces of `sp` are spheres.
645As a result, `shell(1) sp` will return a spherical shell with outer radius 10
646and inner radius 9. The `shell` operator provides direct access to the other isosurfaces.
647(See the Shell section, which is later.)
648
649```
650cuboid(sz) = 3dshape(
651  f([x,y,z]) = max([abs(x-sz.x/2), abs(y-sz.y/2), abs(z-sz.z/2)]),
652  bbox=[ [0,0,0], sz ]);
653```
654I have similar comments for `cuboid`.
655The distance function is designed so that all of the isosurfaces of a given
656cuboid are nested cuboids, and `shell` works as expected.
657
658Here's a thought experiment. Consider defining the distance function for `cuboid`
659so the the distance to the boundary is measured in millimeters along a vector that points to the centre.
660With this definition, only the isosurface at 0 would be a true cuboid. The isosurfaces at positive values
661would have increasingly convex sides, so that at large values, the surface would approach a sphere.
662The isosurfaces at negative values would have increasingly concave sides.
663This behaviour would be visible using `shell`.
664
665Note the use of `max` in the distance function for `cuboid`.
666Generally speaking, if your distance function computes the max of a function of `x` and a function of `y`,
667then your shape will have a right angle between the X and Y axes.
668Read [Olah's essay](https://christopherolah.wordpress.com/2011/11/06/manipulation-of-implicit-functions-with-an-eye-on-cad/) for more discussion of this.
669
670```
671cylinder(h,r) = linear_extrude(h=h) circle(r);
672```
673
674```
675circle(r) = 2dshape(
676    f([x,y]) = sqrt(x^2 + y^2) - r,
677    bbox=[[-r,-r],[r,r]]);
678```
679
680```
681linear_extrude(h)(shape) = 3dshape(
682    f([x,y,z]) = max(shape.f(x,y), abs(z - h/2) - h/2),
683    bbox=[ [shape.bbox[0].x,shape.bbox[0].y,-h/2], [shape.bbox[1].x,shape.bbox[1].y,h/2] ]);
684```
685> The result of `linear_extrude` is centred.
686
687The distance function for `linear_extrude` computes the max
688of a function of x & y with a function of z,
689and the result is a right angle between the X/Y plane and the Z axis.
690See above.
691
692## Translation
693translate(), align(), pack???.
694No need for centre= options.
695
696## Shells and Isosurfaces
697
698### `shell(n) shape`
699hollows out the specified shape, leaving only a shell of the specified
700thickness.
701An analogy is 3D printing a shape without infill.
702
703```
704shell(w)(shape) = 3dshape(
705  f(p) = abs(shape.f(p) + w/2) - w/2,
706  bbox = shape.bbox );
707```
708Explanation:
709* `f(p) = abs(shape.f(p))` is the zero-thickness shell of `shape`.
710* `f(p) = abs(shape.f(p)) - n` (for positive n) is a shell of thickness `n*2`,
711  centred on the zero-thickness shell given above.
712* `f(p) = shape.f(p) + n` (for positive n) is the isosurface of `shape` at `-n`
713  (it's smaller than `shape`)
714
715### `inflate(n) shape`
716returns the isosurface of `shape` at value `n`.
717Positive values of `n` create a larger version of `shape`,
718`n==0` is the identity transformation,
719and negative values "deflate" the shape.
720This is different from the scale transformation; it's more like
721inflating a shape like a balloon, especially for non-convex objects.
722
723![inflate](img/inflate.png)
724<br>[image source](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)
725
726```
727inflate(n)(shape) = 3dshape(
728  f(p) = shape.f(p) - n,
729  bbox=[ shape.bbox[0]-n, shape.bbox[1]+n ]);
730```
731TODO: is the bbox calculation correct in all cases? No, not for anisotropic scaling.
732The bbox could be bigger than this.
733Use f to compute the bbox from shape.bbox using ray-marching.
734
735## Scaling
736Scaling is tricky. Several functional geometry systems get it wrong?
737
738### `isoscale(s) shape`
739This is an isotropic scaling operation:
740it scales a shape by the same factor `s` on all 3 axes,
741where `s` is a positive number.
742The code is easier to understand than for anisotropic scaling.
743
744```
745isoscale(s)(shape) = 3dshape(
746  f(p) = s * shape.f(p / s),
747  bbox = s * shape.bbox
748);
749```
750
751Suppose that the distance function
752was instead `f(p)=shape.f(p/s)`,
753as it is in Antimony. We don't multiply the result by `s`.
754This is good enough to scale the isosurface at 0,
755which means the scaled shape will render correctly.
756But the other isosurfaces will be messed up. Why?
757* Suppose s > 1. Eg, s==2 and we are scaling a centred sphere with radius 3.
758  We want the result to be a sphere with radius 6.
759  If we pass in p=[6,0,0], that's converted to p/s = [3,0,0] before passed to the sphere's distance function,
760  which then returns 0, indicating that p is on the boundary. Good.
761  If we pass in p=[8,0,0], that's converted to p/s = [4,0,0], then shape.f(p/s) returns 1.
762  This indicates we are a minimum of 1 mm from the boundary, which is satisfies
763  the ray-march property. However, the inflate property is not satisfied, since the
764  bounding box returned by `inflate(1)scale(2)sphere(3)` will be too small.
765* Suppose s < 1.
766  The ray-march property fails, but the inflate property is satisfied.
767
768
769### Negative Scale Factor
770In OpenSCAD, a negative scaling factor passed to the scale() operator
771will cause a reflection about the corresponding axis. This isn't documented,
772but it is a natural consequence of how scaling is implemented by an affine transformation matrix.
773
774The scale operators discussed in this document don't work that way: a negative scaling factor
775results in garbage.
776
777### `scale([sx,sy,sz]) shape`
778Anisotropic scaling is hard ...
779
780I don't see a way to implement anisotropic scaling in a way
781that satisfies both the ray-march and the inflate properties.
782The system needs to change.
783
784the effect of anisotropic scaling on `shell`. Need for `spheroid`.
785
786```
787getImplicit3 (Scale3 s@(sx,sy,sz) symbObj) =
788    let
789        obj = getImplicit3 symbObj
790        k = (sx*sy*sz)**(1/3)
791    in
792        \p -> k * obj (p ⋯/ s)
793getBox3 (Scale3 s symbObj) =
794    let
795        (a,b) = getBox3 symbObj
796    in
797        (s ⋯* a, s ⋯* b)
798```
799
800```
801scale(s)(shape) = 3dshape(
802  f(p) = (s.x*s.y*s.z)^(1/3) * shape.f(p / s),
803  bbox = s * shape.bbox
804);
805```
806
807explanation:
808* `f0(p) = shape.f(p/s)` correctly scales the isosurface at 0,
809  so the shape will render correctly.
810  But the other isosurfaces will be messed up,
811  because the distances between isosurfaces will also be scaled.
812  So `f0` isn't a valid distance function:
813  it will cause `shell` and `inflate` to not work, in some cases.
814* ImplicitCAD multiplies f0 by k=cube_root(s.x * s.y * s.z)
815  to get a valid distance function. I don't understand.
816  Suppose s=[27,1,1]. Then k=3. How can this be correct?
817* Antimony just uses `f0`; it doesn't even try to fix the other
818  isosurfaces. I don't think the correct code is even expressible.
819
820Antimony:
821```
822def scale_x(part, x0, sx):
823    # X' = x0 + (X-x0)/sx
824    return part.map(Transform(
825        '+f%(x0)g/-Xf%(x0)gf%(sx)g' % locals()
826                if x0 else '/Xf%g' % sx,
827        'Y',
828        '+f%(x0)g*f%(sx)g-Xf%(x0)g' % locals()
829                if x0 else '*Xf%g' % sx,
830        'Y'))
831def scale_xyz(part, x0, y0, z0, sx, sy, sz):
832   # X' = x0 + (X-x0)/sx
833   # Y' = y0 + (Y-y0)/sy
834   # Z' = z0 + (Z-z0)/sz
835   # X = x0 + (X'-x0)*sx
836   # Y = y0 + (Y'-y0)*sy
837   # Z = z0 + (Z'-z0)*sz
838   return part.map(Transform(
839      '+f%(x0)g/-Xf%(x0)gf%(sx)g' % locals(),
840      '+f%(y0)g/-Yf%(y0)gf%(sy)g' % locals(),
841      '+f%(z0)g/-Zf%(z0)gf%(sz)g' % locals(),
842      '+f%(x0)g*-Xf%(x0)gf%(sx)g' % locals(),
843      '+f%(y0)g*-Yf%(y0)gf%(sy)g' % locals(),
844      '+f%(z0)g*-Zf%(z0)gf%(sz)g' % locals()))
845Shape Shape::map(Transform t) const
846{
847    return Shape("m" + (t.x_forward.length() ? t.x_forward : "_")
848                     + (t.y_forward.length() ? t.y_forward : "_")
849                     + (t.z_forward.length() ? t.z_forward : "_") + math,
850                 bounds.map(t));
851}
852```
853The scale functions also do translation, so you can scale an object locally without moving its origin point.
854With the translation removed, we get
855```
856   # X' = X/sx
857   # X = X'*sx
858```
859Antimony gets it wrong. It doesn't fix up the
860
861## CSG Operations
862everything, nothing, complement,
863union, intersection, difference
864
865```
866complement(s) = 3dshape(
867  f(p) = -s.f(p)
868  bbox = [[-inf,-inf,-inf],[inf,inf,inf]] );
869```
870> Convert a shape or pattern to its inverse: all points inside the object
871> are now outside, and vice versa.
872
873meaning of 'max' and 'min' in a distance function
874
875Note: ImplicitCAD and Antimony use max for union and min for intersection.
876Although this is a common implementation, [Schmitt 2002]
877(http://grenet.drimm.u-bordeaux1.fr/pdf/2002/SCHMITT_BENJAMIN_2002.pdf)
878says "it is well known that using min/max functions for set-theoretic
879operations causes problems in further transformations of the model due to C1 discontinuity of
880the resulting function (see Fig. 1.3). Further blending, offsetting, or metamorphosis can result
881in unnecessary creases, edges, and tearing of such an object." (page 26, and figure 1.3 on page 20).
882His solution is to use an alternative function, one which is differentiable.
883My interpretation is that you need to round any sharp edges and add fillets to sharp corners
884to avoid this alleged problem. I'm going to wait and see if we can actually detect a problem
885once we have working code, before worrying about a fix.
886
887## Rounded edges and Fillets
888rcuboid, runion
889
890```
891rcuboid(r,sz) = 3dshape(
892  f([x,y,z]) = rmax(r, [abs(x-sz.x/2), abs(y-sz.y/2), abs(z-sz.z/2)]),
893  bbox=[ [0,0,0], sz ]);
894```
895> Cuboid with rounded corners (radius=r) from ImplicitCAD.
896> All ImplicitCAD primitives that generate sharp corners have a rounding option.
897> `rmax` is a utility function for creating rounded corners.
898
899```
900runion(r,s1,s2) = 3dshape(
901    f(p) = rmin(r, s1.f(p), s2.f(p)),
902    bbox=[ min(s1.bbox[0],s2.bbox[0])-r, max(s1.bbox[1],s2.bbox[1])+r ]);
903```
904> Create fillets! ImplicitCAD rounded union with radius `r`. (Why is the bbox padded by r?)
905Example:
906  ```
907  runion(r=8,
908  	cube([40,40,20]),
909  	translate([0,0,20]) cube([20,20,30]))
910  ```
911![rounded union](http://thingiverse-rerender-new.s3.amazonaws.com/renders/47/9e/1c/c8/e2/RoundedUnionCubeExample_preview_featured.jpg)
912
913## Perimeter_extrude
914
915## Infinite Patterns
916gyroid, infinite replication grid, etc
917
918## Morphing
919"Distance fields are also commonly used for
920shape metamorphosis. Compared to mesh-based morphing
921techniques, volume-based morphing is easier to implement
922and can naturally morph surfaces of different genus."
923[[Bastos 2008](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)].
924
925Here's a simple morph operator based on linear interpolation
926of the distance functions:
927
928![morph](img/morph.png)
929<br>[image source](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)
930
931```
932morph(r,s1,s2) = 3dshape(
933  f(p) = interp(r, s1.f(p), s2,f(p)),
934  bbox = interp(r, s1.bbox, s2.bbox)
935);
936interp(r,a,b) = a + (b - a)*r;
937```
938`r` normally ranges from 0 to 1, where 0 returns s1, 1 returns s2.
939But values <0 or >1 can also be used.
940
941"Unfortunately, in most cases simple interpolation is
942unable to provide convincing metamorphoses, as it does not
943preserve or blend corresponding shape features. In order to
944improve morphing quality, we could use a warp function
945to guide the distance field interpolation, as proposed by
946Cohen-Or 'Three dimensional distance field metamorphosis'"
947[[Bastos 2008](http://www.gpucomputing.net/sites/default/files/papers/2452/Bastos_GPU_ADF_SMI08.pdf)].
948