Hidden Lines Removal

Geometric hidden line elimination still lives!

Log in to post a comment.

// You can find the Turtle API reference here: https://turtletoy.net/syntax
Canvas.setpenopacity(1);

// Global code will be evaluated once.
const turtle = new Turtle();

// SVG 3D module
const svg3d = SVG3D();

// Vectors

const v2 = {
	length(a, b) {
		const dx = b[0] - a[0];
		const dy = b[1] - a[1];
		return Math.sqrt(dx * dx + dy * dy);
	}
};

const v3 = {
	addScalar(a, s) {
		return [
		    a[0] + s, 
		    a[1] + s, 
		    a[2] + s
		];
	},
	scale(a, s) {
		return [
		    a[0] * s, 
		    a[1] * s, 
	    	a[2] * s
	    ];
	},
	sub(a, b) {
		return [
		    a[0] - b[0], 
		    a[1] - b[1], 
		    a[2] - b[2]
		];
	},
	add(a, b) {
		return [
		    a[0] + b[0], 
		    a[1] + b[1], 
		    a[2] + b[2]
		];
	},
	multiplyMatrix(v, m) {
		const x = v[0],
			y = v[1],
			z = v[2];
		return [
			x * m[0] + y * m[4] + z * m[8] + m[12],
			x * m[1] + y * m[5] + z * m[9] + m[13],
			x * m[2] + y * m[6] + z * m[10] + m[14]
		];
	},
	cross(a, b) {
		return [
			a[1] * b[2] - a[2] * b[1],
			a[2] * b[0] - a[0] * b[2],
			a[0] * b[1] - a[1] * b[0]
		];
	},
	dot(a, b) {
		return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
	},
	length(a, b) {
		const dx = b[0] - a[0];
		const dy = b[1] - a[1];
		const dz = b[2] - a[2];
		return Math.sqrt(dx * dx + dy * dy + dz * dz);
	}
};

// Matrix 4x4

const m4 = {
	identity() {
		return [
		    1, 0, 0, 0, 
		    0, 1, 0, 0, 
		    0, 0, 1, 0, 
		    0, 0, 0, 1
		];
	},
	multiply(m, n) {
		const 
		    m1XX = m[0],  m1XY = m[1],  m1XZ = m[2],  m1XW = m[3],
			m1YX = m[4],  m1YY = m[5],  m1YZ = m[6],  m1YW = m[7],
			m1ZX = m[8],  m1ZY = m[9],  m1ZZ = m[10], m1ZW = m[11],
			m1WX = m[12], m1WY = m[13], m1WZ = m[14], m1WW = m[15],
			m2XX = n[0],  m2XY = n[1],  m2XZ = n[2],  m2XW = n[3],
			m2YX = n[4],  m2YY = n[5],  m2YZ = n[6],  m2YW = n[7],
			m2ZX = n[8],  m2ZY = n[9],  m2ZZ = n[10], m2ZW = n[11],
			m2WX = n[12], m2WY = n[13], m2WZ = n[14], m2WW = n[15];
		return [
			m1XX * m2XX + m1XY * m2YX + m1XZ * m2ZX + m1XW * m2WX,
			m1XX * m2XY + m1XY * m2YY + m1XZ * m2ZY + m1XW * m2WY,
			m1XX * m2XZ + m1XY * m2YZ + m1XZ * m2ZZ + m1XW * m2WZ,
			m1XX * m2XW + m1XY * m2YW + m1XZ * m2ZW + m1XW * m2WW,
			m1YX * m2XX + m1YY * m2YX + m1YZ * m2ZX + m1YW * m2WX,
			m1YX * m2XY + m1YY * m2YY + m1YZ * m2ZY + m1YW * m2WY,
			m1YX * m2XZ + m1YY * m2YZ + m1YZ * m2ZZ + m1YW * m2WZ,
			m1YX * m2XW + m1YY * m2YW + m1YZ * m2ZW + m1YW * m2WW,
			m1ZX * m2XX + m1ZY * m2YX + m1ZZ * m2ZX + m1ZW * m2WX,
			m1ZX * m2XY + m1ZY * m2YY + m1ZZ * m2ZY + m1ZW * m2WY,
			m1ZX * m2XZ + m1ZY * m2YZ + m1ZZ * m2ZZ + m1ZW * m2WZ,
			m1ZX * m2XW + m1ZY * m2YW + m1ZZ * m2ZW + m1ZW * m2WW,
			m1WX * m2XX + m1WY * m2YX + m1WZ * m2ZX + m1WW * m2WX,
			m1WX * m2XY + m1WY * m2YY + m1WZ * m2ZY + m1WW * m2WY,
			m1WX * m2XZ + m1WY * m2YZ + m1WZ * m2ZZ + m1WW * m2WZ,
			m1WX * m2XW + m1WY * m2YW + m1WZ * m2ZW + m1WW * m2WW
		];
	},
	scale(m, x, y, z) {
		return this.multiply(m, [
		    x, 0, 0, 0, 
		    0, y, 0, 0, 
		    0, 0, z, 0, 
		    0, 0, 0, 1
		]);
	},
	translate(m, x, y, z) {
		return this.multiply(m, [
		    1, 0, 0, 0, 
		    0, 1, 0, 0, 
		    0, 0, 1, 0, 
		    x, y, z, 1
		]);
	},
	rotateX(m, angle) {
		const c = Math.cos(angle),
			s = Math.sin(angle);
		return this.multiply(m, [
		    1, 0, 0, 0, 
		    0, c, s, 0, 
		    0, -s, c, 0, 
		    0, 0, 0, 1
		]);
	},
	rotateY(m, angle) {
		const c = Math.cos(angle),
			s = Math.sin(angle);
		return this.multiply(m, [
		    c, 0, -s, 0, 
		    0, 1, 0, 0, 
		    s, 0, c, 0, 
		    0, 0, 0, 1
		]);
	},
	rotateZ(m, angle) {
		const c = Math.cos(angle),
			s = Math.sin(angle);
		return this.multiply(m, [
		    c, -s, 0, 0, 
		    s, c, 0, 0, 
		    0, 0, 1, 0,
		    0, 0, 0, 1
		]);
	}
};



/**
 * simplified port from https://github.com/mapbox/earcut
 
 	Usage
	var triangles = Earcut.triangulate([10,0, 0,50, 60,60, 70,10]); // returns [1,0,3, 3,2,1]
	Signature: earcut(vertices[, holes, dimensions = 2]).

	Vertices is a flat array of vertex coordinates like [x0,y0, x1,y1, x2,y2, ...].
	holes is an array of hole indices if any (e.g. [5, 8] for a 12-vertex input would mean one hole with vertices 5–7 and another with 8–11).
	dimensions is the number of coordinates per vertex in the input array (2 by default).
	Each group of three vertex indices in the resulting array forms a triangle.

	Triangulating a polygon with a hole
	var triangles = Earcut.triangulate([0,0, 100,0, 100,100, 0,100,  20,20, 80,20, 80,80, 20,80], [4]);
	// [3,0,4, 5,4,0, 3,4,7, 5,0,1, 2,3,7, 6,5,1, 2,7,6, 6,1,2]
	
 */
 
 function Earcut () {

    // create a circular doubly linked list from polygon points in the specified winding order
    function linkedList(data, start, end, dim, clockwise) {
    	let i, last;
    	if (clockwise === signedArea(data, start, end, dim) > 0) {
    		for (i = start; i < end; i += dim)
    			last = insertNode(i, data[i], data[i + 1], last);
    	} else {
    		for (i = end - dim; i >= start; i -= dim)
    			last = insertNode(i, data[i], data[i + 1], last);
    	}
    	if (last && equals(last, last.next)) {
    		removeNode(last);
    		last = last.next;
    	}
    	return last;
    }
    
    // eliminate colinear or duplicate points
    function filterPoints(start, end) {
    	if (!start) return start;
    	if (!end) end = start;
    	let p = start,
    		again;
    	do {
    		again = false;
    		if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
    			removeNode(p);
    			p = end = p.prev;
    			if (p === p.next) break;
    			again = true;
    		} else {
    			p = p.next;
    		}
    	} while (again || p !== end);
    	return end;
    }
    
    // main ear slicing loop which triangulates a polygon (given as a linked list)
    function earcutLinked(ear, triangles, dim) {
    	if (!ear) return;
    	let stop = ear,
    		prev,
    		next;
    	// iterate through ears, slicing them one by one
    	while (ear.prev !== ear.next) {
    		prev = ear.prev;
    		next = ear.next;
    		if (isEar(ear)) {
    			// cut off the triangle
    			triangles.push(prev.i / dim);
    			triangles.push(ear.i / dim);
    			triangles.push(next.i / dim);
    			removeNode(ear);
    			// skipping the next vertex leads to less sliver triangles
    			ear = next.next;
    			stop = next.next;
    			continue;
    		}
    		ear = next;
    	}
    }
    
    // check whether a polygon node forms a valid ear with adjacent nodes
    function isEar(ear) {
    	const a = ear.prev,
    		b = ear,
    		c = ear.next;
    	if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
    	// now make sure we don't have other points inside the potential ear
    	let p = ear.next.next;
    	while (p !== ear.prev) {
    		if (
    			pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) &&
    			area(p.prev, p, p.next) >= 0
    		)
    			return false;
    		p = p.next;
    	}
    	return true;
    }
    
    // link every hole into the outer loop, producing a single-ring polygon without holes
    function eliminateHoles(data, holeIndices, outerNode, dim) {
    	const queue = [];
    	let i, len, start, end, list;
    	for (i = 0, len = holeIndices.length; i < len; i++) {
    		start = holeIndices[i] * dim;
    		end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
    		list = linkedList(data, start, end, dim, false);
    		if (list === list.next) list.steiner = true;
    		queue.push(getLeftmost(list));
    	}
    	queue.sort(compareX);
    	// process holes from left to right
    	for (i = 0; i < queue.length; i++) {
    		eliminateHole(queue[i], outerNode);
    		outerNode = filterPoints(outerNode, outerNode.next);
    	}
    	return outerNode;
    }
    
    function compareX(a, b) {
    	return a.x - b.x;
    }
    
    // find a bridge between vertices that connects hole with an outer ring and and link it
    function eliminateHole(hole, outerNode) {
    	outerNode = findHoleBridge(hole, outerNode);
    	if (outerNode) {
    		const b = splitPolygon(outerNode, hole);
    		// filter collinear points around the cuts
    		filterPoints(outerNode, outerNode.next);
    		filterPoints(b, b.next);
    	}
    }
    
    // David Eberly's algorithm for finding a bridge between hole and outer polygon
    function findHoleBridge(hole, outerNode) {
    	let p = outerNode;
    	const hx = hole.x;
    	const hy = hole.y;
    	let qx = -Infinity,
    		m;
    	// find a segment intersected by a ray from the hole's leftmost point to the left;
    	// segment's endpoint with lesser x will be potential connection point
    	do {
    		if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
    			const x = p.x + ((hy - p.y) * (p.next.x - p.x)) / (p.next.y - p.y);
    			if (x <= hx && x > qx) {
    				qx = x;
    				if (x === hx) {
    					if (hy === p.y) return p;
    					if (hy === p.next.y) return p.next;
    				}
    				m = p.x < p.next.x ? p : p.next;
    			}
    		}
    		p = p.next;
    	} while (p !== outerNode);
    	if (!m) return null;
    	if (hx === qx) return m; // hole touches outer segment; pick leftmost endpoint
    	// look for points inside the triangle of hole point, segment intersection and endpoint;
    	// if there are no points found, we have a valid connection;
    	// otherwise choose the point of the minimum angle with the ray as connection point
    	const stop = m,
    		mx = m.x,
    		my = m.y;
    	let tanMin = Infinity,
    		tan;
    	p = m;
    	do {
    		if (
    			hx >= p.x &&
    			p.x >= mx &&
    			hx !== p.x &&
    			pointInTriangle(
    				hy < my ? hx : qx,
    				hy,
    				mx,
    				my,
    				hy < my ? qx : hx,
    				hy,
    				p.x,
    				p.y
    			)
    		) {
    			tan = Math.abs(hy - p.y) / (hx - p.x); // tangential
    			if (
    				locallyInside(p, hole) &&
    				(tan < tanMin ||
    					(tan === tanMin &&
    						(p.x > m.x || (p.x === m.x && sectorContainsSector(m, p)))))
    			) {
    				m = p;
    				tanMin = tan;
    			}
    		}
    		p = p.next;
    	} while (p !== stop);
    	return m;
    }
    
    // whether sector in vertex m contains sector in vertex p in the same coordinates
    function sectorContainsSector(m, p) {
    	return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
    }
    
    // find the leftmost node of a polygon ring
    function getLeftmost(start) {
    	let p = start,
    		leftmost = start;
    	do {
    		if (p.x < leftmost.x || (p.x === leftmost.x && p.y < leftmost.y))
    			leftmost = p;
    		p = p.next;
    	} while (p !== start);
    	return leftmost;
    }
    
    // check if a point lies within a convex triangle
    function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
    	return (
    		(cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
    		(ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
    		(bx - px) * (cy - py) - (cx - px) * (by - py) >= 0
    	);
    }
    
    // signed area of a triangle
    function area(p, q, r) {
    	return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
    }
    
    // check if two points are equal
    function equals(p1, p2) {
    	return p1.x === p2.x && p1.y === p2.y;
    }
    
    // check if a polygon diagonal is locally inside the polygon
    function locallyInside(a, b) {
    	return area(a.prev, a, a.next) < 0
    		? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0
    		: area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
    }
    
    // link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits polygon into two;
    // if one belongs to the outer ring and another to a hole, it merges it into a single ring
    function splitPolygon(a, b) {
    	const a2 = new Node(a.i, a.x, a.y),
    		b2 = new Node(b.i, b.x, b.y),
    		an = a.next,
    		bp = b.prev;
    	a.next = b;
    	b.prev = a;
    	a2.next = an;
    	an.prev = a2;
    	b2.next = a2;
    	a2.prev = b2;
    	bp.next = b2;
    	b2.prev = bp;
    	return b2;
    }
    
    // create a node and optionally link it with previous one (in a circular doubly linked list)
    function insertNode(i, x, y, last) {
    	const p = new Node(i, x, y);
    	if (!last) {
    		p.prev = p;
    		p.next = p;
    	} else {
    		p.next = last.next;
    		p.prev = last;
    		last.next.prev = p;
    		last.next = p;
    	}
    	return p;
    }
    
    function removeNode(p) {
    	p.next.prev = p.prev;
    	p.prev.next = p.next;
    	if (p.prevZ) p.prevZ.nextZ = p.nextZ;
    	if (p.nextZ) p.nextZ.prevZ = p.prevZ;
    }
    
    function Node(i, x, y) {
    	// vertex index in coordinates array
    	this.i = i;
    	// vertex coordinates
    	this.x = x;
    	this.y = y;
    	// previous and next vertex nodes in a polygon ring
    	this.prev = null;
    	this.next = null;
    	// z-order curve value
    	this.z = null;
    	// previous and next nodes in z-order
    	this.prevZ = null;
    	this.nextZ = null;
    	// indicates whether this is a steiner point
    	this.steiner = false;
    }
    
    function signedArea(data, start, end, dim) {
    	let sum = 0;
    	for (let i = start, j = end - dim; i < end; i += dim) {
    		sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
    		j = i;
    	}
    	return sum;
    }
    
    return {
        
        triangulate: function (data, holeIndices, dim = 2) {
        	const hasHoles = holeIndices && holeIndices.length;
        	const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
        	let outerNode = linkedList(data, 0, outerLen, dim, true);
        	const triangles = [];
        	if (!outerNode || outerNode.next === outerNode.prev) return triangles;
        	if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
        	earcutLinked(outerNode, triangles, dim);
        	return triangles;
        }
        
    }
};
    

// SVG 3D module

function SVG3D () {
    
    // triangulation (with holes)
    const earcut = Earcut();

    const Poly = class {
    	constructor(world, points) {
    		this.world = world;
    		this.triangles = [];
    		this.paths = [];
    		this.points = [];
    		this.points3d = [];
    		this.points2d = [];
    		this.holes = [];
    		this.m = m4.identity();
    		this.points.push(...points);
    	}
    	hole(points) {
    		this.holes.push(this.points.length / 2);
    		this.points.push(...points);
    		return this;
    	}
    	translate(x, y, z) {
    		this.m = m4.translate(this.m, x, y, z);
    		return this;
    	}
    	rotateX(a) {
    		this.m = m4.rotateX(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	rotateY(a) {
    		this.m = m4.rotateY(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	rotateZ(a) {
    		this.m = m4.rotateZ(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	pushPoints(start, end) {
    		const len = end - start + 1;
    		const points3d = this.points3d.slice(start, end + 1);
    		const points2d = this.points2d.slice(start, end + 1);
    		for (let j = 0; j < len; j++) {
    			this.paths.push([
    				points3d[j],
    				points3d[(j + 1) % len],
    				v3.length(points3d[j], points3d[(j + 1) % len])
    			]);
    		}
    	}
    	compute() {
    	    
    		// transform
    		this.points3d.length = 0;
    		for (let i = 0; i < this.points.length; i += 2) {
    			const m = m4.multiply(this.m, this.world.m);
    			const v = v3.multiplyMatrix([-50 + this.points[i], - 50 + this.points[i + 1], 0.0], m);
    			this.points3d.push(v);
    		}
    		
    		// project
    		this.points2d.length = 0;
    		const fov = this.world.fov;
    		for (const point of this.points3d) {
    			this.points2d.push([
    				point[0] * (fov / (point[2] + fov)),
    				point[1] * (fov / (point[2] + fov))
    			]);
    		}
    		
    		// triangles
    		this.triangles.length = 0;
    		const triangles = earcut.triangulate(this.points, this.holes, 2);
    		for (let i = 0; i < triangles.length; i += 3) {
    			this.triangles.push([
    				this.points3d[triangles[i]],
    				this.points3d[triangles[i + 1]],
    				this.points3d[triangles[i + 2]]
    			]);
    		}
    		
    		// paths (outer points and hole points)
    		const outerLen = this.holes.length > 0 ? this.holes[0] : this.points3d.length;
    		this.pushPoints(0, outerLen - 1);
    		for (let i = 0; i < this.holes.length; i++) {
    			const start = this.holes[i];
    			const end =
    				i < this.holes.length - 1
    					? this.holes[i + 1] - 1
    					: this.points3d.length - 1;
    			this.pushPoints(start, end);
    		}
    	}
    	
    	render() {
    		const fov = this.world.fov;
    		for (const line of this.paths) {
    			const p0 = line[0];
    			const p1 = line[1];
    			const len = Math.floor(line[2]) * 10;
    			const [dx, dy, dz] = v3.sub(p1, p0);
    			const sx = dx / len;
    			const sy = dy / len;
    			const sz = dz / len;
    			let [px, py, pz] = p0;
    			let pVisible = false;
    			let strokes = [];
    			const R1 = [0, 0, -this.world.fov];
    			for (let i = 0; i < len; i++) {
    				const R0 = [px, py, pz];
    				let visible = true;
    				for (const poly of this.world.polygons) {
    					if (this === poly) continue;
    					for (const triangle of poly.triangles) {
    						const intersec = intersectSegmentWithTriangle(
    							R0,
    							R1,
    							triangle[0],
    							triangle[1],
    							triangle[2]
    						);
    						if (intersec === true) {
    							visible = false;
    							break;
    						}
    					}
    					if (visible === false) break;
    				}
    				if (visible !== pVisible) {
    					strokes.push([
						    px * (fov / (pz + fov)), 
						    py * (fov / (pz + fov))
						]);
    				}
    				px += sx;
    				py += sy;
    				pz += sz;
    				pVisible = visible;
    			}
    			strokes.push([
    			    p1[0] * (fov / (p1[2] + fov)),
    			    p1[1] * (fov / (p1[2] + fov))
    			]);
    			
    			// drawing polyline
    			
    			for (let i = 0; i < strokes.length - 1; i += 2) {
    				const p0 = strokes[i];
    				const p1 = strokes[i + 1];
    				turtle.penup();
    				turtle.goto(p0[0], p0[1]);
    				turtle.pendown();
    				turtle.goto(p1[0], p1[1]);
    			}
    		}
    	}
    };
    
    const World = class {
    	constructor() {
    		this.polygons = [];
    		this.m = m4.identity();
    		this.fov = 350;
    	}
    	translate(x, y, z) {
    		this.m = m4.translate(this.m, x, y, z);
    		return this;
    	}
    	rotateX(a) {
    		this.m = m4.rotateX(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	rotateY(a) {
    		this.m = m4.rotateY(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	rotateZ(a) {
    		this.m = m4.rotateZ(this.m, (a * Math.PI) / 180);
    		return this;
    	}
    	render() {
    		for (const poly of this.polygons) {
    			poly.compute();
    		}
    		for (const poly of this.polygons) {
    			poly.render();
    		}
    	}
    	poly(points) {
    		const poly = new Poly(this, points);
    		this.polygons.push(poly);
    		return poly;
    	}
    	rect(x, y, w, h) {
    		return [
    		    x, 
    		    y, 
    		    x + w, 
    		    y, 
    		    x + w, 
    		    y + h, 
    		    x, 
    		    y + h
    	    ];
    	}
    };
    
    function intersectSegmentWithTriangle(R0, R1, S0, S1, S2) {
    	// Ref: http://geomalgorithms.com/a06-_intersect-2.html
    	// segment plane
    	const u = v3.sub(S1, S0);
    	const v = v3.sub(S2, S0);
    	const n = v3.cross(u, v);
    	const dir = v3.sub(R1, R0);
    	const w0 = v3.sub(R0, S0);
    	const a = -v3.dot(n, w0);
    	const b = v3.dot(n, dir);
    	if (Math.abs(b) < 0.00000001) return false;
    	const r = a / b;
    	if (r < 0.0) return false;
    	if (r > 1.0) return false;
    	// intersection point in i
    	const dr = v3.scale(dir, r);
    	const i = v3.add(R0, dr);
    	const w = v3.sub(i, S0);
    	const uu = v3.dot(u, u);
    	const uv = v3.dot(u, v);
    	const vv = v3.dot(v, v);
    	const wu = v3.dot(w, u);
    	const wv = v3.dot(w, v);
    	const d = uv * uv - uu * vv;
    	const s = (uv * wv - vv * wu) / d;
    	if (s < 0.0 || s > 1.0) return false;
    	const t = (uv * wu - uu * wv) / d;
    	if (t < 0.0 || s + t > 1.0) return false;
    	return true;
    }

    return {
        World: World
    }
};


console.clear();

const scene = () => {

	const world = new svg3d.World();
	const R = world.rect;
	
	const h1 = [10, 5, 15, 25,5, 25];// triangle 
	const h2 = R(20, 15, 40, 65);
	const h3 = R(70, 40, 20, 50);
	const h4 = R(70, 5, 20, 20);

	const wall1 = world.poly(R(0, 0, 100, 100)).hole(h1).hole(h2).hole(h3).hole(h4);
	const wall2 = world.poly(R(0, 0, 100, 100)).hole(h1).hole(h2).hole(h3).hole(h4).translate(12, 0, 0).rotateX(-90);

	world.rotateX(Math.random() * 360).rotateY(Math.random() * 360);

	world.render();
	
};

scene();