Artifact [e44e8eb350]
Not logged in

Artifact e44e8eb350083725018d9cc43ed9fce425ca683c:


<html>
<script src='matrix_vector_lib.js' ></script>

<script>

//
//  Editor modes
//
//  Motion mode  -- change the camera center using the original movement system
//  --------Camera mode  -- adjust the camera,  scale zoom and position zoom, make it stationary or rotating about the center
//  Edit mode -- create points at camera center, move points, create lines by clicking on successive points, create closed polygons etc.

//  Editing Strategy

//  During draw mode only the very edges of the screen rotate the view, and at reduced speed
//  pressing enter or clicking on the center point creates a new point.
//  points are moved with the arrow keys, the direction is absolute, that means it does not depend on the camera angle.
//  the arrow keys move the selected point.
// clicking on a second point when one point is already selected creates a line.

//  TODO  control with zoom scales the selected object
//        control with rotate rotates the selected objects

//  TODO clicking and dragging on a line allows you to place a point somewhere on that line.  Perhaps with the ctrl key it would break up the existing line into line segments.

//  TODO when lines are deleted, readd the solo points to the solo_points list
//  

//  TODO z for undo shift + z for redo.
//

//  TODO drop down minimalist menu in top right of canvas.


//  drag to connect points


var canvas;
var origin = [0, 0, 0];
var frame_rate = 20;
var saveTimeout = 10*1000;

var no_rotate_area = 0.80;
            
            

            

var key_state = {};
var key_state_augmentation_delay = 150; // the permissible time in milliseconds since the last key event to still allow the key state to augment instead of clearing.

function addKeyListener(keycode, func){
    if(!key_state[keycode]) key_state[keycode] = {};
    
    var key_obj = key_state[keycode];
    if(!key_obj.listeners) key_obj.listeners = [];
    
    var listeners = key_obj.listeners;
    listeners.push(func);
}


function keyDown(event, code){
    var time = (new Date()).getTime();
    var code = event.keyCode;
    // alert("you pressed: " + code);
        
    var key_obj = key_state[code];
    if(!key_obj) key_obj = key_state[code] = {}; // create object

    
    //  update state etc.
    
    var state = key_obj.state;
    if(state) return; //key is already pressed.
    
    state = 0;
    
    var latest = key_obj.latest;

    if(latest && time - latest < key_state_augmentation_delay)
        state = key_obj.state = key_obj.lastState + 1;
    else
        state = key_obj.state = 1;
        
    key_obj.latest = time;
    
    key_obj.lastState = state;


    //  call listener functions 
    var listeners = key_obj.listeners
    if(!listeners) return;  //nothing to do.
      
    for(var i = 0; i < listeners.length; ++i)
        listeners[i](event, state, state); }

    
function keyUp(event){
    var time = (new Date()).getTime();
    var code = event.keyCode;
    
    var key_obj = key_state[code];
    if(!key_obj) return;  //nothing to do.
      
    key_obj.latest = time;
    key_obj.lastState = key_obj.state;
    var state = key_obj.state = 0;
    var lastState = key_obj.lastState;
    
    var listeners = key_obj.listeners;
    if(!listeners) return;  //nothing to do.
    
    for(var i = 0; i < listeners.length; ++i)
        listeners[i](event, state, lastState); }

        
        
        
function setUiEvents(){
    document.body.onkeydown = keyDown;
    document.body.onkeyup = keyUp;
    
    document.body.onmousedown = function(event){
        var x = event.pageX;
        var y = event.pageY;
        x -= canvas.offsetLeft;
        y -= canvas.offsetTop;
        last_mouse_down = [x, y];
        
        var button = event.button;
        var keycode = button + 1000;  //treat clicks like a button press but with higher keycode.
        keyDown({keyCode: keycode}); }  // it seems the keyCode of the original event cant be overwritten.
        
    document.body.onmousewheel = function(event){
        var delta = event.wheelDelta;
        
        //if(editor_mode == MODE_DRAWING){
		if(delta < 0)
			zoom_dist /= zoom_factor;
		else if(delta > 0)
			zoom_dist *= zoom_factor; }
        
        
    document.body.onmouseup = function(event){
        var button = event.button;
        var keycode = button + 1000;  //treat clicks like a button press but with higher keycode.
        keyUp({keyCode: keycode});
        last_mouse_down = null;
        mouse_dragging = false; }}
        


    
function changeVelocity(direction, positive, state, lastState){
    if(state == 0){
        state = lastState;
        positive = !positive; }  // reverse the velocity
    
    var delta = Math.pow(2, state - 1) * max_move_delta / frame_rate;
    if(!positive) delta = -delta;
    
    var velocityChange = [0, 0, 0];
    velocityChange[direction] = delta;
    vector_add(delta_position, velocityChange, delta_position); }
    

//
//  This 
//
    
function leftPress(e, s, lasts){
    changeVelocity(0, false, s, lasts); }
    
function rightPress(e, s, lasts){
     changeVelocity(0, true, s, lasts); }
     
function downPress(e, s, lasts){
    changeVelocity(1, false, s, lasts); }
    
function upPress(e, s, lasts){
    changeVelocity(1, true, s, lasts); }

function backwardPress(e, s, lasts){
    changeVelocity(2, false, s, lasts); }
    
function forwardPress(e, s, lasts){
    changeVelocity(2, true, s, lasts); }
        
        
function zoomInPress(e, s, lasts){
    //if(editor_mode == MODE_CAMERA_POSITION)
    //    changeVelocity(2, true, s, lasts);
    //else if(editor_mode == MODE_CAMERA_ROTATION)
        zoom_dist *= zoom_factor; }
        
        
function zoomOutPress(e, s, lasts){
    //if(editor_mode == MODE_CAMERA_POSITION)
    //    changeVelocity(2, true, s, lasts);
    //else if(editor_mode == MODE_CAMERA_ROTATION)
        zoom_dist *= zoom_factor; }


function copySelectedPoints(e, s, lasts){
    if(s){
        var new_selection = [];
        var selection_map = {};
        
        for(var i = 0; i < selected_points.length; ++i){
            selection_map[selected_points[i]] = i;
            var pt = points[selected_points[i]];
            
            var newpt = addPoint(pt);
            new_selection[i] = newpt; }
        
        //if(key_state[16] && key_state[16].state){  //shift + space copies lines as well.
        
        // copy lines as well
        for(var i = 0; i < lines.length; ++i){
           var line = lines[i];
           if(!line) continue;
           
           var pt1 = lines[i][0];
           var pt2 = lines[i][1];
           var i1 = selection_map[pt1];
           var i2 = selection_map[pt2];
           
           if(i1 !== undefined && i2 !== undefined){  // both points are selected
               addLine(new_selection[i1], new_selection[i2]); }}
       
        selected_points = new_selection; }}
            
            
addKeyListener(32, copySelectedPoints);        
    
addKeyListener(37, leftPress);  // arrow keys
addKeyListener(38, forwardPress);
addKeyListener(39, rightPress);
addKeyListener(40, backwardPress);


addKeyListener(65, leftPress);  // wasd
addKeyListener(87, forwardPress);
addKeyListener(68, rightPress);
addKeyListener(83, backwardPress);
addKeyListener(69, upPress);
addKeyListener(81, downPress);


addKeyListener(88, function(e, s, lasts){  // x

    //if(editor_mode == MODE_DRAWING && s){  // alert('deleting points');
	
    if(s){
        if(highlight_object !== null && highlight_object > 0){  // delete highlighted object
            
            if(highlight_object < point_projections.length){  // point
                points[highlight_object] = null; }
                
            else if(highlight_object < point_projections.length + line_midpoint_projections.length){  // line
                lines[highlight_object - point_projections.length] = null; }
            else throw "highlight object index to large"; }
        
        else if(selected_points.length){  // delete selected points
            for(var i = 0; i < selected_points.length; ++i){
                points[selected_points[i]] = null; }
            selected_points.length = 0; }
        
        cleanupDeletedPoints();
        highlight_object = null;
        selected_points.length = 0; }});

// addKeyListener(1000, aForwardPress);    // mouse buttons
// addKeyListener(1002, cameraBackwardPress);



// addKeyListener(13, function(e, s, lasts){ if(s) nextMode(); });  // switch modes


//  select point
addKeyListener(1000, function(e, s, lasts){

    //if(editor_mode == MODE_DRAWING){
        if(s){
            if(highlight_object !== null){
                if(highlight_object == -1)
                    highlight_object = addPoint(origin);  // create a new point at the origin
                    
                
                var alreadySelected = false;  // only set for shift clicks
                
                if(key_state[16] && key_state[16].state){  // shift key is pressed.
                
                    for(var i = 0; i < selected_points.length; ++i){
                        if(selected_points[i] == highlight_object){
                           selected_points.splice(i, 1); // deselect point
                           alreadySelected = true; 
                           break; }}}
                else{
                    selected_points.length = 0;}
                    
                if(!alreadySelected) selected_points.push(highlight_object); }

            else if(!key_state[16] || !key_state[16].state)
                selected_points.length = 0; }
            
            
        else if(lasts && mouse_dragging ){  // drag actions
            
            //  connect selected points to highlight point with lines
            if(selected_points.length && (!key_state[16] || !key_state[16].state)){  //  TODO right now this really only works for drawing a single line, 
            
                if(highlight_object !== null && highlight_object < point_projections.length){
                    if(highlight_object == -1) highlight_object = addPoint(origin);
                    
                    for(var i = 0; i < selected_points.length; ++i){
                        var pt = selected_points[i];
                        if(pt == highlight_object) continue;
                        addLine(pt, highlight_object); }
                    selected_points.length = 0; }}
                
            // select points inside selection rectangle
            else if(mouse_loc && last_mouse_down){ 
            
                var minx = Math.min(mouse_loc[0], last_mouse_down[0]);
                var maxx = Math.max(mouse_loc[0], last_mouse_down[0]);
                var miny = Math.min(mouse_loc[1], last_mouse_down[1]);
                var maxy = Math.max(mouse_loc[1], last_mouse_down[1]);
                
                
                var selected_point_map = {};             
                
                if(!key_state[16] || !key_state[16].state){
                    selected_points.length = 0;
                    selected_lines.length = 0; }
                else{
                    for(var i = 0; i < selected_points.length; ++i){
                        selected_point_map[selected_points[i]] = i; }}
                
                for(var i = 0; i < point_projections.length; ++i){
                    if(selected_point_map[i]) continue; // already selected.
                    
                    var pt = point_projections[i];
                    if(!pt) continue;
                    var _x = pt[0];
                    var _y = pt[1];
                    
                    if(minx < _x && _x < maxx
                     && miny < _y && _y < maxy){
                       selected_points.push(i); }}}                
        }});





// to prevent camera from rotating beyond straight vertical
var vertical_camera_rotation = 0;
var NAVIGATION_ZOOM_DIST = 0.01;
var zoom_dist = 1;
var zoom_factor = 0.83;
var zoom_scale = 2.667;  // scalar zoom property.
var last_zoom_dist = 1;


// camera animation
var max_move_delta = 0.01;
var max_angle_delta = Math.PI / 2;

var delta_horizontal_angle = 0;
var delta_vertical_angle = 0;
var delta_position = [0,0,0];


// var MODE_DRAWING = 0;
// var MODE_CAMERA_POSITION = 1; //  change the camera focus point
// var MODE_CAMERA_ROTATION = 2; //  rotate and zoom camera

// var editor_mode = MODE_DRAWING;

/*
function drawMode(){
    editor_mode = MODE_DRAWING;
    delta_position = [0,0,0];
    // delta_horizontal_angle = 0;
    // delta_vertical_angle = 0;
    if(last_zoom_dist) zoom_dist = last_zoom_dist;
    else zoom_dist = 1;
               
    document.body.style.cursor = "crosshair"; }
    
function cameraPositionMode(){
    document.body.style.cursor = "default";
    last_zoom_dist = zoom_dist;
    delta_horizontal_angle = 0;
    delta_vertical_angle = 0;
    zoom_dist = NAVIGATION_ZOOM_DIST;
    editor_mode = MODE_CAMERA_POSITION; }

function cameraRotationMode(){
    document.body.style.cursor = "default";
    delta_horizontal_angle = 0;
    delta_vertical_angle = 0;
    if(last_zoom_dist) zoom_dist = last_zoom_dist;
    else zoom_dist = 1;
    editor_mode = MODE_CAMERA_ROTATION; }
    
function nextMode(){
    if(editor_mode == MODE_DRAWING) cameraPositionMode();
    else if(editor_mode == MODE_CAMERA_POSITION) drawMode(); }

    // skipping camera rotation mode
    //else if(editor_mode == MODE_CAMERA_POSITION) cameraRotationMode();
    //else if(editor_mode == MODE_CAMERA_ROTATION)  drawMode(); }  
    
   */ 


var points = []; //global
var solo_points = {};
var lines = [];

var point_projections = [];
var line_midpoint_projections = [];

var highlight_object = null;
var mouse_dragging = false;
var last_mouse_down = null;
var mouse_loc = null;
var MIN_DRAG_DIST = 10;
var POINT_HIGHLIGHT_DIST = 10;
var LINE_HIGHLIGHT_DIST = 15;

var selected_points = [];
var selected_lines = [];

var msg = "3D drawing tool";


var view_transform =
    [[1, 0, 0],
     [0, 1, 0],
     [0, 0, 1]];
    

    
//  camera functions

function rotateView(norm, theta, cameracentric){

    // camera centric uses the camera axes to perform the rotation instead of the space axes.   
    if(cameracentric) norm = matrix_mult(view_transform, [norm])[0];
    var rotation = vector_rotation(norm, theta);
    
    view_transform = matrix_mult(rotation, view_transform); }
    
    
    
function rotateHorizontal(theta){
    rotateView([0, 1, 0], theta, false); }
    
function rotateVertical(theta){
    if(vertical_camera_rotation + theta > Math.PI/2)
        theta = Math.PI/2 - vertical_camera_rotation;
        
    if(vertical_camera_rotation + theta < -Math.PI/2)
        theta = -Math.PI/2 - vertical_camera_rotation;
        
    vertical_camera_rotation += theta;
    
    rotateView([1, 0, 0], theta, true); }
    
    
function moveCamera(offset, cameracentric){
    if(cameracentric) offset = matrix_mult(view_transform, [offset])[0];
    //alert("offset: " + offset);
    vector_add(origin, offset, origin); }
    

// converts a point from three space to the canvas plane.
// Note: because of depth perspective, this conversion is not
// easy to define if the point lies behind the camera.
// There are two options:
// When drawing a line, another colinear point in front of the camera may be provided
// to help find an alertnate point.
// if both points lie behind the camera or the colinear_point is not provided,
// this function will return null.
 
function project(canvas, xyz, view_transform, origin, colinear_point){
    if(!xyz) return null;  // point has been deleted or does not exist
    
    var w = canvas.width;
    var h = canvas.height;
    
    var scale = Math.min(w, h);
    
    var v = xyz.slice(0);
    if(origin) v = vector_minus(v, origin, v);
    
    var z = vector_dot(view_transform[2], v);
    
    if(z <= -zoom_dist){
        if(!colinear_point) return null;
        
        var v2 = colinear_point.slice(0);
        if(origin) vector_minus(v2, origin, v2);
        
        var z2 = vector_dot(view_transform[2], v2);
        
        if(z2 < 0) return null;
        
        // get the coefficients for a complex combination.
        // t*z + (1-t)*z2 = 0.0002  -- z of new point is just barely infront of the camera.
        
        var t = (0.0002 - z2)/(z - z2);  // no division by zero, z is negative, z2 is positive
        
        vector_add(vector_scale(v, t, v),
                   vector_scale(v2, 1-t, v2),
                   v);
                   
        z = vector_dot(view_transform[2], v); }

        
    var scale2 = zoom_scale * scale / (zoom_dist + z);
    
    return [ scale2 * vector_dot(view_transform[0], v) + 0.5 * w,
             scale2 * vector_dot(view_transform[1], v) + 0.5 * h ]; }
   

   
//  removes deleted points and lines
//  works with global objects.

function cleanupDeletedPoints(){
    var newpoints = [];
    var newlines = [];
    var pointmap = {};
    
    var j = 0;
    
    for(var i = 0; i < points.length; ++i){
       
       if(points[i]){
           newpoints.push(points[i]);
           pointmap[i] = j;
           ++j; }
           
       else{
           pointmap[i] = -1; }}
    
    
    for(var i = 0; i < lines.length; ++i){
        if(!lines[i]) continue;
        var a = lines[i][0];
        var b = lines[i][1];
        
        if(pointmap[a] != -1 && pointmap[b] != -1){
            newlines.push([ pointmap[a], pointmap[b] ]); }}
    
    points = newpoints;
    lines = newlines; }
        

function addPoint(pt){
    points.push(pt.slice(0));
    var index = points.length - 1;
    solo_points[index] = true;
    return index; }
    
    
function addLine(pt0, pt1){
    delete solo_points[pt0];
    delete solo_points[pt1];
    lines.push([pt0, pt1]);
    return lines.length - 1; }

function addBox(xyz, wdh){
    var x = xyz[0];
    var y = xyz[1];
    var z = xyz[2];
    
    var w = wdh[0];
    var d = wdh[1];
    var h = wdh[2];
    
    var x0 = x - w/2;
    var y0 = y - d/2;
    var z0 = z - d/2;
    
    var x1 = x + w/2;
    var y1 = y + d/2;
    var z1 = z + h/2;
    
    var a = addPoint([x0, y0, z0]);
    var b = addPoint([x1, y0, z0]);
    var c = addPoint([x1, y1, z0]);
    var d = addPoint([x0, y1, z0]);
    var e = addPoint([x0, y0, z1]);
    var f = addPoint([x1, y0, z1]);
    var g = addPoint([x1, y1, z1]);
    var h = addPoint([x0, y1, z1]);
   

    addLine(a, b);
    addLine(b, c);
    addLine(c, d);
    addLine(d, a);
    addLine(e, f);
    addLine(f, g);
    addLine(g, h);
    addLine(h, e);
    
    addLine(a, e);
    addLine(b, f);
    addLine(c, g);
    addLine(d, h); }
    
    
    
    
function draw(canvas){
    var ctx = canvas.getContext('2d');
    var w = canvas.width;
    var h = canvas.height;
    
    // ctx.fillStyle = "rgba(100, 100, 100, .05)";
    
    
    ctx.beginPath();
    
    for(var i = 0; i < lines.length; ++i){
        var line = lines[i];
        if(!line) continue;
        //draw line
        
        var pt0 = points[line[0]];
        var pt1 = points[line[1]];
            
        var _pt0 = point_projections[line[0]];
        var _pt1 = point_projections[line[1]];
        
        //  finish the line even if one point is on wrong side of viewer.
        if(!_pt0 && _pt1){
            _pt0 = project(canvas, pt0, view_transform, origin, pt1); }
        else if(_pt0 && !_pt1){
            _pt1 = project(canvas, pt1, view_transform, origin, pt0); }
            
        if(!_pt0 || !_pt1) continue;
        
        var x0 = _pt0[0];
        var y0 = _pt0[1];

        var x1 = _pt1[0];
        var y1 = _pt1[1];
        
        ctx.moveTo(x0, y0);
        ctx.lineTo(x1, y1); }
    
    ctx.clearRect(0, 0, w, h);
    var hoffset = h * (1 - no_rotate_area)/ 2;
    var woffset = w * (1 - no_rotate_area)/ 2;
    
    ctx.fillStyle = "rgba(128, 128, 128, 0.05)";
    ctx.fillRect(0, 0, w, hoffset);
    ctx.fillRect(0, hoffset, woffset, h - 2 * hoffset);
    ctx.fillRect(w - woffset, hoffset, woffset, h - 2 * hoffset);
    ctx.fillRect(0, h - hoffset, w, hoffset);
    
    ctx.fillStyle = "rgb(0, 0, 128)";
    ctx.stroke();
    
    for(var k in solo_points){
       var pt = point_projections[k];
       if(pt) ctx.fillRect(pt[0] - 2, pt[1] - 2, 4, 4); }
        
    
    
    
    //if(editor_mode == MODE_DRAWING){
	ctx.fillStyle = "rgba(250, 0, 0, 0.9)";
	ctx.fillRect(w/2 - 2, h/2 - 2, 4, 4);
	
	for(var i = 0; i < selected_points.length; ++i){
		var pt;
		
		if(selected_points[i] == -1)
			pt = [w/2, h/2];
		else pt = point_projections[selected_points[i]];
		
		ctx.fillStyle = "rgba(0, 0, 0, 0.9)";
		if(pt) ctx.fillRect(pt[0] - 4, pt[1] - 4, 8, 8); }
	
	
	if(highlight_object !== null){
		
		
		if(highlight_object < point_projections.length){
			var pt;
			if(highlight_object == -1)
				pt = [w/2, h/2];
			else pt = point_projections[highlight_object];
			
			ctx.fillStyle = "rgba(50, 50, 50, .6)";    
			ctx.fillRect(pt[0] - 4, pt[1] - 4, 8, 8);
			document.body.style.cursor = "hand"; }
			
		else if(highlight_object < point_projections.length + line_midpoint_projections.length){
			// alert("drawing highlighted line");
			
			var line = lines[highlight_object - point_projections.length];
			if(line){
				var pt1 = point_projections[line[0]];
				var pt2 = point_projections[line[1]];
				
				if(pt1 && pt2){
				
					ctx.beginPath();
					
					ctx.moveTo(pt1[0], pt1[1]);
					ctx.lineTo(pt2[0], pt2[1]);
					ctx.lineWidth = 3;
					ctx.stroke();
					ctx.lineWidth = 1; }}}
			
		else{
			throw "highlight object index to large"; }}
		
	else{
		document.body.style.cursor = "crosshair"; }
	
	if(mouse_dragging){
	
		if(selected_points.length && (!key_state[16] || !key_state[16].state)){  // if shift is held, we are selecting more points.
			if(mouse_loc){
				ctx.beginPath();
				for(var i = 0; i < selected_points.length; ++i){
					var pt = point_projections[selected_points[i]];
					if(pt){
						ctx.moveTo(mouse_loc[0], mouse_loc[1]);
						ctx.lineTo(pt[0], pt[1]); }}
				ctx.stroke(); }}
		
		else if(mouse_loc && last_mouse_down){  // select points inside square            
			var minx = Math.min(mouse_loc[0], last_mouse_down[0]);
			var maxx = Math.max(mouse_loc[0], last_mouse_down[0]);
			var miny = Math.min(mouse_loc[1], last_mouse_down[1]);
			var maxy = Math.max(mouse_loc[1], last_mouse_down[1]);
			ctx.beginPath();
			ctx.rect(minx, miny, maxx-minx, maxy-miny);
			ctx.stroke(); }
	}
    
    ctx.fillStyle = "rgb(0,0,0)";
    ctx.fillText(msg, 5, 20); }

    
    
//function buildFractal(children, func, levels, origin, scale, baseonly){
    
//}


function serpenskiPyramid(level, origin, scale){
    if(!scale) scale = 1;
    if(!origin) origin = [0, 0, 0];
    if(level === undefined) level = 4;

    var spoints = [[0,0,0], [0.5,0,0], [0, -0.5, 0], [0, 0, 0.5]];
    
    
    for(var i = 0; i < spoints.length; ++i){
        var x = spoints[i];
        vector_scale(x, scale);
        vector_add(x, origin, x); }
    
    
    if(level > 0){
        for(var i = 0; i < spoints.length; ++i){
            var point = spoints[i];
            vector_add(origin, point, origin);
            serpenskiPyramid(level - 1, origin, 0.5 * scale);
            vector_minus(origin, point, origin); }
            
    }
    else if(level == 0){
        var pointRefs = [];
        for(var i = 0; i < spoints.length; ++i){
            vector_scale(spoints[i], 2);  // scale to points to full length;
            pointRefs[i] = addPoint(spoints[i]); }
            
        
        for(var i = 0; i < spoints.length; ++i){
            var a = pointRefs[i];
            
            for(var j = i + 1; j < spoints.length; ++j){
                var b = pointRefs[j];
                addLine(a, b); }}}
}

    
function buildArray(origin, increment, size, buildfunc, extra1, extra2, extra3){
    var point = origin.slice(0);
    point_indexes = [];
    
    for(var i = 0; i < size.length; ++i){
        point_indexes[i] = 0; }
    
    var last_index = point_indexes.length - 1;
    var index = last_index;
    
    // build object at origin.
    buildfunc(point, extra1, extra2, extra3);
    
    while(index >= 0){
        //depth first
        
        if(point_indexes[index] == size[index] - 1){
            point[index] = origin[index];
            point_indexes[index] = 0;
            --index; }
        else{
            ++point_indexes[index];
            point[index] += increment[index];
            index = last_index;  // always go to the end position
            buildfunc(point, extra1, extra2, extra3); }}
}
    

window.onload = function(){

    // load localStorage saved state.
    if(localStorage.points && localStorage.lines){
        points = JSON.parse(localStorage.points);
        lines = JSON.parse(localStorage.lines); }
    
    canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
    
    canvas.width = window.innerWidth - 20;
    canvas.height = window.innerHeight - 20;
    
    
    window.onresize = function(){
        canvas.width = window.innerWidth - 20;
        canvas.height = window.innerHeight - 20; }

    //addBox([0, 0, 0], [.5, .5, .5]);
    
    /*
    buildArray( [0, 0, 0],  // origin
                [2, 2, 2], // increment
                [11, 1, 11], // size
                addBox,
                [.5, .5, .5]); */
    
    //var a = addPoint([0, 0, 1]);
    //var b = addPoint([.5, 0, 1]);
    //var c = addPoint([0, .5, 1]);
    

    
    //addLine(a, b);
    //addLine(b, c);
    //addLine(c, a);
    
    //serpenskiPyramid(5, null, 1);
    
    
    
    function getRotationAngle(x, size, max, center_size){

        if(!center_size) center_size = 0.25;  // the proportional size of the area in which no rotation is effected by the mouse movement
        if(!max) max = Math.PI/128;
        x -= size/2;
        
        //  center does nothing
        if(Math.abs(x) < size * center_size/2) x = 0;
        else if(x < 0) x += size * center_size/2;
        else           x -= size * center_size/2;
        
        return max * x / ((1 - center_size) * size/2); }
        
    
    document.body.onmousemove = function(event){
    //var test = function(event){

        var x = event.pageX;
        var y = event.pageY;
        x -= canvas.offsetLeft;
        y -= canvas.offsetTop;
        mouse_loc = [x, y]

        var w = canvas.width;
        var h = canvas.height;
        
        if(!mouse_dragging && last_mouse_down){
            var _x = x - last_mouse_down[0];
            var _y = y - last_mouse_down[1];
            
            var dist2 = _x*_x + _y*_y;
            
            if(dist2 > MIN_DRAG_DIST * MIN_DRAG_DIST){
                mouse_dragging = true; }}
                
    /*
        if(editor_mode == MODE_CAMERA_ROTATION){   //  rotate about center
        
            delta_horizontal_angle = getRotationAngle(x, w, max_angle_delta/frame_rate);
            delta_vertical_angle = -getRotationAngle(y, h, max_angle_delta/frame_rate); }


        if(editor_mode == MODE_CAMERA_POSITION){        
        
            delta_horizontal_angle = -getRotationAngle(x, w, max_angle_delta/frame_rate);
            delta_vertical_angle = getRotationAngle(y, h, max_angle_delta/frame_rate); }
            

        else if(editor_mode == MODE_DRAWING){ */

		delta_horizontal_angle = getRotationAngle(x, w, max_angle_delta/frame_rate, no_rotate_area);
		delta_vertical_angle = -getRotationAngle(y, h, max_angle_delta/frame_rate, no_rotate_area);
		
		var min_point_dist2 = LINE_HIGHLIGHT_DIST * LINE_HIGHLIGHT_DIST;
		highlight_object = null;
		
		var _x = canvas.width/2 - x;
		var _y = canvas.height/2 - y;
		var point_dist = _x*_x + _y*_y;
		
		if(point_dist < POINT_HIGHLIGHT_DIST * POINT_HIGHLIGHT_DIST){
			highlight_object = -1;
			min_point_dist = point_dist; }
			
			
		for(var i = 0; i < line_midpoint_projections.length; ++i){
			var pt = line_midpoint_projections[i];
			if(!pt) continue;
			
			var _x = pt[0] - x;
			var _y = pt[1] - y;
			point_dist = _x*_x + _y*_y;
			
			if(point_dist < min_point_dist2){  // alert('highlighting line');
				highlight_object = i + point_projections.length;
				min_point_dist = point_dist; }}
				
		min_point_dist2 = Math.min( min_point_dist2, POINT_HIGHLIGHT_DIST * POINT_HIGHLIGHT_DIST);
			
		for(var i = 0; i < point_projections.length; ++i){
			var pt = point_projections[i];
			if(!pt) continue;
			
			var _x = pt[0] - x;
			var _y = pt[1] - y;
			point_dist = _x*_x + _y*_y;
			
			if(point_dist < min_point_dist2){
				highlight_object = i;
				min_point_dist = point_dist; }}
    }
 
    setUiEvents();     
        
    document.body.oncontextmenu = function(){
        return false; };
   
    
    // draw(canvas);
    // var ctx = canvas.getContext('2d');
    // ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // 
    
    
    function animateLoop(){
        
        //if(editor_mode == MODE_DRAWING){
		if(selected_points.length){  // move selection if exists
			for(var i = 0; i < selected_points.length; ++i){
				var pt = points[selected_points[i]];
				vector_add(pt, delta_position, pt); }}
		
		else{
			moveCamera(delta_position, false); }
        
        //else if(editor_mode == MODE_CAMERA_POSITION){
        //    if(delta_position)  moveCamera(delta_position, true); }  // motion depends on camera angle
        
        if(delta_horizontal_angle) rotateHorizontal(delta_horizontal_angle);
        if(delta_vertical_angle) rotateVertical(delta_vertical_angle);
        
       
        //  overwrite point projections
        //  TODO this may not be perfect if points are deleted etc.        
        point_projections.length = 0;
        for(var i = 0; i < points.length; ++i){
            point_projections[i] = project(canvas, points[i], view_transform, origin); }
            
        line_midpoint_projections.length = 0;
        
        for(var i = 0; i < lines.length; ++i){
            var line = lines[i];
            if(!line){
                line_midpoint_projections[i] = null;
                continue; }
                
            var pt1 = point_projections[line[0]];
            var pt2 = point_projections[line[1]];
            if(pt1 && pt2){
                line_midpoint_projections[i] = [(pt1[0] + pt2[0])/2, (pt1[1] + pt2[1])/2]; }
            else{
                line_midpoint_projections[i] = null; }}
            

        draw(canvas);
    }
    setInterval(animateLoop, 1000/frame_rate);
    
    var saveAction = function(){
        localStorage.points = JSON.stringify(points);
        localStorage.lines = JSON.stringify(lines);
    }
        
    setInterval(saveAction, saveTimeout);
}


</script>


<body></body>
</html>