// Copyright 2015 Charles Staats III
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// smoothcontour3
// An Asymptote module for drawing smooth implicitly defined surfaces
// author: Charles Staats III
// charles dot staats dot iii at gmail dot com
import graph_settings; // for nmesh
import three;
import math;
/***********************************************/
/******** CREATING BEZIER PATCHES **************/
/******** WITH SPECIFIED NORMALS **************/
/***********************************************/
// The weight given to minimizing the sum of squares of
// the mixed partials at the corners of the bezier patch.
// If this weight is zero, the result is undefined in
// places and can be rather wild even where it is
// defined.
// The struct is used to as a namespace.
struct pathwithnormals_settings {
static real wildnessweight = 1e-3;
}
private from pathwithnormals_settings unravel wildnessweight;
// The Bernstein basis polynomials of degree 3:
real B03(real t) { return (1-t)^3; }
real B13(real t) { return 3*t*(1-t)^2; }
real B23(real t) { return 3*t^2*(1-t); }
real B33(real t) { return t^3; }
private typedef real function(real);
function[] bernstein = new function[] {B03, B13, B23, B33};
// This function attempts to produce a Bezier patch
// with the specified boundary path and normal directions.
// For instance, the patch should be normal to
// u0normals[0] at (0, 0.25),
// normal to u0normals[1] at (0, 0.5), and
// normal to u0normals[2] at (0, 0.75).
// The actual normal (as computed by the patch.normal() function)
// may be parallel to the specified normal, antiparallel, or
// even zero.
//
// A small amount of deviation is allowed in order to stabilize
// the algorithm (by keeping the mixed partials at the corners from
// growing too large).
//
// Note that the specified normals are projected to be orthogonal to
// the specified boundary path. However, the entries in the array
// remain intact.
patch patchwithnormals(path3 external, triple[] u0normals, triple[] u1normals,
triple[] v0normals, triple[] v1normals)
{
assert(cyclic(external));
assert(length(external) == 4);
assert(u0normals.length == 3);
assert(u1normals.length == 3);
assert(v0normals.length == 3);
assert(v1normals.length == 3);
triple[][] controlpoints = new triple[4][4];
controlpoints[0][0] = point(external,0);
controlpoints[1][0] = postcontrol(external,0);
controlpoints[2][0] = precontrol(external,1);
controlpoints[3][0] = point(external,1);
controlpoints[3][1] = postcontrol(external,1);
controlpoints[3][2] = precontrol(external,2);
controlpoints[3][3] = point(external,2);
controlpoints[2][3] = postcontrol(external,2);
controlpoints[1][3] = precontrol(external,3);
controlpoints[0][3] = point(external,3);
controlpoints[0][2] = postcontrol(external,3);
controlpoints[0][1] = precontrol(external, 4);
real[][] matrix = new real[24][12];
for (int i = 0; i < matrix.length; ++i)
for (int j = 0; j < matrix[i].length; ++j)
matrix[i][j] = 0;
real[] rightvector = new real[24];
for (int i = 0; i < rightvector.length; ++i)
rightvector[i] = 0;
void addtocoeff(int i, int j, int count, triple coeffs) {
if (1 <= i && i <= 2 && 1 <= j && j <= 2) {
int position = 3 * (2 * (i-1) + (j-1));
matrix[count][position] += coeffs.x;
matrix[count][position+1] += coeffs.y;
matrix[count][position+2] += coeffs.z;
} else {
rightvector[count] -= dot(controlpoints[i][j], coeffs);
}
}
void addtocoeff(int i, int j, int count, real coeff) {
if (1 <= i && i <= 2 && 1 <= j && j <= 2) {
int position = 3 * (2 * (i-1) + (j-1));
matrix[count][position] += coeff;
matrix[count+1][position+1] += coeff;
matrix[count+2][position+2] += coeff;
} else {
rightvector[count] -= controlpoints[i][j].x * coeff;
rightvector[count+1] -= controlpoints[i][j].y * coeff;
rightvector[count+2] -= controlpoints[i][j].z * coeff;
}
}
int count = 0;
void apply_u0(int j, real a, triple n) {
real factor = 3 * bernstein[j](a);
addtocoeff(0,j,count,-factor*n);
addtocoeff(1,j,count,factor*n);
}
void apply_u0(real a, triple n) {
triple tangent = dir(external, 4-a);
n -= dot(n,tangent)*tangent;
n = unit(n);
for (int j = 0; j < 4; ++j) {
apply_u0(j,a,n);
}
++count;
}
apply_u0(0.25, u0normals[0]);
apply_u0(0.5, u0normals[1]);
apply_u0(0.75, u0normals[2]);
void apply_u1(int j, real a, triple n) {
real factor = 3 * bernstein[j](a);
addtocoeff(3,j,count,factor*n);
addtocoeff(2,j,count,-factor*n);
}
void apply_u1(real a, triple n) {
triple tangent = dir(external, 1+a);
n -= dot(n,tangent)*tangent;
n = unit(n);
for (int j = 0; j < 4; ++j)
apply_u1(j,a,n);
++count;
}
apply_u1(0.25, u1normals[0]);
apply_u1(0.5, u1normals[1]);
apply_u1(0.75, u1normals[2]);
void apply_v0(int i, real a, triple n) {
real factor = 3 * bernstein[i](a);
addtocoeff(i,0,count,-factor*n);
addtocoeff(i,1,count,factor*n);
}
void apply_v0(real a, triple n) {
triple tangent = dir(external, a);
n -= dot(n,tangent) * tangent;
n = unit(n);
for (int i = 0; i < 4; ++i)
apply_v0(i,a,n);
++count;
}
apply_v0(0.25, v0normals[0]);
apply_v0(0.5, v0normals[1]);
apply_v0(0.75, v0normals[2]);
void apply_v1(int i, real a, triple n) {
real factor = 3 * bernstein[i](a);
addtocoeff(i,3,count,factor*n);
addtocoeff(i,2,count,-factor*n);
}
void apply_v1(real a, triple n) {
triple tangent = dir(external, 3-a);
n -= dot(n,tangent)*tangent;
n = unit(n);
for (int i = 0; i < 4; ++i)
apply_v1(i,a,n);
++count;
}
apply_v1(0.25, v1normals[0]);
apply_v1(0.5, v1normals[1]);
apply_v1(0.75, v1normals[2]);
addtocoeff(0,0,count,9*wildnessweight);
addtocoeff(1,1,count,9*wildnessweight);
addtocoeff(0,1,count,-9*wildnessweight);
addtocoeff(1,0,count,-9*wildnessweight);
count+=3;
addtocoeff(3,3,count,9*wildnessweight);
addtocoeff(2,2,count,9*wildnessweight);
addtocoeff(3,2,count,-9*wildnessweight);
addtocoeff(2,3,count,-9*wildnessweight);
count+=3;
addtocoeff(0,3,count,9*wildnessweight);
addtocoeff(1,2,count,9*wildnessweight);
addtocoeff(1,3,count,-9*wildnessweight);
addtocoeff(0,2,count,-9*wildnessweight);
count += 3;
addtocoeff(3,0,count,9*wildnessweight);
addtocoeff(2,1,count,9*wildnessweight);
addtocoeff(3,1,count,-9*wildnessweight);
addtocoeff(2,0,count,-9*wildnessweight);
count += 3;
real[] solution = leastsquares(matrix, rightvector, warn=false);
if (solution.length == 0) { // if the matrix was singular
write("Warning: unable to solve matrix for specifying edge normals "
+ "on bezier patch. Using coons patch.");
return patch(external);
}
for (int i = 1; i <= 2; ++i) {
for (int j = 1; j <= 2; ++j) {
int position = 3 * (2 * (i-1) + (j-1));
controlpoints[i][j] = (solution[position],
solution[position+1],
solution[position+2]);
}
}
return patch(controlpoints);
}
// This function attempts to produce a Bezier triangle
// with the specified boundary path and normal directions at the
// edge midpoints. The bezier triangle should be normal to
// n1 at point(external, 0.5),
// normal to n2 at point(external, 1.5), and
// normal to n3 at point(external, 2.5).
// The actual normal (as computed by the patch.normal() function)
// may be parallel to the specified normal, antiparallel, or
// even zero.
//
// A small amount of deviation is allowed in order to stabilize
// the algorithm (by keeping the mixed partials at the corners from
// growing too large).
patch trianglewithnormals(path3 external, triple n1,
triple n2, triple n3) {
assert(cyclic(external));
assert(length(external) == 3);
// Use the formal symbols a3, a2b, abc, etc. to denote the control points,
// following the Wikipedia article on Bezier triangles.
triple a3 = point(external, 0), a2b = postcontrol(external, 0),
ab2 = precontrol(external, 1), b3 = point(external, 1),
b2c = postcontrol(external, 1), bc2 = precontrol(external, 2),
c3 = point(external, 2), ac2 = postcontrol(external, 2),
a2c = precontrol(external, 0);
// Use orthogonal projection to ensure that the normal vectors are
// actually normal to the boundary path.
triple tangent = dir(external, 0.5);
n1 -= dot(n1,tangent)*tangent;
n1 = unit(n1);
tangent = dir(external, 1.5);
n2 -= dot(n2,tangent)*tangent;
n2 = unit(n2);
tangent = dir(external, 2.5);
n3 -= dot(n3,tangent)*tangent;
n3 = unit(n3);
real wild = 2 * wildnessweight;
real[][] matrix = { {n1.x, n1.y, n1.z},
{n2.x, n2.y, n2.z},
{n3.x, n3.y, n3.z},
{ wild, 0, 0},
{ 0, wild, 0},
{ 0, 0, wild} };
real[] rightvector =
{ dot(n1, (a3 + 3a2b + 3ab2 + b3 - 2a2c - 2b2c)) / 4,
dot(n2, (b3 + 3b2c + 3bc2 + c3 - 2ab2 - 2ac2)) / 4,
dot(n3, (c3 + 3ac2 + 3a2c + a3 - 2bc2 - 2a2b)) / 4 };
// The inner control point that minimizes the sum of squares of
// the mixed partials on the corners.
triple tameinnercontrol =
((a2b + a2c - a3) + (ab2 + b2c - b3) + (ac2 + bc2 - c3)) / 3;
rightvector.append(wild * new real[]
{tameinnercontrol.x, tameinnercontrol.y, tameinnercontrol.z});
real[] solution = leastsquares(matrix, rightvector, warn=false);
if (solution.length == 0) { // if the matrix was singular
write("Warning: unable to solve matrix for specifying edge normals "
+ "on bezier triangle. Using coons triangle.");
return patch(external);
}
triple innercontrol = (solution[0], solution[1], solution[2]);
return patch(external, innercontrol);
}
// A wrapper for the previous functions when the normal direction
// is given as a function of direction. The wrapper can also
// accommodate cyclic boundary paths of between one and four
// segments, although the results are best by far when there
// are three or four segments.
patch patchwithnormals(path3 external, triple normalat(triple)) {
assert(cyclic(external));
assert(1 <= length(external) && length(external) <= 4);
if (length(external) == 3) {
triple n1 = normalat(point(external, 0.5));
triple n2 = normalat(point(external, 1.5));
triple n3 = normalat(point(external, 2.5));
return trianglewithnormals(external, n1, n2, n3);
}
while (length(external) < 4) external = external -- cycle;
triple[] u0normals = new triple[3];
triple[] u1normals = new triple[3];
triple[] v0normals = new triple[3];
triple[] v1normals = new triple[3];
for (int i = 1; i <= 3; ++i) {
v0normals[i-1] = unit(normalat(point(external, i/4)));
u1normals[i-1] = unit(normalat(point(external, 1 + i/4)));
v1normals[i-1] = unit(normalat(point(external, 3 - i/4)));
u0normals[i-1] = unit(normalat(point(external, 4 - i/4)));
}
return patchwithnormals(external, u0normals, u1normals, v0normals, v1normals);
}
/***********************************************/
/********* DUAL CUBE GRAPH UTILITY *************/
/***********************************************/
// Suppose a plane intersects a (hollow) cube, and
// does not intersect any vertices. Then its intersection
// with cube forms a cycle. The goal of the code below
// is to reconstruct the order of the cycle
// given only an unordered list of which edges the plane
// intersects.
//
// Basically, the question is this: If we know the points
// in which a more-or-less planar surface intersects the
// edges of cube, how do we connect those points?
//
// When I wrote the code, I was thinking in terms of the
// dual graph of a cube, in which "vertices" are really
// faces of the cube and "edges" connect those "vertices."
// An enum for the different "vertices" (i.e. faces)
// available. NULL_VERTEX is primarily intended as a
// return value to indicate the absence of a desired
// vertex.
private int NULL_VERTEX = -1;
private int XHIGH = 0;
private int XLOW = 1;
private int YHIGH = 2;
private int YLOW = 3;
private int ZHIGH = 4;
private int ZLOW = 5;
// An unordered set of nonnegative integers.
// Since the intent is to use
// only the six values from the enum above, no effort
// was made to use scalable algorithms.
struct intset {
private bool[] ints = new bool[0];
private int size = 0;
bool contains(int item) {
assert(item >= 0);
if (item >= ints.length) return false;
return ints[item];
}
// Returns true if the item was added (i.e., was
// not already present).
bool add(int item) {
assert(item >= 0);
while (item >= ints.length) ints.push(false);
if (ints[item]) return false;
ints[item] = true;
++size;
return true;
}
int[] elements() {
int[] toreturn;
for (int i = 0; i < ints.length; ++i) {
if (ints[i]) toreturn.push(i);
}
return toreturn;
}
int size() { return size; }
}
// A map from integers to sets of integers. Again, no
// attempt is made to use scalable data structures.
struct int_to_intset {
int[] keys = new int[0];
intset[] values = new intset[0];
void add(int key, int value) {
for (int i = 0; i < keys.length; ++i) {
if (keys[i] == key) {
values[i].add(value);
return;
}
}
keys.push(key);
intset newset;
values.push(newset);
newset.add(value);
}
private int indexOf(int key) {
for (int i = 0; i < keys.length; ++i) {
if (keys[i] == key) return i;
}
return -1;
}
int[] get(int key) {
int i = indexOf(key);
if (i < 0) return new int[0];
else return values[i].elements();
}
int numvalues(int key) {
int i = indexOf(key);
if (i < 0) return 0;
else return values[i].size();
}
int numkeys() {
return keys.length;
}
}
// A struct intended to represent an undirected edge between
// two "vertices."
struct edge {
int start;
int end;
void operator init(int a, int b) {
start = a;
end = b;
}
bool bordersvertex(int v) { return start == v || end == v; }
}
string operator cast(edge e) {
int a, b;
if (e.start <= e.end) {a = e.start; b = e.end;}
else {a = e.end; b = e.start; }
return (string)a + " <-> " + (string)b;
}
bool operator == (edge a, edge b) {
if (a.start == b.start && a.end == b.end) return true;
if (a.start == b.end && a.end == b.start) return true;
return false;
}
string operator cast(edge[] edges) {
string toreturn = "{ ";
for (int i = 0; i < edges.length; ++i) {
toreturn += edges[i];
if (i < edges.length-1) toreturn += ", ";
}
return toreturn + " }";
}
// Finally, the function that strings together a list of edges
// into a cycle. It makes assumptions that hold true if the
// list of edges did in fact come from a plane intersection
// containing no vertices of the cube. For instance, such a
// plane can contain at most two noncollinear points of any
// one face; consequently, no face can border more than two of
// the selected edges.
//
// If the underlying assumptions prove to be false, the function
// returns null.
int[] makecircle(edge[] edges) {
if (edges.length == 0) return new int[0];
int_to_intset graph;
for (edge e : edges) {
graph.add(e.start, e.end);
graph.add(e.end, e.start);
}
int currentvertex = edges[0].start;
int startvertex = currentvertex;
int lastvertex = NULL_VERTEX;
int[] toreturn = new int[0];
do {
toreturn.push(currentvertex);
int[] adjacentvertices = graph.get(currentvertex);
if (adjacentvertices.length != 2) return null;
for (int v : adjacentvertices) {
if (v != lastvertex) {
lastvertex = currentvertex;
currentvertex = v;
break;
}
}
} while (currentvertex != startvertex);
if (toreturn.length != graph.numkeys()) return null;
toreturn.cyclic = true;
return toreturn;
}
/***********************************************/
/********** PATHS BETWEEN POINTS ***************/
/***********************************************/
// Construct paths between two points with additional
// constraints; for instance, the path must be orthogonal
// to a certain vector at each of the endpoints, must
// lie within a specified plane or a specified face
// of a rectangular solid,....
// A vector (typically a normal vector) at a specified position.
struct positionedvector {
triple position;
triple direction;
void operator init(triple position, triple direction) {
this.position = position;
this.direction = direction;
}
}
string operator cast(positionedvector vv) {
return "position: " + (string)(vv.position) + " vector: " + (string)vv.direction;
}
// The angle, in degrees, between two vectors.
real angledegrees(triple a, triple b) {
real dotprod = dot(a,b);
real lengthprod = max(abs(a) * abs(b), abs(dotprod));
if (lengthprod == 0) return 0;
return aCos(dotprod / lengthprod);
}
// A path (single curved segment) between two points. At each point
// is specified a vector orthogonal to the path.
path3 pathbetween(positionedvector v1, positionedvector v2) {
triple n1 = unit(v1.direction);
triple n2 = unit(v2.direction);
triple p1 = v1.position;
triple p2 = v2.position;
triple delta = p2-p1;
triple dir1 = delta - dot(delta, n1)*n1;
triple dir2 = delta - dot(delta, n2)*n2;
return p1 {dir1} .. {dir2} p2;
}
// Assuming v1 and v2 are linearly independent, returns an array {a, b}
// such that a v1 + b v2 is the orthogonal projection of toproject onto
// the span of v1 and v2. If v1 and v2 are dependent, returns an empty array
// (if warn==false) or throws an error (if warn==true).
real[] projecttospan_findcoeffs(triple toproject, triple v1, triple v2,
bool warn=false) {
real[][] matrix = {{v1.x, v2.x},
{v1.y, v2.y},
{v1.z, v2.z}};
real[] desiredanswer = {toproject.x, toproject.y, toproject.z};
return leastsquares(matrix, desiredanswer, warn=warn);
}
// Project the triple toproject into the span of a and b, but restrict
// to the quarter-plane of linear combinations a v1 + b v2 such that
// a >= mincoeff and b >= mincoeff. If v1 and v2 are linearly dependent,
// return a random (positive) linear combination.
triple projecttospan(triple toproject, triple v1, triple v2,
real mincoeff = 0.05) {
real[] coeffs = projecttospan_findcoeffs(toproject, v1, v2, warn=false);
real a, b;
if (coeffs.length == 0) {
a = mincoeff + unitrand();
b = mincoeff + unitrand();
} else {
a = max(coeffs[0], mincoeff);
b = max(coeffs[1], mincoeff);
}
return a*v1 + b*v2;
}
// A path between two specified vertices of a cyclic path. The
// path tangent at each endpoint is guaranteed to lie within the
// quarter-plane spanned by positive linear combinations of the
// tangents of the two outgoing paths at that endpoint.
path3 pathbetween(path3 edgecycle, int vertex1, int vertex2) {
triple point1 = point(edgecycle, vertex1);
triple point2 = point(edgecycle, vertex2);
triple v1 = -dir(edgecycle, vertex1, sign=-1);
triple v2 = dir(edgecycle, vertex1, sign= 1);
triple direction1 = projecttospan(unit(point2-point1), v1, v2);
v1 = -dir(edgecycle, vertex2, sign=-1);
v2 = dir(edgecycle, vertex2, sign= 1);
triple direction2 = projecttospan(unit(point1-point2), v1, v2);
return point1 {direction1} .. {-direction2} point2;
}
// This function applies a heuristic to choose two "opposite"
// vertices (separated by three segments) of edgecycle, which
// is required to be a cyclic path consisting of 5 or 6 segments.
// The two chosen vertices are pushed to savevertices.
//
// The function returns a path between the two chosen vertices. The
// path tangent at each endpoint is guaranteed to lie within the
// quarter-plane spanned by positive linear combinations of the
// tangents of the two outgoing paths at that endpoint.
path3 bisector(path3 edgecycle, int[] savevertices) {
real mincoeff = 0.05;
assert(cyclic(edgecycle));
int n = length(edgecycle);
assert(n >= 5 && n <= 6);
triple[] forwarddirections = sequence(new triple(int i) {
return dir(edgecycle, i, sign=1);
}, n);
forwarddirections.cyclic = true;
triple[] backwarddirections = sequence(new triple(int i) {
return -dir(edgecycle, i, sign=-1);
}, n);
backwarddirections.cyclic = true;
real[] angles = sequence(new real(int i) {
return angledegrees(forwarddirections[i], backwarddirections[i]);
}, n);
angles.cyclic = true;
int lastindex = (n == 5 ? 4 : 2);
real maxgoodness = 0;
int chosenindex = -1;
triple directionout, directionin;
for (int i = 0; i <= lastindex; ++i) {
int opposite = i + 3;
triple vec = unit(point(edgecycle, opposite) - point(edgecycle, i));
real[] coeffsbegin = projecttospan_findcoeffs(vec, forwarddirections[i],
backwarddirections[i]);
if (coeffsbegin.length == 0) continue;
coeffsbegin[0] = max(coeffsbegin[0], mincoeff);
coeffsbegin[1] = max(coeffsbegin[1], mincoeff);
real[] coeffsend = projecttospan_findcoeffs(-vec, forwarddirections[opposite],
backwarddirections[opposite]);
if (coeffsend.length == 0) continue;
coeffsend[0] = max(coeffsend[0], mincoeff);
coeffsend[1] = max(coeffsend[1], mincoeff);
real goodness = angles[i] * angles[opposite] * coeffsbegin[0] * coeffsend[0]
* coeffsbegin[1] * coeffsend[1];
if (goodness > maxgoodness) {
maxgoodness = goodness;
directionout = coeffsbegin[0] * forwarddirections[i] +
coeffsbegin[1] * backwarddirections[i];
directionin = -(coeffsend[0] * forwarddirections[opposite] +
coeffsend[1] * backwarddirections[opposite]);
chosenindex = i;
}
}
if (chosenindex == -1) {
savevertices.push(0);
savevertices.push(3);
return pathbetween(edgecycle, 0, 3);
} else {
savevertices.push(chosenindex);
savevertices.push(chosenindex+3);
return point(edgecycle, chosenindex) {directionout} ..
{directionin} point(edgecycle, chosenindex + 3);
}
}
// A path between two specified points (with specified normals) that lies
// within a specified face of a rectangular solid.
path3 pathinface(positionedvector v1, positionedvector v2,
triple facenorm, triple edge1normout, triple edge2normout)
{
triple dir1 = cross(v1.direction, facenorm);
real dotprod = dot(dir1, edge1normout);
if (dotprod > 0) dir1 = -dir1;
// Believe it or not, this "tiebreaker" is actually relevant at times,
// for instance, when graphing the cone x^2 + y^2 = z^2 over the region
// -1 <= x,y,z <= 1.
else if (dotprod == 0 && dot(dir1, v2.position - v1.position) < 0) dir1 = -dir1;
triple dir2 = cross(v2.direction, facenorm);
dotprod = dot(dir2, edge2normout);
if (dotprod < 0) dir2 = -dir2;
else if (dotprod == 0 && dot(dir2, v2.position - v1.position) < 0) dir2 = -dir2;
return v1.position {dir1} .. {dir2} v2.position;
}
triple normalout(int face) {
if (face == XHIGH) return X;
else if (face == YHIGH) return Y;
else if (face == ZHIGH) return Z;
else if (face == XLOW) return -X;
else if (face == YLOW) return -Y;
else if (face == ZLOW) return -Z;
else return O;
}
// A path between two specified points (with specified normals) that lies
// within a specified face of a rectangular solid.
path3 pathinface(positionedvector v1, positionedvector v2,
int face, int edge1face, int edge2face) {
return pathinface(v1, v2, normalout(face), normalout(edge1face),
normalout(edge2face));
}
/***********************************************/
/******** DRAWING IMPLICIT SURFACES ************/
/***********************************************/
// DEPRECATED
// Quadrilateralization:
// Produce a surface (array of *nondegenerate* Bezier patches) with a
// specified three-segment boundary. The surface should approximate the
// zero locus of the specified f with its specified gradient.
//
// If it is not possible to produce the desired result without leaving the
// specified rectangular region, returns a length-zero array.
//
// Dividing a triangle into smaller quadrilaterals this way is opposite
// the usual trend in mathematics. However, *before the introduction of bezier
// triangles,* the pathwithnormals algorithm
// did a poor job of choosing a good surface when the boundary path did
// not consist of four positive-length segments.
patch[] triangletoquads(path3 external, real f(triple), triple grad(triple),
triple a, triple b) {
static real epsilon = 1e-3;
assert(length(external) == 3);
assert(cyclic(external));
triple c0 = point(external, 0);
triple c1 = point(external, 1);
triple c2 = point(external, 2);
triple center = (c0 + c1 + c2) / 3;
triple n = unit(cross(c1-c0, c2-c0));
real g(real t) { return f(center + t*n); }
real tmin = -realMax, tmax = realMax;
void absorb(real t) {
if (t < 0) tmin = max(t,tmin);
else tmax = min(t,tmax);
}
if (n.x != 0) {
absorb((a.x - center.x) / n.x);
absorb((b.x - center.x) / n.x);
}
if (n.y != 0) {
absorb((a.y - center.y) / n.y);
absorb((b.y - center.y) / n.y);
}
if (n.z != 0) {
absorb((a.z - center.z) / n.z);
absorb((b.z - center.z) / n.z);
}
real fa = g(tmin);
real fb = g(tmax);
if ((fa > 0 && fb > 0) || (fa < 0 && fb < 0)) {
return new patch[0];
} else {
real t = findroot(g, tmin, tmax, fa=fa, fb=fb);
center += t * n;
}
n = unit(grad(center));
triple m0 = point(external, 0.5);
positionedvector m0 = positionedvector(m0, unit(grad(m0)));
triple m1 = point(external, 1.5);
positionedvector m1 = positionedvector(m1, unit(grad(m1)));
triple m2 = point(external, 2.5);
positionedvector m2 = positionedvector(m2, unit(grad(m2)));
positionedvector c = positionedvector(center, unit(grad(center)));
path3 pathto_m0 = pathbetween(c, m0);
path3 pathto_m1 = pathbetween(c, m1);
path3 pathto_m2 = pathbetween(c, m2);
path3 quad0 = subpath(external, 0, 0.5)
& reverse(pathto_m0)
& pathto_m2
& subpath(external, -0.5, 0)
& cycle;
path3 quad1 = subpath(external, 1, 1.5)
& reverse(pathto_m1)
& pathto_m0
& subpath(external, 0.5, 1)
& cycle;
path3 quad2 = subpath(external, 2, 2.5)
& reverse(pathto_m2)
& pathto_m1
& subpath(external, 1.5, 2)
& cycle;
return new patch[] {patchwithnormals(quad0, grad),
patchwithnormals(quad1, grad),
patchwithnormals(quad2, grad)};
}
// Attempts to fill the path external (which should by a cyclic path consisting of
// three segments) with bezier triangle(s). Returns an empty array if it fails.
//
// In more detail: A single bezier triangle is computed using trianglewithnormals. The normals of
// the resulting triangle at the midpoint of each edge are computed. If any of these normals
// is in the negative f direction, the external triangle is subdivided into four external triangles
// and the same procedure is applied to each. If one or more of them has an incorrectly oriented
// edge normal, the function gives up and returns an empty array.
//
// Thus, the returned array consists of 0, 1, or 4 bezier triangles; no other array lengths
// are possible.
//
// This function assumes that the path orientation is consistent with f (and its gradient)
// -- i.e., that
// at a corner, (tangent in) x (tangent out) is in the positive f direction.
patch[] maketriangle(path3 external, real f(triple),
triple grad(triple), bool allowsubdivide = true) {
assert(cyclic(external));
assert(length(external) == 3);
triple m1 = point(external, 0.5);
triple n1 = unit(grad(m1));
triple m2 = point(external, 1.5);
triple n2 = unit(grad(m2));
triple m3 = point(external, 2.5);
triple n3 = unit(grad(m3));
patch beziertriangle = trianglewithnormals(external, n1, n2, n3);
if (dot(n1, beziertriangle.normal(0.5, 0)) >= 0 &&
dot(n2, beziertriangle.normal(0.5, 0.5)) >= 0 &&
dot(n3, beziertriangle.normal(0, 0.5)) >= 0)
return new patch[] {beziertriangle};
if (!allowsubdivide) return new patch[0];
positionedvector m1 = positionedvector(m1, n1);
positionedvector m2 = positionedvector(m2, n2);
positionedvector m3 = positionedvector(m3, n3);
path3 p12 = pathbetween(m1, m2);
path3 p23 = pathbetween(m2, m3);
path3 p31 = pathbetween(m3, m1);
patch[] triangles = maketriangle(p12 & p23 & p31 & cycle, f, grad=grad,
allowsubdivide=false);
if (triangles.length < 1) return new patch[0];
triangles.append(maketriangle(subpath(external, -0.5, 0.5) & reverse(p31) & cycle,
f, grad=grad, allowsubdivide=false));
if (triangles.length < 2) return new patch[0];
triangles.append(maketriangle(subpath(external, 0.5, 1.5) & reverse(p12) & cycle,
f, grad=grad, allowsubdivide=false));
if (triangles.length < 3) return new patch[0];
triangles.append(maketriangle(subpath(external, 1.5, 2.5) & reverse(p23) & cycle,
f, grad=grad, allowsubdivide=false));
if (triangles.length < 4) return new patch[0];
return triangles;
}
// Returns true if the point is "nonsingular" (in the sense that the magnitude
// of the gradient is not too small) AND very close to the zero locus of f
// (assuming f is locally linear).
bool check_fpt_zero(triple testpoint, real f(triple), triple grad(triple)) {
real testval = f(testpoint);
real slope = abs(grad(testpoint));
static real tolerance = 2*rootfinder_settings.roottolerance;
return !(slope > tolerance && abs(testval) / slope > tolerance);
}
// Returns true if pt lies within the rectangular solid with
// opposite corners at a and b.
bool checkptincube(triple pt, triple a, triple b) {
real xmin = a.x;
real xmax = b.x;
real ymin = a.y;
real ymax = b.y;
real zmin = a.z;
real zmax = b.z;
if (xmin > xmax) { real t = xmax; xmax=xmin; xmin=t; }
if (ymin > ymax) { real t = ymax; ymax=ymin; ymin=t; }
if (zmin > zmax) { real t = zmax; zmax=zmin; zmin=t; }
return ((xmin <= pt.x) && (pt.x <= xmax) &&
(ymin <= pt.y) && (pt.y <= ymax) &&
(zmin <= pt.z) && (pt.z <= zmax));
}
// A convenience function for combining the previous two tests.
bool checkpt(triple testpt, real f(triple), triple grad(triple),
triple a, triple b) {
return checkptincube(testpt, a, b) &&
check_fpt_zero(testpt, f, grad);
}
// Attempts to fill in the boundary cycle with a collection of
// patches to approximate smoothly the zero locus of f. If unable to
// do so while satisfying certain checks, returns null.
// This is distinct from returning an empty
// array, which merely indicates that the boundary cycle is too small
// to be worth filling in.
patch[] quadpatches(path3 edgecycle, positionedvector[] corners,
real f(triple), triple grad(triple),
triple a, triple b, bool usetriangles) {
assert(corners.cyclic);
// The tolerance for considering two points "essentially identical."
static real tolerance = 2.5 * rootfinder_settings.roottolerance;
// If there are two neighboring vertices that are essentially identical,
// unify them into one.
for (int i = 0; i < corners.length; ++i) {
if (abs(corners[i].position - corners[i+1].position) < tolerance) {
if (corners.length == 2) return new patch[0];
corners.delete(i);
edgecycle = subpath(edgecycle, 0, i)
& subpath(edgecycle, i+1, length(edgecycle))
& cycle;
--i;
assert(length(edgecycle) == corners.length);
}
}
static real areatolerance = tolerance^2;
assert(corners.length >= 2);
if (corners.length == 2) {
// If the area is too small, just ignore it; otherwise, subdivide.
real area0 = abs(cross(-dir(edgecycle, 0, sign=-1, normalize=false),
dir(edgecycle, 0, sign=1, normalize=false)));
real area1 = abs(cross(-dir(edgecycle, 1, sign=-1, normalize=false),
dir(edgecycle, 1, sign=1, normalize=false)));
if (area0 < areatolerance && area1 < areatolerance) return new patch[0];
else return null;
}
if (length(edgecycle) > 6) abort("too many edges: not possible.");
for (int i = 0; i < length(edgecycle); ++i) {
if (angledegrees(dir(edgecycle,i,sign=1),
dir(edgecycle,i+1,sign=-1)) > 80) {
return null;
}
}
if (length(edgecycle) == 3) {
patch[] toreturn = usetriangles ? maketriangle(edgecycle, f, grad)
: triangletoquads(edgecycle, f, grad, a, b);
if (toreturn.length == 0) return null;
else return toreturn;
}
if (length(edgecycle) == 4) {
return new patch[] {patchwithnormals(edgecycle, grad)};
}
int[] bisectorindices;
path3 middleguide = bisector(edgecycle, bisectorindices);
triple testpoint = point(middleguide, 0.5);
if (!checkpt(testpoint, f, grad, a, b)) {
return null;
}
patch[] toreturn = null;
path3 firstpatch = subpath(edgecycle, bisectorindices[0], bisectorindices[1])
& reverse(middleguide) & cycle;
if (length(edgecycle) == 5) {
path3 secondpatch = middleguide
& subpath(edgecycle, bisectorindices[1], 5+bisectorindices[0]) & cycle;
toreturn = usetriangles ? maketriangle(secondpatch, f, grad)
: triangletoquads(secondpatch, f, grad, a, b);
if (toreturn.length == 0) return null;
toreturn.push(patchwithnormals(firstpatch, grad));
} else {
// now length(edgecycle) == 6
path3 secondpatch = middleguide
& subpath(edgecycle, bisectorindices[1], 6+bisectorindices[0])
& cycle;
toreturn = new patch[] {patchwithnormals(firstpatch, grad),
patchwithnormals(secondpatch, grad)};
}
return toreturn;
}
// Numerical gradient of a function
typedef triple vectorfunction(triple);
vectorfunction nGrad(real f(triple)) {
static real epsilon = 1e-3;
return new triple(triple v) {
return ( (f(v + epsilon*X) - f(v - epsilon*X)) / (2 epsilon),
(f(v + epsilon*Y) - f(v - epsilon*Y)) / (2 epsilon),
(f(v + epsilon*Z) - f(v - epsilon*Z)) / (2 epsilon) );
};
}
// A point together with a value at that location.
struct evaluatedpoint {
triple pt;
real value;
void operator init(triple pt, real value) {
this.pt = pt;
this.value = value;
}
}
triple operator cast(evaluatedpoint p) { return p.pt; }
// Compute the values of a function at every vertex of an nx by ny by nz
// array of rectangular solids.
evaluatedpoint[][][] make3dgrid(triple a, triple b, int nx, int ny, int nz,
real f(triple), bool allowzero = false)
{
evaluatedpoint[][][] toreturn = new evaluatedpoint[nx+1][ny+1][nz+1];
for (int i = 0; i <= nx; ++i) {
for (int j = 0; j <= ny; ++j) {
for (int k = 0; k <= nz; ++k) {
triple pt = (interp(a.x, b.x, i/nx),
interp(a.y, b.y, j/ny),
interp(a.z, b.z, k/nz));
real value = f(pt);
if (value == 0 && !allowzero) value = 1e-5;
toreturn[i][j][k] = evaluatedpoint(pt, value);
}
}
}
return toreturn;
}
// The following utilities make, for instance, slice(A, i, j, k, l)
// equivalent to what A[i:j][k:l] ought to mean for two- and three-
// -dimensional arrays of evaluatedpoints and of positionedvectors.
typedef evaluatedpoint T;
T[][] slice(T[][] a, int start1, int end1, int start2, int end2) {
T[][] toreturn = new T[end1-start1][];
for (int i = start1; i < end1; ++i) {
toreturn[i-start1] = a[i][start2:end2];
}
return toreturn;
}
T[][][] slice(T[][][] a, int start1, int end1,
int start2, int end2,
int start3, int end3) {
T[][][] toreturn = new T[end1-start1][][];
for (int i = start1; i < end1; ++i) {
toreturn[i-start1] = slice(a[i], start2, end2, start3, end3);
}
return toreturn;
}
typedef positionedvector T;
T[][] slice(T[][] a, int start1, int end1, int start2, int end2) {
T[][] toreturn = new T[end1-start1][];
for (int i = start1; i < end1; ++i) {
toreturn[i-start1] = a[i][start2:end2];
}
return toreturn;
}
T[][][] slice(T[][][] a, int start1, int end1,
int start2, int end2,
int start3, int end3) {
T[][][] toreturn = new T[end1-start1][][];
for (int i = start1; i < end1; ++i) {
toreturn[i-start1] = slice(a[i], start2, end2, start3, end3);
}
return toreturn;
}
// An object of class gridwithzeros stores the values of a function at each vertex
// of a three-dimensional grid, together with zeros of the function along edges
// of the grid and the gradient of the function at each such zero.
struct gridwithzeros {
int nx, ny, nz;
evaluatedpoint[][][] corners;
positionedvector[][][] xdirzeros;
positionedvector[][][] ydirzeros;
positionedvector[][][] zdirzeros;
triple grad(triple);
real f(triple);
int maxdepth;
bool usetriangles;
// Populate the edges with zeros that have a sign change and are not already
// populated.
void fillzeros() {
for (int j = 0; j < ny+1; ++j) {
for (int k = 0; k < nz+1; ++k) {
real y = corners[0][j][k].pt.y;
real z = corners[0][j][k].pt.z;
real f_along_x(real t) { return f((t, y, z)); }
for (int i = 0; i < nx; ++i) {
if (xdirzeros[i][j][k] != null) continue;
evaluatedpoint start = corners[i][j][k];
evaluatedpoint end = corners[i+1][j][k];
if ((start.value > 0 && end.value > 0) || (start.value < 0 && end.value < 0))
xdirzeros[i][j][k] = null;
else {
triple root = (0,y,z);
root += X * findroot(f_along_x, start.pt.x, end.pt.x,
fa=start.value, fb=end.value);
triple normal = grad(root);
xdirzeros[i][j][k] = positionedvector(root, normal);
}
}
}
}
for (int i = 0; i < nx+1; ++i) {
for (int k = 0; k < nz+1; ++k) {
real x = corners[i][0][k].pt.x;
real z = corners[i][0][k].pt.z;
real f_along_y(real t) { return f((x, t, z)); }
for (int j = 0; j < ny; ++j) {
if (ydirzeros[i][j][k] != null) continue;
evaluatedpoint start = corners[i][j][k];
evaluatedpoint end = corners[i][j+1][k];
if ((start.value > 0 && end.value > 0) || (start.value < 0 && end.value < 0))
ydirzeros[i][j][k] = null;
else {
triple root = (x,0,z);
root += Y * findroot(f_along_y, start.pt.y, end.pt.y,
fa=start.value, fb=end.value);
triple normal = grad(root);
ydirzeros[i][j][k] = positionedvector(root, normal);
}
}
}
}
for (int i = 0; i < nx+1; ++i) {
for (int j = 0; j < ny+1; ++j) {
real x = corners[i][j][0].pt.x;
real y = corners[i][j][0].pt.y;
real f_along_z(real t) { return f((x, y, t)); }
for (int k = 0; k < nz; ++k) {
if (zdirzeros[i][j][k] != null) continue;
evaluatedpoint start = corners[i][j][k];
evaluatedpoint end = corners[i][j][k+1];
if ((start.value > 0 && end.value > 0) || (start.value < 0 && end.value < 0))
zdirzeros[i][j][k] = null;
else {
triple root = (x,y,0);
root += Z * findroot(f_along_z, start.pt.z, end.pt.z,
fa=start.value, fb=end.value);
triple normal = grad(root);
zdirzeros[i][j][k] = positionedvector(root, normal);
}
}
}
}
}
// Fill in the grid vertices and the zeros along edges. Each cube starts at
// depth one and the depth increases each time it subdivides; maxdepth is the
// maximum subdivision depth. When a cube at maxdepth cannot be resolved to
// patches, it is left empty.
void operator init(int nx, int ny, int nz,
real f(triple), triple a, triple b,
int maxdepth = 6, bool usetriangles) {
this.nx = nx;
this.ny = ny;
this.nz = nz;
grad = nGrad(f);
this.f = f;
this.maxdepth = maxdepth;
this.usetriangles = usetriangles;
corners = make3dgrid(a, b, nx, ny, nz, f);
xdirzeros = new positionedvector[nx][ny+1][nz+1];
ydirzeros = new positionedvector[nx+1][ny][nz+1];
zdirzeros = new positionedvector[nx+1][ny+1][nz];
for (int i = 0; i <= nx; ++i) {
for (int j = 0; j <= ny; ++j) {
for (int k = 0; k <= nz; ++k) {
if (i < nx) xdirzeros[i][j][k] = null;
if (j < ny) ydirzeros[i][j][k] = null;
if (k < nz) zdirzeros[i][j][k] = null;
}
}
}
fillzeros();
}
// Doubles nx, ny, and nz by halving the sizes of the cubes along the x, y, and z
// directions (resulting in 8 times as many cubes). Already existing data about
// function values and zeros is copied; vertices and edges with no such pre-existing
// data are populated.
//
// Returns true if subdivide succeeded, false if it failed (because maxdepth
// was exceeded).
bool subdivide() {
if (maxdepth <= 1) {
return false;
}
--maxdepth;
triple a = corners[0][0][0];
triple b = corners[nx][ny][nz];
nx *= 2;
ny *= 2;
nz *= 2;
evaluatedpoint[][][] oldcorners = corners;
corners = new evaluatedpoint[nx+1][ny+1][nz+1];
for (int i = 0; i <= nx; ++i) {
for (int j = 0; j <= ny; ++j) {
for (int k = 0; k <= nz; ++k) {
if (i % 2 == 0 && j % 2 == 0 && k % 2 == 0) {
corners[i][j][k] = oldcorners[quotient(i,2)][quotient(j,2)][quotient(k,2)];
} else {
triple pt = (interp(a.x, b.x, i/nx),
interp(a.y, b.y, j/ny),
interp(a.z, b.z, k/nz));
real value = f(pt);
if (value == 0) value = 1e-5;
corners[i][j][k] = evaluatedpoint(pt, value);
}
}
}
}
positionedvector[][][] oldxdir = xdirzeros;
xdirzeros = new positionedvector[nx][ny+1][nz+1];
for (int i = 0; i < nx; ++i) {
for (int j = 0; j < ny + 1; ++j) {
for (int k = 0; k < nz + 1; ++k) {
if (j % 2 != 0 || k % 2 != 0) {
xdirzeros[i][j][k] = null;
} else {
positionedvector zero = oldxdir[quotient(i,2)][quotient(j,2)][quotient(k,2)];
if (zero == null) {
xdirzeros[i][j][k] = null;
continue;
}
real x = zero.position.x;
if (x > interp(a.x, b.x, i/nx) && x < interp(a.x, b.x, (i+1)/nx)) {
xdirzeros[i][j][k] = zero;
} else {
xdirzeros[i][j][k] = null;
}
}
}
}
}
positionedvector[][][] oldydir = ydirzeros;
ydirzeros = new positionedvector[nx+1][ny][nz+1];
for (int i = 0; i < nx+1; ++i) {
for (int j = 0; j < ny; ++j) {
for (int k = 0; k < nz + 1; ++k) {
if (i % 2 != 0 || k % 2 != 0) {
ydirzeros[i][j][k] = null;
} else {
positionedvector zero = oldydir[quotient(i,2)][quotient(j,2)][quotient(k,2)];
if (zero == null) {
ydirzeros[i][j][k] = null;
continue;
}
real y = zero.position.y;
if (y > interp(a.y, b.y, j/ny) && y < interp(a.y, b.y, (j+1)/ny)) {
ydirzeros[i][j][k] = zero;
} else {
ydirzeros[i][j][k] = null;
}
}
}
}
}
positionedvector[][][] oldzdir = zdirzeros;
zdirzeros = new positionedvector[nx+1][ny+1][nz];
for (int i = 0; i < nx + 1; ++i) {
for (int j = 0; j < ny + 1; ++j) {
for (int k = 0; k < nz; ++k) {
if (i % 2 != 0 || j % 2 != 0) {
zdirzeros[i][j][k] = null;
} else {
positionedvector zero = oldzdir[quotient(i,2)][quotient(j,2)][quotient(k,2)];
if (zero == null) {
zdirzeros[i][j][k] = null;
continue;
}
real z = zero.position.z;
if (z > interp(a.z, b.z, k/nz) && z < interp(a.z, b.z, (k+1)/nz)) {
zdirzeros[i][j][k] = zero;
} else {
zdirzeros[i][j][k] = null;
}
}
}
}
}
fillzeros();
return true;
}
// Forward declaration of the draw method, which will be called by drawcube().
patch[] draw(bool[] reportactive = null);
// Construct the patches, assuming that we are working
// with a single cube (nx = ny = nz = 1). This method will subdivide the
// cube if necessary. The parameter reportactive should be an array of
// length 6. Setting an entry to true indicates that the surface abuts the
// corresponding face (according to the earlier enum), and thus that the
// algorithm should be sure that something is drawn in the cube sharing
// that face--even if all the vertices of that cube have the same sign.
patch[] drawcube(bool[] reportactive = null) {
// First, determine which edges (if any) actually have zeros on them.
edge[] zeroedges = new edge[0];
positionedvector[] zeros = new positionedvector[0];
int currentface, nextface;
void pushifnonnull(positionedvector v) {
if (v != null) {
zeroedges.push(edge(currentface, nextface));
zeros.push(v);
}
}
positionedvector findzero(int face1, int face2) {
edge e = edge(face1, face2);
for (int i = 0; i < zeroedges.length; ++i) {
if (zeroedges[i] == e) return zeros[i];
}
return null;
}
currentface = XLOW;
nextface = YHIGH;
pushifnonnull(zdirzeros[0][1][0]);
nextface = YLOW;
pushifnonnull(zdirzeros[0][0][0]);
nextface = ZHIGH;
pushifnonnull(ydirzeros[0][0][1]);
nextface = ZLOW;
pushifnonnull(ydirzeros[0][0][0]);
currentface = XHIGH;
nextface = YHIGH;
pushifnonnull(zdirzeros[1][1][0]);
nextface = YLOW;
pushifnonnull(zdirzeros[1][0][0]);
nextface = ZHIGH;
pushifnonnull(ydirzeros[1][0][1]);
nextface = ZLOW;
pushifnonnull(ydirzeros[1][0][0]);
currentface = YHIGH;
nextface = ZHIGH;
pushifnonnull(xdirzeros[0][1][1]);
currentface = ZHIGH;
nextface = YLOW;
pushifnonnull(xdirzeros[0][0][1]);
currentface = YLOW;
nextface = ZLOW;
pushifnonnull(xdirzeros[0][0][0]);
currentface = ZLOW;
nextface = YHIGH;
pushifnonnull(xdirzeros[0][1][0]);
//Now, string those edges together to make a circle.
patch[] subdividecube() {
if (!subdivide()) {
return new patch[0];
}
return draw(reportactive);
}
if (zeroedges.length < 3) {
return subdividecube();
}
int[] faceorder = makecircle(zeroedges);
if (alias(faceorder,null)) {
return subdividecube();
}
positionedvector[] patchcorners = new positionedvector[0];
for (int i = 0; i < faceorder.length; ++i) {
patchcorners.push(findzero(faceorder[i], faceorder[i+1]));
}
patchcorners.cyclic = true;
//Now, produce the cyclic path around the edges.
path3 edgecycle;
for (int i = 0; i < faceorder.length; ++i) {
path3 currentpath = pathinface(patchcorners[i], patchcorners[i+1],
faceorder[i+1], faceorder[i],
faceorder[i+2]);
triple testpoint = point(currentpath, 0.5);
if (!checkpt(testpoint, f, grad, corners[0][0][0], corners[1][1][1])) {
return subdividecube();
}
edgecycle = edgecycle & currentpath;
}
edgecycle = edgecycle & cycle;
{ // Ensure the outward normals are pointing in the same direction as the gradient.
triple tangentin = patchcorners[0].position - precontrol(edgecycle, 0);
triple tangentout = postcontrol(edgecycle, 0) - patchcorners[0].position;
triple normal = cross(tangentin, tangentout);
if (dot(normal, patchcorners[0].direction) < 0) {
edgecycle = reverse(edgecycle);
patchcorners = patchcorners[-sequence(patchcorners.length)];
patchcorners.cyclic = true;
}
}
patch[] toreturn = quadpatches(edgecycle, patchcorners, f, grad,
corners[0][0][0], corners[1][1][1], usetriangles);
if (alias(toreturn, null)) return subdividecube();
return toreturn;
}
// Extracts the specified cube as a gridwithzeros object with
// nx = ny = nz = 1.
gridwithzeros getcube(int i, int j, int k) {
gridwithzeros cube = new gridwithzeros;
cube.grad = grad;
cube.f = f;
cube.nx = 1;
cube.ny = 1;
cube.nz = 1;
cube.maxdepth = maxdepth;
cube.usetriangles = usetriangles;
cube.corners = slice(corners,i,i+2,j,j+2,k,k+2);
cube.xdirzeros = slice(xdirzeros,i,i+1,j,j+2,k,k+2);
cube.ydirzeros = slice(ydirzeros,i,i+2,j,j+1,k,k+2);
cube.zdirzeros = slice(zdirzeros,i,i+2,j,j+2,k,k+1);
return cube;
}
// Returns an array of patches representing the surface.
// The parameter reportactive should be an array of
// length 6. Setting an entry to true indicates that the surface abuts the
// corresponding face of the cube that bounds the entire grid.
//
// If reportactive == null, it is assumed that this is a top-level call;
// a dot is printed to stdout for each cube drawn as a very rough
// progress indicator.
//
// If reportactive != null, then it is assumed that the caller had a strong
// reason to believe that this grid contains a part of the surface; the
// grid will subdivide all the way to maxdepth if necessary to find points
// on the surface.
draw = new patch[](bool[] reportactive = null) {
if (alias(reportactive, null)) progress(true);
// A list of all the patches not already drawn but known
// to contain part of the surface. This "queue" is
// actually implemented as stack for simplicity, since
// it does not make any difference. In a multi-threaded
// version of the algorithm, a queue (shared across all threads)
// would make more sense than a stack.
triple[] queue = new triple[0];
bool[][][] enqueued = new bool[nx][ny][nz];
for (int i = 0; i < enqueued.length; ++i) {
for (int j = 0; j < enqueued[i].length; ++j) {
for (int k = 0; k < enqueued[i][j].length; ++k) {
enqueued[i][j][k] = false;
}
}
}
void enqueue(int i, int j, int k) {
if (i >= 0 && i < nx
&& j >= 0 && j < ny
&& k >= 0 && k < nz
&& !enqueued[i][j][k]) {
queue.push((i,j,k));
enqueued[i][j][k] = true;
}
if (!alias(reportactive, null)) {
if (i < 0) reportactive[XLOW] = true;
if (i >= nx) reportactive[XHIGH] = true;
if (j < 0) reportactive[YLOW] = true;
if (j >= ny) reportactive[YHIGH] = true;
if (k < 0) reportactive[ZLOW] = true;
if (k >= nz) reportactive[ZHIGH] = true;
}
}
for (int i = 0; i < nx+1; ++i) {
for (int j = 0; j < ny+1; ++j) {
for (int k = 0; k < nz+1; ++k) {
if (i < nx && xdirzeros[i][j][k] != null) {
for (int jj = j-1; jj <= j; ++jj)
for (int kk = k-1; kk <= k; ++kk)
enqueue(i, jj, kk);
}
if (j < ny && ydirzeros[i][j][k] != null) {
for (int ii = i-1; ii <= i; ++ii)
for (int kk = k-1; kk <= k; ++kk)
enqueue(ii, j, kk);
}
if (k < nz && zdirzeros[i][j][k] != null) {
for (int ii = i-1; ii <= i; ++ii)
for (int jj = j-1; jj <= j; ++jj)
enqueue(ii, jj, k);
}
}
}
}
if (!alias(reportactive, null) && queue.length == 0) {
if (subdivide()) return draw(reportactive);
}
patch[] surface = new patch[0];
while (queue.length > 0) {
triple coord = queue.pop();
int i = floor(coord.x);
int j = floor(coord.y);
int k = floor(coord.z);
bool[] reportface = array(6, false);
patch[] toappend = getcube(i,j,k).drawcube(reportface);
if (reportface[XLOW]) enqueue(i-1,j,k);
if (reportface[XHIGH]) enqueue(i+1,j,k);
if (reportface[YLOW]) enqueue(i,j-1,k);
if (reportface[YHIGH]) enqueue(i,j+1,k);
if (reportface[ZLOW]) enqueue(i,j,k-1);
if (reportface[ZHIGH]) enqueue(i,j,k+1);
surface.append(toappend);
if (alias(reportactive, null)) progress();
}
if (alias(reportactive, null)) progress(false);
return surface;
};
}
// The external interface of this whole module. Accepts exactly one
// function (throws an error if two or zero functions are specified).
// The function should be differentiable. (Whatever you do, do not
// pass in an indicator function!) Ideally, the zero locus of the
// function should be smooth; singularities will significantly slow
// down the algorithm and potentially give bad results.
//
// Returns a plot of the zero locus of the function within the
// rectangular solid with opposite corners at a and b.
//
// Additional parameters:
// n - the number of initial segments in each of the x, y, z directions.
// overlapedges - if true, the patches of the surface are slightly enlarged
// to compensate for an artifact in which the viewer can see through the
// boundary between patches. (Some of this may actually be a result of
// edges not lining up perfectly, but I'm fairly sure a lot of it arises
// purely as a rendering artifact.)
// nx - override n in the x direction
// ny - override n in the y direction
// nz - override n in the z direction
// maxdepth - the maximum depth to which the algorithm will subdivide in
// an effort to find patches that closely approximate the true surface.
surface implicitsurface(real f(triple) = null, real ff(real,real,real) = null,
triple a, triple b,
int n = nmesh,
bool keyword overlapedges = false,
int keyword nx=n, int keyword ny=n,
int keyword nz=n,
int keyword maxdepth = 8,
bool keyword usetriangles=true) {
if (f == null && ff == null)
abort("implicitsurface called without specifying a function.");
if (f != null && ff != null)
abort("Only specify one function when calling implicitsurface.");
if (f == null) f = new real(triple w) { return ff(w.x, w.y, w.z); };
gridwithzeros grid = gridwithzeros(nx, ny, nz, f, a, b, maxdepth=maxdepth,
usetriangles=usetriangles);
patch[] patches = grid.draw();
if (overlapedges) {
for (int i = 0; i < patches.length; ++i) {
triple center = (patches[i].triangular ?
patches[i].point(1/3, 1/3) : patches[i].point(1/2,1/2));
transform3 T=shift(center) * scale3(1.03) * shift(-center);
patches[i] = T * patches[i];
}
}
return surface(...patches);
}