]>
git.scottworley.com Git - nt3d/blob - nt3d.js
1 /* nt3d - Javascript library for doing some 3D stuff.
2 * Copyright (C) 2012 Scott Worley <ScottWorley@ScottWorley.com>
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU Affero General Public License as
6 * published by the Free Software Foundation, either version 3 of the
7 * License, or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU Affero General Public License for more details.
14 * You should have received a copy of the GNU Affero General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 triangle : function ( a
, b
, c
) {
22 quad : function ( a
, b
, c
, d
) {
23 return this . triangle ( a
, b
, c
). concat (
24 this . triangle ( c
, d
, a
));
26 trianglefan : function ( fan
) {
28 for ( var i
= 2 ; i
< fan
. length
; i
++) {
29 result
. push ( fan
[ 0 ], fan
[ i
- 1 ], fan
[ i
]);
33 closed_trianglefan : function ( fan
) {
34 return this . trianglefan ( fan
. concat ([ fan
[ 1 ]]));
36 quadstrip : function ( strip
) {
37 if ( strip
. length
% 2 != 0 ) {
38 alert ( "quadstrip length not divisble by 2!" );
41 for ( var i
= 2 ; i
< strip
. length
; i
+= 2 ) {
42 result
= result
. concat ( this . quad ( strip
[ i
- 2 ], strip
[ i
- 1 ], strip
[ i
+ 1 ], strip
[ i
]));
46 closed_quadstrip : function ( strip
) {
47 return this . quadstrip ( strip
. concat ([ strip
[ 0 ], strip
[ 1 ]]));
49 circle : function ( r
, n
) {
51 for ( var i
= 0 ; i
< n
; i
++) {
52 points
. push ([ r
* Math
. cos ( 2 * Math
. PI
* i
/ n
),
53 r
* Math
. sin ( 2 * Math
. PI
* i
/ n
),
58 cone : function ( base_center
, apex
, radius
, steps
) {
59 var base
= this . circle ( radius
, steps
);
60 base
= this . rotate_onto ( base
, [ 0 , 0 , 1 ], this . sub ( apex
, base_center
));
61 base
= this . translate ( base
, base_center
);
62 return this . closed_trianglefan ([ apex
]. concat ( base
)). concat (
63 this . trianglefan ( base
. reverse ()));
65 sphere : function ( center
, radius
, latitude_steps
, longitude_steps
) {
66 return this . oriented_sphere ( center
, radius
, [ 0 , 0 , 1 ], [ 1 , 0 , 0 ], latitude_steps
, longitude_steps
);
68 oriented_sphere : function ( center
, radius
, north
, greenwich
, latitude_steps
, longitude_steps
) {
69 var unit_north
= this . unit ( north
);
70 var north_pole
= this . translate_point ( this . scale ( unit_north
, radius
), center
);
71 var south_pole
= this . translate_point ( this . scale ( unit_north
, - radius
), center
);
72 return this . spheroid ( north_pole
, south_pole
, radius
, greenwich
, latitude_steps
, longitude_steps
);
74 spheroid : function ( north_pole
, south_pole
, radius
, greenwich
, latitude_steps
, longitude_steps
) {
75 var delta
= this . sub ( north_pole
, south_pole
);
77 for ( var i
= 0 ; i
< latitude_steps
- 1 ; i
++) {
78 path
. push ( this . translate_point ( south_pole
, this . scale ( delta
, ( 1 - Math
. cos ( Math
. PI
* i
/(latitude_steps-1)))/ 2 )));
80 path
. push ( north_pole
);
82 return nt3d
. circle ( radius
* Math
. sin ( Math
. PI
* i
/( latitude_steps
- 1 )), longitude_steps
);
84 return this . extrude ( path
, shape
, delta
, greenwich
);
86 shapenormals_from_closed_path : function ( path
) {
88 var prev
= ( i
== 0 ) ? path
. length
- 1 : i
- 1 ;
89 var next
= ( i
== path
. length
- 1 ) ? 0 : i
+ 1 ;
90 return nt3d
. sub ( path
[ next
], path
[ prev
]);
93 shapenormals_from_path_and_extra_points : function ( path
, first_point
, last_point
) {
95 var prev
= ( i
== 0 ) ? first_point : path
[ i
- 1 ];
96 var next
= ( i
== path
. length
- 1 ) ? last_point : path
[ i
+ 1 ];
97 return nt3d
. sub ( next
, prev
);
100 shapenormals_from_path_and_first_and_last_normals : function ( path
, first_normal
, last_normal
) {
102 if ( i
== 0 ) { return first_normal
; }
103 if ( i
== path
. length
- 1 ) { return last_normal
; }
104 return nt3d
. sub ( path
[ i
+ 1 ], path
[ i
- 1 ]);
107 pathnormals_from_point : function ( path
, p
) {
108 // Use this with any point that is not on any path tangent line
109 var pathnormals
= [];
110 for ( var i
= 0 ; i
< path
. length
; i
++) {
111 pathnormals
. push ( this . sub ( path
[ i
], p
));
115 to_function : function ( thing
, make_indexer
) {
116 // If thing is a point, just yield thing every time.
117 // If thing is a list of points && make_indexer, index into thing.
118 // If thing is already a function, just return it.
119 if (({}). toString
. call ( thing
) === "[object Function]" ) {
120 return thing
; // Already a function
122 if ( make_indexer
&& Array
. isArray ( thing
[ 0 ])) {
123 // Looks like a list of points.
124 return function ( i
) { return thing
[ i
]; }
126 return function () { return thing
; }
128 extrude : function ( path
, shape
, shapenormals
, pathnormals
) {
130 var guts_result
= this . _extrude_guts ( path
, shape
, shapenormals
, pathnormals
);
132 // XXX: This doesn't work if shape is not convex
133 return guts_result
. points
. concat (
134 this . trianglefan ( guts_result
. first_loop
. reverse ()),
135 this . trianglefan ( guts_result
. last_loop
));
138 closed_extrude : function ( path
, shape
, shapenormals
, pathnormals
) {
139 var guts_result
= this . _extrude_guts ( path
, shape
, shapenormals
, pathnormals
);
140 // Stitch the ends together
141 return guts_result
. points
. concat (
142 this . closed_quadstrip ( this . zip ( guts_result
. first_loop
, guts_result
. last_loop
)));
144 _extrude_guts : function ( path
, shape
, shapenormals
, pathnormals
) {
145 var shape_fun
= this . to_function ( shape
, false );
146 var shapenormal_fun
= this . to_function ( shapenormals
, true );
147 var pathnormal_fun
= this . to_function ( pathnormals
, true );
148 var result
= { points : [] };
150 for ( var i
= 0 ; i
< path
. length
; i
++) {
151 var shapenormali
= shapenormal_fun ( i
, path
[ i
]);
152 var pathnormali
= pathnormal_fun ( i
, path
[ i
], shapenormali
);
154 // Fix pathnormali to be perfectly perpendicular to
155 // shapenormali. pathnormali must be perpendicular to
156 // shapenormali or the second rotation will take loop
157 // back out of the shapenormali plane that the first
158 // rotation so carefully placed it in. But, letting
159 // callers be sloppy with the pathnormals can greatly
160 // simplify generating them -- so much so that you can
161 // often just pass a constant to use the same value
162 // along the whole path.
163 pathnormali
= this . project_to_orthogonal ( shapenormali
, pathnormali
);
165 var shapei
= shape_fun ( i
, path
[ i
], shapenormali
, pathnormali
);
167 // loop is shapei in 3d with (0,0) at path[i], shape's
168 // z axis in the direction of shapenormali, and shape's
169 // x axis in the direction of pathnormali. We tack
170 // [1,0,0] onto the end as a hack to see where it ends
171 // up after the first rotation. This is removed later.
172 var loop
= shapei
. concat ([[ 1 , 0 , 0 ]]);
174 // This is done in three steps:
175 // 1. Rotate shape out of the xy plane so that [0,0,1]
176 // becomes shapenormali. This puts the shape in
177 // the correct plane, but does not constrain its
178 // rotation about shapenormali.
179 loop
= this . rotate_onto ( loop
, [ 0 , 0 , 1 ], shapenormali
);
180 var shapex
= loop
. pop ();
182 // 2. Rotate around shapenormali so that [1,0,0]
183 // becomes pathnormali.
184 if (! this . opposite ( shapex
, pathnormali
)) {
185 loop
= this . rotate_onto ( loop
, shapex
, pathnormali
);
187 // Rare edge case: When shapex and pathnormali are
188 // opposite, rotate_onto cannot cross them to get
189 // an axis of rotation. In this case, we (extrude)
190 // already know what to do -- just rotate PI around
192 loop
= this . rotate_about_origin ( loop
, shapenormali
, Math
. PI
);
195 // (This would probably be faster and more numerically stable
196 // if the two rotations were applied as one combined operation
197 // rather than separate steps.)
199 // 3. Translate to path[i].
200 loop
= this . translate ( loop
, path
[ i
]);
203 result
. first_loop
= loop
;
205 result
. points
= result
. points
. concat ( this . closed_quadstrip ( this . zip ( loop
, prev_loop
)));
209 result
. last_loop
= prev_loop
;
212 zip : function ( a
, b
) {
214 if ( a
. length
!= b
. length
) {
215 alert ( "Zip over different-sized inputs" );
217 for ( var i
= 0 ; i
< a
. length
; i
++) {
218 result
. push ( a
[ i
], b
[ i
]);
222 magnitude : function ( a
) {
223 return Math
. sqrt ( a
[ 0 ]* a
[ 0 ] + a
[ 1 ]* a
[ 1 ] + a
[ 2 ]* a
[ 2 ]);
226 return this . scale ( a
, 1 / this . magnitude ( a
));
228 sub : function ( a
, b
) {
234 return [- a
[ 0 ], - a
[ 1 ], - a
[ 2 ]];
236 dot : function ( a
, b
) {
237 return a
[ 0 ]* b
[ 0 ] + a
[ 1 ]* b
[ 1 ] + a
[ 2 ]* b
[ 2 ];
239 scale : function ( v
, s
) { // Scale vector v by scalar s
240 return [ s
* v
[ 0 ], s
* v
[ 1 ], s
* v
[ 2 ]];
242 cross : function ( a
, b
) {
243 return [ a
[ 1 ]* b
[ 2 ] - a
[ 2 ]* b
[ 1 ],
244 a
[ 2 ]* b
[ 0 ] - a
[ 0 ]* b
[ 2 ],
245 a
[ 0 ]* b
[ 1 ] - a
[ 1 ]* b
[ 0 ]];
247 normal : function ( a
, b
, c
) {
248 return this . cross ( this . sub ( a
, b
), this . sub ( b
, c
));
250 project : function ( a
, b
) { // Project b onto a
251 var a_magnitude
= this . magnitude ( a
);
252 return this . scale ( a
, this . dot ( a
, b
) / ( a_magnitude
* a_magnitude
));
254 project_to_orthogonal : function ( a
, b
) {
255 // The nearest thing to b that is orthogonal to a
256 return this . sub ( b
, this . project ( a
, b
));
258 translate : function ( points
, offset
) {
260 for ( var i
= 0 ; i
< points
. length
; i
++) {
261 translated
[ i
] = this . translate_point ( points
[ i
], offset
);
265 translate_point : function ( point
, offset
) {
266 return [ point
[ 0 ] + offset
[ 0 ],
267 point
[ 1 ] + offset
[ 1 ],
268 point
[ 2 ] + offset
[ 2 ]];
270 angle_between : function ( a
, b
) { // a and b must be unit vectors
271 var the_dot
= this . dot ( a
, b
);
278 return Math
. acos ( the_dot
);
280 rotate_about_origin : function ( points
, axis
, angle
) { // axis must be a unit vector
281 // From http://inside.mines.edu/~gmurray/ArbitraryAxisRotation/
282 var cosangle
= Math
. cos ( angle
);
283 var sinangle
= Math
. sin ( angle
);
285 for ( var i
= 0 ; i
< points
. length
; i
++) {
287 var tmp
= this . dot ( p
, axis
) * ( 1 - cosangle
);
289 axis
[ 0 ]* tmp
+ p
[ 0 ]* cosangle
+ (- axis
[ 2 ]* p
[ 1 ] + axis
[ 1 ]* p
[ 2 ])* sinangle
,
290 axis
[ 1 ]* tmp
+ p
[ 1 ]* cosangle
+ ( axis
[ 2 ]* p
[ 0 ] - axis
[ 0 ]* p
[ 2 ])* sinangle
,
291 axis
[ 2 ]* tmp
+ p
[ 2 ]* cosangle
+ (- axis
[ 1 ]* p
[ 0 ] + axis
[ 0 ]* p
[ 1 ])* sinangle
];
296 opposite : function ( a
, b
) {
297 // Do a and b point in exactly opposite directions?
298 return Math
. abs ( this . angle_between ( this . unit ( a
), this . unit ( b
)) - Math
. PI
) < this . angle_epsilon
;
300 rotate_onto : function ( points
, a
, b
) {
301 // Rotate points such that a (in points-space) maps onto b
302 // by crossing a and b to get a rotation axis and using
303 // angle_between to get a rotation angle.
304 var angle
= this . angle_between ( this . unit ( a
), this . unit ( b
));
305 var abs_angle
= Math
. abs ( angle
);
306 if ( Math
. abs ( angle
) < this . angle_epsilon
) {
307 // No significant rotation to perform. Bail to avoid
308 // NaNs and numerical error
312 if ( Math
. abs ( abs_angle
- Math
. PI
) < this . angle_epsilon
) {
313 // a and b point in opposite directions, so
314 // we cannot cross them. So just pick something.
315 // If the caller wishes to avoid this behaviour,
316 // they should check with this.opposite() first.
317 axis
= this . project_to_orthogonal ( a
, [ 1 , 0 , 0 ]);
318 console
. log ( "rotate_onto: a and b are opposite! If you carefully chose them to meet some other constraint, you will be sad! Arbitrarily using axis [1,0,0] ->" , axis
);
319 if ( this . magnitude ( axis
) < this . angle_epsilon
) {
320 // Oh, double bad luck! Our arbitrary choice
321 // lines up too! A second, orthogonal arbitrary
322 // choice is now guaranteed to succeed.
323 axis
= this . project_to_orthogonal ( a
, [ 0 , 1 , 0 ]);
324 console
. log ( "rotate_onto: Double bad luck! Arbitrarily using axis [0,1,0] ->" , axis
);
327 axis
= this . unit ( this . cross ( a
, b
));
329 return this . rotate_about_origin ( points
, axis
, angle
);
331 rotate : function ( points
, center
, axis
, angle
) { // axis must be a unit vector
332 return this . translate (
333 this . rotate_about_origin (
334 this . translate ( points
, this . neg ( center
)),
339 point_equal : function ( a
, b
, epsilon
) {
340 return Math
. abs ( a
[ 0 ] - b
[ 0 ]) < epsilon
&&
341 Math
. abs ( a
[ 1 ] - b
[ 1 ]) < epsilon
&&
342 Math
. abs ( a
[ 2 ] - b
[ 2 ]) < epsilon
;
344 degenerate_face_epsilon : 1e-10 ,
345 is_degenerate : function ( a
, b
, c
) {
346 return this . point_equal ( a
, b
, this . degenerate_face_epsilon
) ||
347 this . point_equal ( b
, c
, this . degenerate_face_epsilon
) ||
348 this . point_equal ( c
, a
, this . degenerate_face_epsilon
);
350 validate : function ( points
) {
351 // Do a little validation
352 if ( points
. length
% 3 != 0 ) {
353 alert ( "Points list length not divisble by 3!" );
356 var nan_point_count
= 0 ;
357 var nan_face_count
= 0 ;
358 for ( var i
= 0 ; i
< points
. length
/ 3 ; i
++) {
359 var nan_in_face
= false ;
360 for ( var j
= 0 ; j
< 3 ; j
++) {
361 var nan_in_point
= false ;
362 for ( var k
= 0 ; k
< 3 ; k
++) {
363 if ( isNaN ( points
[ i
* 3 + j
][ k
])) {
369 if ( nan_in_point
) nan_point_count
++;
371 if ( nan_in_face
) nan_face_count
++;
373 if ( nan_count
!= 0 ) {
374 alert ( nan_count
+ " NaNs in " + nan_point_count
+ " points in " + nan_face_count
+ " faces (" + ( 100 * nan_face_count
/ ( points
. length
/ 3 )) + "% of faces)." );
377 remove_degenerate_faces : function ( points
) {
378 // Note: This modifies points
379 var degenerate_face_count
= 0 ;
380 for ( var i
= 0 ; i
< points
. length
/ 3 ; i
++) {
381 if ( this . is_degenerate ( points
[ i
* 3 + 0 ],
384 points
. splice ( i
* 3 , 3 );
386 degenerate_face_count
++;
389 if ( degenerate_face_count
!= 0 ) {
390 console
. log ( "Removed " + degenerate_face_count
+ " degenerate faces" );
394 to_stl : function ( points
, name
) {
395 var stl
= "solid " + name
+ " \n " ;
396 for ( var i
= 0 ; i
< points
. length
/ 3 ; i
++) {
397 var a
= points
[ i
* 3 + 0 ];
398 var b
= points
[ i
* 3 + 1 ];
399 var c
= points
[ i
* 3 + 2 ];
400 var normal
= this . normal ( a
, b
, c
);
401 stl
+= "facet normal " + normal
[ 0 ] + " " + normal
[ 1 ] + " " + normal
[ 2 ] + " \n " +
403 "vertex " + a
[ 0 ] + " " + a
[ 1 ] + " " + a
[ 2 ] + " \n " +
404 "vertex " + b
[ 0 ] + " " + b
[ 1 ] + " " + b
[ 2 ] + " \n " +
405 "vertex " + c
[ 0 ] + " " + c
[ 1 ] + " " + c
[ 2 ] + " \n " +
409 stl
+= "endsolid " + name
+ " \n " ;
413 // Remove any previous download links
414 var old_download_link
= document
. getElementById ( "nt3d_download" );
415 if ( old_download_link
) {
416 old_download_link
. parentNode
. removeChild ( old_download_link
);
419 // Continue in a callback, so that there's not a stale download
420 // link hanging around while we process.
421 setTimeout ( function () {
423 // Get params from form
425 for ( var i
= 0 ; i
< this . user_params
. length
; i
++) {
426 var as_string
= this . form
. elements
[ "param" + i
]. value
;
427 var as_num
= + as_string
;
428 params
[ this . user_params
[ i
][ 0 ]] = isNaN ( as_num
) ? as_string : as_num
;
431 this . points
= this . user_function
. call ( null , params
);
433 this . validate ( this . points
);
435 this . remove_degenerate_faces ( this . points
);
437 this . stl
= this . to_stl ( this . points
, this . user_function
. name
);
439 // Offer result as download
440 var download_link
= document
. createElement ( "a" );
441 download_link
. appendChild ( document
. createTextNode ( "Download!" ));
442 download_link
. setAttribute ( "id" , "nt3d_download" );
443 download_link
. setAttribute ( "style" , "background-color: blue" );
444 download_link
. setAttribute ( "download" , this . user_function
. name
+ ".stl" );
445 download_link
. setAttribute ( "href" , "data:application/sla," + encodeURIComponent ( this . stl
));
446 this . ui
. appendChild ( download_link
);
447 setTimeout ( function () { download_link
. setAttribute ( "style" , "-webkit-transition: background-color 0.4s; -moz-transition: background-color 0.4s; -o-transition: background-color 0.4s; -ms-transition: background-color 0.4s; transition: background-color 0.4s; background-color: inherit" ); }, 0 );
449 }. bind ( this ), 0 ); // (We were in a callback this whole time, remember?)
451 framework : function ( f
, params
) {
452 this . user_function
= f
;
453 this . user_params
= params
;
456 this . ui
= document
. getElementById ( "nt3dui" );
458 this . ui
= document
. createElement ( "div" );
459 this . ui
. setAttribute ( "id" , "nt3dui" );
460 document
. body
. appendChild ( this . ui
);
462 this . form
= document
. createElement ( "form" );
463 this . form
. setAttribute ( "onsubmit" , "nt3d.go(); return false" );
464 this . ui
. appendChild ( this . form
);
465 var table
= document
. createElement ( "table" );
466 this . form
. appendChild ( table
);
467 var tr
= document
. createElement ( "tr" );
468 table
. appendChild ( tr
);
469 var th
= document
. createElement ( "th" );
470 th
. appendChild ( document
. createTextNode ( "Variable" ));
472 th
= document
. createElement ( "th" );
473 th
. appendChild ( document
. createTextNode ( "Value" ));
475 for ( var i
= 0 ; i
< params
. length
; i
++) {
476 tr
= document
. createElement ( "tr" );
477 table
. appendChild ( tr
);
478 var td
= document
. createElement ( "td" );
480 if ( params
[ i
]. length
> 2 ) {
481 description
= params
[ i
][ 2 ];
483 description
= params
[ i
][ 0 ];
484 description
= description
[ 0 ]. toUpperCase () + description
. substr ( 1 );
485 description
= description
. replace ( /_(.)/g , function ( _
, c
) {
486 return " " + c
. toUpperCase ();
488 description
= description
. replace ( "Num " , "Number of " );
490 td
. appendChild ( document
. createTextNode ( description
));
492 td
= document
. createElement ( "td" );
493 var input
= document
. createElement ( "input" );
494 input
. setAttribute ( "name" , "param" + i
);
495 input
. setAttribute ( "value" , params
[ i
][ 1 ]);
496 td
. appendChild ( input
);
499 var go
= document
. createElement ( "input" );
500 go
. setAttribute ( "type" , "button" );
501 go
. setAttribute ( "value" , "Go!" );
502 go
. setAttribute ( "onclick" , "nt3d.go()" );
503 this . form
. appendChild ( go
);