Compare commits

...

No commits in common. "master" and "gh-pages" have entirely different histories.

16 changed files with 8298 additions and 21 deletions

92
Math.uuid.js Normal file
View File

@ -0,0 +1,92 @@
/*!
Math.uuid.js (v1.4)
http://www.broofa.com
mailto:robert@broofa.com
Copyright (c) 2010 Robert Kieffer
Dual licensed under the MIT and GPL licenses.
*/
/*
* Generate a random uuid.
*
* USAGE: Math.uuid(length, radix)
* length - the desired number of characters
* radix - the number of allowable values for each character.
*
* EXAMPLES:
* // No arguments - returns RFC4122, version 4 ID
* >>> Math.uuid()
* "92329D39-6F5C-4520-ABFC-AAB64544E172"
*
* // One argument - returns ID of the specified length
* >>> Math.uuid(15) // 15 character ID (default base=62)
* "VcydxgltxrVZSTV"
*
* // Two arguments - returns ID of the specified length, and radix. (Radix must be <= 62)
* >>> Math.uuid(8, 2) // 8 character ID (base=2)
* "01001010"
* >>> Math.uuid(8, 10) // 8 character ID (base=10)
* "47473046"
* >>> Math.uuid(8, 16) // 8 character ID (base=16)
* "098F4D35"
*/
(function() {
// Private array of chars to use
var CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
Math.uuid = function (len, radix) {
var chars = CHARS, uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
};
// A more performant, but slightly bulkier, RFC4122v4 solution. We boost performance
// by minimizing calls to random()
Math.uuidFast = function() {
var chars = CHARS, uuid = new Array(36), rnd=0, r;
for (var i = 0; i < 36; i++) {
if (i==8 || i==13 || i==18 || i==23) {
uuid[i] = '-';
} else if (i==14) {
uuid[i] = '4';
} else {
if (rnd <= 0x02) rnd = 0x2000000 + (Math.random()*0x1000000)|0;
r = rnd & 0xf;
rnd = rnd >> 4;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
return uuid.join('');
};
// A more compact, but less performant, RFC4122v4 solution:
Math.uuidCompact = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
})();

View File

@ -1,21 +0,0 @@
# OpenJsCad
Enthousiastic about OpenSCAD but somewhat frustrated by the limitations of its language, here is an alternative using Javascript. It's free, completely open source, still seriously under construction, based on Evan Wallace's CSG.js library and can be used from within the Chrome browser.
Launch your Chrome browser and go to: http://joostn.github.com/OpenJsCad/
The benefits over openSCAD are mainly in the language itself: you can use dynamic arrays for example, and solids can be stored in variables.
# Contribute
Contributions are welcome, fork me on GitHub!
The only content here is the 'GUI' part (i.e. index.html) in the gh-pages branch at https://github.com/joostn/OpenJsCad/tree/gh-pages
The actual CSG engine is in csg.js. Don't fork the copy of csg.js here, but instead create a fork of this branch:
https://github.com/joostn/csg.js
# License
Copyright (c) 2012 Joost Nieuwenhuijse (joost@newhouse.nl), under the [MIT license](http://www.opensource.org/licenses/mit-license.php).
Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the [MIT license](http://www.opensource.org/licenses/mit-license.php).

8
coffee-script.js Normal file

File diff suppressed because one or more lines are too long

4464
csg.js Normal file

File diff suppressed because it is too large Load Diff

187
gearsdemo.html Normal file
View File

@ -0,0 +1,187 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
margin: 0 auto;
padding: 10px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea {
height: 200px;
}
textarea:focus {
outline: none;
}
canvas { cursor: move; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
updateSolid();
}
function updateSolid()
{
gProcessor.setJsCad(document.getElementById('code').value);
}
</script>
<title>OpenJsCad demo: involute gears</title>
</head>
<body onload="onload()">
<h1>OpenJsCad demo: involute gears</h1>
<div id="viewer"></div>
<h2>Source code</h2>
Below is the OpenJsCad script for this demo. To build your own models, create a .jscad script
and use the <a href="processfile.html"><b>OpenJsCad parser</b></a>. For more information see the
<a href="index.html">OpenJsCad documentation</a>.
<br><br>
<textarea id="code">
// Here we define the user editable parameters:
function getParameterDefinitions() {
return [
{ name: 'numTeeth', caption: 'Number of teeth:', type: 'int', default: 10 },
{ name: 'circularPitch', caption: 'Circular pitch:', type: 'float', default: 5 },
{ name: 'pressureAngle', caption: 'Pressure angle:', type: 'float', default: 20 },
{ name: 'clearance', caption: 'Clearance:', type: 'float', default: 0 },
{ name: 'thickness', caption: 'Thickness:', type: 'float', default: 5 },
{ name: 'centerholeradius', caption: 'Radius of center hole (0 for no hole):', type: 'float', default: 2 },
];
}
// Main entry point; here we construct our solid:
function main(params)
{
var gear = involuteGear(
params.numTeeth,
params.circularPitch,
params.pressureAngle,
params.clearance,
params.thickness
);
if(params.centerholeradius > 0)
{
var centerhole = CSG.cylinder({start: [0,0,-params.thickness], end: [0,0,params.thickness], radius: params.centerholeradius, resolution: 16});
gear = gear.subtract(centerhole);
}
return gear;
}
/*
For gear terminology see:
http://www.astronomiainumbria.org/advanced_internet_files/meccanica/easyweb.easynet.co.uk/_chrish/geardata.htm
Algorithm based on:
http://www.cartertools.com/involute.html
circularPitch: The distance between adjacent teeth measured at the pitch circle
*/
function involuteGear(numTeeth, circularPitch, pressureAngle, clearance, thickness)
{
// default values:
if(arguments.length < 3) pressureAngle = 20;
if(arguments.length < 4) clearance = 0;
if(arguments.length < 4) thickness = 1;
var addendum = circularPitch / Math.PI;
var dedendum = addendum + clearance;
// radiuses of the 4 circles:
var pitchRadius = numTeeth * circularPitch / (2 * Math.PI);
var baseRadius = pitchRadius * Math.cos(Math.PI * pressureAngle / 180);
var outerRadius = pitchRadius + addendum;
var rootRadius = pitchRadius - dedendum;
var maxtanlength = Math.sqrt(outerRadius*outerRadius - baseRadius*baseRadius);
var maxangle = maxtanlength / baseRadius;
var tl_at_pitchcircle = Math.sqrt(pitchRadius*pitchRadius - baseRadius*baseRadius);
var angle_at_pitchcircle = tl_at_pitchcircle / baseRadius;
var diffangle = angle_at_pitchcircle - Math.atan(angle_at_pitchcircle);
var angularToothWidthAtBase = Math.PI / numTeeth + 2*diffangle;
// build a single 2d tooth in the 'points' array:
var resolution = 5;
var points = [new CSG.Vector2D(0,0)];
for(var i = 0; i <= resolution; i++)
{
// first side of the tooth:
var angle = maxangle * i / resolution;
var tanlength = angle * baseRadius;
var radvector = CSG.Vector2D.fromAngle(angle);
var tanvector = radvector.normal();
var p = radvector.times(baseRadius).plus(tanvector.times(tanlength));
points[i+1] = p;
// opposite side of the tooth:
radvector = CSG.Vector2D.fromAngle(angularToothWidthAtBase - angle);
tanvector = radvector.normal().negated();
p = radvector.times(baseRadius).plus(tanvector.times(tanlength));
points[2 * resolution + 2 - i] = p;
}
// create the polygon and extrude into 3D:
var tooth3d = new CSG.Polygon2D(points).extrude({offset: [0, 0, thickness]});
var allteeth = new CSG();
for(var i = 0; i < numTeeth; i++)
{
var angle = i*360/numTeeth;
var rotatedtooth = tooth3d.rotateZ(angle);
allteeth = allteeth.unionForNonIntersecting(rotatedtooth);
}
// build the root circle:
points = [];
var toothAngle = 2 * Math.PI / numTeeth;
var toothCenterAngle = 0.5 * angularToothWidthAtBase;
for(var i = 0; i < numTeeth; i++)
{
var angle = toothCenterAngle + i * toothAngle;
var p = CSG.Vector2D.fromAngle(angle).times(rootRadius);
points.push(p);
}
// create the polygon and extrude into 3D:
var rootcircle = new CSG.Polygon2D(points).extrude({offset: [0, 0, thickness]});
var result = rootcircle.union(allteeth);
// center at origin:
result = result.translate([0, 0, -thickness/2]);
return result;
}
</textarea><br>
<input type="submit" value="Update" onclick="updateSolid(); return false;">
<br><br>
</body>
</html>

338
grilledemo.html Normal file
View File

@ -0,0 +1,338 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
margin: 0 auto;
padding: 10px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea {
height: 200px;
}
textarea:focus {
outline: none;
}
canvas { cursor: move; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
updateSolid();
}
function updateSolid()
{
gProcessor.setJsCad(document.getElementById('code').value);
}
</script>
<title>OpenJsCad demo: Parametric Grille</title>
</head>
<body onload="onload()">
<h1>OpenJsCad demo: Parametric Grille</h1>
<div id="viewer"></div>
<h2>Source code</h2>
Below is the OpenJsCad script for this demo. To build your own models, create a .jscad script
and use the <a href="processfile.html"><b>OpenJsCad parser</b></a>. For more information see the
<a href="index.html">OpenJsCad documentation</a>.
<br><br>
<textarea id="code" style="height: 500px">
// Here we define the user editable parameters:
function getParameterDefinitions() {
return [
{ name: 'outerwidth', caption: 'Outer width of grille:', type: 'float', default: 190 },
{ name: 'outerheight', caption: 'Outer height of grille:', type: 'float', default: 120 },
{ name: 'outerdepth', caption: 'Outer depth of grille:', type: 'float', default: 12 },
{ name: 'thickness', caption: 'Wall thickness:', type: 'float', default: 2.5 },
{ name: 'innerdistance', caption: 'Inner standoff distance:', type: 'float', default: 2 },
{ name: 'bladescale', caption: 'Relative size of blades (1.0 is default):', type: 'float', default: 1 },
{ name: 'numdividers', caption: 'Number of vertical dividers:', type: 'int', default: 2 },
{
name: 'addlooseners',
type: 'choice',
caption: 'Add loops (for easy removal):',
values: [0, 1],
captions: ["No", "Yes"],
default: 1,
},
{
name: 'show',
type: 'choice',
caption: 'Show:',
values: ["all", "grille", "holders"],
captions: ["All", "Grille (for printing)", "Holders (for printing)"],
default: "all",
},
{
name: 'mouseears',
type: 'choice',
caption: 'Add mouse ears:',
values: [0, 1],
captions: ["No", "Yes"],
default: 1,
},
{
name: 'quality',
type: 'choice',
caption: 'Quality:',
values: [0, 1],
captions: ["Draft", "Final"],
default: 0,
},
];
}
function main(params)
{
var outerwidth = params.outerwidth;
var outerheight = params.outerheight;
var thickness = params.thickness;
var outerdepth = params.outerdepth;
var innerdistance = params.innerdistance;
var bladescale = params.bladescale;
var draft = params.quality == 0;
var marginleftright = 21;
var margintopbottom = 15;
var bladedistance = 12 * bladescale;
var frontroundradius = 5;
frontroundradius = Math.max(frontroundradius, thickness+0.2);
var outerroundradius = 3;
outerroundradius = Math.max(outerroundradius, thickness+0.2);
outerdepth = Math.max(outerdepth, outerroundradius);
outerdepth = Math.max(outerdepth, innerdistance+thickness);
var frontextend = innerdistance + bladescale*12 + thickness - outerdepth;
frontextend = Math.max(frontextend, 1.5);
var bladewidth = outerwidth - 2*marginleftright;
var bladesheight = outerheight - 2*margintopbottom;
var numblades = Math.ceil(bladesheight / bladedistance) + 1;
var topnotchsize = new CSG.Vector3D([20, 8, 3]);
var topnotchpos = new CSG.Vector3D([outerwidth/2-thickness - topnotchsize.x/2, outerheight/2-thickness - topnotchsize.y/2, topnotchsize.z/2]);
var bottomnotchsize = new CSG.Vector3D([12, 4, topnotchsize.z]);
var bottomnotchpos = new CSG.Vector3D([outerwidth/2-thickness - 4 - bottomnotchsize.x/2, -outerheight/2+thickness + bottomnotchsize.y/2, bottomnotchsize.z/2]);
var roundresolution = draft? 4 : 16;
var result = new CSG();
if(params.show != "holders")
{
// build the shell:
var z0plane = CSG.Plane.fromNormalAndPoint([0, 0, -1], [0, 0, 0]);
var outershell = CSG.roundedCube({center: [0,0,0], radius: [outerwidth/2, outerheight/2, outerdepth], roundradius: outerroundradius, resolution: roundresolution});
outershell = outershell.cutByPlane(z0plane);
var innershell = CSG.roundedCube({center: [0,0,0], radius: [outerwidth/2-thickness, outerheight/2-thickness, outerdepth-thickness], roundradius: outerroundradius-thickness, resolution: roundresolution});
innershell = innershell.cutByPlane(z0plane);
var shell = outershell.subtract(innershell);
var frontextendoutershell = CSG.roundedCube({center: [0,0,0], radius: [bladewidth/2+thickness, bladesheight/2+thickness, outerdepth+frontextend+frontroundradius+thickness], roundradius: frontroundradius, resolution: roundresolution});
var frontextendinnershell = CSG.roundedCube({center: [0,0,0], radius: [bladewidth/2, bladesheight/2, outerdepth+frontextend+frontroundradius+thickness+100], roundradius: frontroundradius-thickness, resolution: roundresolution});
frontextendinnershell = frontextendinnershell.cutByPlane(z0plane);
var plane3 = CSG.Plane.fromNormalAndPoint([0, 0, 1], [0, 0, outerdepth+frontextend+100]);
frontextendinnershell = frontextendinnershell.cutByPlane(plane3);
var plane1 = CSG.Plane.fromNormalAndPoint([0, 0, 1], [0, 0, outerdepth+frontextend]);
frontextendoutershell = frontextendoutershell.cutByPlane(plane1);
var plane2 = CSG.Plane.fromNormalAndPoint([0, 0, -1], [0, 0, innerdistance]);
frontextendoutershell = frontextendoutershell.cutByPlane(plane2);
shell = shell.subtract(frontextendinnershell);
var frontextendshell = frontextendoutershell.subtract(frontextendinnershell);
shell = shell.union(frontextendshell);
// build a blade:
var curvedpath = CSG.Path2D.arc({
center: [0,0,0],
radius: 15 * bladescale,
startangle: 20,
endangle: 80,
resolution: draft? 8:32,
});
var blade = curvedpath.rectangularExtrude(thickness, bladewidth, draft? 4:16, true);
var bladecenter = blade.getBounds()[0].plus(blade.getBounds()[1]).times(0.5);
blade = blade.translate(bladecenter.negated());
blade = blade.rotateY(-90);
blade = blade.translate([0, 0, -blade.getBounds()[0].z+innerdistance]);
var bladesize = blade.getBounds()[1].minus(blade.getBounds()[0]);
// add the blades to the shell:
var blades = new CSG();
for(var i = 0; i < numblades; i++)
{
var topy = bladesheight/2 + thickness - i*bladedistance;
var translatedblade = blade.translate([0, topy-bladesize.y/2, 0]);
blades = blades.union(translatedblade);
}
blades = blades.intersect(frontextendinnershell);
var grille = shell;
grille = shell.union(blades);
// add the dividers:
var dividers = new CSG();
if(params.numdividers > 0)
{
var w1 = (bladewidth - params.numdividers * thickness)/(params.numdividers+1);
for(var i = 0; i < params.numdividers; i++)
{
var x = -(params.numdividers-1)*(w1+thickness)/2 + i*(w1+thickness);
var z1 = outerdepth+frontextend;
var divider = CSG.cube({center: [x, 0, (z1+innerdistance)/2], radius: [thickness/2, bladesheight/2, (z1-innerdistance)/2]});
dividers = dividers.union(divider);
}
}
grille = grille.union(dividers);
// create the notches:
var notches = new CSG();
var topnotch1 = CSG.cube({center: topnotchpos, radius: topnotchsize.times(0.5) });
notches = notches.union(topnotch1);
var topnotch2 = CSG.cube({center: [-topnotchpos.x, topnotchpos.y, topnotchpos.z], radius: topnotchsize.times(0.5) });
notches = notches.union(topnotch2);
var bottomnotch1 = CSG.cube({center: bottomnotchpos, radius: bottomnotchsize.times(0.5) });
notches = notches.union(bottomnotch1);
var bottomnotch2 = CSG.cube({center: [-bottomnotchpos.x, bottomnotchpos.y, bottomnotchpos.z], radius: bottomnotchsize.times(0.5) });
notches = notches.union(bottomnotch2);
notches = notches.intersect(outershell);
grille = grille.union(notches);
result = result.union(grille);
// create the looseners:
if(params.addlooseners != 0)
{
var loosenerinnerwidth = 5;
var loosenerinnerheight = 2;
var loosenerdepth = 4;
var loosener = CSG.cube({center: [0, -outerheight/2 - loosenerinnerheight/2, loosenerdepth/2],
radius: [loosenerinnerwidth/2+thickness, loosenerinnerheight/2+thickness, loosenerdepth/2]});
loosener = loosener.subtract(CSG.cube({center: [0, -outerheight/2 - loosenerinnerheight/2, loosenerdepth/2],
radius: [loosenerinnerwidth/2, loosenerinnerheight/2, loosenerdepth/2]}));
var loosenerx = -outerwidth/2 + loosenerinnerwidth/2 + 5 + thickness;
var looseners = loosener.translate([loosenerx, 0, 0]);
looseners = looseners.union(loosener.translate([-loosenerx, 0, 0]));
result = result.union(looseners);
}
if(params.mouseears != 0)
{
for(var i = 0; i < 4; i++)
{
var xpos=outerwidth/2-10;
var ypos=outerheight/2;
if(i&1) xpos = -xpos;
if(i&2) ypos = -ypos;
var cylinder = CSG.cylinder({start: [xpos, ypos, 0], end: [xpos, ypos, 0.5], radius: 15});
result = result.union(cylinder);
}
for(var i = 0; i < 4; i++)
{
var xpos=bladewidth/2 + thickness/2;
var ypos=bladesheight/2 + thickness/2;
if(i&1) xpos = -xpos;
if(i&2) ypos = -ypos;
var cyl1 = CSG.cylinder({start: [xpos, ypos, 0], end: [xpos, ypos, 0.5], radius: 15});
var cyl2 = CSG.cylinder({start: [xpos, ypos, 0], end: [xpos, ypos, innerdistance], radius: 5});
result = result.union(cyl1.union(cyl2));
}
}
}
if(params.show != "grille")
{
// create the holders:
var holderyoffset = 0.5;
var holderzoffset = 1;
var holderwidth = 10;
var holderthickness = 3;
var holdertopclipheight = 4;
var holderbottomclipheight = 2;
var holderscrewholeradius = 2;
var holderscrewholedistance = 6;
var holderx;
if(params.show == "holders")
{
// just the holders:
holderx = holderwidth/2+2;
}
else
{
holderx = bottomnotchpos.x + bottomnotchsize.x/2 - holderwidth/2;
}
var holdery1 = topnotchpos.y - topnotchsize.y/2 - holderyoffset;
var holdery2 = bottomnotchpos.y + bottomnotchsize.y/2 + holderyoffset;
var holdery0 = holdery1+holdertopclipheight;
var holdery3 = holdery2-holderbottomclipheight;
var holder = CSG.cube({center: [0, (holdery1 + holdery2)/2 , holderthickness/2],
radius: [holderwidth/2, (holdery1-holdery2)/2, holderthickness/2]});
var d1 = 7;
var holdery1a = holdery1 - d1;
var holdery2a = holdery2 + d1;
var holderz1 = topnotchsize.z + holderzoffset;
var holderz2 = holderz1 + holderthickness;
holder = holder.union(CSG.cube({
center: [0, (holdery1a+holdery1)/2, holderz2/2],
radius: [holderwidth/2, (holdery1-holdery1a)/2, holderz2/2]
}));
holder = holder.union(CSG.cube({
center: [0, (holdery2a+holdery2)/2, holderz2/2],
radius: [holderwidth/2, (holdery2a-holdery2)/2, holderz2/2]
}));
holder = holder.union(CSG.cube({
center: [0, (holdery0+holdery1a)/2, (holderz2+holderz1)/2],
radius: [holderwidth/2, (holdery0-holdery1a)/2, (holderz2-holderz1)/2]
}));
holder = holder.union(CSG.cube({
center: [0, (holdery2a+holdery3)/2, (holderz2+holderz1)/2],
radius: [holderwidth/2, (holdery2a-holdery3)/2, (holderz2-holderz1)/2]
}));
var screwhole = CSG.cylinder({start: [0,0,0], end: [0, 0, holderthickness], radius: holderscrewholeradius, resolution: 16});
holder = holder.subtract(screwhole.translate([0, holdery1a-holderscrewholedistance, 0]));
holder = holder.subtract(screwhole.translate([0, holdery2a+holderscrewholedistance, 0]));
holder = holder.setColor(0, 1, 0);
var holders = holder.translate([holderx,0,0]);
holders = holders.union(holder.translate([-holderx,0,0]));
if(params.show == "holders")
{
holders = holders.rotateZ(90);
}
result = result.union(holders);
}
result = result.rotateZ(90);
return result;
}
</textarea><br>
<input type="submit" value="Update" onclick="updateSolid(); return false;">
<br><br>
</body>
</html>

210
hookdemo.html Normal file
View File

@ -0,0 +1,210 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
margin: 0 auto;
padding: 10px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea {
height: 200px;
}
textarea:focus {
outline: none;
}
canvas { cursor: move; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
updateSolid();
}
function updateSolid()
{
gProcessor.setJsCad(document.getElementById('code').value);
}
</script>
<title>OpenJsCad demo: Parametric S hook</title>
</head>
<body onload="onload()">
<h1>OpenJsCad demo: Parametric S hook</h1>
<div id="viewer"></div>
<h2>Source code</h2>
Below is the OpenJsCad script for this demo. To build your own models, create a .jscad script
and use the <a href="processfile.html"><b>OpenJsCad parser</b></a>. For more information see the
<a href="index.html">OpenJsCad documentation</a>.
<br><br>
<textarea id="code">
// Here we define the user editable parameters:
function getParameterDefinitions() {
return [
{ name: 'topdiameter', caption: 'Inner diameter of top hook:', type: 'float', default: 16.7 },
{ name: 'clampfactor', caption: 'Snugness of top hook (0 - 100):', type: 'float', default: 25 },
{ name: 'cliplength', caption: 'Top hook clip length:', type: 'float', default: 5 },
{ name: 'bottomdiameter', caption: 'Inner diameter of bottom hook:', type: 'float', default: 20 },
{ name: 'height', caption: 'Outer height of the hook:', type: 'float', default: 60 },
{ name: 'thickness', caption: 'Thickness:', type: 'float', default: 5 },
{ name: 'width', caption: 'Width:', type: 'float', default: 7 },
{
name: 'rounded',
type: 'choice',
caption: 'Rounded edges',
values: [0, 1],
captions: ["No", "Yes (rendering will take a long time!)"],
default: 0,
},
{ name: 'roundness', caption: 'Diameter of rounded edges (if enabled):', type: 'float', default: 1.5 },
{ name: 'buildwidth', caption: 'Width (x) of build area (to print multiple copies):', type: 'float', default: 90 },
{ name: 'builddepth', caption: 'Depth (y) of build area (to print multiple copies):', type: 'float', default: 90 },
];
}
function main(params) {
if(OpenJsCad.log) OpenJsCad.log("start");
var pathresolution = 16;
var expandresolution = 6;
// construct the 2D path:
var topradius = params.topdiameter/2;
var bottomradius = params.bottomdiameter/2;
var halfthickness = params.thickness/2;
topradius += halfthickness;
bottomradius += halfthickness;
var roundness = params.roundness;
if(params.rounded == 0)
{
roundness = 0;
}
roundness = Math.min(roundness, halfthickness-0.1, params.width/2-0.1);
if(roundness < 0) roundness = 0;
var clampfactor = params.clampfactor / 100;
if(clampfactor < 0) clampfactor = 0;
if(clampfactor >= 1) clampfactor = 1;
clampfactor *= (topradius-halfthickness)/topradius;
var topstartangle = - 180 * Math.acos(1 - 2*clampfactor) / Math.PI;
var tophookcenter = new CSG.Vector2D(topradius, 0);
var tophookstart = tophookcenter.plus(CSG.Vector2D.fromAngleDegrees(topstartangle).times(topradius));
var circledistance = params.height - topradius - bottomradius - 2 * params.thickness;
if(circledistance < 0) circledistance = 0;
var bottomhookcenter = new CSG.Vector2D(-bottomradius, -circledistance);
var gravityangle = 90 - tophookcenter.minus(bottomhookcenter).angleDegrees();
var path = new CSG.Path2D();
// top hook curve:
if(params.cliplength > 0)
{
var clipstart = new CSG.Vector2D([0, -1]).times(params.cliplength).plus(tophookstart);
path = path.appendPoint(clipstart);
}
var topcurvepath = CSG.Path2D.arc({
center: tophookcenter,
radius: topradius,
startangle: topstartangle,
endangle: 180,
resolution: pathresolution,
maketangent: true,
});
path = path.concat(topcurvepath);
// straight middle part:
if(circledistance > 0)
{
path = path.appendPoint([0, -circledistance]);
}
// bottom hook curve:
var bottomcurvepath = CSG.Path2D.arc({
center: bottomhookcenter,
radius: bottomradius,
startangle: 0,
endangle: -180-gravityangle,
resolution: pathresolution,
maketangent: true,
});
path = path.concat(bottomcurvepath);
// center around origin, and rotate as it would hang under gravity:
var centerpoint = tophookcenter.plus(bottomhookcenter).times(0.5);
var matrix = CSG.Matrix4x4.translation(centerpoint.negated().toVector3D(0));
matrix = matrix.multiply(CSG.Matrix4x4.rotationZ(gravityangle));
path = path.transform(matrix);
// extrude the path to create a 3D solid
var hook = path.rectangularExtrude(2*(halfthickness-roundness), params.width-2*roundness, pathresolution, true);
hook = hook.translate([0, 0, -params.width/2+roundness]);
// expand to create rounded corners:
if(roundness > 0)
{
hook = hook.expand(roundness, expandresolution);
}
// hook = hook.toPointCloud(0.1);
// determine how many objects will fit in the build area:
var bounds = hook.getBounds();
var objsize = bounds[1].minus(bounds[0]);
var margin = 5; // distance between the copies
var numx = Math.floor((params.buildwidth + margin) / (objsize.x + margin));
var numy = Math.floor((params.builddepth + margin) / (objsize.y + margin));
if(numx < 1) numx = 1;
if(numy < 1) numy = 1;
// and make the copies:
var result = new CSG();
for(var x = 0; x < numx; x++)
{
var deltax = ((1-numx)/2+x) * (objsize.x + margin);
var colresult = new CSG();
for(var y = 0; y < numy; y++)
{
var deltay = ((1-numy)/2+y) * (objsize.y + margin);
var translated = hook.translate(new CSG.Vector3D(deltax, deltay, 0));
colresult = colresult.union(translated);
}
result = result.union(colresult);
}
if(OpenJsCad.log) OpenJsCad.log("finish");
return result;
}
</textarea><br>
<input type="submit" value="Update" onclick="updateSolid(); return false;">
<br><br>
</body>
</html>

709
index.html Normal file
View File

@ -0,0 +1,709 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
margin: 0 auto;
padding: 10px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea {
height: 200px;
}
textarea:focus {
outline: none;
}
canvas { cursor: move; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
updateSolid();
}
function updateSolid()
{
gProcessor.setJsCad(document.getElementById('code').value);
}
</script>
<title>OpenJsCad</title>
</head>
<body onload="onload()">
<h1>OpenJsCad</h1>
Create an STL file for 3D printing using constructive solid modeling in Javascript.
<div id="viewer"></div>
<h2>Playground</h2>
Try it by entering some code below. Anything you enter will be lost as soon as this page is reloaded;
to build your own models you should instead store them in a .jscad file on your computer
and use the <a href="processfile.html"><b>OpenJsCad parser</b></a>.
<br><br>
<textarea id="code">
function main()
{
var resolution = 24; // increase to get smoother corners (will get slow!)
var cube1 = CSG.roundedCube({center: [0,0,0], radius: [10,10,10], roundradius: 2, resolution: resolution});
var sphere1 = CSG.sphere({center: [5, 5, 5], radius: 10, resolution: resolution });
var sphere2 = sphere1.translate([12, 5, 0]);
var sphere3 = CSG.sphere({center: [20, 0, 0], radius: 30, resolution: resolution });
var result = cube1;
result = result.union(sphere1);
result = result.subtract(sphere2);
result = result.intersect(sphere3);
return result;
}
</textarea><br>
<input type="submit" value="Update" onclick="updateSolid(); return false;">
<br>
<h2>About</h2>
This is intended to become a Javascript based alternative to <a href="http://www.openscad.org/">OpenSCAD</a>,
for 3D solid modeling.
CSG model is contructed using Javascript. For example:<br>
<pre>function main() {
var cube = CSG.cube();
return cube;
}</pre>
creates a cube with a radius of 1 and centered at the origin.
The code should always contain a main() function, returning a CSG solid.
<br><br>
To build your own models, create a .jscad file with your javascript code and parse the file using the
<a href="processfile.html">OpenJsCad parser</a>. When finished, click on Generate STL and save the result
in an .stl file, ready to be printed on your 3d printer.
<h2>Why use OpenJsCad?</h2>
Some of the benefits:
<ul>
<li>Runs in your browser, no need to install any software.</li>
<li>You can create parametric models with user editable parameters: parameters can be changed in the browser window,
without the need to edit the source script. See the <a href="gearsdemo.html">Gears demo</a> for example!</li>
<li>JavaScript is an extremely flexible language, supporting dynamic arrays, object oriented programming, closures, anonymous functions and more</li>
<li>Solids are stored in variables. This allows for example conditional cloning of objects, something which is nearly impossible in OpenSCAD.</li>
<li>Properties and Connectors (see below) make it very easy to attach objects to each other at predetermined
points, even if you don't know the actual orientation or size.</li>
<li>Extensive built in <a href="#math">support for 2D and 3D math</a> (classes for Vector2D, Vector3D, Plane, Line3D, Line2D)</li>
<li>Debugging support: step through your code, set breakpoints, inspect variables, etc. See the
<a href="processfile.html">OpenJsCad parser</a> for details.</li>
</ul>
<h2>Viewer navigation</h2>
Click and drag to rotate the model around the origin.<br>
Shift+Drag moves the model around.<br>
Alt+drag zooms (by changing the distance between camera and model).
<h2>Demos</h2>
<ul>
<li><a href="gearsdemo.html">Involute Gears</a></li>
<li><a href="hookdemo.html">Parametric S hook</a>: shows how to extrude 2d paths</li>
<li><a href="servodemo.html">Servo motor</a>: shows how to work with properties and connectors</li>
<li><a href="grilledemo.html">Parametric Grille</a></li>
</ul>
<h2>License</h2>
Copyright (c) 2012 Joost Nieuwenhuijse.
Uses CSG.js, <a href="https://github.com/evanw/csg.js">original</a> copyright (c) 2011 Evan Wallace,
<a href="https://github.com/joostn/csg.js">extensively modified</a>, copyright (c) 2012 Joost Nieuwenhuijse.
Uses <a href="https://github.com/evanw/lightgl.js">lightgl.js</a> by Evan Wallace for WebGL rendering.
All code released under MIT license.
<br><br>
Contributions are welcome! It's all written in Javascript, so if you know how to use it you
know how to modify it as well.<br><br>
To contribute go to <a href="https://github.com/joostn/OpenJsCad/tree/gh-pages">OpenJsCad at GitHub (gh-pages tree)</a>,
<a href="http://help.github.com/fork-a-repo/">create your own fork</a> and
<a href="http://help.github.com/send-pull-requests/">send me a pull request</a>.
<h2>Primitive solids</h2>
Currently the following solids are supported. The parameters are passed in an object; most
parameters are optional. 3D vectors can be passed in an array. If a scalar is passed
for a parameter which expects a 3D vector, it is used for the x, y and z value.
In other words: <code>radius: 1</code> will give <code>radius: [1,1,1]</code>.
<br><br>
All rounded solids have a 'resolution' parameter which controls tesselation. If resolution
is set to 8, then 8 polygons per 360 degree of revolution are used. Beware that rendering
time will increase dramatically when increasing the resolution. For a sphere the number of polygons
increases quadratically with the resolution used.
<br><br>
<pre>
// a cube:
var cube = CSG.cube({
center: [0, 0, 0],
radius: [1, 1, 1]
});
// a sphere:
var sphere = CSG.sphere({
center: [0, 0, 0],
radius: 2, // must be scalar
resolution: 32
});
// a cylinder:
var cylinder = CSG.cylinder({
start: [0, -1, 0],
end: [0, 1, 0],
radius: 1,
resolution: 16
});
// like a cylinder, but with spherical endpoints:
var roundedCylinder = CSG.roundedCylinder({
start: [0, -1, 0],
end: [0, 1, 0],
radius: 1,
resolution: 16
});
// a rounded cube:
var cube = CSG.roundedCube({
center: [0, 0, 0],
radius: 1,
roundradius: 0.2,
resolution: 8,
});
</pre>
<h2>CSG operations</h2>
The 3 standard CSG operations are supported. All CSG operations return a new solid; the source solids
are not modified:
<pre>
var csg1 = cube.union(sphere);
var csg2 = cube.intersect(sphere);
var csg3 = cube.subtract(sphere);
</pre>
<h2>Transformations</h2>
Solids can be translated, scaled and rotated. Multiple transforms can be combined into a single matrix transform.
For <a href="#math">matrix and vector math see below</a>.
<pre>
var cube = CSG.cube();
// translation:
var cube2 = cube.translate([1, 2, 3]);
// scaling:
var largecube = cube.scale(2.0);
var stretchedcube = cube.scale([1.5, 1, 0.5]);
// rotation:
var rotated1 = cube.rotateX(-45); // rotate around the X axis
var rotated2 = cube.rotateY(90); // rotate around the Y axis
var rotated3 = cube.rotateZ(20); // rotate around the Z axis
// combine multiple transforms into a single matrix transform:
var m = new CSG.Matrix4x4();
m = m.multiply(CSG.Matrix4x4.rotationX(40));
m = m.multiply(CSG.Matrix4x4.rotationZ(40));
m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]));
m = m.multiply(CSG.Matrix4x4.scaling([1.1, 1.2, 1.3]));
// and apply the transform:
var cube3 = cube.transform(m);
</pre>
<h2>Mirroring</h2>
Solids can be mirrored in any plane in 3D space. For <a href="#math">plane math see below</a>.
<pre>
var cube = CSG.cube().translate([1,0,0]);
var cube2 = cube.mirroredX(); // mirrored in the x=0 plane
var cube3 = cube.mirroredY(); // mirrored in the y=0 plane
var cube4 = cube.mirroredZ(); // mirrored in the z=0 plane
// create a plane by specifying 3 points:
var plane = CSG.Plane.fromPoints([5,0,0], [5, 1, 0], [3, 1, 7]);
// and mirror in that plane:
var cube5 = cube.mirrored(plane);
</pre>
<h2>Cutting by a plane</h2>
A solid can be cut by a plane; only the part on the back side is kept:
<pre>
var cube = CSG.cube({radius: 10});
// create a plane by specifying 3 points:
var plane1 = CSG.Plane.fromPoints([5,0,0], [7, 1, 0], [3, 1, 7]);
// or by specifying a normal and a point on the plane:
var plane2 = CSG.Plane.fromNormalAndPoint([3, 1, 2], [5, 0, 0]);
// and cut by the plane:
var part1 = cube.cutByPlane(plane2);
// or if we need the other half of the cube:
var part2 = cube.cutByPlane(plane2.flipped());
</pre>
<h2>Expansion and contraction</h2>
Expansion can be seen
as the 3D convolution of an object with a sphere. Contraction is the reverse: the area outside the solid
is expanded, and this is then subtracted from the solid.
<br><br>
Expansion and contraction are very powerful ways to get an object with nice smooth corners. For example
a rounded cube can be created by expanding a normal cube.
<br><br>
Note that these are expensive operations: spheroids are created around every corner and edge in the original
object, so the number of polygons quickly increases. Expansion and contraction therefore are only practical for simple
non-curved objects.
<br><br>
expand() and contract() take two parameters: the first is the radius of expansion or contraction; the second
parameter is optional and specififies the resolution (number of polygons on spherical surfaces, per 360 degree revolution).
<pre>
var cube1 = CSG.cube({radius: 1.0});
var cube2 = CSG.cube({radius: 1.0}).translate([-0.3, -0.3, -0.3]);
var csg = cube1.subtract(cube2);
var rounded = csg.expand(0.2, 8);
</pre>
<h2>Using Properties</h2>
The 'property' property of a solid can be used to store metadata for the object,
for example the coordinate of a specific point of interest of the solid. Whenever
the object is transformed (i.e. rotated, scaled or translated), the properties
are transformed with it. So the property will keep pointing to the same point
of interest even after several transformations have been applied to the solid.
<br><br>
Properties can have any type, but only the properties of classes supporting
a 'transform' method will actually be transformed. This includes CSG.Vector3D,
CSG.Plane and CSG.Connector. In particular CSG.Connector properties (see below)
can be very useful: these can
be used to attach a solid to another solid at a predetermined location regardless of the
current orientation.
<br><br>
It's even possible to include a CSG solid as a property of another solid. This could
be used for example
to define the cutout cylinders to create matching screw holes for an object. Those 'solid properties'
get the same transformations as the owning solid but they will not be visible in the result
of CSG operations such as union().
<br><br>
Other kind of properties (for
example, strings) will still be included in the properties of the transformed
solid, but the properties will not get any transformation when the owning solid is transformed. <br><br>
All primitive solids have some predefined properties, such as the center point
of a sphere (TODO: document).
<br><br>
The solid resulting from CSG operations (union(), subtract(), intersect()) will get
the merged properties of both source solids. If identically named properties exist, only
one of them will be kept.
<pre>
var cube = CSG.cube({radius: 1.0});
cube.properties.aCorner = new CSG.Vector3D([1, 1, 1]);
cube = cube.translate([5, 0, 0]);
cube = cube.scale(2);
// cube.properties.aCorner will now point to [12, 2, 2],
// which is still the same corner point
// Properties can be stored in arrays; all properties in the array
// will be transformed if the solid is transformed:
cube.properties.otherCorners = [
new CSG.Vector3D([-1, 1, 1]),
new CSG.Vector3D([-1, -1, 1])
];
// and we can create sub-property objects; these must be of the
// CSG.Properties class. All sub properties will be transformed with
// the solid:
cube.properties.myProperties = new CSG.Properties();
cube.properties.myProperties.someProperty = new CSG.Vector3D([-1, -1, -1]);
</pre>
For an example see the <a href="servodemo.html">Servo motor</a> demo.
<h2>Connectors</h2>
The CSG.Connector class is intended to facilitate
attaching two solids to each other at a predetermined
location and orientation.
For example suppose we have a CSG solid depicting a servo motor
and a solid of a servo arm: by defining a Connector property for each of them, we
can easily attach the servo arm to the servo motor at the correct position
(i.e. the motor shaft) and orientation (i.e. arm perpendicular to the shaft)
even if we don't know their current position and orientation
in 3D space.<br><br>
In other words Connector give us the freedom to rotate and translate objects at will without the need
to keep track of their positions and boundaries. And if a third party library exposes connectors for
its solids, the user of the library does not have to know the actual dimensions or
shapes, only the names of the connector properties.
<br><br>
A CSG.Connector consist of 3 properties:<br>
<b>point</b>: a CSG.Vector3D defining the connection point in 3D space<br>
<b>axis</b>: a CSG.Vector3D defining the direction vector of the connection
(in the case of the servo motor example it would point in the direction of the shaft)<br>
<b>normal</b>: a CSG.Vector3D direction vector somewhat perpendicular to axis; this
defines the &quot;12 o'clock&quot; orientation of the connection.
<br><br>
When connecting two connectors, the solid is transformed such that the <b>point</b>
properties will be identical, the <b>axis</b> properties will have the same direction
(or opposite direction if mirror == true), and the <b>normal</b>s match as much as possible.
<br><br>
Connectors can be connected by means of two methods:<br>
A CSG solid's <b>connectTo()</b> function transforms a solid such that two connectors
become connected.<br>
Alternatively we can use a connector's <b>getTransformationTo()</b> method to obtain
a transformation matrix which would connect the connectors. This can be used if we
need to apply the same transform to multiple solids.
<pre>
var cube1 = CSG.cube({radius: 10});
var cube2 = CSG.cube({radius: 4});
// define a connector on the center of one face of cube1
// The connector's axis points outwards and its normal points
// towards the positive z axis:
cube1.properties.myConnector = new CSG.Connector([10, 0, 0], [1, 0, 0], [0, 0, 1]);
// define a similar connector for cube 2:
cube2.properties.myConnector = new CSG.Connector([0, -4, 0], [0, -1, 0], [0, 0, 1]);
// do some random transformations on cube 1:
cube1 = cube1.rotateX(30);
cube1 = cube1.translate([3.1, 2, 0]);
// Now attach cube2 to cube 1:
cube2 = cube2.connectTo(
cube2.properties.myConnector,
cube1.properties.myConnector,
true, // mirror
0 // normalrotation
);
// Or alternatively:
var matrix = cube2.properties.myConnector.getTransformationTo(
cube1.properties.myConnector,
true, // mirror
0 // normalrotation
);
cube2 = cube2.transform(matrix);
var result = cube2.union(cube1);
</pre>
For a more complete example see the <a href="servodemo.html">Servo motor</a> demo.
<h2>Determining the bounds of an object</h2>
The getBounds() function can be used to retrieve the bounding box of an object.
getBounds() returns
an array with two elements specifying the minimum x,y,z coordinate and the maximum x,y,z coordinate:
<pre>
var cube1 = CSG.cube({radius: 10});
var cube2 = CSG.cube({radius: 5});
// get the right bound of cube1 and the left bound of cube2:
var deltax = cube1.getBounds()[1].x - cube2.getBounds()[0].x;
// align cube2 so it touches cube1:
cube2 = cube2.translate([deltax, 0, 0]);
return cube1.union(cube2);
</pre>
<h2>2D shapes</h2>
Two dimensional shapes can be defined through the Polygon2D class. Currently this requires the polygon to be convex
(i.e. all corners less than 180 degrees). Shapes can be transformed (rotation, translation, scaling).
To actually use the shape it needs to be extruded into a 3D CSG object through the extrude() function. extrude()
places the 2D solid onto the z=0 plane, and extrudes in the specified direction. Extrusion can be done with an optional
twist. This rotates the solid around the z axis (and not necessariy around the extrusion axis) during extrusion.
The total degrees of rotation is specified in the twistangle parameter, and twiststeps determine the number of steps
between the bottom and top surface.
<pre>
// Create a shape; argument is an array of 2D coordinates
// The shape must be convex, can be specified in clockwise or in counterclockwise direction
var shape2d=new CSG.Polygon2D([[0,0], [5,0], [3,5], [0,5]]);
// Do some transformations:
shape2d=shape2d.translate([-2, -2]);
shape2d=shape2d.rotate(20);
shape2d=shape2d.scale([0.2, 0.2]);
// And extrude. This creates a CSG solid:
var extruded=shape2d.extrude({
offset: [0.5, 0, 2], // direction for extrusion
twistangle: 180, // top surface is rotated 180 degrees
twiststeps: 100 // create 100 slices
});
</pre>
For an example of 2D shapes see the <a href="hookdemo.html">Parametric S hook</a> demo.
<h2>2D Paths</h2>
A path is simply a series of points, connected by lines. A path can be open or closed (an additional line
is drawn between the first and last point). 2D paths are supported through the CSG.Path2D class.
<br><br>
Paths can be contructed either by giving a series of 2D coordinates, or through the CSG.Path2D.arc() function,
which creates a circular curved path. Paths can be concatenated, the result is a new path.
<br><br>
Creating a 3D solid is currently supported by the rectangularExtrude() function. This creates a 3D shape
by following the path with a 2D rectangle (upright, perpendicular to the path direction).
<pre>
var path = new CSG.Path2D([[10,10], [-10,10]], /* closed = */ false);
var anotherpath = new CSG.Path2D([[-10,-10]]);
path = path.concat(anotherpath);
path = path.appendPoint([10,-10]);
path = path.close(); // close the path
// of course we could simply have done:
// var path = new CSG.Path2D([[10,10], [-10,10], [-10,-10], [10,-10]], /* closed = */ true);
// We can make arcs and circles:
var curvedpath = CSG.Path2D.arc({
center: [0,0,0],
radius: 10,
startangle: 0,
endangle: 180,
resolution: 16,
});
// Extrude the path by following it with a rectangle (upright, perpendicular to the path direction)
// Returns a CSG solid
// width: width of the extrusion, in the z=0 plane
// height: height of the extrusion in the z direction
// resolution: number of segments per 360 degrees for the curve in a corner
// roundEnds: if true, the ends of the polygon will be rounded, otherwise they will be flat
var csg = path.rectangularExtrude(3, 4, 16, true);
return csg;
</pre>
<h2>Interactive parametric models</h2>
It is possible to make certain parameters
editable in the browser. This allows users not familiar with JavaScript to create customized STL files.
<br><br>
To do so, add a function getParameterDefinitions() to your .jscad source. This function should return
an array with parameter definitions. Currently 4 parameters types are supported: float, int, text and choice.
The user edited values of the parameters will be supplied as an object parameter to the main() function of your .jscad file.
<br><br>
A float, int or text parameter is created by including the following object in the array returned by getParameterDefinitions():
<pre>{
name: 'width',
type: 'float', // or 'text' or 'int'
default: 1.23, // optional, sets the initial value
caption: 'Width of the thingy:', // optional, displayed left of the input field
// if omitted, the 'name' is displayed (i.e. 'width')
}</pre>
A 'choice' parameter is created using the following object:
<pre>{
name: 'shape',
type: 'choice',
values: ["TRI", "SQU", "CIR"], // these are the values that will be supplied to your script
captions: ["Triangle", "Square", "Circle"], // optional, these values are shown in the listbox
// if omitted, the items in the 'values' array are used
caption: 'Shape:', // optional, displayed left of the input field
default: "SQU", // optional, default selected value
// if omitted, the first item is selected by default
}</pre>
To use the values add an argument to your main() function. This argument will be supplied an object
with the user edited parameter values:
<pre>
function main(params)
{
// custom error checking:
if(params.width <= 0) throw new Error("Width should be positive!");
if(params.shape == "TRI")
{
// do something
}
}
</pre>
A complete example. Copy/paste it into the Playground at the top of this page to see how it works:
<pre>
function getParameterDefinitions() {
return [
{
name: 'width',
type: 'float',
default: 10,
caption: "Width of the cube:",
},
{
name: 'height',
type: 'float',
default: 14,
caption: "Height of the cube:",
},
{
name: 'depth',
type: 'float',
default: 7,
caption: "Depth of the cube:",
},
{
name: 'rounded',
type: 'choice',
caption: 'Round the corners?',
values: [0, 1],
captions: ["No thanks", "Yes please"],
default: 1,
},
];
}
function main(params) {
var result;
if(params.rounded == 1)
{
result = CSG.roundedCube({radius: [params.width, params.height, params.depth], roundradius: 2, resolution: 32});
}
else
{
result = CSG.cube({radius: [params.width, params.height, params.depth]});
}
return result;
}
</pre>
Or see the <a href="gearsdemo.html">Gears demo</a> for another example of interactive parameters.
<h2>Miscellaneous</h2>
Solids can be given a color using the setColor(r, g, b) function. Beware: this returns a new solid,
the original solid is not modified! Faces of the solid will keep their original color when doing
CSG operations (union() etc). Colors values range between 0.0 and 1.0 (not 255).
<pre>
var cube1 = CSG.cube({radius: 10});
cube1 = cube1.setColor(0.5, 0, 0);
var cube2 = CSG.cube({radius: 10});
cube2 = cube2.setColor(0, 0.5, 0);
cube2 = cube2.translate([5,1,4]);
var result = cube1.subtract(cube2);
// the resulting solid will have faces with 2 different colors
</pre>
<h2><a name="math"></a>2D and 3D Math</h2>
There are utility classes for many 2D and 3D operations. Below is a quick summary, for details
view the source of csg.js:
<pre>
// --------- Vector3D ---------------------
var vec1 = new CSG.Vector3D(1,2,3); // 3 arguments
var vec2 = new CSG.Vector3D( [1,2,3] ); // 1 array argument
var vec3 = new CSG.Vector3D(vec2); // cloning a vector
// get the values as: vec1.x, vec.y, vec1.z
// vector math. All operations return a new vector, the original is unmodified!
// vectors cannot be modified. Instead you should create a new vector.
vec.negated()
vec.abs()
vec.plus(othervector)
vec.minus(othervector)
vec.times(3.0)
vec.dividedBy(-5)
vec.dot(othervector)
vec.lerp(othervector, t) // linear interpolation (0 <= t <= 1)
vec.length()
vec.lengthSquared() // == vec.length()^2
vec.unit()
vec.cross(othervector) // cross product: returns a vector perpendicular to both
vec.distanceTo(othervector)
vec.distanceToSquared(othervector) // == vec.distanceTo(othervector)^2
vec.equals(othervector)
vec.multiply4x4(matrix4x4) // right multiply by a 4x4 matrix
vec.min(othervector) // returns a new vector with the minimum x,y and z values
vec.max(othervector) // returns a new vector with the maximum x,y and z values
// --------- Vector2D ---------------------
var vec1 = new CSG.Vector2D(1,2); // 2 arguments
var vec2 = new CSG.Vector2D( [1,2] ); // 1 array argument
var vec3 = new CSG.Vector2D(vec2); // cloning a vector
// vector math. All operations return a new vector, the original is unmodified!
vec.negated()
vec.abs()
vec.plus(othervector)
vec.minus(othervector)
vec.times(3.0)
vec.dividedBy(-5)
vec.dot(othervector)
vec.lerp(othervector, t) // linear interpolation (0 <= t <= 1)
vec.length()
vec.unit()
vec.normal() // returns a 90 degree clockwise rotated vector
vec.distanceTo(othervector)
vec.equals(othervector)
vec.multiply4x4(matrix4x4) // right multiply by a 4x4 matrix
vec.toVector3D(z) // convert to a vector3D by adding a z coordinate
vec.angleDegrees() // returns the angle of the vector: [1,0] = 0 degrees, [0, 1] = 90 degrees, etc
vec.angleRadians() // ditto in radians
var vec = CSG.Vector2D.fromAngleDegrees(degrees); // returns a vector at the specified angle
var vec = CSG.Vector2D.fromAngleRadians(radians); // returns a vector at the specified angle
// --------- Matrix4x4 ---------------------
var m1 = new CSG.Matrix4x4(); // unity matrix
var m2 = new CSG.Matrix4x4( [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] );
// elements are passed in row order
var result = m1.plus(m2);
var result = m1.minus(m2);
var result = m1.multiply(m2);
// matrix vector multiplication (vectors are padded with zeroes to get a 4x1 vector):
var vec3d = m1.rightMultiply1x3Vector(vec3d); // matrix * vector
var vec3d = m1.leftMultiply1x3Vector(vec3d); // vector * matrix
var vec2d = m1.rightMultiply1x2Vector(vec2d); // matrix * vector
var vec2d = m1.leftMultiply1x2Vector(vec2d); // vector * matrix
// common transformation matrices:
var m = CSG.Matrix4x4.rotationX(degrees); // matrix for rotation about X axis
var m = CSG.Matrix4x4.rotationY(degrees); // matrix for rotation about Y axis
var m = CSG.Matrix4x4.rotationZ(degrees); // matrix for rotation about Z axis
var m = CSG.Matrix4x4.rotation(rotationCenter, rotationAxis, degrees); // rotation about arbitrary point and axis
var m = CSG.Matrix4x4.translation(vec3d); // translation
var m = CSG.Matrix4x4.scaling(vec3d); // scale
var m = CSG.Matrix4x4.mirroring(plane); // mirroring in a plane; the argument must be a CSG.Plane
// matrix transformations can be concatenated:
var transform = CSG.Matrix4x4.rotationX(20).multiply(CSG.Matrix4x4.rotationY(30));
// Use a CSG solid's transform() method to apply the transformation to a CSG solid
// ------------ Plane --------------------------
// a 3D plane is represented by a normal vector (should have unit length) and a distance from the origin w
// the plane passes through normal.times(w)
var plane1 = new CSG.Plane(normal, w);
// Or we can construct a plane from 3 points:
var plane2 = CSG.Plane.fromPoints(p1, p2, p3);
// Or from a normal vector and 1 point:
var plane3 = CSG.Plane.fromNormalAndPoint(normal, point);
// Flip a plane (front side becomes back side):
var plane4 = plane3.flipped();
// Apply transformations (rotation, scaling, translation):
var transformed = plane3.transformed(matrix4x4); // argument is a CSG.Matrix4x4
// Intersection of plane and 3d line:
var point = plane3.intersectWithLine(line); // argument is CSG.Line3D, returns a CSG.Vector3D
// Intersection of 2 planes:
var line = plane3.intersectWithPlane(plane); // argument is another CSG.Plane, returns a CSG.Line3D
// Distance to point:
var w = signedDistanceToPoint(point); // argument is CSG.Vector3D, returns a float (positive
// if in front of plane, negative if in back)
// ------------ Line3D --------------------------
// A line in 3d space is represented by a point and a direction vector.
// Direction should be a unit vector. Point can be any point on the line:
var line = new CSG.Line3D(point, direction); // argumenst are CSG.Vector3D
// or by giving two points:
var line = CSG.Line3D.fromPoints(p1, p2); // argumenst are CSG.Vector3D
var point = intersectWithPlane(plane); // == plane.intersectWithLine(this)
var line2 = line.reverse(); // same line but reverse direction
var line2 = line.transform(matrix4x4); // for rotation, scaling, etc
var p = line.closestPointOnLine(point); // project point onto the line
var d = line.distanceToPoint(point);
</pre>
</body>
</html>

4
jquery-1.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
jquery.js vendored Symbolic link
View File

@ -0,0 +1 @@
jquery-1.7.1.min.js

61
lightgl.js Normal file
View File

@ -0,0 +1,61 @@
/*
* lightgl.js
* http://github.com/evanw/lightgl.js/
*
* Copyright 2011 Evan Wallace
* Released under the MIT license
*/
var GL=function(){function I(){d.MODELVIEW=C|1;d.PROJECTION=C|2;var b=new n,c=new n;d.modelviewMatrix=new n;d.projectionMatrix=new n;var a=[],f=[],g,j;d.matrixMode=function(h){switch(h){case d.MODELVIEW:g="modelviewMatrix";j=a;break;case d.PROJECTION:g="projectionMatrix";j=f;break;default:throw"invalid matrix mode "+h;}};d.loadIdentity=function(){n.identity(d[g])};d.loadMatrix=function(h){h=h.m;for(var i=d[g].m,m=0;m<16;m++)i[m]=h[m]};d.multMatrix=function(h){d.loadMatrix(n.multiply(d[g],h,c))};d.perspective=
function(h,i,m,k){d.multMatrix(n.perspective(h,i,m,k,b))};d.frustum=function(h,i,m,k,o,p){d.multMatrix(n.frustum(h,i,m,k,o,p,b))};d.ortho=function(h,i,m,k,o,p){d.multMatrix(n.ortho(h,i,m,k,o,p,b))};d.scale=function(h,i,m){d.multMatrix(n.scale(h,i,m,b))};d.translate=function(h,i,m){d.multMatrix(n.translate(h,i,m,b))};d.rotate=function(h,i,m,k){d.multMatrix(n.rotate(h,i,m,k,b))};d.lookAt=function(h,i,m,k,o,p,J,K,L){d.multMatrix(n.lookAt(h,i,m,k,o,p,J,K,L,b))};d.pushMatrix=function(){j.push(Array.prototype.slice.call(d[g].m))};
d.popMatrix=function(){var h=j.pop();d[g].m=D?new Float32Array(h):h};d.project=function(h,i,m,k,o,p){k=k||d.modelviewMatrix;o=o||d.projectionMatrix;p=p||d.getParameter(d.VIEWPORT);h=o.transformPoint(k.transformPoint(new l(h,i,m)));return new l(p[0]+p[2]*(h.x*0.5+0.5),p[1]+p[3]*(h.y*0.5+0.5),h.z*0.5+0.5)};d.unProject=function(h,i,m,k,o,p){k=k||d.modelviewMatrix;o=o||d.projectionMatrix;p=p||d.getParameter(d.VIEWPORT);h=new l((h-p[0])/p[2]*2-1,(i-p[1])/p[3]*2-1,m*2-1);return n.inverse(n.multiply(o,k,
b),c).transformPoint(h)};d.matrixMode(d.MODELVIEW)}function M(){var b={mesh:new q({coords:true,colors:true,triangles:false}),mode:-1,coord:[0,0,0,0],color:[1,1,1,1],pointSize:1,shader:new y("uniform float pointSize;varying vec4 color;varying vec4 coord;varying vec2 pixel;void main(){color=gl_Color;coord=gl_TexCoord;gl_Position=gl_ModelViewProjectionMatrix*gl_Vertex;pixel=gl_Position.xy/gl_Position.w*0.5+0.5;gl_PointSize=pointSize;}",
"uniform sampler2D texture;uniform float pointSize;uniform bool useTexture;uniform vec2 windowSize;varying vec4 color;varying vec4 coord;varying vec2 pixel;void main(){gl_FragColor=color;if(useTexture)gl_FragColor*=texture2D(texture,coord.xy);}")};d.pointSize=function(c){b.shader.uniforms({pointSize:c})};d.begin=function(c){if(b.mode!=-1)throw"mismatched gl.begin() and gl.end() calls";b.mode=c;b.mesh.colors=[];b.mesh.coords=
[];b.mesh.vertices=[]};d.color=function(c,a,f,g){b.color=arguments.length==1?c.toArray().concat(1):[c,a,f,g||1]};d.texCoord=function(c,a){b.coord=arguments.length==1?c.toArray(2):[c,a]};d.vertex=function(c,a,f){b.mesh.colors.push(b.color);b.mesh.coords.push(b.coord);b.mesh.vertices.push(arguments.length==1?c.toArray():[c,a,f])};d.end=function(){if(b.mode==-1)throw"mismatched gl.begin() and gl.end() calls";b.mesh.compile();b.shader.uniforms({windowSize:[d.canvas.width,d.canvas.height],useTexture:!!d.getParameter(d.TEXTURE_BINDING_2D)}).draw(b.mesh,
b.mode);b.mode=-1}}function N(){function b(){for(var k in i)if(i[k])return true;return false}function c(k){e=Object.create(k);e.original=k;e.x=e.pageX;e.y=e.pageY;for(k=d.canvas;k;k=k.offsetParent){e.x-=k.offsetLeft;e.y-=k.offsetTop}if(m){e.deltaX=e.x-j;e.deltaY=e.y-h}else{e.deltaX=0;e.deltaY=0;m=true}j=e.x;h=e.y;e.dragging=b();e.preventDefault=function(){e.original.preventDefault()};e.stopPropagation=function(){e.original.stopPropagation()};return e}function a(k){k=c(k);d.onmousemove&&d.onmousemove(k);
k.preventDefault()}function f(k){i[k.which]=false;if(!b()){document.removeEventListener("mousemove",a);document.removeEventListener("mouseup",f);d.canvas.addEventListener("mousemove",a);d.canvas.addEventListener("mouseup",f)}k=c(k);d.onmouseup&&d.onmouseup(k);k.preventDefault()}function g(){m=false}var j=0,h=0,i={},m=false;z(d.canvas,"mousedown",function(k){if(!b()){document.addEventListener("mousemove",a);document.addEventListener("mouseup",f);d.canvas.removeEventListener("mousemove",a);d.canvas.removeEventListener("mouseup",
f)}i[k.which]=true;k=c(k);d.onmousedown&&d.onmousedown(k);k.preventDefault()});d.canvas.addEventListener("mousemove",a);d.canvas.addEventListener("mouseup",f);d.canvas.addEventListener("mouseover",g);d.canvas.addEventListener("mouseout",g)}function E(b){return{8:"BACKSPACE",9:"TAB",13:"ENTER",16:"SHIFT",27:"ESCAPE",32:"SPACE",37:"LEFT",38:"UP",39:"RIGHT",40:"DOWN"}[b]||(b>=65&&b<=90?String.fromCharCode(b):null)}function z(b,c,a){b.addEventListener(c,a)}function O(){(function(b){d.makeCurrent=function(){d=
b}})(d);d.animate=function(){function b(){d=f;var g=new Date;d.onupdate&&d.onupdate((g-a)/1E3);d.ondraw&&d.ondraw();c(b);a=g}var c=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||function(g){setTimeout(g,1E3/60)},a=new Date,f=d;b()};d.fullscreen=function(b){function c(){d.canvas.width=window.innerWidth-f-g;d.canvas.height=window.innerHeight-a-j;d.viewport(0,0,d.canvas.width,d.canvas.height);if(b.camera||!("camera"in b)){d.matrixMode(d.PROJECTION);
d.loadIdentity();d.perspective(b.fov||45,d.canvas.width/d.canvas.height,b.near||0.1,b.far||1E3);d.matrixMode(d.MODELVIEW)}d.ondraw&&d.ondraw()}b=b||{};var a=b.paddingTop||0,f=b.paddingLeft||0,g=b.paddingRight||0,j=b.paddingBottom||0;if(!document.body)throw"document.body doesn't exist yet (call gl.fullscreen() from window.onload() or from inside the <body> tag)";document.body.appendChild(d.canvas);document.body.style.overflow="hidden";d.canvas.style.position="absolute";d.canvas.style.left=f+"px";d.canvas.style.top=
a+"px";window.addEventListener("resize",c);c()}}function n(){var b=Array.prototype.concat.apply([],arguments);b.length||(b=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]);this.m=D?new Float32Array(b):b}function w(){this.unique=[];this.indices=[];this.map={}}function x(b,c){this.buffer=null;this.target=b;this.type=c;this.data=[]}function q(b){b=b||{};this.vertexBuffers={};this.indexBuffers={};this.addVertexBuffer("vertices","gl_Vertex");b.coords&&this.addVertexBuffer("coords","gl_TexCoord");b.normals&&this.addVertexBuffer("normals",
"gl_Normal");b.colors&&this.addVertexBuffer("colors","gl_Color");if(!("triangles"in b)||b.triangles)this.addIndexBuffer("triangles");b.lines&&this.addIndexBuffer("lines")}function F(b){return new l((b&1)*2-1,(b&2)-1,(b&4)/2-1)}function t(b,c,a){this.t=arguments.length?b:Number.MAX_VALUE;this.hit=c;this.normal=a}function u(){var b=d.getParameter(d.VIEWPORT),c=d.modelviewMatrix.m,a=new l(c[0],c[4],c[8]),f=new l(c[1],c[5],c[9]),g=new l(c[2],c[6],c[10]);c=new l(c[3],c[7],c[11]);this.eye=new l(-c.dot(a),
-c.dot(f),-c.dot(g));a=b[0];f=a+b[2];g=b[1];c=g+b[3];this.ray00=d.unProject(a,g,1).subtract(this.eye);this.ray10=d.unProject(f,g,1).subtract(this.eye);this.ray01=d.unProject(a,c,1).subtract(this.eye);this.ray11=d.unProject(f,c,1).subtract(this.eye);this.viewport=b}function y(b,c){function a(i,m,k){for(;(result=i.exec(m))!=null;)k(result)}function f(i,m){var k={},o=/^((\s*\/\/.*\n|\s*#extension.*\n)+)[^]*$/.exec(m);m=o?o[1]+i+m.substr(o[1].length):i+m;a(/\bgl_\w+\b/g,i,function(p){if(!(p in k)){m=
m.replace(RegExp("\\b"+p+"\\b","g"),"_"+p);k[p]=true}});return m}function g(i,m){var k=d.createShader(i);d.shaderSource(k,m);d.compileShader(k);if(!d.getShaderParameter(k,d.COMPILE_STATUS))throw"compile error: "+d.getShaderInfoLog(k);return k}var j=b+c;this.needsMVPM=/(gl_ModelViewProjectionMatrix|ftransform)/.test(j);this.needsNM=/gl_NormalMatrix/.test(j);b=f("uniform mat3 gl_NormalMatrix;uniform mat4 gl_ModelViewMatrix;uniform mat4 gl_ProjectionMatrix;uniform mat4 gl_ModelViewProjectionMatrix;attribute vec4 gl_Vertex;attribute vec4 gl_TexCoord;attribute vec3 gl_Normal;attribute vec4 gl_Color;vec4 ftransform(){return gl_ModelViewProjectionMatrix*gl_Vertex;}",
b);c=f("precision highp float;uniform mat3 gl_NormalMatrix;uniform mat4 gl_ModelViewMatrix;uniform mat4 gl_ProjectionMatrix;uniform mat4 gl_ModelViewProjectionMatrix;",c);this.program=d.createProgram();d.attachShader(this.program,g(d.VERTEX_SHADER,b));d.attachShader(this.program,g(d.FRAGMENT_SHADER,c));d.linkProgram(this.program);if(!d.getProgramParameter(this.program,d.LINK_STATUS))throw"link error: "+d.getProgramInfoLog(this.program);this.attributes={};var h={};a(/uniform\s+sampler(1D|2D|3D|Cube)\s+(\w+)\s*;/g,
b+c,function(i){h[i[2]]=1});this.isSampler=h}function s(b,c,a){a=a||{};this.id=d.createTexture();this.width=b;this.height=c;this.format=a.format||d.RGBA;this.type=a.type||d.UNSIGNED_BYTE;d.bindTexture(d.TEXTURE_2D,this.id);d.pixelStorei(d.UNPACK_FLIP_Y_WEBGL,1);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_MAG_FILTER,a.filter||a.magFilter||d.LINEAR);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_MIN_FILTER,a.filter||a.minFilter||d.LINEAR);d.texParameteri(d.TEXTURE_2D,d.TEXTURE_WRAP_S,a.wrap||a.wrapS||d.CLAMP_TO_EDGE);
d.texParameteri(d.TEXTURE_2D,d.TEXTURE_WRAP_T,a.wrap||a.wrapT||d.CLAMP_TO_EDGE);d.texImage2D(d.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)}function l(b,c,a){this.x=b||0;this.y=c||0;this.z=a||0}var d,v={create:function(b){b=b||{};var c=document.createElement("canvas");c.width=800;c.height=600;if(!("alpha"in b))b.alpha=false;try{d=c.getContext("webgl",b)}catch(a){}try{d=d||c.getContext("experimental-webgl",b)}catch(f){}if(!d)throw"WebGL not supported";I();M();N();O();return d},keys:{},
Matrix:n,Indexer:w,Buffer:x,Mesh:q,HitTest:t,Raytracer:u,Shader:y,Texture:s,Vector:l};z(document,"keydown",function(b){if(!b.altKey&&!b.ctrlKey&&!b.metaKey){var c=E(b.keyCode);if(c)v.keys[c]=true;v.keys[b.keyCode]=true}});z(document,"keyup",function(b){if(!b.altKey&&!b.ctrlKey&&!b.metaKey){var c=E(b.keyCode);if(c)v.keys[c]=false;v.keys[b.keyCode]=false}});var C=305397760,D=typeof Float32Array!="undefined";n.prototype={inverse:function(){return n.inverse(this,new n)},transpose:function(){return n.transpose(this,
new n)},multiply:function(b){return n.multiply(this,b,new n)},transformPoint:function(b){var c=this.m;return(new l(c[0]*b.x+c[1]*b.y+c[2]*b.z+c[3],c[4]*b.x+c[5]*b.y+c[6]*b.z+c[7],c[8]*b.x+c[9]*b.y+c[10]*b.z+c[11])).divide(c[12]*b.x+c[13]*b.y+c[14]*b.z+c[15])},transformVector:function(b){var c=this.m;return new l(c[0]*b.x+c[1]*b.y+c[2]*b.z,c[4]*b.x+c[5]*b.y+c[6]*b.z,c[8]*b.x+c[9]*b.y+c[10]*b.z)}};n.inverse=function(b,c){c=c||new n;var a=b.m,f=c.m;f[0]=a[5]*a[10]*a[15]-a[5]*a[14]*a[11]-a[6]*a[9]*a[15]+
a[6]*a[13]*a[11]+a[7]*a[9]*a[14]-a[7]*a[13]*a[10];f[1]=-a[1]*a[10]*a[15]+a[1]*a[14]*a[11]+a[2]*a[9]*a[15]-a[2]*a[13]*a[11]-a[3]*a[9]*a[14]+a[3]*a[13]*a[10];f[2]=a[1]*a[6]*a[15]-a[1]*a[14]*a[7]-a[2]*a[5]*a[15]+a[2]*a[13]*a[7]+a[3]*a[5]*a[14]-a[3]*a[13]*a[6];f[3]=-a[1]*a[6]*a[11]+a[1]*a[10]*a[7]+a[2]*a[5]*a[11]-a[2]*a[9]*a[7]-a[3]*a[5]*a[10]+a[3]*a[9]*a[6];f[4]=-a[4]*a[10]*a[15]+a[4]*a[14]*a[11]+a[6]*a[8]*a[15]-a[6]*a[12]*a[11]-a[7]*a[8]*a[14]+a[7]*a[12]*a[10];f[5]=a[0]*a[10]*a[15]-a[0]*a[14]*a[11]-
a[2]*a[8]*a[15]+a[2]*a[12]*a[11]+a[3]*a[8]*a[14]-a[3]*a[12]*a[10];f[6]=-a[0]*a[6]*a[15]+a[0]*a[14]*a[7]+a[2]*a[4]*a[15]-a[2]*a[12]*a[7]-a[3]*a[4]*a[14]+a[3]*a[12]*a[6];f[7]=a[0]*a[6]*a[11]-a[0]*a[10]*a[7]-a[2]*a[4]*a[11]+a[2]*a[8]*a[7]+a[3]*a[4]*a[10]-a[3]*a[8]*a[6];f[8]=a[4]*a[9]*a[15]-a[4]*a[13]*a[11]-a[5]*a[8]*a[15]+a[5]*a[12]*a[11]+a[7]*a[8]*a[13]-a[7]*a[12]*a[9];f[9]=-a[0]*a[9]*a[15]+a[0]*a[13]*a[11]+a[1]*a[8]*a[15]-a[1]*a[12]*a[11]-a[3]*a[8]*a[13]+a[3]*a[12]*a[9];f[10]=a[0]*a[5]*a[15]-a[0]*
a[13]*a[7]-a[1]*a[4]*a[15]+a[1]*a[12]*a[7]+a[3]*a[4]*a[13]-a[3]*a[12]*a[5];f[11]=-a[0]*a[5]*a[11]+a[0]*a[9]*a[7]+a[1]*a[4]*a[11]-a[1]*a[8]*a[7]-a[3]*a[4]*a[9]+a[3]*a[8]*a[5];f[12]=-a[4]*a[9]*a[14]+a[4]*a[13]*a[10]+a[5]*a[8]*a[14]-a[5]*a[12]*a[10]-a[6]*a[8]*a[13]+a[6]*a[12]*a[9];f[13]=a[0]*a[9]*a[14]-a[0]*a[13]*a[10]-a[1]*a[8]*a[14]+a[1]*a[12]*a[10]+a[2]*a[8]*a[13]-a[2]*a[12]*a[9];f[14]=-a[0]*a[5]*a[14]+a[0]*a[13]*a[6]+a[1]*a[4]*a[14]-a[1]*a[12]*a[6]-a[2]*a[4]*a[13]+a[2]*a[12]*a[5];f[15]=a[0]*a[5]*
a[10]-a[0]*a[9]*a[6]-a[1]*a[4]*a[10]+a[1]*a[8]*a[6]+a[2]*a[4]*a[9]-a[2]*a[8]*a[5];a=a[0]*f[0]+a[1]*f[4]+a[2]*f[8]+a[3]*f[12];for(var g=0;g<16;g++)f[g]/=a;return c};n.transpose=function(b,c){c=c||new n;var a=b.m,f=c.m;f[0]=a[0];f[1]=a[4];f[2]=a[8];f[3]=a[12];f[4]=a[1];f[5]=a[5];f[6]=a[9];f[7]=a[13];f[8]=a[2];f[9]=a[6];f[10]=a[10];f[11]=a[14];f[12]=a[3];f[13]=a[7];f[14]=a[11];f[15]=a[15];return c};n.multiply=function(b,c,a){a=a||new n;b=b.m;c=c.m;var f=a.m;f[0]=b[0]*c[0]+b[1]*c[4]+b[2]*c[8]+b[3]*c[12];
f[1]=b[0]*c[1]+b[1]*c[5]+b[2]*c[9]+b[3]*c[13];f[2]=b[0]*c[2]+b[1]*c[6]+b[2]*c[10]+b[3]*c[14];f[3]=b[0]*c[3]+b[1]*c[7]+b[2]*c[11]+b[3]*c[15];f[4]=b[4]*c[0]+b[5]*c[4]+b[6]*c[8]+b[7]*c[12];f[5]=b[4]*c[1]+b[5]*c[5]+b[6]*c[9]+b[7]*c[13];f[6]=b[4]*c[2]+b[5]*c[6]+b[6]*c[10]+b[7]*c[14];f[7]=b[4]*c[3]+b[5]*c[7]+b[6]*c[11]+b[7]*c[15];f[8]=b[8]*c[0]+b[9]*c[4]+b[10]*c[8]+b[11]*c[12];f[9]=b[8]*c[1]+b[9]*c[5]+b[10]*c[9]+b[11]*c[13];f[10]=b[8]*c[2]+b[9]*c[6]+b[10]*c[10]+b[11]*c[14];f[11]=b[8]*c[3]+b[9]*c[7]+b[10]*
c[11]+b[11]*c[15];f[12]=b[12]*c[0]+b[13]*c[4]+b[14]*c[8]+b[15]*c[12];f[13]=b[12]*c[1]+b[13]*c[5]+b[14]*c[9]+b[15]*c[13];f[14]=b[12]*c[2]+b[13]*c[6]+b[14]*c[10]+b[15]*c[14];f[15]=b[12]*c[3]+b[13]*c[7]+b[14]*c[11]+b[15]*c[15];return a};n.identity=function(b){b=b||new n;var c=b.m;c[0]=c[5]=c[10]=c[15]=1;c[1]=c[2]=c[3]=c[4]=c[6]=c[7]=c[8]=c[9]=c[11]=c[12]=c[13]=c[14]=0;return b};n.perspective=function(b,c,a,f,g){b=Math.tan(b*Math.PI/360)*a;c=b*c;return n.frustum(-c,c,-b,b,a,f,g)};n.frustum=function(b,
c,a,f,g,j,h){h=h||new n;var i=h.m;i[0]=2*g/(c-b);i[1]=0;i[2]=(c+b)/(c-b);i[3]=0;i[4]=0;i[5]=2*g/(f-a);i[6]=(f+a)/(f-a);i[7]=0;i[8]=0;i[9]=0;i[10]=-(j+g)/(j-g);i[11]=-2*j*g/(j-g);i[12]=0;i[13]=0;i[14]=-1;i[15]=0;return h};n.ortho=function(b,c,a,f,g,j,h){h=h||new n;var i=h.m;i[0]=2/(c-b);i[1]=0;i[2]=0;i[3]=-(c+b)/(c-b);i[4]=0;i[5]=2/(f-a);i[6]=0;i[7]=-(f+a)/(f-a);i[8]=0;i[9]=0;i[10]=-2/(j-g);i[11]=-(j+g)/(j-g);i[12]=0;i[13]=0;i[14]=0;i[15]=1;return h};n.scale=function(b,c,a,f){f=f||new n;var g=f.m;
g[0]=b;g[1]=0;g[2]=0;g[3]=0;g[4]=0;g[5]=c;g[6]=0;g[7]=0;g[8]=0;g[9]=0;g[10]=a;g[11]=0;g[12]=0;g[13]=0;g[14]=0;g[15]=1;return f};n.translate=function(b,c,a,f){f=f||new n;var g=f.m;g[0]=1;g[1]=0;g[2]=0;g[3]=b;g[4]=0;g[5]=1;g[6]=0;g[7]=c;g[8]=0;g[9]=0;g[10]=1;g[11]=a;g[12]=0;g[13]=0;g[14]=0;g[15]=1;return f};n.rotate=function(b,c,a,f,g){if(!b||!c&&!a&&!f)return n.identity(g);g=g||new n;var j=g.m,h=Math.sqrt(c*c+a*a+f*f);b*=Math.PI/180;c/=h;a/=h;f/=h;h=Math.cos(b);b=Math.sin(b);var i=1-h;j[0]=c*c*i+h;
j[1]=c*a*i-f*b;j[2]=c*f*i+a*b;j[3]=0;j[4]=a*c*i+f*b;j[5]=a*a*i+h;j[6]=a*f*i-c*b;j[7]=0;j[8]=f*c*i-a*b;j[9]=f*a*i+c*b;j[10]=f*f*i+h;j[11]=0;j[12]=0;j[13]=0;j[14]=0;j[15]=1;return g};n.lookAt=function(b,c,a,f,g,j,h,i,m,k){k=k||new n;var o=k.m;b=new l(b,c,a);f=new l(f,g,j);i=new l(h,i,m);h=b.subtract(f).unit();i=i.cross(h).unit();m=h.cross(i).unit();o[0]=i.x;o[1]=i.y;o[2]=i.z;o[3]=-i.dot(b);o[4]=m.x;o[5]=m.y;o[6]=m.z;o[7]=-m.dot(b);o[8]=h.x;o[9]=h.y;o[10]=h.z;o[11]=-h.dot(b);o[12]=0;o[13]=0;o[14]=0;
o[15]=1;return k};w.prototype={add:function(b){var c=JSON.stringify(b);if(!(c in this.map)){this.map[c]=this.unique.length;this.unique.push(b)}return this.map[c]}};x.prototype={compile:function(b){for(var c=[],a=0;a<this.data.length;a+=1E4)c=Array.prototype.concat.apply(c,this.data.slice(a,a+1E4));a=this.data.length?c.length/this.data.length:0;if(a!=Math.round(a))throw"buffer elements not of consistent size, average size is "+a;this.buffer=this.buffer||d.createBuffer();this.buffer.length=c.length;
this.buffer.spacing=a;d.bindBuffer(this.target,this.buffer);d.bufferData(this.target,new this.type(c),b||d.STATIC_DRAW)}};q.prototype={addVertexBuffer:function(b,c){(this.vertexBuffers[c]=new x(d.ARRAY_BUFFER,Float32Array)).name=b;this[b]=[]},addIndexBuffer:function(b){this.indexBuffers[b]=new x(d.ELEMENT_ARRAY_BUFFER,Int16Array);this[b]=[]},compile:function(){for(var b in this.vertexBuffers){var c=this.vertexBuffers[b];c.data=this[c.name];c.compile()}for(var a in this.indexBuffers){c=this.indexBuffers[a];
c.data=this[a];c.compile()}},transform:function(b){this.vertices=this.vertices.map(function(a){return b.transformPoint(l.fromArray(a)).toArray()});if(this.normals){var c=b.inverse().transpose();this.normals=this.normals.map(function(a){return c.transformVector(l.fromArray(a)).unit().toArray()})}this.compile();return this},computeNormals:function(){this.normals||this.addVertexBuffer("normals","gl_Normal");for(var b=0;b<this.vertices.length;b++)this.normals[b]=new l;for(b=0;b<this.triangles.length;b++){var c=
this.triangles[b],a=l.fromArray(this.vertices[c[0]]),f=l.fromArray(this.vertices[c[1]]),g=l.fromArray(this.vertices[c[2]]);a=f.subtract(a).cross(g.subtract(a)).unit();this.normals[c[0]]=this.normals[c[0]].add(a);this.normals[c[1]]=this.normals[c[1]].add(a);this.normals[c[2]]=this.normals[c[2]].add(a)}for(b=0;b<this.vertices.length;b++)this.normals[b]=this.normals[b].unit().toArray();this.compile();return this},computeWireframe:function(){for(var b=new w,c=0;c<this.triangles.length;c++)for(var a=this.triangles[c],
f=0;f<a.length;f++){var g=a[f],j=a[(f+1)%a.length];b.add([Math.min(g,j),Math.max(g,j)])}this.lines||this.addIndexBuffer("lines");this.lines=b.unique;this.compile();return this},getAABB:function(){var b={min:new l(Number.MAX_VALUE,Number.MAX_VALUE,Number.MAX_VALUE)};b.max=b.min.negative();for(var c=0;c<this.vertices.length;c++){var a=l.fromArray(this.vertices[c]);b.min=l.min(b.min,a);b.max=l.max(b.max,a)}return b},getBoundingSphere:function(){var b=this.getAABB();b={center:b.min.add(b.max).divide(2),
radius:0};for(var c=0;c<this.vertices.length;c++)b.radius=Math.max(b.radius,l.fromArray(this.vertices[c]).subtract(b.center).length());return b}};q.plane=function(b){b=b||{};var c=new q(b);detailX=b.detailX||b.detail||1;detailY=b.detailY||b.detail||1;for(b=0;b<=detailY;b++)for(var a=b/detailY,f=0;f<=detailX;f++){var g=f/detailX;c.vertices.push([2*g-1,2*a-1,0]);c.coords&&c.coords.push([g,a]);c.normals&&c.normals.push([0,0,1]);if(f<detailX&&b<detailY){g=f+b*(detailX+1);c.triangles.push([g,g+1,g+detailX+
1]);c.triangles.push([g+detailX+1,g+1,g+detailX+2])}}c.compile();return c};var G=[[0,4,2,6,-1,0,0],[1,3,5,7,+1,0,0],[0,1,4,5,0,-1,0],[2,6,3,7,0,+1,0],[0,2,1,3,0,0,-1],[4,5,6,7,0,0,+1]];q.cube=function(b){b=new q(b);for(var c=0;c<G.length;c++){for(var a=G[c],f=c*4,g=0;g<4;g++){b.vertices.push(F(a[g]).toArray());b.coords&&b.coords.push([g&1,(g&2)/2]);b.normals&&b.normals.push(a.slice(4,7))}b.triangles.push([f,f+1,f+2]);b.triangles.push([f+2,f+1,f+3])}b.compile();return b};q.sphere=function(b){b=b||
{};var c=new q(b),a=new w;detail=b.detail||6;for(b=0;b<8;b++)for(var f=F(b),g=f.x*f.y*f.z>0,j=[],h=0;h<=detail;h++){for(var i=0;h+i<=detail;i++){var m=h/detail,k=i/detail,o=(detail-h-i)/detail;k={vertex:(new l(m+(m-m*m)/2,k+(k-k*k)/2,o+(o-o*o)/2)).unit().multiply(f).toArray()};if(c.coords)k.coord=f.y>0?[1-m,o]:[o,1-m];j.push(a.add(k))}if(h>0)for(i=0;h+i<=detail;i++){m=(h-1)*(detail+1)+(h-1-(h-1)*(h-1))/2+i;k=h*(detail+1)+(h-h*h)/2+i;c.triangles.push(g?[j[m],j[k],j[m+1]]:[j[m],j[m+1],j[k]]);h+i<detail&&
c.triangles.push(g?[j[k],j[k+1],j[m+1]]:[j[k],j[m+1],j[k+1]])}}c.vertices=a.unique.map(function(p){return p.vertex});if(c.coords)c.coords=a.unique.map(function(p){return p.coord});if(c.normals)c.normals=c.vertices;c.compile();return c};q.load=function(b,c){c=c||{};if(!("coords"in c))c.coords=!!b.coords;if(!("normals"in c))c.normals=!!b.normals;if(!("triangles"in c))c.triangles=!!b.triangles;if(!("lines"in c))c.lines=!!b.lines;var a=new q(c);a.vertices=b.vertices;if(a.coords)a.coords=b.coords;if(a.normals)a.normals=
b.normals;if(a.triangles)a.triangles=b.triangles;if(a.lines)a.lines=b.lines;a.compile();return a};t.prototype={mergeWith:function(b){if(b.t>0&&b.t<this.t){this.t=b.t;this.hit=b.hit;this.normal=b.normal}}};u.prototype={getRayForPixel:function(b,c){b=(b-this.viewport[0])/this.viewport[2];c=1-(c-this.viewport[1])/this.viewport[3];var a=l.lerp(this.ray00,this.ray10,b),f=l.lerp(this.ray01,this.ray11,b);return l.lerp(a,f,c).unit()}};u.hitTestBox=function(b,c,a,f){var g=a.subtract(b).divide(c),j=f.subtract(b).divide(c),
h=l.min(g,j);g=l.max(g,j);h=h.max();g=g.min();if(h>0&&h<g){b=b.add(c.multiply(h));a=a.add(1.0E-6);f=f.subtract(1.0E-6);return new t(h,b,new l((b.x>f.x)-(b.x<a.x),(b.y>f.y)-(b.y<a.y),(b.z>f.z)-(b.z<a.z)))}return null};u.hitTestSphere=function(b,c,a,f){var g=b.subtract(a),j=c.dot(c),h=2*c.dot(g);g=g.dot(g)-f*f;g=h*h-4*j*g;if(g>0){j=(-h-Math.sqrt(g))/(2*j);b=b.add(c.multiply(j));return new t(j,b,b.subtract(a).divide(f))}return null};u.hitTestTriangle=function(b,c,a,f,g){var j=f.subtract(a),h=g.subtract(a);
g=j.cross(h).unit();f=g.dot(a.subtract(b)).divide(g.dot(c));if(f>0){b=b.add(c.multiply(f));var i=b.subtract(a);a=h.dot(h);c=h.dot(j);h=h.dot(i);var m=j.dot(j);j=j.dot(i);i=a*m-c*c;m=(m*h-c*j)/i;j=(a*j-c*h)/i;if(m>=0&&j>=0&&m+j<=1)return new t(f,b,g)}return null};var P=new n,H=new n;y.prototype={uniforms:function(b){d.useProgram(this.program);for(var c in b){var a=d.getUniformLocation(this.program,c);if(a){var f=b[c];if(f instanceof l)f=[f.x,f.y,f.z];else if(f instanceof n)f=f.m;var g=Object.prototype.toString.call(f);
if(g=="[object Array]"||g=="[object Float32Array]")switch(f.length){case 1:d.uniform1fv(a,new Float32Array(f));break;case 2:d.uniform2fv(a,new Float32Array(f));break;case 3:d.uniform3fv(a,new Float32Array(f));break;case 4:d.uniform4fv(a,new Float32Array(f));break;case 9:d.uniformMatrix3fv(a,false,new Float32Array([f[0],f[3],f[6],f[1],f[4],f[7],f[2],f[5],f[8]]));break;case 16:d.uniformMatrix4fv(a,false,new Float32Array([f[0],f[4],f[8],f[12],f[1],f[5],f[9],f[13],f[2],f[6],f[10],f[14],f[3],f[7],f[11],
f[15]]));break;default:throw"don't know how to load uniform \""+c+'" of length '+f.length;}else{g=Object.prototype.toString.call(f);if(g=="[object Number]"||g=="[object Boolean]")(this.isSampler[c]?d.uniform1i:d.uniform1f).call(d,a,f);else throw'attempted to set uniform "'+c+'" to invalid value '+f;}}}return this},draw:function(b,c){this.drawBuffers(b.vertexBuffers,b.indexBuffers[c==d.LINES?"lines":"triangles"],arguments.length<2?d.TRIANGLES:c)},drawBuffers:function(b,c,a){this.uniforms({_gl_ModelViewMatrix:d.modelviewMatrix,
_gl_ProjectionMatrix:d.projectionMatrix});this.needsMVPM&&this.uniforms({_gl_ModelViewProjectionMatrix:n.multiply(d.projectionMatrix,d.modelviewMatrix,H)});if(this.needsNM){var f=n.transpose(n.inverse(d.modelviewMatrix,P),H).m;this.uniforms({_gl_NormalMatrix:[f[0],f[1],f[2],f[4],f[5],f[6],f[8],f[9],f[10]]})}f=0;for(var g in b){var j=b[g],h=this.attributes[g]||d.getAttribLocation(this.program,g.replace(/^gl_/,"_gl_"));if(!(h==-1||!j.buffer)){this.attributes[g]=h;d.bindBuffer(d.ARRAY_BUFFER,j.buffer);
d.enableVertexAttribArray(h);d.vertexAttribPointer(h,j.buffer.spacing,d.FLOAT,false,0,0);f=j.buffer.length/j.buffer.spacing}}for(g in this.attributes)g in b||d.disableVertexAttribArray(this.attributes[g]);if(f&&(!c||c.buffer))if(c){d.bindBuffer(d.ELEMENT_ARRAY_BUFFER,c.buffer);d.drawElements(a,c.buffer.length,d.UNSIGNED_SHORT,0)}else d.drawArrays(a,0,f);return this}};var A,r,B;s.prototype={bind:function(b){d.activeTexture(d.TEXTURE0+(b||0));d.bindTexture(d.TEXTURE_2D,this.id)},unbind:function(b){d.activeTexture(d.TEXTURE0+
(b||0));d.bindTexture(d.TEXTURE_2D,null)},drawTo:function(b){var c=d.getParameter(d.VIEWPORT);A=A||d.createFramebuffer();r=r||d.createRenderbuffer();d.bindFramebuffer(d.FRAMEBUFFER,A);d.bindRenderbuffer(d.RENDERBUFFER,r);if(this.width!=r.width||this.height!=r.height){r.width=this.width;r.height=this.height;d.renderbufferStorage(d.RENDERBUFFER,d.DEPTH_COMPONENT16,this.width,this.height)}d.framebufferTexture2D(d.FRAMEBUFFER,d.COLOR_ATTACHMENT0,d.TEXTURE_2D,this.id,0);d.framebufferRenderbuffer(d.FRAMEBUFFER,
d.DEPTH_ATTACHMENT,d.RENDERBUFFER,r);d.viewport(0,0,this.width,this.height);b();d.bindFramebuffer(d.FRAMEBUFFER,null);d.bindRenderbuffer(d.RENDERBUFFER,null);d.viewport(c[0],c[1],c[2],c[3])},swapWith:function(b){var c;c=b.id;b.id=this.id;this.id=c;c=b.width;b.width=this.width;this.width=c;c=b.height;b.height=this.height;this.height=c}};s.fromImage=function(b,c){c=c||{};var a=new s(b.width,b.height,c);try{d.texImage2D(d.TEXTURE_2D,0,a.format,a.format,a.type,b)}catch(f){if(location.protocol=="file:")throw'image not loaded for security reasons (serve this page over "http://" instead)';
else throw"image not loaded for security reasons (image must originate from the same domain as this page or use Cross-Origin Resource Sharing)";}c.minFilter&&c.minFilter!=d.NEAREST&&c.minFilter!=d.LINEAR&&d.generateMipmap(d.TEXTURE_2D);return a};s.fromURL=function(b,c){B=B||function(){var g=document.createElement("canvas").getContext("2d");g.canvas.width=g.canvas.height=128;for(var j=0;j<g.canvas.height;j+=16)for(var h=0;h<g.canvas.width;h+=16){g.fillStyle=(h^j)&16?"#FFF":"#DDD";g.fillRect(h,j,16,
16)}return g.canvas}();var a=s.fromImage(B,c),f=new Image;f.onload=function(){s.fromImage(f,c).swapWith(a)};f.src=b;return a};l.prototype={negative:function(){return new l(-this.x,-this.y,-this.z)},add:function(b){return b instanceof l?new l(this.x+b.x,this.y+b.y,this.z+b.z):new l(this.x+b,this.y+b,this.z+b)},subtract:function(b){return b instanceof l?new l(this.x-b.x,this.y-b.y,this.z-b.z):new l(this.x-b,this.y-b,this.z-b)},multiply:function(b){return b instanceof l?new l(this.x*b.x,this.y*b.y,this.z*
b.z):new l(this.x*b,this.y*b,this.z*b)},divide:function(b){return b instanceof l?new l(this.x/b.x,this.y/b.y,this.z/b.z):new l(this.x/b,this.y/b,this.z/b)},equals:function(b){return this.x==b.x&&this.y==b.y&&this.z==b.z},dot:function(b){return this.x*b.x+this.y*b.y+this.z*b.z},cross:function(b){return new l(this.y*b.z-this.z*b.y,this.z*b.x-this.x*b.z,this.x*b.y-this.y*b.x)},length:function(){return Math.sqrt(this.dot(this))},unit:function(){return this.divide(this.length())},min:function(){return Math.min(Math.min(this.x,
this.y),this.z)},max:function(){return Math.max(Math.max(this.x,this.y),this.z)},toAngles:function(){return{theta:Math.atan2(this.z,this.x),phi:Math.asin(this.y/this.length())}},toArray:function(b){return[this.x,this.y,this.z].slice(0,b||3)},clone:function(){return new l(this.x,this.y,this.z)},init:function(b,c,a){this.x=b;this.y=c;this.z=a;return this}};l.negative=function(b,c){c.x=-b.x;c.y=-b.y;c.z=-b.z;return c};l.add=function(b,c,a){if(c instanceof l){a.x=b.x+c.x;a.y=b.y+c.y;a.z=b.z+c.z}else{a.x=
b.x+c;a.y=b.y+c;a.z=b.z+c}return a};l.subtract=function(b,c,a){if(c instanceof l){a.x=b.x-c.x;a.y=b.y-c.y;a.z=b.z-c.z}else{a.x=b.x-c;a.y=b.y-c;a.z=b.z-c}return a};l.multiply=function(b,c,a){if(c instanceof l){a.x=b.x*c.x;a.y=b.y*c.y;a.z=b.z*c.z}else{a.x=b.x*c;a.y=b.y*c;a.z=b.z*c}return a};l.divide=function(b,c,a){if(c instanceof l){a.x=b.x/c.x;a.y=b.y/c.y;a.z=b.z/c.z}else{a.x=b.x/c;a.y=b.y/c;a.z=b.z/c}return a};l.cross=function(b,c,a){a.x=b.y*c.z-b.z*c.y;a.y=b.z*c.x-b.x*c.z;a.z=b.x*c.y-b.y*c.x;return a};
l.unit=function(b,c){var a=b.length();c.x=b.x/a;c.y=b.y/a;c.z=b.z/a;return c};l.fromAngles=function(b,c){return new l(Math.cos(b)*Math.cos(c),Math.sin(c),Math.sin(b)*Math.cos(c))};l.randomDirection=function(){return l.fromAngles(Math.random()*Math.PI*2,Math.asin(Math.random()*2-1))};l.min=function(b,c){return new l(Math.min(b.x,c.x),Math.min(b.y,c.y),Math.min(b.z,c.z))};l.max=function(b,c){return new l(Math.max(b.x,c.x),Math.max(b.y,c.y),Math.max(b.z,c.z))};l.lerp=function(b,c,a){return c.subtract(b).multiply(a).add(b)};
l.fromArray=function(b){return new l(b[0],b[1],b[2])};return v}();

41
openjscad.css Normal file
View File

@ -0,0 +1,41 @@
#filedropzone {
border: 2px dashed #bbb;
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
padding: 15px;
color: black;
}
#filedropzone_empty {
text-align: center;
color: #bbb;
}
#filedropzone_filled {
color: black;
display: none;
}
#filebuttons {
float: right;
}
canvas { cursor: move; }
.parametersdiv {
border: 1px solid rgb(200,200,200);
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
padding: 10px;
}
.parametersdiv .header {
font-weight: bold;
}
.statusdiv {
width: 800px;
margin-bottom: 10px;
}

972
openjscad.js Normal file
View File

@ -0,0 +1,972 @@
OpenJsCad = function() {
};
OpenJsCad.log = function(txt) {
var timeInMs = Date.now();
var prevtime = OpenJsCad.log.prevLogTime;
if(!prevtime) prevtime = timeInMs;
var deltatime = timeInMs - prevtime;
OpenJsCad.log.prevLogTime = timeInMs;
var timefmt = (deltatime*0.001).toFixed(3);
txt = "["+timefmt+"] "+txt;
if( (typeof(console) == "object") && (typeof(console.log) == "function") )
{
console.log(txt);
}
else if( (typeof(self) == "object") && (typeof(self.postMessage) == "function") )
{
self.postMessage({cmd: 'log', txt: txt});
}
else throw new Error("Cannot log");
};
// A viewer is a WebGL canvas that lets the user view a mesh. The user can
// tumble it around by dragging the mouse.
OpenJsCad.Viewer = function(containerelement, width, height, initialdepth) {
var gl = GL.create();
this.gl = gl;
this.angleX = 0;
this.angleY = 0;
this.viewpointX = 0;
this.viewpointY = 0;
this.viewpointZ = initialdepth;
// Draw triangle lines:
this.drawLines = false;
// Set to true so lines don't use the depth buffer
this.lineOverlay = false;
// Set up the viewport
gl.canvas.width = width;
gl.canvas.height = height;
gl.viewport(0, 0, width, height);
gl.matrixMode(gl.PROJECTION);
gl.loadIdentity();
gl.perspective(45, width / height, 0.5, 1000);
gl.matrixMode(gl.MODELVIEW);
// Set up WebGL state
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.clearColor(0.93, 0.93, 0.93, 1);
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
gl.polygonOffset(1, 1);
// Black shader for wireframe
this.blackShader = new GL.Shader('\
void main() {\
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\
}\
', '\
void main() {\
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.1);\
}\
');
// Shader with diffuse and specular lighting
this.lightingShader = new GL.Shader('\
varying vec3 color;\
varying vec3 normal;\
varying vec3 light;\
void main() {\
const vec3 lightDir = vec3(1.0, 2.0, 3.0) / 3.741657386773941;\
light = lightDir;\
color = gl_Color.rgb;\
normal = gl_NormalMatrix * gl_Normal;\
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\
}\
', '\
varying vec3 color;\
varying vec3 normal;\
varying vec3 light;\
void main() {\
vec3 n = normalize(normal);\
float diffuse = max(0.0, dot(light, n));\
float specular = pow(max(0.0, -reflect(light, n).z), 10.0) * sqrt(diffuse);\
gl_FragColor = vec4(mix(color * (0.3 + 0.7 * diffuse), vec3(1.0), specular), 1.0);\
}\
');
containerelement.appendChild(gl.canvas);
var _this=this;
gl.onmousemove = function(e) {
_this.onMouseMove(e);
};
gl.onmousewheel = function(e) {
_this.onMouseWheel(e);
}
gl.ondraw = function() {
_this.onDraw();
};
this.clear();
};
OpenJsCad.Viewer.prototype = {
setCsg: function(csg) {
this.mesh = OpenJsCad.Viewer.csgToMesh(csg);
this.onDraw();
},
clear: function() {
// empty mesh:
this.mesh = new GL.Mesh();
this.onDraw();
},
supported: function() {
return !!this.gl;
},
onMouseWheel: function(e) {
e.preventDefault();
console.log( 'mousewheel', e);
var factor = 1e-2;
this.viewpointZ *= Math.pow(2,factor * e.wheelDeltaY);
this.onDraw();
return false;
},
onMouseMove: function(e) {
if (e.dragging) {
e.preventDefault();
if(e.altKey||e.ctrlKey||e.metaKey)
{
var factor = 1e-2;
this.viewpointZ *= Math.pow(2,factor * e.deltaY);
}
else if(e.shiftKey)
{
var factor = 5e-3;
this.viewpointX += factor * e.deltaX * this.viewpointZ;
this.viewpointY -= factor * e.deltaY * this.viewpointZ;
}
else
{
this.angleY += e.deltaX * 2;
this.angleX += e.deltaY * 2;
this.angleX = Math.max(-90, Math.min(90, this.angleX));
}
this.onDraw();
}
},
onDraw: function(e) {
var gl = this.gl;
gl.makeCurrent();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.loadIdentity();
gl.translate(this.viewpointX, this.viewpointY, -this.viewpointZ);
gl.rotate(this.angleX, 1, 0, 0);
gl.rotate(this.angleY, 0, 1, 0);
if (!this.lineOverlay) gl.enable(gl.POLYGON_OFFSET_FILL);
this.lightingShader.draw(this.mesh, gl.TRIANGLES);
if (!this.lineOverlay) gl.disable(gl.POLYGON_OFFSET_FILL);
if(this.drawLines)
{
if (this.lineOverlay) gl.disable(gl.DEPTH_TEST);
gl.enable(gl.BLEND);
this.blackShader.draw(this.mesh, gl.LINES);
gl.disable(gl.BLEND);
if (this.lineOverlay) gl.enable(gl.DEPTH_TEST);
}
},
}
// Convert from CSG solid to GL.Mesh object
OpenJsCad.Viewer.csgToMesh = function(csg) {
var csg = csg.canonicalized();
var mesh = new GL.Mesh({ normals: true, colors: true });
var vertexTag2Index = {};
var vertices = [];
var colors = [];
var triangles = [];
// set to true if we want to use interpolated vertex normals
// this creates nice round spheres but does not represent the shape of
// the actual model
var smoothlighting = false;
var polygons = csg.toPolygons();
var numpolygons = polygons.length;
for(var polygonindex = 0; polygonindex < numpolygons; polygonindex++)
{
var polygon = polygons[polygonindex];
var color = [0,0,1];
if(polygon.shared && polygon.shared.color)
{
color = polygon.shared.color;
}
var indices = polygon.vertices.map(function(vertex) {
var vertextag = vertex.getTag();
var vertexindex;
if(smoothlighting && (vertextag in vertexTag2Index))
{
vertexindex = vertexTag2Index[vertextag];
}
else
{
vertexindex = vertices.length;
vertexTag2Index[vertextag] = vertexindex;
vertices.push([vertex.pos.x, vertex.pos.y, vertex.pos.z]);
colors.push(color);
}
return vertexindex;
});
for (var i = 2; i < indices.length; i++) {
triangles.push([indices[0], indices[i - 1], indices[i]]);
}
}
mesh.triangles = triangles;
mesh.vertices = vertices;
mesh.colors = colors;
mesh.computeWireframe();
mesh.computeNormals();
return mesh;
};
// this is a bit of a hack; doesn't properly supports urls that start with '/'
// but does handle relative urls containing ../
OpenJsCad.makeAbsoluteUrl = function(url, baseurl) {
if(!url.match(/^[a-z]+\:/i))
{
var basecomps = baseurl.split("/");
if(basecomps.length > 0)
{
basecomps.splice(basecomps.length - 1, 1);
}
var urlcomps = url.split("/");
var comps = basecomps.concat(urlcomps);
var comps2 = [];
comps.map(function(c) {
if(c == "..")
{
if(comps2.length > 0)
{
comps2.splice(comps2.length - 1, 1);
}
}
else
{
comps2.push(c);
}
});
url = "";
for(var i = 0; i < comps2.length; i++)
{
if(i > 0) url += "/";
url += comps2[i];
}
}
return url;
};
OpenJsCad.isChrome = function()
{
return (navigator.userAgent.search("Chrome") >= 0);
};
// This is called from within the web worker. Execute the main() function of the supplied script
// and post a message to the calling thread when finished
OpenJsCad.runMainInWorker = function(mainParameters)
{
try
{
if(typeof(main) != 'function') throw new Error('Your jscad file should contain a function main() which returns a CSG solid.');
OpenJsCad.log.prevLogTime = Date.now();
var csg = main(mainParameters);
if( (typeof(csg) != "object") || (!(csg instanceof CSG)) )
{
throw new Error("Your main() function should return a CSG solid.");
}
var csg_bin = csg.toCompactBinary();
csg = null; // not needed anymore
self.postMessage({cmd: 'rendered', csg: csg_bin});
}
catch(e)
{
var errtxt = e.stack;
if(!errtxt)
{
errtxt = e.toString();
}
self.postMessage({cmd: 'error', err: errtxt});
}
};
OpenJsCad.javaScriptToSolidSync = function(script, mainParameters, debugging) {
var workerscript = "";
workerscript += script;
if(debugging)
{
workerscript += "\n\n\n\n\n\n\n/* -------------------------------------------------------------------------\n";
workerscript += "OpenJsCad debugging\n\nAssuming you are running Chrome:\nF10 steps over an instruction\nF11 steps into an instruction\n";
workerscript += "F8 continues running\nPress the (||) button at the bottom to enable pausing whenever an error occurs\n";
workerscript += "Click on a line number to set or clear a breakpoint\n";
workerscript += "For more information see: http://code.google.com/chrome/devtools/docs/overview.html\n\n";
workerscript += "------------------------------------------------------------------------- */\n";
workerscript += "\n\n// Now press F11 twice to enter your main() function:\n\n";
workerscript += "debugger;\n";
}
workerscript += "return main("+JSON.stringify(mainParameters)+");";
var f = new Function(workerscript);
OpenJsCad.log.prevLogTime = Date.now();
var csg = f();
return csg;
};
// callback: should be function(error, csg)
OpenJsCad.javaScriptToSolidASync = function(script, mainParameters, callback) {
var baselibraries = [
"csg.js",
"openjscad.js"
];
var baseurl = document.location + '';
var base = document.getElementsByTagName('base');
if( base) {
base = base.getAttribute('href');
if( base)
baseurl = OpenJsCad.makeAbsoluteUrl(baseurl, base);
}
var workerscript = "";
workerscript += script;
workerscript += "\n\n\n\n//// The following code is added by OpenJsCad:\n";
workerscript += "var _csg_libraries=" + JSON.stringify(baselibraries)+";\n";
workerscript += "var _csg_baseurl=" + JSON.stringify(baseurl)+";\n";
workerscript += "var _csg_makeAbsoluteURL=" + OpenJsCad.makeAbsoluteUrl.toString()+";\n";
// workerscript += "if(typeof(libs) == 'function') _csg_libraries = _csg_libraries.concat(libs());\n";
workerscript += "_csg_libraries = _csg_libraries.map(function(l){return _csg_makeAbsoluteURL(l,_csg_baseurl);});\n";
workerscript += "_csg_libraries.map(function(l){importScripts(l)});\n";
workerscript += "self.addEventListener('message', function(e) {if(e.data && e.data.cmd == 'render'){";
workerscript += " OpenJsCad.runMainInWorker("+JSON.stringify(mainParameters)+");";
// workerscript += " if(typeof(main) != 'function') throw new Error('Your jscad file should contain a function main() which returns a CSG solid.');\n";
// workerscript += " var csg; try {csg = main("+JSON.stringify(mainParameters)+"); self.postMessage({cmd: 'rendered', csg: csg});}";
// workerscript += " catch(e) {var errtxt = e.stack; self.postMessage({cmd: 'error', err: errtxt});}";
workerscript += "}},false);\n";
var blobURL = OpenJsCad.textToBlobUrl(workerscript);
if(!window.Worker) throw new Error("Your browser doesn't support Web Workers. Please try the Chrome browser instead.");
var worker = new Worker(blobURL);
worker.onmessage = function(e) {
if(e.data)
{
if(e.data.cmd == 'rendered')
{
//var csg = CSG.fromObject(e.data.csg);
var csg = CSG.fromCompactBinary(e.data.csg);
callback(null, csg);
}
else if(e.data.cmd == "error")
{
// var errtxt = "Error in line "+e.data.err.lineno+": "+e.data.err.message;
callback(e.data.err, null);
}
else if(e.data.cmd == "log")
{
console.log(e.data.txt);
}
}
};
worker.onerror = function(e) {
var errtxt = "Error in line "+e.lineno+": "+e.message;
callback(errtxt, null);
};
worker.postMessage({
cmd: "render"
}); // Start the worker.
return worker;
};
OpenJsCad.textToBlobUrl = function(txt) {
var bb;
if(window.BlobBuilder) bb = new window.BlobBuilder()
else if(window.WebKitBlobBuilder) bb = new window.WebKitBlobBuilder()
else if(window.MozBlobBuilder) bb = new window.MozBlobBuilder()
else throw new Error("Your browser doesn't support BlobBuilder");
bb.append(txt);
var blob = bb.getBlob();
var blobURL;
if(window.URL) blobURL = window.URL.createObjectURL(blob)
else if(window.webkitURL) blobURL = window.webkitURL.createObjectURL(blob)
else throw new Error("Your browser doesn't support window.URL");
if(!blobURL) throw new Error("createObjectURL() failed");
return blobURL;
};
OpenJsCad.revokeBlobUrl = function(url) {
if(window.URL) window.URL.revokeObjectURL(url)
else if(window.webkitURL) window.webkitURL.revokeObjectURL(url)
else throw new Error("Your browser doesn't support window.URL");
};
OpenJsCad.FileSystemApiErrorHandler = function(fileError, operation) {
var errormap = {
1: 'NOT_FOUND_ERR',
2: 'SECURITY_ERR',
3: 'ABORT_ERR',
4: 'NOT_READABLE_ERR',
5: 'ENCODING_ERR',
6: 'NO_MODIFICATION_ALLOWED_ERR',
7: 'INVALID_STATE_ERR',
8: 'SYNTAX_ERR',
9: 'INVALID_MODIFICATION_ERR',
10: 'QUOTA_EXCEEDED_ERR',
11: 'TYPE_MISMATCH_ERR',
12: 'PATH_EXISTS_ERR',
};
var errname;
if(fileError.code in errormap)
{
errname = errormap[fileError.code];
}
else
{
errname = "Error #"+fileError.code;
}
var errtxt = "FileSystem API error: "+operation+" returned error "+errname;
throw new Error(errtxt);
};
OpenJsCad.AlertUserOfUncaughtExceptions = function() {
window.onerror = function(message, url, line) {
message = message.replace(/^Uncaught /i, "");
alert(message+"\n\n("+url+" line "+line+")");
};
};
// parse the jscad script to get the parameter definitions
OpenJsCad.getParamDefinitions = function(script) {
var scriptisvalid = true;
try
{
// first try to execute the script itself
// this will catch any syntax errors
var f = new Function(script);
f();
}
catch(e) {
scriptisvalid = false;
}
var params = [];
if(scriptisvalid)
{
var script1 = "if(typeof(getParameterDefinitions) == 'function') {return getParameterDefinitions();} else {return [];} ";
script1 += script;
var f = new Function(script1);
params = f();
if( (typeof(params) != "object") || (typeof(params.length) != "number") )
{
throw new Error("The getParameterDefinitions() function should return an array with the parameter definitions");
}
}
return params;
};
OpenJsCad.Processor = function(containerdiv, onchange) {
this.containerdiv = containerdiv;
this.onchange = onchange;
this.viewerdiv = null;
this.viewer = null;
this.viewerwidth = 800;
this.viewerheight = 600;
this.initialViewerDistance = 50;
this.processing = false;
this.solid = null;
this.validcsg = false;
this.hasstl = false;
this.worker = null;
this.paramDefinitions = [];
this.paramControls = [];
this.script = null;
this.hasError = false;
this.debugging = false;
this.createElements();
};
OpenJsCad.Processor.prototype = {
createElements: function() {
while(this.containerdiv.children.length > 0)
{
this.containerdiv.removeChild(0);
}
if(!OpenJsCad.isChrome() )
{
var div = document.createElement("div");
div.innerHTML = "Please note: OpenJsCad currently only runs reliably on Google Chrome!";
this.containerdiv.appendChild(div);
}
var viewerdiv = document.createElement("div");
viewerdiv.className = "viewer";
viewerdiv.style.width = this.viewerwidth + "px";
viewerdiv.style.height = this.viewerheight + "px";
viewerdiv.style.backgroundColor = "rgb(200,200,200)";
this.containerdiv.appendChild(viewerdiv);
this.viewerdiv = viewerdiv;
try
{
this.viewer = new OpenJsCad.Viewer(this.viewerdiv, this.viewerwidth, this.viewerheight, this.initialViewerDistance);
} catch (e) {
// this.viewer = null;
this.viewerdiv.innerHTML = "<b><br><br>Error: "+e.toString()+"</b><br><br>OpenJsCad currently requires Google Chrome with WebGL enabled";
// this.viewerdiv.innerHTML = e.toString();
}
this.errordiv = document.createElement("div");
this.errorpre = document.createElement("pre");
this.errordiv.appendChild(this.errorpre);
this.statusdiv = document.createElement("div");
this.statusdiv.className = "statusdiv";
//this.statusdiv.style.width = this.viewerwidth + "px";
this.statusspan = document.createElement("span");
this.statusbuttons = document.createElement("div");
this.statusbuttons.style.float = "right";
this.statusdiv.appendChild(this.statusspan);
this.statusdiv.appendChild(this.statusbuttons);
this.abortbutton = document.createElement("button");
this.abortbutton.innerHTML = "Abort";
var that = this;
this.abortbutton.onclick = function(e) {
that.abort();
};
this.statusbuttons.appendChild(this.abortbutton);
this.generateStlButton = document.createElement("button");
this.generateStlButton.innerHTML = "Generate STL";
this.generateStlButton.onclick = function(e) {
that.generateStl();
};
this.statusbuttons.appendChild(this.generateStlButton);
this.downloadStlLink = document.createElement("a");
this.downloadStlLink.innerHTML = "Download STL";
this.statusbuttons.appendChild(this.downloadStlLink);
this.parametersdiv = document.createElement("div");
this.parametersdiv.className = "parametersdiv";
var headerdiv = document.createElement("div");
headerdiv.innerText = "Parameters:";
headerdiv.className = "header";
this.parametersdiv.appendChild(headerdiv);
this.parameterstable = document.createElement("table");
this.parameterstable.className = "parameterstable";
this.parametersdiv.appendChild(this.parameterstable);
var parseParametersButton = document.createElement("button");
parseParametersButton.innerHTML = "Update";
parseParametersButton.onclick = function(e) {
that.rebuildSolid();
};
this.parametersdiv.appendChild(parseParametersButton);
this.enableItems();
this.containerdiv.appendChild(this.statusdiv);
this.containerdiv.appendChild(this.errordiv);
this.containerdiv.appendChild(this.parametersdiv);
this.clearViewer();
},
clearViewer: function() {
this.clearStl();
this.solid = new CSG();
if(this.viewer)
{
this.viewer.setCsg(this.solid);
}
this.validcsg = false;
this.enableItems();
},
abort: function() {
if(this.processing)
{
//todo: abort
this.processing=false;
this.statusspan.innerHTML = "Aborted.";
this.worker.terminate();
this.enableItems();
if(this.onchange) this.onchange();
}
},
enableItems: function() {
this.abortbutton.style.display = this.processing? "inline":"none";
this.generateStlButton.style.display = ((!this.hasstl)&&(this.validcsg))? "inline":"none";
this.downloadStlLink.style.display = this.hasstl? "inline":"none";
this.parametersdiv.style.display = (this.paramControls.length > 0)? "block":"none";
this.errordiv.style.display = this.hasError? "block":"none";
this.statusdiv.style.display = this.hasError? "none":"block";
},
setError: function(txt) {
this.hasError = (txt != "");
this.errorpre.innerText = txt;
this.enableItems();
},
setDebugging: function(debugging) {
this.debugging = debugging;
},
// script: javascript code
// filename: optional, the name of the .jscad file
setJsCad: function(script, filename) {
if(!filename) filename = "openjscad.jscad";
filename = filename.replace(/\.jscad$/i, "");
this.abort();
this.clearViewer();
this.paramDefinitions = [];
this.paramControls = [];
this.script = null;
this.setError("");
var scripthaserrors = false;
try
{
this.paramDefinitions = OpenJsCad.getParamDefinitions(script);
this.createParamControls();
}
catch(e)
{
this.setError(e.toString());
this.statusspan.innerHTML = "Error.";
scripthaserrors = true;
}
if(!scripthaserrors)
{
this.script = script;
this.filename = filename;
this.rebuildSolid();
}
else
{
this.enableItems();
if(this.onchange) this.onchange();
}
},
getParamValues: function()
{
var paramValues = {};
for(var i = 0; i < this.paramDefinitions.length; i++)
{
var paramdef = this.paramDefinitions[i];
var type = "text";
if('type' in paramdef)
{
type = paramdef.type;
}
var control = this.paramControls[i];
var value;
if( (type == "text") || (type == "float") || (type == "int") )
{
value = control.value;
if( (type == "float") || (type == "int") )
{
var isnumber = !isNaN(parseFloat(value)) && isFinite(value);
if(!isnumber)
{
throw new Error("Not a number: "+value);
}
if(type == "int")
{
value = parseInt(value);
}
else
{
value = parseFloat(value);
}
}
}
else if(type == "choice")
{
value = control.options[control.selectedIndex].value;
}
paramValues[paramdef.name] = value;
}
return paramValues;
},
rebuildSolid: function()
{
this.abort();
this.setError("");
this.clearViewer();
this.processing = true;
this.statusspan.innerHTML = "Processing, please wait...";
this.enableItems();
var that = this;
var paramValues = this.getParamValues();
var useSync = this.debugging;
if(!useSync)
{
try
{
this.worker = OpenJsCad.javaScriptToSolidASync(this.script, paramValues, function(err, csg) {
that.processing = false;
that.worker = null;
if(err)
{
that.setError(err);
that.statusspan.innerHTML = "Error.";
}
else
{
that.solid = csg;
if(that.viewer) that.viewer.setCsg(csg);
that.validcsg = true;
that.statusspan.innerHTML = "Ready.";
}
that.enableItems();
if(that.onchange) that.onchange();
});
}
catch(e)
{
useSync = true;
}
}
if(useSync)
{
try
{
var csg = OpenJsCad.javaScriptToSolidSync(this.script, paramValues, this.debugging);
that.processing = false;
that.solid = csg;
if(that.viewer) that.viewer.setCsg(csg);
that.validcsg = true;
that.statusspan.innerHTML = "Ready.";
}
catch(e)
{
that.processing = false;
var errtxt = e.stack;
if(!errtxt)
{
errtxt = e.toString();
}
that.setError(errtxt);
that.statusspan.innerHTML = "Error.";
}
that.enableItems();
if(that.onchange) that.onchange();
}
},
hasSolid: function() {
return this.validcsg;
},
isProcessing: function() {
return this.processing;
},
/*
clearStl1: function() {
if(this.hasstl)
{
this.hasstl = false;
OpenJsCad.revokeBlobUrl(this.stlBlobUrl);
this.stlBlobUrl = null;
this.enableItems();
if(this.onchange) this.onchange();
}
},
*/
clearStl: function() {
if(this.hasstl)
{
this.hasstl = false;
if(this.stlDirEntry)
{
this.stlDirEntry.removeRecursively(function(){});
this.stlDirEntry=null;
}
if(this.stlBlobUrl)
{
OpenJsCad.revokeBlobUrl(this.stlBlobUrl);
this.stlBlobUrl = null;
}
this.enableItems();
if(this.onchange) this.onchange();
}
},
generateStl: function() {
this.clearStl();
if(this.validcsg)
{
try
{
this.generateStlFileSystem();
}
catch(e)
{
this.generateStlBlobUrl();
}
}
},
generateStlBlobUrl: function() {
var stltxt = this.solid.toStlString();
this.stlBlobUrl = OpenJsCad.textToBlobUrl(stltxt);
this.hasstl = true;
this.downloadStlLink.href = this.stlBlobUrl;
this.enableItems();
if(this.onchange) this.onchange();
},
generateStlFileSystem: function() {
var stltxt = this.solid.toStlString();
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder;
if(!window.requestFileSystem)
{
throw new Error("Your browser does not support the HTML5 FileSystem API. Please try the Chrome browser instead.");
}
if(!window.BlobBuilder)
{
throw new Error("Your browser does not support the HTML5 BlobBuilder API. Please try the Chrome browser instead.");
}
// create a random directory name:
var dirname = "OpenJsCadStlOutput1_"+parseInt(Math.random()*1000000000, 10)+".stl";
var filename = this.filename+".stl";
var that = this;
window.requestFileSystem(TEMPORARY, 20*1024*1024, function(fs){
fs.root.getDirectory(dirname, {create: true, exclusive: true}, function(dirEntry) {
that.stlDirEntry = dirEntry;
dirEntry.getFile(filename, {create: true, exclusive: true}, function(fileEntry) {
fileEntry.createWriter(function(fileWriter) {
fileWriter.onwriteend = function(e) {
that.hasstl = true;
that.downloadStlLink.href = fileEntry.toURL();
that.enableItems();
if(that.onchange) that.onchange();
};
fileWriter.onerror = function(e) {
throw new Error('Write failed: ' + e.toString());
};
// Create a new Blob and write it to log.txt.
var bb = new window.BlobBuilder(); // Note: window.WebKitBlobBuilder in Chrome 12.
bb.append(stltxt);
fileWriter.write(bb.getBlob());
},
function(fileerror){OpenJsCad.FileSystemApiErrorHandler(fileerror, "createWriter");}
);
},
function(fileerror){OpenJsCad.FileSystemApiErrorHandler(fileerror, "getFile('"+filename+"')");}
);
},
function(fileerror){OpenJsCad.FileSystemApiErrorHandler(fileerror, "getDirectory('"+dirname+"')");}
);
},
function(fileerror){OpenJsCad.FileSystemApiErrorHandler(fileerror, "requestFileSystem");}
);
},
createParamControls: function() {
this.parameterstable.innerHTML = "";
this.paramControls = [];
var paramControls = [];
var tablerows = [];
for(var i = 0; i < this.paramDefinitions.length; i++)
{
var errorprefix = "Error in parameter definition #"+(i+1)+": ";
var paramdef = this.paramDefinitions[i];
if(!('name' in paramdef))
{
throw new Error(errorprefix + "Should include a 'name' parameter");
}
var type = "text";
if('type' in paramdef)
{
type = paramdef.type;
}
if( (type !== "text") && (type !== "int") && (type !== "float") && (type !== "choice") )
{
throw new Error(errorprefix + "Unknown parameter type '"+type+"'");
}
var control;
if( (type == "text") || (type == "int") || (type == "float") )
{
control = document.createElement("input");
control.type = "text";
if('default' in paramdef)
{
control.value = paramdef.default;
}
else
{
if( (type == "int") || (type == "float") )
{
control.value = "0";
}
else
{
control.value = "";
}
}
}
else if(type == "choice")
{
if(!('values' in paramdef))
{
throw new Error(errorprefix + "Should include a 'values' parameter");
}
control = document.createElement("select");
var values = paramdef.values;
var captions;
if('captions' in paramdef)
{
captions = paramdef.captions;
if(captions.length != values.length)
{
throw new Error(errorprefix + "'captions' and 'values' should have the same number of items");
}
}
else
{
captions = values;
}
var selectedindex = 0;
for(var valueindex = 0; valueindex < values.length; valueindex++)
{
var option = document.createElement("option");
option.value = values[valueindex];
option.text = captions[valueindex];
control.add(option);
if('default' in paramdef)
{
if(paramdef.default == values[valueindex])
{
selectedindex = valueindex;
}
}
}
if(values.length > 0)
{
control.selectedIndex = selectedindex;
}
}
paramControls.push(control);
var tr = document.createElement("tr");
var td = document.createElement("td");
var label = paramdef.name + ":";
if('caption' in paramdef)
{
label = paramdef.caption;
}
td.innerHTML = label;
tr.appendChild(td);
td = document.createElement("td");
td.appendChild(control);
tr.appendChild(td);
tablerows.push(tr);
}
var that = this;
tablerows.map(function(tr){
that.parameterstable.appendChild(tr);
});
this.paramControls = paramControls;
},
};

212
processfile.html Normal file
View File

@ -0,0 +1,212 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea:focus {
outline: none;
}
a { color: inherit; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gCurrentFile = null;
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
setupDragDrop();
}
function setupDragDrop()
{
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList) {
// Great success! All the File APIs are supported.
} else {
throw new Error("Error: Your browser does not fully support the HTML File API");
}
var dropZone = document.getElementById('filedropzone');
dropZone.addEventListener('dragover', function(evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}, false);
dropZone.addEventListener('drop', handleFileSelect, false);
}
function handleFileSelect(evt)
{
evt.stopPropagation();
evt.preventDefault();
if(!evt.dataTransfer) throw new Error("Not a datatransfer (1)");
if(!evt.dataTransfer.files) throw new Error("Not a datatransfer (2)");
if(evt.dataTransfer.files.length != 1)
{
throw new Error("Please drop a single .jscad file");
}
var file = evt.dataTransfer.files[0];
if(!file.name.match(/\.jscad$/i))
{
throw new Error("Please drop a file with .jscad extension");
}
if(file.size == 0)
{
throw new Error("You have dropped an empty file");
}
gCurrentFile = file;
gPreviousModificationTime = "";
fileChanged();
}
function fileChanged()
{
var dropZone = document.getElementById('filedropzone');
if(gCurrentFile)
{
var txt = "Current file: "+gCurrentFile.name;
document.getElementById("currentfile").innerHTML = txt;
document.getElementById("filedropzone_filled").style.display = "block";
document.getElementById("filedropzone_empty").style.display = "none";
}
else
{
document.getElementById("filedropzone_filled").style.display = "none";
document.getElementById("filedropzone_empty").style.display = "block";
}
parseFile(false);
}
function parseFile(debugging)
{
if(gCurrentFile)
{
var reader = new FileReader();
reader.onload = function(evt) {
var txt = evt.target.result;
};
reader.onloadend = function(evt) {
if (evt.target.readyState == FileReader.DONE)
{
var jscadscript = evt.target.result;
if(jscadscript == "")
{
if(document.location.toString().match(/^file\:\//i))
{
throw new Error("Could not read file. You are using a local copy of OpenJsCad; if you are using Chrome, you need to launch it with the following command line option:\n\n--allow-file-access-from-files\n\notherwise the browser will not have access to uploaded files due to security restrictions.");
}
else
{
throw new Error("Could not read file.");
}
}
else
{
if(gProcessor)
{
var filename = gCurrentFile.name;
filename = filename.replace(/^.*\/([^\/]*)$/, "$1");
gProcessor.setDebugging(debugging);
gProcessor.setJsCad(jscadscript, filename);
}
}
}
else
{
throw new Error("Failed to read file");
if(gProcessor) gProcessor.clearViewer();
}
};
reader.readAsText(gCurrentFile, "UTF-8");
}
}
</script>
<title>OpenJsCad parser</title>
<body onload="onload()">
<h1>OpenJsCad parser</h1>
<div id="viewer"></div>
<br>
<div id="filedropzone">
<div id="filedropzone_empty">Drop your .jscad file here</div>
<div id="filedropzone_filled">
<span id="currentfile">dfghdfgh</span>
<div id="filebuttons">
<button id="getstlbutton" style="display:none" onclick="getStl();">Get STL</button>
<button onclick="parseFile(false);">Reload</button>
<button onclick="parseFile(true);">Debug (see below)</button>
</div>
</div>
</div>
<br>
<h2>Instructions:</h2>
Create a new file in your favorite text editor. To get started enter the following text:
<br>
<pre>function main() {
var cube = CSG.roundedCube({radius: 10, roundradius: 2, resolution: 16});
var sphere = CSG.sphere({radius: 10, resolution: 16}).translate([5, 5, 5]);
return cube.union(sphere);
}
</pre>
Save this to a file on your desktop with .jscad extension. Then drag &amp; drop the file from your desktop
to the box above (marked with 'drop your .jscad file here).<br><br>
The 3d model should now appear. You can continue to make changes to your .jscad file. Just press Reload
to parse the changes, you do not need to drag &amp; drop the file again.
<br><br>
When finished press Generate STL to generate the STL file. Then click Save STL and save it to
a file with .stl extension.
<br><br>
For more information about OpenJsCad see the <a href="index.html">introduction</a>.
<h2>Debugging</h2>
By default your .jscad file is parsed in a separate thread (in a Web Worker). This allows long running
scripts to be executed while the web browser stays responsive. The web browser's debugger does not
have access to scripts running in web workers however. To allow debugging you can use the Debug button above to execute
your jscad code in the main thread instead of in a web worker.
<br><br>
To debug your code in Google Chrome: open the Developer Tools by typing Ctrl+Shift+I (or Command+Option+I on mac).
Then press the Debug button above. The debugger will stop just before executing your main() function.
<br><br>
For more information about debugging in Chrome see
<a href="http://code.google.com/chrome/devtools/docs/overview.html" target="_blank">Chrome Developer Tools: Overview</a>
<br><br>
You can output log messages from your script using:
<pre>
OpenJsCad.log("Hello");
</pre>
The log messages will appear in the browser's console (shown using Ctrl+Shift+I in Chrome). They will appear
even while not actually debugging.
<br><br>
</body>
</html>

227
servodemo.html Normal file
View File

@ -0,0 +1,227 @@
<!DOCTYPE html>
<html><head>
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
max-width: 820px;
margin: 0 auto;
padding: 10px;
}
pre, code, textarea {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0 3px;
color: #555;
}
pre, textarea {
padding: 10px;
width: 100%;
}
textarea {
height: 200px;
}
textarea:focus {
outline: none;
}
canvas { cursor: move; }
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script>
var gProcessor=null;
// Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions();
function onload()
{
gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"));
updateSolid();
}
function updateSolid()
{
gProcessor.setJsCad(document.getElementById('code').value);
}
</script>
<title>OpenJsCad demo: servo motor</title>
</head>
<body onload="onload()">
<h1>OpenJsCad demo: servo motor</h1>
<div id="viewer"></div>
<h2>Source code</h2>
Below is the OpenJsCad script for this demo. To build your own models, create a .jscad script
and use the <a href="processfile.html"><b>OpenJsCad parser</b></a>. For more information see the
<a href="index.html">OpenJsCad documentation</a>.
<br><br>
<textarea id="code" style="height: 500px">
// This demo intends to show how to use properties and connectors.
// The servoMotor() function constructs the shape of a standard servo
// It also defines a property for the cutout shape, and a connector property
// which can be used to correctly attach the motor to a surface.
//
// By using connectors, we don't need to know the actual orientation of each
// object. Instead we just attach the two objects by attaching their connectors.
//
// The cutout shape is automatically transformed with every transformation of
// the servo. We can simply subtract it from an object to make space for the servo
// motor.
function main(params)
{
// the servo motor solid:
var servo = servoMotor();
// the plate:
var plate = CSG.cube({radius: [40,40,4]});
// Define a Connector on the plate, at the place where we want to attach the servo:
plate.properties.servoConnector = new CSG.Connector(
[0, 0, 4], // point
[0, 0, 1], // axis: pointing upwards
[2.5, 1.1, 0] // normal: use some random vector in the z plane
);
// Do some random rotations:
plate = plate.rotateX(25);
plate = plate.rotateZ(10);
// now we really don't know the orientation of the plane anymore!
// Still we can perfectly align the servo motor to the plane at the servoConnector
// point that we decided on earlier:
servo = servo.connectTo(
servo.properties.servomotor.surfaceConnector, // the servo's pre defined Connector
plate.properties.servoConnector, // the connector on the surface
false, // we don't want to mirror; the Connector's axes should point in the same direction
0 // normalrotation; we could use it to rotate the servo in the plane
);
// Construct the result; use the parameters set by the end user:
var result = new CSG();
if(params.cutout != 0) plate = plate.subtract(servo.properties.servomotor.cutout);
if(params.showplate != 0) result = result.union(plate);
if(params.showservo != 0) result = result.union(servo);
if(params.showcutout != 0) result = result.union(servo.properties.servomotor.cutout.setColor(0,0.8,0));
return result;
}
// Here we define the user editable parameters:
function getParameterDefinitions() {
return [
{ name: 'showservo', caption: 'Show servo:', type: 'choice', values: [0, 1], default: 1, captions: ["No", "Yes"]},
{ name: 'showplate', caption: 'Show plate:', type: 'choice', values: [0, 1], default: 1, captions: ["No", "Yes"]},
{ name: 'showcutout', caption: 'Show cutout:', type: 'choice', values: [0, 1], default: 0, captions: ["No", "Yes"]},
{ name: 'cutout', caption: 'Subtract the servo cutout shape from the plate:', type: 'choice', values: [0, 1], default: 1, captions: ["No", "Yes"]},
];
}
// The servoMotor() function constructs the shape of a standard '3003' servo
// Returns a CSG solid. The solid has the following additional properties:
// .properties.servomotor.cutout: a CSG solid that can be used to create a cutout for the servo motor (including screw holes)
// .properties.servomotor.surfaceConnector: a CSG.Connector that can be used to attach the servo motor to a surface
function servoMotor()
{
var width = 20;
var length = 40.5;
var h1 = 26.5;
var h2 = 29;
var h4 = 35.9;
var l1 = 55.4;
var w1 = 17.8;
var holeradius = 2.1;
var holex1 = 2 * 2.54;
var holey1 = 9.5 * 2.54;
var cutoutmargin = 1;
var shaftradius = 3;
var shafty = 9.25;
var shafttop = 43;
var cyl2radius = 7;
var cyl2height = 2;
var resolution = 16; // for all circular objects
// result: this is the solid in which we will store the servomotor
var result = CSG.cube({radius:[width/2, length/2, h4/2]});
// cutout: this is the solid for the cutout. It is never rendered directly,
// but it will be returned as a property of the resulting solid.
// it can be used to cutout the shape of the servo motor and screw holes
// from another solid
var cutout = CSG.cube({radius:[width/2+cutoutmargin, length/2+cutoutmargin, h4/2+cutoutmargin]});
// add a 'bottomface' property. Since the cube already has predifined connectors at each face,
// we can just copy the 5th:
result.properties.servomotor = new CSG.Properties();
result.properties.servomotor.bottomface = result.properties.cube.facecenters[5];
// get the z coordinate of the bottom face:
var bottomz = result.properties.servomotor.bottomface.point.z;
// the tabs at the end containing the screw holes:
var cube2 = CSG.cube({radius:[w1/2, l1/2, (h2-h1)/2]});
cube2 = cube2.translate([0, 0, (h2-h1)/2 + bottomz + h1]);
result = result.union(cube2);
// create the cylinders for cutting out the screw holes:
for(var hole = 0; hole < 4; hole++)
{
var xoffset = (hole & 1)? holex1 : -holex1;
var yoffset = (hole & 2)? holey1 : -holey1;
var cylstart = new CSG.Vector3D([xoffset, yoffset, bottomz+h2]);
var cylend = new CSG.Vector3D([xoffset, yoffset, bottomz]);
var cutoutcylinder = CSG.cylinder({start: cylstart, end: cylend, radius: holeradius, resolution: resolution});
// create the screw hole in the tabs:
result = result.subtract(cutoutcylinder);
// And also add the cutout cylinder to the cutout shape:
cutout = cutout.union(cutoutcylinder);
}
// cylinder at top:
var p1 = new CSG.Vector3D([0, shafty, bottomz+h4]);
p2 = p1.plus(new CSG.Vector3D([0, 0, cyl2height]));
var cyl = CSG.cylinder({start: p1, end: p2, radius: cyl2radius, resolution: resolution});
result = result.union(cyl);
// make the entire motor grey:
result = result.setColor(0.2,0.2,0.2);
// create the shaft:
p1 = new CSG.Vector3D([0, shafty, bottomz+h4]);
p2 = p1.plus(new CSG.Vector3D([0, 0, cyl2height]));
var shaft = CSG.cylinder({start: [0, shafty, bottomz+h4], end: [0, shafty, bottomz+shafttop], radius: shaftradius, resolution: resolution});
shaft = shaft.setColor(1,1,1);
result = result.union(shaft);
// add the cutout solid to the properties:
result.properties.servomotor.cutout = cutout;
// Add a Connector to facilitate proper alignment of the servo motor to a surface
// The connector's point is at the x/y center of the box, in the bottom plane of the tabs
// The connector's axis points towards the top of the box
// The connector's normal points towards one of the tabs at the side
result.properties.servomotor.surfaceConnector = new CSG.Connector(
[0, 0, bottomz+h1], // point
[0, 0, 1], // axis
[0, 1, 0] // normal
);
return result;
}
</textarea><br>
<input type="submit" value="Update" onclick="updateSolid(); return false;">
<br><br>
</body>
</html>

772
simple.html Normal file
View File

@ -0,0 +1,772 @@
<!DOCTYPE html>
<html>
<head>
<base href="../" />
<script src="lightgl.js"></script>
<script src="csg.js"></script>
<script src="openjscad.js"></script>
<script src="jquery.js"></script>
<script src="coffee-script.js"></script>
<script src="Math.uuid.js"></script>
<style>
body {
font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
pre, code, textarea, canvas {
font: 12px/20px Monaco, monospace;
border: 1px solid #CCC;
border-radius: 3px;
background: #F9F9F9;
padding: 0;
margin: 0;
color: #555;
}
body > * {
box-sizing: border-box;
-webkit-box-sizing: border-box;
}
pre {
padding: 10px;
width: 100%;
}
textarea:focus {
outline: none;
}
canvas {
cursor: move;
}
header {
padding: 0;
margin: 0;
position: fixed;
top: 0;
right: 0;
left: 0;
border-bottom: 1px solid black;
background: #bbb;
height: 1.9em;
}
header > * {
display: inline-block;
}
body {
margin: 2em 0 0 0;
padding: 0;
}
#viewer > .statusdiv + div {
overflow: auto;
box-sizing: border-box;
-webkit-box-sizing: border-box;
width: 800px;
}
</style>
<link rel="stylesheet" href="openjscad.css" type="text/css">
<script type="text/coffeescript">
window.DelayedCall = (fun, delay = 250) ->
self = (args...) ->
self.stop()
self.fire(args...)
self.call = fun
self.delay = delay
self.fire = (args...) ->
self.stop()
self.to = setTimeout (-> self.call( args...)), self.delay
this
self.stop = ->
clearTimeout self.to
this
self
CSG.loadFile = (fn) ->
[fn, rev, lang, code] = openjscadUI.loadFile()
eval openjscadUI.prepareCode( lang, code)
class UI
constructor: ->
self = this
# Show all exceptions to the user:
OpenJsCad.AlertUserOfUncaughtExceptions()
@gProcessor = null
@revision = null
@$code = $ '#code'
@$lang = $ '#lang'
@$viewer = $ '#viewer'
@$body = $ 'body'
[fn, rev, lang, code] = @loadFile @currentFilename()
@$lang.val lang
@$code.
on( 'keyup.openjscadui', -> self.edit_history()).
val code
@windowResized = new DelayedCall -> self.resizeCode()
$(window).on 'resize.openjscadui', @windowResized
@gProcessor = new OpenJsCad.Processor @$viewer[0]
@updateSolid()
@resizeCode()
destroy: ->
@$code.off '.openjscadui'
$(window).off '.openjscadui'
prepareCode: (lang, code) ->
code = CoffeeScript.compile code, bare: 'on' if 'CoffeeScript' == lang
console.group 'code'
console.log code
console.groupEnd()
code
getCode: -> @$code.val()
getLang: -> @$lang.val()
getJsCode: -> @prepareCode @getLang(), @getCode()
updateSolid: -> @gProcessor.setJsCad @getJsCode()
currentFilename: -> /\/([^\/]*?)$/.exec(window.location.pathname)[1]
loadFile: (fn) ->
if rev = localStorage[fn]
JSON.parse localStorage[rev]
else
[fn, null, 'CoffeeScript', "main = ->\n\tCSG.cube radius: 1"]
writeFile: (fn, lang, code) ->
rev = Math.uuid()
localStorage[rev] = JSON.stringify [fn, @revision, @getLang(), @getCode()]
localStorage[fn] = @revision = rev
# Store the file-history (for undo) in the browser-storage:
edit_history: (event) ->
fn = @currentFilename()
code = @getCode()
lang = @getLang()
[_,_, oldlang, oldcode] = @loadFile fn
@writeFile fn, lang, code unless oldcode == code and oldlang == lang
true
resizeCode: ->
@$code.css
width: "#{@$body.innerWidth() - @$viewer.outerWidth() - @$code.outerWidth() + @$code.width()}px"
height: '600px'
zoom: (i) ->
factor = -1
viewer = @gProcessor.viewer
viewer.viewpointZ *= Math.pow 2, factor * i
viewer.onDraw()
# http://pallieter.org/Projects/insertTab/
window.insertTab = (o, e) ->
kC = e.keyCode ? e.charCode ? e.which
if kC == 9 && !e.shiftKey && !e.ctrlKey && !e.altKey
oS = o.scrollTop
if o.setSelectionRange
sS = o.selectionStart
sE = o.selectionEnd
o.value = "#{o.value.substring 0, sS}\t#{o.value.substr sE}"
o.setSelectionRange sS + 1, sS + 1
o.focus()
else if o.createTextRange
document.selection.createRange().text = "\t"
e.returnValue = false
o.scrollTop = oS
if e.preventDefault
e.preventDefault()
true
$ ->
window.openjscadUI = new UI
openjscadUI.$code.on 'keydown', (e)-> insertTab this, e
</script>
<title>OpenJsCad</title>
</head>
<body>
<header>
<button onclick="openjscadUI.updateSolid();return false;">Update</button>
<span style="float:right">
<button onclick="openjscadUI.zoom(1);return false;">+</button>
<button onclick="openjscadUI.zoom(-1);return false;">-</button>
</span>
</header>
<div id="viewer" style="float:right;"></div>
<div id="editor">
<textarea id="code" name="code"></textarea>
<br>
<select id="lang" name="lang">
<option>JavaScript</option>
<option selected>CoffeeScript</option>
</select>
</div>
<div id="help" style="display:none">
<h2>Primitive solids</h2>
Currently the following solids are supported. The parameters are passed in an object; most
parameters are optional. 3D vectors can be passed in an array. If a scalar is passed
for a parameter which expects a 3D vector, it is used for the x, y and z value.
In other words: <code>radius: 1</code> will give <code>radius: [1,1,1]</code>.
<br><br>
All rounded solids have a 'resolution' parameter which controls tesselation. If resolution
is set to 8, then 8 polygons per 360 degree of revolution are used. Beware that rendering
time will increase dramatically when increasing the resolution. For a sphere the number of polygons
increases quadratically with the resolution used.
<br><br>
<pre>
// a cube:
var cube = CSG.cube({
center: [0, 0, 0],
radius: [1, 1, 1]
});
// a sphere:
var sphere = CSG.sphere({
center: [0, 0, 0],
radius: 2, // must be scalar
resolution: 32
});
// a cylinder:
var cylinder = CSG.cylinder({
start: [0, -1, 0],
end: [0, 1, 0],
radius: 1,
resolution: 16
});
// like a cylinder, but with spherical endpoints:
var roundedCylinder = CSG.roundedCylinder({
start: [0, -1, 0],
end: [0, 1, 0],
radius: 1,
resolution: 16
});
// a rounded cube:
var cube = CSG.roundedCube({
center: [0, 0, 0],
radius: 1,
roundradius: 0.2,
resolution: 8,
});
</pre>
<h2>CSG operations</h2>
The 3 standard CSG operations are supported. All CSG operations return a new solid; the source solids
are not modified:
<pre>
var csg1 = cube.union(sphere);
var csg2 = cube.intersect(sphere);
var csg3 = cube.subtract(sphere);
</pre>
<h2>Transformations</h2>
Solids can be translated, scaled and rotated. Multiple transforms can be combined into a single matrix transform.
For <a href="#math">matrix and vector math see below</a>.
<pre>
var cube = CSG.cube();
// translation:
var cube2 = cube.translate([1, 2, 3]);
// scaling:
var largecube = cube.scale(2.0);
var stretchedcube = cube.scale([1.5, 1, 0.5]);
// rotation:
var rotated1 = cube.rotateX(-45); // rotate around the X axis
var rotated2 = cube.rotateY(90); // rotate around the Y axis
var rotated3 = cube.rotateZ(20); // rotate around the Z axis
// combine multiple transforms into a single matrix transform:
var m = new CSG.Matrix4x4();
m = m.multiply(CSG.Matrix4x4.rotationX(40));
m = m.multiply(CSG.Matrix4x4.rotationZ(40));
m = m.multiply(CSG.Matrix4x4.translation([-.5, 0, 0]));
m = m.multiply(CSG.Matrix4x4.scaling([1.1, 1.2, 1.3]));
// and apply the transform:
var cube3 = cube.transform(m);
</pre>
<h2>Mirroring</h2>
Solids can be mirrored in any plane in 3D space. For <a href="#math">plane math see below</a>.
<pre>
var cube = CSG.cube().translate([1,0,0]);
var cube2 = cube.mirroredX(); // mirrored in the x=0 plane
var cube3 = cube.mirroredY(); // mirrored in the y=0 plane
var cube4 = cube.mirroredZ(); // mirrored in the z=0 plane
// create a plane by specifying 3 points:
var plane = CSG.Plane.fromPoints([5,0,0], [5, 1, 0], [3, 1, 7]);
// and mirror in that plane:
var cube5 = cube.mirrored(plane);
</pre>
<h2>Cutting by a plane</h2>
A solid can be cut by a plane; only the part on the back side is kept:
<pre>
var cube = CSG.cube({radius: 10});
// create a plane by specifying 3 points:
var plane1 = CSG.Plane.fromPoints([5,0,0], [7, 1, 0], [3, 1, 7]);
// or by specifying a normal and a point on the plane:
var plane2 = CSG.Plane.fromNormalAndPoint([3, 1, 2], [5, 0, 0]);
// and cut by the plane:
var part1 = cube.cutByPlane(plane2);
// or if we need the other half of the cube:
var part2 = cube.cutByPlane(plane2.flipped());
</pre>
<h2>Expansion and contraction</h2>
Expansion can be seen
as the 3D convolution of an object with a sphere. Contraction is the reverse: the area outside the solid
is expanded, and this is then subtracted from the solid.
<br><br>
Expansion and contraction are very powerful ways to get an object with nice smooth corners. For example
a rounded cube can be created by expanding a normal cube.
<br><br>
Note that these are expensive operations: spheroids are created around every corner and edge in the original
object, so the number of polygons quickly increases. Expansion and contraction therefore are only practical for simple
non-curved objects.
<br><br>
expand() and contract() take two parameters: the first is the radius of expansion or contraction; the second
parameter is optional and specififies the resolution (number of polygons on spherical surfaces, per 360 degree revolution).
<pre>
var cube1 = CSG.cube({radius: 1.0});
var cube2 = CSG.cube({radius: 1.0}).translate([-0.3, -0.3, -0.3]);
var csg = cube1.subtract(cube2);
var rounded = csg.expand(0.2, 8);
</pre>
<h2>Using Properties</h2>
The 'property' property of a solid can be used to store metadata for the object,
for example the coordinate of a specific point of interest of the solid. Whenever
the object is transformed (i.e. rotated, scaled or translated), the properties
are transformed with it. So the property will keep pointing to the same point
of interest even after several transformations have been applied to the solid.
<br><br>
Properties can have any type, but only the properties of classes supporting
a 'transform' method will actually be transformed. This includes CSG.Vector3D,
CSG.Plane and CSG.Connector. In particular CSG.Connector properties (see below)
can be very useful: these can
be used to attach a solid to another solid at a predetermined location regardless of the
current orientation.
<br><br>
It's even possible to include a CSG solid as a property of another solid. This could
be used for example
to define the cutout cylinders to create matching screw holes for an object. Those 'solid properties'
get the same transformations as the owning solid but they will not be visible in the result
of CSG operations such as union().
<br><br>
Other kind of properties (for
example, strings) will still be included in the properties of the transformed
solid, but the properties will not get any transformation when the owning solid is transformed. <br><br>
All primitive solids have some predefined properties, such as the center point
of a sphere (TODO: document).
<br><br>
The solid resulting from CSG operations (union(), subtract(), intersect()) will get
the merged properties of both source solids. If identically named properties exist, only
one of them will be kept.
<pre>
var cube = CSG.cube({radius: 1.0});
cube.properties.aCorner = new CSG.Vector3D([1, 1, 1]);
cube = cube.translate([5, 0, 0]);
cube = cube.scale(2);
// cube.properties.aCorner will now point to [12, 2, 2],
// which is still the same corner point
// Properties can be stored in arrays; all properties in the array
// will be transformed if the solid is transformed:
cube.properties.otherCorners = [
new CSG.Vector3D([-1, 1, 1]),
new CSG.Vector3D([-1, -1, 1])
];
// and we can create sub-property objects; these must be of the
// CSG.Properties class. All sub properties will be transformed with
// the solid:
cube.properties.myProperties = new CSG.Properties();
cube.properties.myProperties.someProperty = new CSG.Vector3D([-1, -1, -1]);
</pre>
For an example see the <a href="servodemo.html">Servo motor</a> demo.
<h2>Connectors</h2>
The CSG.Connector class is intended to facilitate
attaching two solids to each other at a predetermined
location and orientation.
For example suppose we have a CSG solid depicting a servo motor
and a solid of a servo arm: by defining a Connector property for each of them, we
can easily attach the servo arm to the servo motor at the correct position
(i.e. the motor shaft) and orientation (i.e. arm perpendicular to the shaft)
even if we don't know their current position and orientation
in 3D space.<br><br>
In other words Connector give us the freedom to rotate and translate objects at will without the need
to keep track of their positions and boundaries. And if a third party library exposes connectors for
its solids, the user of the library does not have to know the actual dimensions or
shapes, only the names of the connector properties.
<br><br>
A CSG.Connector consist of 3 properties:<br>
<b>point</b>: a CSG.Vector3D defining the connection point in 3D space<br>
<b>axis</b>: a CSG.Vector3D defining the direction vector of the connection
(in the case of the servo motor example it would point in the direction of the shaft)<br>
<b>normal</b>: a CSG.Vector3D direction vector somewhat perpendicular to axis; this
defines the &quot;12 o'clock&quot; orientation of the connection.
<br><br>
When connecting two connectors, the solid is transformed such that the <b>point</b>
properties will be identical, the <b>axis</b> properties will have the same direction
(or opposite direction if mirror == true), and the <b>normal</b>s match as much as possible.
<br><br>
Connectors can be connected by means of two methods:<br>
A CSG solid's <b>connectTo()</b> function transforms a solid such that two connectors
become connected.<br>
Alternatively we can use a connector's <b>getTransformationTo()</b> method to obtain
a transformation matrix which would connect the connectors. This can be used if we
need to apply the same transform to multiple solids.
<pre>
var cube1 = CSG.cube({radius: 10});
var cube2 = CSG.cube({radius: 4});
// define a connector on the center of one face of cube1
// The connector's axis points outwards and its normal points
// towards the positive z axis:
cube1.properties.myConnector = new CSG.Connector([10, 0, 0], [1, 0, 0], [0, 0, 1]);
// define a similar connector for cube 2:
cube2.properties.myConnector = new CSG.Connector([0, -4, 0], [0, -1, 0], [0, 0, 1]);
// do some random transformations on cube 1:
cube1 = cube1.rotateX(30);
cube1 = cube1.translate([3.1, 2, 0]);
// Now attach cube2 to cube 1:
cube2 = cube2.connectTo(
cube2.properties.myConnector,
cube1.properties.myConnector,
true, // mirror
0 // normalrotation
);
// Or alternatively:
var matrix = cube2.properties.myConnector.getTransformationTo(
cube1.properties.myConnector,
true, // mirror
0 // normalrotation
);
cube2 = cube2.transform(matrix);
var result = cube2.union(cube1);
</pre>
For a more complete example see the <a href="servodemo.html">Servo motor</a> demo.
<h2>Determining the bounds of an object</h2>
The getBounds() function can be used to retrieve the bounding box of an object.
getBounds() returns
an array with two elements specifying the minimum x,y,z coordinate and the maximum x,y,z coordinate:
<pre>
var cube1 = CSG.cube({radius: 10});
var cube2 = CSG.cube({radius: 5});
// get the right bound of cube1 and the left bound of cube2:
var deltax = cube1.getBounds()[1].x - cube2.getBounds()[0].x;
// align cube2 so it touches cube1:
cube2 = cube2.translate([deltax, 0, 0]);
return cube1.union(cube2);
</pre>
<h2>2D shapes</h2>
Two dimensional shapes can be defined through the Polygon2D class. Currently this requires the polygon to be convex
(i.e. all corners less than 180 degrees). Shapes can be transformed (rotation, translation, scaling).
To actually use the shape it needs to be extruded into a 3D CSG object through the extrude() function. extrude()
places the 2D solid onto the z=0 plane, and extrudes in the specified direction. Extrusion can be done with an optional
twist. This rotates the solid around the z axis (and not necessariy around the extrusion axis) during extrusion.
The total degrees of rotation is specified in the twistangle parameter, and twiststeps determine the number of steps
between the bottom and top surface.
<pre>
// Create a shape; argument is an array of 2D coordinates
// The shape must be convex, can be specified in clockwise or in counterclockwise direction
var shape2d=new CSG.Polygon2D([[0,0], [5,0], [3,5], [0,5]]);
// Do some transformations:
shape2d=shape2d.translate([-2, -2]);
shape2d=shape2d.rotate(20);
shape2d=shape2d.scale([0.2, 0.2]);
// And extrude. This creates a CSG solid:
var extruded=shape2d.extrude({
offset: [0.5, 0, 2], // direction for extrusion
twistangle: 180, // top surface is rotated 180 degrees
twiststeps: 100 // create 100 slices
});
</pre>
For an example of 2D shapes see the <a href="hookdemo.html">Parametric S hook</a> demo.
<h2>2D Paths</h2>
A path is simply a series of points, connected by lines. A path can be open or closed (an additional line
is drawn between the first and last point). 2D paths are supported through the CSG.Path2D class.
<br><br>
Paths can be contructed either by giving a series of 2D coordinates, or through the CSG.Path2D.arc() function,
which creates a circular curved path. Paths can be concatenated, the result is a new path.
<br><br>
Creating a 3D solid is currently supported by the rectangularExtrude() function. This creates a 3D shape
by following the path with a 2D rectangle (upright, perpendicular to the path direction).
<pre>
var path = new CSG.Path2D([[10,10], [-10,10]], /* closed = */ false);
var anotherpath = new CSG.Path2D([[-10,-10]]);
path = path.concat(anotherpath);
path = path.appendPoint([10,-10]);
path = path.close(); // close the path
// of course we could simply have done:
// var path = new CSG.Path2D([[10,10], [-10,10], [-10,-10], [10,-10]], /* closed = */ true);
// We can make arcs and circles:
var curvedpath = CSG.Path2D.arc({
center: [0,0,0],
radius: 10,
startangle: 0,
endangle: 180,
resolution: 16,
});
// Extrude the path by following it with a rectangle (upright, perpendicular to the path direction)
// Returns a CSG solid
// width: width of the extrusion, in the z=0 plane
// height: height of the extrusion in the z direction
// resolution: number of segments per 360 degrees for the curve in a corner
// roundEnds: if true, the ends of the polygon will be rounded, otherwise they will be flat
var csg = path.rectangularExtrude(3, 4, 16, true);
return csg;
</pre>
<h2>Interactive parametric models</h2>
It is possible to make certain parameters
editable in the browser. This allows users not familiar with JavaScript to create customized STL files.
<br><br>
To do so, add a function getParameterDefinitions() to your .jscad source. This function should return
an array with parameter definitions. Currently 4 parameters types are supported: float, int, text and choice.
The user edited values of the parameters will be supplied as an object parameter to the main() function of your .jscad file.
<br><br>
A float, int or text parameter is created by including the following object in the array returned by getParameterDefinitions():
<pre>{
name: 'width',
type: 'float', // or 'text' or 'int'
default: 1.23, // optional, sets the initial value
caption: 'Width of the thingy:', // optional, displayed left of the input field
// if omitted, the 'name' is displayed (i.e. 'width')
}</pre>
A 'choice' parameter is created using the following object:
<pre>{
name: 'shape',
type: 'choice',
values: ["TRI", "SQU", "CIR"], // these are the values that will be supplied to your script
captions: ["Triangle", "Square", "Circle"], // optional, these values are shown in the listbox
// if omitted, the items in the 'values' array are used
caption: 'Shape:', // optional, displayed left of the input field
default: "SQU", // optional, default selected value
// if omitted, the first item is selected by default
}</pre>
To use the values add an argument to your main() function. This argument will be supplied an object
with the user edited parameter values:
<pre>
function main(params)
{
// custom error checking:
if(params.width <= 0) throw new Error("Width should be positive!");
if(params.shape == "TRI")
{
// do something
}
}
</pre>
A complete example. Copy/paste it into the Playground at the top of this page to see how it works:
<pre>
function getParameterDefinitions() {
return [
{
name: 'width',
type: 'float',
default: 10,
caption: "Width of the cube:",
},
{
name: 'height',
type: 'float',
default: 14,
caption: "Height of the cube:",
},
{
name: 'depth',
type: 'float',
default: 7,
caption: "Depth of the cube:",
},
{
name: 'rounded',
type: 'choice',
caption: 'Round the corners?',
values: [0, 1],
captions: ["No thanks", "Yes please"],
default: 1,
},
];
}
function main(params) {
var result;
if(params.rounded == 1)
{
result = CSG.roundedCube({radius: [params.width, params.height, params.depth], roundradius: 2, resolution: 32});
}
else
{
result = CSG.cube({radius: [params.width, params.height, params.depth]});
}
return result;
}
</pre>
Or see the <a href="gearsdemo.html">Gears demo</a> for another example of interactive parameters.
<h2>Miscellaneous</h2>
Solids can be given a color using the setColor(r, g, b) function. Beware: this returns a new solid,
the original solid is not modified! Faces of the solid will keep their original color when doing
CSG operations (union() etc). Colors values range between 0.0 and 1.0 (not 255).
<pre>
var cube1 = CSG.cube({radius: 10});
cube1 = cube1.setColor(0.5, 0, 0);
var cube2 = CSG.cube({radius: 10});
cube2 = cube2.setColor(0, 0.5, 0);
cube2 = cube2.translate([5,1,4]);
var result = cube1.subtract(cube2);
// the resulting solid will have faces with 2 different colors
</pre>
<h2><a name="math"></a>2D and 3D Math</h2>
There are utility classes for many 2D and 3D operations. Below is a quick summary, for details
view the source of csg.js:
<pre>
// --------- Vector3D ---------------------
var vec1 = new CSG.Vector3D(1,2,3); // 3 arguments
var vec2 = new CSG.Vector3D( [1,2,3] ); // 1 array argument
var vec3 = new CSG.Vector3D(vec2); // cloning a vector
// get the values as: vec1.x, vec.y, vec1.z
// vector math. All operations return a new vector, the original is unmodified!
// vectors cannot be modified. Instead you should create a new vector.
vec.negated()
vec.abs()
vec.plus(othervector)
vec.minus(othervector)
vec.times(3.0)
vec.dividedBy(-5)
vec.dot(othervector)
vec.lerp(othervector, t) // linear interpolation (0 &lt;= t &lt;= 1)
vec.length()
vec.lengthSquared() // == vec.length()^2
vec.unit()
vec.cross(othervector) // cross product: returns a vector perpendicular to both
vec.distanceTo(othervector)
vec.distanceToSquared(othervector) // == vec.distanceTo(othervector)^2
vec.equals(othervector)
vec.multiply4x4(matrix4x4) // right multiply by a 4x4 matrix
vec.min(othervector) // returns a new vector with the minimum x,y and z values
vec.max(othervector) // returns a new vector with the maximum x,y and z values
// --------- Vector2D ---------------------
var vec1 = new CSG.Vector2D(1,2); // 2 arguments
var vec2 = new CSG.Vector2D( [1,2] ); // 1 array argument
var vec3 = new CSG.Vector2D(vec2); // cloning a vector
// vector math. All operations return a new vector, the original is unmodified!
vec.negated()
vec.abs()
vec.plus(othervector)
vec.minus(othervector)
vec.times(3.0)
vec.dividedBy(-5)
vec.dot(othervector)
vec.lerp(othervector, t) // linear interpolation (0 &lt;= t &lt;= 1)
vec.length()
vec.unit()
vec.normal() // returns a 90 degree clockwise rotated vector
vec.distanceTo(othervector)
vec.equals(othervector)
vec.multiply4x4(matrix4x4) // right multiply by a 4x4 matrix
vec.toVector3D(z) // convert to a vector3D by adding a z coordinate
vec.angleDegrees() // returns the angle of the vector: [1,0] = 0 degrees, [0, 1] = 90 degrees, etc
vec.angleRadians() // ditto in radians
var vec = CSG.Vector2D.fromAngleDegrees(degrees); // returns a vector at the specified angle
var vec = CSG.Vector2D.fromAngleRadians(radians); // returns a vector at the specified angle
// --------- Matrix4x4 ---------------------
var m1 = new CSG.Matrix4x4(); // unity matrix
var m2 = new CSG.Matrix4x4( [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] );
// elements are passed in row order
var result = m1.plus(m2);
var result = m1.minus(m2);
var result = m1.multiply(m2);
// matrix vector multiplication (vectors are padded with zeroes to get a 4x1 vector):
var vec3d = m1.rightMultiply1x3Vector(vec3d); // matrix * vector
var vec3d = m1.leftMultiply1x3Vector(vec3d); // vector * matrix
var vec2d = m1.rightMultiply1x2Vector(vec2d); // matrix * vector
var vec2d = m1.leftMultiply1x2Vector(vec2d); // vector * matrix
// common transformation matrices:
var m = CSG.Matrix4x4.rotationX(degrees); // matrix for rotation about X axis
var m = CSG.Matrix4x4.rotationY(degrees); // matrix for rotation about Y axis
var m = CSG.Matrix4x4.rotationZ(degrees); // matrix for rotation about Z axis
var m = CSG.Matrix4x4.rotation(rotationCenter, rotationAxis, degrees); // rotation about arbitrary point and axis
var m = CSG.Matrix4x4.translation(vec3d); // translation
var m = CSG.Matrix4x4.scaling(vec3d); // scale
var m = CSG.Matrix4x4.mirroring(plane); // mirroring in a plane; the argument must be a CSG.Plane
// matrix transformations can be concatenated:
var transform = CSG.Matrix4x4.rotationX(20).multiply(CSG.Matrix4x4.rotationY(30));
// Use a CSG solid's transform() method to apply the transformation to a CSG solid
// ------------ Plane --------------------------
// a 3D plane is represented by a normal vector (should have unit length) and a distance from the origin w
// the plane passes through normal.times(w)
var plane1 = new CSG.Plane(normal, w);
// Or we can construct a plane from 3 points:
var plane2 = CSG.Plane.fromPoints(p1, p2, p3);
// Or from a normal vector and 1 point:
var plane3 = CSG.Plane.fromNormalAndPoint(normal, point);
// Flip a plane (front side becomes back side):
var plane4 = plane3.flipped();
// Apply transformations (rotation, scaling, translation):
var transformed = plane3.transformed(matrix4x4); // argument is a CSG.Matrix4x4
// Intersection of plane and 3d line:
var point = plane3.intersectWithLine(line); // argument is CSG.Line3D, returns a CSG.Vector3D
// Intersection of 2 planes:
var line = plane3.intersectWithPlane(plane); // argument is another CSG.Plane, returns a CSG.Line3D
// Distance to point:
var w = signedDistanceToPoint(point); // argument is CSG.Vector3D, returns a float (positive
// if in front of plane, negative if in back)
// ------------ Line3D --------------------------
// A line in 3d space is represented by a point and a direction vector.
// Direction should be a unit vector. Point can be any point on the line:
var line = new CSG.Line3D(point, direction); // argumenst are CSG.Vector3D
// or by giving two points:
var line = CSG.Line3D.fromPoints(p1, p2); // argumenst are CSG.Vector3D
var point = intersectWithPlane(plane); // == plane.intersectWithLine(this)
var line2 = line.reverse(); // same line but reverse direction
var line2 = line.transform(matrix4x4); // for rotation, scaling, etc
var p = line.closestPointOnLine(point); // project point onto the line
var d = line.distanceToPoint(point);
</pre>
</div> <!-- help -->
</body>
</html>