307 lines
8.9 KiB
JavaScript
307 lines
8.9 KiB
JavaScript
/*
|
|
* 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
|
|
);
|
|
}
|
|
);
|
|
}
|
|
};
|