diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1377554
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.swp
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4b98043
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,25 @@
+# Relies on minify: https://www.npmjs.com/package/minify
+
+# Scripts to help sed wrap our javascript in an anonymous function
+# Helps with namespacing + minification
+SED_JS_PREFIX = '1i(() => {'
+SED_JS_SUFFIX = '$$a})();'
+
+# Javscript build rule
+# > This concatenates + wraps all matching JS files and minifies
+build-js: scripts/*.js
+build-js:
+ sed -e $(SED_JS_PREFIX) -e $(SED_JS_SUFFIX) $^ \
+ | minify --js \
+ > build/stlwebviewer2.js
+
+# CSS build rule
+# > This simply concatenates all matching CSS files and minifies
+build-css: stylesheets/*.css
+build-css:
+ cat $^ \
+ | minify --css \
+ > build/stlwebviewer2.css
+
+build: build-css build-js
+.DEFAULT_GOAL := build
diff --git a/README.md b/README.md
index 76325a0..b8b3865 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,24 @@
# stl_web_viewer2
-Just a fork from https://github.com/brentyi/stl_web_viewer2
\ No newline at end of file
+Demo: https://brentyi.github.io/stl_web_viewer2/
+
+Friendly utility for embedding 3D models into webpages.
+
+Basically the same as the [original](https://github.com/brentyi/stl_web_viewer), but with less nodejs nonsense.
+
+### Usage:
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+```
diff --git a/build/stlwebviewer2.css b/build/stlwebviewer2.css
new file mode 100644
index 0000000..f5c3ff7
--- /dev/null
+++ b/build/stlwebviewer2.css
@@ -0,0 +1 @@
+.stlwv2-model{position:relative;min-height:20em;border:1px solid #ccc}.stlwv2-inner{position:absolute;overflow:hidden;top:0;bottom:0;left:0;right:0}.stlwv2-inner>.stlwv2-percent{position:absolute;top:50%;transform:translateY(-50%);font-size:5em;text-align:center;width:100%;color:#ccc;animation-name:stlwv2-pulse;animation-duration:2s;animation-iteration-count:infinite}@keyframes stlwv2-pulse{0%{opacity:1}50%{opacity:.625}}.stlwv2-inner>canvas{position:absolute;top:0;left:0;opacity:0;transition:opacity .5s}.stlwv2-inner.stlwv2-loaded>canvas{opacity:1}.stlwv2-fullscreen-checkbox{display:none!important}.stlwv2-hud{display:none}.stlwv2-loaded>.stlwv2-hud{position:absolute;padding:.25em;z-index:1000;cursor:pointer;font-weight:400}.stlwv2-loaded>.stlwv2-github-link{font-size:1.2em;top:.57em;right:3em;text-decoration:none;color:#999;display:none}.stlwv2-loaded>.stlwv2-fullscreen-on{font-size:1.5em;top:0;right:.2em;transform:rotate(90deg);color:#ccc;display:block}.stlwv2-loaded>.stlwv2-fullscreen-off{font-size:2em;top:0;right:.5em;color:#c33;display:none}.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-github-link{display:block}.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-on{display:none}.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-off{display:block}
diff --git a/build/stlwebviewer2.js b/build/stlwebviewer2.js
new file mode 100644
index 0000000..eb62821
--- /dev/null
+++ b/build/stlwebviewer2.js
@@ -0,0 +1 @@
+(()=>{var e=function(){return void 0!==window.pageYOffset?window.pageYOffset:document.documentElement.scrollTop?document.documentElement.scrollTop:document.body.scrollTop?document.body.scrollTop:0},t=function(){return void 0!==window.pageXOffset?window.pageXOffset:document.documentElement.scrollLeft?document.documentElement.scrollLeft:document.body.scrollLeft?document.body.scrollLeft:0},n=function(e){this.manager=void 0!==e?e:THREE.DefaultLoadingManager};function o(e,t){if(this.modelUrl=e,this.$container=t,!a.webgl)return void a.addGetWebGLMessage({parent:this.$container[0]});o.instanceCount=(o.instanceCount||0)+1;let r="stlwv2-fullscreen-checkbox-"+(o.instanceCount-1);this.$container.append(['','"].join("\n")),this.$innerContainer=this.$container.children(".stlwv2-inner"),this.$fullscreenCheckbox=$("#"+r),this.$fullscreenCheckbox.on("change",this.fullscreenToggleHandler.bind(this)),this.scene=new THREE.Scene,this.camera=new THREE.PerspectiveCamera(40,this.$innerContainer.width()/this.$innerContainer.height(),1,15e3),this.cameraTarget=new THREE.Vector3,this.renderer=this.makeRenderer(antialias=!0),this.$innerContainer.append(this.renderer.domElement),this.controls=new i(this.camera,this.$innerContainer.get(0)),this.controls.target=this.cameraTarget,this.controls.enableDamping=!0,this.controls.enableKeys=!1,this.controls.rotateSpeed=.15,this.controls.dampingFactor=.125,this.controls.enableZoom=!0,this.controls.autoRotate=!0,this.controls.autoRotateSpeed=.25,this.controls.autoRotateDelay=5e3,this.hemisphereLight=new THREE.HemisphereLight(10066329,5592405),this.scene.add(this.hemisphereLight),this.pointLight=new THREE.PointLight(14540253,.75,0),this.camera.add(this.pointLight),this.scene.add(this.camera),(new n).load(this.modelUrl,this.stlLoadedCallback.bind(this),this.updateProgress.bind(this))}n.prototype={constructor:n,load:function(e,t,n,o){var a=this,i=new THREE.FileLoader(a.manager);i.setResponseType("arraybuffer"),i.load(e,(function(e){t(a.parse(e))}),n,o)},parse:function(e){var t=this.ensureBinary(e);return function(){var e;if(50,84+50*(e=new DataView(t)).getUint32(80,!0)===e.byteLength)return!0;for(var n=e.byteLength,o=0;o127)return!0;return!1}()?this.parseBinary(t):this.parseASCII(this.ensureString(e))},parseBinary:function(e){for(var t,n,o,a,i,r,s,c,l=new DataView(e),d=l.getUint32(80,!0),h=!1,u=0;u<70;u++)1129270351==l.getUint32(u,!1)&&82==l.getUint8(u+4)&&61==l.getUint8(u+5)&&(h=!0,a=[],i=l.getUint8(u+6)/255,r=l.getUint8(u+7)/255,s=l.getUint8(u+8)/255,c=l.getUint8(u+9)/255);for(var m=new THREE.BufferGeometry,p=[],b=[],f=0;f>5&31)/31,o=(y>>10&31)/31):(t=i,n=r,o=s)}for(var R=1;R<=3;R++){var T=g+12*R;p.push(l.getFloat32(T,!0)),p.push(l.getFloat32(T+4,!0)),p.push(l.getFloat32(T+8,!0)),b.push(E,w,v),h&&a.push(t,n,o)}}return m.addAttribute("position",new THREE.BufferAttribute(new Float32Array(p),3)),m.addAttribute("normal",new THREE.BufferAttribute(new Float32Array(b),3)),h&&(m.addAttribute("color",new THREE.BufferAttribute(new Float32Array(a),3)),m.hasColors=!0,m.alpha=c),m},parseASCII:function(e){var t,n,o,a,i,r;t=new THREE.BufferGeometry,n=/facet([\s\S]*?)endfacet/g;for(var s=[],c=[],l=new THREE.Vector3;null!==(i=n.exec(e));){for(r=i[0],o=/normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;null!==(i=o.exec(r));)l.x=parseFloat(i[1]),l.y=parseFloat(i[3]),l.z=parseFloat(i[5]);for(a=/vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;null!==(i=a.exec(r));)s.push(parseFloat(i[1]),parseFloat(i[3]),parseFloat(i[5])),c.push(l.x,l.y,l.z)}return t.addAttribute("position",new THREE.BufferAttribute(new Float32Array(s),3)),t.addAttribute("normal",new THREE.BufferAttribute(new Float32Array(c),3)),t},ensureString:function(e){if("string"!=typeof e){for(var t=new Uint8Array(e),n=[],o=0;o{$(".stlwv2-model").each((function(){let e=$(this);new o(e.data("model-url"),e)})),$(document).keyup((function(e){"Escape"===e.key&&$(".stlwv2-model .stlwv2-fullscreen-checkbox").each((function(){$(this).prop("checked")&&$(this).prop("checked",!1).trigger("change")}))}))}),o.prototype.updateProgress=function(e){console.log("Loading "+this.modelUrl+": "+e.loaded+"/"+e.total),this.$innerContainer.children(".stlwv2-percent").text(Math.floor(e.loaded/e.total*100)+"%")},o.prototype.stlLoadedCallback=function(e){let t=new THREE.MeshPhongMaterial({color:16251135,specular:1118481,shininess:0,wireframe:!1,polygonOffset:!0,polygonOffsetFactor:1,polygonOffsetUnits:1,transparent:!0,opacity:.85}),n=new THREE.Mesh(e,t);n.position.set(0,0,0),n.castShadow=!1,n.receiveShadow=!1,this.scene.add(n);let o=new THREE.EdgesGeometry(e,29),a=new THREE.LineSegments(o,new THREE.LineBasicMaterial({color:6710886}));this.scene.add(a),e.computeBoundingSphere(),e.computeBoundingBox(),this.cameraTarget.copy(e.boundingSphere.center);let i=e.boundingSphere.radius;this.controls.maxDistance=10*i,this.pointLight.position.set(0,i,0),this.camera.position.set(1.5*i+this.cameraTarget.x,1.5*i+this.cameraTarget.y,1.5*i+this.cameraTarget.z),this.animate(),this.$innerContainer.addClass("stlwv2-loaded")},o.prototype.animate=function(){if(this.animateLoops=(this.animateLoops||0)+1,!this.performanceChecked)if(5==this.animateLoops)this.performanceCheckStartTime=performance.now();else if(this.animateLoops>5){let e=performance.now()-this.performanceCheckStartTime;if(e>2e3){let t=1e3*(this.animateLoops-5)/e;console.log("Cumulative framerate: "+t),t<30&&(console.log("Disabling anti-aliasing"),this.renderer.domElement.remove(),delete this.renderer,this.renderer=this.makeRenderer(antialias=!1),this.$innerContainer.append(this.renderer.domElement)),this.performanceChecked=!0}}this.camera.aspect=this.$innerContainer.width()/this.$innerContainer.height(),this.camera.updateProjectionMatrix(),this.renderer.setSize(this.$innerContainer.width(),this.$innerContainer.height()),requestAnimationFrame(this.animate.bind(this)),this.controls.update(),this.renderer.render(this.scene,this.camera)},o.prototype.makeRenderer=function(e){let t=new THREE.WebGLRenderer({antialias:e});return t.setClearColor(16777215),t.setPixelRatio(Math.min(window.devicePixelRatio,2)),t.gammaInput=!0,t.gammaOutput=!0,t.shadowMap.enabled=!0,t},o.prototype.fullscreenToggleHandler=function(){let n=this.$container.position().top-e()+1,o=this.$container.position().left-t()+1,a=$(window).height()-(n+this.$container.innerHeight())+1,i=this.$container.width()-2;this.$fullscreenCheckbox.prop("checked")?(this.$innerContainer.css({top:n+"px",bottom:a+"px",left:o+"px",width:i+"px",position:"fixed",opacity:"0.5","z-index":2e3}),this.$innerContainer.animate({top:"0",bottom:"0",left:"0",width:"100%",opacity:"1"},300,()=>{this.$innerContainer.animate({opacity:"1"},500)})):(this.$innerContainer.css({opacity:"0.5"}),this.$innerContainer.animate({top:n+"px",bottom:a+"px",left:o+"px",width:i+"px"},300,()=>{this.$innerContainer.css({position:"",top:"",bottom:"",left:"",width:"","z-index":""}),this.$innerContainer.animate({opacity:"1"},500)}))};var a={canvas:!!window.CanvasRenderingContext2D,webgl:function(){try{var e=document.createElement("canvas");return!(!window.WebGLRenderingContext||!e.getContext("webgl")&&!e.getContext("experimental-webgl"))}catch(e){return!1}}(),workers:!!window.Worker,fileapi:window.File&&window.FileReader&&window.FileList&&window.Blob,getWebGLErrorMessage:function(){var e=document.createElement("div");return e.id="webgl-error-message",e.style.fontFamily="monospace",e.style.fontSize="13px",e.style.fontWeight="normal",e.style.textAlign="center",e.style.background="#fff",e.style.color="#000",e.style.padding="1.5em",e.style.width="400px",e.style.margin="5em auto 0",this.webgl||(e.innerHTML=window.WebGLRenderingContext?['Your graphics card does not seem to support WebGL.
','Find out how to get it here.'].join("\n"):['Your browser does not seem to support WebGL.
','Find out how to get it here.'].join("\n")),e},addGetWebGLMessage:function(e){var t,n,o;t=void 0!==(e=e||{}).parent?e.parent:document.body,n=void 0!==e.id?e.id:"oldie",(o=a.getWebGLErrorMessage()).id=n,t.appendChild(o)}};"object"==typeof module&&(module.exports=a);var i=function(e,t){var n,o,a,i,r;this.object=e,this.domElement=void 0!==t?t:document,this.enabled=!0,this.target=new THREE.Vector3,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.minAzimuthAngle=-1/0,this.maxAzimuthAngle=1/0,this.enableDamping=!1,this.dampingFactor=.25,this.enableZoom=!0,this.zoomSpeed=1,this.enableRotate=!0,this.rotateSpeed=1,this.enablePan=!0,this.keyPanSpeed=7,this.autoRotate=!1,this.autoRotateSpeed=2,this.autoRotateDelay=0,this.autoRotateTimeout,this.enableKeys=!0,this.keys={LEFT:37,UP:38,RIGHT:39,BOTTOM:40},this.mouseButtons={ORBIT:THREE.MOUSE.LEFT,ZOOM:THREE.MOUSE.MIDDLE,PAN:THREE.MOUSE.RIGHT},this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.zoom0=this.object.zoom,this.getPolarAngle=function(){return p.phi},this.getAzimuthalAngle=function(){return p.theta},this.reset=function(){s.target.copy(s.target0),s.object.position.copy(s.position0),s.object.zoom=s.zoom0,s.object.updateProjectionMatrix(),s.dispatchEvent(c),s.update(),u=h.NONE},this.update=(n=new THREE.Vector3,o=(new THREE.Quaternion).setFromUnitVectors(e.up,new THREE.Vector3(0,1,0)),a=o.clone().inverse(),i=new THREE.Vector3,r=new THREE.Quaternion,function(){var e=s.object.position;return n.copy(e).sub(s.target),n.applyQuaternion(o),p.setFromVector3(n),s.autoRotate&&u===h.NONE&&!w?P(2*Math.PI/60/60*s.autoRotateSpeed):s.autoRotate&&s.autoRotateDelay>0&&(s.autoRotateSpeed>0&&(s.autoRotateSpeedActual=s.autoRotateSpeed,s.autoRotateSpeed=0),clearTimeout(s.autoRotateTimeout),s.autoRotateTimeout=setTimeout((function(){s.autoRotateSpeed=s.autoRotateSpeedActual}),s.autoRotateDelay),w=!1),p.theta+=b.theta,p.phi+=b.phi,p.theta=Math.max(s.minAzimuthAngle,Math.min(s.maxAzimuthAngle,p.theta)),p.phi=Math.max(s.minPolarAngle,Math.min(s.maxPolarAngle,p.phi)),p.makeSafe(),p.radius*=f,p.radius=Math.max(s.minDistance,Math.min(s.maxDistance,p.radius)),s.target.add(g),n.setFromSpherical(p),n.applyQuaternion(a),e.copy(s.target).add(n),s.object.lookAt(s.target),!0===s.enableDamping?(b.theta*=1-s.dampingFactor,b.phi*=1-s.dampingFactor):b.set(0,0,0),f=1,g.set(0,0,0),!!(E||i.distanceToSquared(s.object.position)>m||8*(1-r.dot(s.object.quaternion))>m)&&(s.dispatchEvent(c),i.copy(s.object.position),r.copy(s.object.quaternion),E=!1,!0)}),this.dispose=function(){s.domElement.removeEventListener("contextmenu",I,!1),s.domElement.removeEventListener("mousedown",D,!1),s.domElement.removeEventListener("wheel",z,!1),s.domElement.removeEventListener("touchstart",B,!1),s.domElement.removeEventListener("touchend",Z,!1),s.domElement.removeEventListener("touchmove",G,!1),document.removeEventListener("mousemove",N,!1),document.removeEventListener("mouseup",V,!1),document.removeEventListener("mouseleave",V,!1),window.removeEventListener("keydown",Y,!1)};var s=this,c={type:"change"},l={type:"start"},d={type:"end"},h={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_DOLLY:4,TOUCH_PAN:5},u=h.NONE,m=1e-6,p=new THREE.Spherical,b=new THREE.Spherical,f=1,g=new THREE.Vector3,E=!1,w=!1,v=new THREE.Vector2,y=new THREE.Vector2,R=new THREE.Vector2,T=new THREE.Vector2,C=new THREE.Vector2,L=new THREE.Vector2,O=new THREE.Vector2,H=new THREE.Vector2,x=new THREE.Vector2;function A(){return Math.pow(.95,s.zoomSpeed)}function P(e){b.theta-=e}function k(e){b.phi-=e}var M,j=(M=new THREE.Vector3,function(e,t){M.setFromMatrixColumn(t,0),M.multiplyScalar(-e),g.add(M)}),S=function(){var e=new THREE.Vector3;return function(t,n){e.setFromMatrixColumn(n,1),e.multiplyScalar(t),g.add(e)}}(),F=function(){var e=new THREE.Vector3;return function(t,n){var o=s.domElement===document?s.domElement.body:s.domElement;if(s.object instanceof THREE.PerspectiveCamera){var a=s.object.position;e.copy(a).sub(s.target);var i=e.length();i*=Math.tan(s.object.fov/2*Math.PI/180),j(2*t*i/o.clientHeight,s.object.matrix),S(2*n*i/o.clientHeight,s.object.matrix)}else s.object instanceof THREE.OrthographicCamera?(j(t*(s.object.right-s.object.left)/s.object.zoom/o.clientWidth,s.object.matrix),S(n*(s.object.top-s.object.bottom)/s.object.zoom/o.clientHeight,s.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),s.enablePan=!1)}}();function U(e){s.object instanceof THREE.PerspectiveCamera?f/=e:s.object instanceof THREE.OrthographicCamera?(s.object.zoom=Math.max(s.minZoom,Math.min(s.maxZoom,s.object.zoom*e)),s.object.updateProjectionMatrix(),E=!0):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),s.enableZoom=!1)}function $(e){s.object instanceof THREE.PerspectiveCamera?f*=e:s.object instanceof THREE.OrthographicCamera?(s.object.zoom=Math.max(s.minZoom,Math.min(s.maxZoom,s.object.zoom/e)),s.object.updateProjectionMatrix(),E=!0):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),s.enableZoom=!1)}function D(e){if(!1!==s.enabled){if(e.preventDefault(),e.button===s.mouseButtons.ORBIT){if(!1===s.enableRotate)return;!function(e){v.set(e.clientX,e.clientY)}(e),u=h.ROTATE}else if(e.button===s.mouseButtons.ZOOM){if(!1===s.enableZoom)return;!function(e){O.set(e.clientX,e.clientY)}(e),u=h.DOLLY}else if(e.button===s.mouseButtons.PAN){if(!1===s.enablePan)return;!function(e){T.set(e.clientX,e.clientY)}(e),u=h.PAN}u!==h.NONE&&(document.addEventListener("mousemove",N,!1),document.addEventListener("mouseup",V,!1),document.addEventListener("mouseleave",V,!1),s.dispatchEvent(l))}}function N(e){if(!1!==s.enabled)if(e.preventDefault(),u===h.ROTATE){if(!1===s.enableRotate)return;!function(e){y.set(e.clientX,e.clientY),R.subVectors(y,v);var t=s.domElement===document?s.domElement.body:s.domElement;P(2*Math.PI*R.x/t.clientWidth*s.rotateSpeed),k(2*Math.PI*R.y/t.clientHeight*s.rotateSpeed),v.copy(y),s.update()}(e)}else if(u===h.DOLLY){if(!1===s.enableZoom)return;!function(e){H.set(e.clientX,e.clientY),x.subVectors(H,O),x.y>0?U(A()):x.y<0&&$(A()),O.copy(H),s.update()}(e)}else if(u===h.PAN){if(!1===s.enablePan)return;!function(e){C.set(e.clientX,e.clientY),L.subVectors(C,T),F(L.x,L.y),T.copy(C),s.update()}(e)}}function V(e){!1!==s.enabled&&(document.removeEventListener("mousemove",N,!1),document.removeEventListener("mouseup",V,!1),document.removeEventListener("mouseleave",V,!1),s.dispatchEvent(d),u=h.NONE)}function z(e){if(!1!==s.enabled&&!1!==s.enableZoom&&(u===h.NONE||u===h.ROTATE))return function(e){e.deltaY<0?$(A()):e.deltaY>0&&U(A()),s.update()}(e),s.dispatchEvent(l),s.dispatchEvent(d),w=!0,e.preventDefault(),e.stopPropagation(),!1}function Y(e){!1!==s.enabled&&!1!==s.enableKeys&&!1!==s.enablePan&&function(e){switch(e.keyCode){case s.keys.UP:F(0,s.keyPanSpeed),s.update();break;case s.keys.BOTTOM:F(0,-s.keyPanSpeed),s.update();break;case s.keys.LEFT:F(s.keyPanSpeed,0),s.update();break;case s.keys.RIGHT:F(-s.keyPanSpeed,0),s.update()}}(e)}function B(e){if(!1!==s.enabled){switch(e.touches.length){case 1:if(!1===s.enableRotate)return;!function(e){v.set(e.touches[0].pageX,e.touches[0].pageY)}(e),u=h.TOUCH_ROTATE;break;case 2:if(!1===s.enableZoom)return;!function(e){var t=e.touches[0].pageX-e.touches[1].pageX,n=e.touches[0].pageY-e.touches[1].pageY,o=Math.sqrt(t*t+n*n);O.set(0,o)}(e),u=h.TOUCH_DOLLY;break;case 3:if(!1===s.enablePan)return;!function(e){T.set(e.touches[0].pageX,e.touches[0].pageY)}(e),u=h.TOUCH_PAN;break;default:u=h.NONE}u!==h.NONE&&s.dispatchEvent(l)}}function G(e){if(!1!==s.enabled)switch(e.preventDefault(),e.stopPropagation(),e.touches.length){case 1:if(!1===s.enableRotate)return;if(u!==h.TOUCH_ROTATE)return;!function(e){y.set(e.touches[0].pageX,e.touches[0].pageY),R.subVectors(y,v);var t=s.domElement===document?s.domElement.body:s.domElement;P(2*Math.PI*R.x/t.clientWidth*s.rotateSpeed),k(2*Math.PI*R.y/t.clientHeight*s.rotateSpeed),v.copy(y),s.update()}(e);break;case 2:if(!1===s.enableZoom)return;if(u!==h.TOUCH_DOLLY)return;!function(e){var t=e.touches[0].pageX-e.touches[1].pageX,n=e.touches[0].pageY-e.touches[1].pageY,o=Math.sqrt(t*t+n*n);H.set(0,o),x.subVectors(H,O),x.y>0?$(A()):x.y<0&&U(A()),O.copy(H),s.update()}(e);break;case 3:if(!1===s.enablePan)return;if(u!==h.TOUCH_PAN)return;!function(e){C.set(e.touches[0].pageX,e.touches[0].pageY),L.subVectors(C,T),F(L.x,L.y),T.copy(C),s.update()}(e);break;default:u=h.NONE}}function Z(e){!1!==s.enabled&&(s.dispatchEvent(d),u=h.NONE)}function I(e){e.preventDefault()}s.domElement.addEventListener("contextmenu",I,!1),s.domElement.addEventListener("mousedown",D,!1),s.domElement.addEventListener("wheel",z,!1),s.domElement.addEventListener("touchstart",B,!1),s.domElement.addEventListener("touchend",Z,!1),s.domElement.addEventListener("touchmove",G,!1),window.addEventListener("keydown",Y,!1),this.update()};i.prototype=Object.create(THREE.EventDispatcher.prototype),i.prototype.constructor=i,Object.defineProperties(i.prototype,{center:{get:function(){return console.warn("OrbitControls: .center has been renamed to .target"),this.target}},noZoom:{get:function(){return console.warn("OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."),!this.enableZoom},set:function(e){console.warn("OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."),this.enableZoom=!e}},noRotate:{get:function(){return console.warn("OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."),!this.enableRotate},set:function(e){console.warn("OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."),this.enableRotate=!e}},noPan:{get:function(){return console.warn("OrbitControls: .noPan has been deprecated. Use .enablePan instead."),!this.enablePan},set:function(e){console.warn("OrbitControls: .noPan has been deprecated. Use .enablePan instead."),this.enablePan=!e}},noKeys:{get:function(){return console.warn("OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."),!this.enableKeys},set:function(e){console.warn("OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."),this.enableKeys=!e}},staticMoving:{get:function(){return console.warn("OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."),!this.enableDamping},set:function(e){console.warn("OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."),this.enableDamping=!e}},dynamicDampingFactor:{get:function(){return console.warn("OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."),this.dampingFactor},set:function(e){console.warn("OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."),this.dampingFactor=e}}})})();
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..48dc70e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/models/planet_gear.stl b/models/planet_gear.stl
new file mode 100644
index 0000000..cf74a3d
Binary files /dev/null and b/models/planet_gear.stl differ
diff --git a/models/plotter.stl b/models/plotter.stl
new file mode 100644
index 0000000..ad6b076
Binary files /dev/null and b/models/plotter.stl differ
diff --git a/models/printer.stl b/models/printer.stl
new file mode 100644
index 0000000..029642a
Binary files /dev/null and b/models/printer.stl differ
diff --git a/models/remote.stl b/models/remote.stl
new file mode 100644
index 0000000..ce1f1cb
Binary files /dev/null and b/models/remote.stl differ
diff --git a/scripts/Detector.js b/scripts/Detector.js
new file mode 100644
index 0000000..e939b72
--- /dev/null
+++ b/scripts/Detector.js
@@ -0,0 +1,69 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ * @author mr.doob / http://mrdoob.com/
+ */
+
+var Detector = {
+ canvas: !!window.CanvasRenderingContext2D,
+ webgl: (function () {
+ try {
+ var canvas = document.createElement("canvas");
+ return !!(
+ window.WebGLRenderingContext &&
+ (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
+ );
+ } catch (e) {
+ return false;
+ }
+ })(),
+ workers: !!window.Worker,
+ fileapi: window.File && window.FileReader && window.FileList && window.Blob,
+
+ getWebGLErrorMessage: function () {
+ var element = document.createElement("div");
+ element.id = "webgl-error-message";
+ element.style.fontFamily = "monospace";
+ element.style.fontSize = "13px";
+ element.style.fontWeight = "normal";
+ element.style.textAlign = "center";
+ element.style.background = "#fff";
+ element.style.color = "#000";
+ element.style.padding = "1.5em";
+ element.style.width = "400px";
+ element.style.margin = "5em auto 0";
+
+ if (!this.webgl) {
+ element.innerHTML = window.WebGLRenderingContext
+ ? [
+ 'Your graphics card does not seem to support WebGL.
',
+ 'Find out how to get it here.',
+ ].join("\n")
+ : [
+ 'Your browser does not seem to support WebGL.
',
+ 'Find out how to get it here.',
+ ].join("\n");
+ }
+
+ return element;
+ },
+
+ addGetWebGLMessage: function (parameters) {
+ var parent, id, element;
+
+ parameters = parameters || {};
+
+ parent =
+ parameters.parent !== undefined ? parameters.parent : document.body;
+ id = parameters.id !== undefined ? parameters.id : "oldie";
+
+ element = Detector.getWebGLErrorMessage();
+ element.id = id;
+
+ parent.appendChild(element);
+ },
+};
+
+// browserify support
+if (typeof module === "object") {
+ module.exports = Detector;
+}
diff --git a/scripts/OrbitControls.js b/scripts/OrbitControls.js
new file mode 100644
index 0000000..7067d64
--- /dev/null
+++ b/scripts/OrbitControls.js
@@ -0,0 +1,944 @@
+/**
+ * @author qiao / https://github.com/qiao
+ * @author mrdoob / http://mrdoob.com
+ * @author alteredq / http://alteredqualia.com/
+ * @author WestLangley / http://github.com/WestLangley
+ * @author erich666 / http://erichaines.com
+ * @author brent / https://github.com/brentyi
+ */
+
+// This is a modified version of the three.js OrbitControls example, with some enhancements
+// for auto-rotation & embedding.
+//
+// This set of controls performs orbiting, dollying (zooming), and panning.
+// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+//
+// Orbit - left mouse / touch: one finger move
+// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish
+// Pan - right mouse, or arrow keys / touch: three finger swipe
+
+var OrbitControls = function (object, domElement) {
+ this.object = object;
+
+ this.domElement = domElement !== undefined ? domElement : document;
+
+ // Set to false to disable this control
+ this.enabled = true;
+
+ // "target" sets the location of focus, where the object orbits around
+ this.target = new THREE.Vector3();
+
+ // How far you can dolly in and out ( PerspectiveCamera only )
+ this.minDistance = 0;
+ this.maxDistance = Infinity;
+
+ // How far you can zoom in and out ( OrthographicCamera only )
+ this.minZoom = 0;
+ this.maxZoom = Infinity;
+
+ // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+ this.minPolarAngle = 0; // radians
+ this.maxPolarAngle = Math.PI; // radians
+
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
+ this.minAzimuthAngle = -Infinity; // radians
+ this.maxAzimuthAngle = Infinity; // radians
+
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+ this.enableDamping = false;
+ this.dampingFactor = 0.25;
+
+ // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0;
+
+ // Set to false to disable rotating
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0;
+
+ // Set to false to disable panning
+ this.enablePan = true;
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
+ this.autoRotateDelay = 0.0; // how long after an action do we start rotating again?
+ this.autoRotateTimeout;
+
+ // Set to false to disable use of the keys
+ this.enableKeys = true;
+
+ // The four arrow keys
+ this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };
+
+ // Mouse buttons
+ this.mouseButtons = {
+ ORBIT: THREE.MOUSE.LEFT,
+ ZOOM: THREE.MOUSE.MIDDLE,
+ PAN: THREE.MOUSE.RIGHT,
+ };
+
+ // for reset
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = this.object.zoom;
+
+ //
+ // public methods
+ //
+
+ this.getPolarAngle = function () {
+ return spherical.phi;
+ };
+
+ this.getAzimuthalAngle = function () {
+ return spherical.theta;
+ };
+
+ this.reset = function () {
+ scope.target.copy(scope.target0);
+ scope.object.position.copy(scope.position0);
+ scope.object.zoom = scope.zoom0;
+
+ scope.object.updateProjectionMatrix();
+ scope.dispatchEvent(changeEvent);
+
+ scope.update();
+
+ state = STATE.NONE;
+ };
+
+ // this method is exposed, but perhaps it would be better if we can make it private...
+ this.update = (function () {
+ var offset = new THREE.Vector3();
+
+ // so camera.up is the orbit axis
+ var quat = new THREE.Quaternion().setFromUnitVectors(
+ object.up,
+ new THREE.Vector3(0, 1, 0)
+ );
+ var quatInverse = quat.clone().inverse();
+
+ var lastPosition = new THREE.Vector3();
+ var lastQuaternion = new THREE.Quaternion();
+
+ return function update() {
+ var position = scope.object.position;
+
+ offset.copy(position).sub(scope.target);
+
+ // rotate offset to "y-axis-is-up" space
+ offset.applyQuaternion(quat);
+
+ // angle from z-axis around y-axis
+ spherical.setFromVector3(offset);
+
+ if (scope.autoRotate && state === STATE.NONE && !wheelZoomed) {
+ rotateLeft(getAutoRotationAngle());
+ } else if (scope.autoRotate && scope.autoRotateDelay > 0) {
+ if (scope.autoRotateSpeed > 0.0) {
+ scope.autoRotateSpeedActual = scope.autoRotateSpeed;
+ scope.autoRotateSpeed = 0.0;
+ }
+
+ clearTimeout(scope.autoRotateTimeout);
+ scope.autoRotateTimeout = setTimeout(function () {
+ scope.autoRotateSpeed = scope.autoRotateSpeedActual;
+ }, scope.autoRotateDelay);
+
+ wheelZoomed = false;
+ }
+
+ spherical.theta += sphericalDelta.theta;
+ spherical.phi += sphericalDelta.phi;
+
+ // restrict theta to be between desired limits
+ spherical.theta = Math.max(
+ scope.minAzimuthAngle,
+ Math.min(scope.maxAzimuthAngle, spherical.theta)
+ );
+
+ // restrict phi to be between desired limits
+ spherical.phi = Math.max(
+ scope.minPolarAngle,
+ Math.min(scope.maxPolarAngle, spherical.phi)
+ );
+
+ spherical.makeSafe();
+
+ spherical.radius *= scale;
+
+ // restrict radius to be between desired limits
+ spherical.radius = Math.max(
+ scope.minDistance,
+ Math.min(scope.maxDistance, spherical.radius)
+ );
+
+ // move target to panned location
+ scope.target.add(panOffset);
+
+ offset.setFromSpherical(spherical);
+
+ // rotate offset back to "camera-up-vector-is-up" space
+ offset.applyQuaternion(quatInverse);
+
+ position.copy(scope.target).add(offset);
+
+ scope.object.lookAt(scope.target);
+
+ if (scope.enableDamping === true) {
+ sphericalDelta.theta *= 1 - scope.dampingFactor;
+ sphericalDelta.phi *= 1 - scope.dampingFactor;
+ } else {
+ sphericalDelta.set(0, 0, 0);
+ }
+
+ scale = 1;
+ panOffset.set(0, 0, 0);
+
+ // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if (
+ zoomChanged ||
+ lastPosition.distanceToSquared(scope.object.position) > EPS ||
+ 8 * (1 - lastQuaternion.dot(scope.object.quaternion)) > EPS
+ ) {
+ scope.dispatchEvent(changeEvent);
+
+ lastPosition.copy(scope.object.position);
+ lastQuaternion.copy(scope.object.quaternion);
+ zoomChanged = false;
+
+ return true;
+ }
+
+ return false;
+ };
+ })();
+
+ this.dispose = function () {
+ scope.domElement.removeEventListener("contextmenu", onContextMenu, false);
+ scope.domElement.removeEventListener("mousedown", onMouseDown, false);
+ scope.domElement.removeEventListener("wheel", onMouseWheel, false);
+
+ scope.domElement.removeEventListener("touchstart", onTouchStart, false);
+ scope.domElement.removeEventListener("touchend", onTouchEnd, false);
+ scope.domElement.removeEventListener("touchmove", onTouchMove, false);
+
+ document.removeEventListener("mousemove", onMouseMove, false);
+ document.removeEventListener("mouseup", onMouseUp, false);
+ document.removeEventListener("mouseleave", onMouseUp, false);
+
+ window.removeEventListener("keydown", onKeyDown, false);
+
+ //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+ };
+
+ //
+ // internals
+ //
+
+ var scope = this;
+
+ var changeEvent = { type: "change" };
+ var startEvent = { type: "start" };
+ var endEvent = { type: "end" };
+
+ var STATE = {
+ NONE: -1,
+ ROTATE: 0,
+ DOLLY: 1,
+ PAN: 2,
+ TOUCH_ROTATE: 3,
+ TOUCH_DOLLY: 4,
+ TOUCH_PAN: 5,
+ };
+
+ var state = STATE.NONE;
+
+ var EPS = 0.000001;
+
+ // current position in spherical coordinates
+ var spherical = new THREE.Spherical();
+ var sphericalDelta = new THREE.Spherical();
+
+ var scale = 1;
+ var panOffset = new THREE.Vector3();
+ var zoomChanged = false;
+
+ var wheelZoomed = false; // for auto-rotate
+
+ var rotateStart = new THREE.Vector2();
+ var rotateEnd = new THREE.Vector2();
+ var rotateDelta = new THREE.Vector2();
+
+ var panStart = new THREE.Vector2();
+ var panEnd = new THREE.Vector2();
+ var panDelta = new THREE.Vector2();
+
+ var dollyStart = new THREE.Vector2();
+ var dollyEnd = new THREE.Vector2();
+ var dollyDelta = new THREE.Vector2();
+
+ function getAutoRotationAngle() {
+ return ((2 * Math.PI) / 60 / 60) * scope.autoRotateSpeed;
+ }
+
+ function getZoomScale() {
+ return Math.pow(0.95, scope.zoomSpeed);
+ }
+
+ function rotateLeft(angle) {
+ sphericalDelta.theta -= angle;
+ }
+
+ function rotateUp(angle) {
+ sphericalDelta.phi -= angle;
+ }
+
+ var panLeft = (function () {
+ var v = new THREE.Vector3();
+
+ return function panLeft(distance, objectMatrix) {
+ v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix
+ v.multiplyScalar(-distance);
+
+ panOffset.add(v);
+ };
+ })();
+
+ var panUp = (function () {
+ var v = new THREE.Vector3();
+
+ return function panUp(distance, objectMatrix) {
+ v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix
+ v.multiplyScalar(distance);
+
+ panOffset.add(v);
+ };
+ })();
+
+ // deltaX and deltaY are in pixels; right and down are positive
+ var pan = (function () {
+ var offset = new THREE.Vector3();
+
+ return function pan(deltaX, deltaY) {
+ var element =
+ scope.domElement === document
+ ? scope.domElement.body
+ : scope.domElement;
+
+ if (scope.object instanceof THREE.PerspectiveCamera) {
+ // perspective
+ var position = scope.object.position;
+ offset.copy(position).sub(scope.target);
+ var targetDistance = offset.length();
+
+ // half of the fov is center to top of screen
+ targetDistance *= Math.tan(((scope.object.fov / 2) * Math.PI) / 180.0);
+
+ // we actually don't use screenWidth, since perspective camera is fixed to screen height
+ panLeft(
+ (2 * deltaX * targetDistance) / element.clientHeight,
+ scope.object.matrix
+ );
+ panUp(
+ (2 * deltaY * targetDistance) / element.clientHeight,
+ scope.object.matrix
+ );
+ } else if (scope.object instanceof THREE.OrthographicCamera) {
+ // orthographic
+ panLeft(
+ (deltaX * (scope.object.right - scope.object.left)) /
+ scope.object.zoom /
+ element.clientWidth,
+ scope.object.matrix
+ );
+ panUp(
+ (deltaY * (scope.object.top - scope.object.bottom)) /
+ scope.object.zoom /
+ element.clientHeight,
+ scope.object.matrix
+ );
+ } else {
+ // camera neither orthographic nor perspective
+ console.warn(
+ "WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."
+ );
+ scope.enablePan = false;
+ }
+ };
+ })();
+
+ function dollyIn(dollyScale) {
+ if (scope.object instanceof THREE.PerspectiveCamera) {
+ scale /= dollyScale;
+ } else if (scope.object instanceof THREE.OrthographicCamera) {
+ scope.object.zoom = Math.max(
+ scope.minZoom,
+ Math.min(scope.maxZoom, scope.object.zoom * dollyScale)
+ );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+ } else {
+ console.warn(
+ "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."
+ );
+ scope.enableZoom = false;
+ }
+ }
+
+ function dollyOut(dollyScale) {
+ if (scope.object instanceof THREE.PerspectiveCamera) {
+ scale *= dollyScale;
+ } else if (scope.object instanceof THREE.OrthographicCamera) {
+ scope.object.zoom = Math.max(
+ scope.minZoom,
+ Math.min(scope.maxZoom, scope.object.zoom / dollyScale)
+ );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+ } else {
+ console.warn(
+ "WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."
+ );
+ scope.enableZoom = false;
+ }
+ }
+
+ //
+ // event callbacks - update the object state
+ //
+
+ function handleMouseDownRotate(event) {
+ //console.log( 'handleMouseDownRotate' );
+
+ rotateStart.set(event.clientX, event.clientY);
+ }
+
+ function handleMouseDownDolly(event) {
+ //console.log( 'handleMouseDownDolly' );
+
+ dollyStart.set(event.clientX, event.clientY);
+ }
+
+ function handleMouseDownPan(event) {
+ //console.log( 'handleMouseDownPan' );
+
+ panStart.set(event.clientX, event.clientY);
+ }
+
+ function handleMouseMoveRotate(event) {
+ //console.log( 'handleMouseMoveRotate' );
+
+ rotateEnd.set(event.clientX, event.clientY);
+ rotateDelta.subVectors(rotateEnd, rotateStart);
+
+ var element =
+ scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ rotateLeft(
+ ((2 * Math.PI * rotateDelta.x) / element.clientWidth) * scope.rotateSpeed
+ );
+
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ rotateUp(
+ ((2 * Math.PI * rotateDelta.y) / element.clientHeight) * scope.rotateSpeed
+ );
+
+ rotateStart.copy(rotateEnd);
+
+ scope.update();
+ }
+
+ function handleMouseMoveDolly(event) {
+ //console.log( 'handleMouseMoveDolly' );
+
+ dollyEnd.set(event.clientX, event.clientY);
+
+ dollyDelta.subVectors(dollyEnd, dollyStart);
+
+ if (dollyDelta.y > 0) {
+ dollyIn(getZoomScale());
+ } else if (dollyDelta.y < 0) {
+ dollyOut(getZoomScale());
+ }
+
+ dollyStart.copy(dollyEnd);
+
+ scope.update();
+ }
+
+ function handleMouseMovePan(event) {
+ //console.log( 'handleMouseMovePan' );
+
+ panEnd.set(event.clientX, event.clientY);
+
+ panDelta.subVectors(panEnd, panStart);
+
+ pan(panDelta.x, panDelta.y);
+
+ panStart.copy(panEnd);
+
+ scope.update();
+ }
+
+ function handleMouseUp(event) {
+ // console.log( 'handleMouseUp' );
+ }
+
+ function handleMouseWheel(event) {
+ // console.log( 'handleMouseWheel' );
+
+ if (event.deltaY < 0) {
+ dollyOut(getZoomScale());
+ } else if (event.deltaY > 0) {
+ dollyIn(getZoomScale());
+ }
+
+ scope.update();
+ }
+
+ function handleKeyDown(event) {
+ //console.log( 'handleKeyDown' );
+
+ switch (event.keyCode) {
+ case scope.keys.UP:
+ pan(0, scope.keyPanSpeed);
+ scope.update();
+ break;
+
+ case scope.keys.BOTTOM:
+ pan(0, -scope.keyPanSpeed);
+ scope.update();
+ break;
+
+ case scope.keys.LEFT:
+ pan(scope.keyPanSpeed, 0);
+ scope.update();
+ break;
+
+ case scope.keys.RIGHT:
+ pan(-scope.keyPanSpeed, 0);
+ scope.update();
+ break;
+ }
+ }
+
+ function handleTouchStartRotate(event) {
+ //console.log( 'handleTouchStartRotate' );
+
+ rotateStart.set(event.touches[0].pageX, event.touches[0].pageY);
+ }
+
+ function handleTouchStartDolly(event) {
+ //console.log( 'handleTouchStartDolly' );
+
+ var dx = event.touches[0].pageX - event.touches[1].pageX;
+ var dy = event.touches[0].pageY - event.touches[1].pageY;
+
+ var distance = Math.sqrt(dx * dx + dy * dy);
+
+ dollyStart.set(0, distance);
+ }
+
+ function handleTouchStartPan(event) {
+ //console.log( 'handleTouchStartPan' );
+
+ panStart.set(event.touches[0].pageX, event.touches[0].pageY);
+ }
+
+ function handleTouchMoveRotate(event) {
+ //console.log( 'handleTouchMoveRotate' );
+
+ rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
+ rotateDelta.subVectors(rotateEnd, rotateStart);
+
+ var element =
+ scope.domElement === document ? scope.domElement.body : scope.domElement;
+
+ // rotating across whole screen goes 360 degrees around
+ rotateLeft(
+ ((2 * Math.PI * rotateDelta.x) / element.clientWidth) * scope.rotateSpeed
+ );
+
+ // rotating up and down along whole screen attempts to go 360, but limited to 180
+ rotateUp(
+ ((2 * Math.PI * rotateDelta.y) / element.clientHeight) * scope.rotateSpeed
+ );
+
+ rotateStart.copy(rotateEnd);
+
+ scope.update();
+ }
+
+ function handleTouchMoveDolly(event) {
+ //console.log( 'handleTouchMoveDolly' );
+
+ var dx = event.touches[0].pageX - event.touches[1].pageX;
+ var dy = event.touches[0].pageY - event.touches[1].pageY;
+
+ var distance = Math.sqrt(dx * dx + dy * dy);
+
+ dollyEnd.set(0, distance);
+
+ dollyDelta.subVectors(dollyEnd, dollyStart);
+
+ if (dollyDelta.y > 0) {
+ dollyOut(getZoomScale());
+ } else if (dollyDelta.y < 0) {
+ dollyIn(getZoomScale());
+ }
+
+ dollyStart.copy(dollyEnd);
+
+ scope.update();
+ }
+
+ function handleTouchMovePan(event) {
+ //console.log( 'handleTouchMovePan' );
+
+ panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
+
+ panDelta.subVectors(panEnd, panStart);
+
+ pan(panDelta.x, panDelta.y);
+
+ panStart.copy(panEnd);
+
+ scope.update();
+ }
+
+ function handleTouchEnd(event) {
+ //console.log( 'handleTouchEnd' );
+ }
+
+ //
+ // event handlers - FSM: listen for events and reset state
+ //
+
+ function onMouseDown(event) {
+ if (scope.enabled === false) return;
+
+ event.preventDefault();
+
+ if (event.button === scope.mouseButtons.ORBIT) {
+ if (scope.enableRotate === false) return;
+
+ handleMouseDownRotate(event);
+
+ state = STATE.ROTATE;
+ } else if (event.button === scope.mouseButtons.ZOOM) {
+ if (scope.enableZoom === false) return;
+
+ handleMouseDownDolly(event);
+
+ state = STATE.DOLLY;
+ } else if (event.button === scope.mouseButtons.PAN) {
+ if (scope.enablePan === false) return;
+
+ handleMouseDownPan(event);
+
+ state = STATE.PAN;
+ }
+
+ if (state !== STATE.NONE) {
+ document.addEventListener("mousemove", onMouseMove, false);
+ document.addEventListener("mouseup", onMouseUp, false);
+ document.addEventListener("mouseleave", onMouseUp, false);
+
+ scope.dispatchEvent(startEvent);
+ }
+ }
+
+ function onMouseMove(event) {
+ if (scope.enabled === false) return;
+
+ event.preventDefault();
+
+ if (state === STATE.ROTATE) {
+ if (scope.enableRotate === false) return;
+
+ handleMouseMoveRotate(event);
+ } else if (state === STATE.DOLLY) {
+ if (scope.enableZoom === false) return;
+
+ handleMouseMoveDolly(event);
+ } else if (state === STATE.PAN) {
+ if (scope.enablePan === false) return;
+
+ handleMouseMovePan(event);
+ }
+ }
+
+ function onMouseUp(event) {
+ if (scope.enabled === false) return;
+
+ handleMouseUp(event);
+
+ document.removeEventListener("mousemove", onMouseMove, false);
+ document.removeEventListener("mouseup", onMouseUp, false);
+ document.removeEventListener("mouseleave", onMouseUp, false);
+
+ scope.dispatchEvent(endEvent);
+
+ state = STATE.NONE;
+ }
+
+ function onMouseWheel(event) {
+ if (
+ scope.enabled === false ||
+ scope.enableZoom === false ||
+ (state !== STATE.NONE && state !== STATE.ROTATE)
+ )
+ return;
+
+ handleMouseWheel(event);
+
+ scope.dispatchEvent(startEvent); // not sure why these are here...
+ scope.dispatchEvent(endEvent);
+
+ wheelZoomed = true;
+
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ }
+
+ function onKeyDown(event) {
+ if (
+ scope.enabled === false ||
+ scope.enableKeys === false ||
+ scope.enablePan === false
+ )
+ return;
+
+ handleKeyDown(event);
+ }
+
+ function onTouchStart(event) {
+ if (scope.enabled === false) return;
+
+ switch (event.touches.length) {
+ case 1: // one-fingered touch: rotate
+ if (scope.enableRotate === false) return;
+
+ handleTouchStartRotate(event);
+
+ state = STATE.TOUCH_ROTATE;
+
+ break;
+
+ case 2: // two-fingered touch: dolly
+ if (scope.enableZoom === false) return;
+
+ handleTouchStartDolly(event);
+
+ state = STATE.TOUCH_DOLLY;
+
+ break;
+
+ case 3: // three-fingered touch: pan
+ if (scope.enablePan === false) return;
+
+ handleTouchStartPan(event);
+
+ state = STATE.TOUCH_PAN;
+
+ break;
+
+ default:
+ state = STATE.NONE;
+ }
+
+ if (state !== STATE.NONE) {
+ scope.dispatchEvent(startEvent);
+ }
+ }
+
+ function onTouchMove(event) {
+ if (scope.enabled === false) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ switch (event.touches.length) {
+ case 1: // one-fingered touch: rotate
+ if (scope.enableRotate === false) return;
+ if (state !== STATE.TOUCH_ROTATE) return; // is this needed?...
+
+ handleTouchMoveRotate(event);
+
+ break;
+
+ case 2: // two-fingered touch: dolly
+ if (scope.enableZoom === false) return;
+ if (state !== STATE.TOUCH_DOLLY) return; // is this needed?...
+
+ handleTouchMoveDolly(event);
+
+ break;
+
+ case 3: // three-fingered touch: pan
+ if (scope.enablePan === false) return;
+ if (state !== STATE.TOUCH_PAN) return; // is this needed?...
+
+ handleTouchMovePan(event);
+
+ break;
+
+ default:
+ state = STATE.NONE;
+ }
+ }
+
+ function onTouchEnd(event) {
+ if (scope.enabled === false) return;
+
+ handleTouchEnd(event);
+
+ scope.dispatchEvent(endEvent);
+
+ state = STATE.NONE;
+ }
+
+ function onContextMenu(event) {
+ event.preventDefault();
+ }
+
+ //
+
+ scope.domElement.addEventListener("contextmenu", onContextMenu, false);
+
+ scope.domElement.addEventListener("mousedown", onMouseDown, false);
+ scope.domElement.addEventListener("wheel", onMouseWheel, false);
+
+ scope.domElement.addEventListener("touchstart", onTouchStart, false);
+ scope.domElement.addEventListener("touchend", onTouchEnd, false);
+ scope.domElement.addEventListener("touchmove", onTouchMove, false);
+
+ window.addEventListener("keydown", onKeyDown, false);
+
+ // force an update at start
+
+ this.update();
+};
+
+OrbitControls.prototype = Object.create(THREE.EventDispatcher.prototype);
+OrbitControls.prototype.constructor = OrbitControls;
+
+Object.defineProperties(OrbitControls.prototype, {
+ center: {
+ get: function () {
+ console.warn("OrbitControls: .center has been renamed to .target");
+ return this.target;
+ },
+ },
+
+ // backward compatibility
+
+ noZoom: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."
+ );
+ return !this.enableZoom;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."
+ );
+ this.enableZoom = !value;
+ },
+ },
+
+ noRotate: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."
+ );
+ return !this.enableRotate;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."
+ );
+ this.enableRotate = !value;
+ },
+ },
+
+ noPan: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .noPan has been deprecated. Use .enablePan instead."
+ );
+ return !this.enablePan;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .noPan has been deprecated. Use .enablePan instead."
+ );
+ this.enablePan = !value;
+ },
+ },
+
+ noKeys: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."
+ );
+ return !this.enableKeys;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."
+ );
+ this.enableKeys = !value;
+ },
+ },
+
+ staticMoving: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."
+ );
+ return !this.enableDamping;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."
+ );
+ this.enableDamping = !value;
+ },
+ },
+
+ dynamicDampingFactor: {
+ get: function () {
+ console.warn(
+ "OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."
+ );
+ return this.dampingFactor;
+ },
+
+ set: function (value) {
+ console.warn(
+ "OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."
+ );
+ this.dampingFactor = value;
+ },
+ },
+});
diff --git a/scripts/STLLoader.js b/scripts/STLLoader.js
new file mode 100644
index 0000000..89e025d
--- /dev/null
+++ b/scripts/STLLoader.js
@@ -0,0 +1,255 @@
+/**
+ * @author aleeper / http://adamleeper.com/
+ * @author mrdoob / http://mrdoob.com/
+ * @author gero3 / https://github.com/gero3
+ * @author Mugen87 / https://github.com/Mugen87
+ *
+ * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
+ *
+ * Supports both binary and ASCII encoded files, with automatic detection of type.
+ *
+ * The loader returns a non-indexed buffer geometry.
+ *
+ * Limitations:
+ * Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
+ * There is perhaps some question as to how valid it is to always assume little-endian-ness.
+ * ASCII decoding assumes file is UTF-8.
+ *
+ * Usage:
+ * var loader = new STLLoader();
+ * loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
+ * scene.add( new THREE.Mesh( geometry ) );
+ * });
+ *
+ * For binary STLs geometry might contain colors for vertices. To use it:
+ * // use the same code to load STL as above
+ * if (geometry.hasColors) {
+ * material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: THREE.VertexColors });
+ * } else { .... }
+ * var mesh = new THREE.Mesh( geometry, material );
+ */
+
+var STLLoader = function (manager) {
+ this.manager = manager !== undefined ? manager : THREE.DefaultLoadingManager;
+};
+
+STLLoader.prototype = {
+ constructor: STLLoader,
+
+ load: function (url, onLoad, onProgress, onError) {
+ var scope = this;
+
+ var loader = new THREE.FileLoader(scope.manager);
+ loader.setResponseType("arraybuffer");
+ loader.load(
+ url,
+ function (text) {
+ onLoad(scope.parse(text));
+ },
+ onProgress,
+ onError
+ );
+ },
+
+ parse: function (data) {
+ var isBinary = function () {
+ var expect, face_size, n_faces, reader;
+ reader = new DataView(binData);
+ face_size = (32 / 8) * 3 + (32 / 8) * 3 * 3 + 16 / 8;
+ n_faces = reader.getUint32(80, true);
+ expect = 80 + 32 / 8 + n_faces * face_size;
+
+ if (expect === reader.byteLength) {
+ return true;
+ }
+
+ // some binary files will have different size from expected,
+ // checking characters higher than ASCII to confirm is binary
+ var fileLength = reader.byteLength;
+ for (var index = 0; index < fileLength; index++) {
+ if (reader.getUint8(index, false) > 127) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ var binData = this.ensureBinary(data);
+
+ return isBinary()
+ ? this.parseBinary(binData)
+ : this.parseASCII(this.ensureString(data));
+ },
+
+ parseBinary: function (data) {
+ var reader = new DataView(data);
+ var faces = reader.getUint32(80, true);
+
+ var r,
+ g,
+ b,
+ hasColors = false,
+ colors;
+ var defaultR, defaultG, defaultB, alpha;
+
+ // process STL header
+ // check for default color in header ("COLOR=rgba" sequence).
+
+ for (var index = 0; index < 80 - 10; index++) {
+ if (
+ reader.getUint32(index, false) == 0x434f4c4f /*COLO*/ &&
+ reader.getUint8(index + 4) == 0x52 /*'R'*/ &&
+ reader.getUint8(index + 5) == 0x3d /*'='*/
+ ) {
+ hasColors = true;
+ colors = [];
+
+ defaultR = reader.getUint8(index + 6) / 255;
+ defaultG = reader.getUint8(index + 7) / 255;
+ defaultB = reader.getUint8(index + 8) / 255;
+ alpha = reader.getUint8(index + 9) / 255;
+ }
+ }
+
+ var dataOffset = 84;
+ var faceLength = 12 * 4 + 2;
+
+ var geometry = new THREE.BufferGeometry();
+
+ var vertices = [];
+ var normals = [];
+
+ for (var face = 0; face < faces; face++) {
+ var start = dataOffset + face * faceLength;
+ var normalX = reader.getFloat32(start, true);
+ var normalY = reader.getFloat32(start + 4, true);
+ var normalZ = reader.getFloat32(start + 8, true);
+
+ if (hasColors) {
+ var packedColor = reader.getUint16(start + 48, true);
+
+ if ((packedColor & 0x8000) === 0) {
+ // facet has its own unique color
+
+ r = (packedColor & 0x1f) / 31;
+ g = ((packedColor >> 5) & 0x1f) / 31;
+ b = ((packedColor >> 10) & 0x1f) / 31;
+ } else {
+ r = defaultR;
+ g = defaultG;
+ b = defaultB;
+ }
+ }
+
+ for (var i = 1; i <= 3; i++) {
+ var vertexstart = start + i * 12;
+
+ vertices.push(reader.getFloat32(vertexstart, true));
+ vertices.push(reader.getFloat32(vertexstart + 4, true));
+ vertices.push(reader.getFloat32(vertexstart + 8, true));
+
+ normals.push(normalX, normalY, normalZ);
+
+ if (hasColors) {
+ colors.push(r, g, b);
+ }
+ }
+ }
+
+ geometry.addAttribute(
+ "position",
+ new THREE.BufferAttribute(new Float32Array(vertices), 3)
+ );
+ geometry.addAttribute(
+ "normal",
+ new THREE.BufferAttribute(new Float32Array(normals), 3)
+ );
+
+ if (hasColors) {
+ geometry.addAttribute(
+ "color",
+ new THREE.BufferAttribute(new Float32Array(colors), 3)
+ );
+ geometry.hasColors = true;
+ geometry.alpha = alpha;
+ }
+
+ return geometry;
+ },
+
+ parseASCII: function (data) {
+ var geometry,
+ length,
+ patternFace,
+ patternNormal,
+ patternVertex,
+ result,
+ text;
+ geometry = new THREE.BufferGeometry();
+ patternFace = /facet([\s\S]*?)endfacet/g;
+
+ var vertices = [];
+ var normals = [];
+
+ var normal = new THREE.Vector3();
+
+ while ((result = patternFace.exec(data)) !== null) {
+ text = result[0];
+ patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;
+
+ while ((result = patternNormal.exec(text)) !== null) {
+ normal.x = parseFloat(result[1]);
+ normal.y = parseFloat(result[3]);
+ normal.z = parseFloat(result[5]);
+ }
+
+ patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;
+
+ while ((result = patternVertex.exec(text)) !== null) {
+ vertices.push(
+ parseFloat(result[1]),
+ parseFloat(result[3]),
+ parseFloat(result[5])
+ );
+ normals.push(normal.x, normal.y, normal.z);
+ }
+ }
+
+ geometry.addAttribute(
+ "position",
+ new THREE.BufferAttribute(new Float32Array(vertices), 3)
+ );
+ geometry.addAttribute(
+ "normal",
+ new THREE.BufferAttribute(new Float32Array(normals), 3)
+ );
+
+ return geometry;
+ },
+
+ ensureString: function (buf) {
+ if (typeof buf !== "string") {
+ var array_buffer = new Uint8Array(buf);
+ var strArray = [];
+ for (var i = 0; i < buf.byteLength; i++) {
+ strArray.push(String.fromCharCode(array_buffer[i])); // implicitly assumes little-endian
+ }
+ return strArray.join("");
+ } else {
+ return buf;
+ }
+ },
+
+ ensureBinary: function (buf) {
+ if (typeof buf === "string") {
+ var array_buffer = new Uint8Array(buf.length);
+ for (var i = 0; i < buf.length; i++) {
+ array_buffer[i] = buf.charCodeAt(i) & 0xff; // implicitly assumes little-endian
+ }
+ return array_buffer.buffer || array_buffer;
+ } else {
+ return buf;
+ }
+ },
+};
diff --git a/scripts/STLWebViewer2.js b/scripts/STLWebViewer2.js
new file mode 100644
index 0000000..03c3bc8
--- /dev/null
+++ b/scripts/STLWebViewer2.js
@@ -0,0 +1,306 @@
+/*
+ * Helper for embedding STL files into webpages!
+ * brentyi@berkeley.edu
+ */
+
+$(() => {
+ // Load and view all STLs
+ $(".stlwv2-model").each(function () {
+ let $container = $(this);
+ let modelUrl = $container.data("model-url");
+ new STLWebViewer2(modelUrl, $container);
+ });
+
+ // Disable fullscreen when the user presses Escape
+ $(document).keyup(function (e) {
+ if (e.key === "Escape") {
+ $(".stlwv2-model .stlwv2-fullscreen-checkbox").each(function () {
+ $(this).prop("checked") &&
+ $(this).prop("checked", false).trigger("change");
+ });
+ }
+ });
+});
+
+function STLWebViewer2(modelUrl, $container) {
+ // Set initial attributes
+ this.modelUrl = modelUrl;
+ this.$container = $container;
+
+ // Check for WebGl support
+ if (!Detector.webgl) {
+ Detector.addGetWebGLMessage({ parent: this.$container[0] });
+ return;
+ }
+
+ // Build out viewer DOM elements
+ STLWebViewer2.instanceCount = (STLWebViewer2.instanceCount || 0) + 1;
+ let checkboxId =
+ "stlwv2-fullscreen-checkbox-" + (STLWebViewer2.instanceCount - 1);
+ this.$container.append(
+ [
+ '',
+ '",
+ ].join("\n")
+ );
+ this.$innerContainer = this.$container.children(".stlwv2-inner");
+
+ // Fullscreen-mode toggle logic
+ this.$fullscreenCheckbox = $("#" + checkboxId);
+ this.$fullscreenCheckbox.on(
+ "change",
+ this.fullscreenToggleHandler.bind(this)
+ );
+
+ // Set up threejs scene, camera, renderer
+ this.scene = new THREE.Scene();
+ this.camera = new THREE.PerspectiveCamera(
+ 40,
+ this.$innerContainer.width() / this.$innerContainer.height(),
+ 1,
+ 15000
+ );
+ this.cameraTarget = new THREE.Vector3();
+ this.renderer = this.makeRenderer((antialias = true));
+ this.$innerContainer.append(this.renderer.domElement);
+
+ // Orbit this.controls
+ this.controls = new OrbitControls(this.camera, this.$innerContainer.get(0));
+ this.controls.target = this.cameraTarget;
+ this.controls.enableDamping = true;
+ this.controls.enableKeys = false;
+ this.controls.rotateSpeed = 0.15;
+ this.controls.dampingFactor = 0.125;
+ this.controls.enableZoom = true;
+ this.controls.autoRotate = true;
+ this.controls.autoRotateSpeed = 0.25;
+ this.controls.autoRotateDelay = 5000;
+
+ // Lights: hemisphere light attached to the world
+ this.hemisphereLight = new THREE.HemisphereLight(0x999999, 0x555555);
+ this.scene.add(this.hemisphereLight);
+
+ // Lights: point light attached to the camera
+ this.pointLight = new THREE.PointLight(0xdddddd, 0.75, 0);
+ this.camera.add(this.pointLight);
+ this.scene.add(this.camera);
+
+ // Load STL file and add to scene
+ new STLLoader().load(
+ this.modelUrl,
+ this.stlLoadedCallback.bind(this),
+ this.updateProgress.bind(this)
+ );
+}
+
+// Progress callback -- (for % loaded indicator)
+STLWebViewer2.prototype.updateProgress = function (event) {
+ console.log(
+ "Loading " + this.modelUrl + ": " + event.loaded + "/" + event.total
+ );
+ this.$innerContainer
+ .children(".stlwv2-percent")
+ .text(Math.floor((event.loaded / event.total) * 100.0) + "%");
+};
+
+// Callback for when our mesh has been fully loaded
+STLWebViewer2.prototype.stlLoadedCallback = function (geometry) {
+ // Define (shaded) mesh and add to this.scene
+ let material = new THREE.MeshPhongMaterial({
+ color: 0xf7f8ff,
+ specular: 0x111111,
+ shininess: 0,
+ wireframe: false,
+ polygonOffset: true,
+ polygonOffsetFactor: 1,
+ polygonOffsetUnits: 1,
+ transparent: true,
+ opacity: 0.85,
+ });
+ let mesh = new THREE.Mesh(geometry, material);
+ mesh.position.set(0, 0, 0);
+ mesh.castShadow = false;
+ mesh.receiveShadow = false;
+ this.scene.add(mesh);
+
+ // Add model edges
+ let edges = new THREE.EdgesGeometry(geometry, 29);
+ let line = new THREE.LineSegments(
+ edges,
+ new THREE.LineBasicMaterial({
+ color: 0x666666,
+ })
+ );
+ this.scene.add(line);
+
+ // Update model bounding box and sphere
+ geometry.computeBoundingSphere();
+ geometry.computeBoundingBox();
+ this.cameraTarget.copy(geometry.boundingSphere.center);
+
+ // Set light, camera, and orbit control parameters based on model size
+ let r = geometry.boundingSphere.radius;
+ this.controls.maxDistance = r * 10;
+ this.pointLight.position.set(0, r, 0);
+ this.camera.position.set(
+ r * 1.5 + this.cameraTarget.x,
+ r * 1.5 + this.cameraTarget.y,
+ r * 1.5 + this.cameraTarget.z
+ );
+
+ // Render & animate scene
+ this.animate();
+
+ // Update CSS styles
+ this.$innerContainer.addClass("stlwv2-loaded");
+};
+
+// Helper for animating 3D model, updating controls, etc
+STLWebViewer2.prototype.animate = function () {
+ // Performance check: disable anti-aliasing if too slow
+ this.animateLoops = (this.animateLoops || 0) + 1;
+ if (!this.performanceChecked) {
+ if (this.animateLoops == 5) {
+ this.performanceCheckStartTime = performance.now();
+ } else if (this.animateLoops > 5) {
+ let delta = performance.now() - this.performanceCheckStartTime;
+ // Check framerate after 2 seconds
+ if (delta > 2000) {
+ let framerate = (1000 * (this.animateLoops - 5)) / delta;
+ console.log("Cumulative framerate: " + framerate);
+ if (framerate < 30) {
+ console.log("Disabling anti-aliasing");
+ this.renderer.domElement.remove();
+ delete this.renderer;
+ this.renderer = this.makeRenderer((antialias = false));
+ this.$innerContainer.append(this.renderer.domElement);
+ }
+ this.performanceChecked = true;
+ }
+ }
+ }
+
+ // Update camera & renderer
+ this.camera.aspect =
+ this.$innerContainer.width() / this.$innerContainer.height();
+ this.camera.updateProjectionMatrix();
+ this.renderer.setSize(
+ this.$innerContainer.width(),
+ this.$innerContainer.height()
+ );
+
+ // Render :)
+ requestAnimationFrame(this.animate.bind(this));
+ this.controls.update();
+ this.renderer.render(this.scene, this.camera);
+};
+
+// Helper for creating a WebGL renderer
+STLWebViewer2.prototype.makeRenderer = function (antialias) {
+ let renderer = new THREE.WebGLRenderer({
+ antialias: antialias,
+ });
+ renderer.setClearColor(0xffffff);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ renderer.gammaInput = true;
+ renderer.gammaOutput = true;
+ renderer.shadowMap.enabled = true;
+ return renderer;
+};
+
+// Helper for handling fullscreen toggle
+// Contains all animation logic, etc
+STLWebViewer2.prototype.fullscreenToggleHandler = function () {
+ // Location and dimensions of viewer outer container
+ let top = this.$container.position().top - ScrollHelpers.top() + 1;
+ let left = this.$container.position().left - ScrollHelpers.left() + 1;
+ let bottom = $(window).height() - (top + this.$container.innerHeight()) + 1;
+ let width = this.$container.width() - 2;
+
+ // We're storing state in an invisible checkbox; poll the 'checked' property
+ // to determine if we're going to or from fullscreen mode
+ if (this.$fullscreenCheckbox.prop("checked")) {
+ // Seamless position:absolute => position:fixed transition
+ // Also fade out a little for dramatic effect
+ this.$innerContainer.css({
+ top: top + "px",
+ bottom: bottom + "px",
+ left: left + "px",
+ width: width + "px",
+ position: "fixed",
+ opacity: "0.5",
+ "z-index": 2000,
+ });
+
+ // Expand to fill screen :)
+ this.$innerContainer.animate(
+ {
+ top: "0",
+ bottom: "0",
+ left: "0",
+ width: "100%",
+ opacity: "1",
+ },
+ 300,
+ () => {
+ // ...and fade back in
+ this.$innerContainer.animate(
+ {
+ opacity: "1",
+ },
+ 500
+ );
+ }
+ );
+ } else {
+ // Fade out a little for dramatic effect
+ this.$innerContainer.css({
+ opacity: "0.5",
+ });
+
+ // Shrink to fill outer container
+ this.$innerContainer.animate(
+ {
+ top: top + "px",
+ bottom: bottom + "px",
+ left: left + "px",
+ width: width + "px",
+ },
+ 300,
+ () => {
+ // Reset all styles to original values in CSS file
+ // Seamless position:fixed => position:absolute transition
+ this.$innerContainer.css({
+ position: "",
+ top: "",
+ bottom: "",
+ left: "",
+ width: "",
+ "z-index": "",
+ });
+
+ // ...and fade back in
+ this.$innerContainer.animate(
+ {
+ opacity: "1",
+ },
+ 500
+ );
+ }
+ );
+ }
+};
diff --git a/scripts/ScrollHelpers.js b/scripts/ScrollHelpers.js
new file mode 100644
index 0000000..1bbdd92
--- /dev/null
+++ b/scripts/ScrollHelpers.js
@@ -0,0 +1,23 @@
+// Helpers for getting scroll position
+// Based on:
+// https://stackoverflow.com/questions/2717252/document-body-scrolltop-is-always-0-in-ie-even-when-scrolling
+var ScrollHelpers = {
+ top: function () {
+ return typeof window.pageYOffset != "undefined"
+ ? window.pageYOffset
+ : document.documentElement.scrollTop
+ ? document.documentElement.scrollTop
+ : document.body.scrollTop
+ ? document.body.scrollTop
+ : 0;
+ },
+ left: function () {
+ return typeof window.pageXOffset != "undefined"
+ ? window.pageXOffset
+ : document.documentElement.scrollLeft
+ ? document.documentElement.scrollLeft
+ : document.body.scrollLeft
+ ? document.body.scrollLeft
+ : 0;
+ },
+};
diff --git a/stylesheets/style.css b/stylesheets/style.css
new file mode 100644
index 0000000..f58112d
--- /dev/null
+++ b/stylesheets/style.css
@@ -0,0 +1,109 @@
+.stlwv2-model {
+ position: relative;
+ min-height: 20em;
+ border: 1px solid #ccc;
+}
+
+.stlwv2-inner {
+ position: absolute;
+ overflow: hidden;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+.stlwv2-inner>.stlwv2-percent {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 5em;
+ text-align: center;
+ width: 100%;
+ color: #ccc;
+ animation-name: stlwv2-pulse;
+ animation-duration: 2s;
+ animation-iteration-count: infinite;
+}
+
+@keyframes stlwv2-pulse {
+ 0% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.625;
+ }
+}
+
+.stlwv2-inner>canvas {
+ position: absolute;
+ top: 0;
+ left: 0;
+ opacity: 0;
+ transition: opacity 0.5s;
+}
+
+.stlwv2-inner.stlwv2-loaded>canvas {
+ opacity: 1;
+}
+
+/*
+ * HUD styles
+ */
+.stlwv2-fullscreen-checkbox {
+ /* This checkbox is just for storing state and should never be displayed */
+ display: none !important;
+}
+
+.stlwv2-hud {
+ /* Hide HUD elements until viewer is finished loading */
+ display: none;
+}
+
+.stlwv2-loaded>.stlwv2-hud {
+ position: absolute;
+ padding: 0.25em;
+ z-index: 1000;
+ cursor: pointer;
+ font-weight: 400;
+}
+
+.stlwv2-loaded>.stlwv2-github-link {
+ font-size: 1.2em;
+ top: 0.57em;
+ right: 3em;
+ text-decoration: none;
+ color: #999;
+ display: none;
+}
+
+.stlwv2-loaded>.stlwv2-fullscreen-on {
+ font-size: 1.5em;
+ top: 0;
+ right: 0.2em;
+ transform: rotate(90deg);
+ /* unicode icon rotate hack */
+ color: #ccc;
+ display: block;
+}
+
+.stlwv2-loaded>.stlwv2-fullscreen-off {
+ font-size: 2em;
+ top: 0;
+ right: 0.5em;
+ color: #c33;
+ display: none;
+}
+
+.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-github-link {
+ display: block;
+}
+
+.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-on {
+ display: none;
+}
+
+.stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-off {
+ display: block;
+}