3D Line Art Engine: Outline

I have ported most of "ln, The 3D Line Art Engine" by Fogleman (@FogleBird) to javascript, so it can be used on Turtletoy. Not all code is ported and not all code-paths are tested: probably there are still (a lot of) bugs left.

This Turtle is a port of the example: github.com/fogleman/ln/blob/master/examples/outline.go

Log in to post a comment.

// 3D Line Art Engine: Function. Created by Reinder Nijhoff 2019
// @reindernijhoff
//
// https://turtletoy.net/turtle/099830e033
//
// I have ported most of "ln, The 3D Line Art Engine"[1] by Fogleman (@FogleBird) to javascript, so
// it can be used on Turtletoy. Not all code is ported and not all code-paths are tested:
// probably there are still (a lot of) bugs left.
//
// This Turtle is a port of the example: https://github.com/fogleman/ln/blob/master/examples/outline.go
//
// [1] https://github.com/fogleman/ln
//

const DRAW_HIDDEN_LINES = false;

const turtle = new Turtle();
let paths, scene;

function walk(i) {
	const eye = new Vector(8, 8, 8);
	const center = new Vector(0, 0, 0);
	const up = new Vector(0, 0, 1);
	const width = 190.0;
	const height = 190.0;
	const fovy = 50.0;
	const znear = 0.1;
	const zfar = 100.0;
	const step = 0.01;      // how finely to chop the paths for visibility testing

    if (i==0) {
        scene = new Scene();
		
		const n = 14;
    	for (let x = -n; x <= n; x++) {
    		for (let y = -n; y <= n; y++) {
    			const z = Math.random() * 3;
    			const v = new Vector(x, y, z);
    			// const sphere = new Cube(v, new Vector(v.X+.4,v.Y+.4,v.Z+.4));// .45);
    			// const sphere = new Sphere(v, .45);
    			const sphere = new OutlineSphere(eye, up, v, .45);
    			scene.Add(sphere);
    		}
    	}
    }
    
    paths = scene.Render(eye, center, up, width, height, fovy, znear, zfar, step, i);
    if (paths) {
        for (let k=0; k<paths.v.length; k++) {
            const path = paths.v[k];
            turtle.penup();
            turtle.goto(path.v[0].X, path.v[0].Y);
            turtle.pendown();
            for (let j=1; j<path.v.length; j++) {
                turtle.goto(path.v[j].X, path.v[j].Y);
            }
        }
        return true;
    } else {
        return false;
    }
}

//  
// https://github.com/fogleman/ln/blob/master/ln/util.go
//
const Radians = (degrees) => degrees * Math.PI / 180;
const Degrees = (radians) => radians * 180 / Math.PI;
const Median = (items) => {
	let n = items.length;
	if (n == 0) {
		return 0;
	} else if (n%2 == 1) {
		return items[n/2];
	} else {
		const a = items[n/2-1];
		const b = items[n/2];
		return (a + b) / 2;
	}
}
const INF = 1e9;
const EPS = 1e-9;
//
// https://github.com/fogleman/ln/blob/master/ln/axis.go
//
const AxisNone = -1;
const AxisX = 0;
const AxisY = 1;
const AxisZ = 2;
//  
// https://github.com/fogleman/ln/blob/master/ln/vector.go 
//
class Vector {
    constructor(x, y, z) {
    	this.X = x; this.Y = y; this.Z = z;
    }
    RandomUnitVector() {
        let x, y, z;
    	do {
    		x = Math.ranomd()*2 - 1;
    		y = Math.ranomd()*2 - 1;
    		z = Math.ranomd()*2 - 1;
    	} while (x*x+y*y+z*z > 1);
    	return new Vector(x, y, z).Normalize();
    }
    Length() { return Math.sqrt(this.X*this.X + this.Y*this.Y + this.Z*this.Z); }
    Distance(b) { return this.Sub(b).Length(); }
    LengthSquared() { return this.X*this.X + this.Y*this.Y + this.Z*this.Z; }
    DistanceSquared(b){ return this.Sub(b).LengthSquared(); }
    Dot(b) { return this.X*b.X + this.Y*b.Y + this.Z*b.Z; }
    Cross(b) { 
        return new Vector(  this.Y*b.Z - this.Z*b.Y, 
                            this.Z*b.X - this.X*b.Z, 
                            this.X*b.Y - this.Y*b.X); }
    Normalize() { return this.MulScalar(1/this.Length()); }
    Add(b) { return new Vector(this.X + b.X, this.Y + b.Y, this.Z + b.Z); }
    Sub(b) { return new Vector(this.X - b.X, this.Y - b.Y, this.Z - b.Z); }
    Mul(b) { return new Vector(this.X * b.X, this.Y * b.Y, this.Z * b.Z); }
    Div(b) { return new Vector(this.X / b.X, this.Y / b.Y, this.Z / b.Z); }
    AddScalar(b) { return new Vector(this.X + b, this.Y + b, this.Z + b); }
    SubScalar(b) { return new Vector(this.X - b, this.Y - b, this.Z - b); }
    MulScalar(b) { return new Vector(this.X * b, this.Y * b, this.Z * b); }
    DivScalar(b) { return new Vector(this.X / b, this.Y / b, this.Z / b); }
    Min(b) { return new Vector(Math.min(this.X, b.X), Math.min(this.Y, b.Y), Math.min(this.Z, b.Z)); }
    Max(b) { return new Vector(Math.max(this.X, b.X), Math.max(this.Y, b.Y), Math.max(this.Z, b.Z)); }
    MinAxis() {
    	const x = Math.abs(this.X), y = Math.abs(this.Y), z = Math.abs(this.Z);
    	if (x <= y && x <= z) {
    		return new Vector(1, 0, 0);
    	} else if (y <= x && y <= z) {
    		return new Vector(0, 1, 0);
    	}
    	return new Vector(0, 0, 1);
    }
    MinComponent() { return Math.min(Math.min(this.X, this.Y), this.Z); }
    SegmentDistance(v, w) {
    	const l2 = v.DistanceSquared(w)
    	if (l2 == 0) {
    		return this.Distance(v);
    	}
    	const t = this.Sub(v).Dot(w.Sub(v)) / l2;
    	if (t < 0) {
    		return this.Distance(v);
    	}
    	if (t > 1){
    		return this.Distance(w);
    	}
    	return v.Add(w.Sub(v).MulScalar(t)).Distance(this);
    }
}

//
// https://github.com/fogleman/ln/blob/master/ln/matrix.go
//
class Matrix {
    constructor(x00, x01, x02, x03,
            	x10, x11, x12, x13,
	            x20, x21, x22, x23,
	            x30, x31, x32, x33) {
	               	this.x00=x00, this.x01=x01, this.x02=x02, this.x03=x03;
	               	this.x10=x10, this.x11=x11, this.x12=x12, this.x13=x13;
	               	this.x20=x20, this.x21=x21, this.x22=x22, this.x23=x23;
	               	this.x30=x30, this.x31=x31, this.x32=x32, this.x33=x33;
	            }
    Translate(v) {
    	return Translate(v).Mul(this);
    }
    Scale(v) {
    	return Scale(v).Mul(this);
    }
    Rotate(v, a) {
    	return Rotate(v, a).Mul(this);
    }
    Frustum(l, r, b, t, n, f) {
    	return Frustum(l, r, b, t, n, f).Mul(this);
    }
    Orthographic(l, r, b, t, n, f) {
    	return Orthographic(l, r, b, t, n, f).Mul(this);
    }
    Perspective(fovy, aspect, near, far) {
    	return Perspective(fovy, aspect, near, far).Mul(this);
    }
    Mul(b) {
    	const m = new Matrix();
    	m.x00 = this.x00*b.x00 + this.x01*b.x10 + this.x02*b.x20 + this.x03*b.x30;
    	m.x10 = this.x10*b.x00 + this.x11*b.x10 + this.x12*b.x20 + this.x13*b.x30;
    	m.x20 = this.x20*b.x00 + this.x21*b.x10 + this.x22*b.x20 + this.x23*b.x30;
    	m.x30 = this.x30*b.x00 + this.x31*b.x10 + this.x32*b.x20 + this.x33*b.x30;
    	m.x01 = this.x00*b.x01 + this.x01*b.x11 + this.x02*b.x21 + this.x03*b.x31;
    	m.x11 = this.x10*b.x01 + this.x11*b.x11 + this.x12*b.x21 + this.x13*b.x31;
    	m.x21 = this.x20*b.x01 + this.x21*b.x11 + this.x22*b.x21 + this.x23*b.x31;
    	m.x31 = this.x30*b.x01 + this.x31*b.x11 + this.x32*b.x21 + this.x33*b.x31;
    	m.x02 = this.x00*b.x02 + this.x01*b.x12 + this.x02*b.x22 + this.x03*b.x32;
    	m.x12 = this.x10*b.x02 + this.x11*b.x12 + this.x12*b.x22 + this.x13*b.x32;
    	m.x22 = this.x20*b.x02 + this.x21*b.x12 + this.x22*b.x22 + this.x23*b.x32;
    	m.x32 = this.x30*b.x02 + this.x31*b.x12 + this.x32*b.x22 + this.x33*b.x32;
    	m.x03 = this.x00*b.x03 + this.x01*b.x13 + this.x02*b.x23 + this.x03*b.x33;
    	m.x13 = this.x10*b.x03 + this.x11*b.x13 + this.x12*b.x23 + this.x13*b.x33;
    	m.x23 = this.x20*b.x03 + this.x21*b.x13 + this.x22*b.x23 + this.x23*b.x33;
    	m.x33 = this.x30*b.x03 + this.x31*b.x13 + this.x32*b.x23 + this.x33*b.x33;
    	return m;
    }
    MulPosition(b) {
    	const x = this.x00*b.X + this.x01*b.Y + this.x02*b.Z + this.x03;
    	const y = this.x10*b.X + this.x11*b.Y + this.x12*b.Z + this.x13;
    	const z = this.x20*b.X + this.x21*b.Y + this.x22*b.Z + this.x23;
    	return new Vector(x, y, z);
    }
    MulPositionW(b) {
    	const x = this.x00*b.X + this.x01*b.Y + this.x02*b.Z + this.x03;
    	const y = this.x10*b.X + this.x11*b.Y + this.x12*b.Z + this.x13;
    	const z = this.x20*b.X + this.x21*b.Y + this.x22*b.Z + this.x23;
    	const w = this.x30*b.X + this.x31*b.Y + this.x32*b.Z + this.x33;
    	return new Vector(x / w, y / w, z / w);
    }
    MulDirection(b) {
    	const x = this.x00*b.X + this.x01*b.Y + this.x02*b.Z;
    	const y = this.x10*b.X + this.x11*b.Y + this.x12*b.Z;
    	const z = this.x20*b.X + this.x21*b.Y + this.x22*b.Z;
    	return new Vector(x, y, z).Normalize();
    }
    MulRay(b) {
    	return new Ray(this.MulPosition(b.Origin), this.MulDirection(b.Direction));
    }
    MulBox(box) {
    	// http://dev.theomader.com/transform-bounding-boxes/
    	const r = new Vector(this.x00, this.x10, this.x20);
    	const u = new Vector(this.x01, this.x11, this.x21);
    	const b = new Vector(this.x02, this.x12, this.x22);
    	const t = new Vector(this.x03, this.x13, this.x23);
    	let xa = r.MulScalar(box.Min.X);
    	let xb = r.MulScalar(box.Max.X);
    	let ya = u.MulScalar(box.Min.Y);
    	let yb = u.MulScalar(box.Max.Y);
    	let za = b.MulScalar(box.Min.Z);
    	let zb = b.MulScalar(box.Max.Z);
    	const min = xa.Min(xb).Add(ya.Min(yb)).Add(za.Min(zb)).Add(t);
    	const max = xa.Max(xb).Add(ya.Max(yb)).Add(za.Max(zb)).Add(t);
    	return new Box(min, max);
    }
    Transpose() {
    	return new Matrix(
    		this.x00, this.x10, this.x20, this.x30,
    		this.x01, this.x11, this.x21, this.x31,
    		this.x02, this.x12, this.x22, this.x32,
    		this.x03, this.x13, this.x23, this.x33);
    }
    Determinant() {
    	return (this.x00*this.x11*this.x22*this.x33 - this.x00*this.x11*this.x23*this.x32 +
    		this.x00*this.x12*this.x23*this.x31 - this.x00*this.x12*this.x21*this.x33 +
    		this.x00*this.x13*this.x21*this.x32 - this.x00*this.x13*this.x22*this.x31 -
    		this.x01*this.x12*this.x23*this.x30 + this.x01*this.x12*this.x20*this.x33 -
    		this.x01*this.x13*this.x20*this.x32 + this.x01*this.x13*this.x22*this.x30 -
    		this.x01*this.x10*this.x22*this.x33 + this.x01*this.x10*this.x23*this.x32 +
    		this.x02*this.x13*this.x20*this.x31 - this.x02*this.x13*this.x21*this.x30 +
    		this.x02*this.x10*this.x21*this.x33 - this.x02*this.x10*this.x23*this.x31 +
    		this.x02*this.x11*this.x23*this.x30 - this.x02*this.x11*this.x20*this.x33 -
    		this.x03*this.x10*this.x21*this.x32 + this.x03*this.x10*this.x22*this.x31 -
    		this.x03*this.x11*this.x22*this.x30 + this.x03*this.x11*this.x20*this.x32 -
    		this.x03*this.x12*this.x20*this.x31 + this.x03*this.x12*this.x21*this.x30);
    }
    Inverse() {
    	const m = new Matrix();
    	const d = this.Determinant();
    	m.x00 = (this.x12*this.x23*this.x31 - this.x13*this.x22*this.x31 + this.x13*this.x21*this.x32 - this.x11*this.x23*this.x32 - this.x12*this.x21*this.x33 + this.x11*this.x22*this.x33) / d;
    	m.x01 = (this.x03*this.x22*this.x31 - this.x02*this.x23*this.x31 - this.x03*this.x21*this.x32 + this.x01*this.x23*this.x32 + this.x02*this.x21*this.x33 - this.x01*this.x22*this.x33) / d;
    	m.x02 = (this.x02*this.x13*this.x31 - this.x03*this.x12*this.x31 + this.x03*this.x11*this.x32 - this.x01*this.x13*this.x32 - this.x02*this.x11*this.x33 + this.x01*this.x12*this.x33) / d;
    	m.x03 = (this.x03*this.x12*this.x21 - this.x02*this.x13*this.x21 - this.x03*this.x11*this.x22 + this.x01*this.x13*this.x22 + this.x02*this.x11*this.x23 - this.x01*this.x12*this.x23) / d;
    	m.x10 = (this.x13*this.x22*this.x30 - this.x12*this.x23*this.x30 - this.x13*this.x20*this.x32 + this.x10*this.x23*this.x32 + this.x12*this.x20*this.x33 - this.x10*this.x22*this.x33) / d;
    	m.x11 = (this.x02*this.x23*this.x30 - this.x03*this.x22*this.x30 + this.x03*this.x20*this.x32 - this.x00*this.x23*this.x32 - this.x02*this.x20*this.x33 + this.x00*this.x22*this.x33) / d;
    	m.x12 = (this.x03*this.x12*this.x30 - this.x02*this.x13*this.x30 - this.x03*this.x10*this.x32 + this.x00*this.x13*this.x32 + this.x02*this.x10*this.x33 - this.x00*this.x12*this.x33) / d;
    	m.x13 = (this.x02*this.x13*this.x20 - this.x03*this.x12*this.x20 + this.x03*this.x10*this.x22 - this.x00*this.x13*this.x22 - this.x02*this.x10*this.x23 + this.x00*this.x12*this.x23) / d;
    	m.x20 = (this.x11*this.x23*this.x30 - this.x13*this.x21*this.x30 + this.x13*this.x20*this.x31 - this.x10*this.x23*this.x31 - this.x11*this.x20*this.x33 + this.x10*this.x21*this.x33) / d;
    	m.x21 = (this.x03*this.x21*this.x30 - this.x01*this.x23*this.x30 - this.x03*this.x20*this.x31 + this.x00*this.x23*this.x31 + this.x01*this.x20*this.x33 - this.x00*this.x21*this.x33) / d;
    	m.x22 = (this.x01*this.x13*this.x30 - this.x03*this.x11*this.x30 + this.x03*this.x10*this.x31 - this.x00*this.x13*this.x31 - this.x01*this.x10*this.x33 + this.x00*this.x11*this.x33) / d;
    	m.x23 = (this.x03*this.x11*this.x20 - this.x01*this.x13*this.x20 - this.x03*this.x10*this.x21 + this.x00*this.x13*this.x21 + this.x01*this.x10*this.x23 - this.x00*this.x11*this.x23) / d;
    	m.x30 = (this.x12*this.x21*this.x30 - this.x11*this.x22*this.x30 - this.x12*this.x20*this.x31 + this.x10*this.x22*this.x31 + this.x11*this.x20*this.x32 - this.x10*this.x21*this.x32) / d;
    	m.x31 = (this.x01*this.x22*this.x30 - this.x02*this.x21*this.x30 + this.x02*this.x20*this.x31 - this.x00*this.x22*this.x31 - this.x01*this.x20*this.x32 + this.x00*this.x21*this.x32) / d;
    	m.x32 = (this.x02*this.x11*this.x30 - this.x01*this.x12*this.x30 - this.x02*this.x10*this.x31 + this.x00*this.x12*this.x31 + this.x01*this.x10*this.x32 - this.x00*this.x11*this.x32) / d;
    	m.x33 = (this.x01*this.x12*this.x20 - this.x02*this.x11*this.x20 + this.x02*this.x10*this.x21 - this.x00*this.x12*this.x21 - this.x01*this.x10*this.x22 + this.x00*this.x11*this.x22) / d;
    	return m;
    }
}
function Identity() {
	return new Matrix(
		1, 0, 0, 0,
		0, 1, 0, 0,
		0, 0, 1, 0,
		0, 0, 0, 1);
}
function Translate(v) {
	return new Matrix(
		1, 0, 0, v.X,
		0, 1, 0, v.Y,
		0, 0, 1, v.Z,
		0, 0, 0, 1);
}
function Scale(v) {
	return new Matrix(
		v.X, 0, 0, 0,
		0, v.Y, 0, 0,
		0, 0, v.Z, 0,
		0, 0, 0, 1);
}
function Rotate(v, a) {
	v = v.Normalize();
	const s = Math.sin(a);
	const c = Math.cos(a);
	const m = 1 - c;
	return new Matrix(
		m*v.X*v.X + c, m*v.X*v.Y + v.Z*s, m*v.Z*v.X - v.Y*s, 0,
		m*v.X*v.Y - v.Z*s, m*v.Y*v.Y + c, m*v.Y*v.Z + v.X*s, 0,
		m*v.Z*v.X + v.Y*s, m*v.Y*v.Z - v.X*s, m*v.Z*v.Z + c, 0,
		0, 0, 0, 1);
}
function Frustum(l, r, b, t, n, f) {
	const t1 = 2 * n;
	const t2 = r - l;
	const t3 = t - b;
	const t4 = f - n;
	return new Matrix(
		t1 / t2, 0, (r + l) / t2, 0,
		0, t1 / t3, (t + b) / t3, 0,
		0, 0, (-f - n) / t4, (-t1 * f) / t4,
		0, 0, -1, 0);
}
function Orthographic(l, r, b, t, n, f) {
	return new Matrix(
		2 / (r - l), 0, 0, -(r + l) / (r - l),
		0, 2 / (t - b), 0, -(t + b) / (t - b),
		0, 0, -2 / (f - n), -(f + n) / (f - n),
		0, 0, 0, 1);
}
function Perspective(fovy, aspect, near, far) {
	const ymax = near * Math.tan(fovy*Math.PI/360);
	const xmax = ymax * aspect;
	return Frustum(-xmax, xmax, -ymax, ymax, near, far);
}
function LookAt(eye, center, up) {
	up = up.Normalize();
	const f = center.Sub(eye).Normalize();
	const s = f.Cross(up).Normalize();
	const u = s.Cross(f).Normalize();
	const m = new Matrix(
		s.X, u.X, -f.X, eye.X,
		s.Y, u.Y, -f.Y, eye.Y,
		s.Z, u.Z, -f.Z, eye.Z,
		0, 0, 0, 1,
	);
	return m.Inverse();
}
//  
// https://github.com/fogleman/ln/blob/master/ln/tree.go
//
class Tree {
    constructor(shapes) {
    	this.Box = BoxForShapes(shapes);
    	this.Root = new Node(shapes);
    	this.Root.Split(0);
    }
    Intersect(r) {
    	const i = this.Box.Intersect(r);
    	if (i.t2 < i.t1 || i.t2 <= 0) {
    		return NoHit;
    	}
    	return this.Root.Intersect(r, i.t1, i.t2);
    }
}

class Node {
    constructor(shapes) {
    	this.Axis =  AxisNone
    	this.Point = 0;
    	this.Shapes = shapes;
    	this.Left  = false;
    	this.Right = false;
    }
    Intersect(r, tmin, tmax) {
    	let tsplit = 0;
    	let leftFirst  = false;
    	switch (this.Axis) {
        	case AxisNone:
        		return this.IntersectShapes(r);
        	case AxisX:
        		tsplit = (this.Point - r.Origin.X) / r.Direction.X;
        		leftFirst = (r.Origin.X < this.Point) || (r.Origin.X == this.Point && r.Direction.X <= 0);
        	case AxisY:
        		tsplit = (this.Point - r.Origin.Y) / r.Direction.Y;
        		leftFirst = (r.Origin.Y < this.Point) || (r.Origin.Y == this.Point && r.Direction.Y <= 0);
        	case AxisZ:
        		tsplit = (this.Point - r.Origin.Z) / r.Direction.Z;
        		leftFirst = (r.Origin.Z < this.Point) || (r.Origin.Z == this.Point && r.Direction.Z <= 0);
    	}
    	let first, second;
    	if (leftFirst) {
    		first = this.Left;
    		second = this.Right;
    	} else {
    		first = this.Right;
    		second = this.Left;
    	}
    	if (tsplit > tmax || tsplit <= 0) {
    		return first.Intersect(r, tmin, tmax);
    	} else if (tsplit < tmin) {
    		return second.Intersect(r, tmin, tmax);
    	} else {
    		const h1 = first.Intersect(r, tmin, tsplit);
    		if (h1.T <= tsplit) {
    			return h1;
    		}
    		const h2 = second.Intersect(r, tsplit, Math.min(tmax, h1.T));
    		if (h1.T <= h2.T) {
    			return h1;
    		} else {
    			return h2;
    		}
    	}
    }
    IntersectShapes(r) {
    	let hit = NoHit;
    	for (const _ in this.Shapes) {
    	    const shape = this.Shapes[_];
    		const h = shape.Intersect(r);
    		if (h.T < hit.T ){
    			hit = h;
    		}
    	}
    	return hit;
    }
    PartitionScore(axis, point) {
    	let left = 0, right = 0;
    	for (const _ in this.Shapes) {
    	    const shape = this.Shapes[_];
    		const box = shape.BoundingBox();
    		const p = box.Partition(axis, point);
    		if (p.left) {
    			left++;
    		}
    		if (p.right) {
    			right++;
    		}
    	}
    	if (left >= right) {
    		return left;
    	} else {
    		return right;
    	}
    }
    Partition(size, axis, point) {
    	const left = [], right = [];
    	for (const _ in this.Shapes) {
    	    const shape = this.Shapes[_];
    		const box = shape.BoundingBox();
    		const p = box.Partition(axis, point);
    		if (p.left) {
    			left.push(shape);
    		}
    		if (p.right) {
    			right.push(shape);
    		}
    	}
    	return { left, right };
    }
    Split(depth) {
    	if (this.Shapes.length < 8) {
    		return;
    	}
    	const xs = [], ys = [], zs = [];
    	for (const _ in this.Shapes) {
    	    const shape = this.Shapes[_];
    		const box = shape.BoundingBox();
    		xs.push(box.Min.X);
    		xs.push(box.Max.X);
    		ys.push(box.Min.Y);
    		ys.push(box.Max.Y);
    		zs.push(box.Min.Z);
    		zs.push(box.Max.Z);
    	}
    	xs.sort();
    	ys.sort();
    	zs.sort();
    	const mx = Median(xs), my = Median(ys), mz = Median(zs);
    	let best = Math.floor(this.Shapes.length * 0.85);
    	let bestAxis = AxisNone;
    	let bestPoint = 0.0;
    	const sx = this.PartitionScore(AxisX, mx);
    	if (sx < best) {
    		best = sx;
    		bestAxis = AxisX;
    		bestPoint = mx;
    	}
    	const sy = this.PartitionScore(AxisY, my);
    	if (sy < best) {
    		best = sy;
    		bestAxis = AxisY;
    		bestPoint = my;
    	}
    	const sz = this.PartitionScore(AxisZ, mz);
    	if (sz < best) {
    		best = sz;
    		bestAxis = AxisZ;
    		bestPoint = mz;
    	}
    	if (bestAxis == AxisNone ){
    		return;
    	}
    	const p = this.Partition(best, bestAxis, bestPoint)
    	this.Axis = bestAxis;
    	this.Point = bestPoint;
    	this.Left = new Node(p.left);
    	this.Right = new Node(p.right);
    	this.Left.Split(depth + 1);
    	this.Right.Split(depth + 1);
    	this.Shapes = false; // only needed at leaf nodes
    }
}
//  
// https://github.com/fogleman/ln/blob/master/ln/shape.go
//
class TransformedShape{
    constructor(shape, matrix) {
        this.Shape = shape;
        this.Matrix = matrix;
        this.Inverse = matrix.Inverse();
    }
    Compile() {
        this.Shape.Compile();
    }
    BoundingBox() {
    	return this.Matrix.MulBox(this.Shape.BoundingBox());
    }
    Contains(v, f) {
    	return this.Shape.Contains(this.Inverse.MulPosition(v), f);
    }
    Intersect(r ) {
    	return this.Shape.Intersect(this.Inverse.MulRay(r));
    }
    Paths() {
    	return this.Shape.Paths().Transform(this.Matrix);
    }
}
//  
// https://github.com/fogleman/ln/blob/master/ln/scene.go
//
class Scene {
    constructor() {
        this.Shapes = [];
        this.Tree = false;
    }
    Compile() {
    	for (const _ in this.Shapes) {
    	    const shape = this.Shapes[_];
    		shape.Compile()
    	}
    	if (!this.Tree) {
    		this.Tree = new Tree(this.Shapes);
    	}
    }
    Add(shapes) {
    	if(Array.isArray(shapes)) {
    	    this.Shapes = [...this.Shapes, ...shapes];
    	} else {
    	    this.Shapes.push(shapes);
    	}
    }
    Intersect(r) {
    	return this.Tree.Intersect(r);
    }
    Visible(eye, point) {
    	const v = eye.Sub(point);
    	const r = new Ray(point, v.Normalize());
    	const hit = this.Intersect(r);
    	return (hit.T >= v.Length());
    }
    Paths(p = -1) {
        if (p >= 0) {
            if (p < this.Shapes.length) {
                return this.Shapes[p].Paths();
            } else {
                return false;
            }
        } else {
        	let result = new Paths();
        	for (const _ in this.Shapes) {
        	    const shape = this.Shapes[_];
        		result.Append(shape.Paths().v);
        	}
        	return result;
        }
    }
    Render(eye, center, up, width, height, fovy, near, far, step, p = -1) {
    	const aspect = width / height;
    	let matrix = LookAt(eye, center, up);
    	matrix = matrix.Perspective(fovy, aspect, near, far);
    	return this.RenderWithMatrix(matrix, eye, width, height, step, p);
    }
    RenderWithMatrix(matrix, eye, width, height, step, p = -1) {
        if (p <= 0) {
        	this.Compile();
        }
    	let paths = this.Paths(p);
    	if (!paths) {
    	    return false;
    	}
    	if (step > 0) {
    		paths = paths.Chop(step);
    	}
    	paths = paths.Filter(new ClipFilter(matrix, eye, this));
    	if (step > 0) {
    		paths = paths.Simplify(1e-6);
    	}
    	matrix = Scale( new Vector(-width / 2, -height / 2, 0));
    	paths = paths.Transform(matrix);
    	return paths;
    }
}
//
// https://github.com/fogleman/ln/blob/master/ln/ray.go
//
class Ray {
    constructor(origin, direction) {
	    this.Origin = origin;
	    this.Direction = direction;
    }
    Position(t) {
	    return this.Origin.Add(this.Direction.MulScalar(t));
    }
}
//
// https://github.com/fogleman/ln/blob/master/ln/path.go
//
class Path {
    constructor(v = []) {
        this.v = v;
    }
    Append(v) {
        if (Array.isArray(v)) {
            this.v = this.v.concat(v);
        } else {
            this.v.push(v);
        }
    }
    BoundingBox() {
    	let box = new Box(this.v[0], this.v[0]);
    	for (const _ in this.v) {
    	    const v = this.v[_];
    		box = box.Extend(new Box(v, v));
    	}
    	return box
    }
    Transform(matrix) {
    	const result = new Path();
    	for (const _ in this.v) {
    	    const v = this.v[_];
    		result.Append(matrix.MulPosition(v));
    	}
    	return result;
    }
    Chop(step) {
    	const result = new Path();
    	for (let i = 0; i < this.v.length-1; i++) {
    		const a = this.v[i];
    		const b = this.v[i+1];
    		const v = b.Sub(a);
    		const l = v.Length();
    		if (i == 0) {
    			result.Append(a);
    		}
    		for (let d = step;d < l; d += step) {
    			result.Append(a.Add(v.MulScalar(d/l)));
    		}
    		result.Append(b);
    	}
    	return result;
    }
    Filter(f) {
    	const result = new Paths();
    	let path = new Path();
    	for (const _ in this.v) {
    	    const v = this.v[_];
    		const fr = f.Filter(v);
    		if (fr.ok || (DRAW_HIDDEN_LINES && _%8 < 4)) { // show hidden lines
    			path.Append(fr.w);
    		} else {
    			if (path && path.v.length > 1) {
    				result.Append(path);
    			}
    			path = new Path();
    		}
    	}
    	if (path && path.v.length > 1) {
    		result.Append(path);
    	}
    	return result;
    }
    Simplify(threshold) {
    	if (this.v.length < 3) {
    		return this;
    	}
    	const a = this.v[0];
    	const b = this.v[this.v.length-1];
    	let index = -1;
    	let distance = 0.0;
    	for (let i = 1; i < this.v.length-1; i++) {
    		const d = this.v[i].SegmentDistance(a, b);
    		if (d > distance) {
    			index = i;
    			distance = d;
    		}
    	}
    	if (distance > threshold) {
    		const r1 = new Path(this.v.slice(0, index+1)).Simplify(threshold);
    		const r2 = new Path(this.v.slice(index)).Simplify(threshold);
    		return new Path([...r1.v.slice(0, r1.v.length-1), ...r2.v]);
    	} else {
    		return new Path([a, b]);
    	}
    }
}

class Paths {
    constructor(v = []) {
        this.v = v;
    }
    Append(v) {
        if (Array.isArray(v)) {
            this.v = this.v.concat(v);   
        } else {
            this.v.push(v);
        }
    }
    BoundingBox() {
    	let box = this.v[0].BoundingBox();
    	for (const _ in this.v) {
    	    const path = this.v[_];
    		box = box.Extend(path.BoundingBox());
    	}
    	return box;
    }
    Transform(matrix) {
    	const result = new Paths();
    	for (const _ in this.v) {
    	    const path = this.v[_];
    		result.Append(path.Transform(matrix));
    	}
    	return result;
    }
    Chop(step) {
    	const result = new Paths();
    	for (const _ in this.v) {
    	    const path = this.v[_];
    		result.Append(path.Chop(step));
    	}
    	return result;
    }
    Filter(f) {
    	const result = new Paths();
    	for (const _ in this.v) {
    	    const path = this.v[_];
    		result.Append(path.Filter(f).v);
    	}
    	return result;
    }
    Simplify(threshold) {
    	const result = new Paths();
    	for (const _ in this.v) {
    	    const path = this.v[_];
    		result.Append(path.Simplify(threshold));
    	}
    	return result;
    }
}
//
// https://github.com/fogleman/ln/blob/master/ln/box.go
//
class Box {
    constructor(min, max) {
    	this.Min = min;
    	this.Max = max;
    }
    Anchor(anchor) { return this.Min.Add(this.Size().Mul(anchor)); }
    Center() { return this.Anchor(new Vector(0.5, 0.5, 0.5)); }
    Size() { return this.Max.Sub(a.Min); }
    Contains(b) {
    	return this.Min.X <= b.X && this.Max.X >= b.X &&
    		this.Min.Y <= b.Y && this.Max.Y >= b.Y &&
    		this.Min.Z <= b.Z && this.Max.Z >= b.Z;
    }
    Extend(b) { return new Box(this.Min.Min(b.Min), this.Max.Max(b.Max)); }
    Intersect(r) {  	
        let x1 = (this.Min.X - r.Origin.X) / r.Direction.X;
    	let y1 = (this.Min.Y - r.Origin.Y) / r.Direction.Y;
    	let z1 = (this.Min.Z - r.Origin.Z) / r.Direction.Z;
    	let x2 = (this.Max.X - r.Origin.X) / r.Direction.X;
    	let y2 = (this.Max.Y - r.Origin.Y) / r.Direction.Y;
    	let z2 = (this.Max.Z - r.Origin.Z) / r.Direction.Z;
    	if (x1 > x2) {
    		const tmp = x1; x1 = x2, x2 = tmp;
    	}
    	if (y1 > y2) {
    		const tmp = y1; y1 = y2, y2 = tmp;
    	}
    	if (z1 > z2) {
    		const tmp = z1; z1 = z2, z2 = tmp;
    	}
    	const t1 = Math.max(Math.max(x1, y1), z1);
    	const t2 = Math.min(Math.min(x2, y2), z2);
    	return {t1, t2};
    }
    Partition(axis, point) {
        let left, right;
    	switch (axis) {
        	case AxisX:
        		left = this.Min.X <= point;
        		right = this.Max.X >= point;
        	case AxisY:
        		left = this.Min.Y <= point;
        		right = this.Max.Y >= point;
        	case AxisZ:
        		left = this.Min.Z <= point;
        		right = this.Max.Z >= point;
    	}
    	return {left, right};
    }
}
function BoxForShapes(shapes) {
	if (shapes.length == 0) {
		return new Box();
	}
	let box = shapes[0].BoundingBox();
	for (const _ in shapes) {
	    const shape = shapes[_];
		box = box.Extend(shape.BoundingBox());
	}
	return box;
}
function BoxForTriangles(shapes) {
	if (shapes.length == 0) {
		return new Box();
	}
	let box = shapes[0].BoundingBox();
	for (const _ in shapes) {
	    const shape = shapes[_];
		box = box.Extend(shape.BoundingBox());
	}
	return box;
}
function BoxForVectors(vectors) {
	if (vectors.length == 0) {
		return new Box();
	}
	let min = vectors[0]
	let max = vectors[0]
	for (const _ in vectors) {
	    const v = vectors[_];
		min = min.Min(v);
		max = max.Max(v);
	}
	return new Box(min, max);
}
//
// https://github.com/fogleman/ln/blob/master/ln/box.go
//
class Hit {
    constructor(shape, T) {
        this.Shape = shape;
        this.T = T;
    }
    Ok() {
    	return this.T < INF;
    }
    Min(b) {
    	if (this.T <= b.T) {
    		return this;
    	}
    	return b;
    }
    Max(b) {
    	if (this.T > b.T) {
    		return a;
    	}
    	return b;
    }
}
const NoHit = new Hit(false, INF);
//
// https://github.com/fogleman/ln/blob/master/ln/filter.go
//
const ClipBox = new Box(new Vector(-1, -1, -1), new Vector(1, 1, 1));
class ClipFilter {
    constructor(m, eye, scene) {
        this.Matrix = m;
        this.Eye = eye;
        this.Scene = scene;
    }
    Filter(v) {
    	const w = this.Matrix.MulPositionW(v);
    	if (!this.Scene.Visible(this.Eye, v)) {
    		return {w, ok: false};
    	}
    	if (!ClipBox.Contains(w)) {
    		return {w, ok: false};
    	}
    	return {w, ok: true};
    }
}
//  
// https://github.com/fogleman/ln/blob/master/ln/triangle.go
//
class Triangle {
    constructor(v1, v2, v3) {
    	this.V1 = v1; this.V2 = v2; this.V3 = v3;
    	this.UpdateBoundingBox();
    }
    UpdateBoundingBox() {
    	const min = this.V1.Min(this.V2).Min(this.V3);
    	const max = this.V1.Max(this.V2).Max(this.V3);
    	this.Box = new Box(min, max);
    }
    Compile() {}
    BoundingBox() { return this.Box; }
    Contains(v, f) { return false; }
    Intersect(r) {
    	const e1 = this.V2.Sub(this.V1);
    	const e2 = this.V3.Sub(this.V1);
    	const p =r.Direction.Cross(e2);
    	const det = e1.Dot(p);
    	if (det > -EPS && det < EPS) {
    		return NoHit;
    	}
    	const inv = 1 / det;
    	const t = r.Origin.Sub(this.V1);
    	const u = t.Dot(p) * inv;
    	if (u < 0 || u > 1) {
    		return NoHit;
    	}
    	const q = t.Cross(e1);
    	const v = r.Direction.Dot(q) * inv;
    	if (v < 0 || u+v > 1) {
    		return NoHit;
    	}
    	const d = e2.Dot(q) * inv;
    	if (d < EPS) {
    		return NoHit;
    	}
    	return new Hit(t, d);
    }
    Paths() { return new Paths([
        new Path([this.V1, this.V2]), 
        new Path([this.V2, this.V3]), 
        new Path([this.V3, this.V1])]);
    }
}
//  
// https://github.com/fogleman/ln/blob/master/ln/cube.go
//
class Cube {
    constructor(min, max) {
        this.Min = min;
        this.Max = max;
        this.Box = new Box(min, max);
    }
    Compile() {}
    BoundingBox() {
    	return this.Box;
    }
    Contains(v, f) {
    	if (v.X < this.Min.X-f || v.X > this.Max.X+f) {
    		return false;
    	}
    	if (v.Y < this.Min.Y-f || v.Y > this.Max.Y+f) {
    		return false;
    	}
    	if (v.Z < this.Min.Z-f || v.Z > this.Max.Z+f) {
    		return false;
    	}
    	return true;
    }
    Intersect(r) {
    	let n = this.Min.Sub(r.Origin).Div(r.Direction);
    	let f = this.Max.Sub(r.Origin).Div(r.Direction);
    	const v = n.Min(f); f = n.Max(f);
    	const t0 = Math.max(Math.max(v.X, v.Y), v.Z);
    	const t1 = Math.min(Math.min(f.X, f.Y), f.Z);
    	if (t0 < 1e-3 && t1 > 1e-3) {
    		return new Hit(this, t1);
    	}
    	if (t0 >= 1e-3 && t0 < t1) {
    		return new Hit(this, t0);
    	}
    	return NoHit;
    }
    Paths() {
    	const x1 = this.Min.X, y1 = this.Min.Y, z1 = this.Min.Z;
    	const x2 = this.Max.X, y2 = this.Max.Y, z2 = this.Max.Z;
    	const paths = new Paths([
    		new Path([new Vector(x1, y1, z1), new Vector(x1, y1, z2)]),
     		new Path([new Vector(x1, y1, z1), new Vector(x1, y2, z1)]),
     		new Path([new Vector(x1, y1, z1), new Vector(x2, y1, z1)]),
     		new Path([new Vector(x1, y1, z2), new Vector(x1, y2, z2)]),
     		new Path([new Vector(x1, y1, z2), new Vector(x2, y1, z2)]),
     		new Path([new Vector(x1, y2, z1), new Vector(x1, y2, z2)]),
     		new Path([new Vector(x1, y2, z1), new Vector(x2, y2, z1)]),
     		new Path([new Vector(x1, y2, z2), new Vector(x2, y2, z2)]),
     		new Path([new Vector(x2, y1, z1), new Vector(x2, y1, z2)]),
     		new Path([new Vector(x2, y1, z1), new Vector(x2, y2, z1)]),
     		new Path([new Vector(x2, y1, z2), new Vector(x2, y2, z2)]),
     		new Path([new Vector(x2, y2, z1), new Vector(x2, y2, z2)]),
    	]);
    	return paths;
    }
}
//
// https://github.com/fogleman/ln/blob/master/examples/csg.go
//
const Intersection = 0;
const Difference = 1;

class BooleanShape {
    constructor(op, A, B) {
        this.Op = op;
        this.A = arguments[1];
        this.B = arguments[2];
        for (let i=3; i<arguments.length; i++) {
		    this.A = new BooleanShape(op, this.A, arguments[i]);
	    }
    }
    Compile() {}
    BoundingBox() {
    	// TODO: fix this
    	const a = this.A.BoundingBox();
    	const b = this.B.BoundingBox();
    	return a.Extend(b);
    }
    Contains(v, f) {
    	f = 1e-3;
    	switch (this.Op) {
        	case Intersection:
        		return this.A.Contains(v, f) && this.B.Contains(v, f);
        	case Difference:
        		return this.A.Contains(v, f) && !this.B.Contains(v, -f);
    	}
    	return false;
    }
    Intersect(r) {
    	const h1 = this.A.Intersect(r);
    	const h2 = this.B.Intersect(r);
    	const h = h1.Min(h2);
    	const v = r.Position(h.T);
    	if (!h.Ok() || this.Contains(v, 0)) {
    		return h;
    	}
    	return this.Intersect(new Ray(r.Position(h.T+0.01), r.Direction));
    }
    Paths() {
    	let p = this.A.Paths();
    	p.Append(this.B.Paths().v);
    	return p.Chop(0.01).Filter(this);
    }
    Filter(w) {
    	return {w, ok: this.Contains(w, 0)};
    }
}
//
// https://github.com/fogleman/ln/blob/master/ln/cylinder.go
//
class Cylinder {
    constructor(radius, z0, z1) {
	    this.Radius = radius;
	    this.Z0 = z0;
	    this.Z1 = z1;
    }
    Compile() {}
    BoundingBox() {
    	const r = this.Radius;
    	return new Box(new Vector(-r, -r, this.Z0), new Vector(r, r, this.Z1));
    }
    Contains(v, f) {
    	const xy = new Vector(v.X, v.Y, 0);
    	if (xy.Length() > this.Radius+f) {
    		return false;
    	}
    	return v.Z >= this.Z0-f && v.Z <= this.Z1+f;
    }
    Intersect(ray)  {
    	const r = this.Radius;
    	const o = ray.Origin;
    	const d = ray.Direction;
    	const a = d.X*d.X + d.Y*d.Y;
    	const b = 2*o.X*d.X + 2*o.Y*d.Y;
    	const c = o.X*o.X + o.Y*o.Y - r*r;
    	const q = b*b - 4*a*c;
    	if (q < 0) {
    		return NoHit;
    	}
    	const s = Math.sqrt(q);
    	let t0 = (-b + s) / (2 * a);
    	let t1 = (-b - s) / (2 * a);
    	if (t0 > t1) {
    	    const tmp = t0;
    	    t0 = t1; t1 = tmp;
    	}
    	const z0 = o.Z + t0*d.Z;
    	const z1 = o.Z + t1*d.Z;
    	if (t0 > 1e-6 && this.Z0 < z0 && z0 < this.Z1) {
    		return new Hit(this, t0);
    	}
    	if (t1 > 1e-6 && this.Z0 < z1 && z1 < this.Z1) {
    		return new Hit(this, t1);
    	}
    	return NoHit;
    
    }
    Paths()  {
    	const result = new Paths();
    	for (let a = 0; a < 360; a += 10) {
    		const x = this.Radius * Math.cos(Radians(a));
    		const y = this.Radius * Math.sin(Radians(a));
    		result.Append(new Path([new Vector(x, y, this.Z0), new Vector(x, y, this.Z1)]));
    	}
    	return result;
    }
}

class OutlineCylinder extends Cylinder {
    constructor(eye, up, radius, z0, z1) {
        super(radius, z0, z1);
        this.Eye = eye;
        this.Up = up;
    }
    Paths() {
    	let center = new Vector(0, 0, this.Z0);
    	let hyp = center.Sub(this.Eye).Length();
    	let opp = this.Radius;
    	let theta = Math.asin(opp / hyp);
    	let adj = opp / Math.tan(theta);
    	let d = Math.cos(theta) * adj;
    	// r := math.Sin(theta) * adj
    	let w = center.Sub(this.Eye).Normalize();
    	let u = w.Cross(this.Up).Normalize();
    	const c0 = this.Eye.Add(w.MulScalar(d));
    	const a0 = c0.Add(u.MulScalar(this.Radius * 1.01));
    	const b0 = c0.Add(u.MulScalar(-this.Radius * 1.01));
    
    	center = new Vector(0, 0, this.Z1);
    	hyp = center.Sub(this.Eye).Length();
    	opp = this.Radius;
    	theta = Math.asin(opp / hyp);
    	adj = opp / Math.tan(theta);
    	d = Math.cos(theta) * adj;
    	// r = math.Sin(theta) * adj
    	w = center.Sub(this.Eye).Normalize();
    	u = w.Cross(this.Up).Normalize();
    	const c1 = this.Eye.Add(w.MulScalar(d));
    	const a1 = c1.Add(u.MulScalar(this.Radius * 1.01));
    	const b1 = c1.Add(u.MulScalar(-this.Radius * 1.01));
    
    	const p0 = new Path(), p1 = new Path();
    	for (let a = 0; a < 360; a++) {
    		const x = this.Radius * Math.cos(Radians(a));
    		const y = this.Radius * Math.sin(Radians(a));
    		p0.Append(new Vector(x, y, this.Z0));
    		p1.Append(new Vector(x, y, this.Z1));
    	}
    	return new Paths([
    		p0,
    		p1,
    		new Path([new Vector(a0.X, a0.Y, this.Z0), new Vector(a1.X, a1.Y, this.Z1)]),
    		new Path([new Vector(b0.X, b0.Y, this.Z0), new Vector(b1.X, b1.Y, this.Z1)]),
    		]);
    }
}

function NewTransformedOutlineCylinder(eye, up, v0, v1, radius) {
	const d = v1.Sub(v0);
	const z = d.Length();
	const a = Math.acos(d.Normalize().Dot(up));
	let m = Translate(v0);
	if (a != 0) {
		const u = d.Cross(up).Normalize();
		m = Rotate(u, a).Translate(v0);
	}
	const c = new OutlineCylinder(m.Inverse().MulPosition(eye), up, radius, 0, z);
	return new TransformedShape(c, m);
}
//
// https://github.com/fogleman/ln/blob/master/ln/sphere.go
//
class Sphere {
    constructor(center, radius) {
        this.Center = center;
        this.Radius = radius;
        
        const min = new Vector(center.X - radius, center.Y - radius, center.Z - radius);
    	const max = new Vector(center.X + radius, center.Y + radius, center.Z + radius);
    	this.Box = new Box(min, max);
    }
    Compile() {}
    BoundingBox() {
        return this.Box;
    }
    Contains(v, f) {
    	return v.Sub(this.Center).Length() <= this.Radius+f;
    }
    Intersect(r) {
    	const radius = this.Radius;
    	const to = r.Origin.Sub(this.Center);
    	const b = to.Dot(r.Direction);
    	const c = to.Dot(to) - radius*radius;
    	let d = b*b - c;
    	if (d > 0) {
    		d = Math.sqrt(d);
    		const t1 = -b - d;
    		if (t1 > 1e-2) {
    			return new Hit(this, t1);
    		}
    		const t2 = -b + d;
    		if (t2 > 1e-2) {
    			return new Hit(this, t2);
    		}
    	}
    	return NoHit
    }
    Paths3() {
    	const paths = new Paths();
    	for (let i = 0; i < 20000; i++) {
    		let v = RandomUnitVector();
    		v = v.MulScalar(this.Radius).Add(this.Center);
    		paths.Append(new Path([v, v]));
    	}
    	return paths;
    }
    Paths2() {
    	var equator = new Path();
    	for (let lng = 0; lng <= 360; lng++) {
    		const v = LatLngToXYZ(0, lng, this.Radius);
    		equator.Append(v);
    	}
    	var paths = new Paths();
    	for (let i = 0; i < 100; i++) {
    		const m = Identity();
    		for (let j = 0; j < 3; j++) {
    			const v = RandomUnitVector();
    			m = m.Rotate(v, Math.random()*2*Math.PI);
    		}
    		m = m.Translate(this.Center);
    		paths.Append(equator.Transform(m));
    	}
    	return paths;
    }
    Paths() {
    	const paths = new Paths();
    	const n = 5;
    	const o = 5;
    	for (let lat = -90 + o; lat <= 90-o; lat += n) {
    		const path = new Path();
    		for (let lng = 0; lng <= 360; lng++) {
    			const v = LatLngToXYZ(lat, lng, this.Radius).Add(this.Center);
    			path.Append(v);
    		}
    		paths.Append(path);
    	}
    	for (let lng = 0; lng < 360; lng += n) {
    		const path = new Path();
    		for (let lat = -90 + o; lat <= 90-o; lat++) {
    			const v = LatLngToXYZ(lat, lng, this.Radius).Add(this.Center);
    			path.Append(v);
    		}
    		paths.Append(path);
    	}
    	return paths;
    }

}

function LatLngToXYZ(lat, lng, radius) {
	const latr = Radians(lat);
	const lngr = Radians(lng);
	return new Vector(  radius * Math.cos(latr) * Math.cos(lngr),
	                    radius * Math.cos(latr) * Math.sin(lngr),
	                    radius * Math.sin(latr) );
}

class OutlineSphere extends Sphere {
    constructor(eye, up, center, radius) {
        super(center, radius);
        this.Eye = eye;
	    this.Up = up;
    }
    Paths() {
    	const path = new Path();
    	const center = this.Center;
    	const radius = this.Radius;
    
    	const hyp = center.Sub(this.Eye).Length();
    	const opp = radius;
    	const theta = Math.asin(opp / hyp);
    	const adj = opp / Math.tan(theta);
    	const d = Math.cos(theta) * adj;
    	const r = Math.sin(theta) * adj;
    
    	const w = center.Sub(this.Eye).Normalize()
    	const u = w.Cross(this.Up).Normalize()
    	const v = w.Cross(u).Normalize()
    	const c = this.Eye.Add(w.MulScalar(d))
    	for (let i = 0; i <= 360; i++) {
    		const a = Radians(i);
    		let p = c;
    		p = p.Add(u.MulScalar(Math.cos(a) * r));
    		p = p.Add(v.MulScalar(Math.sin(a) * r));
    		path.Append(p);
    	}
    	return new Paths([path]);
    }
}
//
// https://github.com/fogleman/ln/blob/master/ln/function.go
//
const Above = 0;
const Below = 1;

class FunctionShape {
    constructor(func, box, direction) {
	    this.Func = func;
	    this.Box = box;
	    this.Direction = direction;
    }
    Compile() {}
    BoundingBox() { return this.Box; }
    Contains(v, eps) {
    	if (this.Direction == Below) {
    		return v.Z < this.Func(v.X, v.Y);
    	} else {
    		return v.Z > this.Func(v.X, v.Y);
    	}
    }
    Intersect(ray) {
    	const step = 1.0 / 64;
    	const sign = this.Contains(ray.Position(step), 0);
    	for (let t = step; t < 10; t += step) {
    		const v = ray.Position(t);
    		if (this.Contains(v, 0) != sign && this.Box.Contains(v)) {
    			return new Hit(this, t);
    		}
    	}
    	return NoHit
    }
    Paths3() {
    	const path = new Path();
    	const n = 10000
    	for (let i = 0; i < n; i++) {
    		const t = i / n;
    		const r = 8 - Math.pow(t, 0.1)*8;
    		const x = Math.cos(Radians(t*2*Math.PI*3000)) * r;
    		const y = Math.sin(Radians(t*2*Math.PI*3000)) * r;
    		let z = this.Func(x, y);
    		z = Math.min(z, this.Box.Max.Z);
    		z = Math.max(z, this.Box.Min.Z);
    		path.Append(new Vector(x, y, z));
    	}
    	// return append(f.Paths2(), path)
    	return new Paths([path]);
    }
    Paths() {
    	const paths = new Paths();
    	const fine = 1.0 / 256;
    	for (let a = 0; a < 360; a += 5) {
    		const path = new Path();
    		for (let r = 0.0; r <= 8.0; r += fine) {
    			let x = Math.cos(Radians(a)) * r;
    			let y = Math.sin(Radians(a)) * r;
    			let z = this.Func(x, y);
    			const o = -Math.pow(-z, 1.4);
    			x = Math.cos(Radians(a)-o) * r;
    			y = Math.sin(Radians(a)-o) * r;
    			z = Math.min(z, this.Box.Max.Z);
    			z = Math.max(z, this.Box.Min.Z);
    			path.Append(new Vector(x, y, z));
    		}
    		paths.Append(path);
    	}
    	return paths;
    }
    Paths1() {
    	const paths = new Paths();
    	const step = 1.0 / 8;
    	const fine = 1.0 / 64;
    	for (let x = this.Box.Min.X; x <= this.Box.Max.X; x += step) {
    		const path = new Path();
    		for (let y = this.Box.Min.Y; y <= this.Box.Max.Y; y += fine) {
    			let z = this.Func(x, y);
    			z = Math.min(z, this.Box.Max.Z);
    			z = Math.max(z, this.Box.Min.Z);
    			path.Append(new Vector(x, y, z));
    		}
    		paths.Append(path);
    	}
    	for (let y = this.Box.Min.Y; y <= this.Box.Max.Y; y += step) {
    		const path = new Path();
    		for (let x = this.Box.Min.X; x <= this.Box.Max.X; x += fine) {
    			let z = this.Func(x, y);
    			z = Math.min(z, this.Box.Max.Z);
    			z = Math.max(z, this.Box.Min.Z);
    			path.Append(new Vector(x, y, z));
    		}
    		paths.Append([path]);
    	}
    	return paths
    }
}