tess2
Version:
GLU tesselator ported to Javascript, performs polygon boolean operations and triangulation
523 lines (457 loc) • 13.5 kB
JavaScript
// Example based on poly2tri.js demo code.
;
var contours = [];
var bounds = [10000000,10000000,-10000000,-10000000];
var offset = [0, 0];
var scale = 1.0;
var tess = null;
var windingRule = Tess2.WINDING_ODD;
var polygonSize = 3;
var elementType = Tess2.POLYGONS;
function clearData() {
$(".info").css('visibility', 'hidden');
$("textarea").val("");
offset = [0,0];
scale = 1.0;
contours = [];
bounds = [10000000,10000000,-10000000,-10000000];
tess = null;
}
function parsePoints(str) {
var floats = str.split(/[^-eE\.\d]+/).filter(function(val) {
return val;
}).map(parseFloat);
return floats;
}
function polygonBounds(points, bounds) {
for (var i = 0; i < points.length; i += 2) {
var x = points[i+0];
var y = points[i+1];
bounds[0] = Math.min(bounds[0], x);
bounds[1] = Math.min(bounds[1], y);
bounds[2] = Math.max(bounds[2], x);
bounds[3] = Math.max(bounds[3], y);
}
}
function polygonPath(ctx, points) {
ctx.beginPath();
for (var i = 0; i < points.length; i += 2) {
var x = points[i+0];
var y = points[i+1];
if (i === 0)
ctx.moveTo(x, y);
else
ctx.lineTo(x, y);
}
ctx.closePath();
}
function polygonPoints(ctx, points, size) {
ctx.beginPath();
for (var i = 0; i < points.length; i += 2) {
var x = points[i+0];
var y = points[i+1];
ctx.arc(x, y, size, 0, 2 * Math.PI, false);
}
ctx.closePath();
}
function polygonArrows(ctx, points, size) {
ctx.beginPath();
var d = 0, dx = 0, dy = 0;
var px = points[points.length-2];
var py = points[points.length-1];
for (var i = 0; i < points.length; i += 2) {
var x = points[i+0];
var y = points[i+1];
dx = x - px;
dy = y - py;
d = Math.sqrt(dx*dx + dy*dy);
if (d > 0) {
dx /= d; dy /= d;
ctx.moveTo(x,y);
ctx.lineTo(x-dx*size-dy*size*0.5, y-dy*size+dx*size*0.5);
ctx.lineTo(x-dx*size+dy*size*0.5, y-dy*size-dx*size*0.5);
}
px = x;
py = y;
}
ctx.moveTo(points[0], points[1]);
ctx.arc(points[0], points[1], size*0.5, 0, 2 * Math.PI, false);
}
function polyCenter(off, elements, vertices, polygonSize) {
var cx = 0, cy = 0, cn = 0;
for (var i = 0; i < polygonSize; i++) {
var idx = elements[off+i];
if (idx == -1) break;
cx += vertices[idx*2+0];
cy += vertices[idx*2+1];
cn++;
}
return [cx/cn, cy/cn];
}
function triangulate() {
contours = [];
bounds = [10000000,10000000,-10000000,-10000000];
// parse holes
var npts = 0;
$("textarea#poly_contours").val().split(/\n\s*\n/).forEach(function(val) {
var cont = parsePoints(val);
if (cont.length > 0) {
polygonBounds(cont, bounds);
contours.push(cont);
npts += cont.length/2;
}
});
$("#contours_size").text(contours.length);
$("#contours_points").text(npts);
var $canvas = $('#canvas');
var ctx = $canvas[0].getContext('2d');
ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();
var xscale = (ctx.canvas.width * 0.8) / (bounds[2] - bounds[0]);
var yscale = (ctx.canvas.height * 0.8) / (bounds[3] - bounds[1]);
scale = Math.min(xscale, yscale);
var t0 = window.performance.now();
switch($("#winding_rule option:selected").attr("value")) {
case 'odd': windingRule = Tess2.WINDING_ODD; break;
case 'nonzero': windingRule = Tess2.WINDING_NONZERO; break;
case 'positive': windingRule = Tess2.WINDING_POSITIVE; break;
case 'negative': windingRule = Tess2.WINDING_NEGATIVE; break;
case 'absgeqtwo': windingRule = Tess2.WINDING_ABS_GEQ_TWO; break;
}
switch($("#element_type option:selected").attr("value")) {
case 'polygons': elementType = Tess2.POLYGONS; break;
case 'connected_polygons': elementType = Tess2.CONNECTED_POLYGONS; break;
case 'boundary_contours': elementType = Tess2.BOUNDARY_CONTOURS; break;
}
switch($("#polygon_size option:selected").attr("value")) {
case '3': polygonSize = 3; break;
case '4': polygonSize = 4; break;
case '6': polygonSize = 6; break;
case '10': polygonSize = 10; break;
case '16': polygonSize = 16; break;
}
tess = Tess2.tesselate({
contours: contours,
windingRule: windingRule, //Tess2.WINDING_ODD,
elementType: elementType, //Tess2.POLYGONS,
polySize: polygonSize, //3,
vertexSize: 2
});
var t1 = window.performance.now();
$("#tess_time").text((t1-t0).toFixed(4));
$("#triangles_size").text(tess.elementCount);
}
function draw() {
var error_points;
var $canvas = $('#canvas');
var ctx = $canvas[0].getContext('2d');
ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();
// clear the canvas
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
$(".info").css('visibility', 'visible');
// auto scale / translate
var cx = (bounds[0]+bounds[2])/2;
var cy = (bounds[1]+bounds[3])/2;
ctx.translate(ctx.canvas.width/2, ctx.canvas.height/2);
ctx.scale(scale, -scale); // set origo at bottom-left.
ctx.translate(-offset[0], -offset[1]);
ctx.translate(-cx, -cy);
var linescale = 1 / scale;
// draw result
ctx.lineWidth = linescale;
// ctx.fillStyle = TRIANGLE_FILL_STYLE;
// ctx.strokeStyle = TRIANGLE_STROKE_STYLE;
ctx.setLineDash([]);
if (tess !== null) {
ctx.strokeStyle = "rgba(0,192,255, 0.6)";
ctx.fillStyle = "rgba(0,192,255, 0.2)";
ctx.lineWidth = linescale;
if (elementType === Tess2.POLYGONS) {
// Draw polygons
for (var i = 0; i < tess.elements.length; i += polygonSize) {
ctx.beginPath();
for (var j = 0; j < polygonSize; j++) {
var idx = tess.elements[i+j];
if (idx == -1) continue;
if (j === 0)
ctx.moveTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
else
ctx.lineTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
}
}
if (elementType === Tess2.CONNECTED_POLYGONS) {
// Draw polygons
for (var i = 0; i < tess.elements.length; i += polygonSize*2) {
ctx.beginPath();
for (var j = 0; j < polygonSize; j++) {
var idx = tess.elements[i+j];
if (idx == -1) continue;
if (j === 0)
ctx.moveTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
else
ctx.lineTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
}
// Draw connections
ctx.strokeStyle = "rgba(0,0,0, 0.6)";
for (var i = 0; i < tess.elements.length; i += polygonSize*2) {
var ci = polyCenter(i, tess.elements, tess.vertices, polygonSize);
ctx.beginPath();
ctx.moveTo(ci[0]-linescale*3,ci[1]);
ctx.lineTo(ci[0]+linescale*3,ci[1]);
ctx.moveTo(ci[0],ci[1]-linescale*3);
ctx.lineTo(ci[0],ci[1]+linescale*3);
for (var j = 0; j < polygonSize; j++) {
var idx = tess.elements[i+j]; // vertex id
var nei = tess.elements[i+j+polygonSize]; // element id
if (nei == -1) continue; // no neighbour, skip
if (nei < i/(polygonSize*2)) continue; // draw only in one direction
var nidx = ((j+1) == polygonSize || tess.elements[i+j+1] == -1) ? tess.elements[i+0] : tess.elements[i+j+1];
var cn = polyCenter(nei*polygonSize*2, tess.elements, tess.vertices, polygonSize);
var dx = cn[0] - ci[0];
var dy = cn[1] - ci[1];
ctx.moveTo(ci[0],ci[1]);
ctx.quadraticCurveTo(ci[0]+dx*0.5+dy*0.3,ci[1]+dy*0.5-dx*0.3, cn[0],cn[1]);
}
ctx.stroke();
}
}
if (elementType === Tess2.BOUNDARY_CONTOURS) {
// Draw polygons
for (var i = 0; i < tess.elements.length; i += 2) {
ctx.beginPath();
var start = tess.elements[i+0];
var count = tess.elements[i+1];
for (var j = 0; j < count; j++) {
var idx = start+j;
if (j === 0)
ctx.moveTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
else
ctx.lineTo(tess.vertices[idx*2+0], tess.vertices[idx*2+1]);
}
ctx.closePath();
ctx.stroke();
ctx.fill();
}
}
if (tess.mesh) {
ctx.fillStyle = "rgba(0,192,255, 0.8)";
var vHead = tess.mesh.vHead;
for (var v = vHead.next; v !== vHead; v = v.next) {
ctx.beginPath();
// ctx.arc(v.coords[0], v.coords[1], linescale*2.0, 0, 2 * Math.PI, false);
ctx.arc(v.s, v.t, linescale*2.0, 0, 2 * Math.PI, false);
ctx.fill();
}
var eHead = tess.mesh.eHead;
ctx.strokeStyle = "rgba(0,192,255, 0.8)";
for (var e = eHead.next; e !== eHead; e = e.next) {
ctx.beginPath();
// ctx.moveTo(e.Org.coords[0], e.Org.coords[1]);
// ctx.lineTo(e.Dst.coords[0], e.Dst.coords[1]);
ctx.moveTo(e.Org.s, e.Org.t);
ctx.lineTo(e.Dst.s, e.Dst.t);
ctx.stroke();
}
}
}
if ($("#draw_bounds").attr('checked')) {
// Bounds
ctx.strokeStyle = "rgba(0,255,0, 0.6)";
ctx.lineWidth = linescale;
ctx.setLineDash([5 * linescale, 5 * linescale]);
ctx.beginPath();
ctx.moveTo(bounds[0], bounds[1]);
ctx.lineTo(bounds[2], bounds[1]);
ctx.lineTo(bounds[2], bounds[3]);
ctx.lineTo(bounds[0], bounds[3]);
ctx.lineTo(bounds[0], bounds[1]);
ctx.stroke();
ctx.setLineDash([]);
ctx.beginPath();
ctx.moveTo(cx-linescale*10, cy);
ctx.lineTo(cx+linescale*10, cy);
ctx.moveTo(cx,cy-linescale*10);
ctx.lineTo(cx,cy+linescale*10);
ctx.stroke();
}
if ($("#draw_input_polys").attr('checked')) {
// Polygons
ctx.lineWidth = linescale*1.5;
ctx.strokeStyle = "rgba(192,32,16, 0.6)";
ctx.fillStyle = "rgba(192,32,16, 0.6)";
// ctx.setLineDash([3 * linescale, 2 * linescale]);
for (var i = 0; i < contours.length; i++) {
polygonPath(ctx, contours[i]);
ctx.stroke();
polygonArrows(ctx, contours[i], linescale*6);
// polygonPoints(ctx, contours[i], linescale*2);
ctx.fill();
}
}
// highlight errors, if any
/* if (error_points) {
ctx.lineWidth = 4 * linescale;
ctx.fillStyle = ERROR_STYLE;
error_points.forEach(function(point) {
ctx.beginPath();
ctx.arc(point.x, point.y, ctx.lineWidth, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
});
}*/
}
$(window).resize(function() {
draw();
});
$(document).ready(function() {
var $canvas = $('#canvas');
var ctx = $canvas[0].getContext('2d');
ctx.canvas.width = $canvas.width();
ctx.canvas.height = $canvas.height();
if (typeof ctx.setLineDash === "undefined") {
ctx.setLineDash = function(a) {
ctx.mozDash = a;
};
}
$("#btnTriangulate").click(function() {
triangulate();
draw();
});
clearData();
$("#draw_bounds").change(function() {
draw();
});
$("#draw_input_polys").change(function() {
draw();
});
$("#winding_rule").change(function() {
triangulate();
draw();
});
$("#element_type").change(function() {
triangulate();
draw();
});
$("#polygon_size").change(function() {
triangulate();
draw();
});
// Load index.json and populate 'preset' menu
$("#preset").empty().append($('<option>', {
text: "--Empty--"
}));
$.ajax({
url: "data/index.json",
dataType: "json",
success: function(data) {
var options = [];
data.forEach(function(group) {
group.files.filter(function(file) {
return file.name; // && file.content;
}).forEach(function(file) {
options.push($('<option>', {
value: file.name,
text: (file.content || file.name)
}).data("file", file));
});
});
// Sort before adding
options.sort(function(a,b) {
return $(a).text().localeCompare($(b).text());
}).forEach(function(option) {
$("#preset").append(option);
});
// Load some default data
$("#preset option[value='glu_winding.dat']").attr("selected", "selected");
$("#preset").change();
}
});
$("#preset").change(function() {
var file = $("#preset option:selected").data("file") || {};
function load(filename, next) {
if (filename) {
$.ajax({
url: "data/" + filename,
success: function(data) {
if (next) next(data);
}
});
} else {
if (next) next("");
}
}
clearData();
var d = "";
load(file.name, function(data1) {
d += data1;
load(file.holes, function(data2) {
if (data2.length > 0) {
d += "\n";
d += data2;
}
$("#poly_contours").val(d);
triangulate();
draw();
});
});
});
var panning = false;
var smouse = [0,0];
var soffset = [0,0];
function mousedown( event ) {
event.preventDefault();
panning = true;
smouse[0] = event.clientX;
smouse[1] = event.clientY;
soffset[0] = offset[0];
soffset[1] = offset[1];
document.addEventListener( 'mousemove', mousemove, false );
document.addEventListener( 'mouseup', mouseup, false );
}
function mousemove( event ) {
event.preventDefault();
if ( panning ) {
var dx = smouse[0] - event.clientX;
var dy = smouse[1] - event.clientY;
offset[0] = soffset[0] + dx/scale;
offset[1] = soffset[1] + dy/-scale;
draw();
}
}
function mouseup( event ) {
event.preventDefault();
document.removeEventListener( 'mousemove', mousemove, false );
document.removeEventListener( 'mouseup', mouseup, false );
}
function mousewheel( event ) {
event.preventDefault();
var delta = 0;
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
} else if ( event.detail ) { // Firefox
delta = -event.detail;
}
if (delta > 0) {
scale *= 1.1;
draw();
} else if (delta < 0) {
scale /= 1.1;
draw();
}
};
var canvas = $canvas[0];
canvas.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
canvas.addEventListener( 'mousedown', mousedown, false );
canvas.addEventListener( 'mousewheel', mousewheel, false );
canvas.addEventListener( 'DOMMouseScroll', mousewheel, false ); // firefox
});