Initial commit

This commit is contained in:
Mario Voigt 2020-08-22 00:20:41 +02:00
parent 042ec35469
commit fc6b0efcb6
17 changed files with 1793 additions and 1 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.swp

0
.nojekyll Normal file
View File

25
Makefile Normal file
View File

@ -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

View File

@ -1,3 +1,24 @@
# stl_web_viewer2
Just a fork from https://github.com/brentyi/stl_web_viewer2
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
<!doctype html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js" integrity="sha256-tAVw6WRAXc3td2Esrjd28l54s3P2y7CDFu1271mu5LE=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://brentyi.github.io/stl_web_viewer2/build/stlwebviewer2.css" />
</head>
<body>
<div class="stlwv2-model" data-model-url="models/planet_gear.stl"></div>
<script src="https://brentyi.github.io/stl_web_viewer2/build/stlwebviewer2.js"></script>
</body>
</html>
```

1
build/stlwebviewer2.css Normal file
View File

@ -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}

1
build/stlwebviewer2.js Normal file

File diff suppressed because one or more lines are too long

37
index.html Normal file
View File

@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js" integrity="sha256-tAVw6WRAXc3td2Esrjd28l54s3P2y7CDFu1271mu5LE=" crossorigin="anonymous"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="build/stlwebviewer2.css" />
<style>
html {
background-color: #f7f7f7;
height: 100%;
}
body {
width: 50em;
max-width: 100%;
margin: 0 auto;
min-height: 100%;
display: flex;
align-items: center;
}
.stlwv2-model {
width: 100%;
height: 30em;
}
</style>
</head>
<body>
<div class="stlwv2-model" data-model-url="models/printer.stl"></div>
<script src="build/stlwebviewer2.js"></script>
</body>
</html>

BIN
models/planet_gear.stl Normal file

Binary file not shown.

BIN
models/plotter.stl Normal file

Binary file not shown.

BIN
models/printer.stl Normal file

Binary file not shown.

BIN
models/remote.stl Normal file

Binary file not shown.

69
scripts/Detector.js Normal file
View File

@ -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 <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br />',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.',
].join("\n")
: [
'Your browser does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br/>',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.',
].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;
}

944
scripts/OrbitControls.js Normal file
View File

@ -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;
},
},
});

255
scripts/STLLoader.js Normal file
View File

@ -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;
}
},
};

306
scripts/STLWebViewer2.js Normal file
View File

@ -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(
[
'<input class="stlwv2-fullscreen-checkbox" id="' +
checkboxId +
'" type="checkbox"></input>',
'<div class="stlwv2-inner">',
' <div class="stlwv2-percent"></div>',
' <label class="stlwv2-hud stlwv2-fullscreen-on" title="Fullscreen" for="' +
checkboxId +
'">',
" &#x21F1;</label>",
' <label class="stlwv2-hud stlwv2-fullscreen-off" title="Close" for="' +
checkboxId +
'">',
" &times;</label>",
' <a class="stlwv2-hud stlwv2-github-link" target="_blank" href="https://github.com/brentyi/stl_web_viewer2">',
" STL Web Viewer</a>",
"</div>",
].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
);
}
);
}
};

23
scripts/ScrollHelpers.js Normal file
View File

@ -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;
},
};

109
stylesheets/style.css Normal file
View File

@ -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;
}