Madeleine.js-for-Gitea/src/Madeleine.js

1357 lines
51 KiB
JavaScript

/**
* @author Junho Jin[junho.jin@kaist.ac.kr] | https://github.com/JinJunho
* @version 1.0.0
*
* [Project] Madeleine.js, Pure JavaScript STL Parser & Renderer.
*
* [Description] Madeleine.js constists of three part: DataReader, Madeleine and Lily.
* DataReader is a helper function to read binary data, which is a customized version
* of DataView. Lily is a helper Object that manages created Madeleines and logs
* messages to console. Madeleine is the part that deals with actual parsing, rendering
* and showing your stl files. Customize them however you want. This project is under
* MIT License (see the LICENSE file for details). You are allowed to do anything with
* this code, as long as you leave the attribution (MUST!). It will be glad if you
* contact me for any bug you found or interesting ideas to do with Madeleine.js.
* I'm willing to co-work with you!
*/
(function() {
// Madeleine constructor
Madeleine = function(options) {
var Madeleine;
// Constants for default setting
var CONVERT_TO_BINARY = true;
var OBJECT_MATERIAL = "matt";
var OBJECT_STATUS = false;
var OBJECT_BACKGROUND = "DADADA";
var OBJECT_COLOR = "FF9900";
var CAMERA_SIGHT = 45;
var CAMERA_NEARFIELD = 1;
var CAMERA_FARFIELD = 100000;
var VIEWER_THEME = "default";
var VIEWER_PREFIX = "mad-";
var VIEWER_CREATE = true;
var VIEWER_HEIGHT = 400;
var VIEWER_WIDTH = 640;
var USER_ROTATE_SENSITIVITY = 0.005;
var USER_ZOOM_SENSITIVITY = 100;
var SCROLL_FACTOR = 10;
// Necessary option check
if (!document.getElementById(options.target)) {
console.log("MADELEINE[ERR] Target must be a valid DOM Element.");
return null;
} else if (!options.data) {
console.log("MADELEINE[ERR] Option must contain target and data.");
return null;
}
// Construct new Madeleine
Madeleine = function(options) {
// Alias to own object
var scope = this;
// Internal properties
this.__uniqueID = Lily.push(this);
this.__containerID = options.target;
this.__timer = {start: (new Date).getTime(), end: null};
// About 3d model
this.__status = null;
this.__object = null;
this.__bounds = null;
this.__center = null;
this.__rawText = null;
this.__converted = null;
this.__arrayBuffer = null;
// Initialize data info object
this.__info = {type: null, load: this.type, vertices: 0, facets: 0, faces: 0};
// About visualization
this.__scene = new THREE.Scene();
this.__camera = null;
this.__viewer = null;
this.__canvas = null;
// About rendering
this.__geometry = null;
this.__renderer = null;
// About camera view
this.__width = null;
this.__height = null;
this.__sizeRatio = 1;
// About user interaction
this.__firstPerson = false;
this.__movable = true;
this.__zoomable = true;
this.__rotatable = true;
this.__rotating = false;
this.__trackMouse = false;
this.__mouseX = 0;
this.__mouseY = 0;
// Crucial properties to render 3d model
this.data = options.data;
this.type = options.type ? options.type : 'file';
this.container = document.getElementById(this.__containerID);
this.relPath = options.path ? options.path + (options.path[options.path.length-1] == "/" ? "" : "/") : "./";
// User configuration
this.options = Lily.extend(true, {}, { // Default option
material : OBJECT_MATERIAL,
showStatus : OBJECT_STATUS,
backgroundColor : OBJECT_BACKGROUND,
objectColor : OBJECT_COLOR,
viewer : {
create : VIEWER_CREATE, // Create new viewer?
prefix : VIEWER_PREFIX, // Viewer id prefix
height : VIEWER_HEIGHT, // Viewer height
width : VIEWER_WIDTH, // Viewer width
theme : VIEWER_THEME, // Viewer theme
},
camera : {
sight : CAMERA_SIGHT, // Vertical Field of View
near : CAMERA_NEARFIELD, // Near Field Distance
far : CAMERA_FARFIELD, // Far Field Distance
},
rotateSensitivity : USER_ROTATE_SENSITIVITY,
scrollFactor : SCROLL_FACTOR,
zoomSensitivity : USER_ZOOM_SENSITIVITY,
}, options);
// Event Listeners
this.scrollHandler = function(e) {
var delta = e.wheelDelta ? e.wheelDelta/40 : (e.detail ? -e.detail : 0);
if (delta < 0) delta -= this.options.scrollFactor;
if (delta > 0) delta += this.options.scrollFactor;
scope.cameraZoom(delta);
e.preventDefault();
}.bind(this);
this.gestureHandler = function(e) {
scope.cameraZoom(( 1 < e.scale ? "in" : "out" ));
e.preventDefault();
};
this.mouseDownHandler = function(e) {
e.preventDefault();
if ((e.which && e.which == 3) || (e.button && e.button == 2)) {
// alert("Right click on 3D viewer is disabled.");
} else {
scope.trackMouse = true;
scope.__rotating = false;
scope.mouseY = e.clientY;
scope.mouseX = e.clientX;
}
};
this.mouseMoveHandler = function(e) {
if (scope.trackMouse) {
// Top-left corner is (0, 0)
// e.clientX grows as mouse goes down
// e.clientY grows as mouse goes right
//IMPROVED move object on wheel drag and rotate with left mouse drag
if (e.which == 1) {//left button mouse
scope.rotateObjectZ(scope.mouseX - e.clientX);
scope.rotateObjectX(scope.mouseY - e.clientY);
}
if (e.which == 2) {//whell button mouse
scope.moveObject(event, scope.mouseX - e.clientX);
scope.moveObject(event, scope.mouseY - e.clientY);
}
scope.mouseY = e.clientY;
scope.mouseX = e.clientX;
}
e.preventDefault();
};
this.mouseUpHandler = function(e) {
scope.trackMouse = false;
scope.__rotating = true;
e.preventDefault();
};
this.touchStartHandler = function(e) {
if (e.changedTouches.length == 1) {
scope.trackMouse = true;
scope.__rotating = false;
scope.mouseY = e.changedTouches[0].clientY;
scope.mouseX = e.changedTouches[0].clientX;
}
};
this.touchMoveHandler = function(e) {
if (scope.trackMouse) {
// Top-left corner is (0, 0)
// e.clientX grows as touch goes down
// e.clientY grows as touch goes right
scope.rotateObjectZ(scope.mouseX - e.clientX);
scope.rotateObjectX(scope.mouseY - e.clientY);
scope.mouseY = e.clientY;
scope.mouseX = e.clientX;
}
e.preventDefault();
};
this.touchEndHandler = function(e) {
scope.trackMouse = false;
scope.__rotating = true;
e.preventDefault();
};
this.rightClickHandler = function(e) {
e.preventDefault();
};
this.viewModeHandler = function(e) {
if (scope.__firstPerson) {
e.target.className = e.target.className.replace(" focused", "");
scope.__firstPerson = false;
scope.disableFirstPersonViewerMode();
} else {
e.target.className += " focused";
scope.__firstPerson = true;
scope.enableFirstPersonViewerMode();
}
};
this.captureHandler = function(e) {
// TODO capture the model
};
// Check if option values are correct
this.adjustUserConfiguration();
// Initialize rendering
this.init();
};
// Initialize rendering
Madeleine.prototype.init = function() {
//IMPROVE callbackstart process i.e.: show another loader in other scope
if (this.options.callbackstart) this.options.callbackstart();
// Get file name
this.__info.name = (typeof this.data == "string") ? this.data.split("/").slice(-1)[0] : this.data.name;
// If create new viewer, set canvas size to the viewer.
if (this.options.viewer.create) {
this.__height = this.options.viewer.height;
this.__width = this.options.viewer.width;
// Get target width and height, otherwise.
} else if (document.defaultView && document.defaultView.getComputedStyle) {
this.__height = parseFloat(document.defaultView.getComputedStyle(this.container,null).getPropertyValue('height'));
this.__width = parseFloat(document.defaultView.getComputedStyle(this.container,null).getPropertyValue('width'));
} else {
this.__height = parseFloat(this.container.currentStyle.height);
this.__width = parseFloat(this.container.currentStyle.width);
}
// Create viewer
this.createViewer();
// Adjust canvas width/height ratio
this.__sizeRatio = this.__width / this.__height;
// Create camera
this.__camera = new THREE.PerspectiveCamera(
this.options.camera.sight,
this.__sizeRatio,
this.options.camera.near,
this.options.camera.far
);
// Set user's point-of-view and default camera position
this.__camera.position.set(0, 0, 0);
this.__scene.add(this.__camera);
// Add lights
if (OBJECT_MATERIAL == "shiny") {
this.addShadowedLight(1, 1, 1, 0xFFFFFF, 1.3);
this.addShadowedLight(0.5, 1, -1, 0xFFCC66, 1);
var ambientLight = new THREE.AmbientLight(0x111111);
this.__scene.add(ambientLight);
} else {
var directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
var ambientLight = new THREE.AmbientLight(0x202020);
directionalLight.position.set(0, 1, 1).normalize();
this.__scene.add(directionalLight);
this.__scene.add(ambientLight);
}
// Start rendering
this.draw();
};
// Rendering start
Madeleine.prototype.draw = function() {
// Wait until data is fully loaded
var queued = (function(scope) {
return function() {
// When data ready, parse and render it.
scope.run(scope.relPath + "lib/MadeleineLoader.js", {
arrbuf: scope.__arrayBuffer,
rawtext: scope.__rawText
}, function(result) {
var hasColors = result.hasColors;
var vertices = result.vertices;
var normals = result.normals;
var colors = result.colors;
var alpha = result.alpha;
// Create new geometry
scope.__geometry = new THREE.Geometry();
// Parsing done. Add vertices and normals to geometry
scope.__geometry = new THREE.BufferGeometry();
scope.__geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
scope.__geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));
// Set color information
if (hasColors) {
scope.__geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
scope.__geometry.hasColors = true;
scope.__geometry.alpha = alpha;
}
// Start rendering
scope.renderObject();
// Compute time consumed in parsing and rendering
scope.__timer.end = (new Date()).getTime();
var consumed = (scope.__timer.end - scope.__timer.start) / 1000;
console.log("MADELEINE[LOG] Time spent: " + consumed + " sec.");
// Render object
scope.render();
// Log rendering status
scope.logStatus();
// Start rotating animation
scope.startAnimation();
// Enable mouse zoom action
scope.enableZoomAsMouseScroll();
// Enable mouse motion action
scope.enableUserInteraction();
//IMPROVED callback end i.e: hide loading of another scope
if (scope.options.callbackend) scope.options.callbackend();
});
};
})(this);
// Check input type and get STL Binary data
switch (this.type) {
case "upload":
this.getDataFromBlob(this.data, queued);break;
case "file":
this.getDataFromUrl(this.data, queued);break;
default:
break;
}
};
// Get arrayBuffer from Blob
Madeleine.prototype.getDataFromBlob = function(file, queuedWork) {
var scope = this;
if (Detector.fileapi) {
var arrbuf = new FileReader();
var rawtxt = new FileReader();
// arraybuffer onload function
arrbuf.onload = function () {
scope.__arrayBuffer = arrbuf.result
//console.log("scope.__arrayBuffer------>", scope.__arrayBuffer);
if (scope.__arrayBuffer && scope.__rawText) {//IMPROVED asynchronous onload arrbuf.onload rawtxt.onload doesnt know witch will finish first causing null __arrayBuffer or null __rawText
console.log('calling o queuedWork no arrbuf onload');
queuedWork();
}
};
// read arrayBuffer from Blob
arrbuf.readAsArrayBuffer(file);
// rawtext onload function
rawtxt.onload = function () {
scope.__rawText = rawtxt.result;
//console.log("scope.__rawText ----->", scope.__rawText);
if (scope.__arrayBuffer && scope.__rawText) {//IMPROVED asynchronous onload arrbuf.onload rawtxt.onload doesnt know witch will finish first causing null __arrayBuffer or null __rawText
console.log('calling in no rawtxt onload');
queuedWork();
}
};
// read raw text data from Blob
rawtxt.readAsText(file);
}
};
// Get arrayBuffer from external file
Madeleine.prototype.getDataFromUrl = function(url, queuedWork, type) {
var scope = this;
var getArrayBuffer = function() {
var arrbuf = new XMLHttpRequest();
arrbuf.onerror = function(e) { console.log("MADELEINE[ERR] Ajax failed.") };
arrbuf.onreadystatechange = function() {
if (arrbuf.readyState == 4 && (arrbuf.status == 200 || arrbuf.status == 0)) {
scope.__arrayBuffer = arrbuf.response;
if (type == "binary") queuedWork();
if (!type) getRawText();
}
};
arrbuf.responseType = "arraybuffer";
arrbuf.open("GET", url, true);
arrbuf.send(null);
};
var getRawText = function() {
var rawtxt = new XMLHttpRequest();
rawtxt.onerror = function(e) { console.log("MADELEINE[ERR] Ajax failed.") };
rawtxt.onreadystatechange = function() {
if (rawtxt.readyState == 4 && (rawtxt.status == 200 || rawtxt.status == 0)) {
scope.__rawText = rawtxt.response;
queuedWork();
}
};
rawtxt.responseType = "text";
rawtxt.open("GET", url, true);
rawtxt.send(null);
};
// Get data from external resource
switch(type) {
case "ascii":
getRawText();
break;
default:
getArrayBuffer();
break;
}
};
// Render object
Madeleine.prototype.renderObject = function() {
// Create renderer
if (!this.__object) this.createRenderer();
// Get material
var material = null;
if (this.options.material == "shiny") {
material = new THREE.MeshPhongMaterial({
color: this.getHexColor(this.options.objectColor),
ambient: this.getHexColor(this.options.objectColor),
specular: 0x1F1F1F,
shininess: 25,
side: THREE.DoubleSide,
overdraw: true
});
} else {
material = new THREE.MeshLambertMaterial({
color: this.getHexColor(this.options.objectColor),
shading: THREE.FlatShading,
side: THREE.DoubleSide,
overdraw: true
});
}
// Generate mesh for object
this.__object = new THREE.Mesh(this.__geometry, material);
this.__geometry.computeBoundingSphere();
this.__geometry.computeVertexNormals();
this.__geometry.computeBoundingBox();
// Adjust positions of camera and object to make object fit into screen properly
var radius = this.__geometry.boundingSphere.radius;
// 1.865 times zoomed object with radius 85 practically fits best to the screen
var zoomFactor = 1.865 * 85 / radius;
// Zoom object to fit into the screen. ZoomFactor is practically best to fit
this.__object.scale.set(zoomFactor, zoomFactor, zoomFactor);
// Height 125 fits well to the screen. If taller, lower the center of y-axis
var maxY = this.__geometry.boundingBox.max.y;
var minY = this.__geometry.boundingBox.min.y;
var height = maxY - minY;
var deltaY = (height > 125 ? parseFloat(0.99 * (height - Math.max(maxY, Math.abs(minY)))) : 15);
// deltaY in some cases can make object out of screen height
//console.log(deltaY, height, minY, maxY);
if (options.XY) {//IMPROVMENT setXY mannually
if (options.XY.X) {
this.__object.position.setX(options.XY.X);
}
if (options.XY.Y) {
this.__object.position.setY(options.XY.Y);
}
} else {
this.__object.position.setY(-deltaY);
}
// If object is too large to fit in, make camera look further
// 500 (default camera distance) : 466 (view height)
// new distance (x) : boundingSphere radius (r) * 2
// x = (2 * r * 500) / 466 ~= 2.146 * r
if (radius < 233) this.__camera.position.z = 500;
else this.__camera.position.z = radius * 2.146;
this.__camera.updateProjectionMatrix();
// Parsing finished
this.__scene.add(this.__object);
this.__object.rotation.x = -1.2;
this.__object.rotation.z = 1.2;
};
// Generate Renderer
Madeleine.prototype.createRenderer = function() {
// create renderer
this.__renderer = Detector.webgl ?
new THREE.WebGLRenderer({
preserveDrawingBuffer: true,
alpha: true
}) : new THREE.CanvasRenderer();
// attach canvas to viewer
this.__canvas = this.__renderer.domElement;
this.__viewer.appendChild(this.__canvas);
// renderer configuration
this.__renderer.setSize(this.__width, this.__height);
this.__renderer.setClearColor(0x000000, 0);
// this.__renderer.shadowMapCullFace = THREE.CullFaceBack;
// this.__renderer.shadowMapEnabled = true;
// this.__renderer.gammaOutput = true;
// this.__renderer.gammaInput = true;
};
// Generate Madeleine Viewer
Madeleine.prototype.createViewer = function() {
if (!this.options.viewer.create) this.container.appendChild(this.__renderer.domElement);
else {
// Create viewer element
this.__viewer = document.createElement("div");
this.__viewer.id = this.options.viewer.prefix + this.__uniqueID;
// Force viewer size
this.container.style["max-height"] = this.__height+"px";
this.container.style["min-height"] = this.__height+"px";
this.__viewer.style["max-height"] = this.__height+"px";
this.__viewer.style["min-height"] = this.__height+"px";
this.__viewer.style["max-width"] = this.__width+"px";
this.__viewer.style["min-width"] = this.__width+"px";
this.__viewer.style.height = this.__height;
this.__viewer.style.width = this.__width;
// Set default style
this.__viewer.style.background = "transparent";
this.__viewer.style.position = "relative";
this.__viewer.style.margin = "0 0 10px 0";
// Progress bar
var progress = document.createElement("div");
progress.id = "progressBar-" + this.__uniqueID;
progress.style["-webkit-transition"] = "width .5s ease-in-out, opacity 2s ease";
progress.style["-moz-transition"] = "width .5s ease-in-out, opacity 2s ease";
progress.style["-ms-transition"] = "width .5s ease-in-out, opacity 2s ease";
progress.style["-o-transition"] = "width .5s ease-in-out, opacity 2s ease";
progress.style["transition"] = "width .5s ease-in-out, opacity 2s ease";
progress.style["min-height"] = "2px";
progress.style.background = "#009999";
progress.style.position = "absolute";
progress.style.height = "2px";
progress.style.width = 0;
progress.style.left = 0;
progress.style.top = 0;
// Viewer iconGrid
var iconGrid = document.createElement("div");
iconGrid.style.cssText += "background:transparent;position:absolute;padding:15px 10px;";
iconGrid.style.cssText += "height:50px;width:"+this.__width+"px;top:0;overflow:hidden;";
iconGrid.className += "box";
var logo = document.createElement("div");
var info = document.createElement("div");
var view = document.createElement("div");
var capture = document.createElement("div");
//var download = document.createElement("div");
var fullscreen = document.createElement("div");
info.id = "model-info-" + this.__uniqueID;
info.className += "model-info noselect";
info.innerHTML = this.__info.name;
logo.className += "clickable pull-left madeleine-logo";
view.className += "clickable pull-right icon-mad-view";
capture.className += "clickable pull-right icon-mad-capture";
//download.className += "clickable pull-right icon-mad-download";
fullscreen.className += "clickable pull-right icon-mad-screen-full";
Lily.bind(view, "click", this.viewModeHandler);
Lily.bind(capture, "click", this.captureHandler);
var rotator = document.createElement("div");
var faster = document.createElement("div");
var slower = document.createElement("div");
var player = document.createElement("div");
rotator.style.cssText += "background:transparent;position:absolute;padding:15px 10px;right:0;";
rotator.style.cssText += "height:50px;width:"+this.__width+"px;top:0;overflow:hidden;";
rotator.style.cssText += "margin-top:"+(this.__height-30)+"px;";
player.className += "icon-clickable pull-right icon-mad-stop";
slower.className += "icon-clickable pull-right icon-mad-slower";
faster.className += "icon-clickable pull-right icon-mad-faster";
rotator.appendChild(faster);
rotator.appendChild(player);
rotator.appendChild(slower);
var controller = document.createElement("div");
var trackball = document.createElement("div");
var right = document.createElement("div");
var left = document.createElement("div");
var down = document.createElement("div");
var up = document.createElement("div");
iconGrid.appendChild(fullscreen);
//iconGrid.appendChild(download);
iconGrid.appendChild(capture);
iconGrid.appendChild(view);
iconGrid.appendChild(logo);
iconGrid.appendChild(info);
iconGrid.appendChild(rotator);
// Append to container
if (this.options.viewer.notappend) {//IMPROVED not append a new canvas put in same
$(this.container).html(this.__viewer);
this.__viewer.appendChild(iconGrid);
this.__viewer.appendChild(progress);
} else {
this.container.appendChild(this.__viewer);
this.__viewer.appendChild(iconGrid);
this.__viewer.appendChild(progress);
}
this.adaptViewerTheme();
}
};
// Set viewer theme
Madeleine.prototype.adaptViewerTheme = function() {
var theme = arguments.length == 0 ? this.options.viewer.theme : arguments[0];
// Adapt theme
switch (theme) {
case "dark":
this.__viewer.style.background = "#000000";
this.options.objectColor = "FFD300";
break;
case "lime":
this.__viewer.style.cssText += this.generateGradation({dark: "2B2B2B"});
this.options.objectColor = "D4FF00"; // [212, 255, 0];
break;
case "rose":
this.__viewer.style.cssText += this.generateGradation({bright: "369075"});
this.options.objectColor = "C94C66"; // [201, 76, 102];
break;
case "lego":
this.__viewer.style.cssText += this.generateGradation({bright: "FFA400"});
this.options.objectColor = "00A08C"; // [0, 160, 140];
break;
case "toxic":
this.__viewer.style.cssText += this.generateGradation({bright: "FFEE4D"});
this.options.objectColor = "5254CB"; // [82, 84, 203];
break;
case "cobalt":
this.__viewer.style.cssText += this.generateGradation({bright: "FFC200"});
this.options.objectColor = "0C6BC0"; // [12, 107, 192];
break;
case "light":
this.__viewer.style.cssText += this.generateGradation({bright: "FFFFFF"});
this.options.objectColor = "F00842";
break;
case "soft":
this.__viewer.style.cssText += this.generateGradation({dark: "0F0F0F", bright: "4D4D4D", pos1: "0", pos2: "60"});
this.options.objectColor = OBJECT_COLOR;
break;
default:
this.__viewer.style.background = this.makeHexString(options.backgroundColor || OBJECT_BACKGROUND);
this.options.objectColor = options.objectColor || OBJECT_COLOR;
break;
}
// If object exists, paint color on it.
this.__object && this.setObjectColor();
};
// Set canvas background color
Madeleine.prototype.setBackgroundColor = function(code) {
var code = arguments.length == 3 ? [arguments[0], arguments[1], arguments[2]] : code;
var color = this.getHexString(code);
this.__viewer.style.background = this.makeHexString(color);
};
// Set object surface color
Madeleine.prototype.setObjectColor = function() {
var color = arguments.length != 0 ? arguments : this.options.objectColor;
this.__object.material.color.setHex(this.getHexColor(color));
};
// Generate gradation css
Madeleine.prototype.generateGradation = function(colors) {
var pos1, pos2, darker, brighter, cssText;
cssText = "background: BRIGHT;" +
"background: -moz-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
"background: -webkit-gradient(radial, center center, 0px, center center, POS2%, color-stop(POS1%,BRIGHT), color-stop(POS2%,DARK))" +
"background: -webkit-radial-gradient(center, ellipse cover, BRIGHT POS1%,DARK POS2%);" +
"background: -o-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
"background: -ms-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
"background: radial-gradient(ellipse at center, BRIGHT POS1%, DARK POS2%);" +
"filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='BRIGHT', endColorstr='DARK',GradientType=1)";
if (colors.dark) {
darker = this.getHexString(colors.dark);
brighter = this.getHexString(colors.bright ? colors.bright : this.adjustBrightness(darker));
} else {
brighter = this.getHexString(colors.bright);
darker = this.getHexString(this.adjustBrightness(brighter));
}
pos1 = colors.pos1 ? colors.pos1 : 27;
pos2 = colors.pos2 ? colors.pos2 : 100;
return cssText.replace(/BRIGHT/g, brighter).replace(/DARK/g, darker).replace(/POS1/g, pos1).replace(/POS2/g, pos2);
};
// Get Hex color (0xXXXXXX format)
Madeleine.prototype.getHexColor = function() {
var code = arguments.length == 3 ? [arguments[0], arguments[1], arguments[2]] : arguments[0];
var color = this.getHexString(code);
return parseInt(color.replace(/\#/g, ''), 16);
};
// Get Hex-style string (#XXXXXX format)
Madeleine.prototype.getHexString = function(code) {
if (typeof code == "string") return this.makeHexString(code);
else if (typeof code == "object") return this.rgbToHex(code);
};
// Strip # from Hex-style string
Madeleine.prototype.cutHexString = function(code) {
return code.charAt(0) == "#" ? code.substring(1) : code;
};
// Prepend # to hex string
Madeleine.prototype.makeHexString = function(code) {
var color = code.length == 3 ? code.replace(/(.)/g, '$1$1') : code;
return color.charAt(0) != "#" ? "#" + color : color;
};
// Convert RGB to Hex
Madeleine.prototype.rgbToHex = function(code) {
var i, color, hex = "#";
for (i = 0; i < code.length; i++) {
color = code[i].toString(16);
hex += (color.length == 1 ? "0" + color : color);
}
return hex;
};
// Convert Hex to RGB
Madeleine.prototype.hexToRgb = function(code) {
var dec = this.cutHexString(code).toString(16);
return [parseInt(dec.substring(0, 2), 16),
parseInt(dec.substring(2, 4), 16),
parseInt(dec.substring(4, 6), 16)];
};
// Get brighter or darker color from input color
// Brightness is automatically set according to its intensity.
Madeleine.prototype.adjustBrightness = function(code) {
var adjustedR, adjustedG, adjustedB;
var intensity, color, rgb, r, g, b;
var darkFactor = 30, brightFactor = 20;
// Get color and RGB
color = this.cutHexString(code);
rgb = this.hexToRgb(color);
r = rgb[0];
g = rgb[1];
b = rgb[2];
// Get grayscale intensity
intensity = Math.sqrt(r*r + g*g + b*b) / 255 * 100;
// New color
adjustedR = intensity > 40 ? (0|(1<<8) + r * (1 - darkFactor / 100)) : (0|(1<<8) + r + (256 - r) * brightFactor / 100);
adjustedG = intensity > 40 ? (0|(1<<8) + g * (1 - darkFactor / 100)) : (0|(1<<8) + g + (256 - g) * brightFactor / 100);
adjustedB = intensity > 40 ? (0|(1<<8) + b * (1 - darkFactor / 100)) : (0|(1<<8) + b + (256 - b) * brightFactor / 100);
return (adjustedR.toString(16)).substr(1) +
(adjustedG.toString(16)).substr(1) +
(adjustedB.toString(16)).substr(1);
};
// Add new background worker
Madeleine.prototype.run = function(path, luggage, onload) {
var progressBar = document.getElementById("progressBar-"+this.__uniqueID);
var worker = new WorkerFacade(path);
var scope = this;
worker.postMessage(luggage);
worker.onmessage = function(event) {
var result = event.data;
switch (result.type) {
case "convert-progress":
// console.log("MADELEINE[LOG] Background converting progress: " + result.data + "%");
break;
case "convert":
if (CONVERT_TO_BINARY) scope.run(scope.relPath + "lib/MadeleineConverter.js", result.data, function(result) { scope.__converted = result });
break;
case "progress":
progressBar.style.width = (scope.__width * result.data / 100) + "px";
if (result.data == 100) {
progressBar.style["-webkit-opacity"] = 0;
progressBar.style["-moz-opacity"] = 0;
progressBar.style["opacity"] = 0;
};
// console.log("MADELEINE[LOG] Background work progress: " + result.data + "%");
break;
case "message":
console.log("MADELEINE[LOG] Background work message: " + result.data);
break;
case "error":
console.log("MADELEINE[ERR] Background work error: " + result.data);
break;
case "info":
scope.__info[result.prop] = result.data;
if (result.prop == "size") {
var infoBox = document.getElementById("model-info-" + scope.__uniqueID);
infoBox.innerHTML += " (" +result.data + ")";
}
break;
case "data":
scope["__" + result.prop] = result.data;
break;
case "done":
onload(result.data);
break;
default:
// Do nothing
break;
}
};
};
// Start animation
Madeleine.prototype.startAnimation = function() {
if (this.__rotatable) this.__rotating = true;
this.interact(this);
};
// Stop animation
Madeleine.prototype.stopAnimation = function() {
this.__rotating = false;
};
// Start rotating object
Madeleine.prototype.startRotation = function() {
this.options.rotateSensitivity = 0.005;
this.__rotating = true;
};
// Stop rotating object
Madeleine.prototype.stopRotation = function() {
this.__rotating = false;
};
// Enable user interaction
Madeleine.prototype.interact = function(scope) {
requestAnimationFrame(function(){scope.interact(scope)});
scope.options.showStatus && scope.__status.update();
scope.render();
};
// Perform actual rendering object
Madeleine.prototype.render = function() {
this.__rotating && this.rotateObjectZ(-1);
this.__renderer.render(this.__scene, this.__camera);
};
// Move object in xy //IMPROVED
let oldX = 0, oldY = 0;
Madeleine.prototype.moveObject = function ($event, delta) {
if (this.__movable) {
let directionX = 0, directionY = 0, diffX = 0, diffY = 0;
let offsetmove = options.MOVE_FACTOR || 3;
if ($event.pageX < oldX) {
directionX = "left"
diffX = oldX - $event.pageX;
} else if ($event.pageX > oldX) {
directionX = "right"
diffX = $event.pageX - oldX;
}
if ($event.pageY < oldY) {
directionY = "top"
diffY = oldY - $event.pageY;
} else if ($event.pageY > oldY) {
directionY = "bottom";
diffY = $event.pageY - oldY;
}
oldX = $event.pageX;
oldY = $event.pageY;
if (directionX == "left") this.__object.position.x -= offsetmove;
if (directionX == "right") this.__object.position.x += offsetmove;
if (directionY == "bottom") this.__object.position.y -= offsetmove;
if (directionY == "top") this.__object.position.y += offsetmove;
}
};
// Rotate object in X direction
Madeleine.prototype.rotateObjectX = function(delta) {
if (this.__movable) this.__object.rotation.x -= delta * this.options.rotateSensitivity;
};
// Rotate object in Y direction
Madeleine.prototype.rotateObjectY = function(delta) {
if (this.__movable) this.__object.rotation.y -= delta * this.options.rotateSensitivity;
};
// Rotate object in Z direction
Madeleine.prototype.rotateObjectZ = function(delta) {
if (this.__movable) this.__object.rotation.z -= delta * this.options.rotateSensitivity;
};
// Make animation faster as much as 'delta'
Madeleine.prototype.animationFaster = function(delta) {
this.options.rotateSensitivity *= (delta ? delta : 2);
};
// Make animation slower as much as 'delta'
Madeleine.prototype.animationSlower = function(delta) {
this.options.rotateSensitivity /= (delta ? delta : 2);
};
// Disable Madeline Viewer to be zoomed by mouse scroll
Madeleine.prototype.disableZoomAsMouseScroll = function() {
var target = this.container;
// remove event handler
Lily.remove(this.container, "mousewheel", this.scrollHandler);
Lily.remove(this.container, "DOMMouseScroll", this.scrollHandler);
Lily.remove(this.container, "gesturechange", this.gestureHandler);
};
// Enable Madeline Viewer to be zoomed by mouse scroll
Madeleine.prototype.enableZoomAsMouseScroll = function() {
if (!this.__zoomable) return;
// attach event handler
Lily.bind(this.container, "mousewheel", this.scrollHandler);
Lily.bind(this.container, "DOMMouseScroll", this.scrollHandler);
Lily.bind(this.container, "gesturechange", this.gestureHandler);
};
// Enable Madeline Viewer to be controlled by mouse movement
Madeleine.prototype.enableUserInteraction = function() {
// block right click action
Lily.bind(this.container, "contextmenu", this.rightClickHandler);
if (!this.__movable) return;
// attach event handler
Lily.bind(this.container, "mousedown", this.mouseDownHandler);
Lily.bind(this.container, "mousemove", this.mouseMoveHandler);
Lily.bind(this.container, "mouseup", this.mouseUpHandler);
// mobile support
Lily.bind(this.container, "touchstart", this.touchStartHandler);
Lily.bind(this.container, "touchmove", this.touchMoveHandler);
Lily.bind(this.container, "touchend", this.touchEndHandler);
};
// Disable Madeline Viewer to be controlled by mouse movement
Madeleine.prototype.disableUserInteraction = function() {
if (!this.__movable) return;
// attach event handler
Lily.remove(this.container, "mousedown", this.mouseDownHandler);
Lily.remove(this.container, "mousemove", this.mouseMoveHandler);
Lily.remove(this.container, "mouseup", this.mouseUpHandler);
// mobile support
Lily.remove(this.container, "touchstart", this.touchStartHandler);
Lily.remove(this.container, "touchmove", this.touchMoveHandler);
Lily.remove(this.container, "touchend", this.touchEndHandler);
};
// Enable first-person viewer mode
Madeleine.prototype.enableFirstPersonViewerMode = function() {
this.disableUserInteraction();
Lily.bind(document, "keypress", this.firstPersonHandler);
Lily.bind(document, "keydown", this.firstPersonHandler);
this.__firstPerson = true;
this.__rotating = false;
this.__zoomable = false;
this.__movable = false;
};
// Disable first-person viewer mode
Madeleine.prototype.disableFirstPersonViewerMode = function() {
this.enableUserInteraction();
this.__firstPerson = false;
this.__rotating = true;
this.__zoomable = true;
this.__movable = true;
};
// Adjust camera zoom in/out
Madeleine.prototype.cameraZoom = function() {
if (!this.__zoomable) return;
else {
var delta, type;
if (arguments.length == 1) {
if (typeof arguments[0] == "string") {
delta = this.options.zoomSensitivity;
type = arguments[0];
} else {
delta = arguments[0];
type = null;
}
} else if (arguments.length == 2) {
delta = arguments[0];
type = arguments[1];
}
if (type == "in") this.__camera.position.z += delta;
else this.__camera.position.z -= delta;
this.__camera.updateProjectionMatrix();
}
};
// Show animation status
Madeleine.prototype.logStatus = function(target) {
if (!this.options.showStatus) return;
this.__status = new Stats();
this.__status.domElement.style.position = 'absolute';
this.__status.domElement.style.top = '0px';
this.__viewer.appendChild(this.__status.domElement);
};
// Show animation status
Madeleine.prototype.stopLogStatus = function(target) {
this.options.showStatus = false;
this.__status = null;
};
// Adjust material, camera settings and sensitivities to have proper values
Madeleine.prototype.adjustUserConfiguration = function() {
this.adjustRotateSensitivity();
this.adjustZoomSensitivity();
this.adjustFocalPoint();
this.checkMaterial();
};
// Check material
Madeleine.prototype.checkMaterial = function() {
if (this.options.material !== "skin" || this.options.material !== "wire") {
this.options.material !== "skin";
}
};
// Adjust camera settings to fit into proper range
Madeleine.prototype.adjustFocalPoint = function() {
var sight = this.options.camera.sight;
var near = this.options.camera.near;
var far = this.options.camera.far;
if (sight && typeof sight === "number" && 75 <= sight && sight <= 1000) return;
else this.options.camera.sight = CAMERA_SIGHT;
if (near && typeof near === "number" && 0 <= near && near <= 1000) return;
else this.options.camera.near = CAMERA_NEARFIELD;
if (far && typeof far === "number" && 5000 <= far && far <= 100000) return;
else this.options.camera.far = CAMERA_FARFIELD;
};
// Adjust zoom sensitivity to fit into proper range
Madeleine.prototype.adjustZoomSensitivity = function() {
var intensity = this.options.zoomSensitivity;
var visibleField = this.options.camera.far - this.options.camera.near;
if (intensity && typeof intensity === "number" && 0 < intensity && intensity <= visibleField/1000) return;
else this.options.zoomSensitivity = visibleField/1000;
};
// Adjust rotate sensitivity to fit into proper range
Madeleine.prototype.adjustRotateSensitivity = function(intensity) {
var intensity = this.options.rotateSensitivity;
if (intensity && typeof intensity === "number" && 0 < intensity && intensity < 0.05) return;
else this.options.rotateSensitivity = USER_ROTATE_SENSITIVITY;
};
// Add directional light
Madeleine.prototype.addShadowedLight = function(x, y, z, color, intensity) {
var directionalLight = new THREE.DirectionalLight(color, intensity);
var d = 1;
directionalLight.position.set(x, y, z);
this.__scene.add(directionalLight);
// directionalLight.shadowCameraVisible = true;
directionalLight.castShadow = true;
directionalLight.shadowCameraTop = d;
directionalLight.shadowCameraLeft = -d;
directionalLight.shadowCameraRight = d;
directionalLight.shadowCameraBottom = -d;
directionalLight.shadowCameraNear = 1;
directionalLight.shadowCameraFar = 4;
directionalLight.shadowMapWidth = 1024;
directionalLight.shadowMapHeight = 1024;
directionalLight.shadowBias = -0.005;
directionalLight.shadowDarkness = 0.15;
};
//Set position
Madeleine.prototype.setXY = function (X, Y) {//IMPROVED
this.__object.position.setY(Y);
this.__object.position.setX(X);
}
return new Madeleine(options);
};
// Lily helps madeleine.
window.Lily = (function() {
// Initialize
var Lily = function() {
if (!Detector.webgl) Detector.addGetWebGLMessage();
this.sisters = [];
};
// Attach Madeleine to file input
Lily.prototype.ready = function(options) {
// Check option fields
if (!options.file || !options.target) {
console.log("MADELEINE[ERR] Option must contain target and file.");
return null;
}
var target = document.getElementById(options.file);
if (!target) {
console.log("MADELEINE[ERR] Please provide valid input file element.");
return null;
}
// Attach file upload event handler
if (target.tagName.toLowerCase() == "input" && target.type.toLowerCase() == "file") this.onFileInputChange(target, options);
// (Optional) Attach drag-and-drop event handler
if (options.dropzone) {
var dragOverHandler = function(e) {
e.stopPropagation();
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
};
var fileDropHandler = function(e) {
var i, files = e.dataTransfer.files;
e.stopPropagation();
e.preventDefault();
for (i = 0; i < files.length; i++) {
// create Madeleine for each file
var _options = window.Lily.extend({}, options, {type: "upload", data: files[i]});
var madeleine = new Madeleine(_options);
}
};
// Attach event handler
target = document.getElementById(options.dropzone);
this.bind(target, "dragover", dragOverHandler);
this.bind(target, "drop", fileDropHandler);
}
};
// Remove attached event handler from element
Lily.prototype.remove = function(elem, event, doThis) {
var attach = (elem.removeEventListener ? "removeEventListener" : (elem.detachEvent ? "detachEvent" : ""));
var onEvent = (elem.removeEventListener ? event : "on" + event);
if (attach == "") elem[onEvent] = doThis;
else elem[attach](onEvent, doThis, false);
};
// Attach new event handler to element
Lily.prototype.bind = function(elem, event, doThis) {
var attach = (elem.addEventListener ? "addEventListener" : (elem.attachEvent ? "attachEvent" : ""));
var onEvent = (elem.addEventListener ? event : "on" + event);
if (attach == "") elem[onEvent] = doThis;
else elem[attach](onEvent, doThis, false);
};
// jQuery source of extend function
// Reference: https://github.com/jquery/jquery/blob/master/src/core.js
Lily.prototype.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty,
push = Array.prototype.push,
slice = Array.prototype.slice,
trim = String.prototype.trim,
indexOf = Array.prototype.indexOf,
class2type = {"[object Boolean]": "boolean","[object Number]": "number","[object String]": "string","[object Function]": "function",
"[object Array]": "array","[object Date]": "date","[object RegExp]": "regexp","[object Object]": "object"},
helper = {
isFunction: function (obj) {return helper.type(obj) === "function"},
isArray: Array.isArray || function (obj) {return helper.type(obj) === "array"},
isWindow: function (obj) {return obj != null && obj == obj.window},
isNumeric: function (obj) {return !isNaN(parseFloat(obj)) && isFinite(obj)},
type: function (obj) {return obj == null ? String(obj) : class2type[toString.call(obj)] || "object"},
isPlainObject: function (obj) {if (!obj || helper.type(obj) !== "object" || obj.nodeType) {return false}
try { if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {return false}} catch (e) {return false}
var key; for (key in obj) {}; return key === undefined || hasOwn.call(obj, key)
}
};
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !helper.isFunction(target) ) {
target = {};
}
// Extend helper itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null ) {
// Extend the base object
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
if ( target === copy ) { continue; }
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( helper.isPlainObject(copy) ||
(copyIsArray = helper.isArray(copy)) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && helper.isArray(src) ? src : [];
} else { clone = src && helper.isPlainObject(src) ? src : {}; }
// Never move original objects, clone them
target[ name ] = this.extend( deep, clone, copy );
// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
};
// When user uploads files, call Madeleine immediately for each stl file.
Lily.prototype.onFileInputChange = function(input, options) {
// event handler
var fileInputChangeHandler = function() {
var i, files = this.files;
if (files.length) {
for (i = 0; i < files.length; i++) {
//IMPROVED - Only accepts stl files and invoke callbackerror and throws new error
if (files[i].type != 'stl' && files[i].name.indexOf("stl") == -1) {
if (options.callbackerror) options.callbackerror({ code: 1001, msg: "Invalid File" });
throw new Error("Invalid Format File. Only Accepts STL!");
}
// create Madeleine for each file
var _options = window.Lily.extend({}, options, {type: "upload", data: files[i]});
var madeleine = new Madeleine(_options);
// hide file input element
input.style.display = "none";
}
}
};
// attach event handler
this.bind(input, "change", fileInputChangeHandler);
};
// Put madeleine to madeleine list
Lily.prototype.push = function(madeleine) {
this.sisters.push(madeleine);
return this.sisters.length-1;
};
// Get madeleine of id 'index'
Lily.prototype.get = function(index) {
return this.sisters[index];
};
return new Lily();
})();
})();
/* A facade for the Web Worker API that fakes it in case it's missing.
* Good when web workers aren't supported in the browser, but it's still fast enough,
* so execution doesn't hang too badly (e.g. Opera 10.5).
* By Stefan Wehrmeyer, licensed under MIT
*/
WorkerFacade = null;
if(!!window.Worker){
WorkerFacade = (function(){
return function(path){
return new window.Worker(path);
};
}());
} else {
WorkerFacade = (function(){
var workers = {}, masters = {}, loaded = false;
var that = function(path){
var theworker = {}, loaded = false, callings = [];
theworker.postToWorkerFunction = function(args){
try{
workers[path]({"data":args});
}catch(err){
theworker.onerror(err);
}
};
theworker.postMessage = function(params){
if(!loaded){
callings.push(params);
return;
}
theworker.postToWorkerFunction(params);
};
masters[path] = theworker;
var scr = document.createElement("SCRIPT");
scr.src = path;
scr.type = "text/javascript";
scr.onload = function(){
loaded = true;
while(callings.length > 0){
theworker.postToWorkerFunction(callings[0]);
callings.shift();
}
};
document.body.appendChild(scr);
var binaryscr = document.createElement("SCRIPT");
binaryscr.src = thingiurlbase + '/binaryReader.js';
binaryscr.type = "text/javascript";
document.body.appendChild(binaryscr);
return theworker;
};
that.fake = true;
that.add = function(pth, worker){
workers[pth] = worker;
return function(param){
masters[pth].onmessage({"data": param});
};
};
that.toString = function(){
return "FakeWorker('"+path+"')";
};
return that;
}());
};