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(['','
','
',' ",' ",' '," STL Web Viewer","
"].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( + [ + '', + '
', + '
', + ' ", + ' ", + ' ', + " STL Web Viewer", + "
", + ].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; +}