Initial commit
This commit is contained in:
parent
042ec35469
commit
fc6b0efcb6
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.swp
|
25
Makefile
Normal file
25
Makefile
Normal 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
|
23
README.md
23
README.md
@ -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
1
build/stlwebviewer2.css
Normal 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
1
build/stlwebviewer2.js
Normal file
File diff suppressed because one or more lines are too long
37
index.html
Normal file
37
index.html
Normal 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
BIN
models/planet_gear.stl
Normal file
Binary file not shown.
BIN
models/plotter.stl
Normal file
BIN
models/plotter.stl
Normal file
Binary file not shown.
BIN
models/printer.stl
Normal file
BIN
models/printer.stl
Normal file
Binary file not shown.
BIN
models/remote.stl
Normal file
BIN
models/remote.stl
Normal file
Binary file not shown.
69
scripts/Detector.js
Normal file
69
scripts/Detector.js
Normal 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
944
scripts/OrbitControls.js
Normal 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
255
scripts/STLLoader.js
Normal 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
306
scripts/STLWebViewer2.js
Normal 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 +
|
||||
'">',
|
||||
" ⇱</label>",
|
||||
' <label class="stlwv2-hud stlwv2-fullscreen-off" title="Close" for="' +
|
||||
checkboxId +
|
||||
'">',
|
||||
" ×</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
23
scripts/ScrollHelpers.js
Normal 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
109
stylesheets/style.css
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user