diff --git a/app.js b/app.js index cac0ac06..6eb4bdab 100644 --- a/app.js +++ b/app.js @@ -10,7 +10,7 @@ const fs = require('fs'); const uglify = require('uglify-es'); const moment = require('moment'); const agent = require('express-useragent'); -const license = require_fresh('./src/add/license.js'); +const license = require_fresh('./src/moto/license.js'); const version = license.VERSION || "rogue"; const fileCache = {}; @@ -227,7 +227,7 @@ const script = { "ext/three-bgu", "ext/three-svg", "ext/jszip", - "add/license", + "moto/license", "ext/clip2", "ext/tween", "ext/fsave", @@ -251,12 +251,12 @@ const script = { "moto/ctrl", "moto/pack", "moto/space", - "moto/load-3mf", - "moto/load-obj", - "moto/load-stl", - "moto/load-svg", - "moto/load-url", - "moto/load-file", + "load/3mf", + "load/obj", + "load/stl", + "load/svg", + "load/url", + "load/file", "moto/broker", "moto/idb", "kiri/ui", @@ -295,7 +295,7 @@ const script = { "ext/three", "ext/pngjs", "ext/jszip", - "add/license", + "moto/license", "ext/clip2", "ext/earcut", "add/array", @@ -344,7 +344,7 @@ const script = { ], minion : [ "main/kiri", - "add/license", + "moto/license", "ext/clip2", "ext/earcut", "add/array", @@ -370,7 +370,7 @@ const script = { ], engine : [ "main/kiri", - "add/license", + "moto/license", "ext/three", "add/array", "add/three", @@ -385,8 +385,8 @@ const script = { "geo/polygons", "moto/kv", "moto/broker", - "moto/load-obj", - "moto/load-stl", + "load/obj", + "load/stl", "kiri/conf", "kiri/client", "kiri/slice", @@ -400,7 +400,7 @@ const script = { ], meta : [ "ext/three", - "add/license", + "moto/license", "ext/tween", "ext/fsave", "ext/earcut", @@ -419,38 +419,38 @@ const script = { "moto/ajax", "moto/ctrl", "moto/space", - "moto/load-obj", - "moto/load-stl", + "load/obj", + "load/stl", "moto/idb", "moto/ui", "kiri/catalog", "main/meta" ], mesh : [ - "main/mesh", "moto/client", + "moto/license", "ext/three", "ext/three-bgu", "ext/three-svg", - "add/license", "ext/tween", "ext/fsave", "add/array", "add/three", "moto/ctrl", "moto/space", - "moto/load-3mf", - "moto/load-obj", - "moto/load-stl", - "moto/load-svg", - "moto/load-url", - "moto/load-file", + "load/3mf", + "load/obj", + "load/stl", + "load/svg", + "load/url", + "load/file", "moto/broker", - "moto/idb" + "moto/idb", + "main/mesh" ], mesh_work : [ "moto/worker", - "add/license", + "moto/license", "ext/jszip", "ext/clip2", "ext/earcut", diff --git a/src/kiri/init.js b/src/kiri/init.js index 11c304c0..f513546f 100644 --- a/src/kiri/init.js +++ b/src/kiri/init.js @@ -7,13 +7,11 @@ if (self.kiri.init) return; - const KIRI = self.kiri, + let KIRI = self.kiri, BASE = self.base, - MOTO = self.moto, CONF = KIRI.conf, WIN = self.window, DOC = self.document, - LOC = self.location, API = KIRI.api, SDB = API.sdb, UI = API.ui, diff --git a/src/kiri/main.js b/src/kiri/main.js index 3c5d1493..26872d9a 100644 --- a/src/kiri/main.js +++ b/src/kiri/main.js @@ -4,8 +4,9 @@ (function () { - let MOTO = moto, + let MOTO = self.moto, KIRI = self.kiri, + LOAD = self.load, BASE = self.base, UTIL = BASE.util, LANG = KIRI.lang.current, @@ -912,7 +913,7 @@ } else { // dialog $('load-file').onchange = function(event) { - MOTO.File.load(event.target.files[0]) + LOAD.File.load(event.target.files[0]) .then(data => onload(data[0].mesh)) .catch(error => console.log({error})); }; @@ -2040,7 +2041,7 @@ function platformLoadSTL(url, onload, formdata) { let scale = 1 / unitScale(); - new MOTO.STL().load(url, function(vertices, filename) { + new LOAD.STL().load(url, function(vertices, filename) { if (vertices) { let widget = newWidget().loadVertices(vertices); widget.meta.file = filename; @@ -2054,7 +2055,7 @@ function platformLoadURL(url, options = {}) { platform.group(); - MOTO.URL.load(url, options).then((objects) => { + LOAD.URL.load(url, options).then((objects) => { let widgets = []; for (let object of objects) { let widget = newWidget(undefined, options.group).loadVertices(object.mesh); @@ -2705,14 +2706,14 @@ } else { platform.add( newWidget(undefined,group) - .loadVertices(new MOTO.STL().parse(e.target.result,unitScale())) + .loadVertices(new LOAD.STL().parse(e.target.result,unitScale())) .saveToCatalog(e.target.file.name) ); } load_dec(); } else if (isobj) { - let objs = MOTO.OBJ.parse(e.target.result); + let objs = LOAD.OBJ.parse(e.target.result); let odon = function() { for (let obj of objs) { let name = e.target.file.name; @@ -2756,7 +2757,7 @@ API.hide.alert(msg); } let msg = API.show.alert('Decoding 3MF'); - MOTO.TMF.parseAsync(e.target.result).then(models => { + LOAD.TMF.parseAsync(e.target.result).then(models => { API.hide.alert(msg); if (models.length > 1 && !group) { UC.confirm(`group ${models.length} objects?`).then(ok => { @@ -2779,7 +2780,7 @@ loadCode(e.target.result, 'svg'); } else { let name = e.target.file.name; - let svg = MOTO.SVG.parse(e.target.result); + let svg = LOAD.SVG.parse(e.target.result); let ind = 0; for (let v of svg) { let num = ind++; diff --git a/src/load/3mf.js b/src/load/3mf.js new file mode 100644 index 00000000..eee4bacc --- /dev/null +++ b/src/load/3mf.js @@ -0,0 +1,112 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.TMF) return; + +load.TMF = { + parseAsync +}; + +function query(node, path, fn) { + let collect = false; + let match = path[0]; + if (match[0] === '+') { + match = match.slice(1); + collect = true; + } + for (let child of [...node.childNodes]) { + if (child.tagName === match) { + if (collect) { + fn(match, child); + } + if (path.length > 1) { + query(child, path.slice(1), fn); + } + } + } +} + +function loadModel(doc) { + let models = []; + function emitModel(model, faces) { + if (faces && faces.length) { + models.push({name: model, faces}); + } + } + + return new Promise((resolve, reject) => { + let model; + let faces; + let units; + let scale = 1; + let scaleMap = { + "inch": (1 / 25.4), + "foot": (1 / 304.8), + "micron": (1 / 1000), + "meter": 1000, + "millimeter": 1, + "centimeter": (1 / 10) + }; + + query(doc, ["+model","resources","+object","+mesh"], (type, node) => { + switch (type) { + case "model": + units = node.getAttribute("unit"); + if (units) { + scale = scaleMap[units] || 1; + } + break; + case "object": + emitModel(model, faces); + faces = []; + model = node.getAttribute("name") || undefined; + break; + case "mesh": + let vertices = []; + query(node, ["vertices","+vertex"], (type, vertex) => { + vertices.push([ + parseFloat(vertex.getAttribute("x")) * scale, + parseFloat(vertex.getAttribute("y")) * scale, + parseFloat(vertex.getAttribute("z")) * scale + ]); + }); + query(node, ["triangles","+triangle"], (type, triangle) => { + let v1 = parseInt(triangle.getAttribute("v1")); + let v2 = parseInt(triangle.getAttribute("v2")); + let v3 = parseInt(triangle.getAttribute("v3")); + faces.appendAll(vertices[v1]); + faces.appendAll(vertices[v2]); + faces.appendAll(vertices[v3]); + }); + break; + } + }); + + emitModel(model, faces); + resolve(models); + }); +} + +/** + * @param {Object} data binary file + * @returns {Array} vertex face array + */ +function parseAsync(data) { + return new Promise((resolve, reject) => { + JSZip.loadAsync(data).then(zip => { + for (let [key,value] of Object.entries(zip.files)) { + if (key.indexOf(".model") > 0) { + value.async("string").then(xml => { + resolve(loadModel(new DOMParser().parseFromString(xml, "text/xml"))); + }); + } + } + }); + }); +} + +})(); diff --git a/src/load/file.js b/src/load/file.js new file mode 100644 index 00000000..91c79d33 --- /dev/null +++ b/src/load/file.js @@ -0,0 +1,63 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.File) return; + +load.File = { + load_data, load_data, + load: load_file +}; + +function load_data(data, file, ext) { + ext = ext || name.toLowerCase().split('.').pop(); + return new Promise((resolve, reject) => { + switch (ext) { + case "stl": + resolve([{ + mesh: new load.STL().parse(data), file + }]); + break; + case "obj": + resolve(load.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + break; + case "3mf": + load.TMF.parseAsync(data).then((meshes) => { + resolve(meshes.map(m => { + return { mesh: m.faces.toFloat32(), file } + })); + }); + break; + case "svg": + resolve(load.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + break; + default: + reject(`unknown file type: "${ext}" from ${url}`); + break; + } + }); +} + +function load_file(file) { + return new Promise((resolve, reject) => { + let reader = new FileReader(); + let name = file.name; + let ext = name.toLowerCase().split('.').pop(); + reader.file = file; + reader.onloadend = function (event) { + load_data(event.target.result, name, ext) + .then(data => resolve(data)) + .catch(e => reject(e)); + }; + if (ext === 'stl') { + reader.readAsArrayBuffer(reader.file); + } else { + reader.readAsBinaryString(reader.file); + } + }); +} + +})(); diff --git a/src/load/obj.js b/src/load/obj.js new file mode 100644 index 00000000..583a3fe6 --- /dev/null +++ b/src/load/obj.js @@ -0,0 +1,64 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.OBJ) return; + +load.OBJ = { + parse, + parseAsync +}; + +/** + * @param {String} text + * @returns {Array} vertex face array + */ + +function parseAsync(text) { + return new Promise((resolve,reject) => { + resolve(parse(text)); + }); +} + +function parse(text) { + + let lines = text.split('\n').map(l => l.trim()); + let verts = []; + let faces = []; + let objs = [ faces ]; + + for (let line of lines) { + let toks = line.split(' '); + switch (toks.shift()) { + case 'v': + let v = toks.map(v => parseFloat(v)).slice(0,3); + if (v.length < 3) { + console.log('??',toks,line); + } + verts.push(v); + break; + case 'f': + let tok = toks.map(f => parseInt(f.split('/')[0])); + // add support for negative indices (offset from last vertex array point) + faces.appendAll(verts[tok[0]-1]); + faces.appendAll(verts[tok[1]-1]); + faces.appendAll(verts[tok[2]-1]); + break; + case 'g': + if (faces.length) { + objs.push(faces = []); + } + if (toks[0]) { + faces.name = toks[0]; + } + break; + } + } + + return objs; +} + +})(); diff --git a/src/load/stl.js b/src/load/stl.js new file mode 100644 index 00000000..f250a83f --- /dev/null +++ b/src/load/stl.js @@ -0,0 +1,271 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.STL) return; + +const SP = STL.prototype; +const CDH = 'Content-Disposition'; + +load.STL = STL; + +/** + * @constructor + */ +function STL() { + this.vertices = null; + this.normals = null; + this.colors = null; +} + + +SP.load = function(url, callback, formdata, scale) { + let stl = this, + xhr = new XMLHttpRequest(); + + function onloaded (event) { + if (event.target.status === 200 || event.target.status === 0) { + stl.parse(event.target.response || event.target.responseText, scale); + let cd = undefined; + if (xhr.getAllResponseHeaders().indexOf(CDH) > 0) { + cd = xhr.getResponseHeader(CDH) + .split(';').map(v => v.trim()).filter(v => { + return v.indexOf('filename=') === 0; + }).map(v => { + return v.substring(10,v.length-1); + })[0]; + } + if (callback) callback(stl.vertices, cd); + } else { + if (callback) callback(null, event.target.statusText); + } + } + + xhr.addEventListener('load', onloaded, false); + xhr.addEventListener('progress', function (event) { }, false); + xhr.addEventListener('error', function () { }, false); + + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.open(formdata ? 'POST' : 'GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.send(formdata); +}; + +SP.encode = function(vertices, normals) { + if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; + + let vc = vertices.length / 3, + bs = (vc * 16) + (vc * (2/3)) + 84, + bin = new ArrayBuffer(bs), + writer = new DataView(bin), + i = 0, + j = 0, + pos = 80; + + function writeInt16(val) { + writer.setUint16(pos, val, true); + pos += 2; + } + + function writeInt32(val) { + writer.setUint32(pos, val, true); + pos += 4; + } + + function writeFloat(val) { + writer.setFloat32(pos, val, true); + pos += 4; + } + + function writeVertex() { + writeFloat(vertices[i++]); // x + writeFloat(vertices[i++]); // y + writeFloat(vertices[i++]); // z + } + + writeInt32(vc / 3); + while (i < vertices.length) { + writeFloat(normals ? normals[j++] : 0); // norm x + writeFloat(normals ? normals[j++] : 0); // norm y + writeFloat(normals ? normals[j++] : 0); // norm z + writeVertex(); // p1 + writeVertex(); // p2 + writeVertex(); // p3 + writeInt16(0); // attributes + } + + return bin; +}; + +/** + * @param {ArrayBuffer} data + * @param {number} scale + * @returns vertex array + */ +SP.parse = function(data, scale) { + let binData = this.convertToBinary(data); + + let isBinary = function () { + let 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); + return expect === reader.byteLength; + }; + + return isBinary() + ? this.parseBinary(binData, scale) + : this.parseASCII(this.convertToString(data), scale); +}; + +SP.parseAsync = function(data, scale) { + return new Promise((resolve, reject) => { + resolve(this.parse(data, scale)); + }); +}; + +SP.parseBinary = function(data, scale = 1) { + let reader = new DataView(data), + faces = reader.getUint32 (80, true), + r, g, b, hasColors = false, colors, + defaultR, defaultG, defaultB, alpha; + + // check for default color in STL header ("COLOR=rgba" sequence). + for (let 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 = new Float32Array(faces * 3 * 3); + defaultR = reader.getUint8(index + 6) / 255; + defaultG = reader.getUint8(index + 7) / 255; + defaultB = reader.getUint8(index + 8) / 255; + alpha = reader.getUint8(index + 9) / 255; + } + } + + let offset = 0, + dataOffset = 84, + faceLength = 12 * 4 + 2, + vertices = new Float32Array(faces * 3 * 3), + normals = new Float32Array(faces * 3 * 3); + + colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; + + for (let face = 0; face < faces; face ++) { + + let start = dataOffset + face * faceLength, + normalX = reader.getFloat32(start, true), + normalY = reader.getFloat32(start + 4, true), + normalZ = reader.getFloat32(start + 8, true); + + if (hasColors) { + let 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; + } + } + + let i = 1, vertexstart; + + while (i <= 3) { + vertexstart = start + (i++) * 12; + vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; + vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; + vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; + normals[offset ] = normalX; + normals[offset + 1] = normalY; + normals[offset + 2] = normalZ; + if (hasColors) { + colors[offset ] = r; + colors[offset + 1] = g; + colors[offset + 2] = b; + } + offset += 3; + } + } + + this.vertices = vertices; + this.normals = normals; + this.colors = colors; + + return vertices; +}; + +SP.parseASCII = function(data, scale = 1) { + let result, + resultText, + patternNormal, + patternVertex, + vertices = [], + normals = [], + patternFace = /facet([\s\S]*?)endfacet/g; + + while ((result = patternFace.exec(data)) !== null) { + resultText = 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; + 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 = patternNormal.exec(resultText)) !== null) { + normals.push(parseFloat(result[1])); + normals.push(parseFloat(result[3])); + normals.push(parseFloat(result[5])); + } + while ((result = patternVertex.exec(resultText)) !== null) { + vertices.push(parseFloat(result[1]) * scale); + vertices.push(parseFloat(result[3]) * scale); + vertices.push(parseFloat(result[5]) * scale); + } + } + + let vToFloat32 = new Float32Array(vertices.length), + nToFloat32 = new Float32Array(normals.length), + i; + + for (i=0; i -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.SVG) return; + +load.SVG = { + parse, + parseAsync +}; + +/** + * @param {String} text + * @returns {Array} vertex face array + */ + +function parseAsync(text) { + return new Promise((resolve,reject) => { + resolve(parse(text)); + }); +} + +function parse(text) { + + let faces = [ ]; + let objs = [ faces ]; + + let data = new THREE.SVGLoader().parse(text); + let paths = data.paths; + + for (let i = 0; i < paths.length; i++) { + let path = paths[i]; + let shapes = path.toShapes(true); + let geom = new THREE.ExtrudeGeometry(shapes, { + steps: 1, + depth: 5, + bevelEnabled: false + }); + let array = geom.attributes.position.array; + // invert y + for (let i=0; i -- All Rights Reserved */ + +'use strict'; + +(function() { + +let load = self.load = self.load || {}; +if (load.URL) return; + +load.URL = { + load: load_url +}; + +const CDH = 'Content-Disposition'; + +function load_url(url, options = {}) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop(); + let ext = file.split('.').pop().toLowerCase(); + let deftype = ext === "obj" || ext === 'svg' ? "text" : "arraybuffer"; + let datatype = options.datatype || deftype; + let formdata = options.formdata; + + function onloaded(event) { + if (event.target.status === 200 || event.target.status === 0) { + if (xhr.getAllResponseHeaders().indexOf(CDH) > 0) { + // attempt to extract filename from content disposition + let fname = xhr.getResponseHeader(CDH) + .split(';').map(v => v.trim()).filter(v => { + return v.indexOf('filename=') === 0; + }).map(v => { + return v.substring(10,v.length-1); + })[0]; + if (fname) { + file = fname; + ext = file.split('.').pop().toLowerCase(); + } + } + let data = event.target.response || event.target.responseText; + // return raw data if indicated + if (options.parse === false) { + return resolve(data, ext); + } + switch (ext) { + case "stl": + resolve([{ + mesh: new load.STL().parse(data), file + }]); + break; + case "obj": + resolve(load.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + break; + case "3mf": + load.TMF.parseAsync(data).then((meshes) => { + resolve(meshes.map(m => { + return { mesh: m.faces.toFloat32(), file } + })); + }); + break; + case "svg": + resolve(load.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); + break; + default: + reject(`unknown file type: "${ext}" from ${url}`); + break; + } + } else { + reject(event.target.statusText); + } + } + + xhr.addEventListener('load', onloaded, false); + xhr.addEventListener('progress', function (event) { }, false); + xhr.addEventListener('error', function () { }, false); + + if (xhr.overrideMimeType) { + xhr.overrideMimeType('text/plain; charset=x-user-defined'); + } + + xhr.open(formdata ? 'POST' : 'GET', url, true); + xhr.responseType = datatype; + xhr.send(formdata); + }); +}; + +})(); \ No newline at end of file diff --git a/src/main/mesh.js b/src/main/mesh.js index 26902e6b..12800eb3 100644 --- a/src/main/mesh.js +++ b/src/main/mesh.js @@ -1,5 +1,6 @@ -// required for license and other grid app dependencies -self.gapp = self.gapp || {}; +/** Copyright Stewart Allen -- All Rights Reserved */ + +"use strict"; function $(id) { return document.getElementById(id); @@ -32,12 +33,13 @@ function init() { space.sky.showGrid(sky); space.sky.setColor(dark ? 0 : 0xffffff); space.init($('container'), delta => { }, ortho); - platform.onMove(delta => { } ); + platform.onMove(delta => { }); platform.setRound(false); platform.setZOff(0.2); platform.setSize(300,300,2.5,300); platform.setGrid(25,5,0x999999,0xcccccc); platform.setColor(0xcccccc); + platform.showVolume(false); platform.opacity(0.3); space.view.setZoom(zoomrev, zoomspd); space.useDefaultKeys(true); diff --git a/src/moto/client.js b/src/moto/client.js index ccb1dfbe..c2c49253 100644 --- a/src/moto/client.js +++ b/src/moto/client.js @@ -4,7 +4,8 @@ (function() { -let moto = self.moto = self.moto || {}, +let gapp = self.gapp = self.gapp || {}, + moto = self.moto = self.moto || {}, time = function() { return new Date().getTime() }, restarting = false, running = {}, diff --git a/src/add/license.js b/src/moto/license.js similarity index 100% rename from src/add/license.js rename to src/moto/license.js diff --git a/src/moto/load-3mf.js b/src/moto/load-3mf.js deleted file mode 100644 index c96b6e61..00000000 --- a/src/moto/load-3mf.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict'; - -(function() { - - let moto = self.moto = self.moto || {}; - if (moto.TMF) return; - - moto.TMF = { - parseAsync - }; - - function query(node, path, fn) { - let collect = false; - let match = path[0]; - if (match[0] === '+') { - match = match.slice(1); - collect = true; - } - for (let child of [...node.childNodes]) { - if (child.tagName === match) { - if (collect) { - fn(match, child); - } - if (path.length > 1) { - query(child, path.slice(1), fn); - } - } - } - } - - function loadModel(doc) { - let models = []; - function emitModel(model, faces) { - if (faces && faces.length) { - models.push({name: model, faces}); - } - } - - return new Promise((resolve, reject) => { - let model; - let faces; - let units; - let scale = 1; - let scaleMap = { - "inch": (1 / 25.4), - "foot": (1 / 304.8), - "micron": (1 / 1000), - "meter": 1000, - "millimeter": 1, - "centimeter": (1 / 10) - }; - - query(doc, ["+model","resources","+object","+mesh"], (type, node) => { - switch (type) { - case "model": - units = node.getAttribute("unit"); - if (units) { - scale = scaleMap[units] || 1; - } - break; - case "object": - emitModel(model, faces); - faces = []; - model = node.getAttribute("name") || undefined; - break; - case "mesh": - let vertices = []; - query(node, ["vertices","+vertex"], (type, vertex) => { - vertices.push([ - parseFloat(vertex.getAttribute("x")) * scale, - parseFloat(vertex.getAttribute("y")) * scale, - parseFloat(vertex.getAttribute("z")) * scale - ]); - }); - query(node, ["triangles","+triangle"], (type, triangle) => { - let v1 = parseInt(triangle.getAttribute("v1")); - let v2 = parseInt(triangle.getAttribute("v2")); - let v3 = parseInt(triangle.getAttribute("v3")); - faces.appendAll(vertices[v1]); - faces.appendAll(vertices[v2]); - faces.appendAll(vertices[v3]); - }); - break; - } - }); - - emitModel(model, faces); - resolve(models); - }); - } - - /** - * @param {Object} data binary file - * @returns {Array} vertex face array - */ - function parseAsync(data) { - return new Promise((resolve, reject) => { - JSZip.loadAsync(data).then(zip => { - for (let [key,value] of Object.entries(zip.files)) { - if (key.indexOf(".model") > 0) { - value.async("string").then(xml => { - resolve(loadModel(new DOMParser().parseFromString(xml, "text/xml"))); - }); - } - } - }); - }); - } - -})(); diff --git a/src/moto/load-file.js b/src/moto/load-file.js deleted file mode 100644 index c33bf51b..00000000 --- a/src/moto/load-file.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -(function() { - - let moto = self.moto = self.moto || {}; - if (moto.File) return; - - moto.File = { - load_data, - load - }; - - function load_data(data, file, ext) { - ext = ext || name.toLowerCase().split('.').pop(); - return new Promise((resolve, reject) => { - switch (ext) { - case "stl": - resolve([{ - mesh: new moto.STL().parse(data), file - }]); - break; - case "obj": - resolve(moto.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); - break; - case "3mf": - moto.TMF.parseAsync(data).then((meshes) => { - resolve(meshes.map(m => { - return { mesh: m.faces.toFloat32(), file } - })); - }); - break; - case "svg": - resolve(moto.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); - break; - default: - reject(`unknown file type: "${ext}" from ${url}`); - break; - } - }); - } - - function load(file) { - return new Promise((resolve, reject) => { - let reader = new FileReader(); - let name = file.name; - let ext = name.toLowerCase().split('.').pop(); - reader.file = file; - reader.onloadend = function (event) { - load_data(event.target.result, name, ext) - .then(data => resolve(data)) - .catch(e => reject(e)); - }; - if (ext === 'stl') { - reader.readAsArrayBuffer(reader.file); - } else { - reader.readAsBinaryString(reader.file); - } - }); - } - -})(); diff --git a/src/moto/load-obj.js b/src/moto/load-obj.js deleted file mode 100644 index b70b99e7..00000000 --- a/src/moto/load-obj.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -(function() { - - let moto = self.moto = self.moto || {}; - if (moto.OBJ) return; - - moto.OBJ = { - parse, - parseAsync - }; - - /** - * @param {String} text - * @returns {Array} vertex face array - */ - - function parseAsync(text) { - return new Promise((resolve,reject) => { - resolve(parse(text)); - }); - } - - function parse(text) { - - let lines = text.split('\n').map(l => l.trim()); - let verts = []; - let faces = []; - let objs = [ faces ]; - - for (let line of lines) { - let toks = line.split(' '); - switch (toks.shift()) { - case 'v': - let v = toks.map(v => parseFloat(v)).slice(0,3); - if (v.length < 3) { - console.log('??',toks,line); - } - verts.push(v); - break; - case 'f': - let tok = toks.map(f => parseInt(f.split('/')[0])); - // add support for negative indices (offset from last vertex array point) - faces.appendAll(verts[tok[0]-1]); - faces.appendAll(verts[tok[1]-1]); - faces.appendAll(verts[tok[2]-1]); - break; - case 'g': - if (faces.length) { - objs.push(faces = []); - } - if (toks[0]) { - faces.name = toks[0]; - } - break; - } - } - - return objs; - } - -})(); diff --git a/src/moto/load-stl.js b/src/moto/load-stl.js deleted file mode 100644 index 3f259952..00000000 --- a/src/moto/load-stl.js +++ /dev/null @@ -1,275 +0,0 @@ -/** - * adapted from THREE.STL-LOADER example - * - * https://en.wikipedia.org/wiki/STL_(file_format) - */ - -'use strict'; - -(function() { - - let moto = self.moto = self.moto || {}; - if (moto.STL) return; - - const SP = STL.prototype; - const CDH = 'Content-Disposition'; - - moto.STL = STL; - - /** - * @constructor - */ - function STL() { - this.vertices = null; - this.normals = null; - this.colors = null; - } - - - SP.load = function(url, callback, formdata, scale) { - let stl = this, - xhr = new XMLHttpRequest(); - - function onloaded (event) { - if (event.target.status === 200 || event.target.status === 0) { - stl.parse(event.target.response || event.target.responseText, scale); - let cd = undefined; - if (xhr.getAllResponseHeaders().indexOf(CDH) > 0) { - cd = xhr.getResponseHeader(CDH) - .split(';').map(v => v.trim()).filter(v => { - return v.indexOf('filename=') === 0; - }).map(v => { - return v.substring(10,v.length-1); - })[0]; - } - if (callback) callback(stl.vertices, cd); - } else { - if (callback) callback(null, event.target.statusText); - } - } - - xhr.addEventListener('load', onloaded, false); - xhr.addEventListener('progress', function (event) { }, false); - xhr.addEventListener('error', function () { }, false); - - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - } - - xhr.open(formdata ? 'POST' : 'GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.send(formdata); - }; - - SP.encode = function(vertices, normals) { - if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices"; - - let vc = vertices.length / 3, - bs = (vc * 16) + (vc * (2/3)) + 84, - bin = new ArrayBuffer(bs), - writer = new DataView(bin), - i = 0, - j = 0, - pos = 80; - - function writeInt16(val) { - writer.setUint16(pos, val, true); - pos += 2; - } - - function writeInt32(val) { - writer.setUint32(pos, val, true); - pos += 4; - } - - function writeFloat(val) { - writer.setFloat32(pos, val, true); - pos += 4; - } - - function writeVertex() { - writeFloat(vertices[i++]); // x - writeFloat(vertices[i++]); // y - writeFloat(vertices[i++]); // z - } - - writeInt32(vc / 3); - while (i < vertices.length) { - writeFloat(normals ? normals[j++] : 0); // norm x - writeFloat(normals ? normals[j++] : 0); // norm y - writeFloat(normals ? normals[j++] : 0); // norm z - writeVertex(); // p1 - writeVertex(); // p2 - writeVertex(); // p3 - writeInt16(0); // attributes - } - - return bin; - }; - - /** - * @param {ArrayBuffer} data - * @param {number} scale - * @returns vertex array - */ - SP.parse = function(data, scale) { - let binData = this.convertToBinary(data); - - let isBinary = function () { - let 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); - return expect === reader.byteLength; - }; - - return isBinary() - ? this.parseBinary(binData, scale) - : this.parseASCII(this.convertToString(data), scale); - }; - - SP.parseAsync = function(data, scale) { - return new Promise((resolve, reject) => { - resolve(this.parse(data, scale)); - }); - }; - - SP.parseBinary = function(data, scale = 1) { - let reader = new DataView(data), - faces = reader.getUint32 (80, true), - r, g, b, hasColors = false, colors, - defaultR, defaultG, defaultB, alpha; - - // check for default color in STL header ("COLOR=rgba" sequence). - for (let 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 = new Float32Array(faces * 3 * 3); - defaultR = reader.getUint8(index + 6) / 255; - defaultG = reader.getUint8(index + 7) / 255; - defaultB = reader.getUint8(index + 8) / 255; - alpha = reader.getUint8(index + 9) / 255; - } - } - - let offset = 0, - dataOffset = 84, - faceLength = 12 * 4 + 2, - vertices = new Float32Array(faces * 3 * 3), - normals = new Float32Array(faces * 3 * 3); - - colors = hasColors ? new Uint16Array(faces * 3 * 3) : null; - - for (let face = 0; face < faces; face ++) { - - let start = dataOffset + face * faceLength, - normalX = reader.getFloat32(start, true), - normalY = reader.getFloat32(start + 4, true), - normalZ = reader.getFloat32(start + 8, true); - - if (hasColors) { - let 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; - } - } - - let i = 1, vertexstart; - - while (i <= 3) { - vertexstart = start + (i++) * 12; - vertices[offset ] = reader.getFloat32 (vertexstart, true) * scale; - vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true) * scale; - vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true) * scale; - normals[offset ] = normalX; - normals[offset + 1] = normalY; - normals[offset + 2] = normalZ; - if (hasColors) { - colors[offset ] = r; - colors[offset + 1] = g; - colors[offset + 2] = b; - } - offset += 3; - } - } - - this.vertices = vertices; - this.normals = normals; - this.colors = colors; - - return vertices; - }; - - SP.parseASCII = function(data, scale = 1) { - let result, - resultText, - patternNormal, - patternVertex, - vertices = [], - normals = [], - patternFace = /facet([\s\S]*?)endfacet/g; - - while ((result = patternFace.exec(data)) !== null) { - resultText = 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; - 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 = patternNormal.exec(resultText)) !== null) { - normals.push(parseFloat(result[1])); - normals.push(parseFloat(result[3])); - normals.push(parseFloat(result[5])); - } - while ((result = patternVertex.exec(resultText)) !== null) { - vertices.push(parseFloat(result[1]) * scale); - vertices.push(parseFloat(result[3]) * scale); - vertices.push(parseFloat(result[5]) * scale); - } - } - - let vToFloat32 = new Float32Array(vertices.length), - nToFloat32 = new Float32Array(normals.length), - i; - - for (i=0; i { - resolve(parse(text)); - }); - } - - function parse(text) { - - let faces = [ ]; - let objs = [ faces ]; - - let data = new THREE.SVGLoader().parse(text); - let paths = data.paths; - - for (let i = 0; i < paths.length; i++) { - let path = paths[i]; - let shapes = path.toShapes(true); - let geom = new THREE.ExtrudeGeometry(shapes, { - steps: 1, - depth: 5, - bevelEnabled: false - }); - let array = geom.attributes.position.array; - // invert y - for (let i=0; i { - let xhr = new XMLHttpRequest(); - let file = options.file || options.filename || (((url.split('?')[0]).split('#')[0]).split('/')).pop(); - let ext = file.split('.').pop().toLowerCase(); - let deftype = ext === "obj" || ext === 'svg' ? "text" : "arraybuffer"; - let datatype = options.datatype || deftype; - let formdata = options.formdata; - - function onloaded(event) { - if (event.target.status === 200 || event.target.status === 0) { - if (xhr.getAllResponseHeaders().indexOf(CDH) > 0) { - // attempt to extract filename from content disposition - let fname = xhr.getResponseHeader(CDH) - .split(';').map(v => v.trim()).filter(v => { - return v.indexOf('filename=') === 0; - }).map(v => { - return v.substring(10,v.length-1); - })[0]; - if (fname) { - file = fname; - ext = file.split('.').pop().toLowerCase(); - } - } - let data = event.target.response || event.target.responseText; - // return raw data if indicated - if (options.parse === false) { - return resolve(data, ext); - } - switch (ext) { - case "stl": - resolve([{ - mesh: new moto.STL().parse(data), file - }]); - break; - case "obj": - resolve(moto.OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); - break; - case "3mf": - moto.TMF.parseAsync(data).then((meshes) => { - resolve(meshes.map(m => { - return { mesh: m.faces.toFloat32(), file } - })); - }); - break; - case "svg": - resolve(moto.SVG.parse(data).map(m => { return {mesh: m.toFloat32(), file} })); - break; - default: - reject(`unknown file type: "${ext}" from ${url}`); - break; - } - } else { - reject(event.target.statusText); - } - } - - xhr.addEventListener('load', onloaded, false); - xhr.addEventListener('progress', function (event) { }, false); - xhr.addEventListener('error', function () { }, false); - - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - } - - xhr.open(formdata ? 'POST' : 'GET', url, true); - xhr.responseType = datatype; - xhr.send(formdata); - }); - }; - -})(); \ No newline at end of file diff --git a/src/moto/space.js b/src/moto/space.js index 383d8abf..da1314b8 100644 --- a/src/moto/space.js +++ b/src/moto/space.js @@ -340,9 +340,9 @@ function setRulers( xon = ruler.xon, yon = ruler.yon, - factor = ruler.factor, - xl = ruler.xlabel, - yl = ruler.ylabel) + factor = ruler.factor || 1, + xl = ruler.xlabel || 'X', + yl = ruler.ylabel || 'Y') { if (xon !== ruler.xon || yon !== ruler.yon || factor !== ruler.factor || xl !== ruler.xlabel || yl !== ruler.ylabel) { ruler.factor = factor; diff --git a/src/moto/worker.js b/src/moto/worker.js index 81b3c86f..28e6b4a1 100644 --- a/src/moto/worker.js +++ b/src/moto/worker.js @@ -4,7 +4,8 @@ (function() { -let moto = self.moto = self.moto || {}, +let gapp = self.gapp = self.gapp || {}, + moto = self.moto = self.moto || {}, time = Date.now; if (moto.worker) return;