mesh v2 at /v2/mesh
This commit is contained in:
parent
aee9fd6ed7
commit
6566c45f9a
85 changed files with 138198 additions and 1 deletions
6
app.js
6
app.js
|
|
@ -330,6 +330,8 @@ function init(mod) {
|
|||
mod.static("/meta/", "web/meta");
|
||||
mod.static("/kiri/", "web/kiri");
|
||||
|
||||
mod.static("/v2/", "web2");
|
||||
|
||||
function load_modules(root, force) {
|
||||
// load modules
|
||||
lastmod(`${dir}/${root}`) && fs.readdirSync(`${dir}/${root}`).forEach(mdir => {
|
||||
|
|
@ -890,7 +892,9 @@ function fixedmap(prefix, map) {
|
|||
|
||||
// HTTP 307 redirect
|
||||
function redir(path, type) {
|
||||
return (req, res, next) => http.redirect(res, path, type);
|
||||
return (req, res, next) => {
|
||||
http.redirect(res, path, type);
|
||||
}
|
||||
}
|
||||
|
||||
// mangle request path
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@
|
|||
"prebuild": "node bin/electron-pre.js",
|
||||
"postbuild": "node bin/electron-post.js",
|
||||
"preinstall": "node bin/install-pre.js && npx webpack --config bin/webpack-three.js",
|
||||
"preinstall2": "npx webpack --config src2/webpack/webpack-three-esm.js",
|
||||
"docs-dev": "docusaurus start",
|
||||
"docs-build": "docusaurus build",
|
||||
"docs-serve": "docusaurus serve"
|
||||
|
|
|
|||
262
src2/add/array.js
Normal file
262
src2/add/array.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
// extend Array, String, Number, Math
|
||||
const AP = {};
|
||||
|
||||
if (!AP.flat) {
|
||||
AP.flat = function() {
|
||||
try {
|
||||
return [].concat.apply([], this);
|
||||
} catch (e) {
|
||||
// console.log({flat_error: e});
|
||||
let out = [];
|
||||
for (let e of this.slice()) {
|
||||
out.push(...e);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AP.findWith = function(fn) {
|
||||
for (let i=0, l=this.length; i<l; i++) {
|
||||
if (fn(this[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
AP.equals = function(arr) {
|
||||
if (!arr) return false;
|
||||
if (arr.length !== this.length) return false;
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (arr[i] !== this[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
AP.peek = function(val = 1) {
|
||||
return this[this.length - val];
|
||||
};
|
||||
|
||||
AP.drop = function(pre = 0, post = 0) {
|
||||
while (pre-- > 0) this.shift();
|
||||
while (post-- > 0) this.pop();
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* allow chaining of push() calls
|
||||
*/
|
||||
AP.append = function(v, flat) {
|
||||
if (flat) {
|
||||
return this.appendAll(v);
|
||||
} else {
|
||||
this.push(v);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* append all array elements to this array
|
||||
*/
|
||||
AP.appendAll = function(arr) {
|
||||
if (arr && arr.length > 0) {
|
||||
// avoid hitting stack limits
|
||||
if (arr.length > 10000) {
|
||||
for (let i = 0, il = arr.length; i < il; i++) {
|
||||
this.push(arr[i]);
|
||||
}
|
||||
} else {
|
||||
// this.push.apply(this,arr); (slower?)
|
||||
this.push(...arr);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
AP.addOnce = function(val) {
|
||||
if (!this.contains(val)) {
|
||||
this.push(val);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* shallow cloning with clone(arg) call on each new element
|
||||
*/
|
||||
AP.clone = function() {
|
||||
return this.map(v => v.clone(...arguments));
|
||||
};
|
||||
|
||||
AP.xray = function(arg) {
|
||||
const na = [],
|
||||
ln = this.length;
|
||||
for (let i = 0; i < ln; i++) {
|
||||
na.push(this[i].xray(arg));
|
||||
}
|
||||
return na;
|
||||
};
|
||||
|
||||
AP.xout = function(label, inset) {
|
||||
if (label) console.log(`${label} [${this.length}]`);
|
||||
if (!inset) inset = ' ';
|
||||
this.forEach((el, i) => {
|
||||
if (el.xout) {
|
||||
el.xout(null, inset + i + inset);
|
||||
} else if (el.xray) {
|
||||
const info = el.xray();
|
||||
console.log(inset, info);
|
||||
Object.values(info).forEach(val => {
|
||||
if (val.xout) {
|
||||
val.xout(null, inset + inset);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* remove and return element from array, if present
|
||||
*/
|
||||
AP.remove = function(val) {
|
||||
let idx = this.indexOf(val);
|
||||
if (idx >= 0) return this.splice(idx, 1);
|
||||
return null;
|
||||
};
|
||||
|
||||
AP.removeAll = function(array) {
|
||||
for (let i = 0, il = array.length; i < il; i++) {
|
||||
this.remove(array[i]);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* return last array element if array length > 0
|
||||
*/
|
||||
AP.last = function() {
|
||||
if (this.length === 0) return null;
|
||||
return this[this.length - 1];
|
||||
};
|
||||
|
||||
AP.contains = function(val) {
|
||||
return this.indexOf(val) >= 0;
|
||||
};
|
||||
|
||||
AP.toFloat32 = function() {
|
||||
return new Float32Array(this);
|
||||
};
|
||||
|
||||
AP.toUint32 = function() {
|
||||
return Uint32Array.from(this);
|
||||
};
|
||||
|
||||
AP.forEachPair = function(fn, incr) {
|
||||
let scope = this,
|
||||
idx = 0,
|
||||
inc = incr || 2,
|
||||
len = scope.length;
|
||||
while (idx < len) {
|
||||
fn(scope[idx], scope[(idx + 1) % len], idx);
|
||||
idx += inc;
|
||||
}
|
||||
};
|
||||
|
||||
AP.show = function(fn, f) {
|
||||
f = f || function(v) {
|
||||
console.log(v)
|
||||
};
|
||||
this.forEach(function(av) {
|
||||
f(av[fn]());
|
||||
})
|
||||
};
|
||||
|
||||
AP.uniq = function() {
|
||||
return this.slice().sort().filter((x, i, a) => !i || x != a[i - 1]);
|
||||
};
|
||||
|
||||
AP.hasNaN = function() {
|
||||
return this.filter(v => isNaN(v)).length > 0;
|
||||
};
|
||||
|
||||
// turn array into arrays of specified size
|
||||
AP.group = function(size) {
|
||||
let na = new Array(Math.ceil(this.length / size));
|
||||
let nai = 0;
|
||||
for (let i = 0; i < this.length; i += size) {
|
||||
let grp = new Array(size);
|
||||
for (let j = 0; j < size; j++) {
|
||||
grp[j] = this[i + j];
|
||||
}
|
||||
na[nai++] = grp;
|
||||
}
|
||||
return na;
|
||||
};
|
||||
|
||||
// wrap a function and unroll the first parameter into
|
||||
// successive function calls if it is an array
|
||||
Array.handle = function(fn) {
|
||||
return function() {
|
||||
let args = [...arguments];
|
||||
let val = args.shift();
|
||||
if (Array.isArray(val)) {
|
||||
let rv = [];
|
||||
for (let v of val.slice()) {
|
||||
rv.push(fn(v, ...args));
|
||||
}
|
||||
return rv;
|
||||
} else {
|
||||
return fn(val, ...args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply all methods to Array.prototype
|
||||
Object.assign(Array.prototype, AP);
|
||||
|
||||
// Export for ES modules
|
||||
export { AP as arrayPrototype };
|
||||
|
||||
Float32Array.prototype.toShared = function() {
|
||||
const newvert = new Float32Array(new SharedArrayBuffer(this.buffer.byteLength));
|
||||
newvert.set(this);
|
||||
return newvert;
|
||||
};
|
||||
|
||||
Float32Array.prototype.toFloat32 = function() { return this };
|
||||
|
||||
ArrayBuffer.prototype.textDecode = function(encoding = 'utf-8') {
|
||||
return new TextDecoder(encoding).decode(this);
|
||||
};
|
||||
|
||||
String.prototype.reverse = function() {
|
||||
return this.split('').reverse().join('');
|
||||
};
|
||||
|
||||
Object.clone = function(o) {
|
||||
// not using structuredClone because failes with some objects
|
||||
// this method "cleans" out non clonables
|
||||
return o ? JSON.parse(JSON.stringify(o)) : o;
|
||||
};
|
||||
|
||||
Object.toArray = function(o) {
|
||||
let ret = [];
|
||||
for (let [key, value] of Object.entries(o)) {
|
||||
ret.push({
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
Math.bound = function(val, min, max) {
|
||||
return Math.max(min, Math.min(max, val));
|
||||
};
|
||||
|
||||
Number.prototype.round = function(digits) {
|
||||
if (digits === 0) return this.valueOf() | 0;
|
||||
const pow = Math.pow(10, digits || 3);
|
||||
return Math.round(this.valueOf() * pow) / pow;
|
||||
};
|
||||
178
src2/add/class.js
Normal file
178
src2/add/class.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
// use: add.array
|
||||
import { arrayPrototype } from './array.js';
|
||||
|
||||
// Make classes available globally
|
||||
class ArrayWriter {
|
||||
constructor() {
|
||||
this.pos = 0;
|
||||
this.array = [];
|
||||
}
|
||||
|
||||
seek(pos) {
|
||||
let last = this.pos;
|
||||
this.pos = pos;
|
||||
return last;
|
||||
}
|
||||
|
||||
skip(len) {
|
||||
this.writeBytes(new Array(len).fill(0), len);
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
writeU8(v) {
|
||||
this.array[this.pos++] = v & 0xff;
|
||||
}
|
||||
|
||||
writeU16(v) {
|
||||
this.array[this.pos++] = (v >> 8) & 0xff;
|
||||
this.array[this.pos++] = v & 0xff;
|
||||
}
|
||||
|
||||
writeU32(v) {
|
||||
this.array[this.pos++] = (v >> 24) & 0xff;
|
||||
this.array[this.pos++] = (v >> 16) & 0xff;
|
||||
this.array[this.pos++] = (v >> 8) & 0xff;
|
||||
this.array[this.pos++] = v & 0xff;
|
||||
}
|
||||
|
||||
writeBytes(bytes) {
|
||||
this.array.appendAll(bytes);
|
||||
this.pos += bytes.length;
|
||||
}
|
||||
|
||||
toBuffer() {
|
||||
return new Uint8ClampedArray(this.array).buffer;
|
||||
}
|
||||
};
|
||||
|
||||
class DataWriter {
|
||||
constructor(view, pos) {
|
||||
this.view = view;
|
||||
this.pos = pos || 0;
|
||||
}
|
||||
|
||||
skip(v) {
|
||||
let p = this.pos;
|
||||
this.pos += v;
|
||||
return p;
|
||||
}
|
||||
|
||||
writeU8(v) {
|
||||
try {
|
||||
this.view.setUint8(this.pos, v);
|
||||
return this.skip(1);
|
||||
} catch (err) {
|
||||
console.log({pos:this.pos, err})
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
writeU16(v,le) {
|
||||
this.view.setUint16(this.pos, v, le);
|
||||
return this.skip(2);
|
||||
}
|
||||
|
||||
writeU32(v,le) {
|
||||
this.view.setUint32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeI32(v,le) {
|
||||
this.view.setInt32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeUB64(v,le) {
|
||||
this.view.setBigUint64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
|
||||
writeB64(v,le) {
|
||||
this.view.setBigInt64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
|
||||
writeF32(v,le) {
|
||||
this.view.setFloat32(this.pos, v, le);
|
||||
return this.skip(4);
|
||||
}
|
||||
|
||||
writeF64(v,le) {
|
||||
this.view.setFloat64(this.pos, v, le);
|
||||
return this.skip(8);
|
||||
}
|
||||
}
|
||||
|
||||
class DataReader {
|
||||
constructor(view, pos) {
|
||||
this.view = view;
|
||||
this.pos = pos || 0;
|
||||
}
|
||||
|
||||
skip(v) {
|
||||
let p = this.pos;
|
||||
this.pos += v;
|
||||
return p;
|
||||
}
|
||||
|
||||
readBytes(len) {
|
||||
let buf = this.view.buffer.slice(this.pos, this.pos + len);
|
||||
this.pos += len;
|
||||
return buf;
|
||||
}
|
||||
|
||||
readU8() {
|
||||
return this.view.getUint8(this.pos++);
|
||||
}
|
||||
|
||||
readU16(le) {
|
||||
let v = this.view.getUint16(this.pos, le);
|
||||
this.pos += 2;
|
||||
return v;
|
||||
}
|
||||
|
||||
readU32(le) {
|
||||
let v = this.view.getUint32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readI32(le) {
|
||||
let v = this.view.getInt32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readB64(le) {
|
||||
let v = this.view.getBigInt64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
readUB64(le) {
|
||||
let v = this.view.getBigUint64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
|
||||
readF32(le) {
|
||||
let v = this.view.getFloat32(this.pos, le);
|
||||
this.pos += 4;
|
||||
return v;
|
||||
}
|
||||
|
||||
readF64(le) {
|
||||
let v = this.view.getFloat64(this.pos, le);
|
||||
this.pos += 8;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
window.ArrayWriter = ArrayWriter;
|
||||
window.DataWriter = DataWriter;
|
||||
window.DataReader = DataReader;
|
||||
|
||||
// Export for ES modules
|
||||
export { ArrayWriter, DataWriter, DataReader };
|
||||
148
src2/add/three.js
Normal file
148
src2/add/three.js
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
// dep: ext.three
|
||||
import {
|
||||
THREE, SVGLoader, BufferGeometryUtils,
|
||||
LineMaterial, Line2, LineGeometry, LineSegments2,
|
||||
LineSegmentsGeometry, MeshBVHLib } from '../ext/three.js';
|
||||
|
||||
// Make THREE available globally
|
||||
window.THREE = THREE;
|
||||
THREE.BufferGeometryUtils = BufferGeometryUtils;
|
||||
THREE.SVGLoader = SVGLoader.SVGLoader;
|
||||
THREE.LineMaterial = LineMaterial;
|
||||
THREE.Line2 = Line2;
|
||||
THREE.LineGeometry = LineGeometry;
|
||||
THREE.LineSegments2 = LineSegments2;
|
||||
THREE.LineSegmentsGeometry = LineSegmentsGeometry;
|
||||
|
||||
let MP = THREE.Mesh.prototype,
|
||||
XP = THREE.Box3.prototype,
|
||||
BP = THREE.BufferGeometry.prototype;
|
||||
|
||||
THREE.computeFaceNormal = function(vA,vB,vC) {
|
||||
const ab = new THREE.Vector3();
|
||||
const cb = new THREE.Vector3();
|
||||
cb.subVectors( vC, vB );
|
||||
ab.subVectors( vA, vB );
|
||||
cb.cross( ab );
|
||||
cb.normalize();
|
||||
return cb;
|
||||
};
|
||||
|
||||
// https://www.npmjs.com/package/three-mesh-bvh
|
||||
THREE.addBVH = function() {
|
||||
const { computeBoundsTree, disposeBoundsTree, acceleratedRaycast } = MeshBVHLib;
|
||||
THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
|
||||
THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
|
||||
THREE.Mesh.prototype.raycast = acceleratedRaycast;
|
||||
};
|
||||
|
||||
THREE.dispose = function(obj) {
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
if (obj.geometry) {
|
||||
obj.geometry.dispose();
|
||||
}
|
||||
if (obj.material && obj.material.map && obj.material.map.dispose) {
|
||||
obj.material.map.dispose();
|
||||
}
|
||||
if (obj.children && obj.children.length) {
|
||||
for (let c of obj.children) {
|
||||
THREE.dispose(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// add .dim and .mid getters to Box3
|
||||
Object.defineProperties(XP, {
|
||||
dim: {
|
||||
get: function() {
|
||||
return this.max.clone().sub(this.min);
|
||||
}
|
||||
},
|
||||
mid: {
|
||||
get: function() {
|
||||
return this.dim.clone().multiplyScalar(0.5).add(this.min);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
MP.getBoundingBox = function(update) {
|
||||
return this.geometry.getBoundingBox(update);
|
||||
};
|
||||
|
||||
MP.mirrorX = function() {
|
||||
return this.mirror(0);
|
||||
};
|
||||
|
||||
MP.mirrorY = function() {
|
||||
return this.mirror(1);
|
||||
};
|
||||
|
||||
MP.mirrorZ = function() {
|
||||
return this.mirror(2);
|
||||
};
|
||||
|
||||
// fast mirror of vertices given axis array offset
|
||||
// array elements are repeating [x,y,z,x,y,z,...]
|
||||
MP.mirror = function(start) {
|
||||
let i,
|
||||
geo = this.geometry,
|
||||
at = geo.attributes,
|
||||
pa = at.position.array,
|
||||
nm = at.normal.array;
|
||||
for (i = start || 0 ; i < pa.length; i += 3) {
|
||||
pa[i] = -pa[i];
|
||||
nm[i] = -nm[i];
|
||||
}
|
||||
geo.computeVertexNormals();
|
||||
return this;
|
||||
};
|
||||
|
||||
// return cached or refreshed (when update = true) bounding box
|
||||
BP.getBoundingBox = function(update) {
|
||||
if (update || !this.boundingBox) {
|
||||
this.boundingBox = null;
|
||||
this.computeBoundingBox();
|
||||
}
|
||||
return this.boundingBox;
|
||||
};
|
||||
|
||||
BP.moveMesh = function(x = 0, y = 0, z = 0) {
|
||||
let gap = this.attributes.position,
|
||||
pa = gap.array;
|
||||
for (let i=0; i < pa.length; i += 3) {
|
||||
pa[i ] += x;
|
||||
pa[i + 1] += y;
|
||||
pa[i + 2] += z;
|
||||
}
|
||||
gap.needsUpdate = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
BP.shallowClone = function() {
|
||||
let geo = new THREE.BufferGeometry();
|
||||
for (let [k, v] of Object.entries(this.attributes)) {
|
||||
geo.setAttribute(k, new THREE.BufferAttribute(v.array, v.itemSize));
|
||||
}
|
||||
return geo;
|
||||
};
|
||||
|
||||
THREE.Object3D.prototype.newGroup = function() {
|
||||
let group = new THREE.Group();
|
||||
this.add(group);
|
||||
return group;
|
||||
};
|
||||
|
||||
THREE.Object3D.prototype.removeAll = function() {
|
||||
this.children.slice().forEach(function (c) {
|
||||
c.parent = undefined;
|
||||
c.dispatchEvent( { type: 'removed' } );
|
||||
});
|
||||
this.children = [];
|
||||
};
|
||||
|
||||
// Export for ES modules
|
||||
export { THREE };
|
||||
1
src2/ext/base64.js
Symbolic link
1
src2/ext/base64.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/base64-js/base64js.min.js
|
||||
6986
src2/ext/clip.js
Executable file
6986
src2/ext/clip.js
Executable file
File diff suppressed because it is too large
Load diff
7
src2/ext/clip2.esm.js
Normal file
7
src2/ext/clip2.esm.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
// Import the original Clipper library
|
||||
import './clip2.js';
|
||||
|
||||
// Re-export the ClipperLib object
|
||||
export const ClipperLib = self.ClipperLib;
|
||||
7629
src2/ext/clip2.js
Normal file
7629
src2/ext/clip2.js
Normal file
File diff suppressed because it is too large
Load diff
1
src2/ext/earcut.js
Symbolic link
1
src2/ext/earcut.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/earcut/dist/earcut.dev.js
|
||||
247
src2/ext/fsave.js
Normal file
247
src2/ext/fsave.js
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
/* FileSaver.js
|
||||
* A saveAs() FileSaver implementation.
|
||||
* 1.1.20151003
|
||||
*
|
||||
* By Eli Grey, http://eligrey.com
|
||||
* License: MIT
|
||||
* See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/*global self */
|
||||
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */
|
||||
|
||||
/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
|
||||
|
||||
"use strict";
|
||||
// IE <10 is explicitly unsupported
|
||||
if (typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = self.document;
|
||||
// only get URL when necessary in case Blob.js hasn't overridden it yet
|
||||
const get_URL = function() {
|
||||
return self.URL || self.webkitURL || self;
|
||||
};
|
||||
|
||||
const save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a");
|
||||
const can_use_save_link = "download" in save_link;
|
||||
|
||||
const click = function(node) {
|
||||
const event = new MouseEvent("click");
|
||||
node.dispatchEvent(event);
|
||||
};
|
||||
|
||||
const is_safari = /Version\/[\d\.]+.*Safari/.test(navigator.userAgent);
|
||||
const webkit_req_fs = self.webkitRequestFileSystem;
|
||||
const req_fs = self.requestFileSystem || webkit_req_fs || self.mozRequestFileSystem;
|
||||
|
||||
const throw_outside = function(ex) {
|
||||
(self.setImmediate || self.setTimeout)(function() {
|
||||
throw ex;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const force_saveable_type = "application/octet-stream";
|
||||
const fs_min_size = 0;
|
||||
|
||||
// See https://code.google.com/p/chromium/issues/detail?id=375297#c7 and
|
||||
// https://github.com/eligrey/FileSaver.js/commit/485930a#commitcomment-8768047
|
||||
// for the reasoning behind the timeout and revocation flow
|
||||
const arbitrary_revoke_timeout = 500; // in ms
|
||||
|
||||
const revoke = function(file) {
|
||||
const revoker = function() {
|
||||
if (typeof file === "string") { // file is an object URL
|
||||
get_URL().revokeObjectURL(file);
|
||||
} else { // file is a File
|
||||
file.remove();
|
||||
}
|
||||
};
|
||||
if (self.chrome) {
|
||||
revoker();
|
||||
} else {
|
||||
setTimeout(revoker, arbitrary_revoke_timeout);
|
||||
}
|
||||
};
|
||||
|
||||
const dispatch = function(filesaver, event_types, event) {
|
||||
event_types = [].concat(event_types);
|
||||
let i = event_types.length;
|
||||
while (i--) {
|
||||
const listener = filesaver["on" + event_types[i]];
|
||||
if (typeof listener === "function") {
|
||||
try {
|
||||
listener.call(filesaver, event || filesaver);
|
||||
} catch (ex) {
|
||||
throw_outside(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto_bom = function(blob) {
|
||||
// prepend BOM for UTF-8 XML and text/* types (including HTML)
|
||||
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) {
|
||||
return new Blob(["\ufeff", blob], {type: blob.type});
|
||||
}
|
||||
return blob;
|
||||
};
|
||||
|
||||
class FileSaver {
|
||||
constructor(blob, name, no_auto_bom) {
|
||||
if (!no_auto_bom) {
|
||||
blob = auto_bom(blob);
|
||||
}
|
||||
// First try a.download, then web filesystem, then object URLs
|
||||
const filesaver = this;
|
||||
const type = blob.type;
|
||||
let blob_changed = false;
|
||||
let object_url;
|
||||
let target_view;
|
||||
const dispatch_all = function() {
|
||||
dispatch(filesaver, "writestart progress write writeend".split(" "));
|
||||
};
|
||||
// on any filesys errors revert to saving with object URLs
|
||||
const fs_error = function() {
|
||||
if (target_view && is_safari && typeof FileReader !== "undefined") {
|
||||
// Safari doesn't allow downloading of blob urls
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function() {
|
||||
const base64Data = reader.result;
|
||||
target_view.location.href = "data:attachment/file" + base64Data.slice(base64Data.search(/[,;]/));
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
return;
|
||||
}
|
||||
// don't create more object URLs than needed
|
||||
if (blob_changed || !object_url) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
}
|
||||
if (target_view) {
|
||||
target_view.location.href = object_url;
|
||||
} else {
|
||||
const new_tab = self.open(object_url, "_blank");
|
||||
if (new_tab == undefined && is_safari) {
|
||||
//Apple do not allow window.open, see http://bit.ly/1kZffRI
|
||||
self.location.href = object_url;
|
||||
}
|
||||
}
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
};
|
||||
const abortable = function(func) {
|
||||
return function() {
|
||||
if (filesaver.readyState !== filesaver.DONE) {
|
||||
return func.apply(this, arguments);
|
||||
}
|
||||
};
|
||||
};
|
||||
const create_if_not_found = {create: true, exclusive: false};
|
||||
let slice;
|
||||
|
||||
filesaver.readyState = filesaver.INIT;
|
||||
|
||||
if (!name) {
|
||||
name = "download";
|
||||
}
|
||||
|
||||
if (can_use_save_link) {
|
||||
object_url = get_URL().createObjectURL(blob);
|
||||
setTimeout(function() {
|
||||
save_link.href = object_url;
|
||||
save_link.download = name;
|
||||
click(save_link);
|
||||
dispatch_all();
|
||||
revoke(object_url);
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Object and web filesystem URLs have a problem saving in Google Chrome when
|
||||
// viewed in a tab, so I force save with application/octet-stream
|
||||
// http://code.google.com/p/chromium/issues/detail?id=91158
|
||||
// Update: Google errantly closed 91158, I submitted it again:
|
||||
// https://code.google.com/p/chromium/issues/detail?id=389642
|
||||
if (self.chrome && type && type !== force_saveable_type) {
|
||||
slice = blob.slice || blob.webkitSlice;
|
||||
blob = slice.call(blob, 0, blob.size, force_saveable_type);
|
||||
blob_changed = true;
|
||||
}
|
||||
|
||||
// Since I can't be sure that the guessed media type will trigger a download
|
||||
// in WebKit, I append .download to the filename.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=65440
|
||||
if (webkit_req_fs && name !== "download") {
|
||||
name += ".download";
|
||||
}
|
||||
|
||||
if (type === force_saveable_type || webkit_req_fs) {
|
||||
target_view = self;
|
||||
}
|
||||
|
||||
if (!req_fs) {
|
||||
fs_error();
|
||||
return;
|
||||
}
|
||||
|
||||
fs_min_size += blob.size;
|
||||
req_fs(self.TEMPORARY, fs_min_size, abortable(function(fs) {
|
||||
fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
|
||||
const save = function() {
|
||||
dir.getFile(name, create_if_not_found, abortable(function(file) {
|
||||
file.createWriter(abortable(function(writer) {
|
||||
writer.onwriteend = function(event) {
|
||||
target_view.location.href = file.toURL();
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
dispatch(filesaver, "writeend", event);
|
||||
revoke(file);
|
||||
};
|
||||
writer.onerror = function() {
|
||||
const error = writer.error;
|
||||
if (error.code !== error.ABORT_ERR) {
|
||||
fs_error();
|
||||
}
|
||||
};
|
||||
"writestart progress write abort".split(" ").forEach(function(event) {
|
||||
writer["on" + event] = filesaver["on" + event];
|
||||
});
|
||||
writer.write(blob);
|
||||
filesaver.abort = function() {
|
||||
writer.abort();
|
||||
filesaver.readyState = filesaver.DONE;
|
||||
};
|
||||
filesaver.readyState = filesaver.WRITING;
|
||||
}), fs_error);
|
||||
}), fs_error);
|
||||
};
|
||||
dir.getFile(name, {create: false}, abortable(function(file) {
|
||||
// delete file if it already exists
|
||||
file.remove();
|
||||
save();
|
||||
}), abortable(function(ex) {
|
||||
if (ex.code === ex.NOT_FOUND_ERR) {
|
||||
save();
|
||||
} else {
|
||||
fs_error();
|
||||
}
|
||||
}));
|
||||
}), fs_error);
|
||||
}), fs_error);
|
||||
}
|
||||
|
||||
static get DONE() { return 2; }
|
||||
static get INIT() { return 0; }
|
||||
static get WRITING() { return 1; }
|
||||
}
|
||||
|
||||
export function saveAs(blob, name, no_auto_bom) {
|
||||
return new FileSaver(blob, name, no_auto_bom);
|
||||
}
|
||||
|
||||
export { FileSaver };
|
||||
1
src2/ext/gerber.js
Symbolic link
1
src2/ext/gerber.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/@tracespace/parser/umd/parser.js
|
||||
1
src2/ext/jszip.js
Symbolic link
1
src2/ext/jszip.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/jszip/dist/jszip.js
|
||||
1
src2/ext/manifold.js
Symbolic link
1
src2/ext/manifold.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/manifold-3d/manifold.js
|
||||
67
src2/ext/md5.js
Normal file
67
src2/ext/md5.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// SOURCE: https://stackoverflow.com/questions/1655769/fastest-md5-implementation-in-javascript
|
||||
|
||||
export function hash(e) {
|
||||
function h(a, b) {
|
||||
var c, d, e, f, g;
|
||||
e = a & 2147483648;
|
||||
f = b & 2147483648;
|
||||
c = a & 1073741824;
|
||||
d = b & 1073741824;
|
||||
g = (a & 1073741823) + (b & 1073741823);
|
||||
return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f
|
||||
}
|
||||
|
||||
function k(a, b, c, d, e, f, g) {
|
||||
a = h(a, h(h(b & c | ~b & d, e), g));
|
||||
return h(a << f | a >>> 32 - f, b)
|
||||
}
|
||||
|
||||
function l(a, b, c, d, e, f, g) {
|
||||
a = h(a, h(h(b & d | c & ~d, e), g));
|
||||
return h(a << f | a >>> 32 - f, b)
|
||||
}
|
||||
|
||||
function m(a, b, d, c, e, f, g) {
|
||||
a = h(a, h(h(b ^ d ^ c, e), g));
|
||||
return h(a << f | a >>> 32 - f, b)
|
||||
}
|
||||
|
||||
function n(a, b, d, c, e, f, g) {
|
||||
a = h(a, h(h(d ^ (b | ~c), e), g));
|
||||
return h(a << f | a >>> 32 - f, b)
|
||||
}
|
||||
|
||||
function p(a) {
|
||||
var b = "",
|
||||
d = "",
|
||||
c;
|
||||
for (c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = "0" + d.toString(16), b += d.substr(d.length - 2, 2);
|
||||
return b
|
||||
}
|
||||
var f = [],
|
||||
q, r, s, t, a, b, c, d;
|
||||
e = function(a) {
|
||||
a = a.replace(/\r\n/g, "\n");
|
||||
for (var b = "", d = 0; d < a.length; d++) {
|
||||
var c = a.charCodeAt(d);
|
||||
128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) : (b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)), b += String.fromCharCode(c & 63 | 128))
|
||||
}
|
||||
return b
|
||||
}(e);
|
||||
f = function(b) {
|
||||
var a, c = b.length;
|
||||
a = c + 8;
|
||||
for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;) a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++;
|
||||
a = (g - g % 4) / 4;
|
||||
e[a] |= 128 << g % 4 * 8;
|
||||
e[d - 2] = c << 3;
|
||||
e[d - 1] = c >>> 29;
|
||||
return e
|
||||
}(e);
|
||||
a = 1732584193;
|
||||
b = 4023233417;
|
||||
c = 2562383102;
|
||||
d = 271733878;
|
||||
for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d, a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
|
||||
return (p(a) + p(b) + p(c) + p(d)).toLowerCase()
|
||||
}
|
||||
7
src2/ext/pngjs.esm.js
Normal file
7
src2/ext/pngjs.esm.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
// Import the original PNG library
|
||||
import './pngjs.js';
|
||||
|
||||
// Re-export the PNG object
|
||||
export const PNG = self.png;
|
||||
16433
src2/ext/pngjs.js
Normal file
16433
src2/ext/pngjs.js
Normal file
File diff suppressed because it is too large
Load diff
80716
src2/ext/three.js
Normal file
80716
src2/ext/three.js
Normal file
File diff suppressed because one or more lines are too long
1
src2/ext/tween.js
Symbolic link
1
src2/ext/tween.js
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/@tweenjs/tween.js/src/Tween.js
|
||||
852
src2/geo/base.js
Normal file
852
src2/geo/base.js
Normal file
|
|
@ -0,0 +1,852 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { newPoint } from './point.js';
|
||||
|
||||
const round_decimal_precision = 5;
|
||||
|
||||
function time() {
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
function lerp(from, to, maxInc, incFrom) {
|
||||
let dir = Math.sign(to - from);
|
||||
let delta = Math.abs(to - from);
|
||||
let steps = Math.floor(delta / maxInc);
|
||||
let rem = delta % maxInc;
|
||||
let per = delta / steps;
|
||||
if (rem) {
|
||||
steps++;
|
||||
per = delta / steps;
|
||||
}
|
||||
let out = incFrom ? [from] : [];
|
||||
while (steps-- > 0) {
|
||||
from += per * dir;
|
||||
out.push(from);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** track an array of promises as they all complete */
|
||||
async function pwait(promises, tracker) {
|
||||
let count = 0;
|
||||
if (tracker)
|
||||
for (let p of promises) {
|
||||
p.then(data => {
|
||||
tracker(count++, promises.length, data);
|
||||
});
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
/** return a promise that resolves after a given time */
|
||||
function ptimer(time) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
||||
|
||||
function numOrDefault(num, def) {
|
||||
return num !== undefined ? num : def;
|
||||
}
|
||||
|
||||
/**
|
||||
* call function with all combinations of a1, a2
|
||||
* and passing in the supplied arg object.
|
||||
* used by polygons.trimTo() for trimming slice projected fills
|
||||
* @param {Array} a1
|
||||
* @param {Array} a2
|
||||
* @param {Object} arg
|
||||
* @param {Function} fn
|
||||
* @returns {Object}
|
||||
*/
|
||||
function doCombinations(a1, a2, arg, fn) {
|
||||
let i, j;
|
||||
for (i = 0; i < a1.length; i++) {
|
||||
for (j = (a1 === a2 ? i + 1 : 0); j < a2.length; j++) {
|
||||
fn(a1[i], a2[j], arg);
|
||||
}
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
|
||||
function isClockwise(p1, p2, p3) {
|
||||
return area2(p1, p2, p3) > 0;
|
||||
}
|
||||
|
||||
function isCounterClockwise(p1, p2, p3) {
|
||||
return area2(p1, p2, p3) < 0;
|
||||
}
|
||||
|
||||
function pac(p1, p2) {
|
||||
return (p2.x - p1.x) * (p2.y + p1.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns 2x area for a triangle with sign indicating handedness
|
||||
* @returns {number} negative for CCW progression, positive for CW progression
|
||||
*/
|
||||
function area2(p1, p2, p3) {
|
||||
return pac(p1, p2) + pac(p2, p3) + pac(p3, p1);
|
||||
}
|
||||
|
||||
function isCloseTo(v1, v2, dist) {
|
||||
return Math.abs(v1 - v2) <= (dist || base.config.precision_merge);
|
||||
}
|
||||
|
||||
function inCloseRange(val, min, max) {
|
||||
return (isCloseTo(val, min) || val >= min) && (isCloseTo(val, max) || val <= max);
|
||||
}
|
||||
|
||||
/**
|
||||
* return square of value
|
||||
*/
|
||||
function sqr(v) {
|
||||
return v * v
|
||||
}
|
||||
|
||||
// radians rotatition around origin
|
||||
function rotate(x,y,radians) {
|
||||
return [
|
||||
x * Math.cos(radians) - y * Math.sin(radians),
|
||||
y * Math.cos(radians) + x * Math.sin(radians)
|
||||
];
|
||||
}
|
||||
|
||||
const deg2rad = (Math.PI / 180);
|
||||
const rad2deg = (180 / Math.PI);
|
||||
|
||||
function toRadians(degrees) {
|
||||
return degrees * deg2rad;
|
||||
}
|
||||
|
||||
function toDegrees(radians) {
|
||||
return radians * rad2deg;
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance between two points
|
||||
*/
|
||||
function dist2D(p1, p2) {
|
||||
return Math.sqrt(distSq(p1, p2));
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance squared between two points
|
||||
*/
|
||||
function distSq(p1, p2) {
|
||||
return sqr(p2.x - p1.x) + sqr(p2.y - p1.y)
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance squared between two points
|
||||
* enables faster Point.nearPolygon()
|
||||
*/
|
||||
function distSqv2(x1, y1, x2, y2) {
|
||||
return sqr(x2 - x1) + sqr(y2 - y1)
|
||||
}
|
||||
|
||||
function offsetPrecision(offset, precision) {
|
||||
return Math.abs(offset) - precision;
|
||||
}
|
||||
|
||||
function inRange(value, min, max) {
|
||||
let val = parseFloat(value);
|
||||
return val >= min && val <= max;
|
||||
}
|
||||
|
||||
function round(v, zeros) {
|
||||
if (typeof v === 'object') {
|
||||
for (let [key,val] of Object.entries(v)) {
|
||||
if (typeof val === 'number') {
|
||||
v[key] = round(val, zeros);
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
const prec = zeros !== undefined ? zeros : round_decimal_precision;
|
||||
if (prec === 0) return v | 0;
|
||||
let pow = Math.pow(10, prec);
|
||||
return Math.round(v * pow) / pow;
|
||||
}
|
||||
|
||||
function clamp(val, low, hi) {
|
||||
return Math.max(low, Math.min(hi, val));
|
||||
}
|
||||
|
||||
/**
|
||||
* used by {@link Polygon.trace} and {@link Polygon.intersect}
|
||||
*/
|
||||
function intersect(p1, p2, p3, p4, test, parallelok) {
|
||||
let keys = base.key,
|
||||
p1x = p1.x,
|
||||
p1y = p1.y,
|
||||
p2x = p2.x,
|
||||
p2y = p2.y,
|
||||
p3x = p3.x,
|
||||
p3y = p3.y,
|
||||
p4x = p4.x,
|
||||
p4y = p4.y,
|
||||
d1x = (p2x - p1x), // ad.x
|
||||
d1y = (p2y - p1y), // ad.y
|
||||
d2x = (p4x - p3x), // bd.x
|
||||
d2y = (p4y - p3y), // bd.y
|
||||
d = (d2y * d1x) - (d2x * d1y); // det
|
||||
|
||||
//if (Math.abs(d) < 0.0000000001) {
|
||||
if (Math.abs(d) < 0.0001) {
|
||||
// lines are parallel or collinear
|
||||
return test && !parallelok ? null : keys.PARALLEL;
|
||||
}
|
||||
|
||||
let a = p1y - p3y, // origin dy
|
||||
b = p1x - p3x, // origin dx
|
||||
n1 = (d2x * a) - (d2y * b),
|
||||
n2 = (d1x * a) - (d1y * b);
|
||||
|
||||
a = n1 / d; // roughly distance from l1 origin to l2 intersection
|
||||
b = n2 / d; // roughly distance from l2 origin to l1 intersection
|
||||
|
||||
let ia = a >= -0.0001 && a <= 1.0001,
|
||||
ib = b >= -0.0001 && b <= 1.0001,
|
||||
segint = (ia && ib),
|
||||
rayint = (a >= 0 && b >= 0);
|
||||
|
||||
if (test === keys.SEGINT && !segint) return null;
|
||||
if (test === keys.RAYINT && !rayint) return null;
|
||||
|
||||
let ip = newPoint(
|
||||
p1x + (a * d1x), // x
|
||||
p1y + (a * d1y), // y
|
||||
p3.z || p4.z, // z
|
||||
segint ? keys.SEGINT : rayint ? keys.RAYINT : keys.PROJECT
|
||||
);
|
||||
|
||||
ip.dist = a;
|
||||
ip.p1 = p3;
|
||||
ip.p2 = p4;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* used by {@link rayIntersect} and {@link Polygon.trace}
|
||||
*/
|
||||
function intersectRayLine(ro, s1, p1, p2, infinite) {
|
||||
let keys = base.key,
|
||||
p1x = ro.x,
|
||||
p1y = ro.y,
|
||||
s1x = s1.dx,
|
||||
s1y = s1.dy,
|
||||
p3x = p1.x,
|
||||
p3y = p1.y,
|
||||
p4x = p2.x,
|
||||
p4y = p2.y,
|
||||
s2x = p4x - p3x,
|
||||
s2y = p4y - p3y,
|
||||
d = (s2y * s1x) - (s2x * s1y);
|
||||
|
||||
let a = p1y - p3y,
|
||||
b = p1x - p3x,
|
||||
n1 = (s2x * a) - (s2y * b),
|
||||
n2 = (s1x * a) - (s1y * b);
|
||||
|
||||
if (Math.abs(d) < 0.000000000001) {
|
||||
// lines are parallel or collinear
|
||||
return null;
|
||||
}
|
||||
|
||||
a = n1 / d;
|
||||
b = n2 / d;
|
||||
|
||||
if (infinite || (inCloseRange(b, 0, 1) && a >= 0)) {
|
||||
let ip = newPoint(
|
||||
p1x + (a * s1x),
|
||||
p1y + (a * s1y),
|
||||
p2.z || ro.z,
|
||||
keys.NONE
|
||||
);
|
||||
ip.dist = a;
|
||||
ip.p1 = p1;
|
||||
ip.p2 = p2;
|
||||
return ip;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {Point} p3
|
||||
* @param {Point} p4
|
||||
* @returns {number}
|
||||
*/
|
||||
function determinant(p1, p2, p3, p4) {
|
||||
let d1x = (p2.x - p1.x),
|
||||
d1y = (p2.y - p1.y),
|
||||
d2x = (p4.x - p3.x),
|
||||
d2y = (p4.y - p3.y);
|
||||
|
||||
return (d2y * d1x) - (d2x * d1y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Z of XY pair given plane defined by 3 points
|
||||
*/
|
||||
function zInPlane(p1, p2, p3, x, y) {
|
||||
let vec1 = new THREE.Vector3(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
|
||||
let vec2 = new THREE.Vector3(p3.x - p1.x, p3.y - p1.y, p3.z - p1.z);
|
||||
vec1.cross(vec2);
|
||||
|
||||
if (vec1.z !== 0) {
|
||||
return ((vec1.x * (x - p1.x) + vec1.y * (y - p1.y)) / -vec1.z) + p1.z;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find circle center given 3 points in XY plane
|
||||
*/
|
||||
function circleCenter(A, B, C) {
|
||||
let denominator = 2 * determinant33([
|
||||
[A.x, A.y, 1],
|
||||
[B.x, B.y, 1],
|
||||
[C.x, C.y, 1]
|
||||
]);
|
||||
let xmat = [
|
||||
[A.x * A.x + A.y * A.y, A.y, 1],
|
||||
[B.x * B.x + B.y * B.y, B.y, 1],
|
||||
[C.x * C.x + C.y * C.y, C.y, 1]
|
||||
];
|
||||
let ymat = [
|
||||
[A.x, A.x * A.x + A.y * A.y, 1],
|
||||
[B.x, B.x * B.x + B.y * B.y, 1],
|
||||
[C.x, C.x * C.x + C.y * C.y, 1]
|
||||
];
|
||||
|
||||
let center = {
|
||||
x: determinant33(xmat) / denominator,
|
||||
y: determinant33(ymat) / denominator,
|
||||
z: A.z
|
||||
};
|
||||
|
||||
if (denominator !== 0) {
|
||||
return center;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find the determinant of a 3x3 matrix array organized [row, col]
|
||||
*/
|
||||
function determinant33(mat33) {
|
||||
let cofactor00 = mat33[0][0] * (mat33[1][1] * mat33[2][2] - mat33[1][2] * mat33[2][1]);
|
||||
let cofactor01 = -mat33[0][1] * (mat33[1][0] * mat33[2][2] - mat33[1][2] * mat33[2][0]);
|
||||
let cofactor02 = mat33[0][2] * (mat33[1][0] * mat33[2][1] - mat33[1][1] * mat33[2][0]);
|
||||
return cofactor00 + cofactor01 + cofactor02;
|
||||
}
|
||||
|
||||
/**
|
||||
* return circle center given three points
|
||||
* from https://stackoverflow.com/questions/4103405/what-is-the-algorithm-for-finding-the-center-of-a-circle-from-three-points
|
||||
*/
|
||||
function center2d(A, B, C, rad) {
|
||||
let center = circleCenter(A, B, C);
|
||||
if (center && rad) {
|
||||
let dx = center.x - A.x;
|
||||
let dy = center.y - A.y;
|
||||
center.r = Math.sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
return center;
|
||||
}
|
||||
|
||||
/**
|
||||
* return one of two possible circle centers given two points, a radius and clock direction
|
||||
* https://stackoverflow.com/questions/36211171/finding-center-of-a-circle-given-two-points-and-radius
|
||||
*/
|
||||
function center2pr(p1, p2, r, clockwise) {
|
||||
let x1 = p1.x,
|
||||
x2 = p2.x,
|
||||
y1 = p1.y,
|
||||
y2 = p2.y,
|
||||
q = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2)),
|
||||
y3 = (y1 + y2) / 2,
|
||||
x3 = (x1 + x2) / 2,
|
||||
basex = Math.sqrt(Math.pow(r, 2) - Math.pow((q / 2), 2)) * (y1 - y2) / q, //calculate once
|
||||
basey = Math.sqrt(Math.pow(r, 2) - Math.pow((q / 2), 2)) * (x2 - x1) / q, //calculate once
|
||||
centerx1 = x3 + basex, //center x of circle 1
|
||||
centery1 = y3 + basey, //center y of circle 1
|
||||
centerx2 = x3 - basex, //center x of circle 2
|
||||
centery2 = y3 - basey, //center y of circle 2
|
||||
dir = new THREE.Vector2(x2 - x1, y2 - y1),
|
||||
vec1 = new THREE.Vector2(centerx1 - x1, centery1 - y1),
|
||||
vec2 = new THREE.Vector2(centerx2 - x1, centery2 - x1);
|
||||
if (clockwise) {
|
||||
return dir.cross(vec1) > 0 ? {
|
||||
x: centerx1,
|
||||
y: centery1
|
||||
} : {
|
||||
x: centerx2,
|
||||
y: centery2
|
||||
};
|
||||
} else {
|
||||
return dir.cross(vec1) < 0 ? {
|
||||
x: centerx1,
|
||||
y: centery1
|
||||
} : {
|
||||
x: centerx2,
|
||||
y: centery2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// find angle difference between 0 and 2pi from n1 to n2 (signed depending on clock direction)
|
||||
function thetaDiff(n1, n2, clockwise) {
|
||||
let diff = n2 - n1;
|
||||
while (diff < -Math.PI) diff += Math.PI * 2;
|
||||
while (diff > Math.PI) diff -= Math.PI * 2;
|
||||
if (clockwise && diff > 0) diff -= Math.PI * 2;
|
||||
if (!clockwise && diff < 0) diff += Math.PI * 2;
|
||||
return diff;
|
||||
}
|
||||
|
||||
// order array of elements using next closest element comparator
|
||||
// used for things like next-closest point walks (fdm thin fill)
|
||||
// in future, replace poly2poly and similar with this
|
||||
function orderClosest(array, fn, from) {
|
||||
if (!array.length) {
|
||||
return array;
|
||||
}
|
||||
let out = new Array(array.length);
|
||||
let outi = 0;
|
||||
let root = 0;
|
||||
if (!from) {
|
||||
from = array[root++];
|
||||
out[outi++] = from;
|
||||
}
|
||||
for (;;) {
|
||||
let best, best_i, best_dist = Infinity;
|
||||
for (let i = root; i < array.length; i++) {
|
||||
let el = array[i];
|
||||
if (!el) continue;
|
||||
let dist = fn(from, el);
|
||||
if (dist < best_dist) {
|
||||
best_dist = dist;
|
||||
best_i = i;
|
||||
best = el;
|
||||
}
|
||||
}
|
||||
if (!best) break;
|
||||
array[best_i] = undefined;
|
||||
from = out[outi++] = best;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// wrapper for earcut that handles higher order dimensions and finds the
|
||||
// two with the greatest delta to pass to the earcut algorith, then returns
|
||||
// an unwrapped array in the original dimensions
|
||||
// at present, only used by load.obj.parse()
|
||||
function triangulate(array, holes, dims, pos) {
|
||||
let narray;
|
||||
let info;
|
||||
if (dims === 2) {
|
||||
narray = array;
|
||||
} else {
|
||||
let min = new Array(dims).fill(Infinity);
|
||||
let max = new Array(dims).fill(-Infinity);
|
||||
for (let i = 0, l = array.length; i < l;) {
|
||||
for (let j = 0, av; j < dims; j++) {
|
||||
av = array[i++];
|
||||
min[j] = Math.min(min[j], av);
|
||||
max[j] = Math.max(max[j], av);
|
||||
}
|
||||
}
|
||||
let delta = new Array(dims);
|
||||
for (let i = 0; i < dims; i++) {
|
||||
delta[i] = max[i] - min[i];
|
||||
}
|
||||
let dmax = 0,
|
||||
d1, d2;
|
||||
for (let i = 0; i < dims; i++) {
|
||||
if (delta[i] > dmax) {
|
||||
dmax = delta[i];
|
||||
d1 = i;
|
||||
}
|
||||
}
|
||||
info = { d1, d2, dmax, delta: delta.slice() };
|
||||
delta[d1] = dmax = 0;
|
||||
for (let i = 0; i < dims; i++) {
|
||||
if (delta[i] > dmax) {
|
||||
dmax = delta[i];
|
||||
d2 = i;
|
||||
}
|
||||
}
|
||||
narray = new Array((array.length / dims) * 2);
|
||||
for (let i = 0, j = 0; i < array.length; i += dims) {
|
||||
narray[j++] = array[i + d1];
|
||||
narray[j++] = array[i + d2];
|
||||
}
|
||||
}
|
||||
let ec = earcut(narray, holes, 2);
|
||||
if (pos) {
|
||||
return ec;
|
||||
}
|
||||
let oa = new Array(ec.length * dims);
|
||||
for (let i = 0, e = 0, l = ec.length, ai; i < l; i++) {
|
||||
ai = ec[i] * dims;
|
||||
for (let j = 0; j < dims; j++) {
|
||||
oa[e++] = array[ai + j];
|
||||
}
|
||||
}
|
||||
if (oa.length === 0) {
|
||||
console.log('debug_triangulate', {array, oa, info});
|
||||
}
|
||||
return oa;
|
||||
}
|
||||
|
||||
function flatten(arr) {
|
||||
return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val), [])
|
||||
}
|
||||
|
||||
function comma(v) {
|
||||
if (!v) return v;
|
||||
let [lt, rt] = v.toString().split('.');
|
||||
lt = lt.toString().split('').reverse().map((v, i, a) => {
|
||||
return (i < a.length - 1 && i % 3 === 2) ? `,${v}` : v
|
||||
}).reverse().join('');
|
||||
return rt ? `${lt}.${rt}` : lt;
|
||||
}
|
||||
|
||||
// Create the base namespace object
|
||||
const base = {
|
||||
config: {
|
||||
// size of gcode debug arrow head
|
||||
debug_arrow: 0.25,
|
||||
// default # of decimal places in generated gcode
|
||||
gcode_decimals: 4,
|
||||
// heal disjoint polygons in slicing (experimental)
|
||||
bridgeLineGapDistance: 0.05,
|
||||
bridgeLineGapDistanceMax: 25,
|
||||
// Bounds default margin nearTo
|
||||
// Polygon.offset mindist2 offset precision
|
||||
precision_offset: 0.05,
|
||||
// Polygon.isEquivalent area() isCloseTo
|
||||
precision_poly_area: 0.05,
|
||||
// Polygon.isEquivalent bounds() equals value
|
||||
precision_poly_bounds: 0.01,
|
||||
// Polygon.isEquivalent point distance to other poly line
|
||||
precision_poly_merge: 0.05,
|
||||
// Polygon.traceIntersects mindist2
|
||||
// Polygon.overlaps (bounds overlaps test precision)
|
||||
// Polygon.isEquivalent circularity (is circle if 1-this < merge)
|
||||
// Slope.isSame (vert/horiz w/in this value)
|
||||
// isCloseTo() default for dist
|
||||
// sliceIntersects point merge dist for non-fill
|
||||
precision_merge: 0.005,
|
||||
precision_slice_z: 0.0001,
|
||||
// Point.isInPolygon nearPolygon value
|
||||
// Point.isInPolygonNotNear nearPolygon value
|
||||
// Point.isMergable2D distToSq2D value
|
||||
// Point.isMergable3D distToSq2D value
|
||||
// Polygon.isInside nearPolygon value
|
||||
// Polygon.isOutside nearPolygon value
|
||||
precision_merge_sq: sqr(0.005),
|
||||
// Bound.isNested inflation value for potential parent
|
||||
precision_bounds: 0.0001,
|
||||
// Slope.isSame default precision
|
||||
precision_slope: 0.02,
|
||||
// Slope.isSame use to calculate precision
|
||||
precision_slope_merge: 0.25,
|
||||
// sliceIntersect point merge distance for fill
|
||||
precision_fill_merge: 0.001,
|
||||
// convertPoints point merge distance
|
||||
// other values break cube-s9 (wtf)
|
||||
precision_decimate: 0.05,
|
||||
// decimate test over this many points
|
||||
decimate_threshold: 500000,
|
||||
// Point.onLine precision distance (endpoints in Polygon.intersect)
|
||||
precision_point_on_line: 0.01,
|
||||
// Polygon.isEquivalent value for determining similar enough to test
|
||||
precision_circularity: 0.001,
|
||||
// polygon fill hinting (settings override)
|
||||
hint_len_min: sqr(3),
|
||||
hint_len_max: sqr(20),
|
||||
hint_min_circ: 0.15,
|
||||
// tolerances to determine if a point is near a masking polygon
|
||||
precision_mask_tolerance: 0.001,
|
||||
// Polygon isInside,isOutside tolerance (accounts for midpoint skew)
|
||||
precision_close_to_poly_sq: sqr(0.001),
|
||||
// how long a segment has to be to trigger a midpoint check (inner/outer)
|
||||
precision_midpoint_check_dist: 1,
|
||||
precision_nested_sq: sqr(0.01),
|
||||
// clipper multiplier
|
||||
clipper: 100000,
|
||||
// clipper poly clean
|
||||
clipperClean: 250
|
||||
},
|
||||
key: {
|
||||
NONE: "",
|
||||
PROJECT: "project",
|
||||
SEGINT: "segint",
|
||||
RAYINT: "rayint",
|
||||
PARALLEL: "parallel"
|
||||
},
|
||||
util: {
|
||||
time,
|
||||
lerp,
|
||||
pwait,
|
||||
ptimer,
|
||||
numOrDefault,
|
||||
doCombinations,
|
||||
isClockwise,
|
||||
isCounterClockwise,
|
||||
pac,
|
||||
area2,
|
||||
isCloseTo,
|
||||
inCloseRange,
|
||||
sqr,
|
||||
rotate,
|
||||
toRadians,
|
||||
toDegrees,
|
||||
dist2D,
|
||||
distSq,
|
||||
distSqv2,
|
||||
offsetPrecision,
|
||||
inRange,
|
||||
round,
|
||||
clamp,
|
||||
intersect,
|
||||
intersectRayLine,
|
||||
determinant,
|
||||
zInPlane,
|
||||
circleCenter,
|
||||
determinant33,
|
||||
center2d,
|
||||
center2pr,
|
||||
thetaDiff,
|
||||
orderClosest,
|
||||
triangulate,
|
||||
flatten,
|
||||
comma
|
||||
},
|
||||
paths: {
|
||||
pointsToPath: function(points, offset, open, miter = 1.5) {
|
||||
const absoff = Math.abs(offset);
|
||||
// calculate segment normals which are used to calculate vertex normals
|
||||
// next segment info is associated with the current point
|
||||
const nupoints = [];
|
||||
const length = points.length;
|
||||
if (length === 2 && points[0].isEqual(points[1])) {
|
||||
return { };
|
||||
}
|
||||
const dedup = (open && length > 2) || (!open && length > 3);
|
||||
for (let i=0; i<length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
p1.normal = calc_normal(p1, p2);
|
||||
// drop next duplicate point if segment length is 0
|
||||
// is possible there are 3 dups in a row and this is
|
||||
// not handled. could make if a while, but that could
|
||||
// end up in a loop without additional checks. ignore
|
||||
// the case when the last and first point are the same
|
||||
// which is valid when the line is an open path
|
||||
if (dedup && p1.normal.len === 0 && i !== length - 1) {
|
||||
p1.normal = calc_normal(p1, points[(i + 2) % length]);
|
||||
i++;
|
||||
}
|
||||
nupoints.push(p1);
|
||||
}
|
||||
if (nupoints.length === 1) {
|
||||
console.log({points, nupoints});
|
||||
}
|
||||
// when points are dropped, we need the new array
|
||||
points = nupoints;
|
||||
// generate left / right paths and triangle faces
|
||||
const left = [];
|
||||
const right = [];
|
||||
const faces = [];
|
||||
const normals = [];
|
||||
const zn = -1;
|
||||
// calculate vertex normals from segments normals
|
||||
// vertex info is associated with the origin point
|
||||
let fl, fr;
|
||||
for (let i=0, l=points.length; i<l; i++) {
|
||||
let n1 = points[(i+l-1)%l].normal;
|
||||
let n2 = points[(i+l)%l].normal;
|
||||
let vn = open && (i === 0 || i === l-1) ?
|
||||
end_vertex(n1, n2, offset, i === 0) :
|
||||
calc_vertex(n1, n2, offset);
|
||||
let { p1, p2 } = n2;
|
||||
let { io, vl } = vn;
|
||||
if (offset < 0) {
|
||||
io = -io;
|
||||
}
|
||||
let split_left = false, split_right = false;
|
||||
if (io > 0) { // right
|
||||
split_left = vl > absoff * miter;
|
||||
split_right = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
} else { // left
|
||||
split_right = vl > absoff * miter;
|
||||
split_left = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
}
|
||||
let l0 = left.peek(1);
|
||||
let r0 = right.peek(1);
|
||||
if (split_left || split_right) {
|
||||
// shorten each leg and insert new point
|
||||
let delta = 0.1;
|
||||
let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 });
|
||||
let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 });
|
||||
let sn1 = np1.normal = calc_normal(np1, np2);
|
||||
let sn2 = np2.normal = p1.normal;
|
||||
let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1);
|
||||
let nv2 = calc_vertex(sn1, sn2, offset, np2);
|
||||
if (split_right) {
|
||||
right.push(v2pr(nv1), v2pr(nv2));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(nv1), v2pl(nv2));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let l2 = left.peek(2);
|
||||
let r2 = right.peek(2);
|
||||
faces.push([l0, r0, l1], [r0, r1, l1], [l1, r1, l2], [r1, r2, l2]);
|
||||
}
|
||||
} else {
|
||||
if (split_right) {
|
||||
right.push(v2pr(vn));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(vn));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
faces.push([l0, r0, l1], [r0, r1, l1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { left, right, faces, normals };
|
||||
},
|
||||
pathTo3D: function(path, height, z) {
|
||||
const { left, right, faces } = path;
|
||||
const vertices = [];
|
||||
const normals = [];
|
||||
const uvs = [];
|
||||
const indices = [];
|
||||
let i, j, k, l, r, f, v, n, u;
|
||||
// add left and right vertices
|
||||
for (i = 0; i < left.length; i++) {
|
||||
l = left[i];
|
||||
r = right[i];
|
||||
v = [l.x, l.y, z, r.x, r.y, z];
|
||||
n = [0, 0, 1, 0, 0, 1];
|
||||
u = [0, 0, 1, 0];
|
||||
vertices.push(...v);
|
||||
normals.push(...n);
|
||||
uvs.push(...u);
|
||||
}
|
||||
// add top vertices
|
||||
for (i = 0; i < left.length; i++) {
|
||||
l = left[i];
|
||||
r = right[i];
|
||||
v = [l.x, l.y, z + height, r.x, r.y, z + height];
|
||||
n = [0, 0, 1, 0, 0, 1];
|
||||
u = [0, 1, 1, 1];
|
||||
vertices.push(...v);
|
||||
normals.push(...n);
|
||||
uvs.push(...u);
|
||||
}
|
||||
// add faces
|
||||
for (i = 0; i < faces.length; i++) {
|
||||
f = faces[i];
|
||||
indices.push(f[0], f[1], f[2]);
|
||||
}
|
||||
// add top faces
|
||||
for (i = 0; i < faces.length; i++) {
|
||||
f = faces[i];
|
||||
indices.push(f[0] + left.length, f[1] + left.length, f[2] + left.length);
|
||||
}
|
||||
// add side faces
|
||||
for (i = 0; i < left.length - 1; i++) {
|
||||
j = i * 2;
|
||||
k = j + 2;
|
||||
indices.push(j, k, j + 1, j + 1, k, k + 1);
|
||||
}
|
||||
return { vertices, normals, uvs, indices };
|
||||
}
|
||||
},
|
||||
newPoint
|
||||
};
|
||||
|
||||
// Helper functions for paths
|
||||
function calc_normal(p1, p2) {
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = p2.y - p1.y;
|
||||
let len = Math.sqrt(dx * dx + dy * dy);
|
||||
let mn = (1 / len);
|
||||
dx *= mn;
|
||||
dy *= mn;
|
||||
return({ dx: dy, dy: -dx, p1, p2, len });
|
||||
}
|
||||
|
||||
function end_vertex(n1, n2, off, start) {
|
||||
let dx, dy;
|
||||
if (start) {
|
||||
dx = n2.dx * off;
|
||||
dy = n2.dy * off;
|
||||
} else {
|
||||
dx = n1.dx * off;
|
||||
dy = n1.dy * off;
|
||||
}
|
||||
return { dx, dy, vp: n1.p2 };
|
||||
}
|
||||
|
||||
function calc_vertex(n1, n2, off, vp) {
|
||||
let dx, dy, io, vl, q, r;
|
||||
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
|
||||
q = off / r;
|
||||
// handle spurs that switch back 180 degrees
|
||||
if (q === Infinity) {
|
||||
q = 0;
|
||||
}
|
||||
dx = (n1.dx + n2.dx) * q;
|
||||
dy = (n1.dy + n2.dy) * q;
|
||||
// io tells us whether we're turning left or right
|
||||
io = (n1.dx * n2.dy - n2.dx * n1.dy);
|
||||
// vertex length can be compared to the previons and next
|
||||
// segment lengths to see if we're highly acute
|
||||
vl = Math.sqrt(dx * dx + dy * dy);
|
||||
return { dx, dy, vp: vp || n1.p2, io, vl };
|
||||
}
|
||||
|
||||
function v2pl(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x += rec.dx;
|
||||
p.y += rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
function v2pr(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x -= rec.dx;
|
||||
p.y -= rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
const earcut = window.earcut;
|
||||
|
||||
// Export the base object and its properties
|
||||
export { base };
|
||||
export { earcut };
|
||||
export const { config, key, util, paths } = base;
|
||||
export { newPoint };
|
||||
133
src2/geo/bounds.js
Normal file
133
src2/geo/bounds.js
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
import { config, util } from './base.js';
|
||||
import { newPoint } from './point.js';
|
||||
|
||||
export class Bounds {
|
||||
constructor() {
|
||||
this.minx = 10e7;
|
||||
this.miny = 10e7;
|
||||
this.maxx = -10e7;
|
||||
this.maxy = -10e7;
|
||||
}
|
||||
|
||||
set(minx, maxx, miny, maxy) {
|
||||
this.minx = minx;
|
||||
this.miny = miny;
|
||||
this.maxx = maxx;
|
||||
this.maxy = maxy;
|
||||
return this;
|
||||
}
|
||||
|
||||
clone() {
|
||||
let b = new Bounds();
|
||||
b.minx = this.minx;
|
||||
b.miny = this.miny;
|
||||
b.maxx = this.maxx;
|
||||
b.maxy = this.maxy;
|
||||
b.maxy = this.maxy;
|
||||
return b;
|
||||
}
|
||||
|
||||
equals(bounds, margin) {
|
||||
if (!margin) margin = config.precision_offset;
|
||||
return util.isCloseTo(this.minx, bounds.minx, margin) &&
|
||||
util.isCloseTo(this.miny, bounds.miny, margin) &&
|
||||
util.isCloseTo(this.maxx, bounds.maxx, margin) &&
|
||||
util.isCloseTo(this.maxy, bounds.maxy, margin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Number} absolute delta in x,y coordinate space
|
||||
*/
|
||||
delta(bounds) {
|
||||
return 0 +
|
||||
Math.abs(this.minx - bounds.minx) +
|
||||
Math.abs(this.miny - bounds.miny) +
|
||||
Math.abs(this.maxx - bounds.maxx) +
|
||||
Math.abs(this.maxy - bounds.maxy);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} b
|
||||
*/
|
||||
merge(b) {
|
||||
this.minx = Math.min(this.minx, b.minx);
|
||||
this.maxx = Math.max(this.maxx, b.maxx);
|
||||
this.miny = Math.min(this.miny, b.miny);
|
||||
this.maxy = Math.max(this.maxy, b.maxy);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} p
|
||||
*/
|
||||
update(p) {
|
||||
this.minx = Math.min(this.minx, p.x);
|
||||
this.maxx = Math.max(this.maxx, p.x);
|
||||
this.miny = Math.min(this.miny, p.y);
|
||||
this.maxy = Math.max(this.maxy, p.y);
|
||||
return this;
|
||||
}
|
||||
|
||||
contains(bounds) {
|
||||
return bounds.isNested(this);
|
||||
}
|
||||
|
||||
containsXY(x, y) {
|
||||
return x >= this.minx && x <= this.maxx && y >= this.miny && y <= this.maxy;
|
||||
}
|
||||
|
||||
containsOffsetXY(x, y, offset) {
|
||||
return x >= this.minx - offset && x <= this.maxx + offset && y >= this.miny - offset && y <= this.maxy + offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} parent
|
||||
* @returns {boolean} true if fully inside parent bounds
|
||||
*/
|
||||
isNested(parent, precision = config.precision_bounds) {
|
||||
return (
|
||||
this.minx >= parent.minx - precision && // min-x
|
||||
this.maxx <= parent.maxx + precision && // max-x
|
||||
this.miny >= parent.miny - precision && // min-y
|
||||
this.maxy <= parent.maxy + precision // max-y
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Bounds} b
|
||||
* @param {number} precision
|
||||
* @returns {boolean}
|
||||
*/
|
||||
overlaps(b, precision = config.precision_bounds) {
|
||||
return (
|
||||
Math.abs(this.centerx() - b.centerx()) * 2 - precision < this.width() + b.width() &&
|
||||
Math.abs(this.centery() - b.centery()) * 2 - precision < this.height() + b.height()
|
||||
);
|
||||
}
|
||||
|
||||
width() {
|
||||
return this.maxx - this.minx;
|
||||
}
|
||||
|
||||
height() {
|
||||
return this.maxy - this.miny;
|
||||
}
|
||||
|
||||
center(z = 0) {
|
||||
return newPoint(this.centerx(), this.centery(), z);
|
||||
}
|
||||
|
||||
centerx() {
|
||||
return this.minx + this.width() / 2;
|
||||
}
|
||||
|
||||
centery() {
|
||||
return this.miny + this.height() / 2;
|
||||
}
|
||||
}
|
||||
|
||||
export function newBounds() {
|
||||
return new Bounds();
|
||||
}
|
||||
132
src2/geo/csg.js
Normal file
132
src2/geo/csg.js
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
/** Copyright 2014-2019 Stewart Allen -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { base } from './base.js';
|
||||
import { THREE } from '../ext/three.js';
|
||||
import manifold from '../ext/manifold.js';
|
||||
const { Mesh, BufferGeometry, Vector3 } = THREE;
|
||||
|
||||
const debug = true;
|
||||
const precision = 0.001;
|
||||
const factor = 1/precision;
|
||||
|
||||
function log() {
|
||||
if (root?.mesh?.log) {
|
||||
root.mesh.log(...arguments)
|
||||
} else if (root.debug === true) {
|
||||
console.log(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
export const CSG = {
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
union() {
|
||||
return CSG.moduleOp('manifold.union', 'union', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
// the fist argument is the target mesh. the rest are negatives
|
||||
subtract() {
|
||||
return CSG.moduleOp('manifold.subtract', 'difference', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
intersect() {
|
||||
return CSG.moduleOp('manifold.intersect', 'intersection', ...arguments);
|
||||
},
|
||||
|
||||
// accepts 2 or more arguments with threejs vertex position arrays
|
||||
difference() {
|
||||
return CSG.moduleOp('manifold.difference', 'difference', ...arguments);
|
||||
},
|
||||
|
||||
moduleOp(name, op) {
|
||||
log(`${name} ${arguments.length} meshes`);
|
||||
const args = [...arguments].slice(2).map(a => new Instance.Manifold(a));
|
||||
if (args.length < 2) {
|
||||
throw "missing one or more meshes";
|
||||
}
|
||||
const result = Instance.Manifold[op](...args);
|
||||
const output = result.getMesh();
|
||||
const errors = [ ...args, result ].map(m => m.status().value);
|
||||
for (let m of [ ...args, result ]) {
|
||||
m.delete();
|
||||
}
|
||||
if (errors.reduce((a,b) => a+b)) {
|
||||
throw `${errors}`;
|
||||
}
|
||||
return output;
|
||||
},
|
||||
|
||||
toPositionArray(mesh) {
|
||||
let { vertProperties, triVerts } = mesh;
|
||||
let vertPos = vertProperties;
|
||||
if (vertPos && triVerts) {
|
||||
const vertices = new Float32Array(triVerts.length * 3);
|
||||
for (let p=0, i=0, l=triVerts.length; i<l; i++) {
|
||||
let vi = triVerts[i] * 3;
|
||||
vertices[p++] = vertPos[vi++];
|
||||
vertices[p++] = vertPos[vi++];
|
||||
vertices[p++] = vertPos[vi++];
|
||||
}
|
||||
return vertices.map(v => v/factor);
|
||||
}
|
||||
throw "mesh missing required fields";
|
||||
},
|
||||
|
||||
fromPositionArray(pos) {
|
||||
const { index, vertices } = indexVertices(pos);
|
||||
let mesh = new Instance.Mesh({
|
||||
vertProperties: vertices.map(v => (v*factor)|0).toFloat32(),
|
||||
triVerts: index.toUint32()
|
||||
});
|
||||
return mesh;
|
||||
},
|
||||
|
||||
toBox3(mesh) {
|
||||
const { min, max } = mesh._boundingBox();
|
||||
const vmin = new THREE.Vector3(min.x, min.y, min.z);
|
||||
const vmax = new THREE.Vector3(max.x, max.y, max.z);
|
||||
return new THREE.Box3(vmin, vmax);
|
||||
},
|
||||
|
||||
indexVertices
|
||||
};
|
||||
|
||||
function indexVertices(pos) {
|
||||
log(`indexing ${pos.length/3} vertices`);
|
||||
let ipos = 0;
|
||||
const index = [];
|
||||
const vertices = [];
|
||||
const cache = {};
|
||||
const temp = { x: 0, y: 0, z: 0 };
|
||||
for (let i=0, length = pos.length; i<length; ) {
|
||||
temp.x = pos[i++];
|
||||
temp.y = pos[i++];
|
||||
temp.z = pos[i++];
|
||||
let key = [
|
||||
((temp.x * 100000) | 0),
|
||||
((temp.y * 100000) | 0),
|
||||
((temp.z * 100000) | 0)
|
||||
].join('');
|
||||
let ip = cache[key];
|
||||
if (ip >= 0) {
|
||||
index.push(ip);
|
||||
} else {
|
||||
index.push(ipos);
|
||||
cache[key] = (ipos++);
|
||||
vertices.push(temp.x, temp.y, temp.z);
|
||||
}
|
||||
}
|
||||
return { index, vertices };
|
||||
}
|
||||
|
||||
let Instance;
|
||||
manifold({
|
||||
locateFile() { return "/wasm/manifold.wasm" }
|
||||
}).then(inst => {
|
||||
inst.setup();
|
||||
Instance = inst;
|
||||
CSG.Instance = () => { return Instance };
|
||||
});
|
||||
209
src2/geo/gyroid.js
Normal file
209
src2/geo/gyroid.js
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
/** Copyright Stewart Allen -- All Rights Reserved */
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
|
||||
let cache = {};
|
||||
let lastVal;
|
||||
let lastRes = 0;
|
||||
let lastSlice = 0;
|
||||
|
||||
/**
|
||||
* @param off {number} z offset value from 0-1
|
||||
* @param res {number} resolution (pixels/slices per side)
|
||||
*/
|
||||
export function slice(off, res, val) {
|
||||
// auto clear cach if it hasn't been hit in the last 20 seconds
|
||||
// or the requested resolution or tip values have changed
|
||||
let now = Date.now();
|
||||
if (res !== lastRes || val !== lastVal || now - lastSlice > 20000) {
|
||||
cache = {};
|
||||
}
|
||||
lastVal = val;
|
||||
lastRes = res;
|
||||
lastSlice = now;
|
||||
let rez = parseInt(res || 200);
|
||||
let inc = PI2 / rez;
|
||||
let z = PI2 * off;
|
||||
let key = (z % PI2).round(3);
|
||||
let hit = cache[key];
|
||||
if (hit) {
|
||||
return hit;
|
||||
}
|
||||
let tip = val || 0;
|
||||
let edge = [];
|
||||
let vals = [];
|
||||
let points = 0;
|
||||
let points_lr = 0;
|
||||
let points_td = 0;
|
||||
for (let x=0; x<PI2; x += inc) {
|
||||
let vrow = []; // raw values row
|
||||
let erow = []; // edge values row
|
||||
edge.push(erow);
|
||||
vals.push(vrow);
|
||||
for (let y=0; y<PI2; y += inc) {
|
||||
erow.push(0);
|
||||
vrow.push(
|
||||
Math.sin(x) * Math.cos(y) +
|
||||
Math.sin(y) * Math.cos(z) +
|
||||
Math.sin(z) * Math.cos(x)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// left-right threshold search (red)
|
||||
vals.forEach((vrow, y) => {
|
||||
let erow = edge[y];
|
||||
let lval = vrow[vrow.length - 1];
|
||||
vrow.forEach((val, x) => {
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
erow[x] = 1;
|
||||
points++;
|
||||
points_lr++;
|
||||
}
|
||||
lval = val;
|
||||
})
|
||||
});
|
||||
|
||||
// top-down threshold search (green)
|
||||
for (let x=0; x<rez; x++) {
|
||||
let lval = vals[vals.length-1][x];
|
||||
for (let y=0; y<rez; y++) {
|
||||
let val = vals[y][x];
|
||||
if (
|
||||
(lval <= tip && val >= tip) || (lval >= tip && val <= tip) ||
|
||||
(lval <= -tip && val >= -tip) || (lval >= -tip && val <= -tip)
|
||||
) {
|
||||
if (edge[y][x]) {
|
||||
edge[y][x] = 3;
|
||||
} else {
|
||||
edge[y][x] = 2;
|
||||
points++
|
||||
}
|
||||
points_td++;
|
||||
}
|
||||
lval = val;
|
||||
}
|
||||
}
|
||||
|
||||
// deterime prevailing direction for chaining
|
||||
let dir = points_td > points_lr ? 'lr' : 'td';
|
||||
|
||||
// create sparse representation
|
||||
let sparse = [];
|
||||
let center = rez / 2;
|
||||
edge.forEach((row,y) => {
|
||||
row.forEach((val,x) => {
|
||||
if (val) {
|
||||
let dx = Math.abs(x - center);
|
||||
let dy = Math.abs(y - center);
|
||||
sparse.push({x: x/rez, y: y/rez, val, dist: Math.max(dx,dy)});
|
||||
}
|
||||
});
|
||||
});
|
||||
sparse.sort((a,b) => {
|
||||
return b.dist - a.dist;
|
||||
});
|
||||
|
||||
// join sparse points array by closest distance
|
||||
let polys = [];
|
||||
let chain;
|
||||
let added;
|
||||
let cleared = 0;
|
||||
let maxdist = 0.05;
|
||||
|
||||
do {
|
||||
chain = null;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
if (sparse[i]) {
|
||||
chain = [ sparse[i] ];
|
||||
polys.push(chain);
|
||||
sparse[i] = null;
|
||||
cleared++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
do {
|
||||
added = false;
|
||||
let target = chain[chain.length - 1];
|
||||
let cl_elm = null;
|
||||
let cl_idx = null;
|
||||
let cl_dst = Infinity;
|
||||
for (let i=0; i<sparse.length; i++) {
|
||||
let test_el = sparse[i];
|
||||
if (test_el) {
|
||||
let dst = distTo(target, test_el, dir);
|
||||
if (cl_idx === null || dst < cl_dst) {
|
||||
cl_idx = i;
|
||||
cl_elm = test_el;
|
||||
cl_dst = dst;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cl_elm) {
|
||||
if (cl_dst > maxdist) {
|
||||
break;
|
||||
}
|
||||
sparse[cl_idx] = null;
|
||||
cleared++;
|
||||
chain.push(cl_elm);
|
||||
added = true;
|
||||
}
|
||||
} while (added);
|
||||
} while (cleared < sparse.length);
|
||||
|
||||
let psimple = polys
|
||||
.map(poly => filter(poly, 0))
|
||||
.map(poly => filter(poly, inc));
|
||||
|
||||
let slice = {edge, points, dir, polys: psimple};
|
||||
cache[key] = slice;
|
||||
return slice;
|
||||
}
|
||||
|
||||
// merge co-linear and distance threshold
|
||||
export function filter(poly, inc) {
|
||||
if (poly.length <= 2) {
|
||||
return poly;
|
||||
}
|
||||
let nupoly = [ poly[0] ];
|
||||
let e1 = poly[1];
|
||||
let e2 = null;
|
||||
let last = poly.length - 2;
|
||||
for (let i=1; i<poly.length; i++) {
|
||||
let el = poly[i];
|
||||
let drop = inc ?
|
||||
(distTo(e1, el) <= inc) :
|
||||
(e1.x === el.x || e1.y === el.y);
|
||||
if (drop) {
|
||||
e2 = el;
|
||||
if (i < last) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (e2) {
|
||||
nupoly.push({x:(e1.x + e2.x)/2, y:(e1.y + e2.y)/2});
|
||||
e2 = null;
|
||||
} else {
|
||||
nupoly.push(e1);
|
||||
if (i === last) {
|
||||
nupoly.push(el);
|
||||
}
|
||||
}
|
||||
e1 = el;
|
||||
}
|
||||
return nupoly;
|
||||
}
|
||||
|
||||
export function distTo(a, b, dir) {
|
||||
let dx = a.x - b.x;
|
||||
let dy = a.y - b.y;
|
||||
if (dir === 'lr') {
|
||||
return Math.abs(dx);
|
||||
} else if (dir === 'td') {
|
||||
return Math.abs(dy);
|
||||
}
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
56
src2/geo/line.js
Normal file
56
src2/geo/line.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
export class Line {
|
||||
constructor(p1, p2, key) {
|
||||
if (!key) key = [p1.key, p2.key].join(';');
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
length() {
|
||||
return Math.sqrt(this.length2());
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {number} square of length
|
||||
*/
|
||||
length2() {
|
||||
return this.p1.distToSq2D(this.p2);
|
||||
}
|
||||
|
||||
slope() {
|
||||
return base.newSlope(this.p1.slopeTo(this.p2));
|
||||
}
|
||||
|
||||
reverse() {
|
||||
let t = this.p1;
|
||||
this.p1 = this.p2;
|
||||
this.p2 = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
midpoint() {
|
||||
return this.p1.midPointTo(this.p2);
|
||||
}
|
||||
|
||||
isCollinear(line) {
|
||||
let p1 = this.p1,
|
||||
p2 = this.p2,
|
||||
p3 = line.p1,
|
||||
p4 = line.p2,
|
||||
d1x = (p2.x - p1.x),
|
||||
d1y = (p2.y - p1.y),
|
||||
d2x = (p4.x - p3.x),
|
||||
d2y = (p4.y - p3.y);
|
||||
return Math.abs( (d2y * d1x) - (d2x * d1y) ) < 0.0001;
|
||||
}
|
||||
}
|
||||
|
||||
export function newLine(p1, p2, key) {
|
||||
return new Line(p1, p2, key);
|
||||
}
|
||||
|
||||
export function newOrderedLine(p1, p2, key) {
|
||||
return p1.key < p2.key ? newLine(p1,p2,key) : newLine(p2,p1,key);
|
||||
}
|
||||
553
src2/geo/paths.js
Normal file
553
src2/geo/paths.js
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base } from './base.js';
|
||||
|
||||
const { util, config } = base;
|
||||
const { sqr, numOrDefault } = util;
|
||||
|
||||
const DEG2RAD = Math.PI / 180;
|
||||
|
||||
/**
|
||||
* emit each element in an array based on
|
||||
* the next closest endpoint. arrays contain
|
||||
* elements with { first, last } points and
|
||||
* may be open polys, unlike poly2polyEmit
|
||||
*/
|
||||
export function tip2tipEmit(array, startPoint, emitter) {
|
||||
let mindist, dist, found, count = 0;
|
||||
for (;;) {
|
||||
found = null;
|
||||
mindist = Infinity;
|
||||
array.forEach(function(el) {
|
||||
if (el.delete) return;
|
||||
dist = startPoint.distTo2D(el.first);
|
||||
if (dist < mindist) {
|
||||
found = {el:el, first:el.first, last:el.last};
|
||||
mindist = dist;
|
||||
}
|
||||
dist = startPoint.distTo2D(el.last);
|
||||
if (dist < mindist) {
|
||||
found = {el:el, first:el.last, last:el.first};
|
||||
mindist = dist;
|
||||
}
|
||||
});
|
||||
if (found) {
|
||||
found.el.delete = true;
|
||||
startPoint = found.last;
|
||||
emitter(found.el, found.first, ++count);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return startPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* like tip2tipEmit but accepts an array of polygons and the next closest
|
||||
* point can be anywhere in the adjacent polygon. should be re-written
|
||||
* to be more like outputOrderClosest() and have the option to account for
|
||||
* depth in determining distance
|
||||
*/
|
||||
export function poly2polyEmit(array, startPoint, emitter, opt = {}) {
|
||||
let marker = opt.mark || 'delete';
|
||||
let mindist, dist, found, count = 0;
|
||||
for (;;) {
|
||||
found = null;
|
||||
mindist = Infinity;
|
||||
for (let poly of array) {
|
||||
if (poly[marker]) {
|
||||
continue;
|
||||
}
|
||||
if (poly.isOpen()) {
|
||||
const d2f = startPoint.distTo2D(poly.first());
|
||||
const d2l = startPoint.distTo2D(poly.last());
|
||||
if (d2f > mindist && d2l > mindist) {
|
||||
continue;
|
||||
}
|
||||
if (d2l < mindist && d2l < d2f && opt.swapdir !== false) {
|
||||
poly.reverse();
|
||||
found = {poly:poly, index:0, point:poly.first()};
|
||||
mindist = d2l;
|
||||
} else if (d2f < mindist) {
|
||||
found = {poly:poly, index:0, point:poly.first()};
|
||||
mindist = d2f;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let area = poly.open ? 1 : poly.area();
|
||||
poly.forEachPoint(function(point, index) {
|
||||
dist = opt.weight ?
|
||||
startPoint.distTo3D(point) * area * area :
|
||||
startPoint.distTo2D(point);
|
||||
if (dist < mindist) {
|
||||
found = {poly:poly, index:index, point:point};
|
||||
mindist = dist;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!found || opt.term) {
|
||||
break;
|
||||
}
|
||||
found.poly[marker] = true;
|
||||
startPoint = emitter(found.poly, found.index, ++count, startPoint) || found.point;
|
||||
}
|
||||
|
||||
// undo delete marks
|
||||
if (opt.perm !== true) {
|
||||
array.forEach(function(poly) { poly[marker] = false });
|
||||
}
|
||||
|
||||
return startPoint;
|
||||
}
|
||||
|
||||
export function calc_normal(p1, p2) {
|
||||
let dx = p2.x - p1.x;
|
||||
let dy = p2.y - p1.y;
|
||||
let len = Math.sqrt(dx * dx + dy * dy);
|
||||
let mn = (1 / len);
|
||||
dx *= mn;
|
||||
dy *= mn;
|
||||
return({ dx: dy, dy: -dx, p1, p2, len });
|
||||
}
|
||||
|
||||
export function end_vertex(n1, n2, off, start) {
|
||||
let dx, dy;
|
||||
if (start) {
|
||||
dx = n2.dx * off;
|
||||
dy = n2.dy * off;
|
||||
} else {
|
||||
dx = n1.dx * off;
|
||||
dy = n1.dy * off;
|
||||
}
|
||||
return { dx, dy, vp: n1.p2 };
|
||||
}
|
||||
|
||||
export function calc_vertex(n1, n2, off, vp) {
|
||||
let dx, dy, io, vl, q, r;
|
||||
r = 1 + (n1.dx * n2.dx + n1.dy * n2.dy);
|
||||
q = off / r;
|
||||
// handle spurs that switch back 180 degrees
|
||||
if (q === Infinity) {
|
||||
q = 0;
|
||||
}
|
||||
dx = (n1.dx + n2.dx) * q;
|
||||
dy = (n1.dy + n2.dy) * q;
|
||||
// io tells us whether we're turning left or right
|
||||
io = (n1.dx * n2.dy - n2.dx * n1.dy);
|
||||
// vertex length can be compared to the previons and next
|
||||
// segment lengths to see if we're highly acute
|
||||
vl = Math.sqrt(dx * dx + dy * dy);
|
||||
return { dx, dy, vp: vp || n1.p2, io, vl };
|
||||
}
|
||||
|
||||
export function v2pl(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x += rec.dx;
|
||||
p.y += rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function v2pr(rec) {
|
||||
let p = rec.vp.clone();
|
||||
p.x -= rec.dx;
|
||||
p.y -= rec.dy;
|
||||
p.vp = rec.vp;
|
||||
return p;
|
||||
}
|
||||
|
||||
export function pointsToPath(points, offset, open, miter = 1.5) {
|
||||
const absoff = Math.abs(offset);
|
||||
// calculate segment normals which are used to calculate vertex normals
|
||||
// next segment info is associated with the current point
|
||||
const nupoints = [];
|
||||
const length = points.length;
|
||||
if (length === 2 && points[0].isEqual(points[1])) {
|
||||
return { };
|
||||
}
|
||||
const dedup = (open && length > 2) || (!open && length > 3);
|
||||
for (let i=0; i<length; i++) {
|
||||
let p1 = points[i];
|
||||
let p2 = points[(i + 1) % length];
|
||||
p1.normal = calc_normal(p1, p2);
|
||||
// drop next duplicate point if segment length is 0
|
||||
// is possible there are 3 dups in a row and this is
|
||||
// not handled. could make if a while, but that could
|
||||
// end up in a loop without additional checks. ignore
|
||||
// the case when the last and first point are the same
|
||||
// which is valid when the line is an open path
|
||||
if (dedup && p1.normal.len === 0 && i !== length - 1) {
|
||||
p1.normal = calc_normal(p1, points[(i + 2) % length]);
|
||||
i++;
|
||||
}
|
||||
nupoints.push(p1);
|
||||
}
|
||||
if (nupoints.length === 1) {
|
||||
console.log({points, nupoints});
|
||||
}
|
||||
// when points are dropped, we need the new array
|
||||
points = nupoints;
|
||||
// generate left / right paths and triangle faces
|
||||
const left = [];
|
||||
const right = [];
|
||||
const faces = [];
|
||||
const normals = [];
|
||||
const zn = -1;
|
||||
// calculate vertex normals from segments normals
|
||||
// vertex info is associated with the origin point
|
||||
let fl, fr;
|
||||
for (let i=0, l=points.length; i<l; i++) {
|
||||
let n1 = points[(i+l-1)%l].normal;
|
||||
let n2 = points[(i+l)%l].normal;
|
||||
let vn = open && (i === 0 || i === l-1) ?
|
||||
end_vertex(n1, n2, offset, i === 0) :
|
||||
calc_vertex(n1, n2, offset);
|
||||
let { p1, p2 } = n2;
|
||||
let { io, vl } = vn;
|
||||
if (offset < 0) {
|
||||
io = -io;
|
||||
}
|
||||
let split_left = false, split_right = false;
|
||||
if (io > 0) { // right
|
||||
split_left = vl > absoff * miter;
|
||||
split_right = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
} else { // left
|
||||
split_right = vl > absoff * miter;
|
||||
split_left = vl > Math.min(n1.len, n2.len) + absoff;
|
||||
}
|
||||
let l0 = left.peek(1);
|
||||
let r0 = right.peek(1);
|
||||
if (split_left || split_right) {
|
||||
// shorten each leg and insert new point
|
||||
let delta = 0.1;
|
||||
let np1 = p1.clone().move({ x: n1.dy * delta, y: -n1.dx * delta, z: 0 });
|
||||
let np2 = p1.clone().move({ x:-n2.dy * delta, y: n2.dx * delta, z: 0 });
|
||||
let sn1 = np1.normal = calc_normal(np1, np2);
|
||||
let sn2 = np2.normal = p1.normal;
|
||||
let nv1 = calc_vertex(n1.p1.normal, sn1, offset, np1);
|
||||
let nv2 = calc_vertex(sn1, sn2, offset, np2);
|
||||
if (split_right) {
|
||||
right.push(v2pr(nv1), v2pr(nv2));
|
||||
} else {
|
||||
right.push(v2pr(vn));
|
||||
}
|
||||
if (split_left) {
|
||||
left.push(v2pl(nv1), v2pl(nv2));
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
}
|
||||
if (faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let l2 = left.peek(2);
|
||||
let r2 = right.peek(2);
|
||||
let ln = l1.vp.normal;
|
||||
let rn = r1.vp.normal;
|
||||
if (split_left && split_right) {
|
||||
faces.push(l1, l2, r1);
|
||||
faces.push(r2, r1, l2);
|
||||
fl = fl || l2;
|
||||
fr = fr || r2;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
} else if (split_left) {
|
||||
faces.push(l1, l2, r1);
|
||||
fl = fl || l2;
|
||||
fr = fr || r1;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
} else { // split right
|
||||
faces.push(r2, r1, l1);
|
||||
fl = fl || l1;
|
||||
fr = fr || r2;
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
left.push(v2pl(vn));
|
||||
right.push(v2pr(vn));
|
||||
fl = fl || left.peek(1);
|
||||
fr = fr || right.peek(1);
|
||||
}
|
||||
if (faces && l0 && r0) {
|
||||
let l1 = left.peek(split_left ? 2 : 1);
|
||||
let r1 = right.peek(split_right ? 2 : 1);
|
||||
faces.push(l1, l0, r1);
|
||||
faces.push(r0, r1, l0);
|
||||
let ln = l0.vp.normal;
|
||||
let rn = r0.vp.normal;
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
}
|
||||
if (open) {
|
||||
// move open ends by offset length
|
||||
const p0 = points[0].normal;
|
||||
const l0 = left[0];
|
||||
const r0 = right[0];
|
||||
// improve visuals of open but 90 degree overlapping ends
|
||||
const move = offset * 0.99;
|
||||
l0.x += p0.dy * move;
|
||||
l0.y -= p0.dx * move;
|
||||
r0.x += p0.dy * move;
|
||||
r0.y -= p0.dx * move;
|
||||
const pn = points.peek(2).normal;
|
||||
const ln = left.peek();
|
||||
const rn = right.peek();
|
||||
ln.x -= pn.dy * move;
|
||||
ln.y += pn.dx * move;
|
||||
rn.x -= pn.dy * move;
|
||||
rn.y += pn.dx * move;
|
||||
}
|
||||
if (!open && faces) {
|
||||
let l1 = left.peek(1);
|
||||
let r1 = right.peek(1);
|
||||
let ln = l1.vp.normal;
|
||||
let rn = r1.vp.normal;
|
||||
faces.push(fl, l1, fr);
|
||||
faces.push(r1, fr, l1);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(-rn.dx, -rn.dy, zn);
|
||||
normals.push(ln.dx, ln.dy, zn);
|
||||
}
|
||||
|
||||
return { left, right, faces, normals, open };
|
||||
}
|
||||
|
||||
export function pathTo3D(path, height, z) {
|
||||
const { faces, normals, left, right, open } = path;
|
||||
const out = [];
|
||||
const nrm = [];
|
||||
if (!(faces && left && right)) {
|
||||
return [];
|
||||
}
|
||||
if (z !== undefined) {
|
||||
for (let p of faces) {
|
||||
p.z = z;
|
||||
}
|
||||
}
|
||||
for (let p of faces) {
|
||||
out.push(p.x, p.y, p.z - height);
|
||||
}
|
||||
for (let p of faces.slice().reverse()) {
|
||||
out.push(p.x, p.y, p.z + height);
|
||||
}
|
||||
nrm.appendAll(normals);
|
||||
// reverse normals to match faces, but underside so reverse Z as well
|
||||
for (let i=normals.length-1; i>0; i-=3) {
|
||||
nrm.push(normals[i-2]);
|
||||
nrm.push(normals[i-1]);
|
||||
nrm.push(-normals[i-0]);
|
||||
}
|
||||
for (let i=0, l=left.length, tl = open ? l-1 : l; i<tl; i++) {
|
||||
let p0 = left[i];
|
||||
let p1 = left[(i+1)%l];
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p0.x, p0.y, p0.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p1.x, p1.y, p1.z + height);
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
let ln = p0.vp.normal;
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, 1);
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
nrm.push(ln.dx, ln.dy, -1);
|
||||
}
|
||||
for (let i=0, l=right.length, tl = open ? l-1 : l; i<tl; i++) {
|
||||
let p0 = right[i];
|
||||
let p1 = right[(i+1)%l];
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p0.x, p0.y, p0.z - height);
|
||||
out.push(p1.x, p1.y, p1.z - height);
|
||||
out.push(p0.x, p0.y, p0.z + height);
|
||||
out.push(p1.x, p1.y, p1.z + height);
|
||||
let rn = p0.vp.normal;
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, -1);
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
nrm.push(-rn.dy, rn.dx, 1);
|
||||
}
|
||||
if (open) {
|
||||
// begin cap
|
||||
let l0 = left[0];
|
||||
let r0 = right[0];
|
||||
out.push(l0.x, l0.y, l0.z + height);
|
||||
out.push(r0.x, r0.y, r0.z - height);
|
||||
out.push(l0.x, l0.y, l0.z - height);
|
||||
out.push(r0.x, r0.y, r0.z + height);
|
||||
out.push(r0.x, r0.y, r0.z - height);
|
||||
out.push(l0.x, l0.y, l0.z + height);
|
||||
let ln = l0.vp.normal;
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
// end cap
|
||||
let le = left.peek();
|
||||
let re = right.peek();
|
||||
out.push(le.x, le.y, le.z + height);
|
||||
out.push(le.x, le.y, le.z - height);
|
||||
out.push(re.x, re.y, re.z - height);
|
||||
out.push(re.x, re.y, re.z + height);
|
||||
out.push(le.x, le.y, le.z + height);
|
||||
out.push(re.x, re.y, re.z - height);
|
||||
ln = re.vp.normal;
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, 1);
|
||||
nrm.push(-ln.dy, ln.dx, -1);
|
||||
}
|
||||
return { faces: out, normals: nrm };
|
||||
}
|
||||
|
||||
// produces indexed geometry which isn't ideal for rendering because
|
||||
// the default threejs generated vertex normals aren't accurate
|
||||
export function shapeToPath(shape, points, closed) {
|
||||
closed = closed !== undefined ? closed : true;
|
||||
|
||||
const profileGeometry = new THREE.ShapeGeometry(shape);
|
||||
profileGeometry.rotateX(Math.PI * .5);
|
||||
|
||||
const profile = profileGeometry.attributes.position;
|
||||
const faces = new Float32Array(profile.count * points.length * 3);
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const v1 = new THREE.Vector2().subVectors(points[i - 1 < 0 ? points.length - 1 : i - 1], points[i]);
|
||||
const v2 = new THREE.Vector2().subVectors(points[i + 1 == points.length ? 0 : i + 1], points[i]);
|
||||
const angle = v2.angle() - v1.angle();
|
||||
const halfAngle = angle * .5;
|
||||
let hA = halfAngle;
|
||||
let tA = v2.angle() + Math.PI * .5;
|
||||
|
||||
if (!closed){
|
||||
if (i == 0 || i == points.length - 1) {hA = Math.PI * .5;}
|
||||
if (i == points.length - 1) {tA = v1.angle() - Math.PI * .5;}
|
||||
}
|
||||
|
||||
const shift = Math.tan(hA - Math.PI * .5);
|
||||
const shiftMatrix = new THREE.Matrix4().set(
|
||||
1, 0, 0, 0,
|
||||
-shift, 1, 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
|
||||
const tempAngle = tA;
|
||||
const rotationMatrix = new THREE.Matrix4().set(
|
||||
Math.cos(tempAngle), -Math.sin(tempAngle), 0, 0,
|
||||
Math.sin(tempAngle), Math.cos(tempAngle), 0, 0,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1
|
||||
);
|
||||
|
||||
const translationMatrix = new THREE.Matrix4().set(
|
||||
1, 0, 0, points[i].x,
|
||||
0, 1, 0, points[i].y,
|
||||
0, 0, 1, 0,
|
||||
0, 0, 0, 1,
|
||||
);
|
||||
|
||||
const cloneProfile = profile.clone();
|
||||
cloneProfile.applyMatrix4(shiftMatrix);
|
||||
cloneProfile.applyMatrix4(rotationMatrix);
|
||||
cloneProfile.applyMatrix4(translationMatrix);
|
||||
|
||||
faces.set(cloneProfile.array, cloneProfile.count * i * 3);
|
||||
}
|
||||
|
||||
const index = [];
|
||||
const lastCorner = closed == false ? points.length - 1: points.length;
|
||||
|
||||
for (let i = 0; i < lastCorner; i++) {
|
||||
for (let j = 0; j < profile.count; j++) {
|
||||
const currCorner = i;
|
||||
const nextCorner = i + 1 == points.length ? 0 : i + 1;
|
||||
const currPoint = j;
|
||||
const nextPoint = j + 1 == profile.count ? 0 : j + 1;
|
||||
|
||||
const a = nextPoint + profile.count * currCorner;
|
||||
const b = currPoint + profile.count * currCorner;
|
||||
const c = currPoint + profile.count * nextCorner;
|
||||
const d = nextPoint + profile.count * nextCorner;
|
||||
|
||||
index.push(a, b, d);
|
||||
index.push(b, c, d);
|
||||
}
|
||||
}
|
||||
|
||||
if (!closed) {
|
||||
// cheating because we know the profile length is 4 (for now)
|
||||
const p1 = 0 + profile.count * 0;
|
||||
const p2 = 1 + profile.count * 0;
|
||||
const p3 = 2 + profile.count * 0;
|
||||
const p4 = 3 + profile.count * 0;
|
||||
index.push(p1, p2, p3);
|
||||
index.push(p1, p3, p4);
|
||||
const lc = lastCorner;
|
||||
const p5 = 0 + profile.count * lc;
|
||||
const p6 = 1 + profile.count * lc;
|
||||
const p7 = 2 + profile.count * lc;
|
||||
const p8 = 3 + profile.count * lc;
|
||||
index.push(p7, p6, p5);
|
||||
index.push(p8, p7, p5);
|
||||
}
|
||||
|
||||
return {index, faces};
|
||||
}
|
||||
|
||||
export class FloatPacker {
|
||||
constructor(size, factor) {
|
||||
this.size = size;
|
||||
this.factor = Math.min(factor || 1.2, 1.1);
|
||||
this.array = new Float32Array(size);
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
push() {
|
||||
const array = this.array;
|
||||
const size = this.size;
|
||||
const args = arguments.length;
|
||||
if (this.pos + args >= size) {
|
||||
let nusize = ((size * this.factor) | 0) + args;
|
||||
let nuarray = new Float32Array(nusize);
|
||||
nuarray.set(array);
|
||||
this.array = nuarray;
|
||||
this.size = nusize;
|
||||
}
|
||||
for (let i=0; i<args; i++) {
|
||||
array[this.pos++] = arguments[i];
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if (this.pos / this.size >= 0.9) {
|
||||
return this.array.subarray(0, this.pos);
|
||||
} else {
|
||||
return this.array.slice(0, this.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
633
src2/geo/point.js
Normal file
633
src2/geo/point.js
Normal file
|
|
@ -0,0 +1,633 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Vector3 } = THREE;
|
||||
|
||||
import { util, config, key } from './base.js';
|
||||
|
||||
export class Point {
|
||||
constructor(x = 0, y = 0, z = 0, key) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
if (key) {
|
||||
this._key = key;
|
||||
}
|
||||
}
|
||||
|
||||
get key() {
|
||||
if (this._key) {
|
||||
return this._key;
|
||||
}
|
||||
return this._key = [
|
||||
((this.x * 100000) | 0),
|
||||
((this.y * 100000) | 0),
|
||||
((this.z * 100000) | 0)
|
||||
].join('');
|
||||
}
|
||||
|
||||
toClipper() {
|
||||
return {
|
||||
X: this.x * config.clipper,
|
||||
Y: this.y * config.clipper
|
||||
};
|
||||
}
|
||||
|
||||
toArray() {
|
||||
return [ this.x, this.y, this.z ];
|
||||
}
|
||||
|
||||
toVector3() {
|
||||
return new THREE.Vector3(this.x, this.y, this.z);
|
||||
}
|
||||
|
||||
set(x, y, z) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
delete this._key;
|
||||
return this;
|
||||
}
|
||||
|
||||
setX(x) {
|
||||
this.x = x;
|
||||
return this;
|
||||
}
|
||||
|
||||
setY(y) {
|
||||
this.y = y;
|
||||
return this;
|
||||
}
|
||||
|
||||
setZ(z) {
|
||||
this.z = z;
|
||||
return this;
|
||||
}
|
||||
|
||||
setA(a) {
|
||||
this.a = a;
|
||||
return this;
|
||||
}
|
||||
|
||||
swapXZ() {
|
||||
let p = this,
|
||||
t = p.x;
|
||||
p.x = p.z;
|
||||
p.z = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
swapYZ() {
|
||||
let p = this,
|
||||
t = p.y;
|
||||
p.y = p.z;
|
||||
p.z = t;
|
||||
return this;
|
||||
}
|
||||
|
||||
scale(x = 1, y = 1, z = 1) {
|
||||
this.x *= x;
|
||||
this.y *= y;
|
||||
this.z *= z;
|
||||
return this;
|
||||
}
|
||||
|
||||
round(precision) {
|
||||
return newPoint(
|
||||
this.x.round(precision),
|
||||
this.y.round(precision),
|
||||
this.z.round(precision));
|
||||
}
|
||||
|
||||
addFacet(facet) {
|
||||
if (!this.group) this.group = [];
|
||||
this.group.push(facet);
|
||||
return this;
|
||||
}
|
||||
|
||||
rekey() {
|
||||
this._key = undefined;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
clone() {
|
||||
let p = newPoint(this.x, this.y, this.z, this._key);
|
||||
if (this.a !== undefined) {
|
||||
p.a = this.a;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
slopeTo(p) {
|
||||
return base.newSlope(this, p);
|
||||
}
|
||||
|
||||
lineTo(p, k) {
|
||||
return base.newLine(this, p, k);
|
||||
}
|
||||
|
||||
isNear(p, dist) {
|
||||
return util.isCloseTo(this.x, p.x, dist) && util.isCloseTo(this.y, p.y, dist);
|
||||
}
|
||||
|
||||
/**
|
||||
* return distance to line connecting points p1, p2
|
||||
* distance is calculated on the perpendicular (normal) to line
|
||||
*
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @returns {number}
|
||||
*/
|
||||
distToLine(p1, p2) {
|
||||
return Math.sqrt(this.distToLineSq(p1, p2));
|
||||
}
|
||||
|
||||
distToLine3D(lp1, lp2) {
|
||||
// Convert points to vectors
|
||||
const p0 = [this.x, this.y, this.z];
|
||||
const p1 = [lp1.x, lp1.y, lp1.z];
|
||||
const p2 = [lp2.x, lp2.y, lp2.z];
|
||||
|
||||
// Calculate the direction vector of the line
|
||||
const lineDir = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
||||
|
||||
// Calculate the vector from p1 to the point
|
||||
const p1ToP = [p0[0] - p1[0], p0[1] - p1[1], p0[2] - p1[2]];
|
||||
|
||||
// Calculate the cross product of lineDir and p1ToP
|
||||
const crossProd = [
|
||||
lineDir[1] * p1ToP[2] - lineDir[2] * p1ToP[1],
|
||||
lineDir[2] * p1ToP[0] - lineDir[0] * p1ToP[2],
|
||||
lineDir[0] * p1ToP[1] - lineDir[1] * p1ToP[0]
|
||||
];
|
||||
|
||||
// Calculate the magnitude of the cross product
|
||||
const crossProdMag = Math.sqrt(crossProd[0] ** 2 + crossProd[1] ** 2 + crossProd[2] ** 2);
|
||||
|
||||
// Calculate the magnitude of the direction vector of the line
|
||||
const lineDirMag = Math.sqrt(lineDir[0] ** 2 + lineDir[1] ** 2 + lineDir[2] ** 2);
|
||||
|
||||
// Distance from point to line is the magnitude of the cross product divided by the magnitude of the direction vector
|
||||
const distance = crossProdMag / lineDirMag;
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
/**
|
||||
* used exclusively in new fill code. output does not agree with
|
||||
* old distToLine, but is the only method that seems to work for
|
||||
* fill. using new distToLine as a global replacement breaks support
|
||||
* offset clipping. both need to be investigated and a single line
|
||||
* normal distance needs to be formulated to replace both functions.
|
||||
*/
|
||||
distToLineNew(p1, p2) {
|
||||
return p2l(this, p1, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* return square of distance to line connecting points p1, p2
|
||||
* distance is calculated on the perpendicular (normal) to line
|
||||
*/
|
||||
distToLineSq(p1, p2) {
|
||||
let p = this,
|
||||
d = util.distSq(p1, p2);
|
||||
|
||||
let t = ((p.x - p1.x) * (p2.x - p1.x) + (p.y - p1.y) * (p2.y - p1.y)) / d;
|
||||
|
||||
if (t < 0) return util.distSq(p, p1);
|
||||
if (t > 1) return util.distSq(p, p2);
|
||||
|
||||
return util.distSqv2(p.x, p.y, p1.x + t * (p2.x - p1.x), p1.y + t * (p2.y - p1.y));
|
||||
}
|
||||
|
||||
withinDist2(p1, p2, dist2) {
|
||||
let ll2 = p1.distToSq2D(p2),
|
||||
dp1 = this.distToSq2D(p1),
|
||||
dp2 = this.distToSq2D(p2);
|
||||
// if the line segment described is less than dist2
|
||||
// then add dist2 to ll2. if this point is not closer
|
||||
// than newll2 to either point, then it can't be closer
|
||||
// to the described segment than dist2
|
||||
if (ll2 < dist2) {
|
||||
ll2 += dist2;
|
||||
if (dp1 > ll2 && dp2 > ll2) return false;
|
||||
}
|
||||
// if point is farther from each point that the distance
|
||||
// between the points and that distance is greater than dist2
|
||||
// then it's not possible for the point to be closer than
|
||||
// dist2 to the described line segment.
|
||||
if (dp1 > ll2 && dp2 > ll2) return false;
|
||||
return this.distToLineSq(p1, p2) < dist2;
|
||||
}
|
||||
|
||||
midPointTo(p2) {
|
||||
return newPoint((this.x + p2.x) / 2, (this.y + p2.y) / 2, this.z);
|
||||
}
|
||||
|
||||
midPointTo3D(p2) {
|
||||
return newPoint(
|
||||
(this.x + p2.x) / 2,
|
||||
(this.y + p2.y) / 2,
|
||||
(this.z + p2.z) / 2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* non-scale corrected version of follow()
|
||||
*/
|
||||
projectOnSlope(slope, mult) {
|
||||
return newPoint(
|
||||
this.x + slope.dx * mult,
|
||||
this.y + slope.dy * mult,
|
||||
this.z);
|
||||
}
|
||||
|
||||
followTo(point, mult) {
|
||||
return this.follow(this.slopeTo(point), mult);
|
||||
}
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are closer to this point.
|
||||
*/
|
||||
offsetPointFrom(p2, dist) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y,
|
||||
ls = dist / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls;
|
||||
return newPoint(p2.x - ox, p2.y - oy, p2.z, key.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* return a point along the line this from point to p2 offset
|
||||
* by a distance. positive distances are farther from this point.
|
||||
*/
|
||||
offsetPointTo(p2, dist) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y;
|
||||
|
||||
if (dx === 0 && dy === 0) return this;
|
||||
|
||||
let ls = dist / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls;
|
||||
|
||||
return newPoint(p1.x + ox, p1.y + oy, p2.z, key.NONE);
|
||||
}
|
||||
|
||||
offsetLineTo(p2, offset) {
|
||||
let p1 = this,
|
||||
dx = p2.x - p1.x,
|
||||
dy = p2.y - p1.y,
|
||||
ls = offset / Math.sqrt(dx * dx + dy * dy),
|
||||
ox = dx * ls,
|
||||
oy = dy * ls,
|
||||
np1 = newPoint(p1.x - oy, p1.y + ox, p1.z, key.NONE),
|
||||
np2 = newPoint(p2.x - oy, p2.y + ox, p2.z, key.NONE);
|
||||
np1.op = p1;
|
||||
np2.op = p2;
|
||||
return base.newLine(np1, np2, key.NONE);
|
||||
}
|
||||
|
||||
offset(x, y, z) {
|
||||
return newPoint(this.x + x, this.y + y, this.z + z);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a point is inside of a polygon
|
||||
* does not check children/holes
|
||||
*/
|
||||
inPolygon(poly) {
|
||||
if (!poly.bounds.containsXY(this.x, this.y)) return false;
|
||||
|
||||
let p = poly.points,
|
||||
pl = p.length,
|
||||
p1, p2, i, inside = false;
|
||||
|
||||
for (i = 0; i < pl; i++) {
|
||||
p1 = p[i];
|
||||
p2 = p[(i + 1) % pl];
|
||||
if ((p1.y >= this.y) != (p2.y >= this.y) &&
|
||||
(this.x <= (p2.x - p1.x) * (this.y - p1.y) / (p2.y - p1.y) + p1.x)) {
|
||||
inside = !inside;
|
||||
}
|
||||
}
|
||||
|
||||
return inside;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the point is inside of a polygon but
|
||||
* not inside any of it's children
|
||||
*/
|
||||
isInPolygon(poly) {
|
||||
let point = this,
|
||||
i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i = 0; i < poly.length; i++) {
|
||||
if (point.isInPolygon(poly[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let holes = poly.inner;
|
||||
if (point.inPolygon(poly) || point.nearPolygon(poly, config.precision_merge_sq)) {
|
||||
for (i = 0; holes && i < holes.length; i++) {
|
||||
if (point.inPolygon(holes[i]) && !point.nearPolygon(holes[i], config.precision_merge_sq)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the point is inside of a polygon but
|
||||
* not inside any of it's children
|
||||
*/
|
||||
isInPolygonOnly(poly) {
|
||||
let point = this,
|
||||
i;
|
||||
if (Array.isArray(poly)) {
|
||||
for (i = 0; i < poly.length; i++) {
|
||||
if (point.isInPolygonOnly(poly[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let holes = poly.inner;
|
||||
if (point.inPolygon(poly)) {
|
||||
for (i = 0; holes && i < holes.length; i++) {
|
||||
if (point.inPolygon(holes[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if point is near polygon edge. distance is squared.
|
||||
* @param {boolean} [inner] process inner polygons
|
||||
*/
|
||||
nearPolygon(poly, dist2, inner) {
|
||||
// throw new Error("nearPolygon");
|
||||
for (let i = 0, p = poly.points, pl = p.length; i < pl; i++) {
|
||||
if (this.withinDist2(p[i], p[(i + 1) % pl], dist2)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (inner && poly.inner) {
|
||||
for (let i = 0; i < poly.inner.length; i++) {
|
||||
if (this.nearPolygon(poly.inner[i], dist2)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point will not be trimmed later
|
||||
*/
|
||||
insideOffset(poly, offset, mindist2) {
|
||||
return this.inPolygon(poly) === (offset > 0) && !this.nearPolygon(poly, mindist2);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new point following given slope for given distance
|
||||
* same as projectOnSlope() but scaled
|
||||
*/
|
||||
follow(slope, distance) {
|
||||
let ls = distance / Math.sqrt(slope.dx * slope.dx + slope.dy * slope.dy);
|
||||
return newPoint(this.x + slope.dx * ls, this.y + slope.dy * ls, this.z);
|
||||
}
|
||||
|
||||
/**
|
||||
* for point, return intersecting point on z to next point if points
|
||||
* are on either size of z
|
||||
*/
|
||||
intersectZ(p, z) {
|
||||
let dx = p.x - this.x,
|
||||
dy = p.y - this.y,
|
||||
dz = p.z - this.z,
|
||||
pct = 1 - ((p.z - z) / dz);
|
||||
return newPoint(this.x + dx * pct, this.y + dy * pct, this.z + dz * pct);
|
||||
}
|
||||
|
||||
isEqual2D(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if points are close enough to be considered equivalent
|
||||
*/
|
||||
isMergable2D(p) {
|
||||
return this.isEqual2D(p) || (this.distToSq2D(p) < config.precision_merge_sq);
|
||||
}
|
||||
|
||||
/**
|
||||
* compares 3D point
|
||||
*/
|
||||
isEqual(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y && this.z === p.z);
|
||||
}
|
||||
|
||||
isEqual2D(p) {
|
||||
return this === p || (this.x === p.x && this.y === p.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if points are close enough to be considered equivalent
|
||||
*/
|
||||
isMergable3D(p) {
|
||||
return this.isEqual(p) || (this.distToSq3D(p) < config.precision_merge_sq);
|
||||
}
|
||||
|
||||
/**
|
||||
* return true if point is inside 2D square size dist*2 around p
|
||||
*/
|
||||
isInBox(p, dist) {
|
||||
return Math.abs(this.x - p.x) < dist && Math.abs(this.y - p.y) < dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* return min distance from point to a polygon
|
||||
* stops searching if any point is closer than threshold
|
||||
*
|
||||
* @param {Polygon} poly
|
||||
* @param {number} [threshold] stop looking if under threshold
|
||||
*/
|
||||
distToPolySegments(poly, threshold) {
|
||||
let point = this,
|
||||
mindist = Infinity;
|
||||
poly.forEachSegment(function(p1, p2) {
|
||||
const nextdist = Math.min(mindist, point.distToLine(p1, p2));
|
||||
mindist = Math.min(nextdist, mindist);
|
||||
// returning true terminates forEachSegment()
|
||||
if (mindist <= threshold) return true;
|
||||
});
|
||||
return mindist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Polygon} poly
|
||||
* @param {number} [threshold] stop looking if under threshold
|
||||
*/
|
||||
distToPolyPoints(poly, threshold) {
|
||||
let point = this,
|
||||
mindist = Infinity;
|
||||
poly.forEachPoint(function(pp) {
|
||||
mindist = Math.min(mindist, point.distTo2D(pp));
|
||||
if (mindist < threshold) return true;
|
||||
});
|
||||
return mindist;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Point} nearest point (less than max) from array to this point
|
||||
*/
|
||||
nearestTo(points, max) {
|
||||
if (!max) throw "missing max";
|
||||
let mind = Infinity,
|
||||
minp = null,
|
||||
i, p, d;
|
||||
for (i = 0; i < points.length; i++) {
|
||||
p = points[i];
|
||||
if (p === this || p.del) continue;
|
||||
d = this.distToSq2D(p);
|
||||
if (d < max && d < mind) {
|
||||
mind = d;
|
||||
minp = p;
|
||||
}
|
||||
}
|
||||
return minp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point[]} points
|
||||
* @return {number} average square dist to cloud of points
|
||||
*/
|
||||
averageDistTo(points) {
|
||||
let sum = 0.0,
|
||||
count = 0,
|
||||
i;
|
||||
for (i = 0; i < points.length; i++) {
|
||||
if (points[i] != this) {
|
||||
sum += this.distToSq2D(points[i]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
/**
|
||||
* dist to point in 2D
|
||||
*/
|
||||
distTo2D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* square of distance in 2D
|
||||
*/
|
||||
distToSq2D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
|
||||
distTo3D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y,
|
||||
dz = this.z - p.z;
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* square of distance in 3D
|
||||
*/
|
||||
distToSq3D(p) {
|
||||
let dx = this.x - p.x,
|
||||
dy = this.y - p.y,
|
||||
dz = this.z - p.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point is inside triangle described by three points
|
||||
*/
|
||||
inTriangle(a, b, c) {
|
||||
let as_x = this.x - a.x,
|
||||
as_y = this.y - a.y,
|
||||
s_ab = (b.x - a.x) * as_y - (b.y - a.y) * as_x > 0;
|
||||
if ((c.x - a.x) * as_y - (c.y - a.y) * as_x > 0 == s_ab) return false;
|
||||
if ((c.x - b.x) * (this.y - b.y) - (c.y - b.y) * (this.x - b.x) > 0 != s_ab) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if point is on a line described by two points.
|
||||
* test sum of distances p1->this + this->p2 ~= p1->p2 whens
|
||||
* slopes from p1->this same as this->p2
|
||||
*/
|
||||
onLine(p1, p2) {
|
||||
return this.distToLine(p1, p2) < config.precision_point_on_line;
|
||||
}
|
||||
|
||||
add(delta) {
|
||||
return newPoint(this.x + delta.x, this.y + delta.y, this.z + delta.z);
|
||||
}
|
||||
|
||||
sub(delta) {
|
||||
return newPoint(this.x - delta.x, this.y - delta.y, this.z - delta.z);
|
||||
}
|
||||
|
||||
move(delta) {
|
||||
this.x += delta.x;
|
||||
this.y += delta.y;
|
||||
this.z += delta.z;
|
||||
return this;
|
||||
}
|
||||
|
||||
// radians rotatition in XY around origin
|
||||
rotate(angle) {
|
||||
const { x, y } = this;
|
||||
this.x = x * Math.cos(angle) - y * Math.sin(angle);
|
||||
this.y = y * Math.cos(angle) + x * Math.sin(angle);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function dot(u, v) {
|
||||
return u.x * v.x + u.y * v.y;
|
||||
}
|
||||
|
||||
function norm(v) {
|
||||
return Math.sqrt(dot(v, v));
|
||||
}
|
||||
|
||||
function d(u, v) {
|
||||
return Math.sqrt(sqr(u.x - v.x) + sqr(u.y - v.y));
|
||||
}
|
||||
|
||||
function p2l(p, l1, l2) {
|
||||
let v = l2.clone().sub(l1);
|
||||
let w = p.clone().sub(l1);
|
||||
let proj = w.dot(v) / v.dot(v);
|
||||
if (proj < 0) return w.length();
|
||||
if (proj > 1) return p.distTo(l2);
|
||||
return w.sub(v.multiplyScalar(proj)).length();
|
||||
}
|
||||
|
||||
export function newPoint(x, y, z, key) {
|
||||
return new Point(x, y, z, key);
|
||||
}
|
||||
|
||||
export function pointFromClipper(cp, z) {
|
||||
return newPoint(cp.X / config.clipper, cp.Y / config.clipper, z);
|
||||
}
|
||||
128
src2/geo/points.js
Normal file
128
src2/geo/points.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { newPoint } from '../geo/point.js';
|
||||
|
||||
const { config } = base;
|
||||
|
||||
/**
|
||||
* converts a geometry point array into a kiri point array
|
||||
* with optional decimation
|
||||
*/
|
||||
export function verticesToPoints(array, options) {
|
||||
let parr = new Array(array.length / 3),
|
||||
i = 0,
|
||||
j = 0,
|
||||
t = Date.now(),
|
||||
hash = {},
|
||||
unique = 0,
|
||||
passes = 0,
|
||||
points,
|
||||
oldpoints = parr.length,
|
||||
newpoints;
|
||||
|
||||
let {threshold, precision, maxpass} = options || {};
|
||||
// threshold = point count for triggering decimation
|
||||
// precision = under which points are merged
|
||||
// maxpass = max number of decimations
|
||||
threshold = threshold > 0 ? threshold : config.decimate_threshold;
|
||||
precision = precision >= 0 ? precision : config.precision_decimate;
|
||||
maxpass = maxpass >= 0 ? maxpass : 0;
|
||||
|
||||
// replace point objects with their equivalents
|
||||
if (maxpass) {
|
||||
while (i < array.length) {
|
||||
let p = newPoint(array[i++], array[i++], array[i++]),
|
||||
k = p.key,
|
||||
m = hash[k];
|
||||
if (!m) {
|
||||
m = p;
|
||||
hash[k] = p;
|
||||
unique++;
|
||||
}
|
||||
parr[j++] = m;
|
||||
}
|
||||
} else {
|
||||
while (i < array.length) {
|
||||
parr[j++] = newPoint(array[i++], array[i++], array[i++]);
|
||||
}
|
||||
}
|
||||
|
||||
// decimate until all point spacing > precision
|
||||
if (maxpass && precision > 0.0)
|
||||
while (parr.length > threshold) {
|
||||
let lines = [], line, dec = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
lines.push( {p1:p1, p2:p2, d:Math.sqrt(p1.distToSq3D(p2))} );
|
||||
lines.push( {p1:p1, p2:p3, d:Math.sqrt(p1.distToSq3D(p3))} );
|
||||
lines.push( {p1:p2, p2:p3, d:Math.sqrt(p2.distToSq3D(p3))} );
|
||||
}
|
||||
// sort by ascending line length
|
||||
lines.sort(function(a,b) {
|
||||
return a.d - b.d
|
||||
});
|
||||
// create offset mid-points
|
||||
for (i=0; i<lines.length; i++) {
|
||||
line = lines[i];
|
||||
// skip lines longer than precision threshold
|
||||
if (line.d >= precision) break;
|
||||
// skip lines where one of the points is already offset
|
||||
if (line.p1.op || line.p2.op) continue;
|
||||
// todo skip dropping lines where either point is a "sharp" on 3 vectors
|
||||
// todo skip dropping lines where either point connects to a "long" line
|
||||
line.p1.op = line.p2.op = line.p1.midPointTo3D(line.p2);
|
||||
dec++;
|
||||
}
|
||||
// exit if nothing to decimate
|
||||
if (dec === 0) break;
|
||||
passes++;
|
||||
// create new facets
|
||||
points = new Array(oldpoints);
|
||||
newpoints = 0;
|
||||
for (i=0; i<oldpoints; ) {
|
||||
let p1 = parr[i++],
|
||||
p2 = parr[i++],
|
||||
p3 = parr[i++];
|
||||
// drop facets with two offset points
|
||||
if (p1.op && p1.op === p2.op) continue;
|
||||
if (p1.op && p1.op === p3.op) continue;
|
||||
if (p2.op && p2.op === p3.op) continue;
|
||||
// otherwise emit altered facet
|
||||
points[newpoints++] = p1.op || p1;
|
||||
points[newpoints++] = p2.op || p2;
|
||||
points[newpoints++] = p3.op || p3;
|
||||
}
|
||||
parr = points.slice(0,newpoints);
|
||||
oldpoints = newpoints;
|
||||
if (passes >= maxpass) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if (passes) console.trace({passes, threshold, precision, maxpass});
|
||||
|
||||
if (passes) console.log({
|
||||
before: array.length / 3,
|
||||
after: parr.length,
|
||||
unique: unique,
|
||||
decimations: passes,
|
||||
time: (Date.now() - t)
|
||||
});
|
||||
|
||||
return parr;
|
||||
}
|
||||
|
||||
export function pointsToVertices(points) {
|
||||
let vertices = new Float32Array(points.length * 3),
|
||||
i = 0, vi = 0;
|
||||
while (i < points.length) {
|
||||
vertices[vi++] = points[i].x;
|
||||
vertices[vi++] = points[i].y;
|
||||
vertices[vi++] = points[i++].z;
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
2386
src2/geo/polygon.js
Normal file
2386
src2/geo/polygon.js
Normal file
File diff suppressed because it is too large
Load diff
1109
src2/geo/polygons.js
Normal file
1109
src2/geo/polygons.js
Normal file
File diff suppressed because it is too large
Load diff
884
src2/geo/slicer.js
Normal file
884
src2/geo/slicer.js
Normal file
|
|
@ -0,0 +1,884 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* basic slice and line connection. In future, replace kiri's fdm and cam slicers
|
||||
* with wrappers on this one.
|
||||
*/
|
||||
// dep: geo.base
|
||||
// dep: geo.line
|
||||
// dep: geo.point
|
||||
// dep: geo.polygon
|
||||
// dep: geo.polygons
|
||||
import { newPolygon, newPoint, newOrderedLine } from '../geo/polygon.js';
|
||||
gapp.register("geo.slicer", [], (root, exports) => {
|
||||
|
||||
const { base } = root;
|
||||
const { config, util, polygons } = base
|
||||
|
||||
const POLY = base.polygons;
|
||||
|
||||
function dval(v, dv) {
|
||||
return v !== undefined ? v : dv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of points as triples, a bounding box and a set of
|
||||
* slicing controls, emit an array of Slice objects to the ondone()
|
||||
* function. onupdate() will be called with two parameters (% completion
|
||||
* and an optional message) so that the UI can report progress to the user.
|
||||
*
|
||||
* @param {Point[]} points vertex array
|
||||
* @param {Object} options slicing parameters
|
||||
*/
|
||||
async function slice(points, options = {}) {
|
||||
let zMin = options.zMin || 0,
|
||||
zMax = options.zMax || 0,
|
||||
zInc = options.zInc || 0,
|
||||
zGen = options.zGen, // optional z index generator function
|
||||
zIndexes = options.indices || [],
|
||||
minStep = options.minstep || 0,
|
||||
zFlat = {}, // map area of z index flat areas
|
||||
zList = {}, // fast map of z indexes
|
||||
zLine = [], // map of line counts co-linear to z plane
|
||||
zScale, // bucket span in z units
|
||||
zSum = 0.0, // sanity check that points enclose non-zere volume
|
||||
buckets = [], // banded/grouped faces to speed up slice/search
|
||||
overlapMax = options.overlap || 0.75,
|
||||
bucketMax = options.bucketMax || 100,
|
||||
onupdate = options.onupdate || function() {},
|
||||
sliceFn = dval(options.slicer, sliceZ),
|
||||
{ debug, flat, autoDim } = options,
|
||||
i, j, p1, p2, p3;
|
||||
|
||||
if (!(points && points.length)) {
|
||||
throw "missing points array";
|
||||
}
|
||||
|
||||
// convert threejs position array into points array
|
||||
if (flat) {
|
||||
let array = [];
|
||||
for (i=0, j=points.length; i<j; ) {
|
||||
array.push(newPoint(
|
||||
points[i++].round(3),
|
||||
points[i++].round(3),
|
||||
points[i++].round(3)
|
||||
));
|
||||
}
|
||||
points = array;
|
||||
} else {
|
||||
// round points
|
||||
for (i = 0; i < points.length; i++) {
|
||||
points[i] = points[i].round(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// gather z-index stats
|
||||
for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
// used to calculate buckets (rough sum of z span)
|
||||
zSum += (Math.abs(p1.z - p2.z) + Math.abs(p2.z - p3.z) + Math.abs(p3.z - p1.z));
|
||||
// use co-flat and co-line detection to adjust slice Z
|
||||
if (p1.z === p2.z && p2.z === p3.z && p1.z >= zMin) {
|
||||
// detect faces co-planar with Z and sum the enclosed area
|
||||
let zkey = p1.z,
|
||||
area = Math.abs(util.area2(p1,p2,p3)) / 2;
|
||||
if (!zFlat[zkey]) {
|
||||
zFlat[zkey] = area;
|
||||
} else {
|
||||
zFlat[zkey] += area;
|
||||
}
|
||||
zLine[zkey] = (zLine[zkey] || 0) + 1;
|
||||
} else if (p1.z === p2.z) {
|
||||
zLine[p1.z] = (zLine[p1.z] || 0) + 1;
|
||||
} else if (p2.z === p3.z) {
|
||||
zLine[p2.z] = (zLine[p2.z] || 0) + 1;
|
||||
} else if (p3.z === p1.z) {
|
||||
zLine[p3.z] = (zLine[p3.z] || 0) + 1;
|
||||
}
|
||||
if (autoDim) {
|
||||
zMin = Math.min(zMin, p1.z, p2.z, p3.z);
|
||||
zMax = Math.max(zMax, p1.z, p2.z, p3.z);
|
||||
}
|
||||
zList[p1.z] = p1.z;
|
||||
zList[p2.z] = p2.z;
|
||||
zList[p3.z] = p3.z;
|
||||
}
|
||||
|
||||
if (zInc) {
|
||||
for (i = zMin; i <= zMax; i += zInc) {
|
||||
zIndexes.push(i);
|
||||
}
|
||||
} else if (!zIndexes.length) {
|
||||
zIndexes = Object.values(zList).sort((a,b) => a - b);
|
||||
if (minStep > 0) {
|
||||
let lastOut;
|
||||
zIndexes = zIndexes.filter(v => {
|
||||
if (lastOut !== undefined && v - lastOut < minStep) {
|
||||
return false;
|
||||
} else {
|
||||
lastOut = v;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// allow zGen to override or update zIndexes
|
||||
// FDM, CAM, Laser slicers will use this to align or interpolate layers
|
||||
if (zGen) {
|
||||
zIndexes = zGen({ zMin, zMax, zLine, zFlat, zIndexes, options });
|
||||
}
|
||||
|
||||
// ensure bucket aligmnent
|
||||
zIndexes = zIndexes.map(v => v.round(3));
|
||||
|
||||
/**
|
||||
* bucket polygons into z-bounded groups (inside or crossing)
|
||||
* to reduce the search space in complex models
|
||||
*/
|
||||
let zSpan = zMax - zMin;
|
||||
let zSpanAvg = zSum / points.length;
|
||||
let bucketCount = options.bucket !== false ?
|
||||
Math.min(bucketMax, Math.max(1, Math.floor(zSpan / zSpanAvg))) : 1;
|
||||
|
||||
zScale = 1 / (zMax / bucketCount);
|
||||
|
||||
if (debug) {
|
||||
console.log({
|
||||
zMin, zMax, zIndexes, zScale, zSum, zSpanAvg,
|
||||
points, bucketCount,
|
||||
options, buckets
|
||||
});
|
||||
}
|
||||
|
||||
/** short-circuit for microscopic and invalid objects */
|
||||
if (zSpan == 0 || zSum == 0 || points.length == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// create empty buckets
|
||||
for (i = 0; i < bucketCount; i++) {
|
||||
buckets.push({ points: [], slices: [] });
|
||||
}
|
||||
|
||||
if (bucketCount > 1) {
|
||||
let failAt = (points.length * overlapMax) | 0, bucket;
|
||||
// copy triples into all matching z-buckets
|
||||
outer: for (i = 0; i < points.length;) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let zm = Math.max(0, Math.min(p1.z, p2.z, p3.z)),
|
||||
zM = Math.max(p1.z, p2.z, p3.z),
|
||||
bm = Math.floor(zm * zScale),
|
||||
bM = Math.min(Math.ceil(zM * zScale), bucketCount);
|
||||
// add point to all buckets in range
|
||||
for (j = bm; j < bM; j++) {
|
||||
bucket = buckets[j].points;
|
||||
bucket.push(p1);
|
||||
bucket.push(p2);
|
||||
bucket.push(p3);
|
||||
// fail if single bucket exceeds threshold
|
||||
if (bucket.length > failAt) {
|
||||
if (debug) console.log({ bucketFail: bucket.length });
|
||||
bucketCount = 1;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fallback if we can't partition point space
|
||||
if (bucketCount === 1) {
|
||||
buckets = [{ points, slices: [] }];
|
||||
}
|
||||
|
||||
// create buckets data structure
|
||||
for (let i = 0, l = zIndexes.length; i < l; i++) {
|
||||
let z = zIndexes[i],
|
||||
index = bucketCount <= 1 ? 0 :
|
||||
Math.min(Math.floor(z * zScale), bucketCount - 1),
|
||||
bucket = buckets[index];
|
||||
if (bucket) {
|
||||
bucket.slices.push(z);
|
||||
} else {
|
||||
console.log({ missing_bucket: z, index });
|
||||
}
|
||||
onupdate((i / zIndexes.length) * 0.1);
|
||||
}
|
||||
|
||||
async function sliceBuckets() {
|
||||
let output = [];
|
||||
let count = 0;
|
||||
let opt = { ...options, zMin, zMax, zIndexes };
|
||||
let ps = [];
|
||||
|
||||
for (let i = 0, l = buckets.length; i < l; i++) {
|
||||
let bucket = buckets[i];
|
||||
let { points, slices } = bucket;
|
||||
if (slices.length)
|
||||
ps.push(sliceFn(slices, points, {
|
||||
...opt,
|
||||
each(rval) {
|
||||
output.push(rval);
|
||||
onupdate(0.1 + (count++ / zIndexes.length) * 0.9);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// join all returned promises
|
||||
await Promise.all(ps);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// create slices from each bucketed region
|
||||
let slices = sliceFn ? await sliceBuckets() : [];
|
||||
slices = slices.sort((a,b) => a.z - b.z);
|
||||
|
||||
return { slices, points, zMin, zMax, zIndexes, zFlat };
|
||||
}
|
||||
|
||||
/**
|
||||
* given a point, append to the correct
|
||||
* 'where' objec tarray (on, over or under)
|
||||
*
|
||||
* @param {Point} p
|
||||
* @param {number} z offset
|
||||
* @param {Obejct} where
|
||||
*/
|
||||
function checkUnderOverOn(p, z, where) {
|
||||
let delta = p.z - z;
|
||||
if (Math.abs(delta) < config.precision_slice_z) { // on
|
||||
where.on.push(p);
|
||||
} else if (delta < 0) { // under
|
||||
where.under.push(p);
|
||||
} else { // over
|
||||
where.over.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a point over and under a z offset, calculate
|
||||
* and return the intersection point on that z plane
|
||||
*
|
||||
* @param {Point} over
|
||||
* @param {Point} under
|
||||
* @param {number} z offset
|
||||
* @returns {Point} intersection point
|
||||
*/
|
||||
function intersectPoints(over, under, z) {
|
||||
let ip = [];
|
||||
for (let i = 0; i < over.length; i++) {
|
||||
for (let j = 0; j < under.length; j++) {
|
||||
ip.push(over[i].intersectZ(under[j], z));
|
||||
}
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure points are unique with a cache/key algorithm
|
||||
*/
|
||||
function getCachedPoint(phash, p) {
|
||||
let cached = phash[p.key];
|
||||
if (!cached) {
|
||||
phash[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two points and hints about their edges,
|
||||
* return a new Line object with points sorted
|
||||
* lexicographically by key. This allows for future
|
||||
* line de-duplication and joins.
|
||||
*
|
||||
* @param {Object} phash
|
||||
* @param {Point} p1
|
||||
* @param {Point} p2
|
||||
* @param {boolean} [coplanar]
|
||||
* @param {boolean} [edge]
|
||||
* @returns {Line}
|
||||
*/
|
||||
function makeZLine(phash, p1, p2, coplanar, edge) {
|
||||
p1 = getCachedPoint(phash, p1);
|
||||
p2 = getCachedPoint(phash, p2);
|
||||
let line = newOrderedLine(p1,p2);
|
||||
line.coplanar = coplanar || false;
|
||||
line.edge = edge || false;
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* process a single z-slice on a single mesh and
|
||||
* add to slices array
|
||||
*
|
||||
* @param {number} z
|
||||
*/
|
||||
async function sliceZ(z, points, options = {}) {
|
||||
if (Array.isArray(z)) {
|
||||
return Promise.all(z.map(z => sliceZ(z, points, options)));
|
||||
}
|
||||
|
||||
let { zMin, zMax, under, over, both, each } = options,
|
||||
groupFn = dval(options.groupr, both ? null : sliceConnect),
|
||||
phash = {},
|
||||
lines = [],
|
||||
p1, p2, p3;
|
||||
|
||||
// default to 'over' selection with 2 points on a line
|
||||
if (!under && !both) over = true;
|
||||
|
||||
// iterate over matching buckets for this z offset
|
||||
for (let i = 0; i < points.length; ) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
p3 = points[i++];
|
||||
let where = {under: [], over: [], on: []};
|
||||
checkUnderOverOn(p1, z, where);
|
||||
checkUnderOverOn(p2, z, where);
|
||||
checkUnderOverOn(p3, z, where);
|
||||
if (where.under.length === 3 || where.over.length === 3) {
|
||||
// does not intersect (all 3 above or below)
|
||||
} else if (where.on.length === 2) {
|
||||
// one side of triangle is on the Z plane and 3rd is below
|
||||
// drop lines with 3rd above because that leads to ambiguities
|
||||
// with complex nested polygons on flat surface
|
||||
let add2 = both ||
|
||||
(over && (where.over.length === 1 || z === zMax)) ||
|
||||
(under && (where.under.length === 1 || z === zMin));
|
||||
if (add2) {
|
||||
lines.push(makeZLine(phash, where.on[0], where.on[1], false, true));
|
||||
}
|
||||
} else if (where.on.length === 3) {
|
||||
// triangle is coplanar with Z
|
||||
// we drop these because this face is attached to 3 others
|
||||
// that will satisfy the if above (line) with 2 points
|
||||
} else if (where.under.length === 0 || where.over.length === 0) {
|
||||
// does not intersect but one point is on the slice Z plane
|
||||
} else {
|
||||
// compute two point intersections and construct line
|
||||
let line = intersectPoints(where.over, where.under, z);
|
||||
if (line.length < 2 && where.on.length === 1) {
|
||||
line.push(where.on[0]);
|
||||
}
|
||||
if (line.length === 2) {
|
||||
lines.push(makeZLine(phash, line[0], line[1]));
|
||||
} else {
|
||||
console.log({msg: "invalid ips", line: line, where: where});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.length == 0 && options.noEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
// de-dup and group lines
|
||||
lines = removeDuplicateLines(lines);
|
||||
|
||||
let rval = { z, lines };
|
||||
if (groupFn) {
|
||||
let groups = groupFn(lines, z, options);
|
||||
if (options.xor) {
|
||||
groups = polygons.xor(groups);
|
||||
}
|
||||
if (options.union) {
|
||||
let points = groups.map(p => p.length);
|
||||
if (points.length > 1) points = points.reduce((a,b) => a + b);
|
||||
// simplistic healing of non-manifold meshes
|
||||
let opt = { x: 1 };
|
||||
let union = polygons.union(polygons.nest(groups), 0.1, true, opt);
|
||||
// fall back to xor'ing polygons that might overlap
|
||||
// when one does not cleanly contain the other and we lose lots of points
|
||||
// trigger when 2 polygons and we lose > 40% of points in the union
|
||||
let delta = opt.changes < 0 ? Math.abs(opt.changes / points) : 0;
|
||||
if (groups.length === 2 && delta >= 0.4) {
|
||||
let xor = groups[0].xor(groups[1]);
|
||||
console.log({ points, pct: (opt.changes / points).round(3), xor: xor.length });
|
||||
if (xor.length) {
|
||||
union = xor;
|
||||
}
|
||||
}
|
||||
// track total poly length changes to determine if healed
|
||||
rval.changes = opt.changes;
|
||||
groups = polygons.flatten(union, null, true);
|
||||
}
|
||||
rval.groups = groups;
|
||||
}
|
||||
|
||||
// look for driver-specific slice post-processor
|
||||
if (options.post) {
|
||||
let fn = base.slicePost[options.post];
|
||||
if (fn) fn(rval, options);
|
||||
}
|
||||
|
||||
// free objects to be re-claimed and reduce memory pressure
|
||||
if (false) {
|
||||
delete rval.groups
|
||||
delete rval.lines
|
||||
|
||||
if (rval.tops)
|
||||
for (let top of rval.tops) {
|
||||
if (top.poly) {
|
||||
top.poly.freeParentRefs();
|
||||
top.simple.freeParentRefs();
|
||||
for (let shell of top.shells) shell.freeParentRefs();
|
||||
for (let fillo of top.fill_off) fillo.freeParentRefs();
|
||||
for (let last of top.last) last.freeParentRefs();
|
||||
} else {
|
||||
top.freeParentRefs();
|
||||
}
|
||||
}
|
||||
|
||||
if (rval.clip) {
|
||||
for (let clip of rval.clip) clip.freeParentRefs();
|
||||
if (rval.clip.m_AllPolys) delete rval.clip.m_AllPolys
|
||||
}
|
||||
}
|
||||
|
||||
if (each) each(rval);
|
||||
return rval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of input lines (line soup), find the path through
|
||||
* joining line ends that encompasses the greatest area without self
|
||||
* interesection. Eliminate used points and repeat. Unjoined lines
|
||||
* are permitted and handled after all other cases are handled.
|
||||
*
|
||||
* @param {Line[]} input
|
||||
* @param {number} [index]
|
||||
* @returns {Array}
|
||||
*/
|
||||
function sliceConnect(input, z, opt = {}) {
|
||||
let { debug, both } = opt;
|
||||
|
||||
if (both) {
|
||||
if (debug) console.log('unable to connect lines sliced with "both" option');
|
||||
return [];
|
||||
}
|
||||
|
||||
// map points to all other points they're connected to
|
||||
let pmap = {},
|
||||
points = [],
|
||||
output = [],
|
||||
connect = [],
|
||||
emitted = 0,
|
||||
forks = false,
|
||||
frays = false,
|
||||
bridge = config.bridgeLineGapDistance,
|
||||
bridgeMax = config.bridgeLineGapDistanceMax,
|
||||
p1, p2, gl;
|
||||
|
||||
function cachedPoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
function addConnected(p1, p2) {
|
||||
if (!p1.group) p1.group = [ p2 ];
|
||||
else p1.group.push(p2);
|
||||
}
|
||||
|
||||
function perimeter(array) {
|
||||
if (!array.perimeter) {
|
||||
array.perimeter = newPolygon().addPoints(array).perimeter();
|
||||
}
|
||||
return array.perimeter;
|
||||
}
|
||||
|
||||
/**
|
||||
* follow points through connected lines to form candidate output paths
|
||||
*/
|
||||
function findNextPath(point, current, branches, depth = 1) {
|
||||
let path = [];
|
||||
if (current) {
|
||||
current.push(path);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// prevent point re-use
|
||||
point.del = true;
|
||||
// add point to path
|
||||
path.push(point);
|
||||
|
||||
let links = point.group.filter(p => !p.del);
|
||||
|
||||
// no need to recurse at the start
|
||||
if (links.length === 2 && depth === 1) {
|
||||
point = links[0];
|
||||
// if (debug) console.log({start_mid: point, depth});
|
||||
continue;
|
||||
}
|
||||
|
||||
// if fork in the road, follow all paths to their end
|
||||
// and find the longest path
|
||||
let root = !current, nc;
|
||||
if (links.length > 1) {
|
||||
// if (debug) console.log('fork!', {links: links.length, depth, root});
|
||||
if (root) {
|
||||
current = [ path ];
|
||||
branches = [ ];
|
||||
}
|
||||
if (branches.length < 500)
|
||||
for (let p of links) {
|
||||
branches.push(nc = current.slice());
|
||||
let rpath = findNextPath(p, nc, branches, depth + 1);
|
||||
// allow point re-use in other path searches
|
||||
for (let p of rpath) p.del = false;
|
||||
}
|
||||
// flatten and sort in ascending perimeter
|
||||
let flat = branches.map(b => b.flat()).sort((a,b) => {
|
||||
return perimeter(b) - perimeter(a);
|
||||
});
|
||||
let npath = flat[0];
|
||||
if (debug) console.log({
|
||||
root,
|
||||
branches: branches.slice(),
|
||||
flat, path, npath
|
||||
});
|
||||
if (root) {
|
||||
for (let p of npath) p.del = true;
|
||||
return npath;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
// return root ? npath : path;
|
||||
} else {
|
||||
// choose next (unused) point
|
||||
point = links[0];
|
||||
}
|
||||
|
||||
// hit an open end or branch
|
||||
if (!point || point.del) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
throw "invalid state";
|
||||
}
|
||||
|
||||
// emit a polygon if it can be cleaned and still have 2 or more points
|
||||
function emit(poly) {
|
||||
emitted += poly.length;
|
||||
if (!opt.dirty) poly = poly.clean();
|
||||
if (poly.length > 2 || true) output.push(poly);
|
||||
if (debug) console.log('xray',poly);
|
||||
}
|
||||
|
||||
// given an array of paths, emit longest to shortest
|
||||
// eliminating points from the paths as they are emitted
|
||||
// shorter paths any point eliminated are eliminated as candidates.
|
||||
function emitPath(path) {
|
||||
let closed = path[0].group.indexOf(path.peek()) >= 0;
|
||||
if (closed && path.length > 2) {
|
||||
if (debug) console.log({ closed: path.length, path });
|
||||
emit(newPolygon().addPoints(path));
|
||||
} else if (path.length > 1) {
|
||||
let gap = path[0].distTo2D(path.peek()).round(4);
|
||||
if (debug) console.log({ open: path.length, gap, path });
|
||||
connect.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
// create point map, unique point list and point group arrays
|
||||
input.forEach(function(line) {
|
||||
p1 = cachedPoint(line.p1);
|
||||
p2 = cachedPoint(line.p2);
|
||||
addConnected(p1,p2);
|
||||
addConnected(p2,p1);
|
||||
});
|
||||
|
||||
// console.log({points, forks: points.filter(p => p.group.length !== 2)});
|
||||
// for each unused point, find the longest non-intersecting path
|
||||
|
||||
for (let point of points) {
|
||||
gl = point.group.length;
|
||||
forks = forks || gl > 2;
|
||||
frays = frays || gl < 2;
|
||||
}
|
||||
if (debug && (forks || frays)) console.log({forks, frays});
|
||||
|
||||
// process paths starting with forks
|
||||
if (forks) {
|
||||
if (debug) console.log('process forks');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del && point.group.length > 2) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
} }
|
||||
|
||||
// process paths with dangling endpoints
|
||||
if (frays) {
|
||||
if (debug) console.log('process frays');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del && point.group.length === 1) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
} }
|
||||
|
||||
// process normal paths
|
||||
if (debug) console.log('process mids');
|
||||
for (let point of points) {
|
||||
// must not have been used and be a dangling end
|
||||
if (!point.del) {
|
||||
let path = findNextPath(point);
|
||||
if (path) emitPath(path);
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) console.log({
|
||||
points,
|
||||
emitted,
|
||||
used: points.filter(p => p.del),
|
||||
free: points.filter(p => !p.del),
|
||||
});
|
||||
|
||||
if (debug && connect.length) console.log({connect});
|
||||
if (debug) connect = connect.map(a => a.slice());
|
||||
|
||||
// progressively connect open polygons within a bridge distance
|
||||
let iter = 1000;
|
||||
let mingap;
|
||||
if (true) do {
|
||||
mingap = Infinity;
|
||||
|
||||
outer: for (let i=0; i<connect.length; i++) {
|
||||
if (!bridge) {
|
||||
emit(newPolygon().addPoints(root).setOpen());
|
||||
continue;
|
||||
}
|
||||
|
||||
// rollup root with arrays after until no more ends match
|
||||
inner: while (true) {
|
||||
let root = connect[i];
|
||||
|
||||
if (root.delete) break;
|
||||
|
||||
let rfirst = root[0],
|
||||
rlast = root.peek(),
|
||||
dist = rfirst.distToSq2D(rlast),
|
||||
closest = { dist };
|
||||
|
||||
for (let j=i+1; j<connect.length; j++) {
|
||||
let next = connect[j];
|
||||
|
||||
if (next.delete) continue;
|
||||
|
||||
let nfirst = next[0];
|
||||
let nlast = next.peek();
|
||||
|
||||
// test last to next first
|
||||
dist = rlast.distToSq2D(nfirst);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next }
|
||||
}
|
||||
|
||||
// test last to next last
|
||||
dist = rlast.distToSq2D(nlast);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, reverse: next };
|
||||
}
|
||||
|
||||
// test first to next first
|
||||
dist = rfirst.distToSq2D(nfirst);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, reverse: root };
|
||||
}
|
||||
|
||||
// test last to next last
|
||||
dist = rfirst.distToSq2D(nlast);
|
||||
mingap = Math.min(mingap, dist);
|
||||
if (dist < closest.dist && dist <= bridge) {
|
||||
closest = { dist, next, swap: j };
|
||||
}
|
||||
}
|
||||
|
||||
let { next, reverse, swap } = closest;
|
||||
|
||||
if (next && closest.dist < bridge) {
|
||||
if (debug) console.log({
|
||||
rollup: root.slice(),
|
||||
next: next.slice(),
|
||||
reverse,
|
||||
swap
|
||||
});
|
||||
if (reverse) {
|
||||
reverse.reverse();
|
||||
}
|
||||
if (swap) {
|
||||
next.appendAll(root);
|
||||
connect[i] = next;
|
||||
connect[swap] = root;
|
||||
root.delete = true;
|
||||
next.merged = true;
|
||||
} else {
|
||||
root.appendAll(next);
|
||||
next.delete = true;
|
||||
root.merged = true;
|
||||
}
|
||||
} else {
|
||||
break inner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bridge = mingap < Infinity ? Math.max(mingap + 0.01, bridge + 0.1) : bridge + 0.1;
|
||||
if (debug) console.log({iter, bridge, mingap, bridgeMax});
|
||||
|
||||
} while (iter-- > 0 && bridge && bridge < bridgeMax && mingap < bridgeMax);
|
||||
|
||||
if (debug) console.log({ remain: connect.filter(c => !c.delete) });
|
||||
|
||||
for (let array of connect) {
|
||||
if (array.delete) continue;
|
||||
|
||||
if (debug) {
|
||||
let first = array[0];
|
||||
let last = array.peek();
|
||||
let dist = first.distToSq2D(last);
|
||||
console.log({
|
||||
dist: dist.round(4),
|
||||
merged: array.merged || false,
|
||||
array
|
||||
});
|
||||
}
|
||||
|
||||
emit(newPolygon().addPoints(array));
|
||||
}
|
||||
|
||||
if (debug) console.log({ emitted, output });
|
||||
if (debug && emitted < points.length) console.log({ leftovers:points.length - emitted });
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* eliminate duplicate lines and interior-only lines (coplanar)
|
||||
*
|
||||
* lines are sorted using lexicographic point keys such that
|
||||
* they are comparable even if their points are reversed. hinting
|
||||
* for deletion, co-planar and suspect shared edge is detectable at
|
||||
* this time.
|
||||
*
|
||||
* @param {Line[]} lines
|
||||
* @returns {Line[]}
|
||||
*/
|
||||
function removeDuplicateLines(lines) {
|
||||
let output = [],
|
||||
tmplines = [],
|
||||
points = [],
|
||||
pmap = {};
|
||||
|
||||
function cachePoint(p) {
|
||||
let cp = pmap[p.key];
|
||||
if (cp) return cp;
|
||||
points.push(p);
|
||||
pmap[p.key] = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
function addLinesToPoint(point, line) {
|
||||
cachePoint(point);
|
||||
if (!point.group) point.group = [ line ];
|
||||
else point.group.push(line);
|
||||
}
|
||||
|
||||
// mark duplicates for deletion preserving edges
|
||||
lines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = !l1.edge;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// associate points with their lines, cull deleted
|
||||
for (let line of lines) {
|
||||
if (!line.del) {
|
||||
tmplines.push(line);
|
||||
addLinesToPoint(line.p1, line);
|
||||
addLinesToPoint(line.p2, line);
|
||||
}
|
||||
}
|
||||
|
||||
// merge collinear lines
|
||||
for (let point of points) {
|
||||
// only merge when point connects to exactly one other point
|
||||
if (point.group.length != 2) {
|
||||
continue;
|
||||
}
|
||||
let l1 = point.group[0],
|
||||
l2 = point.group[1];
|
||||
if (l1.isCollinear(l2)) {
|
||||
l1.del = true;
|
||||
l2.del = true;
|
||||
// find new endpoints that are not shared point
|
||||
let p1 = l1.p1 != point ? l1.p1 : l1.p2,
|
||||
p2 = l2.p1 != point ? l2.p1 : l2.p2,
|
||||
newline = newOrderedLine(p1,p2);
|
||||
// remove deleted lines from associated points
|
||||
p1.group.remove(l1);
|
||||
p1.group.remove(l2);
|
||||
p2.group.remove(l1);
|
||||
p2.group.remove(l2);
|
||||
// associate new line with points
|
||||
p1.group.push(newline);
|
||||
p2.group.push(newline);
|
||||
// add new line to lines array
|
||||
newline.edge = l1.edge || l2.edge;
|
||||
tmplines.push(newline);
|
||||
}
|
||||
}
|
||||
|
||||
// mark duplicates for deletion
|
||||
// but preserve one if it's an edge
|
||||
tmplines.sort(function (l1, l2) {
|
||||
if (l1.key === l2.key) {
|
||||
l1.del = true;
|
||||
l2.del = !l2.edge;
|
||||
return 0;
|
||||
}
|
||||
return l1.key < l2.key ? -1 : 1;
|
||||
});
|
||||
|
||||
// create new line array culling deleted
|
||||
for (let line of tmplines) {
|
||||
if (!line.del) {
|
||||
output.push(line);
|
||||
line.p1.group = null;
|
||||
line.p2.group = null;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
gapp.overlay(base, {
|
||||
slice,
|
||||
sliceZ,
|
||||
slicePost: {},
|
||||
sliceDedup: removeDuplicateLines,
|
||||
sliceConnect
|
||||
});
|
||||
|
||||
});
|
||||
100
src2/geo/slope.js
Normal file
100
src2/geo/slope.js
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { config } from './base.js';
|
||||
|
||||
const ABS = Math.abs,
|
||||
DEG2RAD = Math.PI / 180,
|
||||
RAD2DEG = 180 / Math.PI;
|
||||
|
||||
export class Slope {
|
||||
constructor(p1, p2, dx, dy) {
|
||||
this.dx = p1 && p2 ? p2.x - p1.x : dx;
|
||||
this.dy = p1 && p2 ? p2.y - p1.y : dy;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return [this.dx, this.dy, this.angle].join(',');
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Slope(null, null, this.dx, this.dy);
|
||||
}
|
||||
|
||||
isSame(s) {
|
||||
// if very close to vertical or horizontal, they're the same
|
||||
if (ABS(this.dx) <= config.precision_merge && ABS(s.dx) <= config.precision_merge) return true;
|
||||
if (ABS(this.dy) <= config.precision_merge && ABS(s.dy) <= config.precision_merge) return true;
|
||||
// check angle within a range
|
||||
let prec = Math.min(1/Math.sqrt(this.dx * this.dx + this.dy * this.dy), config.precision_slope_merge);
|
||||
return angleWithinDelta(this.angle, s.angle, prec || config.precision_slope);
|
||||
}
|
||||
|
||||
normal() {
|
||||
let t = this.dx;
|
||||
this.dx = -this.dy;
|
||||
this.dy = t;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
return this;
|
||||
}
|
||||
|
||||
invert() {
|
||||
this.dx = -this.dx;
|
||||
this.dy = -this.dy;
|
||||
this.angle = Math.atan2(this.dy, this.dx) * RAD2DEG;
|
||||
return this;
|
||||
}
|
||||
|
||||
toUnit() {
|
||||
let max = Math.max(ABS(this.dx), ABS(this.dy));
|
||||
this.dx = this.dx / max;
|
||||
this.dy = this.dy / max;
|
||||
return this;
|
||||
}
|
||||
|
||||
factor(f) {
|
||||
this.dx *= f;
|
||||
this.dy *= f;
|
||||
return this;
|
||||
}
|
||||
|
||||
invert() {
|
||||
this.dx = -this.dx;
|
||||
this.dy = -this.dy;
|
||||
this.angle = 180 - this.angle;
|
||||
return this;
|
||||
}
|
||||
|
||||
angleDiff(s2,sign) {
|
||||
const n1 = this.angle;
|
||||
const n2 = s2.angle;
|
||||
let diff = n2 - n1;
|
||||
while (diff < -180) diff += 360;
|
||||
while (diff > 180) diff -= 360;
|
||||
return sign ? diff : Math.abs(diff);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if the difference between a & b is less than v
|
||||
*/
|
||||
function minDeltaABS(a,b,v) {
|
||||
return ABS(a-b) < v;
|
||||
}
|
||||
|
||||
function angleWithinDelta(a1, a2, delta) {
|
||||
return (ABS(a1-a2) <= delta || 360-ABS(a1-a2) <= delta);
|
||||
}
|
||||
|
||||
export function newSlope(p1, p2, dx, dy) {
|
||||
return new Slope(p1, p2, dx, dy);
|
||||
}
|
||||
|
||||
export function newSlopeFromAngle(angle) {
|
||||
return newSlope(0,0,
|
||||
Math.cos(angle * DEG2RAD),
|
||||
Math.sin(angle * DEG2RAD)
|
||||
);
|
||||
}
|
||||
202
src2/geo/wasm.js
Normal file
202
src2/geo/wasm.js
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { base } from '../geo/base.js';
|
||||
|
||||
const { config } = base;
|
||||
|
||||
const factor = config.clipper;
|
||||
|
||||
export const wasm_ctrl = {
|
||||
enable,
|
||||
disable,
|
||||
count: {
|
||||
offset: 0,
|
||||
union: 0,
|
||||
diff: 0
|
||||
}
|
||||
};
|
||||
|
||||
function log() {
|
||||
console.log(...arguments);
|
||||
}
|
||||
|
||||
function writePolys(view, polys) {
|
||||
let pcount = 0;
|
||||
for (let poly of polys) {
|
||||
pcount += writePoly(view, poly);
|
||||
}
|
||||
return pcount;
|
||||
}
|
||||
|
||||
function writePoly(view, poly, inner) {
|
||||
if (inner) {
|
||||
poly.setCounterClockwise();
|
||||
} else {
|
||||
poly.setClockwise();
|
||||
}
|
||||
let count = 1;
|
||||
let points = poly.points;
|
||||
let inners = poly.inner;
|
||||
view.writeU16(points.length, true);
|
||||
for (let i=0, il=points.length; i<il; i++) {
|
||||
let point = points[i];
|
||||
view.writeI32((point.x * factor)|0, true);
|
||||
view.writeI32((point.y * factor)|0, true);
|
||||
}
|
||||
if (inners) {
|
||||
for (let i=0, il=inners.length; i<il; i++) {
|
||||
count += writePoly(view, inners[i], true);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
function readPoly(view, z) {
|
||||
let points = view.readU16(true);
|
||||
if (points === 0) return;
|
||||
let poly = base.newPolygon();
|
||||
while (points-- > 0) {
|
||||
poly.add(view.readI32(true)/factor, view.readI32(true)/factor, z || 0);
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
function readPolys(view, z, out = []) {
|
||||
for (;;) {
|
||||
let poly = readPoly(view, z);
|
||||
if (poly) {
|
||||
out.push(poly);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function polyOffset(polys, offset, z, clean, simple) {
|
||||
wasm_ctrl.count.offset++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
pcount = writePolys(new DataWriter(wasm.heap, buffer), polys),
|
||||
resat = wasm.fn.offset(buffer, pcount, offset * factor, clean, simple),
|
||||
out = readPolys(new DataReader(wasm.heap, resat), z);
|
||||
return polyNest(out);
|
||||
}
|
||||
|
||||
export function polyUnion(polys, z) {
|
||||
wasm_ctrl.count.union++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
pcount = writePolys(new DataWriter(wasm.heap, buffer), polys),
|
||||
resat = wasm.fn.union(buffer, pcount),
|
||||
out = readPolys(new DataReader(wasm.heap, resat), z);
|
||||
return polyNest(out);
|
||||
}
|
||||
|
||||
export function polyDiff(polysA, polysB, z, AB, BA) {
|
||||
wasm_ctrl.count.diff++;
|
||||
let wasm = base.wasm,
|
||||
buffer = wasm.shared,
|
||||
writer = new DataWriter(wasm.heap, buffer),
|
||||
pcountA = writePolys(writer, polysA),
|
||||
pcountB = writePolys(writer, polysB),
|
||||
resat = wasm.fn.diff(buffer, pcountA, pcountB, AB?1:0, BA?1:0, config.clipperClean),
|
||||
reader = new DataReader(wasm.heap, resat);
|
||||
if (AB) {
|
||||
AB.appendAll(polyNest(readPolys(reader, z)));
|
||||
}
|
||||
if (BA) {
|
||||
BA.appendAll(polyNest(readPolys(reader, z)));
|
||||
}
|
||||
}
|
||||
|
||||
// nest closed polygons without existing parent / child relationships
|
||||
function polyNest(polys) {
|
||||
polys.sort((a,b) => {
|
||||
return b.bounds.minx - a.bounds.minx;
|
||||
});
|
||||
// from smallest to largest, check for enclosing bounds and nest
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let smaller = polys[i];
|
||||
// prevent parent poly from being consumed
|
||||
if (smaller.inner) continue;
|
||||
for (let j=i+1; j<il; j++) {
|
||||
let larger = polys[j];
|
||||
if (larger.bounds.contains(smaller.bounds)) {
|
||||
larger.addInner(smaller);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let tops = [];
|
||||
for (let i=0, il=polys.length; i<il; i++) {
|
||||
let poly = polys[i];
|
||||
if (!poly.parent) {
|
||||
tops.push(poly);
|
||||
}
|
||||
}
|
||||
return tops;
|
||||
}
|
||||
|
||||
function readString(pos, len) {
|
||||
let view = new DataReader(base.wasm.heap, pos);
|
||||
let out = [];
|
||||
while (len-- > 0) {
|
||||
out.push(String.fromCharCode(view.readU8()));
|
||||
}
|
||||
return out.join('');
|
||||
}
|
||||
|
||||
export function enable() {
|
||||
if (base.wasm || base._wasm) {
|
||||
return;
|
||||
}
|
||||
base._wasm = 'loading';
|
||||
fetch('/wasm/kiri-geo.wasm')
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(bytes => WebAssembly.instantiate(bytes, {
|
||||
env: {
|
||||
debug_string: (len, ptr) => { console.log('wasm', readString(ptr, len)) }
|
||||
},
|
||||
wasi_snapshot_preview1: {
|
||||
// args_get: (count,bufsize) => { return 0 },
|
||||
// args_sizes_get: (count,bufsize) => { },
|
||||
// environ_get: (count,bufsize) => { return 0 },
|
||||
// environ_sizes_get: (count,bufsize) => { },
|
||||
proc_exit: (code) => { return code }
|
||||
}
|
||||
}))
|
||||
.then(results => {
|
||||
// console.log({enabled: base.wasm});
|
||||
delete base._wasm;
|
||||
let { module, instance } = results;
|
||||
let { exports } = instance;
|
||||
let heap = new DataView(exports.memory.buffer);
|
||||
let wasm = base.wasm = {
|
||||
heap,
|
||||
exports,
|
||||
memory: exports.memory,
|
||||
memmax: exports.memory.buffer.byteLength,
|
||||
malloc: exports.mem_get,
|
||||
free: exports.mem_clr
|
||||
};
|
||||
wasm.shared = wasm.malloc(1024 * 1024 * 30),
|
||||
wasm.fn = {
|
||||
diff: exports.poly_diff,
|
||||
union: exports.poly_union,
|
||||
offset: exports.poly_offset
|
||||
};
|
||||
wasm.js = {
|
||||
diff: polyDiff,
|
||||
union: polyUnion,
|
||||
offset: polyOffset
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function disable() {
|
||||
if (base.wasm) {
|
||||
base.wasm.free(base.wasm.shared);
|
||||
delete base.wasm;
|
||||
}
|
||||
}
|
||||
232
src2/load/3mf.js
Normal file
232
src2/load/3mf.js
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
'use strict';
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
import * as JSZip from '../ext/jszip.js';
|
||||
|
||||
let { BufferAttribute, Matrix4 } = THREE;
|
||||
|
||||
let scaleMap = {
|
||||
"inch": (1 / 25.4),
|
||||
"foot": (1 / 304.8),
|
||||
"micron": (1 / 1000),
|
||||
"meter": 1000,
|
||||
"millimeter": 1,
|
||||
"centimeter": (1 / 10)
|
||||
};
|
||||
|
||||
export function query(node, path, fn) {
|
||||
let collect = {};
|
||||
let match = path[0].split('|').map(key => {
|
||||
if (key[0] === '+') {
|
||||
key = key.slice(1);
|
||||
collect[key] = true;
|
||||
}
|
||||
return key;
|
||||
});
|
||||
for (let child of [...node.childNodes]) {
|
||||
let { tagName } = child;
|
||||
if (match.indexOf(tagName) >= 0) {
|
||||
if (collect[tagName]) {
|
||||
fn(tagName, child);
|
||||
}
|
||||
if (path.length > 1) {
|
||||
query(child, path.slice(1), fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function transform(def, mesh) {
|
||||
let pos = new BufferAttribute(mesh.toFloat32(), 3);
|
||||
let mat = def.split(' ').map(v => parseFloat(v));
|
||||
mat = [
|
||||
...mat.slice(0,3), 0,
|
||||
...mat.slice(3,6), 0,
|
||||
...mat.slice(6,9), 0,
|
||||
...mat.slice(9,12), 1
|
||||
];
|
||||
pos.applyMatrix4(new Matrix4().fromArray(mat));
|
||||
return pos.array;
|
||||
}
|
||||
|
||||
/** find matching attribute regardless of namespace */
|
||||
function getLocalAttribute(node, name) {
|
||||
for (let attr of node.attributes) {
|
||||
if (attr.localName === name) {
|
||||
return attr.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function loadModel(doc) {
|
||||
let items = [];
|
||||
let objects = {};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let scale = 1;
|
||||
query(doc, ["+model","resources","+object"], (type, node) => {
|
||||
switch (type) {
|
||||
case "model":
|
||||
let units = node.getAttribute("unit");
|
||||
if (units) {
|
||||
scale = scaleMap[units] || 1;
|
||||
}
|
||||
query(node, ["build","+item"], (type, node) => {
|
||||
items.push({
|
||||
oid: node.getAttribute('objectid'),
|
||||
xform: node.getAttribute('transform')
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "object":
|
||||
let object = {
|
||||
name: node.getAttribute("name")
|
||||
};
|
||||
objects[node.getAttribute("id")] = object;
|
||||
// object are allowed one mest or one or more components
|
||||
query(node, ["+mesh"], (type, node) => {
|
||||
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
|
||||
]);
|
||||
});
|
||||
let mesh = object.mesh = [];
|
||||
query(node, ["triangles","+triangle"], (type, triangle) => {
|
||||
let v1 = parseInt(triangle.getAttribute("v1"));
|
||||
let v2 = parseInt(triangle.getAttribute("v2"));
|
||||
let v3 = parseInt(triangle.getAttribute("v3"));
|
||||
mesh.appendAll(vertices[v1]);
|
||||
mesh.appendAll(vertices[v2]);
|
||||
mesh.appendAll(vertices[v3]);
|
||||
});
|
||||
});
|
||||
if (object.mesh) {
|
||||
return;
|
||||
} else {
|
||||
object.components = [];
|
||||
}
|
||||
query(node, ["components","+component"], (type, node) => {
|
||||
object.components.push({
|
||||
oid: node.getAttribute('objectid'),
|
||||
path: getLocalAttribute(node, "path"),
|
||||
xform: node.getAttribute('transform'),
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return resolve({ objects, items });
|
||||
|
||||
// create object mesh from components
|
||||
for (let object of Object.values(objects)) {
|
||||
let { mesh, components } = object;
|
||||
if (mesh) {
|
||||
continue;
|
||||
}
|
||||
mesh = object.mesh = [];
|
||||
for (let component of components) {
|
||||
let { oid, xform } = component;
|
||||
let ref = objects[oid];
|
||||
if (!ref) {
|
||||
console.log({ missing_ref: oid, objects, components, doc });
|
||||
} else if (xform) {
|
||||
mesh.appendAll(transform(xform, ref.mesh));
|
||||
} else {
|
||||
mesh.appendAll(ref.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create export items from object references
|
||||
for (let item of items) {
|
||||
let { oid, xform } = item;
|
||||
let { name, mesh } = objects[oid];
|
||||
item.name = name;
|
||||
if (xform) {
|
||||
item.faces = transform(xform, mesh);
|
||||
} else {
|
||||
item.faces = mesh;
|
||||
}
|
||||
}
|
||||
|
||||
// return array: [{ name, faces }, { name, faces }]
|
||||
resolve(items);
|
||||
});
|
||||
}
|
||||
|
||||
function extractItems(records) {
|
||||
let outItems = [];
|
||||
let models = Object.values(records);
|
||||
|
||||
// create object mesh from components
|
||||
for (let model of models) {
|
||||
let { objects } = model;
|
||||
for (let object of Object.values(objects)) {
|
||||
let { mesh, components } = object;
|
||||
if (mesh) {
|
||||
continue;
|
||||
}
|
||||
mesh = object.mesh = [];
|
||||
for (let component of components) {
|
||||
let { oid, path, xform } = component;
|
||||
let omap = objects;
|
||||
if (path) {
|
||||
omap = records[path.substring(1)].objects;
|
||||
// console.log({ component_from_ob_path: path, using: omap });
|
||||
}
|
||||
let ref = omap[oid];
|
||||
if (!ref) {
|
||||
console.log({ missing_ref: oid, objects, components });
|
||||
} else if (xform) {
|
||||
mesh.appendAll(transform(xform, ref.mesh));
|
||||
} else {
|
||||
mesh.appendAll(ref.mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create export items from object references
|
||||
for (let model of models) {
|
||||
let { items, objects } = model;
|
||||
for (let item of items || []) {
|
||||
let { oid, xform } = item;
|
||||
let { name, mesh } = objects[oid];
|
||||
item.name = name;
|
||||
if (xform) {
|
||||
item.faces = transform(xform, mesh);
|
||||
} else {
|
||||
item.faces = mesh;
|
||||
}
|
||||
}
|
||||
outItems.push(...items);
|
||||
}
|
||||
|
||||
return outItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data binary file
|
||||
* @returns {Array} vertex face array
|
||||
*/
|
||||
export function parseAsync(data) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let zip = await JSZip.default.loadAsync(data);
|
||||
let models = {};
|
||||
for (let [key, value] of Object.entries(zip.files)) {
|
||||
if (key.endsWith(".model")) {
|
||||
let xml = await value.async("string");
|
||||
let { objects, items } = await loadModel(new DOMParser().parseFromString(xml, "text/xml"));
|
||||
models[key] = { objects, items };
|
||||
}
|
||||
}
|
||||
resolve(extractItems(models));
|
||||
});
|
||||
}
|
||||
104
src2/load/file.js
Normal file
104
src2/load/file.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as STL from './stl.js';
|
||||
import * as OBJ from './obj.js';
|
||||
import * as TMF from './3mf.js';
|
||||
import * as SVG from './svg.js';
|
||||
import * as GBR from './gbr.js';
|
||||
import { load as pngLoad } from './png.js';
|
||||
|
||||
const types = {
|
||||
stl(data, file, resolve, reject, opt = {}) {
|
||||
resolve([{
|
||||
mesh: new STL().parse(data), file
|
||||
}]);
|
||||
},
|
||||
|
||||
obj(data, file, resolve, reject, opt = {}) {
|
||||
resolve(OBJ.parse(data).map(m => {
|
||||
return { mesh: m.toFloat32(), file: nameOf(file, m.name, 1) }
|
||||
}));
|
||||
},
|
||||
|
||||
"3mf"(data, file, resolve, reject, opt = {}) {
|
||||
let i = 1;
|
||||
TMF.parseAsync(data).then((meshes) => {
|
||||
resolve(meshes.map(m => {
|
||||
return { mesh: m.faces.toFloat32(), file: nameOf(file, m.name, i++) }
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
svg(data, file, resolve, reject, opt = {}) {
|
||||
let out = SVG.parse(data, opt);
|
||||
resolve(opt.flat ? out : out.map(m => { return { mesh: m.toFloat32(), file } }));
|
||||
},
|
||||
|
||||
png(data, file, resolve, reject, opt = {}) {
|
||||
pngLoad.PNG.parse(data, {
|
||||
...opt,
|
||||
done(vertices) { resolve(vertices) },
|
||||
error(err) { reject(err) }
|
||||
});
|
||||
},
|
||||
|
||||
gbr(data, file, resolve, reject, opt = {}) {
|
||||
if (opt.flat) {
|
||||
resolve([ GBR.parse(data) ])
|
||||
} else {
|
||||
resolve([{ mesh: GBR.toMesh(data).toFloat32(), file }])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const as_buffer = [ "stl", "png", "3mf" ];
|
||||
|
||||
function nameOf(file, part, i) {
|
||||
let lid = file.lastIndexOf('.');
|
||||
if (!part && lid > 0) {
|
||||
file = file.substring(0,lid);
|
||||
}
|
||||
return part ? part : `${file}_${i}`;
|
||||
}
|
||||
|
||||
function load_data(data, file, ext, opt = {}) {
|
||||
ext = ext || file.name.toLowerCase().split('.').pop();
|
||||
return new Promise((resolve, reject) => {
|
||||
let fn = types[ext];
|
||||
if (fn) {
|
||||
fn(data, file, resolve, reject, opt);
|
||||
} else {
|
||||
reject(`unknown file type: "${ext}" from ${file}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_file(file, opt) {
|
||||
if (Array.isArray(file)) {
|
||||
return Promise.all(file.map(file => load_file(file, opt)));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file) {
|
||||
return reject('invalid or missing file');
|
||||
}
|
||||
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, opt)
|
||||
.then(data => resolve(data))
|
||||
.catch(e => reject(e));
|
||||
};
|
||||
if (as_buffer.indexOf(ext) >= 0) {
|
||||
reader.readAsArrayBuffer(reader.file);
|
||||
} else {
|
||||
reader.readAsBinaryString(reader.file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { types, as_buffer, load_data, load_file };
|
||||
export { load_file as load };
|
||||
227
src2/load/gbr.js
Normal file
227
src2/load/gbr.js
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { polygons } from '../geo/polygons.js';
|
||||
import { CSG } from '../geo/csg.js';
|
||||
import * as TracespaceParser from '../ext/gerber.js';
|
||||
import { newPolygon } from '../geo/polygon.js';
|
||||
import { newBounds } from '../geo/bounds.js';
|
||||
|
||||
export function toMesh(text, opt = {}) {
|
||||
const progress = opt.progress || function(){};
|
||||
const debug = opt.debug ? console.log : function(){};
|
||||
|
||||
// for now ignore open trace lines
|
||||
// future exports should allow a way to represent them for cnc
|
||||
const { open, closed, circs, rects } = parse(text);
|
||||
progress(5);
|
||||
|
||||
// for the copper layer:
|
||||
// should produce one top level poly which is the board outline
|
||||
// next level down should be the remaining copper trace which we
|
||||
// subtract from the board to produce paths that can be milled
|
||||
const nest = polygons.nest([...closed, ...circs, ...rects], true);
|
||||
debug({ nest });
|
||||
|
||||
if (nest.length === 0) return [].toFloat32();
|
||||
|
||||
const flat = [];
|
||||
for (let n of nest) n.flattenTo(flat, [], true);
|
||||
progress(8);
|
||||
debug({ flat });
|
||||
|
||||
const z0 = opt.z || opt.z0 || 1;
|
||||
const z1 = opt.z1 || 0.25;
|
||||
|
||||
// encapsulate all polys in one enclosing rect with an offset
|
||||
if (nest.length > 1) {
|
||||
const offset = (opt.offset || 2) * 2;
|
||||
const bounds = nest[0].bounds;
|
||||
for (let p of nest) {
|
||||
bounds.merge(p.bounds);
|
||||
}
|
||||
const rect = newPolygon().centerRectangle(bounds.center(0), bounds.width() + offset, bounds.height() + offset);
|
||||
for (let n of nest) {
|
||||
n.depth++;
|
||||
}
|
||||
flat.push(rect);
|
||||
debug({ bounds, flat });
|
||||
opt.raise = z1;
|
||||
}
|
||||
|
||||
// board outline
|
||||
const top = [];
|
||||
for (let poly of flat.filter(p => p.depth === 0)) {
|
||||
top.push(poly.extrude(z0));
|
||||
}
|
||||
progress(15);
|
||||
debug({ top });
|
||||
|
||||
// nothing to render
|
||||
if (top.length === 0) {
|
||||
return [].toFloat32();
|
||||
}
|
||||
|
||||
// copper pads
|
||||
const mid = [];
|
||||
for (let poly of flat.filter(p => p.depth === 1)) {
|
||||
mid.push(poly.extrude(z1, { zadd: z0 - z1 }));
|
||||
}
|
||||
progress(20);
|
||||
debug({ mid });
|
||||
|
||||
// if no traces, return board
|
||||
if (mid.length === 0) {
|
||||
return top[0];
|
||||
}
|
||||
|
||||
// copper traces (expand first since open)
|
||||
const open_exp = open.map(poly => {
|
||||
const diam = poly.tool?.shape?.diameter;
|
||||
return diam ? poly.offset_open(diam / 2, 'round') : null;
|
||||
}).filter(p => p).flat();
|
||||
|
||||
// union open trace expansions
|
||||
const opn = [];
|
||||
for (let poly of polygons.union(open_exp, 0, true)) {
|
||||
const tool = poly.tool;
|
||||
opn.push(poly.extrude(z0 + (opt.raise || 0)));
|
||||
}
|
||||
progress(25);
|
||||
debug({ opn });
|
||||
|
||||
// inner polys for traces which we add back to the top
|
||||
// and (optionally) remove from the bottom for making vias
|
||||
const low = [];
|
||||
for (let poly of flat.filter(p => p.depth === 2)) {
|
||||
low.push(poly.extrude(z1, { zadd: z0 - z1 }));
|
||||
}
|
||||
progress(30);
|
||||
|
||||
// convert vertex arrays to csg mesh definitions
|
||||
const topMesh = CSG.fromPositionArray(top[0]);
|
||||
const midMesh = mid.map(v => CSG.fromPositionArray(v));
|
||||
progress(35);
|
||||
|
||||
// subtract mid from top
|
||||
let meshOut = CSG.subtract(topMesh, ...midMesh);
|
||||
progress(50);
|
||||
|
||||
if (opn.length) {
|
||||
// add back raised traces (before removing vias)
|
||||
const opnMesh = opn.map(v => CSG.fromPositionArray(v))
|
||||
meshOut = CSG.union(meshOut, ...opnMesh);
|
||||
}
|
||||
progress(70);
|
||||
|
||||
if (low.length) {
|
||||
// subtract low (vias) from remaining
|
||||
const lowMesh = low.map(v => CSG.fromPositionArray(v))
|
||||
meshOut = CSG.union(meshOut, ...lowMesh);
|
||||
}
|
||||
progress(90);
|
||||
|
||||
return CSG.toPositionArray(meshOut);
|
||||
}
|
||||
|
||||
export function parse(text) {
|
||||
let fact = 1000000;
|
||||
let tools = {};
|
||||
let tool;
|
||||
let poly;
|
||||
let polys = [];
|
||||
let circs = [];
|
||||
let rects = [];
|
||||
let last = {};
|
||||
let bounds = newBounds();
|
||||
let p = TracespaceParser.createParser();
|
||||
p.feed(text);
|
||||
let r = p.results();
|
||||
let c = r.children
|
||||
.filter(c => {
|
||||
switch (c.type) {
|
||||
case 'toolDefinition':
|
||||
tools[c.code] = c.shape;
|
||||
break;
|
||||
case 'toolChange':
|
||||
tool = { code: c.code, shape: tools[c.code] };
|
||||
break;
|
||||
case 'graphic':
|
||||
c.tool = tool;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})
|
||||
.map(r => {
|
||||
return {
|
||||
type: r.graphic,
|
||||
tool: r.tool,
|
||||
x: parseInt(r.coordinates.x),
|
||||
y: parseInt(r.coordinates.y)
|
||||
};
|
||||
})
|
||||
.map(r => {
|
||||
const { x, y, tool, type } = r;
|
||||
const pos = { x:x/fact, y:y/fact, z: 0};
|
||||
bounds.update(pos);
|
||||
switch (type) {
|
||||
case 'move':
|
||||
if (true && poly && last.x === x && last.y === y && last.tool === tool) {
|
||||
// console.log('continuation');
|
||||
} else {
|
||||
poly = newPolygon().setOpen().add(pos.x, pos.y, 0);
|
||||
poly.tool = tool;
|
||||
polys.push(poly);
|
||||
}
|
||||
break;
|
||||
case 'segment':
|
||||
poly.add(pos.x, pos.y, 0);
|
||||
break;
|
||||
case 'shape':
|
||||
poly = undefined;
|
||||
let { shape } = tool;
|
||||
switch (shape.type) {
|
||||
case 'obround':
|
||||
if (shape.xSize === shape.ySize) {
|
||||
circs.push(newPolygon().centerCircle(pos, shape.xSize / 2, 20));
|
||||
} else {
|
||||
console.log('TODO', shape);
|
||||
}
|
||||
break;
|
||||
case 'circle':
|
||||
circs.push(newPolygon().centerCircle(pos, shape.diameter / 2, 20));
|
||||
break;
|
||||
case 'rectangle':
|
||||
rects.push(newPolygon().centerRectangle(pos, shape.xSize, shape.ySize));
|
||||
break;
|
||||
default:
|
||||
console.log('TODO', shape);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
last = { x, y, tool };
|
||||
return r;
|
||||
});
|
||||
// center board on origin
|
||||
const { minx, maxx, miny, maxy } = bounds;
|
||||
for (let poly of [...polys, ...circs, ...rects]) {
|
||||
poly.move({
|
||||
x: -((maxx-minx)/2 + minx),
|
||||
y: -((maxy-miny)/2 + miny),
|
||||
z: 0
|
||||
});
|
||||
}
|
||||
// separate open and closed polys (traces vs areas)
|
||||
const open = [];
|
||||
const closed = [];
|
||||
for (let poly of polys) {
|
||||
if (poly.appearsClosed()) {
|
||||
// clean up gnarly overlapping lines
|
||||
// make contiguous, simpler enclosed areas
|
||||
closed.push(...poly.simplify());
|
||||
} else {
|
||||
open.push(poly);
|
||||
}
|
||||
}
|
||||
return { open, closed, circs, rects };
|
||||
}
|
||||
70
src2/load/obj.js
Normal file
70
src2/load/obj.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
'use strict';
|
||||
|
||||
import { util } from '../geo/base.js';
|
||||
|
||||
/**
|
||||
* @param {String} text
|
||||
* @returns {Array} vertex face array
|
||||
*/
|
||||
export function parseAsync(text) {
|
||||
return new Promise((resolve,reject) => {
|
||||
resolve(parse(text));
|
||||
});
|
||||
}
|
||||
|
||||
export function parse(text) {
|
||||
let lines = text.replaceAll(/ +/g,' ').split('\n').map(l => l.trim());
|
||||
let verts = [];
|
||||
let faces = [];
|
||||
let objs = [ faces ];
|
||||
|
||||
// Polyfill for appendAll if not present
|
||||
if (!Array.prototype.appendAll) {
|
||||
Object.defineProperty(Array.prototype, 'appendAll', {
|
||||
value: function(arr) {
|
||||
for (let v of arr) this.push(v);
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
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]));
|
||||
if (tok.length > 3) {
|
||||
const p = tok.map(t => verts[t-1]).flat();
|
||||
const t = util.triangulate(p, [], 3, true);
|
||||
const r = t.map(i => tok[i]);
|
||||
for (let v of r) {
|
||||
faces.appendAll(verts[v - 1]);
|
||||
}
|
||||
} else {
|
||||
// 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.join(' ');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return objs;
|
||||
}
|
||||
31
src2/load/png.js
Normal file
31
src2/load/png.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { PNG } from '../ext/pngjs.esm.js';
|
||||
|
||||
export const load = {
|
||||
PNG: {
|
||||
parse: function(data, opt = {}) {
|
||||
let img = new PNG();
|
||||
let progress = opt.progress || noop;
|
||||
let ondone = opt.done || noop;
|
||||
let onerror = opt.error || noop;
|
||||
let onmeta = opt.meta || noop;
|
||||
|
||||
img.on('metadata', function(meta) {
|
||||
onmeta(meta);
|
||||
});
|
||||
|
||||
img.on('parsed', function(data) {
|
||||
ondone(data);
|
||||
});
|
||||
|
||||
img.on('error', function(err) {
|
||||
onerror(err);
|
||||
});
|
||||
|
||||
img.parse(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function noop() {}
|
||||
250
src2/load/stl.js
Normal file
250
src2/load/stl.js
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
'use strict';
|
||||
|
||||
// Polyfill for appendAll if not present
|
||||
if (!Array.prototype.appendAll) {
|
||||
Object.defineProperty(Array.prototype, 'appendAll', {
|
||||
value: function(arr) {
|
||||
for (let v of arr) this.push(v);
|
||||
},
|
||||
enumerable: false
|
||||
});
|
||||
}
|
||||
|
||||
export class STL {
|
||||
constructor() {
|
||||
this.vertices = null;
|
||||
this.normals = null;
|
||||
this.colors = null;
|
||||
}
|
||||
load(url, callback, formdata, scale, credentials, headers) {
|
||||
const stl = this;
|
||||
|
||||
fetch(url, {
|
||||
method: formdata ? 'POST' : 'GET',
|
||||
credentials: credentials ? credentials : 'same-origin',
|
||||
body: formdata,
|
||||
...(headers ? {headers: headers} : {})
|
||||
}).then(response => {
|
||||
if (response.status === 200 || response.status === 0) {
|
||||
response.arrayBuffer().then(buffer => {
|
||||
stl.parse(buffer, scale);
|
||||
|
||||
let cdhVal = response.headers.get(CDH);
|
||||
if (typeof cdhVal === "string") {
|
||||
cdhVal = cdhVal.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, cdhVal);
|
||||
});
|
||||
} else {
|
||||
if (callback) callback(null, response.statusText);
|
||||
}
|
||||
}).catch(err => {
|
||||
if (callback) callback(null, err)
|
||||
});
|
||||
}
|
||||
encode(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;
|
||||
}
|
||||
parse(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);
|
||||
}
|
||||
parseAsync(data, scale) {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(this.parse(data, scale));
|
||||
});
|
||||
}
|
||||
parseBinary(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;
|
||||
}
|
||||
parseASCII(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<vertices.length; i++) vToFloat32[i] = vertices[i];
|
||||
for (i=0; i<normals.length; i++) nToFloat32[i] = normals[i];
|
||||
|
||||
this.vertices = vToFloat32;
|
||||
this.normals = nToFloat32;
|
||||
|
||||
return vToFloat32;
|
||||
}
|
||||
convertToString(buf) {
|
||||
if (typeof buf !== "string") {
|
||||
let array_buffer = new Uint8Array(buf);
|
||||
let str = '';
|
||||
for (let i = 0; i < buf.byteLength; i++) {
|
||||
str += String.fromCharCode(array_buffer[i]);
|
||||
}
|
||||
return str;
|
||||
} else {
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
convertToBinary(buf) {
|
||||
if (typeof buf === "string") {
|
||||
let array_buffer = new Uint8Array(buf.length);
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
array_buffer[i] = buf.charCodeAt(i) & 0xff;
|
||||
}
|
||||
return array_buffer.buffer || array_buffer;
|
||||
} else {
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src2/load/svg.js
Normal file
78
src2/load/svg.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { newPolygon } from '../geo/polygon.js';
|
||||
import { polygons } from '../geo/polygons.js';
|
||||
import { THREE } from '../ext/three.js';
|
||||
|
||||
export function parseAsync(text, opt) {
|
||||
return new Promise((resolve,reject) => {
|
||||
resolve(parse(text, opt));
|
||||
});
|
||||
}
|
||||
|
||||
export function parse(text, opt = { }) {
|
||||
const justPoly = opt.flat || false;
|
||||
const fromSoup = opt.soup !== false || justPoly;
|
||||
const rez = (opt.resolution || 1);
|
||||
const dpi = (opt.dpi || 0);
|
||||
const segmin = Math.max(1, opt.segmin || 10);
|
||||
const data = new THREE.SVGLoader().parse(text);
|
||||
const paths = data.paths;
|
||||
const xmlat = data.xml.attributes;
|
||||
const objs = [];
|
||||
const polys = [];
|
||||
const isinch = xmlat.width?.value.endsWith('in');
|
||||
const scale = isinch ? 25.4 : (dpi ? 1 / (dpi / 25.4) : 1);
|
||||
const depth = parseFloat(opt.depth || xmlat['data-km-extrude']?.value
|
||||
|| xmlat['extrude']?.value
|
||||
|| 5);
|
||||
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
let path = paths[i];
|
||||
let shapes = path.toShapes(true);
|
||||
let type = path.userData?.node?.nodeName;
|
||||
let width = path.userData?.style?.strokeWidth;
|
||||
let miter = path.userData?.style?.strokeMiterLimit;
|
||||
for (let sub of path.subPaths) {
|
||||
let points = sub.curves.map(curve => {
|
||||
let length = curve.getLength();
|
||||
let segs = curve.type === 'LineCurve' ?
|
||||
1 : Math.max(Math.ceil(length * rez), segmin);
|
||||
return curve.getPoints(segs);
|
||||
}).flat();
|
||||
if (points.length < 3) {
|
||||
continue;
|
||||
}
|
||||
let poly = newPolygon().addPoints(points.map(p => newPolygon.newPoint(p.x, -p.y, 0)));
|
||||
if (poly.appearsClosed()) poly.points.pop();
|
||||
if (type === 'polyline') poly.setOpen(true);
|
||||
poly._svg = { width, miter };
|
||||
polys.push(poly);
|
||||
if (scale !== 1) {
|
||||
poly.scale({ x: scale, y: scale, z: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sub = fromSoup ? polygons.nest(polys) : polys;
|
||||
const nest = sub.filter(p => {
|
||||
for (let pc of polys) {
|
||||
if (pc === p) {
|
||||
return true;
|
||||
} else {
|
||||
return !pc.isEquivalent(p);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (justPoly) {
|
||||
return nest;
|
||||
}
|
||||
|
||||
for (let poly of nest) {
|
||||
let obj = poly.extrude(depth);
|
||||
objs.push(obj);
|
||||
}
|
||||
|
||||
return objs;
|
||||
}
|
||||
81
src2/load/url.js
Normal file
81
src2/load/url.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
'use strict';
|
||||
|
||||
import { STL } from './stl.js';
|
||||
import { OBJ } from './obj.js';
|
||||
import { TMF } from './3mf.js';
|
||||
import { SVG } from './svg.js';
|
||||
|
||||
const CDH = 'Content-Disposition';
|
||||
|
||||
export 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 STL().parse(data), file
|
||||
}]);
|
||||
break;
|
||||
case "obj":
|
||||
resolve(OBJ.parse(data).map(m => { return {mesh: m.toFloat32(), file} }));
|
||||
break;
|
||||
case "3mf":
|
||||
TMF.parseAsync(data).then((meshes) => {
|
||||
resolve(meshes.map(m => {
|
||||
return { mesh: m.faces.toFloat32(), file }
|
||||
}));
|
||||
});
|
||||
break;
|
||||
case "svg":
|
||||
resolve(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);
|
||||
});
|
||||
}
|
||||
775
src2/main/mesh.js
Normal file
775
src2/main/mesh.js
Normal file
|
|
@ -0,0 +1,775 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import '../add/array.js';
|
||||
import '../add/class.js';
|
||||
import '../add/three.js';
|
||||
import '../mesh/build.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { broker as motoBroker } from '../moto/broker.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { open as dataOpen } from '../data/index.js';
|
||||
import { api as meshApi } from '../mesh/api.js';
|
||||
import { split as meshSplit } from '../mesh/split.js';
|
||||
import { model as meshModel, materials } from '../mesh/model.js';
|
||||
import { edges as meshEdges } from '../mesh/edges.js';
|
||||
import { load as fileLoad } from '../load/file.js';
|
||||
import { $, $d, estop } from '../moto/webui.js';
|
||||
import { THREE } from '../ext/three.js';
|
||||
|
||||
const { Quaternion } = THREE;
|
||||
const version = '1.5.6';
|
||||
const api = meshApi;
|
||||
const call = motoBroker.send;
|
||||
const dbindex = [ "admin", "space" ];
|
||||
|
||||
function log() {
|
||||
return meshApi.log.emit(...arguments);
|
||||
}
|
||||
|
||||
// set below. called once the DOM readyState = complete
|
||||
// this is the main() entrypoint called after all dependents load
|
||||
function init() {
|
||||
let stores = dataOpen('mesh', { stores: dbindex, version: 4 }).init(),
|
||||
dark = false,
|
||||
ortho = false,
|
||||
zoomrev = true,
|
||||
zoomspd = 1,
|
||||
platform = motoSpace.platform,
|
||||
db = meshApi.db = {
|
||||
admin: stores.promise('admin'),
|
||||
space: stores.promise('space')
|
||||
};
|
||||
|
||||
// mark init time and use count
|
||||
db.admin.put("init", Date.now());
|
||||
db.admin.get("uses").then(v => db.admin.put("uses", (v||0) + 1));
|
||||
|
||||
// setup default workspace
|
||||
motoSpace.setAntiAlias(true);
|
||||
motoSpace.useDefaultKeys(false);
|
||||
motoSpace.init($('container'), delta => { }, ortho);
|
||||
motoSpace.sky.set({
|
||||
grid: false,
|
||||
color: dark ? 0 : 0xffffff
|
||||
});
|
||||
platform.set({
|
||||
volume: false,
|
||||
round: false,
|
||||
zOffset: 0,
|
||||
opacity: 0,
|
||||
color: 0xdddddd,
|
||||
zoom: { reverse: true, speed: 1 },
|
||||
size: { width: 2000, depth: 2000, height: 0.05, maxz: 2000 },
|
||||
grid: { major: 25, minor: 5, zOffset: 0,
|
||||
colorMajor: 0xcccccc, colorMinor: 0xeeeeee,
|
||||
colorX: 0xff7777, colorY: 0x7777ff },
|
||||
});
|
||||
platform.onMove(() => {
|
||||
// save last location and focus
|
||||
db.admin.put('camera', {
|
||||
place: motoSpace.view.save(),
|
||||
focus: motoSpace.view.getFocus()
|
||||
});
|
||||
}, 100);
|
||||
motoSpace.view.setZoom(zoomrev, zoomspd);
|
||||
|
||||
// reload stored space when worker is ready
|
||||
motoClient.on('ready', restore_space);
|
||||
|
||||
// start worker
|
||||
motoClient.start(`/code/mesh_work?${version}`);
|
||||
|
||||
// trigger space event binding
|
||||
call.space_init({ space: motoSpace, platform });
|
||||
|
||||
// trigger ui building
|
||||
call.ui_build();
|
||||
}
|
||||
|
||||
// restore space layout and view from previous session
|
||||
async function restore_space() {
|
||||
const api = meshApi;
|
||||
const space = motoSpace;
|
||||
const db_admin = meshApi.db.admin;
|
||||
const db_space = meshApi.db.space;
|
||||
// let mcache = {};
|
||||
await db_admin.get("camera")
|
||||
.then(saved => {
|
||||
if (saved) {
|
||||
space.view.load(saved.place);
|
||||
space.view.setFocus(saved.focus);
|
||||
}
|
||||
});
|
||||
const mcache = await db_admin.get("meta") || {};
|
||||
let count = 0;
|
||||
await db_space.iterate({ map: true }).then(cached => {
|
||||
const keys = [];
|
||||
const claimed = [];
|
||||
for (let [id, data] of Object.entries(cached)) {
|
||||
// console.log({ id, data });
|
||||
keys.push(id);
|
||||
if (count++ === 0) {
|
||||
log(`restoring workspace`);
|
||||
}
|
||||
// restore object based on type
|
||||
// group arrays load models they contain
|
||||
// sketches are loaded by type since they're not grouped
|
||||
if (Array.isArray(data)) {
|
||||
claimed.push(id);
|
||||
let models = data
|
||||
.map(id => {
|
||||
claimed.push(id);
|
||||
return { id, md: cached[id] }
|
||||
})
|
||||
.filter(r => r.md) // filter cache misses
|
||||
.map(r => new meshModel(r.md, r.id).applyMeta(mcache[r.id]))
|
||||
if (models.length) {
|
||||
log(`restored ${models.length} model(s)`);
|
||||
api.group.new(models, id).applyMeta(mcache[id])
|
||||
} else {
|
||||
log(`removed empty group ${id}`);
|
||||
db_space.remove(id);
|
||||
}
|
||||
} else if (data.type === 'sketch') {
|
||||
claimed.push(id);
|
||||
api.add.sketch({ id, ...data });
|
||||
}
|
||||
}
|
||||
for (let id of claimed) {
|
||||
keys.remove(id);
|
||||
}
|
||||
if (keys.length) {
|
||||
log(`removing ${keys.length} unclaimed meshes`);
|
||||
}
|
||||
// clear out meshes left in the space db along with their meta-data
|
||||
for (let id of keys) {
|
||||
db_space.remove(id);
|
||||
delete mcache[id];
|
||||
}
|
||||
// restore global cache only after objects are restored
|
||||
// otherwise their setup will corrupt the cache for other restores
|
||||
metaCache = mcache;
|
||||
store_meta();
|
||||
}).then(() => {
|
||||
// restore preferences after models are restored
|
||||
return api.prefs.load().then(() => {
|
||||
let { map } = api.prefs;
|
||||
let { space, mode } = map;
|
||||
api.grid(space.grid);
|
||||
// restore selected state
|
||||
let selist = space.select || [];
|
||||
let smodel = api.model.list().filter(m => selist.contains(m.id));
|
||||
let sgroup = api.group.list().filter(m => selist.contains(m.id));
|
||||
let tolist = space.tools || [];
|
||||
let tmodel = api.model.list().filter(m => tolist.contains(m.id));
|
||||
let tgroup = api.group.list().filter(m => tolist.contains(m.id));
|
||||
let sklist = api.sketch.list().filter(s => selist.contains(s.id));
|
||||
api.selection.set([...smodel, ...sgroup, ...sklist], [...tmodel, ...tgroup]);
|
||||
// restore edit mode
|
||||
api.mode[mode]();
|
||||
// restore dark mode
|
||||
set_darkmode(map.space.dark);
|
||||
});
|
||||
}).finally(() => {
|
||||
// hide loading curtain
|
||||
$d('curtain','none');
|
||||
if (api.prefs.map.info.welcome !== false) {
|
||||
api.welcome(version);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// add space event bindings
|
||||
function space_init(data) {
|
||||
let platcolor = 0x00ff00;
|
||||
let { space, platform } = data;
|
||||
let { selection } = meshApi;
|
||||
// add file drop handler
|
||||
space.event.addHandlers(self, [
|
||||
'drop', (evt) => {
|
||||
estop(evt);
|
||||
platform.set({ opacity: 0, color: platcolor });
|
||||
call.load_files([...evt.dataTransfer.files]);
|
||||
},
|
||||
'dragover', evt => {
|
||||
estop(evt);
|
||||
evt.dataTransfer.dropEffect = 'copy';
|
||||
let color = platform.setColor(0x00ff00);
|
||||
if (color !== 0x00ff00) platcolor = color;
|
||||
platform.set({ opacity: 0.1 });
|
||||
},
|
||||
'dragleave', evt => {
|
||||
platform.set({ opacity: 0, color: platcolor });
|
||||
},
|
||||
'keypress', evt => {
|
||||
if (api.modal.showing) {
|
||||
return;
|
||||
}
|
||||
if (evt.key === '?') {
|
||||
return api.welcome(version);
|
||||
}
|
||||
let { shiftKey, metaKey, ctrlKey, code } = evt;
|
||||
switch (code) {
|
||||
case 'Digit1':
|
||||
return api.mode.sketch();
|
||||
case 'Digit2':
|
||||
return api.mode.object();
|
||||
case 'Digit3':
|
||||
return api.mode.tool();
|
||||
case 'Digit4':
|
||||
return api.mode.surface();
|
||||
case 'Digit5':
|
||||
return api.mode.face();
|
||||
case 'Digit6':
|
||||
return api.mode.edge();
|
||||
case 'KeyQ':
|
||||
return api.settings();
|
||||
case 'KeyI':
|
||||
return shiftKey ? api.tool.invert() : api.file.import();
|
||||
case 'KeyX':
|
||||
return api.file.export();
|
||||
case 'KeyD':
|
||||
return shiftKey && api.tool.duplicate();
|
||||
case 'KeyC':
|
||||
return selection.centerXY().focus();
|
||||
case 'KeyF':
|
||||
return shiftKey ? selection.focus() : selection.floor().focus();
|
||||
case 'KeyM':
|
||||
return shiftKey ? api.tool.merge() : api.tool.mirror();
|
||||
case 'KeyU':
|
||||
return shiftKey && api.tool.union();
|
||||
case 'KeyA':
|
||||
if (api.mode.is([ api.modes.edge ])) return meshEdges.add();
|
||||
return shiftKey && api.tool.analyze();
|
||||
case 'KeyE':
|
||||
if (api.mode.is([ api.modes.sketch ])) {
|
||||
estop(evt);
|
||||
return api.sketch.extrude();
|
||||
}
|
||||
return;
|
||||
case 'KeyV':
|
||||
return shiftKey ? selection.show() : selection.focus();
|
||||
case 'KeyN':
|
||||
return shiftKey ? estop(evt, api.tool.rename()) : api.normals();
|
||||
case 'KeyW':
|
||||
return api.wireframe();
|
||||
case 'KeyG':
|
||||
return shiftKey ?
|
||||
(api.mode.is([ api.modes.sketch ]) ? api.sketch.arrange.group() : api.tool.regroup()) :
|
||||
api.grid();
|
||||
case 'KeyL':
|
||||
return api.log.toggle({ spinner: false });
|
||||
case 'KeyS':
|
||||
if (!api.mode.is([ api.modes.object ])) return;
|
||||
return shiftKey ? selection.visible({toggle:true}) : meshSplit.start();
|
||||
case 'KeyB':
|
||||
return selection.boundsBox({toggle:true});
|
||||
case 'KeyH':
|
||||
return shiftKey ? selection.hide() : space.view.home();
|
||||
case 'KeyT':
|
||||
return shiftKey ? api.tool.triangulate() : space.view.top();
|
||||
case 'KeyZ':
|
||||
return space.view.reset();
|
||||
}
|
||||
},
|
||||
'keydown', evt => {
|
||||
let { shiftKey, metaKey, ctrlKey, code } = evt;
|
||||
let once = keyOnce[code];
|
||||
if (once) {
|
||||
delete keyOnce[code];
|
||||
return once(evt);
|
||||
}
|
||||
let rv = (Math.PI / 12);
|
||||
if (api.modal.showing) {
|
||||
if (code === 'Escape') {
|
||||
api.modal.cancel();
|
||||
}
|
||||
return;
|
||||
}
|
||||
let rot, floor = api.prefs.map.space.floor !== false;
|
||||
switch (code) {
|
||||
case 'KeyA':
|
||||
if (metaKey || ctrlKey) {
|
||||
if (!selection.sketch()?.selection.all()) {
|
||||
api.mode.object();
|
||||
selection.set(api.group.list());
|
||||
}
|
||||
estop(evt);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
if (selection.clear()) {
|
||||
meshEdges.clear();
|
||||
meshSplit.end();
|
||||
}
|
||||
estop(evt);
|
||||
break;
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
let mode = api.mode.get();
|
||||
if ([api.modes.object, api.modes.tool, api.modes.sketch].contains(mode)) {
|
||||
selection.delete();
|
||||
} else {
|
||||
for (let m of api.model.list()) {
|
||||
m.deleteSelections();
|
||||
space.refresh()
|
||||
}
|
||||
}
|
||||
estop(evt);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
rot = selection.rotate(-rv,0,0);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
rot = selection.rotate(rv,0,0);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
if (shiftKey) {
|
||||
rot = selection.rotate(0,-rv,0);
|
||||
} else {
|
||||
floor = false;
|
||||
rot = selection.rotate(0,0,rv);
|
||||
}
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
if (shiftKey) {
|
||||
rot = selection.rotate(0,rv,0);
|
||||
} else {
|
||||
floor = false;
|
||||
rot = selection.rotate(0,0,-rv);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (rot && floor) {
|
||||
rot.floor();
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
// mouse hover/click handlers. required to enable model drag in space.js
|
||||
// called two ways:
|
||||
// without args to return a list of selectable targets
|
||||
// with args when a selection is made
|
||||
space.mouse.downSelect((int, event, ints) => {
|
||||
let obj = int?.object;
|
||||
if (obj) api.selection.drag({ start: int.object });
|
||||
if (obj) obj.sketch_item?.sketch.drag({ start: int.object });
|
||||
if (event?.shiftKey) {
|
||||
return api.objects();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// called two ways:
|
||||
// without args to return a list of selectable targets
|
||||
// with args when a selection is made
|
||||
space.mouse.upSelect((int, event, ints) => {
|
||||
if (ints?.length > 1) {
|
||||
// ensure sketch items are returned before sketch plane / handles
|
||||
let els = ints.filter(i => i.object.sketch_item);
|
||||
int = els.length ? els[0] : int;
|
||||
}
|
||||
if (event?.target?.nodeName === "CANVAS") {
|
||||
// a selection was made
|
||||
const { model, sketch, sketch_item } = int?.object || {};
|
||||
const { altKey, ctrlKey, metaKey, shiftKey } = event;
|
||||
if (meshSplit.active()) {
|
||||
return meshSplit.select(model);
|
||||
}
|
||||
if (model) {
|
||||
const group = model.group;
|
||||
if (metaKey) {
|
||||
// set focus on intersected face
|
||||
const { x, y, z } = int.point;
|
||||
const q = new Quaternion().setFromRotationMatrix(group.object.matrix);
|
||||
// rotate normal using group's matrix
|
||||
const normal = shiftKey ? int.face.normal.applyQuaternion(q) : undefined;
|
||||
// y,z swap due to world rotation for orbit controls
|
||||
api.focus({center: { x, y:-z, z:y }, normal});
|
||||
let one = api.sketch.selected.one;
|
||||
if (one && confirm('attach sketch to face?')) {
|
||||
one.center = {
|
||||
x, y: -z, z: y
|
||||
};
|
||||
one.normal = {
|
||||
x: normal.x,
|
||||
y: normal.y,
|
||||
z: normal.z
|
||||
};
|
||||
one.render();
|
||||
}
|
||||
} else if (ctrlKey) {
|
||||
// rotate selected face towawrd z "floor"
|
||||
group.rotateTowardZ(int.face.normal);
|
||||
selection.update();
|
||||
} else {
|
||||
const { modes } = api;
|
||||
const { surface } = api.prefs.map;
|
||||
const opt = { radians: 0, radius: surface.radius };
|
||||
const mode = api.mode.get();
|
||||
switch(mode) {
|
||||
case modes.sketch:
|
||||
api.mode.object();
|
||||
case modes.object:
|
||||
case modes.tool:
|
||||
selection.toggle(shiftKey ? model : model.group, mode === modes.tool);
|
||||
break;
|
||||
case modes.surface:
|
||||
opt.radians = surface.radians;
|
||||
case modes.face:
|
||||
// find faces adjacent to point/line clicked
|
||||
model.find(int,
|
||||
altKey ? { toggle: true } :
|
||||
shiftKey ? { clear: true } : { select: true },
|
||||
opt);
|
||||
break;
|
||||
case modes.edge:
|
||||
meshEdges.select();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (sketch) {
|
||||
if (metaKey) {
|
||||
// set focus on intersected face
|
||||
const { x, y, z } = int.point;
|
||||
// rotate normal using group's matrix
|
||||
const normal = shiftKey ? int.face.normal.clone(): undefined;
|
||||
// ransform the normal to align with sketch group
|
||||
normal?.transformDirection(int.object.sketch.group.matrix);
|
||||
// y,z swap due to world rotation for orbit controls
|
||||
api.focus({ center: { x, y:-z, z:y }, normal });
|
||||
} else {
|
||||
selection.toggle(sketch);
|
||||
}
|
||||
} else if (sketch_item) {
|
||||
sketch_item.toggle();
|
||||
}
|
||||
} else {
|
||||
// return objects upSelect can choose from
|
||||
let visible = api.objects().filter(o => o.visible);
|
||||
let sketches = api.selection.sketches();
|
||||
if (sketches.length) {
|
||||
// when sketch selected, append sketch items, too
|
||||
visible.appendAll(sketches[0].object.children);
|
||||
};
|
||||
return visible;
|
||||
}
|
||||
});
|
||||
|
||||
space.mouse.onDrag((delta, offset, up = false) => {
|
||||
let { mode, modes } = api;
|
||||
if (delta) {
|
||||
if (delta.event?.shiftKey && selection.count()) {
|
||||
selection.drag({ delta, offset });
|
||||
}
|
||||
} else if (up) {
|
||||
if (selection.count()) {
|
||||
selection.drag({ end: true });
|
||||
}
|
||||
} else if (mode.is([ modes.object, modes.tool, modes.sketch ])) {
|
||||
// return true if there are draggable elements
|
||||
return api.objects().length > 0 || api.selection.sketches().length;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function load_files(files) {
|
||||
log(`loading file...`);
|
||||
let api = meshApi;
|
||||
let has_image = false;
|
||||
let has_svg = false;
|
||||
let has_gbr = false;
|
||||
let sketch = api.sketch.selected.one;
|
||||
for (let file of files) {
|
||||
has_image = has_image || file.type === 'image/png';
|
||||
has_svg = has_svg || file.name.toLowerCase().endsWith(".svg") > 0;
|
||||
has_gbr = has_gbr || file.name.toLowerCase().endsWith(".gbr") > 0;
|
||||
}
|
||||
if (sketch && has_gbr) {
|
||||
fileLoad.load([...files], { flat: true }).then(layers => {
|
||||
for (let layer of layers.flat()) {
|
||||
let { circs, closed, open, rects } = layer;
|
||||
open = open.map(poly => {
|
||||
const diam = poly.tool?.shape?.diameter;
|
||||
return diam ? poly.offset_open(diam / 2, 'round') : null;
|
||||
}).filter(p => p).flat();
|
||||
for (let set of [ closed, open, circs, rects ]) {
|
||||
let group = meshApi.util.uuid();
|
||||
for (let poly of set) {
|
||||
sketch.add.polygon({ poly, group });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else
|
||||
if (sketch && has_svg) {
|
||||
fileLoad.load([...files], { flat: true })
|
||||
.then(polys => polys.forEach(set => {
|
||||
let group = meshApi.util.uuid();
|
||||
set.forEach(poly => sketch.add.polygon({ poly, group }))
|
||||
}))
|
||||
.catch(error => dbug.error(error))
|
||||
.finally(() => meshApi.log.hide());
|
||||
} else
|
||||
if (has_svg) {
|
||||
api.modal.dialog({
|
||||
title: `svg import`,
|
||||
body: [ h.div({ class: "image-import" }, [
|
||||
h.div([
|
||||
h.label("extrude"),
|
||||
h.input({ id: "extrude_height", value: 5, size: 4 })
|
||||
]),
|
||||
h.div([
|
||||
h.label("repair"),
|
||||
h.input({ id: "svg_repair", type: "checkbox", checked: true })
|
||||
]),
|
||||
h.div([
|
||||
h.button({ _: "import", onclick() {
|
||||
let { svg_repair, extrude_height } = api.modal.bound;
|
||||
load_files_opt(files, {
|
||||
soup: svg_repair.checked,
|
||||
depth: parseFloat(extrude_height.value)
|
||||
});
|
||||
api.modal.hide();
|
||||
} }),
|
||||
])
|
||||
]) ]
|
||||
});
|
||||
} else
|
||||
if (has_image) {
|
||||
api.modal.dialog({
|
||||
title: `image import`,
|
||||
body: [ h.div({ class: "image-import" }, [
|
||||
h.div([
|
||||
h.label("invert pixels"),
|
||||
h.input({ id: "inv_image", type: "checkbox" })
|
||||
]),
|
||||
h.div([
|
||||
h.label("invert alpha"),
|
||||
h.input({ id: "inv_alpha", type: "checkbox" })
|
||||
]),
|
||||
h.div([
|
||||
h.label("border size"),
|
||||
h.input({ id: "img_border", value: 0, size: 4 })
|
||||
]),
|
||||
h.div([
|
||||
h.label("blur pixels"),
|
||||
h.input({ id: "img_blur", value: 0, size: 4 })
|
||||
]),
|
||||
h.div([
|
||||
h.label("base pixels"),
|
||||
h.input({ id: "img_base", value: 0, size: 4 })
|
||||
]),
|
||||
h.div([
|
||||
h.button({ _: "import", onclick() {
|
||||
let { inv_image, inv_alpha, img_border, img_blur, img_base } = api.modal.bound;
|
||||
load_files_opt(files, {
|
||||
inv_image: inv_image.checked,
|
||||
inv_alpha: inv_alpha.checked,
|
||||
border: parseInt(img_border.value || 0),
|
||||
blur: parseInt(img_blur.value || 0),
|
||||
base: parseInt(img_base.value || 0),
|
||||
});
|
||||
api.modal.hide();
|
||||
} }),
|
||||
])
|
||||
]) ]
|
||||
});
|
||||
} else {
|
||||
load_files_opt(files);
|
||||
}
|
||||
}
|
||||
|
||||
function load_files_opt(files, opt) {
|
||||
return fileLoad.load([...files], opt)
|
||||
.then(data => call.space_load(data))
|
||||
.catch(error => log(error).pin({}) && dbug.error(error))
|
||||
// .finally(() => meshApi.log.hide());
|
||||
}
|
||||
|
||||
// add object loader
|
||||
function space_load(data) {
|
||||
if (data && data.length && (data = data.flat()).length)
|
||||
meshApi.group.new(data.map(el => new meshModel(el)))
|
||||
.promote()
|
||||
.focus();
|
||||
}
|
||||
|
||||
let metaCache = {};
|
||||
let keyOnce = {};
|
||||
|
||||
function key_once(data) {
|
||||
const { code, fn } = data;
|
||||
keyOnce[code] = fn;
|
||||
}
|
||||
|
||||
function key_once_cancel(code) {
|
||||
delete keyOnce[code];
|
||||
}
|
||||
|
||||
function store_meta() {
|
||||
meshApi.db.admin.put("meta", metaCache);
|
||||
}
|
||||
|
||||
function update_meta(id, data) {
|
||||
let meta = metaCache[id] = metaCache[id] || {};
|
||||
Object.assign(meta, data);
|
||||
store_meta();
|
||||
}
|
||||
|
||||
// cache model visibility for page restores
|
||||
function object_visible(data) {
|
||||
let { id, visible } = data;
|
||||
update_meta(id, { visible });
|
||||
}
|
||||
|
||||
// cache model matrices for page restores
|
||||
function object_meta(data) {
|
||||
let { id, meta } = data;
|
||||
update_meta(id, meta);
|
||||
}
|
||||
|
||||
function object_destroy(id) {
|
||||
delete metaCache[id];
|
||||
store_meta();
|
||||
}
|
||||
|
||||
// listen for changes like dark mode toggle
|
||||
function set_darkmode(dark) {
|
||||
let { prefs, model } = meshApi;
|
||||
let { sky, platform } = motoSpace;
|
||||
prefs.map.space.dark = dark;
|
||||
if (dark) {
|
||||
materials.wireframe.color.set(0xaaaaaa);
|
||||
materials.wireline.color.set(0xaaaaaa);
|
||||
$('app').classList.add('dark');
|
||||
} else {
|
||||
materials.wireframe.color.set(0,0,0);
|
||||
materials.wireline.color.set(0,0,0);
|
||||
$('app').classList.remove('dark');
|
||||
}
|
||||
sky.set({
|
||||
color: dark ? 0 : 0xffffff,
|
||||
ambient: { intensity: dark ? 0.55 : 1.1 }
|
||||
});
|
||||
platform.set({
|
||||
light: dark ? 0.08 : 0.08,
|
||||
grid: dark ? {
|
||||
colorMajor: 0x666666,
|
||||
colorMinor: 0x333333,
|
||||
} : {
|
||||
colorMajor: 0xcccccc,
|
||||
colorMinor: 0xeeeeee,
|
||||
},
|
||||
});
|
||||
meshApi.updateFog();
|
||||
platform.setSize();
|
||||
for (let m of model.list()) {
|
||||
m.normals({ refresh: true });
|
||||
}
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_normals_length(length) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.normals.length = length || 1;
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_normals_color(color) {
|
||||
let { prefs } = meshApi;
|
||||
let { map } = prefs;
|
||||
if (map.space.dark) {
|
||||
map.normals.color_dark = color || 0;
|
||||
} else {
|
||||
map.normals.color_lite = color || 0;
|
||||
}
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_surface_radians(radians) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.surface.radians = parseFloat(radians || 0.1);
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_surface_radius(radius) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.surface.radius = parseFloat(radius || 0.2);
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_wireframe_opacity(opacity) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.wireframe.opacity = parseFloat(opacity || 0.15);
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_wireframe_fog(fogx) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.wireframe.fog = parseFloat(fogx || 3);
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
function set_snap_value(snap) {
|
||||
let { prefs } = meshApi;
|
||||
prefs.map.space.snap = parseFloat(snap || 1);
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
// bind functions to topics
|
||||
motoBroker.listeners({
|
||||
key_once,
|
||||
key_once_cancel,
|
||||
load_files,
|
||||
object_meta,
|
||||
object_destroy,
|
||||
object_visible,
|
||||
space_init,
|
||||
space_load,
|
||||
set_darkmode,
|
||||
set_normals_color,
|
||||
set_normals_length,
|
||||
set_surface_radians,
|
||||
set_surface_radius,
|
||||
set_wireframe_opacity,
|
||||
set_wireframe_fog,
|
||||
set_snap_value
|
||||
});
|
||||
|
||||
// remove version cache bust from url
|
||||
window.history.replaceState({},'','/v2/mesh/');
|
||||
|
||||
// setup init() trigger when dom + scripts complete
|
||||
document.onreadystatechange = function() {
|
||||
if (document.readyState === 'complete') {
|
||||
init();
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
version,
|
||||
init,
|
||||
log,
|
||||
restore_space,
|
||||
space_init,
|
||||
load_files,
|
||||
load_files_opt,
|
||||
space_load,
|
||||
key_once,
|
||||
key_once_cancel,
|
||||
store_meta,
|
||||
update_meta,
|
||||
object_visible,
|
||||
object_meta,
|
||||
object_destroy,
|
||||
set_darkmode,
|
||||
set_normals_length,
|
||||
set_normals_color,
|
||||
set_surface_radians,
|
||||
set_surface_radius,
|
||||
set_wireframe_opacity,
|
||||
set_wireframe_fog,
|
||||
set_snap_value
|
||||
};
|
||||
35
src2/main/service.js
Normal file
35
src2/main/service.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const service_worker = navigator.serviceWorker;
|
||||
|
||||
import { license } from '../moto/license.js';
|
||||
|
||||
async function start_service_worker() {
|
||||
const version = license.version;
|
||||
function debug() {
|
||||
console.log(`[${version}]`, ...arguments);
|
||||
}
|
||||
|
||||
// install service worker
|
||||
debug('service worker registration');
|
||||
|
||||
try {
|
||||
// const reg = await navigator.serviceWorker.register("/src/moto/service.js?013", { scope: "/" });
|
||||
const reg = await navigator.serviceWorker.register("/code/service.js?"+version, { scope: "/" });
|
||||
if (reg.installing) {
|
||||
debug('service worker installing');
|
||||
} else if (reg.waiting) {
|
||||
debug('service worker waiting');
|
||||
} else if (reg.active) {
|
||||
debug('service worker active');
|
||||
} else {
|
||||
debug({ service_worker: reg });
|
||||
}
|
||||
|
||||
if (service_worker.controller) {
|
||||
service_worker.controller.postMessage('ctrl message');
|
||||
}
|
||||
} catch (err) {
|
||||
debug('service worker registration failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (service_worker && self.enable_service) start_service_worker();
|
||||
1624
src2/mesh/api.js
Normal file
1624
src2/mesh/api.js
Normal file
File diff suppressed because it is too large
Load diff
1034
src2/mesh/build.js
Normal file
1034
src2/mesh/build.js
Normal file
File diff suppressed because it is too large
Load diff
217
src2/mesh/edges.js
Normal file
217
src2/mesh/edges.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* facet edge selecetor. allows for creating new facets
|
||||
* when two connected edges share a single point.
|
||||
*/
|
||||
|
||||
import { THREE, Line2, LineSegmentsGeometry, LineSegments2, LineGeometry, LineMaterial } from '../ext/three.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { newPoint } from '../geo/point.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
|
||||
const { Vector3, Group } = THREE;
|
||||
|
||||
let isActive;
|
||||
let hovered;
|
||||
let selected = [];
|
||||
|
||||
function line2(verts) {
|
||||
let geo = new LineSegmentsGeometry();
|
||||
let segs = new LineSegments2(geo, new LineMaterial({
|
||||
linewidth: 4,
|
||||
color: 0x0088ff,
|
||||
alphaToCoverage: false,
|
||||
}));
|
||||
geo.setPositions(verts);
|
||||
return segs;
|
||||
}
|
||||
|
||||
// hovered edge object
|
||||
let geo, mat, obj = new Line2(
|
||||
geo = new LineGeometry(),
|
||||
mat = new LineMaterial({
|
||||
linewidth: 6,
|
||||
color: 0x88ff00,
|
||||
alphaToCoverage: false,
|
||||
})
|
||||
);
|
||||
|
||||
// true if values differ by less than 10e-3
|
||||
function cmp(v1, v2) {
|
||||
return Math.abs(v1 - v2) < 0.001;
|
||||
}
|
||||
|
||||
// points equal if all axis values are within tolerance of each other
|
||||
function eq(p1, p2) {
|
||||
return
|
||||
cmp(p1.x, p2.x) &&
|
||||
cmp(p1.y, p2.y) &&
|
||||
cmp(p1.z, p2.z);
|
||||
}
|
||||
|
||||
// split functions
|
||||
const edges = {
|
||||
active() {
|
||||
return isActive ? true : false;
|
||||
},
|
||||
|
||||
start() {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { api } = meshApi;
|
||||
|
||||
isActive = true;
|
||||
selected.length = 0;
|
||||
obj.visible = false;
|
||||
|
||||
motoSpace.scene.add(obj);
|
||||
|
||||
// enable temp mode
|
||||
let state = edges.state = { obj };
|
||||
let models = state.models = api.selection.models();
|
||||
let meshes = models.map(m => m.mesh);
|
||||
|
||||
// find closest facet edge to hover location
|
||||
motoSpace.mouse.onHover((int, event, ints) => {
|
||||
if (!event) {
|
||||
obj.visible = false;
|
||||
return meshes;
|
||||
}
|
||||
if (event.buttons) {
|
||||
return;
|
||||
}
|
||||
|
||||
obj.visible = true;
|
||||
|
||||
let { point, face, object } = int;
|
||||
let side = edges.closest_edge(point, face, object);
|
||||
let { p0, p1 } = side;
|
||||
let verts = side.verts = [ p0.x, -p0.z, p0.y, p1.x, -p1.z, p1.y ];
|
||||
geo.setPositions(verts);
|
||||
hovered = { object, face, side };
|
||||
});
|
||||
isActive = true;
|
||||
},
|
||||
|
||||
end() {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
let space = motoSpace;
|
||||
space.scene.remove(obj);
|
||||
space.scene.remove(edges.selected);
|
||||
space.mouse.onHover(undefined);
|
||||
isActive = edges.state = undefined;
|
||||
meshApi.api.selection.update();
|
||||
},
|
||||
|
||||
async add() {
|
||||
if (selected.length === 2 && selected[0].object === selected[1].object) {
|
||||
let model = selected[0].object.model;
|
||||
let pos = new Vector3().fromArray(model.meta.pos);
|
||||
let verts = selected.map(s => [ s.side.p0, s.side.p1 ]).flat();
|
||||
// translate into mesh space
|
||||
for (let v of verts) {
|
||||
v.x = (v.x - pos.x);
|
||||
v.y = (v.y - pos.z);
|
||||
v.z = (v.z + pos.y);
|
||||
}
|
||||
// sort points so dups show up next to each other
|
||||
let sort = verts.sort((a,b) => {
|
||||
if (a.x !== b.x) {
|
||||
return a.x - b.x;
|
||||
} else if (a.y !== b.y) {
|
||||
return a.y - b.y;
|
||||
} else {
|
||||
return a.z - b.z;
|
||||
}
|
||||
}).filter((point, index, arr) => {
|
||||
// allow first point then compare others to one before
|
||||
return index === 0 || !eq(point, arr[index - 1]);
|
||||
});
|
||||
if (sort.length === 3) {
|
||||
await model.duplicate({ append: [
|
||||
sort[0].x, -sort[0].z, sort[0].y,
|
||||
sort[1].x, -sort[1].z, sort[1].y,
|
||||
sort[2].x, -sort[2].z, sort[2].y,
|
||||
]});
|
||||
model.remove();
|
||||
} else if (verts.length === 4) {
|
||||
await model.duplicate({ append: [
|
||||
verts[0].x, -verts[0].z, verts[0].y,
|
||||
verts[1].x, -verts[1].z, verts[1].y,
|
||||
verts[2].x, -verts[2].z, verts[2].y,
|
||||
verts[2].x, -verts[2].z, verts[2].y,
|
||||
verts[1].x, -verts[1].z, verts[1].y,
|
||||
verts[3].x, -verts[3].z, verts[3].y,
|
||||
]});
|
||||
model.remove();
|
||||
}
|
||||
// refresh object target cache
|
||||
edges.end();
|
||||
edges.start();
|
||||
}
|
||||
},
|
||||
|
||||
clear() {
|
||||
selected.length = 0;
|
||||
motoSpace.scene.remove(edges.selected);
|
||||
},
|
||||
|
||||
select() {
|
||||
motoSpace.scene.remove(edges.selected);
|
||||
if (!hovered) return;
|
||||
|
||||
const { object, face, side } = hovered;
|
||||
let add = true;
|
||||
for (let i=0; i<selected.length; i++) {
|
||||
let s = selected[i];
|
||||
let { p0, p1 } = s.side;
|
||||
let sp = newPoint(
|
||||
(p0.x + p1.x) / 2,
|
||||
(p0.y + p1.y) / 2,
|
||||
(p0.z + p1.z) / 2,
|
||||
);
|
||||
let tp = newPoint(
|
||||
(side.p0.x + side.p1.x) / 2,
|
||||
(side.p0.y + side.p1.y) / 2,
|
||||
(side.p0.z + side.p1.z) / 2,
|
||||
);
|
||||
// because two faces can share edges that don't match
|
||||
// we compare midpoints of the line segments for a match
|
||||
let pd = sp.distTo3D(tp);
|
||||
if (s.object === object && Math.abs(pd) < 0.0001) {
|
||||
selected.splice(i,1);
|
||||
add = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (add) {
|
||||
selected.push(hovered);
|
||||
}
|
||||
|
||||
const group = edges.selected = new Group();
|
||||
motoSpace.scene.add(edges.selected);
|
||||
let verts = selected.map(s => s.side.verts).flat();
|
||||
group.add(line2(verts));
|
||||
},
|
||||
|
||||
closest_edge(point, face, object) {
|
||||
let { position } = object.geometry.attributes;
|
||||
let matrix = object.matrixWorld;
|
||||
point = newPoint(point.x, point.y, point.z);
|
||||
let v0 = new Vector3(position.getX(face.a), position.getY(face.a), position.getZ(face.a)).applyMatrix4(matrix);
|
||||
let v1 = new Vector3(position.getX(face.b), position.getY(face.b), position.getZ(face.b)).applyMatrix4(matrix);
|
||||
let v2 = new Vector3(position.getX(face.c), position.getY(face.c), position.getZ(face.c)).applyMatrix4(matrix);
|
||||
let l0 = { p0:v0, p1:v1, d:point.distToLine3D(v0, v1), v:[face.a,face.b].sort() };
|
||||
let l1 = { p0:v1, p1:v2, d:point.distToLine3D(v1, v2), v:[face.b,face.c].sort() };
|
||||
let l2 = { p0:v2, p1:v0, d:point.distToLine3D(v2, v0), v:[face.c,face.a].sort() };
|
||||
return [ l0, l1, l2 ].sort((a,b) => a.d - b.d)[0];
|
||||
}
|
||||
};
|
||||
|
||||
export default edges;
|
||||
export { edges };
|
||||
92
src2/mesh/geom.js
Normal file
92
src2/mesh/geom.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Matrix4, Matrix3, Vector3, Box3 } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
|
||||
// geometry helper functions
|
||||
const geom = {
|
||||
// given a closed polyline as an expanded point array
|
||||
// return enclosed area with sign indicating winding order
|
||||
areaSigned(points, axes = 3) {
|
||||
let area = 0;
|
||||
let len = points.length;
|
||||
for (let p1, p2, pi = 0; pi < len; ) {
|
||||
p1 = points.slice(pi, pi + axes);
|
||||
pi += axes;
|
||||
p2 = pi < len - axes ?
|
||||
points.slice(pi, pi + axes) :
|
||||
points.slice(0, axes);
|
||||
area += (p2[0] - p1[0]) * (p2[1] + p1[1]);
|
||||
}
|
||||
return area / 2;
|
||||
},
|
||||
|
||||
// returns an unsigned area generally more useful for comparison
|
||||
area(points) {
|
||||
return Math.abs(geom.areaSigned(points));
|
||||
},
|
||||
|
||||
// given an expanded point array, return bounds
|
||||
bounds(points, axes = 3) {
|
||||
let box = new Box3();
|
||||
let p3 = new Vector3();
|
||||
for (let p, pi = 0, len = points.length; pi < len; pi += axes) {
|
||||
box.expandByPoint(p3.fromArray(points, pi));
|
||||
}
|
||||
return box;
|
||||
},
|
||||
|
||||
// given two nest loop records, determine if
|
||||
// inner is completely inside outer without overlap
|
||||
isInside(outer, inner) {
|
||||
return outer.bounds.containsBox(inner.bounds);
|
||||
},
|
||||
|
||||
// given an array of closed polyline arrays, determine parentage
|
||||
nest(loops) {
|
||||
// create record per loop
|
||||
let recs = loops.map(points => {
|
||||
return {
|
||||
points,
|
||||
area: 0,
|
||||
depth: 0,
|
||||
inner: [],
|
||||
bounds: geom.bounds(points)
|
||||
}
|
||||
});
|
||||
|
||||
// sort loops by area ascending and cache loop area
|
||||
recs = recs.sort((a,b) => {
|
||||
a.area = a.area || geom.areaSigned(a.points);
|
||||
b.area = b.area || geom.areaSigned(b.points);
|
||||
return Math.abs(a.area) - Math.abs(b.area);
|
||||
});
|
||||
|
||||
// deep nesting
|
||||
let count = recs.length;
|
||||
outer: for (let i = 0; i < count - 1; i++) {
|
||||
let child = recs[i];
|
||||
for (let j = i + 1; j < count; j++) {
|
||||
let parent = recs[j];
|
||||
if (geom.isInside(parent, child)) {
|
||||
child.parent = parent;
|
||||
parent.inner.push(child);
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculate inner depth (steps from topmost)
|
||||
// let inners = recs.filter(r => r.parent);
|
||||
for (let rec of recs) {
|
||||
for (let p = rec.parent, d = 0; p ; p = p.parent) {
|
||||
rec.depth++;
|
||||
}
|
||||
}
|
||||
|
||||
return recs;
|
||||
}
|
||||
};
|
||||
|
||||
export { geom };
|
||||
170
src2/mesh/group.js
Normal file
170
src2/mesh/group.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Group, Box3 } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
import { broker as motoBroker } from '../moto/broker.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { object as meshObject } from './object.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { model as meshModel } from './model.js';
|
||||
|
||||
const call = motoBroker.send;
|
||||
const worker = motoClient.fn;
|
||||
|
||||
class MeshGroup extends meshObject {
|
||||
// @param group {MeshModel[]}
|
||||
constructor(models = [], id, name) {
|
||||
super(id);
|
||||
this.group3 = new Group();
|
||||
this.models = [];
|
||||
this.name = name;
|
||||
for (let model of models) {
|
||||
this.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "group";
|
||||
}
|
||||
|
||||
get object() {
|
||||
return this.group3;
|
||||
}
|
||||
|
||||
get bounds() {
|
||||
let box3 = new Box3();
|
||||
for (let model of this.models) {
|
||||
box3.union(model.world_bounds);
|
||||
}
|
||||
return box3;
|
||||
}
|
||||
|
||||
// @param model {MeshModel}
|
||||
add(model) {
|
||||
model.group = this;
|
||||
this.models.addOnce(model);
|
||||
this.group3.add(model.mesh);
|
||||
motoSpace.update();
|
||||
call.model_add({model, group:this});
|
||||
worker.group_add({id: this.id, model:model.id});
|
||||
// update data store
|
||||
meshApi.db.space.put(this.id, this.models.map(m => m.id));
|
||||
return this;
|
||||
}
|
||||
|
||||
// @param model {MeshModel}
|
||||
// @param free {boolean} prevent cleanup of model so it can be re-used
|
||||
remove(model, opt = { free: true }) {
|
||||
// remove all models and group
|
||||
if (arguments.length === 0) {
|
||||
for (let m of this.models.slice()) {
|
||||
this.remove(m);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.models.remove(model);
|
||||
this.group3.remove(model.mesh);
|
||||
// create message for listeners
|
||||
call.model_remove({model, group:this});
|
||||
// trigger sync with worker
|
||||
worker.group_remove({id: this.id, model: model.id});
|
||||
// ensure worker cleanup of model
|
||||
if (opt.free) model.remove(true);
|
||||
// auto-remove group when empty
|
||||
if (this.group3.children.length === 0) {
|
||||
meshApi.group.remove(this);
|
||||
// manage lifecycle with worker, mesh app caches, etc
|
||||
this.destroy();
|
||||
} else {
|
||||
// update data store
|
||||
meshApi.db.space.put(this.id, this.models.map(m => m.id));
|
||||
}
|
||||
motoSpace.update();
|
||||
return this;
|
||||
}
|
||||
|
||||
promote() {
|
||||
let { dim, mid } = this.bounds;
|
||||
let { x, y, z } = dim;
|
||||
let max = Math.max(x, y, z);
|
||||
if (max < 2) {
|
||||
meshApi.log.emit(`auto-scaling import from ${max.round(5)}`);
|
||||
this.scale(1000, 1000, 1000);
|
||||
}
|
||||
let { center, floor } = meshApi.prefs.map.space;
|
||||
if (center !== false) this.centerXY();
|
||||
if (floor !== false) this.floor();
|
||||
return this;
|
||||
}
|
||||
|
||||
rotate(x = 0, y = 0, z = 0) {
|
||||
this.log('group-rotate', ...arguments);
|
||||
for (let model of this.models) {
|
||||
model.rotate(...arguments);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
qrotate(quaternion) {
|
||||
this.log('group-rotate', quaternion.toArray());
|
||||
for (let model of this.models) {
|
||||
model.qrotate(quaternion);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
scale(x = 1, y = 1, z = 1) {
|
||||
this.log('group-scale', ...arguments);
|
||||
for (let m of this.models) {
|
||||
m.scale(x, y, z);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
move(x = 0, y = 0, z = 0) {
|
||||
this.log('group-move', ...arguments);
|
||||
if (!(x || y || z)) return;
|
||||
for (let model of this.models) {
|
||||
model.move(x, y, z);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
center() {
|
||||
let b = this.bounds;
|
||||
return this.move(-b.mid.x, -b.mid.y, -b.mid.z);
|
||||
}
|
||||
|
||||
centerXY() {
|
||||
let b = this.bounds;
|
||||
return this.move(-b.mid.x, -b.mid.y, 0);
|
||||
}
|
||||
|
||||
opacity() {
|
||||
for (let model of this.models) {
|
||||
model.opacity(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
wireframe() {
|
||||
for (let model of this.models) {
|
||||
model.wireframe(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
normals() {
|
||||
for (let model of this.models) {
|
||||
model.normals(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
select() {
|
||||
for (let model of this.models) {
|
||||
model.select(...arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MeshGroup as group };
|
||||
802
src2/mesh/model.js
Normal file
802
src2/mesh/model.js
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { object as meshObject } from './object.js';
|
||||
import { model as meshModel } from './model.js';
|
||||
import { newPolygon, polygons } from '../geo/polygon.js';
|
||||
import { space } from '../moto/space.js';
|
||||
import { util } from './util.js';
|
||||
|
||||
const { MeshPhongMaterial, MeshBasicMaterial, LineBasicMaterial, BufferGeometry, BufferAttribute, DoubleSide, Mesh, Box3, Vector3, Triangle } = THREE;
|
||||
const worker = motoClient.fn;
|
||||
const zero = new Vector3(0,0,0);
|
||||
|
||||
/** default materials **/
|
||||
const materials = {
|
||||
// model unselected
|
||||
normal: new MeshPhongMaterial({
|
||||
flatShading: true,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
shininess: 125,
|
||||
specular: 0x202020,
|
||||
color: 0xf0f000,
|
||||
opacity: 1
|
||||
}),
|
||||
// model selected
|
||||
select: new MeshPhongMaterial({
|
||||
flatShading: true,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
shininess: 125,
|
||||
specular: 0x202020,
|
||||
color: 0x00e000,
|
||||
opacity: 1
|
||||
}),
|
||||
// model selected as tool
|
||||
tool: new MeshPhongMaterial({
|
||||
flatShading: true,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
shininess: 125,
|
||||
specular: 0x202020,
|
||||
color: 0xe00000,
|
||||
opacity: 1
|
||||
}),
|
||||
// face selected (for groups ranges)
|
||||
face: new MeshPhongMaterial({
|
||||
flatShading: true,
|
||||
side: DoubleSide,
|
||||
transparent: true,
|
||||
shininess: 100,
|
||||
specular: 0x202020,
|
||||
color: 0x0088ee,
|
||||
opacity: 1
|
||||
}),
|
||||
wireframe: new MeshBasicMaterial({
|
||||
side: DoubleSide,
|
||||
wireframe: true,
|
||||
color: 0x0,
|
||||
transparent: true,
|
||||
opacity: 0.5
|
||||
}),
|
||||
wireline: new LineBasicMaterial({
|
||||
side: DoubleSide,
|
||||
color: 0x0,
|
||||
transparent: true,
|
||||
opacity: 0.5
|
||||
}),
|
||||
};
|
||||
|
||||
/** 3D model rendered on plaform **/
|
||||
class MeshModel extends meshObject {
|
||||
constructor(data, id) {
|
||||
super(id);
|
||||
let { file, mesh, vertices } = data;
|
||||
|
||||
if (!mesh) {
|
||||
dbug.error(`'${file}' missing mesh data`);
|
||||
return;
|
||||
}
|
||||
|
||||
// remove file name extensions
|
||||
let text = file || '';
|
||||
let dot = text.lastIndexOf('.');
|
||||
if (dot > 0) file = text.substring(0, dot);
|
||||
|
||||
// create local materials
|
||||
this.mats = {
|
||||
normal: materials.normal.clone(),
|
||||
select: materials.select.clone(),
|
||||
tool: materials.tool.clone(),
|
||||
face: materials.face.clone()
|
||||
};
|
||||
|
||||
// information about selected faces, lines, and vertices
|
||||
// vertex compound keys are sorted least index to greatest
|
||||
this.sel = {
|
||||
faces: [], // first index into vertices
|
||||
lines: {}, // key = vertex-vertex, val = [ faces ]
|
||||
verts: {}, // key = x-y-z, val = { faces:[], sphere }
|
||||
};
|
||||
|
||||
this.file = file || 'unnamed';
|
||||
this.load(mesh || vertices);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "model";
|
||||
}
|
||||
|
||||
get object() {
|
||||
return this.mesh;
|
||||
}
|
||||
|
||||
get bounds() {
|
||||
return this.geometry.boundingBox.clone();
|
||||
}
|
||||
|
||||
get world_bounds() {
|
||||
return this.bounds.translate(this.position());
|
||||
}
|
||||
|
||||
get positions() {
|
||||
return this.geometry.attributes.position.array;
|
||||
}
|
||||
|
||||
get matrixWorld() {
|
||||
return this.mesh.matrixWorld;
|
||||
}
|
||||
|
||||
drag(opt = {}) {
|
||||
let { start, delta, offset, end } = opt;
|
||||
let { snap, snapon } = meshApi.prefs.map.space;
|
||||
if (start) {
|
||||
let mid = this.bounds.mid;
|
||||
this._save = {
|
||||
pos: Object.assign({}, mid),
|
||||
start: Object.assign({}, mid),
|
||||
}
|
||||
} else if (end) {
|
||||
delete this._save;
|
||||
} else if (offset) {
|
||||
let { pos, start } = this._save;
|
||||
let target = {
|
||||
x: start.x + offset.x,
|
||||
y: start.y + offset.y,
|
||||
z: start.z + offset.z
|
||||
};
|
||||
if (snapon && snap) {
|
||||
target.x = Math.round(target.x / snap) * snap;
|
||||
target.y = Math.round(target.y / snap) * snap;
|
||||
}
|
||||
delta = {
|
||||
x: target.x - pos.x,
|
||||
y: target.y - pos.y,
|
||||
z: target.z - pos.z
|
||||
};
|
||||
pos.x += delta.x;
|
||||
pos.y += delta.y;
|
||||
pos.z += delta.z;
|
||||
}
|
||||
if (delta) {
|
||||
this.move(delta.x, delta.y, delta.z);
|
||||
}
|
||||
}
|
||||
|
||||
qrotate(quaternion) {
|
||||
this.log('model-rotate', quaternion.toArray());
|
||||
this.geometry.applyQuaternion(quaternion);
|
||||
this.geometry._model_invalid = true;
|
||||
this.updateBounds();
|
||||
}
|
||||
|
||||
scale(x = 1, y = 1, z = 1) {
|
||||
if (x === 1 && y === 1 && z === 1) return;
|
||||
this.log('model-scale', ...arguments, this.bounds);
|
||||
this.geometry.scale(x, y, z);
|
||||
this.geometry._model_invalid = true;
|
||||
this.updateBounds();
|
||||
}
|
||||
|
||||
translate(x = 0, y = 0, z = 0) {
|
||||
if (!(x || y || z)) return;
|
||||
this.log('model-translate', ...arguments);
|
||||
this.geometry.translate(x, y, z);
|
||||
this.geometry._model_invalid = true;
|
||||
this.updateBounds();
|
||||
}
|
||||
|
||||
move(x = 0, y = 0, z = 0) {
|
||||
if (!(x || y || z)) return;
|
||||
this.log('model-move', ...arguments);
|
||||
let pos = this.position();
|
||||
return this.position(pos.x + x, pos.y + y, pos.z + z);
|
||||
}
|
||||
|
||||
position() {
|
||||
let pos = this.object.position;
|
||||
if (arguments.length === 0) {
|
||||
return pos;
|
||||
}
|
||||
pos.set(...arguments);
|
||||
this.metaChanged({ pos: pos.toArray() });
|
||||
return this;
|
||||
}
|
||||
|
||||
// preserves world location while updating mesh for rotation and scaling
|
||||
// moves mesh center to 0,0,0 via translation then
|
||||
// moves mesh object to former bounds center
|
||||
reCenter() {
|
||||
this.log('model-recenter');
|
||||
let pos = this.position();
|
||||
let { mid } = this.bounds;
|
||||
this.translate(-mid.x, -mid.y, -mid.z);
|
||||
this.position(mid.x + pos.x, mid.y + pos.y, mid.z + pos.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
// preserves world location while updating mesh for rotation and scaling
|
||||
// moves mesh center to current target offset via translation then
|
||||
// moves mesh object center to target
|
||||
centerTo(to) {
|
||||
this.log('model-centerto', to);
|
||||
let pos = this.position();
|
||||
let { mid } = this.bounds;
|
||||
this.translate(to.x - mid.x, to.y - mid.y, to.z - mid.z);
|
||||
this.position(pos.x, pos.y, pos.z);
|
||||
return this;
|
||||
}
|
||||
|
||||
// when geometry updates, recompute bounds for ray intersections
|
||||
// and sync data to worker and indexed db
|
||||
updateBounds() {
|
||||
if (this._wire)
|
||||
this._wire.geometry.attributes.position.needsUpdate = true;
|
||||
this.attributes.position.needsUpdate = true;
|
||||
this.geometry.computeBoundingBox();
|
||||
this.geometry.computeBoundingSphere();
|
||||
this.log('update-bounds', this.geometry.boundingBox);
|
||||
this.normals({ refresh: true });
|
||||
this.updateBoundsBox();
|
||||
space.update();
|
||||
this.sync();
|
||||
}
|
||||
|
||||
updateBoundsBox() {
|
||||
this.group?.updateBoundsBox();
|
||||
}
|
||||
|
||||
mirror() {
|
||||
return this.duplicate({ mirror: true });
|
||||
}
|
||||
|
||||
// return a model containing just this model
|
||||
// translated into world coordinates (rebuilt from rotation matrix)
|
||||
// defaults to returning a new model in a new group
|
||||
// options to mirror, re-use a group, or update model in-place
|
||||
duplicate(opt = { select: true }) {
|
||||
let pos = this.position();
|
||||
let data = this.attributes.position.clone().array;
|
||||
if (opt.append) data = [...data, ...opt.append].toFloat32();
|
||||
if (opt.x) return this.reload(data);
|
||||
let model = new meshModel({ file: `${this.file}`, mesh: data });
|
||||
let group = opt.group || meshApi.group.new();
|
||||
let bounds = this.group.bounds;
|
||||
group.add(model);
|
||||
model.position(pos.x, pos.y, pos.z);
|
||||
model.wireframe(this.wireframe());
|
||||
if (opt.select) {
|
||||
meshApi.selection.add(model);
|
||||
}
|
||||
if (opt.mirror) {
|
||||
group.move(0, 0, bounds.dim.z);
|
||||
} else if (opt.shift) {
|
||||
group.move(bounds.dim.x, 0, 0);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
load(vertices) {
|
||||
this.log('load');
|
||||
let geo = new BufferGeometry();
|
||||
geo.setAttribute('position', new BufferAttribute(vertices, 3));
|
||||
let meh = this.mesh = new Mesh(geo, [
|
||||
this.mats.normal,
|
||||
this.mats.face
|
||||
]);
|
||||
geo.addGroup(0, Infinity, 0);
|
||||
meh.receiveShadow = true;
|
||||
meh.castShadow = true;
|
||||
meh.renderOrder = 1;
|
||||
// sets fallback opacity for wireframe toggle
|
||||
this.opacity(1);
|
||||
// this ref allows clicks to be traced to models and groups
|
||||
meh.model = this;
|
||||
// sync worker, allows raycasting to work
|
||||
this.updateBounds();
|
||||
}
|
||||
|
||||
reload(vertices) {
|
||||
this.log('reload');
|
||||
let was = this.wireframe(false);
|
||||
let geo = this.mesh.geometry;
|
||||
geo.setAttribute('position', new BufferAttribute(vertices, 3));
|
||||
// signal util.box3expand that geometry changed
|
||||
geo._model_invalid = true;
|
||||
geo.computeVertexNormals();
|
||||
// restore wireframe state
|
||||
this.wireframe(was);
|
||||
// fixup normals
|
||||
this.normals({refresh: true});
|
||||
// sync worker, allows raycasting to work
|
||||
this.updateBounds();
|
||||
// re-gen face index in surface mode
|
||||
meshApi.mode.check();
|
||||
}
|
||||
|
||||
rename(file) {
|
||||
this.file = file;
|
||||
this.sync();
|
||||
}
|
||||
|
||||
// sync to worker and indexeddb for page restoration or worker ops
|
||||
sync() {
|
||||
// sync to worker
|
||||
worker.model_load({ id: this.id, name: this.file, vertices: this.positions });
|
||||
// persist in db so it can be restored on page load
|
||||
meshApi.db.space.put(this.id, {
|
||||
file: this.file,
|
||||
mesh: this.attributes.position.array
|
||||
});
|
||||
}
|
||||
|
||||
get group() {
|
||||
return this._group;
|
||||
}
|
||||
|
||||
set group(gv) {
|
||||
if (gv && this._group && this._group !== gv) {
|
||||
throw "models can only belong to one group";
|
||||
}
|
||||
this._group = gv;
|
||||
}
|
||||
|
||||
get geometry() {
|
||||
return this.mesh.geometry;
|
||||
}
|
||||
|
||||
get attributes() {
|
||||
return this.geometry.attributes;
|
||||
}
|
||||
|
||||
get vertices() {
|
||||
return this.attributes.position.count;
|
||||
}
|
||||
|
||||
get faces() {
|
||||
return this.vertices / 3;
|
||||
}
|
||||
|
||||
// get, set, or toggle selection of model (coloring)
|
||||
select(bool, stool) {
|
||||
const cmat = this.mesh.material[0];
|
||||
const { normal, select, tool } = this.mats;
|
||||
if (bool === undefined) {
|
||||
return cmat !== normal;
|
||||
}
|
||||
if (bool.toggle) {
|
||||
return this.select(!this.select(), cmat === tool);
|
||||
}
|
||||
this.mesh.material[0] = bool ? (stool ? tool : select) : normal;
|
||||
return bool;
|
||||
}
|
||||
|
||||
// return selected state
|
||||
selected() {
|
||||
return this.select();
|
||||
}
|
||||
|
||||
// toggle whether this is a boolean subtraction tool or not
|
||||
tool(bool) {
|
||||
if (bool === undefined) {
|
||||
return this.mesh.material[0] === this.mats.tool;
|
||||
}
|
||||
return this.select(this.selected(), bool);
|
||||
}
|
||||
|
||||
opacity(ov, opt = {}) {
|
||||
let mat = this.mesh.material;
|
||||
if (ov === undefined) {
|
||||
return mat[0].opacity;
|
||||
}
|
||||
if (ov.restore) {
|
||||
ov = this._op;
|
||||
} else if (ov.temp !== undefined) {
|
||||
ov = ov.temp;
|
||||
} else {
|
||||
this._op = ov;
|
||||
}
|
||||
for (let m of Object.values(this.mats)) {
|
||||
if (ov <= 0.0) {
|
||||
m.transparent = false;
|
||||
m.opacity = 1;
|
||||
m.visible = false;
|
||||
} else {
|
||||
m.transparent = true;
|
||||
m.opacity = ov;
|
||||
m.visible = true;
|
||||
}
|
||||
}
|
||||
space.update();
|
||||
}
|
||||
|
||||
highlight() {
|
||||
if (!this.wireframe().enabled) {
|
||||
this.opacity({temp: 0.5});
|
||||
}
|
||||
}
|
||||
|
||||
unhighlight() {
|
||||
if (!this.wireframe().enabled) {
|
||||
this.opacity({restore: true});
|
||||
}
|
||||
}
|
||||
|
||||
wireframe(bool, opt = {}) {
|
||||
let was = this._wire ? true : false;
|
||||
if (bool === undefined) {
|
||||
return was ? {
|
||||
enabled: true,
|
||||
opacity: this.opacity(),
|
||||
color: was ? this._wire.material.color : undefined,
|
||||
} : {
|
||||
enabled: false
|
||||
};
|
||||
}
|
||||
if (bool.toggle) {
|
||||
bool = !was;
|
||||
}
|
||||
// no change
|
||||
if (was === bool) {
|
||||
return was;
|
||||
}
|
||||
if (was) {
|
||||
this.mesh.remove(this._wire);
|
||||
this._wire = undefined;
|
||||
this.opacity({restore: true});
|
||||
}
|
||||
if (bool === 'edges') {
|
||||
let edges = new THREE.EdgesGeometry(this.mesh.geometry, 5);
|
||||
this._wire = new THREE.LineSegments(edges, materials.wireframe);
|
||||
this.mesh.add(this._wire);
|
||||
this.opacity({temp: opt.opacity || 0.15});
|
||||
} else if (bool) {
|
||||
this._wire = new Mesh(this.mesh.geometry.shallowClone(), materials.wireframe);
|
||||
this.mesh.add(this._wire);
|
||||
this.opacity({temp: opt.opacity || 0.15});
|
||||
}
|
||||
space.update();
|
||||
return was;
|
||||
}
|
||||
|
||||
normals(bool) {
|
||||
let was = this._norm ? true : false;
|
||||
if (bool === undefined) {
|
||||
return was;
|
||||
}
|
||||
if (bool.toggle) {
|
||||
bool = !was;
|
||||
}
|
||||
if (bool.refresh && !was) {
|
||||
return;
|
||||
}
|
||||
// no change
|
||||
if (was === bool) {
|
||||
return was;
|
||||
}
|
||||
if (was) {
|
||||
this.mesh.remove(this._norm);
|
||||
this._norm = undefined;
|
||||
}
|
||||
if (bool) {
|
||||
this.mesh.add(this._norm = util.faceNormals(this.mesh));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
// invert normals for entire mesh or selected faces depending on mode
|
||||
invert(mode) {
|
||||
let { modes } = meshApi;
|
||||
let geo = this.mesh.geometry;
|
||||
let pos = geo.attributes.position;
|
||||
let arr = pos.array;
|
||||
function swap(i) {
|
||||
let v1x = arr[i ];
|
||||
let v1y = arr[i+1];
|
||||
let v1z = arr[i+2];
|
||||
arr[i ] = arr[i+3];
|
||||
arr[i+1] = arr[i+4];
|
||||
arr[i+2] = arr[i+5];
|
||||
arr[i+3] = v1x;
|
||||
arr[i+4] = v1y;
|
||||
arr[i+5] = v1z;
|
||||
}
|
||||
switch (mode) {
|
||||
case modes.object:
|
||||
for (let i=0, l=arr.length; i<l; i += 9) {
|
||||
swap(i);
|
||||
}
|
||||
break;
|
||||
case modes.surface:
|
||||
case modes.face:
|
||||
for (let face of this.sel.faces) {
|
||||
swap(face * 9);
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.reload(arr);
|
||||
if (this._norm) this._norm.update();
|
||||
}
|
||||
|
||||
// split model along given plane
|
||||
split(plane) {
|
||||
// extract axes from plane and split when present (only z for now)
|
||||
let { z } = plane;
|
||||
let { id, group } = this;
|
||||
return new Promise((resolve,reject) => {
|
||||
worker.model_split({ id, z }).then(data => {
|
||||
let { o1, o2 } = data;
|
||||
if (!(o1 || o2)) {
|
||||
return resolve(group);
|
||||
}
|
||||
if (o1?.length)
|
||||
group.add(new meshModel({
|
||||
file: `${this.file}-bot`,
|
||||
mesh: o1
|
||||
}).reCenter());
|
||||
if (o2?.length)
|
||||
group.add(new meshModel({
|
||||
file: `${this.file}-top`,
|
||||
mesh: o2
|
||||
}).reCenter());
|
||||
this.remove();
|
||||
resolve(group);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// release from a group but remain in memory and storage
|
||||
// so it can be re-assigned to another group
|
||||
ungroup() {
|
||||
if (this.group) {
|
||||
this.group.remove(this, { free: false });
|
||||
this.group = undefined;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// remove model from group and space
|
||||
remove() {
|
||||
if (arguments.length === 0) {
|
||||
// direct call requires pass through group
|
||||
this.group.remove(this);
|
||||
this.group = undefined;
|
||||
this.removed = 'pending';
|
||||
} else {
|
||||
// manage lifecycle with worker, mesh app caches, etc
|
||||
this.destroy();
|
||||
// tag removed for debugging
|
||||
this.removed = 'complete';
|
||||
}
|
||||
}
|
||||
|
||||
collectFacesByMaterialIndex(index) {
|
||||
let { geometry } = this.mesh;
|
||||
let { groups } = geometry;
|
||||
let { array } = geometry.attributes.position;
|
||||
let newtot = 0;
|
||||
// filter to unselected groups
|
||||
groups = groups.filter(g => g.materialIndex === index);
|
||||
for (let group of groups) {
|
||||
let start = group.start * 3;
|
||||
let count = group.count * 3;
|
||||
newtot += group.count < Infinity ? count : array.length - start;
|
||||
}
|
||||
// nothing to do if new length is the same
|
||||
if (newtot === array.length) {
|
||||
return;
|
||||
}
|
||||
let newverts = new Float32Array(newtot);
|
||||
let pos = 0;
|
||||
// copy back unselected faces
|
||||
for (let group of groups) {
|
||||
let start = group.start * 3;
|
||||
let count = group.count * 3;
|
||||
if (count === Infinity) count = array.length - start;
|
||||
let slice = array.slice(start, start + count);
|
||||
newverts.set(slice, pos);
|
||||
pos += count;
|
||||
}
|
||||
return newverts;
|
||||
}
|
||||
|
||||
clearSelections() {
|
||||
// clear face selections (since they've been deleted);
|
||||
this.sel.faces = [];
|
||||
this.updateSelections();
|
||||
}
|
||||
|
||||
selectionToSketch() {
|
||||
let { polys, quaternion } = this.selectionToRotationPolys();
|
||||
if (!polys) {
|
||||
return;
|
||||
}
|
||||
quaternion.invert();
|
||||
// center points, move to Z=0
|
||||
let bounds = new Box3().setFromArray(polys.map(p => p.points.map(p => p.toArray())).flat().flat());
|
||||
let center = bounds.getCenter(new Vector3);
|
||||
polys.forEach(p => p.move({ x: -center.x, y: -center.y, z: -center.z }));
|
||||
let normal = new Vector3(0,0,1).applyQuaternion(quaternion);
|
||||
let sketch = meshApi.add.sketch({
|
||||
normal,
|
||||
center: center.applyQuaternion(quaternion).add(this.position())
|
||||
});
|
||||
for (let poly of polys) {
|
||||
sketch.add.polygon({ poly });
|
||||
}
|
||||
return sketch;
|
||||
}
|
||||
|
||||
selectionToRotationPolys() {
|
||||
let selverts = this.collectFacesByMaterialIndex(1);
|
||||
if (selverts?.length) {
|
||||
let points = [...selverts].group(3).map(a => new Vector3().fromArray(a));
|
||||
let tris = points.group(3).map(a => new Triangle(...a));
|
||||
let norms = tris.map(t => t.getNormal(new Vector3()));
|
||||
let norm = norms.reduce((a,b) => a.add(b)).normalize();
|
||||
// compute quaternion and rotate triangles to face Z up
|
||||
let targetNorm = new THREE.Vector3(0, 0, 1);
|
||||
let rotato = new THREE.Quaternion().setFromUnitVectors(norm, targetNorm);
|
||||
points.forEach(p => p.applyQuaternion(rotato));
|
||||
// union / nest result
|
||||
let polys = tris.map(t => newPolygon().fromVectors([ t.a, t.b, t.c ]));
|
||||
let union = polygons.union(polys,0,true);
|
||||
return { polys: union, quaternion: rotato };
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
selectionFlatten() {
|
||||
let faces = this.sel.faces;
|
||||
if (faces.length) {
|
||||
worker.model_flatten({ id: this.id, faces }).then(data => {
|
||||
if (data.vertices) {
|
||||
this.reload(data.vertices);
|
||||
this.sel.faces = [];
|
||||
this.updateSelections();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
triangulateSelections() {
|
||||
let { polys, quaternion } = this.selectionToRotationPolys();
|
||||
if (polys) {
|
||||
let ears = polys.map(p => p.earcut()).flat();
|
||||
let nupoints = ears.map(p => p.points).flat().map(p => p.toVector3());
|
||||
// invert quaternion to restore points alignment to original face
|
||||
quaternion.invert();
|
||||
nupoints.forEach(p => p.applyQuaternion(quaternion));
|
||||
let nuverts = nupoints.map(p => p.toArray()).flat().toFloat32();
|
||||
// remove selection and append nuverts
|
||||
this.deleteSelections(nuverts);
|
||||
}
|
||||
}
|
||||
|
||||
deleteSelections(append) {
|
||||
if (!this.sel.faces.length) {
|
||||
return;
|
||||
}
|
||||
let newverts = this.collectFacesByMaterialIndex(0);
|
||||
if (newverts) {
|
||||
if (append) {
|
||||
let all = new Float32Array(newverts.length + append.length);
|
||||
all.set(newverts);
|
||||
all.set(append, newverts.length);
|
||||
newverts = all;
|
||||
}
|
||||
this.reload(newverts);
|
||||
// clear face selections (since they've been deleted);
|
||||
this.sel.faces = [];
|
||||
this.updateSelections();
|
||||
}
|
||||
}
|
||||
|
||||
updateSelections() {
|
||||
let groups = util.facesToGroups(this.sel.faces);
|
||||
let geo = this.mesh.geometry;
|
||||
geo.clearGroups();
|
||||
for (let group of groups) {
|
||||
geo.addGroup(group.start*3, group.count*3, group.mat || 0);
|
||||
}
|
||||
}
|
||||
|
||||
selectFaces(list = [], action = {}) {
|
||||
let faces = this.sel.faces;
|
||||
let map = {};
|
||||
for (let f of list) {
|
||||
map[f] = f;
|
||||
}
|
||||
if (action.toggle) {
|
||||
faces = faces.filter(f => {
|
||||
if (map[f] !== undefined) {
|
||||
map[f] = undefined;
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
faces.appendAll(Object.entries(map).filter(kv => {
|
||||
return kv[1] !== undefined;
|
||||
}).map(kv => {
|
||||
return kv[1];
|
||||
}));
|
||||
this.sel.faces = faces;
|
||||
} else if (action.clear) {
|
||||
this.sel.faces = faces.filter(f => map[f] === undefined);
|
||||
} else if (action.select) {
|
||||
for (let f of faces) {
|
||||
map[f] = undefined;
|
||||
}
|
||||
faces.appendAll(Object.values(map).filter(f => f !== undefined));
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelectedVertices(vert) {
|
||||
if (Array.isArray(vert)) {
|
||||
for (let e of vert) {
|
||||
this.toggleSelectedVertices(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let sel = this.sel;
|
||||
let key = [x,y,z].map(v => v.round(5)).join('-');
|
||||
let rec = sel.verts[key];
|
||||
if (rec) {
|
||||
delete sel.verts[key];
|
||||
space.scene.remove(rec.sphere);
|
||||
} else {
|
||||
let geometry = new THREE.SphereGeometry( 0.5, 16, 16 );
|
||||
let material = new MeshPhongMaterial( { color: 0x777777, transparent: true, opacity: 0.25 } );
|
||||
let sphere = new Mesh( geometry, material );
|
||||
sphere.position.set(x, y, z);
|
||||
space.scene.add(sphere);
|
||||
sel.verts[key] = {
|
||||
sphere,
|
||||
faces,
|
||||
verts
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// find adjacent faces to clicked point/line on a face
|
||||
find(int, action, surface) {
|
||||
let { point, face } = int;
|
||||
let { x, y, z } = point;
|
||||
let { a, b, c } = face;
|
||||
let timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
meshApi.log.emit("matching surface").pin();
|
||||
}, 150);
|
||||
// let mark = Date.now();
|
||||
worker.model_select({
|
||||
id: this.id, x, y:-z, z:y, a, b, c, surface
|
||||
}).then(data => {
|
||||
// meshApi.log.emit(`... data time = ${Date.now() - mark}`); mark = Date.now();
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
let { faces, edges, verts, point } = data;
|
||||
// console.log({data});
|
||||
// this.toggleSelectedVertices(verts);
|
||||
this.selectFaces(faces, action);
|
||||
// meshApi.log.emit(`... select time = ${Date.now() - mark}`); mark = Date.now();
|
||||
this.updateSelections();
|
||||
if (!timer) {
|
||||
api.log.emit("surface match complete").unpin();
|
||||
space.refresh();
|
||||
}
|
||||
// meshApi.log.emit(`... paint time = ${Date.now() - mark}`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { MeshModel as model, materials };
|
||||
204
src2/mesh/object.js
Normal file
204
src2/mesh/object.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Matrix4, Vector3, Box3, Box3Helper, Quaternion } = THREE;
|
||||
import { broker as motoBroker } from '../moto/broker.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { util as meshUtil } from './util.js';
|
||||
|
||||
const worker = motoClient.fn;
|
||||
const lookUp = new Vector3(0,0,-1);
|
||||
|
||||
// broker updates generated by objects
|
||||
const publish = {
|
||||
meta: motoBroker.bind("object_meta"),
|
||||
destroy: motoBroker.bind("object_destroy"),
|
||||
visible: motoBroker.bind("object_visible"),
|
||||
};
|
||||
|
||||
class MeshObject {
|
||||
constructor(id) {
|
||||
this.id = id || meshUtil.uuid();
|
||||
this.meta = { pos: [0,0,0] };
|
||||
worker.object_create({ id: this.id, type: this.type });
|
||||
worker.object_meta({ id: this.id, meta: this.meta });
|
||||
this.log('NEW');
|
||||
}
|
||||
|
||||
log() {
|
||||
// meshApi.log.emit(this.id, this.type, ...arguments);
|
||||
}
|
||||
|
||||
get type() {
|
||||
throw "type() requires implementation";
|
||||
}
|
||||
|
||||
// @returns {THREE.Object3D}
|
||||
get object() {
|
||||
throw "object() requires implementation";
|
||||
}
|
||||
|
||||
get bounds() {
|
||||
throw "bounds() requires implementation";
|
||||
}
|
||||
|
||||
// manage lifecycle with worker, mesh app caches, etc
|
||||
destroy() {
|
||||
// update worker state
|
||||
worker.object_destroy({id: this.id});
|
||||
// update object store
|
||||
meshApi.db.space.remove(this.id);
|
||||
// main app cache workspace updates
|
||||
publish.destroy(this.id);
|
||||
}
|
||||
|
||||
// used during state restoration
|
||||
applyMeta(meta = {}) {
|
||||
this.log('apply-meta', meta);
|
||||
if (meta.pos) this.position(...meta.pos);
|
||||
this.visible(meta.visible ?? true);
|
||||
this.metaChanged(meta);
|
||||
return this;
|
||||
}
|
||||
|
||||
// used during state restoration
|
||||
applyMatrix(elements) {
|
||||
if (elements) {
|
||||
this.log('apply-matrix');
|
||||
this.object.applyMatrix4(new Matrix4().fromArray(elements));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// used during model splitting
|
||||
applyMatrix4(matrix) {
|
||||
return this.applyMatrix(matrix.elements);
|
||||
}
|
||||
|
||||
select() {
|
||||
throw "select() requires implementation";
|
||||
}
|
||||
|
||||
remove() {
|
||||
throw "remove() requires implementation";
|
||||
}
|
||||
|
||||
focus() {
|
||||
meshApi.focus(this.object);
|
||||
return this;
|
||||
}
|
||||
|
||||
visible(opt) {
|
||||
if (opt === undefined) {
|
||||
return this.object.visible;
|
||||
}
|
||||
if (opt.toggle) {
|
||||
this.visible(!this.visible());
|
||||
} else {
|
||||
this.object.visible = opt;
|
||||
}
|
||||
publish.visible({ id: this.id, visible: this.object.visible });
|
||||
return this;
|
||||
}
|
||||
|
||||
floor() {
|
||||
let b = this.bounds;
|
||||
return this.move(0, 0, -b.min.z);
|
||||
}
|
||||
|
||||
center() {
|
||||
throw "center() requires implementation";
|
||||
let b = this.bounds;
|
||||
return this.move(-b.mid.x, -b.mid.y, -b.mid.z);
|
||||
}
|
||||
|
||||
centerXY() {
|
||||
throw "centerXY() requires implementation";
|
||||
let b = this.bounds;
|
||||
return this.move(-b.mid.x, -b.mid.y, 0);
|
||||
}
|
||||
|
||||
move() {
|
||||
throw "move() requires implementation";
|
||||
}
|
||||
|
||||
scale() {
|
||||
throw "scale() requires implementation";
|
||||
}
|
||||
|
||||
qrotate(quaternion) {
|
||||
throw "qrotate() requires implementation";
|
||||
}
|
||||
|
||||
rotate(x = 0, y = 0, z = 0) {
|
||||
this.log('object-rotate', ...arguments);
|
||||
let m = new Matrix4();
|
||||
if (x) m.multiply(new Matrix4().makeRotationX(x));
|
||||
if (y) m.multiply(new Matrix4().makeRotationY(y));
|
||||
if (z) m.multiply(new Matrix4().makeRotationZ(z));
|
||||
this.qrotate(new Quaternion().setFromRotationMatrix(m));
|
||||
return this;
|
||||
}
|
||||
|
||||
rotation() {
|
||||
throw "rotation() requires implementation";
|
||||
}
|
||||
|
||||
// rotate object in the directon of normal (place face on Z plane)
|
||||
rotateTowardZ(normal) {
|
||||
let q = new Quaternion().setFromUnitVectors(normal, lookUp);
|
||||
this.qrotate(q);
|
||||
this.floor();
|
||||
}
|
||||
|
||||
position() {
|
||||
throw "position() requires implementation";
|
||||
}
|
||||
|
||||
showBounds(bool) {
|
||||
let was = this._showBounds;
|
||||
if (bool && bool.toggle) {
|
||||
bool = !this._showBounds;
|
||||
}
|
||||
if (was === bool) {
|
||||
return;
|
||||
}
|
||||
this._showBounds = bool;
|
||||
this.updateBoundsBox();
|
||||
}
|
||||
|
||||
updateBoundsBox() {
|
||||
this.log('update-bounds-box', this._showBounds);
|
||||
let helper = this._boundsBox;
|
||||
let world = motoSpace.world;
|
||||
if (helper) {
|
||||
world.remove(helper);
|
||||
}
|
||||
if (this._showBounds) {
|
||||
let { mid, dim } = this.bounds;
|
||||
let b3 = new Box3().setFromCenterAndSize(
|
||||
new Vector3(mid.x, mid.y, mid.z),
|
||||
new Vector3(dim.x, dim.y, dim.z)
|
||||
);
|
||||
let helper = this._boundsBox = new Box3Helper(b3, 0x555555);
|
||||
world.add(helper);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
metaChanged(values = {}) {
|
||||
this.object.updateMatrix();
|
||||
this.updateBoundsBox();
|
||||
motoSpace.update();
|
||||
Object.assign(this.meta, values, {
|
||||
// matrix: this.object.matrix.elements
|
||||
});
|
||||
worker.object_meta({ id: this.id, meta: this.meta });
|
||||
publish.meta({ id: this.id, meta: this.meta });
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export { MeshObject as object };
|
||||
7
src2/mesh/pool.js
Normal file
7
src2/mesh/pool.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
import { worker as motoWorker } from '../moto/worker.js';
|
||||
|
||||
// todo
|
||||
export const pool = {};
|
||||
641
src2/mesh/sketch.js
Normal file
641
src2/mesh/sketch.js
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { BufferGeometry, BufferAttribute, Quaternion, MeshBasicMaterial, LineBasicMaterial, LineSegments, DoubleSide, PlaneGeometry, EdgesGeometry, SphereGeometry, Vector3, Mesh, Group } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { broker as motoBroker } from '../moto/broker.js';
|
||||
import { object as meshObject } from './object.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { util as meshUtil } from './util.js';
|
||||
import { group as meshGroup } from './group.js';
|
||||
import { newPolygon } from '../geo/polygon.js';
|
||||
import { polygons as POLYS } from '../geo/polygons.js';
|
||||
|
||||
const worker = motoClient.fn;
|
||||
const drag = {};
|
||||
const hpos = [
|
||||
[-1, 1, 1],
|
||||
[ 1, 1, 1],
|
||||
[-1,-1, 1],
|
||||
[ 1,-1, 1],
|
||||
];
|
||||
|
||||
const material = {
|
||||
normal: new MeshBasicMaterial({ color: 0x888888, side: DoubleSide, transparent: true, opacity: 0.25 }),
|
||||
selected: new MeshBasicMaterial({ color: 0x889988, side: DoubleSide, transparent: true, opacity: 0.25 }),
|
||||
highlight: new LineBasicMaterial({ color: 0x88ff88, side: DoubleSide, transparent: true, opacity: 0.50 }),
|
||||
};
|
||||
|
||||
function log() {
|
||||
meshApi.log.emit(...arguments);
|
||||
}
|
||||
|
||||
/** 2D plane containing open and closed polygons which can be extruded **/
|
||||
class MeshSketch extends meshObject {
|
||||
constructor(opt = {}) {
|
||||
super(opt.id);
|
||||
|
||||
this.file = opt.file || this.id;
|
||||
this.scale = opt.scale || { x: 50, y: 50, z: 0 };
|
||||
this.center = opt.center || { x: 0, y: 0, z: 0 };
|
||||
this.normal = opt.normal || { x: 0, y: 0, z: 1 };
|
||||
this.items = opt.items || [];
|
||||
|
||||
const group = this.group = new Group();
|
||||
group.sketch = this;
|
||||
|
||||
const planeGeometry = new PlaneGeometry(1, 1);
|
||||
const planeMaterial = material.normal;
|
||||
const plane = this.plane = new Mesh(planeGeometry, planeMaterial);
|
||||
plane.sketch = this;
|
||||
|
||||
const outlineGeometry = new EdgesGeometry(planeGeometry);
|
||||
const outlineMaterial = material.normal;
|
||||
const outline = this.outline = new LineSegments(outlineGeometry, outlineMaterial);
|
||||
|
||||
const handleGeometry = new SphereGeometry(0.5, 16, 16);
|
||||
const handleMaterial = material.normal;
|
||||
|
||||
const handles = this.handles = [];
|
||||
const corners = this.corners = [ [-0.5, 0.5, 0], [0.5, 0.5, 0], [-0.5, -0.5, 0], [0.5, -0.5, 0] ];
|
||||
|
||||
for (let corner of corners) {
|
||||
const handle = new Mesh(handleGeometry, handleMaterial);
|
||||
handle.position.set(...corner);
|
||||
handle.sketch = this;
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
group.add(...handles, outline, plane);
|
||||
|
||||
this.render_defer();
|
||||
}
|
||||
|
||||
update() {
|
||||
let { group, plane, outline, center, handles, corners, normal, scale } = this;
|
||||
|
||||
plane.scale.set(scale.x, scale.y, 1);
|
||||
outline.scale.set(scale.x, scale.y, 1);
|
||||
group.position.set(center.x, center.y, center.z);
|
||||
|
||||
for (let i=0; i<4; i++) {
|
||||
const handle = handles[i];
|
||||
const corner = corners[i];
|
||||
handle.position.set(
|
||||
(corner[0] * scale.x),
|
||||
(corner[1] * scale.y),
|
||||
(corner[2] * scale.z)
|
||||
);
|
||||
}
|
||||
|
||||
normal = new Vector3(normal.x, normal.y, normal.z).normalize();
|
||||
group.quaternion.copy(new Quaternion().setFromUnitVectors(new Vector3(0, 0, 1), normal));
|
||||
|
||||
this.#db_save();
|
||||
}
|
||||
|
||||
lookat(x,y,z) {
|
||||
this.group.lookAt(new Vector3(x,y,z));
|
||||
motoSpace.update();
|
||||
}
|
||||
|
||||
#db_save() {
|
||||
let { center, normal, scale, type, file, items } = this;
|
||||
meshApi.db.space.put(this.id, { center, normal, scale, type, file, items });
|
||||
}
|
||||
|
||||
#db_remove() {
|
||||
meshApi.sketch.remove(this);
|
||||
meshApi.db.space.remove(this.id);
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "sketch";
|
||||
}
|
||||
|
||||
get object() {
|
||||
return this.group;
|
||||
}
|
||||
|
||||
get meshes() {
|
||||
return this.group.children.filter(c => {
|
||||
return c.sketch || (c.sketch_item && c.sketch_item.selected) ? c : undefined
|
||||
}).reverse();
|
||||
}
|
||||
|
||||
get elements() {
|
||||
return this.group.children.filter(c => c.sketch_item);
|
||||
}
|
||||
|
||||
get selection() {
|
||||
let sketch = this;
|
||||
return {
|
||||
// for boolean and patterning, we need the polygon artifacts
|
||||
mesh_items() {
|
||||
return sketch.group.children.filter(c => c.sketch_item && c.sketch_item.selected);
|
||||
},
|
||||
|
||||
items() {
|
||||
return sketch.selection.mesh_items().map(c => c.sketch_item);
|
||||
},
|
||||
|
||||
count() {
|
||||
return sketch.items.filter(i => i.selected).length;
|
||||
},
|
||||
|
||||
all() {
|
||||
sketch.items.forEach(i => i.selected = true);
|
||||
sketch.render();
|
||||
motoBroker.publish('sketch_selections');
|
||||
return sketch.items.length;
|
||||
},
|
||||
|
||||
// return true if any selections were cleared
|
||||
clear() {
|
||||
let sel = sketch.items.filter(i => i.selected);
|
||||
if (sel.length) {
|
||||
sel.forEach(s => s.selected = false);
|
||||
sketch.render();
|
||||
motoBroker.publish('sketch_selections');
|
||||
}
|
||||
return sel.length;
|
||||
},
|
||||
|
||||
// return true if any selections were deleted
|
||||
delete() {
|
||||
let sel = sketch.items.filter(i => i.selected);
|
||||
if (sel.length) {
|
||||
sketch.items = sketch.items.filter(i => !i.selected);
|
||||
sketch.render();
|
||||
motoBroker.publish('sketch_selections');
|
||||
}
|
||||
return sel.length;
|
||||
},
|
||||
|
||||
centerXY() {
|
||||
let sel = sketch.items.filter(i => i.selected);
|
||||
for (let s of sel) {
|
||||
s.center.x = 0;
|
||||
s.center.y = 0;
|
||||
}
|
||||
return sel.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get add() {
|
||||
let sketch = this;
|
||||
return {
|
||||
circle(opt = {}) {
|
||||
return sketch.add.item({
|
||||
type: "circle",
|
||||
selected: true,
|
||||
...Object.assign({}, { center: {x:0, y:0, z:0}, radius:5 }, opt)
|
||||
});
|
||||
},
|
||||
|
||||
rectangle(opt = {}) {
|
||||
return sketch.add.polygon({
|
||||
...opt,
|
||||
poly: newPolygon().centerRectangle(
|
||||
opt.center || {x:0, y:0, z:0},
|
||||
opt.height || 15,
|
||||
opt.width || 10
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
polygon(opt = {}) {
|
||||
let poly = opt.poly;
|
||||
delete opt.poly;
|
||||
let { width, miter } = poly._svg || {};
|
||||
return sketch.add.item({
|
||||
type: "polygon",
|
||||
width,
|
||||
miter,
|
||||
selected: true,
|
||||
...Object.assign({}, { center: { x:0, y:0, z:0 } }, opt),
|
||||
...poly.toObject()
|
||||
});
|
||||
},
|
||||
|
||||
item(item) {
|
||||
item.group = item.group || meshUtil.uuid();
|
||||
sketch.items.push(item);
|
||||
sketch.render_defer();
|
||||
meshApi.selection.update_defer();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get boolean() {
|
||||
let sketch = this;
|
||||
return {
|
||||
union() {
|
||||
let items = sketch.selection.mesh_items();
|
||||
if (items.length < 2) return log('operation requires at least 2 items');
|
||||
let polys = items.map(i => i.sketch_item.poly);
|
||||
let union = POLYS.union(polys, 0, true);
|
||||
sketch.selection.delete();
|
||||
sketch.arrange.group(union.map(poly => sketch.add.polygon({ poly })));
|
||||
},
|
||||
|
||||
intersect() {
|
||||
let items = sketch.selection.mesh_items();
|
||||
if (items.length !== 2) return log('operation requires 2 items');
|
||||
let polys = items.map(i => i.sketch_item.poly);
|
||||
let trim = POLYS.trimTo([polys[0]], [polys[1]]);
|
||||
sketch.selection.delete();
|
||||
for (let poly of trim) {
|
||||
sketch.add.polygon({ poly });
|
||||
}
|
||||
},
|
||||
|
||||
difference() {
|
||||
let items = sketch.selection.mesh_items();
|
||||
if (items.length !== 2) return log('operation requires 2 items');
|
||||
let polys = items.map(i => i.sketch_item.poly);
|
||||
let diff1 = POLYS.diff([polys[0]], [polys[1]]);
|
||||
let diff2 = POLYS.diff([polys[1]], [polys[0]]);
|
||||
sketch.selection.delete();
|
||||
for (let poly of [...diff1, ...diff2]) {
|
||||
sketch.add.polygon({ poly });
|
||||
}
|
||||
},
|
||||
|
||||
nest() {
|
||||
let items = sketch.selection.items();
|
||||
if (items.length < 2) return log('operation requires at least 2 items');
|
||||
let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group']));
|
||||
let union = POLYS.nest(POLYS.flatten(polys, [], true));
|
||||
sketch.selection.delete();
|
||||
for (let poly of union) {
|
||||
sketch.add.polygon({ poly, group:poly.group });
|
||||
}
|
||||
},
|
||||
|
||||
evenodd() {
|
||||
let items = sketch.selection.items();
|
||||
let polys = items.map(si => si.poly.annotate({group:si.group}));
|
||||
let even = POLYS.nest(POLYS.flatten(polys.clone(true,['group']), [], true));
|
||||
let odd = POLYS.nest(POLYS.flatten(even.clone(true,['group']), [], true).filter(p => p.depth > 0));
|
||||
sketch.selection.delete();
|
||||
for (let poly of [...even, ...odd]) {
|
||||
sketch.add.polygon({ poly, group:poly.group });
|
||||
}
|
||||
},
|
||||
|
||||
flatten() {
|
||||
let items = sketch.selection.items();
|
||||
let polys = items.map(si => si.poly.annotate({group:si.group}).clone(true, ['group']));
|
||||
let flat = POLYS.flatten(polys, [], true);
|
||||
sketch.selection.delete();
|
||||
for (let poly of flat) {
|
||||
sketch.add.polygon({ poly });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get arrange() {
|
||||
let sketch = this;
|
||||
let { items } = sketch;
|
||||
return {
|
||||
move(item, target) {
|
||||
let pos = items.indexOf(item);
|
||||
if (pos < 0 || pos >= items.length) {
|
||||
throw new Error('index out of bounds');
|
||||
}
|
||||
if (target === 'top') {
|
||||
let x = items.splice(pos,1);
|
||||
return items.appendAll(x);
|
||||
} else if (target === 'bottom') {
|
||||
let x = items.splice(pos,1);
|
||||
return sketch.items = [...x, ...items];
|
||||
}
|
||||
let newPos = { 'down': pos - 1, 'up': pos + 1 }[target];
|
||||
if (newPos >= 0 && newPos < items.length) {
|
||||
[items[pos], items[newPos]] = [items[newPos], items[pos]];
|
||||
}
|
||||
},
|
||||
|
||||
up(item) {
|
||||
sketch.arrange.move(item, 'up');
|
||||
},
|
||||
|
||||
down(item) {
|
||||
sketch.arrange.move(item, 'down');
|
||||
},
|
||||
|
||||
top(item) {
|
||||
sketch.arrange.move(item, 'top');
|
||||
},
|
||||
|
||||
bottom(item) {
|
||||
sketch.arrange.move(item, 'bottom');
|
||||
},
|
||||
|
||||
group(items) {
|
||||
let group = Date.now().toString(36);
|
||||
items.forEach(i => i.group = group);
|
||||
sketch.render();
|
||||
},
|
||||
|
||||
ungroup(items) {
|
||||
items.forEach(i => i.group = meshUtil.uuid());
|
||||
sketch.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get pattern() {
|
||||
return {
|
||||
circle() {
|
||||
console.log('sketch pattern circle');
|
||||
},
|
||||
|
||||
grid() {
|
||||
console.log('sketch pattern grid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rename(newname) {
|
||||
this.file = newname;
|
||||
this.#db_save();
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.#db_remove();
|
||||
}
|
||||
|
||||
highlight() {
|
||||
this.outline.material = material.highlight;
|
||||
}
|
||||
|
||||
unhighlight() {
|
||||
this.outline.material = material.normal;
|
||||
}
|
||||
|
||||
select(bool) {
|
||||
const { plane, handles } = this;
|
||||
if (bool === undefined) {
|
||||
return plane.material === material.selected;
|
||||
}
|
||||
if (bool.toggle) {
|
||||
return this.select(!this.select());
|
||||
}
|
||||
plane.material = (bool ? material.selected : material.normal);
|
||||
for (let handle of handles) {
|
||||
handle.material = bool ? material.highlight : plane.material;
|
||||
}
|
||||
this.render();
|
||||
return bool;
|
||||
}
|
||||
|
||||
move(x, y, z = 0) {
|
||||
const { target } = drag;
|
||||
const { center, scale, plane, handles } = this;
|
||||
const handle = handles.indexOf(target);
|
||||
if (target === plane) {
|
||||
center.x += x;
|
||||
center.y += y;
|
||||
center.z += z;
|
||||
this.render();
|
||||
} else if (handle >= 0) {
|
||||
const sf = hpos[handle];
|
||||
center.x += x / 2;
|
||||
center.y += y / 2;
|
||||
center.z += z / 2;
|
||||
scale.x += x * sf[0];
|
||||
scale.y += y * sf[1];
|
||||
scale.z += z * sf[2];
|
||||
this.render();
|
||||
} else if (Array.isArray(target)) {
|
||||
for (let item of target) {
|
||||
let { center } = item;
|
||||
center.x += x;
|
||||
center.y += y;
|
||||
center.z += z;
|
||||
}
|
||||
this.render();
|
||||
} else {
|
||||
this.center = {x, y, z};
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
centerXY() {
|
||||
if (!this.selection.centerXY()) {
|
||||
this.center.x = 0;
|
||||
this.center.y = 0;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
handle_pos(handle) {
|
||||
if (handle < 0) return;
|
||||
let { scale, center } = this;
|
||||
let v = hpos[handle];
|
||||
return {
|
||||
x: v[0] * scale.x/2 + center.x,
|
||||
y: v[1] * scale.y/2 + center.y,
|
||||
z: v[2] * scale.z/2 + center.z,
|
||||
};
|
||||
}
|
||||
|
||||
drag(opt = {}) {
|
||||
let { items, center, handles } = this;
|
||||
let { start, delta, offset, end } = opt;
|
||||
if (start) {
|
||||
let selected = items.filter(i => i.selected);
|
||||
let target = start.sketch_item ? selected : start;
|
||||
let item = Array.isArray(target) ? target[0] : undefined;
|
||||
drag.handle = handles.indexOf(target);
|
||||
drag.item = item;
|
||||
drag.target = target;
|
||||
drag.start = Object.assign({}, drag.handle >= 0 ?
|
||||
this.handle_pos(drag.handle) :
|
||||
item?.center ?? center);
|
||||
} else if (offset) {
|
||||
let { start, item, handle } = drag;
|
||||
let { snap, snapon } = meshApi.prefs.map.space;
|
||||
let pos = handle >= 0 ?
|
||||
this.handle_pos(handle) :
|
||||
item?.center ?? center;
|
||||
let end = {
|
||||
x: start.x + offset.x,
|
||||
y: start.y + offset.y,
|
||||
z: start.z + offset.z
|
||||
};
|
||||
if (snap && snapon) {
|
||||
end.x = Math.round(end.x / snap) * snap;
|
||||
end.y = Math.round(end.y / snap) * snap;
|
||||
}
|
||||
delta = {
|
||||
x: end.x - pos.x,
|
||||
y: end.y - pos.y,
|
||||
z: end.z - pos.z
|
||||
};
|
||||
this.move(delta.x, delta.y, delta.z);
|
||||
} else if (delta) {
|
||||
this.move(delta.x, delta.y, delta.z);
|
||||
} else if (end) {
|
||||
drag.target = undefined;
|
||||
} else {
|
||||
console.log({ invalid_sketch_drag: opt });
|
||||
}
|
||||
}
|
||||
|
||||
// render items unto the group object
|
||||
render() {
|
||||
let { group } = this;
|
||||
let group_order = {};
|
||||
let group_next = 0;
|
||||
let scale = this.scale.z;
|
||||
// remove previous item/poly-based children of group
|
||||
group.children.filter(c => c.sketch_item || c.sketch_line).forEach(c => group.remove(c));
|
||||
// map items into polys into meshes to add to group
|
||||
for (let item of this.items) {
|
||||
let order = scale ? (group_order[item.group] ?? group_next++) : group_next++;
|
||||
group_order[item.group] = order;
|
||||
let sketch_item = new SketchItem(this, item, order);
|
||||
group.add(sketch_item.mesh);
|
||||
group.add(...sketch_item.outs);
|
||||
}
|
||||
this.update();
|
||||
motoSpace.refresh();
|
||||
}
|
||||
|
||||
render_defer() {
|
||||
meshUtil.defer(() => this.render());
|
||||
}
|
||||
|
||||
extrude(opt = {}) {
|
||||
let { selection, height, chamfer, chamfer_top, chamfer_bottom } = opt;
|
||||
let models = [];
|
||||
let items = this.group.children
|
||||
.filter(c => c.sketch_item)
|
||||
.filter(c => !selection || c.sketch_item.selected);
|
||||
for (let item of items) {
|
||||
let vert = item.sketch_item.extrude(height, {
|
||||
chamfer,
|
||||
chamfer_top,
|
||||
chamfer_bottom
|
||||
});
|
||||
let nmdl = new mesh.model({ file: "item", mesh: vert.toFloat32() });
|
||||
models.push(nmdl);
|
||||
}
|
||||
if (models.length) {
|
||||
log('extrude', this.file || this.id, 'into', models.length, 'solid(s)');
|
||||
let { center } = this;
|
||||
let ngrp = meshApi.group.new(models);
|
||||
ngrp.move(center.x, center.y, center.z);
|
||||
// align extrusion with sketch plane
|
||||
const euler = this.group.rotation;
|
||||
const quaternion = new THREE.Quaternion();
|
||||
quaternion.setFromEuler(euler);
|
||||
ngrp.qrotate(quaternion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SketchItem {
|
||||
constructor(sketch, item, order) {
|
||||
this.sketch = sketch;
|
||||
this.item = item;
|
||||
this.order = order;
|
||||
this.update();
|
||||
}
|
||||
|
||||
get type() {
|
||||
return "sketch_item";
|
||||
}
|
||||
|
||||
get group() {
|
||||
return this.item.group;
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.item.selected;
|
||||
}
|
||||
|
||||
toggle() {
|
||||
let { sketch, item } = this;
|
||||
item.selected = !item.selected;
|
||||
if (item.group) {
|
||||
sketch.items
|
||||
.filter(i => i.group === item.group)
|
||||
.forEach(i => i.selected = item.selected);
|
||||
}
|
||||
this.update();
|
||||
sketch.render();
|
||||
motoBroker.publish('sketch_selections');
|
||||
}
|
||||
|
||||
extrude(z, opt = {}) {
|
||||
return this.poly.extrude(z, opt);
|
||||
}
|
||||
|
||||
centerXY() {
|
||||
let { center } = this.item;
|
||||
center.x = 0;
|
||||
center.y = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
let { item, sketch, order } = this;
|
||||
let { material } = mesh;
|
||||
let { type, center, width, height, miter, radius, points, spacing, poly, selected } = item;
|
||||
let { open_close, open_width, open_type } = meshApi.prefs.map.sketch;
|
||||
let base = 0.025;
|
||||
let bump = sketch.scale.z || 0.0001;
|
||||
if (type === 'circle') {
|
||||
let circumference = 2 * Math.PI * radius;
|
||||
points = points || Math.floor(circumference / (spacing || 1));
|
||||
poly = newPolygon().centerCircle(center, radius, points).annotate({ item } ).rotate(item.rotation || 0);
|
||||
} else if (type === 'rectangle') {
|
||||
poly = newPolygon().centerRectangle(center, width, height).annotate({ item });
|
||||
} else if (type === 'polygon') {
|
||||
poly = newPolygon().fromObject(item);
|
||||
poly.move(center);
|
||||
if (poly.isOpen() && open_close !== true) {
|
||||
poly = poly.offset_open(width || open_width || 1, open_type, miter)[0];
|
||||
}
|
||||
} else {
|
||||
throw `invalid sketch type: ${type}`;
|
||||
}
|
||||
this.poly = poly;
|
||||
let isSelected = selected && sketch.select();
|
||||
// create solid filled area
|
||||
let mat = (isSelected ? material.select : material.normal).clone();
|
||||
mat.transparent = true;
|
||||
mat.opacity = 0.5;
|
||||
let vrt = poly.extrude(0).toFloat32();
|
||||
let geo = new BufferGeometry();
|
||||
geo.setAttribute('position', new BufferAttribute(vrt, 3));
|
||||
let meh = this.mesh = new Mesh(geo, mat);
|
||||
meh.renderOrder = -1;
|
||||
meh.sketch_item = this;
|
||||
// bump z to avoid z order conflict and ensure item ray intersect priority
|
||||
meh.position.z += base + (order * bump);
|
||||
// create poly outline(s)
|
||||
let outs = this.outs = [];
|
||||
for (let p of [poly, ...(poly.inner || [])]) {
|
||||
let lpt = p.points.map(p => new Vector3(p.x, p.y, p.z));
|
||||
lpt.push(lpt[0]);
|
||||
let lge = new BufferGeometry().setFromPoints(lpt);
|
||||
let out = new THREE.Line(lge, material.wireline);
|
||||
out.renderOrder = -1;
|
||||
out.sketch_line = this;
|
||||
out.position.z += base + (order * bump);
|
||||
out.renderOrder = -10 + (bump * order);
|
||||
outs.push(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { MeshSketch as sketch, SketchItem };
|
||||
147
src2/mesh/split.js
Normal file
147
src2/mesh/split.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
/**
|
||||
* toggle edit/split on Z tool
|
||||
* presents a Z plane on hover
|
||||
* click to split the selected mesh(es)
|
||||
*/
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
import { space as motoSpace } from '../moto/space.js';
|
||||
import { broker as motoBroker } from '../moto/broker.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { util as meshUtil } from './util.js';
|
||||
|
||||
const { Mesh, MeshPhongMaterial, PlaneGeometry, DoubleSide, Vector3 } = THREE;
|
||||
|
||||
let isActive;
|
||||
|
||||
const key_once = motoBroker.bind('key_once');
|
||||
const key_cancel = motoBroker.bind('key_once_cancel');
|
||||
|
||||
// split functions
|
||||
const split = {
|
||||
active() {
|
||||
return isActive ? true : false;
|
||||
},
|
||||
|
||||
start() {
|
||||
if (isActive) {
|
||||
return;
|
||||
}
|
||||
let { log } = meshApi;
|
||||
if (meshApi.selection.models().length === 0) {
|
||||
log.emit('no models selected for splitting');
|
||||
return;
|
||||
}
|
||||
// create split plane visual
|
||||
let geo, mat, obj = new Mesh(
|
||||
geo = new PlaneGeometry(1,1),
|
||||
mat = new MeshPhongMaterial({
|
||||
side: DoubleSide,
|
||||
color: 0x5555aa,
|
||||
transparent: false,
|
||||
opacity: 0.5
|
||||
})
|
||||
);
|
||||
motoSpace.scene.add(obj);
|
||||
// hide until first hover
|
||||
obj.visible = false;
|
||||
// enable temp mode
|
||||
let state = split.state = { obj };
|
||||
let models = state.models = meshApi.selection.models();
|
||||
let meshes = models.map(m => m.mesh);
|
||||
// for split and lay flat modes
|
||||
motoSpace.mouse.onHover((int, event, ints) => {
|
||||
if (!event) {
|
||||
return meshes;
|
||||
}
|
||||
obj.visible = false;
|
||||
let { button, buttons } = event;
|
||||
if (buttons) {
|
||||
return;
|
||||
}
|
||||
let { dim, mid } = meshUtil.bounds(meshes);
|
||||
let { point, face, object } = int;
|
||||
let { x, y, z } = point;
|
||||
// set appearance of split plane
|
||||
mat.color.set(meshApi.prefs.map.space.dark ? 0x0059bb : 0x0079ff);
|
||||
obj.visible = true;
|
||||
if (event.shiftKey) {
|
||||
y = split.closestZ(y, object, face).y;
|
||||
}
|
||||
// y is z in model space for the purposes of a split
|
||||
state.plane = { z: y };
|
||||
// ensure split plane exceeds bounds of split target
|
||||
obj.scale.set(dim.x + 2, dim.y + 2, 1);
|
||||
obj.position.set(mid.x, y, -mid.y);
|
||||
});
|
||||
isActive = true;
|
||||
key_once({ code: 'KeyS', fn(evt) {
|
||||
split.select();
|
||||
}});
|
||||
key_once({ code: 'KeyV', fn(evt) {
|
||||
evt.preventDefault();
|
||||
let state = split.state;
|
||||
split.end();
|
||||
function doit(z) {
|
||||
state.plane = { z };
|
||||
split.select(state);
|
||||
}
|
||||
meshApi.modal.dialog({
|
||||
title: "split object Z",
|
||||
body: [ h.div({ class: "additem" }, [
|
||||
h.label('Z value'),
|
||||
h.input({ value: 0, size: 5, id: "_value" }),
|
||||
h.button({ _: "split", onclick() {
|
||||
const { _value } = meshApi.modal.bound;
|
||||
doit(parseFloat(_value.value));
|
||||
meshApi.modal.hide();
|
||||
} })
|
||||
]) ]
|
||||
});
|
||||
meshApi.modal.bound._value.focus();
|
||||
} });
|
||||
},
|
||||
|
||||
select(state) {
|
||||
let { log } = meshApi;
|
||||
let { models, plane } = split.state || state;
|
||||
if (!(models && plane)) {
|
||||
return split.end();
|
||||
}
|
||||
log.emit(`splitting ${models.length} model(s) at ${plane.z.round(3)}`).pin();
|
||||
Promise.all(models.map(m => m.split(plane))).then(models => {
|
||||
meshApi.selection.set(models.filter(m => m));
|
||||
log.emit('split complete').unpin();
|
||||
split.end();
|
||||
});
|
||||
},
|
||||
|
||||
end() {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
key_cancel('KeyS');
|
||||
key_cancel('KeyV');
|
||||
let { obj } = split.state;
|
||||
motoSpace.scene.remove(obj);
|
||||
motoSpace.mouse.onHover(undefined);
|
||||
isActive = split.state = undefined;
|
||||
meshApi.selection.update();
|
||||
},
|
||||
|
||||
closestZ(z, object, face) {
|
||||
let { position } = object.geometry.attributes;
|
||||
let matrix = object.matrixWorld;
|
||||
let v0 = new Vector3(position.getX(face.a), position.getY(face.a), position.getZ(face.a)).applyMatrix4(matrix);
|
||||
let v1 = new Vector3(position.getX(face.b), position.getY(face.b), position.getZ(face.b)).applyMatrix4(matrix);
|
||||
let v2 = new Vector3(position.getX(face.c), position.getY(face.c), position.getZ(face.c)).applyMatrix4(matrix);
|
||||
v0._d = Math.abs(v0.y - z);
|
||||
v1._d = Math.abs(v1.y - z);
|
||||
v2._d = Math.abs(v2.y - z);
|
||||
return [ v0, v1, v2 ].sort((a,b) => a._d - b._d)[0];
|
||||
}
|
||||
};
|
||||
|
||||
export { split };
|
||||
889
src2/mesh/tool.js
Normal file
889
src2/mesh/tool.js
Normal file
|
|
@ -0,0 +1,889 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Vector3 } = THREE;
|
||||
import { geom } from './geom.js';
|
||||
|
||||
const empty = 0xffffffff;
|
||||
|
||||
/**
|
||||
* tool for identiying defects and healing them
|
||||
* copied from base/mesh which is used by KM. the
|
||||
* two need to be merged. earcut needs to be migrated
|
||||
* to base.util.triagulate
|
||||
*/
|
||||
class MeshTool {
|
||||
constructor(params = {}) {
|
||||
this.precision = Math.pow(10, params.precision || 6);
|
||||
}
|
||||
|
||||
checkVertices(vertices, mod) {
|
||||
if (!vertices) {
|
||||
throw "missing vertices";
|
||||
}
|
||||
if (mod && vertices.length % mod !== 0) {
|
||||
throw "invalid vertices";
|
||||
}
|
||||
return vertices;
|
||||
}
|
||||
|
||||
getIndex() {
|
||||
if (!this.indexed) {
|
||||
throw "missing index";
|
||||
}
|
||||
return this.indexed;
|
||||
}
|
||||
|
||||
listZ() {
|
||||
let verts = this.checkVertices(this.uvert).group(3);
|
||||
let zs = new Set();
|
||||
for (let vert of verts) {
|
||||
zs.add(vert[2]);
|
||||
}
|
||||
return Array.from(zs).sort((a,b) => a - b);
|
||||
}
|
||||
|
||||
flattenZ(faceIdxs) {
|
||||
let { uvert, faces } = this;
|
||||
let zsum = 0;
|
||||
let zunq = [];
|
||||
for (let faceIdx of faceIdxs) {
|
||||
let fv1 = faces[faceIdx * 3 + 0];
|
||||
let fv2 = faces[faceIdx * 3 + 1];
|
||||
let fv3 = faces[faceIdx * 3 + 2];
|
||||
let z1 = uvert[fv1 + 2];
|
||||
let z2 = uvert[fv2 + 2];
|
||||
let z3 = uvert[fv3 + 2];
|
||||
zunq.push(z1,z2,z3);
|
||||
zsum = zsum + z1 + z2 + z3;
|
||||
}
|
||||
let zavg = ((( zsum / (faceIdxs.length * 3) ) * 10000 ) | 0) / 10000;
|
||||
for (let faceIdx of faceIdxs) {
|
||||
let fv1 = faces[faceIdx * 3 + 0];
|
||||
let fv2 = faces[faceIdx * 3 + 1];
|
||||
let fv3 = faces[faceIdx * 3 + 2];
|
||||
uvert[fv1 + 2] = zavg;
|
||||
uvert[fv2 + 2] = zavg;
|
||||
uvert[fv3 + 2] = zavg;
|
||||
}
|
||||
return { unique: zunq.sort().uniq(), average: zavg };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} vertices non-indexed
|
||||
*/
|
||||
generateFaces(vertices, opt = {}) {
|
||||
this.checkVertices(vertices, 3);
|
||||
let doClean = opt.clean !== false;
|
||||
let doDedup = opt.dedup !== false;
|
||||
let round = Math.pow(10, opt.round || 0);
|
||||
let faces = [];
|
||||
let fcac = {}; // seen face hash
|
||||
let fnew = []; // accumulate vertex triplets
|
||||
let nuvt = []; // new vertex list
|
||||
let hash = {}; // find vertex matches
|
||||
let prec = this.precision;
|
||||
let dups = 0;
|
||||
let cull = 0;
|
||||
if (round > 1) {
|
||||
for (let i=0, l=vertices.length; i<l; i++) {
|
||||
vertices[i] = ((vertices[i] * round) | 0) / round;
|
||||
}
|
||||
}
|
||||
for (let i=0, l=vertices.length; i<l; i += 3) {
|
||||
let x = vertices[i];
|
||||
let y = vertices[i+1];
|
||||
let z = vertices[i+2];
|
||||
let key = [
|
||||
(x * prec) | 0,
|
||||
(y * prec) | 0,
|
||||
(z * prec) | 0
|
||||
].join('');
|
||||
let vpos = hash[key];
|
||||
if (vpos === undefined) {
|
||||
// add new vertex x,y,z and hash entry with index
|
||||
hash[key] = vpos = nuvt.length;
|
||||
nuvt.push(x);
|
||||
nuvt.push(y);
|
||||
nuvt.push(z);
|
||||
}
|
||||
if (doClean) {
|
||||
// add vertex position to face accumulator
|
||||
fnew.push(vpos);
|
||||
if (fnew.length === 3) {
|
||||
// check and emit face if it's unique
|
||||
// cull invalid faces (has 2 or more shared vertices)
|
||||
if (fnew[0] === fnew[1] || fnew[0] === fnew[2] || fnew[1] === fnew[2]) {
|
||||
fnew = [];
|
||||
cull++;
|
||||
continue;
|
||||
}
|
||||
if (doDedup) {
|
||||
let key = fnew.slice().sort().join('-');
|
||||
// drop duplicate faces (sort handles reverse order)
|
||||
if (!fcac[key]) {
|
||||
faces.appendAll(fnew);
|
||||
fcac[key] = key;
|
||||
} else {
|
||||
dups++;
|
||||
}
|
||||
} else {
|
||||
faces.appendAll(fnew);
|
||||
}
|
||||
fnew = [];
|
||||
}
|
||||
} else {
|
||||
faces.push(vpos);
|
||||
}
|
||||
}
|
||||
this.stats = {cull, dups, faces: faces.length/3};
|
||||
// unique vertex array
|
||||
this.uvert = nuvt;
|
||||
// unique face array. elements are offset into vertices
|
||||
this.faces = faces;
|
||||
return this;
|
||||
}
|
||||
|
||||
// todo: supercede generatedFaces() for patch()
|
||||
// face record: vi1, vi2, vi3, nx, ny, nz
|
||||
// vi = vertex index
|
||||
// n = normal
|
||||
// af = adjacent face index
|
||||
index(vertices) {
|
||||
this.vertices = this.checkVertices(vertices, 3);
|
||||
const prec = this.precision;
|
||||
const vcount = vertices.length / 3;
|
||||
const fcount = vcount / 3;
|
||||
const vround = vertices.map(v => (v * prec) | 0);
|
||||
// side records [ vi0, vi1, fn1, fn2 ]
|
||||
const sides = new Uint32Array(fcount * 4 * 3).fill(empty);
|
||||
// map of side index to face count
|
||||
const srecs = {};
|
||||
// side extended records when face count exceeds 2 (bad mesh)
|
||||
const sideExt = {};
|
||||
// face record array [ nx, ny, nz, sn0, sn1, sn2 ]
|
||||
const faces = new Float32Array(fcount * 6);
|
||||
// vertex key to vertex index
|
||||
const vimap = {};
|
||||
// normal key to normal index
|
||||
const nimap = {};
|
||||
// side key to side index
|
||||
const simap = {};
|
||||
// tmp face vertex indices
|
||||
const vinds = [ 0, 0, 0 ];
|
||||
// tmp face raw vertex offset
|
||||
const viraw = [ 0, 0, 0 ];
|
||||
// tmp vector array
|
||||
const vects = [ new Vector3(), new Vector3(), new Vector3() ];
|
||||
// i=vround index, fi=faces record index
|
||||
// vi=vinds index % 3, x,y,z = tmp vars
|
||||
// fn=next face index, vn=next vertex index, sn=next side index
|
||||
for (let i=0, l=vround.length, fn=0, vn=0, sn=0, fi=0, vi=0, x, y, z; i<l; ) {
|
||||
const vroot = i;
|
||||
// create unique vertex map
|
||||
vects[vi].set(x = vround[i++], y = vround[i++], z = vround[i++]);
|
||||
let key = x + ',' + y + ',' + z;
|
||||
// vertex index
|
||||
let vid = vimap[key];
|
||||
if (vid === undefined) {
|
||||
vid = vimap[key] = vn++;
|
||||
}
|
||||
viraw[vi] = vroot;
|
||||
vinds[vi++] = vid;
|
||||
// completed record for a face
|
||||
if (vi === 3) {
|
||||
// create indexed unique normal map
|
||||
const cfn = THREE.computeFaceNormal(...vects);
|
||||
const [ v0, v1, v2 ] = vinds;
|
||||
// create consistent side order for index key
|
||||
const s0 = (v0 < v1 ? v0 + "," + v1 : v1 + "," +v0);
|
||||
const s1 = (v1 < v2 ? v1 + "," + v2 : v2 + "," +v1);
|
||||
const s2 = (v2 < v0 ? v2 + "," + v0 : v0 + "," +v2);
|
||||
// for storing raw vertex offset in side record
|
||||
const [ vr0, vr1, vr2 ] = viraw;
|
||||
// store face indexes into sdrec array for each side
|
||||
const smap = [ s0, s1, s2 ].map((key,ki) => {
|
||||
let sid = simap[key], sdoff, sdcnt;
|
||||
if (sid === undefined) {
|
||||
sid = simap[key] = sn++;
|
||||
sdcnt = srecs[sid] = 1;
|
||||
sdoff = sid * 4;
|
||||
sides[sdoff] = viraw[ki];
|
||||
sides[sdoff + 1] = viraw[(ki + 1) % 3];
|
||||
} else {
|
||||
sdoff = sid * 4;
|
||||
sdcnt = ++srecs[sid];
|
||||
}
|
||||
if (sdcnt > 2) {
|
||||
(sideExt[sid] = sideExt[sid] ||
|
||||
[ sides[sdoff], sides[sdoff + 1], sides[sdoff + 2], sides[sdoff + 3] ]
|
||||
).push(fn);
|
||||
} else {
|
||||
sides[sdoff + sdcnt + 1] = fn;
|
||||
}
|
||||
return sid;
|
||||
});
|
||||
faces[fi++] = cfn.x;
|
||||
faces[fi++] = cfn.y;
|
||||
faces[fi++] = cfn.z;
|
||||
faces[fi++] = smap[0];
|
||||
faces[fi++] = smap[1];
|
||||
faces[fi++] = smap[2];
|
||||
vi = 0;
|
||||
fn++;
|
||||
}
|
||||
}
|
||||
this.indexed = {
|
||||
faces, sides, sideExt
|
||||
};
|
||||
}
|
||||
|
||||
getAdjacentFaces(face) {
|
||||
const { faces, sides } = this.getIndex();
|
||||
const foff = face * 6;
|
||||
const s0 = faces[foff + 3];
|
||||
const s1 = faces[foff + 4];
|
||||
const s2 = faces[foff + 5];
|
||||
const adj = [];
|
||||
for (let s of [s0, s1, s2]) {
|
||||
const soff = s * 4;
|
||||
const f1 = sides[soff + 2];
|
||||
const f2 = sides[soff + 3];
|
||||
if (f1 !== empty) adj.push(f1);
|
||||
if (f2 !== empty) adj.push(f2);
|
||||
}
|
||||
return adj;
|
||||
}
|
||||
|
||||
findConnectedSurface(faces, radians, filterZ, found = {}) {
|
||||
const norms = this.getIndex().faces;
|
||||
if (filterZ !== undefined) {
|
||||
// optional filter to z normal >= value
|
||||
faces = faces.filter(f => norms[f * 6 + 2] >= filterZ);
|
||||
}
|
||||
const checked = {};
|
||||
const check = faces.slice();
|
||||
for (let face of faces) {
|
||||
found[face] = 1;
|
||||
}
|
||||
while (check.length) {
|
||||
const face = check.shift();
|
||||
const froot = face * 6;
|
||||
if (filterZ !== undefined && norms[froot + 2] < filterZ) {
|
||||
continue;
|
||||
}
|
||||
const fadj = this.getAdjacentFaces(face);
|
||||
for (let f of fadj) {
|
||||
if (found[f] || checked[f]) {
|
||||
continue;
|
||||
}
|
||||
const aroot = f * 6;
|
||||
let sum = 0;
|
||||
for (let i=0; i<3; i++) {
|
||||
sum += Math.pow(norms[froot + i] - norms[aroot + i], 2);
|
||||
}
|
||||
const fn = Math.sqrt(sum);
|
||||
if (fn <= radians) {
|
||||
faces.push(f);
|
||||
check.push(f)
|
||||
checked[f] = 1;
|
||||
found[f] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return faces;
|
||||
}
|
||||
|
||||
generateOutlines(list) {
|
||||
const { faces, sides } = this.getIndex();
|
||||
const prec = this.precision;
|
||||
const verts = this.vertices;
|
||||
const lines = {};
|
||||
const point = {};
|
||||
const pindx = {};
|
||||
let pnext = 0;
|
||||
function vert2point(v) {
|
||||
const x = verts[v];
|
||||
const y = verts[v + 1];
|
||||
const z = verts[v + 2];
|
||||
const key = ((x * prec) | 0) + ',' + ((y * prec) | 0) + ',' + ((z * prec) | 0);
|
||||
const rec = point[key];
|
||||
if (rec === undefined) {
|
||||
pindx[pnext] = { x, y, z, to: [] };
|
||||
return point[key] = pnext++;
|
||||
} else {
|
||||
return rec;
|
||||
}
|
||||
}
|
||||
function addLine(s) {
|
||||
if (s === empty) return;
|
||||
const v0 = vert2point(sides[s * 4]);
|
||||
const v1 = vert2point(sides[s * 4 + 1]);
|
||||
const key = v0 < v1 ? v0 + ',' + v1 : v1 +',' + v0;
|
||||
const rec = lines[key];
|
||||
if (rec) {
|
||||
rec.del = true;
|
||||
} else {
|
||||
lines[key] = { v0, v1 };
|
||||
}
|
||||
}
|
||||
for (let face of list) {
|
||||
const foff = face * 6;
|
||||
addLine(faces[foff + 3]);
|
||||
addLine(faces[foff + 4]);
|
||||
addLine(faces[foff + 5]);
|
||||
}
|
||||
const pairs = Object.values(lines).filter(l => !l.del);
|
||||
for (let line of pairs) {
|
||||
const { v0, v1 } = line;
|
||||
const p0 = pindx[v0];
|
||||
const p1 = pindx[v1];
|
||||
p0.to.push(v1);
|
||||
p1.to.push(v0);
|
||||
}
|
||||
let curr;
|
||||
let outs = [ ];
|
||||
for (let prec of Object.values(pindx)) {
|
||||
if (prec.used || prec.to.length === 0) continue;
|
||||
outs.push(curr = []);
|
||||
while (prec) {
|
||||
let { to, x, y, z } = prec;
|
||||
if (to.length < 2) {
|
||||
throw `invalid to ${to.length} @ ${x},${y},${z}`;
|
||||
}
|
||||
prec.used = true;
|
||||
curr.push({ x, y, z });
|
||||
const t0 = pindx[to[0]];
|
||||
const t1 = pindx[to[1]];
|
||||
if (!t0.used) {
|
||||
prec = t0;
|
||||
} else if (!t1.used) {
|
||||
prec = t1;
|
||||
} else {
|
||||
prec = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
// console.log({lines, pairs, pindx, outs});
|
||||
return outs;
|
||||
}
|
||||
|
||||
isolateBodies() {
|
||||
const verts = this.checkVertices(this.vertices);
|
||||
const bodies = [];
|
||||
const used = {};
|
||||
for (let i=0, l=verts.length / 9; i<l; i++) {
|
||||
if (used[i]) {
|
||||
continue;
|
||||
}
|
||||
const body = this.findConnectedSurface([i], Infinity, undefined, used);
|
||||
if (body.length) {
|
||||
// todo: pre-allocate known length
|
||||
// then copy array regions from verts to bverts
|
||||
const bverts = [];
|
||||
for (let f of body) {
|
||||
bverts.push(...verts.slice(f*9, f*9+9));
|
||||
}
|
||||
bodies.push(bverts);
|
||||
}
|
||||
}
|
||||
return bodies;
|
||||
}
|
||||
|
||||
patch(opt = { merge: true }) {
|
||||
let vertices = this.checkVertices(this.uvert);
|
||||
let faces = this.faces;
|
||||
let hash = {}; // key to line map
|
||||
let vmap = {}; // vertext to line map
|
||||
let lines = [];
|
||||
|
||||
class Line {
|
||||
constructor(v1, v2) {
|
||||
this.v1 = v1;
|
||||
this.v2 = v2;
|
||||
this.faces = [];
|
||||
}
|
||||
|
||||
addFace(face) {
|
||||
if (this.faces.indexOf(face) < 0) {
|
||||
this.faces.push(face);
|
||||
} else if (false) {
|
||||
console.log('adding face to line twice', this, face);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
adjacentUnused() {
|
||||
return vmap[this.v1]
|
||||
.concat(vmap[this.v2])
|
||||
.filter(l => l !== this && !l.used && l.faces.length === 1);
|
||||
}
|
||||
|
||||
adjacentUsed() {
|
||||
return vmap[this.v1]
|
||||
.concat(vmap[this.v2])
|
||||
.filter(l => l !== this && l.used && l.faces.length === 1);
|
||||
}
|
||||
|
||||
touches(line) {
|
||||
return this.v1 === line.v1 ||
|
||||
this.v1 === line.v2 ||
|
||||
this.v2 === line.v1 ||
|
||||
this.v2 === line.v2;
|
||||
}
|
||||
|
||||
unitVector() {
|
||||
if (this.unit !== undefined) {
|
||||
return this.unit;
|
||||
}
|
||||
let v1 = this.v1;
|
||||
let v2 = this.v2;
|
||||
let d = [
|
||||
vertices[v1] - vertices[v2],
|
||||
vertices[v1 + 1] - vertices[v2 + 1],
|
||||
vertices[v1 + 2] - vertices[v2 + 2],
|
||||
];
|
||||
let max = Math.max(...d.map(v => Math.abs(v)));
|
||||
// console.log({d, max});
|
||||
return this.unit = d.map(v => v / max);
|
||||
}
|
||||
}
|
||||
|
||||
function diffUnitVector(uv1, uv2) {
|
||||
let diff = 0;
|
||||
for (let i=0; i<3; i++) {
|
||||
diff += Math.abs(uv1[i] - uv2[i]);
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
|
||||
function getLine(v1, v2) {
|
||||
let s1 = vertices[v1] + vertices[v1+1]*1000 + vertices[v1+2]*100000;
|
||||
let s2 = vertices[v2] + vertices[v2+1]*1000 + vertices[v2+2]*100000;
|
||||
let key = s1 <= s2 ? [v1,v2].join('-') : [v2,v1].join('-');
|
||||
let line = hash[key];
|
||||
if (!line) {
|
||||
// point order will be swapped for reconstructed faces
|
||||
// to preserve chirality (assuming face normals are correct)
|
||||
line = hash[key] = new Line(v1, v2);
|
||||
line.key = key;
|
||||
lines.push(line);
|
||||
let lm1 = vmap[v1] = vmap[v1] || [];
|
||||
let lm2 = vmap[v2] = vmap[v2] || [];
|
||||
lm1.push(line);
|
||||
lm2.push(line);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function getXYZ(v) {
|
||||
return {x: vertices[v], y: vertices[v+1], z: vertices[v+2]};
|
||||
}
|
||||
|
||||
for (let i=0, l=this.faces.length; i<l; i += 3) {
|
||||
let v1 = faces[i];
|
||||
let v2 = faces[i+1];
|
||||
let v3 = faces[i+2];
|
||||
let l1 = getLine(v1, v2).addFace(i);
|
||||
let l2 = getLine(v2, v3).addFace(i);
|
||||
let l3 = getLine(v3, v1).addFace(i);
|
||||
}
|
||||
|
||||
// array of unique line segment records with vertices
|
||||
// and a list of faces. one = edge, two = inside
|
||||
// all lines should be inside or object is not manifold
|
||||
// but does not preclude self-intersecting geometries
|
||||
this.lines = lines;
|
||||
|
||||
// lines belonging to only one face are on the edge of a hole
|
||||
let edges = this.edges = lines.filter(l => l.faces.length === 1);
|
||||
|
||||
// arrays of connected edges that form a closed loop
|
||||
let loops = this.loops = [];
|
||||
|
||||
// connect edge lines into closed loops
|
||||
for (let i=0, l=edges.length; i<l; i++) {
|
||||
let line = edges[i];
|
||||
if (line.used) {
|
||||
continue;
|
||||
}
|
||||
// cannot start on split
|
||||
if (line.adjacentUnused().length > 2) {
|
||||
// console.log('cannot start on a split');
|
||||
continue;
|
||||
}
|
||||
// line.used = true;
|
||||
let loop = [ ];
|
||||
// build line through adjacent lines
|
||||
while (true) {
|
||||
let adjacent = line.adjacentUnused();
|
||||
if (adjacent.length === 0) {
|
||||
let first = loop[0];
|
||||
let last = loop.last();
|
||||
loop.open = loop.length < 3
|
||||
|| (first.v1 !== last.v1
|
||||
&& first.v2 !== last.v2
|
||||
&& first.v1 !== last.v2
|
||||
&& first.v2 !== last.v1);
|
||||
// console.log({term: line, adj: line.adjacentUsed(), open, loop});
|
||||
break;
|
||||
} else if (adjacent.length === 1) {
|
||||
if (line.v2 !== adjacent[0].v1) {
|
||||
let tmp = adjacent[0].v1;
|
||||
adjacent[0].v1 = adjacent[0].v2;
|
||||
adjacent[0].v2 = tmp;
|
||||
// console.log('chirality mismatch fixed');
|
||||
}
|
||||
line = adjacent[0];
|
||||
} else if (adjacent.length === 2) {
|
||||
// follow edges according to chirality
|
||||
line = adjacent[0].v1 === line.v2 ?
|
||||
adjacent[0] :
|
||||
adjacent[1]
|
||||
} else {
|
||||
// console.log('error adjacent', line, adjacent, loops.length, loop.length);
|
||||
line.split = adjacent;
|
||||
line = adjacent[0];
|
||||
}
|
||||
loop.push(line);
|
||||
line.used = true;
|
||||
}
|
||||
if (loop.length) {
|
||||
loops.push(loop);
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to connect open loops
|
||||
if (loops.length > 1) {
|
||||
outer: for (let i=0; i<loops.length-1; i++) {
|
||||
let l1 = loops[i];
|
||||
if (!(l1 && l1.open)) {
|
||||
continue;
|
||||
}
|
||||
for (let j=i+1; j<loops.length; j++) {
|
||||
let l2 = loops[j];
|
||||
if (!(l2 && l2.open)) {
|
||||
continue;
|
||||
}
|
||||
let nuloop;
|
||||
if (l1[0].touches(l2[0])) {
|
||||
nuloop = [...l2.reverse(), ...l1];
|
||||
} else if (l1[0].touches(l2.last())) {
|
||||
nuloop = [...l2, ...l1];
|
||||
} else if (l1.last().touches(l2[0])) {
|
||||
nuloop = [...l1, ...l2];
|
||||
} else if (l1.last().touches(l2.last())) {
|
||||
nuloop = [...l1, ...l2.reverse()];
|
||||
}
|
||||
if (nuloop) {
|
||||
nuloop.open = true;
|
||||
loops.push(nuloop);
|
||||
loops[i] = loops[j] = undefined;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// store invalid loops for display / culling
|
||||
this.shorts = loops.filter(l => l && l.length <= 2);
|
||||
// console.log({edges, loops, shorts: this.shorts});
|
||||
|
||||
// filter out null and short loops
|
||||
this.loops = loops = loops.filter(l => l && l.length > 2);
|
||||
// console.log({loops_filtered: loops});
|
||||
|
||||
// rotate/flatten to Z plane and use earcut to generate faces
|
||||
// because earcut emits point indexes, no need to un-rotate
|
||||
function emitLoop(loop) {
|
||||
let lastPoint;
|
||||
let pindex = [];
|
||||
let points = [];
|
||||
let dx = 0, dy = 0, dz = 0;
|
||||
|
||||
// gather points, find axis with least deviaton to swap with Z
|
||||
let loop1 = loop.slice();
|
||||
loop1.push(loop[0]);
|
||||
for (let i=0; i<loop1.length; i++) {
|
||||
let line = loop1[i];
|
||||
let nextPoint;
|
||||
if (line.v1 === lastPoint) {
|
||||
nextPoint = line.v2;
|
||||
} else {
|
||||
nextPoint = line.v1;
|
||||
}
|
||||
if (i < loop1.length - 1) {
|
||||
pindex.push(nextPoint);
|
||||
points.push([
|
||||
vertices[nextPoint],
|
||||
vertices[nextPoint+1],
|
||||
vertices[nextPoint+2]
|
||||
]);
|
||||
}
|
||||
if (lastPoint >= 0) {
|
||||
dx = Math.max(dx, Math.abs(vertices[lastPoint] - vertices[nextPoint]));
|
||||
dy = Math.max(dy, Math.abs(vertices[lastPoint+1] - vertices[nextPoint+1]));
|
||||
dz = Math.max(dz, Math.abs(vertices[lastPoint+2] - vertices[nextPoint+2]));
|
||||
}
|
||||
lastPoint = nextPoint;
|
||||
}
|
||||
|
||||
// swap Z axis with least delta axis
|
||||
let swap = 2;
|
||||
if (dy < dx && dy < dz) {
|
||||
// swap y,z
|
||||
swap = 1;
|
||||
} else if (dx < dy && dx < dz) {
|
||||
// swap x,z
|
||||
swap = 0;
|
||||
}
|
||||
if (swap < 2) {
|
||||
// swap x or y for z axes
|
||||
for (let point of points) {
|
||||
let tmpz = point[2];
|
||||
point[2] = point[swap];
|
||||
point[swap] = tmpz;
|
||||
}
|
||||
}
|
||||
|
||||
return { index: pindex, points: points.flat(), swap };
|
||||
}
|
||||
|
||||
// given a loop with holes, emit faces
|
||||
function emitFaces(loop) {
|
||||
let { index, points } = loop;
|
||||
let ec = earcut(points, undefined, 3);
|
||||
// map earcut point indexes back to vertex indexes
|
||||
return ec.map(p => index[p]);
|
||||
}
|
||||
|
||||
function emitTop(rec) {
|
||||
let points = rec.loop.points.slice();
|
||||
let index = rec.loop.index.slice();
|
||||
let holes = [];
|
||||
for (let inner of rec.inner) {
|
||||
holes.push(points.length / 3);
|
||||
points.appendAll(inner.loop.points);
|
||||
index.appendAll(inner.loop.index);
|
||||
}
|
||||
let ec = earcut(points, holes, 3);
|
||||
return ec.map(p => index[p]);
|
||||
}
|
||||
|
||||
// new patch areas
|
||||
let areas = this.areas = [];
|
||||
|
||||
if (opt.compound) {
|
||||
// group loops by normal (using swap as crude proxy for now)
|
||||
// then attempt to nest them (find holes)
|
||||
let polys = loops.map(loop => {
|
||||
return emitLoop(loop);
|
||||
});
|
||||
|
||||
// return nested mapping
|
||||
let nested = geom.nest(polys.map(p => p.points));
|
||||
// recover and map loops from point arrays
|
||||
for (let rec of nested) {
|
||||
rec.loop = polys.filter(loop => loop.points === rec.points)[0];
|
||||
}
|
||||
|
||||
// extract tops which have nesting depth mod 2 === 0
|
||||
let tops = nested.filter(rec => rec.depth % 2 === 0);
|
||||
for (let top of tops) {
|
||||
areas.push(emitTop(top));
|
||||
}
|
||||
} else {
|
||||
// create area faces covering loops
|
||||
for (let loop of loops) {
|
||||
let l2 = emitLoop(loop);
|
||||
let fs = emitFaces(l2);
|
||||
areas.push(fs);
|
||||
}
|
||||
}
|
||||
|
||||
// auto-merge newly found enclosed areas
|
||||
if (opt.merge) {
|
||||
this.merge();
|
||||
}
|
||||
|
||||
// de-reference loops and edges
|
||||
if (opt.mapped) {
|
||||
this.mapped = {
|
||||
edges: this.expandLoop(this.edges),
|
||||
areas: this.areas.map(a => this.expandArray(a))
|
||||
};
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
appendVertex(index, array = []) {
|
||||
let vertx = this.uvert;
|
||||
array.push(vertx[index++]);
|
||||
array.push(vertx[index++]);
|
||||
array.push(vertx[index]);
|
||||
return array;
|
||||
}
|
||||
|
||||
expandArray(indices, array = []) {
|
||||
for (let i of indices) {
|
||||
this.appendVertex(i, array);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
expandLoop(loop, array = []) {
|
||||
for (let line of loop) {
|
||||
this.appendVertex(line.v1, array);
|
||||
this.appendVertex(line.v2, array);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
merge() {
|
||||
for (let area of this.areas || []) {
|
||||
this.faces.appendAll(area);
|
||||
}
|
||||
}
|
||||
|
||||
unrolled() {
|
||||
let out = [];
|
||||
for (let face of this.faces) {
|
||||
this.appendVertex(face, out);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
generateGear(numTeeth, module, pressureAngle, offset) {
|
||||
// Adapted from: Public Domain Parametric Involute Spur Gear by Leemon Baird, 2011, Leemon@Leemon.com http://www.thingiverse.com/thing:5505
|
||||
// see also http://grabcad.com/questions/tutorial-how-to-model-involute-gears-in-solidworks-and-show-design-intent
|
||||
|
||||
// const pitch_radius = (numTeeth * module) / 2;
|
||||
const pi = Math.PI;
|
||||
|
||||
// degrees to radians
|
||||
function degrees_to_radians(theta) { return theta / 180 * pi; }
|
||||
|
||||
// polar to cartesian
|
||||
function polar(r, theta) { return [r * Math.sin(theta), r * Math.cos(theta)]; }
|
||||
|
||||
// point on involute curve
|
||||
function q6(b, s, t, d) { return polar(d, s * (iang(b, d) + t)); }
|
||||
|
||||
// unwind this many degrees to go from r1 to r2
|
||||
function iang(r1, r2) { return Math.sqrt((r2 / r1) * (r2 / r1) - 1) - Math.acos(r1 / r2); }
|
||||
|
||||
// radius a fraction f up the curved side of the tooth
|
||||
function q7(f, r, b, r2, t, s) { return q6(b, s, t, (1 - f) * Math.max(b, r) + f * r2); }
|
||||
|
||||
// lerp q7 0..1 with a given # steps
|
||||
function qf(steps, r, b, r2, t, s) {
|
||||
let res = [];
|
||||
let step = 1/steps;
|
||||
for (let f=0; f <= 1; f += step) {
|
||||
res.push(q7(f, r, b, r2, t, s));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// rotate an array of 2d points
|
||||
function rotate(points_array, angle) {
|
||||
let answer = [];
|
||||
for (let i = 0; i < points_array.length; i++) {
|
||||
let x = points_array[i][0];
|
||||
let y = points_array[i][1];
|
||||
let xr = x * Math.cos(angle) - y * Math.sin(angle);
|
||||
let yr = y * Math.cos(angle) + x * Math.sin(angle);
|
||||
answer.push([xr, yr]);
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
// gear parameter setup
|
||||
let mm_per_tooth = module * pi; // mm size of one gear tooth
|
||||
let clearance = offset/2 || 0; // freedom between two gear centers
|
||||
let backlash = offset || 0; // freedom between two gear contact points
|
||||
let pressure_angle = degrees_to_radians(pressureAngle);
|
||||
let gear = [];
|
||||
let pitch;
|
||||
|
||||
// involute gear maker
|
||||
{
|
||||
let p = mm_per_tooth * numTeeth / pi / 2; // radius of pitch circle
|
||||
let c = p + mm_per_tooth / pi - clearance; // radius of outer circle
|
||||
let b = p * Math.cos(pressure_angle); // radius of base circle
|
||||
let r = p - (c - p) - clearance; // radius of root circle
|
||||
let t = mm_per_tooth / 2 - backlash / 2; // tooth thickness at pitch circle
|
||||
let k = -iang(b, p) - t / 2 / p; // angle where involute meets base circle on side of tooth
|
||||
let f = 10 + ((p/5) | 0); // increase number of tooth curve points on larger gears
|
||||
let points = [ // [x,y] points for a single gear tooth
|
||||
polar(r, -pi / numTeeth),
|
||||
polar(r, r < b ? k : -pi / numTeeth),
|
||||
...qf(f, r, b, c, k, 1),
|
||||
...qf(f, r, b, c, k, -1).reverse(),
|
||||
polar(r, r < b ? -k : pi / numTeeth),
|
||||
// polar(r, 3.142 / numTeeth) // omit b/c overlaps start when rotated
|
||||
];
|
||||
|
||||
// create all gear teeth by rotating the first tooth
|
||||
for (var i = 0; i < numTeeth; i++) {
|
||||
gear.appendAll(rotate(points, -i * 2 * pi / numTeeth));
|
||||
}
|
||||
|
||||
pitch = p.round(3);
|
||||
geom.log(`gear pitch radius: ${pitch}`);
|
||||
}
|
||||
|
||||
return { gear, pitch };
|
||||
}
|
||||
|
||||
generateThreads(height, radius, turns, depth, steps, taper) {
|
||||
let verts = [];
|
||||
let zstep = height / turns;
|
||||
let rstep = (Math.PI * 2) / steps;
|
||||
let z2step = zstep / steps;
|
||||
let points = []
|
||||
let iter = 0;
|
||||
let iter_max = turns * steps + steps;
|
||||
let mute_min = Math.round(steps * 2.5);
|
||||
let mute_max = iter_max - mute_min;
|
||||
for (let turn=0; turn<turns; turn++) {
|
||||
for (let step=0; step<steps; step++) {
|
||||
iter++;
|
||||
let itern = iter + steps;
|
||||
let rm = radius - depth;
|
||||
let rx = true &&
|
||||
taper && iter < mute_min ? rm + (iter / mute_min) * depth :
|
||||
taper && iter > mute_max ? rm + ((iter_max - iter) / mute_min) * depth :
|
||||
radius;
|
||||
let rxt = true &&
|
||||
taper && itern < mute_min ? rm + (itern / mute_min) * depth :
|
||||
taper && itern > mute_max ? rm + ((iter_max - itern) / mute_min) * depth :
|
||||
radius;
|
||||
let angle0 = rstep * step - Math.PI/2;
|
||||
let z0 = (zstep * turn) + (z2step * step);
|
||||
let x0 = Math.cos(angle0) * (rm);
|
||||
let y0 = Math.sin(angle0) * (rm);
|
||||
let x1 = Math.cos(angle0) * (rx);
|
||||
let y1 = Math.sin(angle0) * (rx);
|
||||
let x1t = Math.cos(angle0) * (rxt);
|
||||
let y1t = Math.sin(angle0) * (rxt);
|
||||
let p0 = [ x1t, y1t, z0 + zstep/2 ];
|
||||
let p1 = [ x0, y0, z0 ];
|
||||
let p2 = [ x1, y1, z0 - zstep/2 ];
|
||||
let [ pp0, pp1, pp2 ] = points;
|
||||
if (pp0 && pp1 && pp2) {
|
||||
verts.push(...pp0, ...pp1, ...p0);
|
||||
verts.push(...p0, ...pp1, ...p1);
|
||||
verts.push(...pp1, ...pp2, ...p1);
|
||||
verts.push(...p1, ...pp2, ...p2);
|
||||
}
|
||||
points = [ p0, p1, p2 ];
|
||||
}
|
||||
}
|
||||
return verts;
|
||||
}
|
||||
}
|
||||
|
||||
export { MeshTool as tool };
|
||||
335
src2/mesh/util.js
Normal file
335
src2/mesh/util.js
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Matrix4, Matrix3, Vector3, Box3, LineSegments, BufferGeometry, Float32BufferAttribute, LineBasicMaterial } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
import { api as meshApi } from './api.js';
|
||||
import { object as meshObject } from './object.js';
|
||||
|
||||
const deferFn = [];
|
||||
const boundsCache = {};
|
||||
|
||||
// util functions augmented in build (download)
|
||||
const util = {
|
||||
|
||||
uuid(segs = 1) {
|
||||
let uid = [];
|
||||
while (segs-- > 0) {
|
||||
uid.push(Math.round(Math.random() * 0xffffffff).toString(36));
|
||||
}
|
||||
return uid.join('-');
|
||||
},
|
||||
|
||||
toHexRGB(v) {
|
||||
return [
|
||||
((v >> 16) & 0xff).toString(16).padStart(2,0),
|
||||
((v >> 8) & 0xff).toString(16).padStart(2,0),
|
||||
((v >> 0) & 0xff).toString(16).padStart(2,0)
|
||||
].join('');
|
||||
},
|
||||
|
||||
fromHexRGB(v) {
|
||||
return 0 +
|
||||
parseInt(v.substring(0,2), 16) << 16 |
|
||||
parseInt(v.substring(2,4), 16) << 8 |
|
||||
parseInt(v.substring(4,6), 16);
|
||||
},
|
||||
|
||||
// add comma separator to 1000s
|
||||
comma(val) {
|
||||
let str = val.toString().split(".");
|
||||
str[0] = str[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return str.join(".");
|
||||
},
|
||||
|
||||
// merge repeated function calls like updates
|
||||
// that importantly take no arguments
|
||||
defer(fn, time = 50) {
|
||||
for (let rec of deferFn) {
|
||||
if (rec.fn === fn) {
|
||||
clearTimeout(rec.timer);
|
||||
deferFn.remove(rec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let rec = { fn };
|
||||
rec.timer = setTimeout(() => {
|
||||
deferFn.remove(rec);
|
||||
fn();
|
||||
}, time);
|
||||
deferFn.push(rec);
|
||||
},
|
||||
|
||||
// return a function wrapper which can be re-used
|
||||
deferWrap(fn, time) {
|
||||
return function() {
|
||||
util.defer(fn, time);
|
||||
}
|
||||
},
|
||||
|
||||
// @param object {THREE.Object3D | THREE.Object3D[] | MeshObject | MeshObject[]}
|
||||
// @returns bounds modified for moto.space
|
||||
bounds(object) {
|
||||
let box = new Box3();
|
||||
if (Array.isArray(object)) {
|
||||
for (let o of object) {
|
||||
util.box3expand(box, o instanceof meshObject ? o.object : o);
|
||||
}
|
||||
} else if (object) {
|
||||
util.box3expand(box, object instanceof meshObject ? object.object : object);
|
||||
} else {
|
||||
return box;
|
||||
}
|
||||
let bnd = {
|
||||
min: {
|
||||
x: box.min.x,
|
||||
y: box.min.z,
|
||||
z: box.min.y
|
||||
},
|
||||
max: {
|
||||
x: box.max.x,
|
||||
y: box.max.z,
|
||||
z: box.max.y
|
||||
}
|
||||
};
|
||||
bnd.size = bnd.dim = {
|
||||
x: bnd.max.x - bnd.min.x,
|
||||
y: bnd.max.y - bnd.min.y,
|
||||
z: bnd.max.z - bnd.min.z
|
||||
};
|
||||
bnd.center = bnd.mid = {
|
||||
x: (bnd.max.x + bnd.min.x) / 2,
|
||||
y: -(bnd.max.y + bnd.min.y) / 2,
|
||||
z: (bnd.max.z + bnd.min.z) / 2
|
||||
};
|
||||
return bnd;
|
||||
},
|
||||
|
||||
// bounding box workaround adapted from:
|
||||
// https://discourse.threejs.org/t/bounding-box-bigger-than-concave-object-extrudegeometry/26073/2
|
||||
// https://discourse.threejs.org/t/invalid-bounds-generated-for-some-orientations/33205
|
||||
box3expand(box3, object) {
|
||||
if (object._no_bounds) {
|
||||
return;
|
||||
}
|
||||
|
||||
let geometry = object.geometry;
|
||||
object.updateWorldMatrix(geometry ? true : false, false);
|
||||
|
||||
if (geometry) {
|
||||
let matrix = object.matrixWorld;
|
||||
let bkey = [matrix.elements.map(v => v.round(5))].join(',')
|
||||
let cached = boundsCache[object.id];
|
||||
// geometry._model_invalid set on model.reload(), usually after a split
|
||||
if (!cached || cached.bkey !== bkey || geometry._model_invalid) {
|
||||
let position = geometry.attributes.position.clone();
|
||||
position.applyMatrix4(new Matrix4().extractRotation(matrix));
|
||||
let bounds = new Box3().setFromBufferAttribute(position);
|
||||
// let scale = new Vector3().setFromMatrixScale(matrix);
|
||||
// bounds.min.multiply(scale);
|
||||
// bounds.max.multiply(scale);
|
||||
cached = boundsCache[object.id] = { bkey, bounds };
|
||||
geometry._model_invalid = undefined;
|
||||
}
|
||||
let bt = cached.bounds.clone();
|
||||
let m4 = new Matrix4();
|
||||
m4.setPosition(new Vector3().setFromMatrixPosition(object.matrixWorld));
|
||||
bt.applyMatrix4(m4);
|
||||
box3.union(bt);
|
||||
}
|
||||
|
||||
let children = object.children;
|
||||
for (let i = 0, l = children.length; i < l; i++) {
|
||||
util.box3expand(box3, children[i]);
|
||||
}
|
||||
},
|
||||
|
||||
// extract object fields into an array with optional rounding
|
||||
extract(object, opt = {}) {
|
||||
let field = opt.fields || ['x', 'y', 'z'];
|
||||
let array = [];
|
||||
for (let k of field) {
|
||||
let v = object[k] || 0;
|
||||
if (opt.round !== undefined) v = v.round(opt.round);
|
||||
if (opt.fixed !== undefined) v = v.toFixed(opt.fixed);
|
||||
array.push(v);
|
||||
}
|
||||
return opt.map ?
|
||||
{
|
||||
[field[0]] : array[0],
|
||||
[field[1]] : array[1],
|
||||
[field[2]] : array[2],
|
||||
} :
|
||||
array;
|
||||
},
|
||||
|
||||
// for an array of maps, return the average of all named fields in a new map
|
||||
average(array, opt = {}) {
|
||||
let fields = opt.fields || ['x', 'y', 'z'];
|
||||
let avg = {};
|
||||
for (let e of array) {
|
||||
for (let f of fields) {
|
||||
avg[f] = (avg[f] || 0) + e[f];
|
||||
}
|
||||
}
|
||||
for (let f of fields) {
|
||||
avg[f] = (avg[f] || 0) / array.length;
|
||||
}
|
||||
return avg;
|
||||
},
|
||||
|
||||
faceNormals(obj, opt = { }) {
|
||||
const _va = new Vector3();
|
||||
const _vb = new Vector3();
|
||||
const _vc = new Vector3();
|
||||
const _v1 = new Vector3();
|
||||
const _v2 = new Vector3();
|
||||
const _normalMatrix = new Matrix3();
|
||||
const prefs = meshApi.prefs.map;
|
||||
const norms = prefs.normals;
|
||||
const defcolor = prefs.space.dark ? norms.color_dark : norms.color_lite;
|
||||
const normlen = norms.length || 1;
|
||||
|
||||
class FaceNormalsHelper extends LineSegments {
|
||||
constructor(object, size = opt.size || normlen, color = opt.color || defcolor) {
|
||||
const objGeometry = object.geometry;
|
||||
const nNormals = objGeometry.attributes.position.count / 3;
|
||||
const geometry = new BufferGeometry();
|
||||
const positions = new Float32BufferAttribute(nNormals * 3 * 2, 3);
|
||||
geometry.setAttribute('position', positions);
|
||||
super(geometry, new LineBasicMaterial({
|
||||
color, toneMapped: false
|
||||
}));
|
||||
this.object = object;
|
||||
this.size = size;
|
||||
this.type = 'FaceNormalsHelper';
|
||||
this.matrixAutoUpdate = false;
|
||||
this._no_bounds = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.object.updateMatrixWorld(true);
|
||||
const position = this.geometry.attributes.position;
|
||||
const objGeometry = this.object.geometry;
|
||||
const objArr = objGeometry.attributes.position.array;
|
||||
let j=0;
|
||||
for (let idx = 0, xj = 0, jl = objArr.length; j < jl; ) {
|
||||
_va.set(objArr[j++], objArr[j++], objArr[j++]);
|
||||
_vb.set(objArr[j++], objArr[j++], objArr[j++]);
|
||||
_vc.set(objArr[j++], objArr[j++], objArr[j++]);
|
||||
let vn = THREE.computeFaceNormal(_va, _vb, _vc);
|
||||
let vc = _va.add(_vb).add(_vc).divideScalar(3);
|
||||
_v2.copy(vc).add(vn.multiplyScalar(this.size));
|
||||
position.setXYZ(idx++, vc.x, vc.y, vc.z);
|
||||
position.setXYZ(idx++, _v2.x, _v2.y, _v2.z);
|
||||
}
|
||||
position.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new FaceNormalsHelper(obj);
|
||||
},
|
||||
|
||||
vertexNormals(mesh) {
|
||||
const _v1 = new Vector3();
|
||||
const _v2 = new Vector3();
|
||||
const _normalMatrix = new Matrix3();
|
||||
|
||||
class VertexNormalsHelper extends LineSegments {
|
||||
constructor(object, size = 1, color = 0xff0000) {
|
||||
const objGeometry = object.geometry;
|
||||
const nNormals = objGeometry.attributes.normal.count;
|
||||
const geometry = new BufferGeometry();
|
||||
const positions = new Float32BufferAttribute(nNormals * 2 * 3, 3);
|
||||
geometry.setAttribute('position', positions);
|
||||
super(geometry, new LineBasicMaterial({
|
||||
color, toneMapped: false
|
||||
}));
|
||||
this.object = object;
|
||||
this.size = size;
|
||||
this.type = 'VertexNormalsHelper';
|
||||
this.matrixAutoUpdate = false;
|
||||
this._no_bounds = true;
|
||||
this.update();
|
||||
}
|
||||
|
||||
update() {
|
||||
this.object.updateMatrixWorld(true);
|
||||
const position = this.geometry.attributes.position;
|
||||
const objGeometry = this.object.geometry;
|
||||
const objArr = objGeometry.attributes.position.array;
|
||||
const normArr = objGeometry.attributes.normal.array;
|
||||
let j=0;
|
||||
for (let idx = 0, jl = objArr.length; j < jl; ) {
|
||||
_v1.set(objArr[j++], objArr[j++], objArr[j++]);
|
||||
_v2.set(normArr[j-3], normArr[j-2], normArr[j-1]);
|
||||
_v2.multiplyScalar(this.size).add(_v1);
|
||||
position.setXYZ(idx++, _v1.x, _v1.y, _v1.z);
|
||||
position.setXYZ(idx++, _v2.x, _v2.y, _v2.z);
|
||||
}
|
||||
position.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new VertexNormalsHelper(mesh);
|
||||
},
|
||||
|
||||
facesToGroups(faces) {
|
||||
let groups = [];
|
||||
if (faces && faces.length) {
|
||||
faces = faces.sort((a,b) => a - b).slice();
|
||||
let first = faces.shift();
|
||||
if (first > 0) {
|
||||
groups.push({ start: 0, count: first, mat: 0 });
|
||||
}
|
||||
let range = { start: first, count: 1, mat: 1 };
|
||||
groups.push(range);
|
||||
for (let face of faces) {
|
||||
if (face === range.start + range.count) {
|
||||
range.count++;
|
||||
} else {
|
||||
groups.push(range = { start: range.start + range.count, count: face - range.start - range.count });
|
||||
groups.push(range = { start: face, count: 1, mat: 1 });
|
||||
}
|
||||
}
|
||||
groups.push({ start: range.start + range.count, count: Infinity });
|
||||
} else {
|
||||
groups.push({ start: 0, count: Infinity });
|
||||
}
|
||||
return groups;
|
||||
},
|
||||
|
||||
toSharedF32(float32) {
|
||||
if (float32.buffer instanceof SharedArrayBuffer) {
|
||||
return float32;
|
||||
}
|
||||
let { buffer } = float32;
|
||||
let shared = new Float32Array(new SharedArrayBuffer(buffer.byteLength));
|
||||
shared.set(float32);
|
||||
return shared;
|
||||
},
|
||||
|
||||
toLocal32(shared32) {
|
||||
if (!(shared32.buffer instanceof SharedArrayBuffer)) {
|
||||
return shared32;
|
||||
}
|
||||
let local = new Float32Array(shared32.length);
|
||||
local.set(shared32);
|
||||
return local;
|
||||
},
|
||||
|
||||
diagram(object, depth = 0) {
|
||||
let pre = '|'.padStart(depth * 3, '-');
|
||||
let ent = Object.entries(object).sort((a,b) => a[0] > b[0] ? 1 : -1);
|
||||
for (let [key, v] of ent) {
|
||||
console.log(pre, key);
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||
util.diagram(v, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { util };
|
||||
574
src2/mesh/work.js
Normal file
574
src2/mesh/work.js
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE;
|
||||
import { license as motoLicense } from '../moto/license.js';
|
||||
import { client as motoClient } from '../moto/client.js';
|
||||
import { worker as motoWorker } from '../moto/worker.js';
|
||||
import { util as meshUtil } from './util.js';
|
||||
import { tool as meshTool } from './tool.js';
|
||||
import { CSG, newPoint, newPolygon, sliceConnect, polygons } from '../geo/base.js';
|
||||
import { line as geoLine } from '../geo/line.js';
|
||||
import { point as geoPoint } from '../geo/point.js';
|
||||
import { polygon as geoPolygon } from '../geo/polygon.js';
|
||||
import { polygons as geoPolygons } from '../geo/polygons.js';
|
||||
import { bounds as geoBounds } from '../geo/bounds.js';
|
||||
import { slicer as geoSlicer } from '../geo/slicer.js';
|
||||
import { csg as geoCsg } from '../geo/csg.js';
|
||||
import { broker } from '../moto/broker.js';
|
||||
import { api } from './api.js';
|
||||
import { model } from './model.js';
|
||||
|
||||
const cache = {};
|
||||
|
||||
// add scoped access to cache
|
||||
const work = { cache };
|
||||
|
||||
// start worker pool (disabled for now with *0)
|
||||
motoClient.start(`/code/mesh_pool?${license.version}`, motoClient.max() * 0);
|
||||
|
||||
function log(msg) {
|
||||
return motoWorker.publish("mesh.log", msg);
|
||||
}
|
||||
|
||||
function cacheUpdate(id, data) {
|
||||
return Object.assign(cache[id], data);
|
||||
}
|
||||
|
||||
// translate original mesh vertices into UI world view (PI/2 rotation on X)
|
||||
function translate_encode(id) {
|
||||
let rec = cache[id];
|
||||
let { pos } = rec;
|
||||
let mkey = pos.map(v => v.round(6)).join('-');
|
||||
// re-translate on missing cache or changed position
|
||||
if (!rec || !rec.trans || rec.mkey !== mkey) {
|
||||
let geo = rec.geo.clone().translate(new Vector3().fromArray(pos));
|
||||
rec.mkey = mkey;
|
||||
rec.trans = geo.attributes.position.array
|
||||
}
|
||||
return rec.trans;
|
||||
}
|
||||
|
||||
function analyze(id, opt = {}) {
|
||||
let rec = cache[id];
|
||||
let { geo, tool } = rec;
|
||||
if (!tool) {
|
||||
tool = rec.tool = new meshTool(opt);
|
||||
}
|
||||
if (tool.faces) {
|
||||
log(`${id} | analysis cached`);
|
||||
} else {
|
||||
log(`${id} | analyzing...`);
|
||||
tool.generateFaces(geo.attributes.position.array, opt);
|
||||
log(`${id} | patching...`);
|
||||
tool.patch(opt);
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
function isolateBodies(id) {
|
||||
let tool = indexFaces(id);
|
||||
log(`${id} | isolating bodies`);
|
||||
return tool.isolateBodies();
|
||||
}
|
||||
|
||||
function indexFaces(id) {
|
||||
let rec = cache[id];
|
||||
let { geo, tool } = rec;
|
||||
if (!tool) {
|
||||
tool = rec.tool = new meshTool();
|
||||
}
|
||||
if (!tool.indexed) {
|
||||
log(`${id} | indexing mesh`);
|
||||
tool.index(geo.attributes.position.array);
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
// return an array of edges on the open spaces left by the split
|
||||
// to be joined into polys, earcut, and turned into patching faces
|
||||
function splitFindEdges(z, o1, o1p) {
|
||||
let edges = [];
|
||||
// find unshared edges on Z plane
|
||||
o1p.forEach((face, i) => {
|
||||
face = face.filter(v => v.z === z);
|
||||
if (face.length >= 2) {
|
||||
edges.push({ p1: face[0], p2: face[1], i });
|
||||
}
|
||||
if (face.length > 2) {
|
||||
edges.push({ p1: face[1], p2: face[2], i });
|
||||
edges.push({ p1: face[2], p2: face[0], i });
|
||||
}
|
||||
});
|
||||
o1.forEach((face, i) => {
|
||||
face = face.filter(v => v.z === z);
|
||||
if (face.length >= 2) {
|
||||
edges.push({ p1: face[0], p2: face[1] });
|
||||
}
|
||||
if (face.length > 2) {
|
||||
edges.push({ p1: face[1], p2: face[2] });
|
||||
edges.push({ p1: face[2], p2: face[0] });
|
||||
}
|
||||
});
|
||||
// eliminate edges that show up twice since it means they're shared
|
||||
outer: for (let i = 0, l = edges.length; i < l; i++) {
|
||||
for (let j = i + 1; j < l; j++) {
|
||||
let e1 = edges[i];
|
||||
let e2 = edges[j];
|
||||
if (!(e1 && e2)) {
|
||||
continue;
|
||||
}
|
||||
let m1 = (e1.p1 === e2.p1 && e1.p2 === e2.p2);
|
||||
let m2 = (e1.p2 === e2.p1 && e1.p1 === e2.p2);
|
||||
if (m1 || m2) {
|
||||
edges[i] = edges[j] = undefined;
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges.filter(e => e);
|
||||
}
|
||||
|
||||
function splitHeal(z, o1, edges, rev) {
|
||||
// filter and convert Vector3 to Point for sliceConnect()
|
||||
edges = edges.filter(e => e).map(e => {
|
||||
return {
|
||||
p1: newPoint().move(e.p1),
|
||||
p2: newPoint().move(e.p2),
|
||||
}
|
||||
});
|
||||
if (edges.length) {
|
||||
// heal unshared edges created along Z split
|
||||
// normals (from point array) are reversed for the bottom split
|
||||
let heal = polygons.nest(sliceConnect(edges, z, { dirty: true }));
|
||||
let ear = heal.map(poly => poly.earcut()).flat();
|
||||
let o1p = ear.map(poly => {
|
||||
return (rev ? poly.points.reverse() : poly.points).map(p => [ p.x, p.y, p.z ])
|
||||
});
|
||||
o1.appendAll(o1p.flat().flat());
|
||||
// console.log({ edges, heal, ear, o1 });
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
const model = {
|
||||
load(data) {
|
||||
let { vertices, name, id } = data;
|
||||
let geo = new BufferGeometry();
|
||||
geo.setAttribute('position', new BufferAttribute(vertices, 3));
|
||||
cacheUpdate(id, { name, geo, mkey: undefined, tool: undefined });
|
||||
},
|
||||
|
||||
// merge several model vertices into a single array
|
||||
merge(recs) {
|
||||
let arrays = recs.map(rec => translate_encode(rec.id, rec.matrix));
|
||||
let length = arrays.map(a => a.length).reduce((v,a) => v + a);
|
||||
let data = new Float32Array(length);
|
||||
for (let i=0, l=arrays.length, p=0; i<l; ) {
|
||||
data.set(arrays[i], p);
|
||||
p += arrays[i++].length;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
union(ids) {
|
||||
let arrays = ids.map(id => translate_encode(id));
|
||||
let solids = arrays.map(a => CSG.fromPositionArray(a));
|
||||
let union = CSG.union(...solids);
|
||||
return CSG.toPositionArray(union);
|
||||
},
|
||||
|
||||
difference(ids) {
|
||||
let arrays = ids.map(id => translate_encode(id));
|
||||
let solids = arrays.map(a => CSG.fromPositionArray(a));
|
||||
let diff = CSG.difference(...solids);
|
||||
return CSG.toPositionArray(diff);
|
||||
},
|
||||
|
||||
intersect(ids) {
|
||||
let arrays = ids.map(id => translate_encode(id));
|
||||
let solids = arrays.map(a => CSG.fromPositionArray(a));
|
||||
let union = CSG.intersect(...solids);
|
||||
return CSG.toPositionArray(union);
|
||||
},
|
||||
|
||||
subtract(recs) {
|
||||
let bases = recs.filter(rec => !rec.tool)
|
||||
.map(rec => translate_encode(rec.id))
|
||||
.map(a => CSG.fromPositionArray(a));
|
||||
let tools = recs.filter(rec => rec.tool)
|
||||
.map(rec => translate_encode(rec.id))
|
||||
.map(a => CSG.fromPositionArray(a));
|
||||
let diff = CSG.difference(...bases, ...tools);
|
||||
return CSG.toPositionArray(diff);
|
||||
},
|
||||
|
||||
// split a model along an axis at a given point
|
||||
// return two arrays of vertices for each resulting object
|
||||
split(data) {
|
||||
let { id, z } = data;
|
||||
let scale = 100000;
|
||||
z = Math.round(z * scale) | 0;
|
||||
let pos = data.pos || translate_encode(id);
|
||||
let o1 = []; // new bottom
|
||||
let o2 = []; // new top
|
||||
let o1p = []; // o1 new split faces
|
||||
let o2p = []; // o2 new split faces
|
||||
let on = [];
|
||||
let over = [];
|
||||
let under = [];
|
||||
let cache = {}; // vertex dedup cache
|
||||
function sort(v) {
|
||||
if (v.z < z) return under.push(v);
|
||||
if (v.z > z) return over.push(v);
|
||||
on.push(v);
|
||||
}
|
||||
function lerp(v1, v2) {
|
||||
let zd = Math.abs(v1.z - v2.z);
|
||||
let z1 = Math.abs(v1.z - z);
|
||||
let v3 = v1.clone().lerp(v2, z1/zd);
|
||||
return newV(v3.x/scale, v3.y/scale, v3.z/scale);
|
||||
}
|
||||
function newV() {
|
||||
let args = [...arguments].map(v => Math.round(v * scale) | 0);
|
||||
let key = args.join(':');
|
||||
let cached = cache[key];
|
||||
if (!cached) {
|
||||
let [ x, y, z ] = args;
|
||||
cache[key] = cached = new Vector3(x,y,z);
|
||||
} else {
|
||||
// console.log('cache hit', cached, key);
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
// todo put proposed faces into top or bottom arrays
|
||||
// check proposed faces that have one point shared on only two edges
|
||||
// and if they're coplanar, merge them. these points cause non-manifold
|
||||
for (let i=0, l=pos.length; i<l; ) {
|
||||
on.length = over.length = under.length = 0;
|
||||
let v1 = newV(pos[i++], pos[i++], pos[i++]);
|
||||
let v2 = newV(pos[i++], pos[i++], pos[i++]);
|
||||
let v3 = newV(pos[i++], pos[i++], pos[i++]);
|
||||
sort(v1);
|
||||
sort(v2);
|
||||
sort(v3);
|
||||
let onl = on.length;
|
||||
let overl = over.length;
|
||||
let underl = under.length;
|
||||
let isover = (onl + overl === 3);
|
||||
let isunder = (onl + underl === 3);
|
||||
if (onl === 3) {
|
||||
// co-planar
|
||||
let tri = new Triangle(...on);
|
||||
let norm = tri.getNormal(new Vector3());
|
||||
if (norm.z > 0) {
|
||||
isover = false;
|
||||
isunder = true;
|
||||
} else {
|
||||
isover = true;
|
||||
isunder = false;
|
||||
}
|
||||
}
|
||||
if (isover) {
|
||||
// all points on or over
|
||||
o2.push([ v1, v2, v3 ]);
|
||||
} else if (isunder) {
|
||||
// all points on or under
|
||||
o1.push([ v1, v2, v3 ]);
|
||||
} else {
|
||||
let g1, g2, oa, ua;
|
||||
if (overl === 2) {
|
||||
// two over, one under
|
||||
g1 = o2p;
|
||||
g2 = o1p;
|
||||
oa = over;
|
||||
ua = under;
|
||||
} else if (underl === 2) {
|
||||
// two under, one over
|
||||
g1 = o1p;
|
||||
g2 = o2p;
|
||||
oa = under;
|
||||
ua = over;
|
||||
} else if (onl === 1) {
|
||||
// one on, one over, one under
|
||||
let p1 = over[0];
|
||||
let p2 = on[0];
|
||||
let p3 = under[0];
|
||||
let p4 = lerp(p1, p3);
|
||||
let cw = (v1 === p1 && v2 === p2)
|
||||
|| (v1 === p2 && v2 === p3)
|
||||
|| (v1 === p3 && v2 === p1);
|
||||
// clockwise vs counter-clockwise
|
||||
if (cw) {
|
||||
o1p.push([ p2, p3, p4 ]);
|
||||
o2p.push([ p1, p2, p4 ]);
|
||||
} else {
|
||||
o1p.push([ p3, p2, p4 ]);
|
||||
o2p.push([ p2, p1, p4 ]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let [ p1, p2 ] = oa;
|
||||
let p3 = ua[0] || on[0]; // lone point
|
||||
let m1 = lerp(p1, p3);
|
||||
let m2 = lerp(p2, p3);
|
||||
if (v2 === ua[0]) {
|
||||
// reverse when the mid point gap
|
||||
g1.push([ m1, p2, p1 ]);
|
||||
g1.push([ m1, m2, p2 ]);
|
||||
g2.push([ p3, m2, m1 ]);
|
||||
} else {
|
||||
g1.push([ p1, p2, m1 ]);
|
||||
g1.push([ p2, m2, m1 ]);
|
||||
g2.push([ m1, m2, p3 ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let e1 = splitFindEdges(z, o1, o1p);
|
||||
let e2 = splitFindEdges(z, o2, o2p);
|
||||
// merge o1p and o2p into o1 and o2
|
||||
o1.appendAll(o1p.filter(e => e));
|
||||
o2.appendAll(o2p.filter(e => e));
|
||||
// flatten output points to arrays
|
||||
o1 = o1.flat().map(e => [ ...e ]).flat();
|
||||
o2 = o2.flat().map(e => [ ...e ]).flat();
|
||||
// // filter and convert Vector3 to Point for sliceConnect()
|
||||
splitHeal(z, o1, e1);
|
||||
splitHeal(z, o2, e2, true);
|
||||
return {
|
||||
o1: o1.map(v => v/scale).toFloat32(),
|
||||
o2: o2.map(v => v/scale).toFloat32()
|
||||
};
|
||||
},
|
||||
|
||||
analyze(data) {
|
||||
let { id, opt } = data;
|
||||
let tool = analyze(id, { mapped: true, ...opt });
|
||||
let { stats, mapped } = tool;
|
||||
let { cull, dups, faces } = stats;
|
||||
log(`${id} | face count=${faces} cull=${cull} dup=${dups}`);
|
||||
log(`${id} | open loops=${tool.loops.length} edges=${tool.edges.length}`);
|
||||
return { stats, mapped };
|
||||
},
|
||||
|
||||
heal(data) {
|
||||
let { id, opt } = data;
|
||||
let tool = analyze(id, opt);
|
||||
log(`${id} | unrolling...`);
|
||||
let zlist = tool.listZ();
|
||||
let unrolled = tool.unrolled();
|
||||
return { vertices: unrolled.toFloat32(), zlist };
|
||||
},
|
||||
|
||||
flatten(data) {
|
||||
let { id, faces } = data;
|
||||
let tool = analyze(id, { });
|
||||
log(`${id} | flatten...`);
|
||||
let { unique, average } = tool.flattenZ(faces);
|
||||
log(`${id} | average=${average.round(3)} uniques=${unique.length}`);
|
||||
let unrolled = tool.unrolled();
|
||||
return { vertices: unrolled.toFloat32() };
|
||||
},
|
||||
|
||||
indexFaces(data) {
|
||||
let { id, opt } = data;
|
||||
let tool = indexFaces(id);
|
||||
return { mapped: true };
|
||||
},
|
||||
|
||||
isolate(data) {
|
||||
let { id } = data;
|
||||
return isolateBodies(id);
|
||||
},
|
||||
|
||||
// given model and point, locate matching vertices, lines, and faces
|
||||
select(data) {
|
||||
let { id, x, y, z, a, b, c, surface } = data;
|
||||
let { radians, radius, filterZ } = surface;
|
||||
const rec = cache[id];
|
||||
const arr = rec.geo.attributes.position.array;
|
||||
// distance tolerance for click to vertex (rough distance)
|
||||
const eps = radius || 0.2;
|
||||
const faces = [];
|
||||
const verts = [];
|
||||
const edges = [];
|
||||
let point;
|
||||
if (!radians)
|
||||
for (let i=0, l=arr.length; i<l; ) {
|
||||
// matches here are within radius of a vertex
|
||||
// select all faces that share a matched vertex
|
||||
let vert = i/3; // vertex index
|
||||
let face = (i/9) | 0; // face index
|
||||
let ax = arr[i++];
|
||||
let ay = arr[i++];
|
||||
let az = arr[i++];
|
||||
let dx = Math.abs(ax - x);
|
||||
let dy = Math.abs(ay - y);
|
||||
let dz = Math.abs(az - z);
|
||||
if (dx < eps && dy < eps && dz < eps) {
|
||||
faces.addOnce(face);
|
||||
verts.push(vert);
|
||||
// console.log(`match @ ${i-3} = ${face}`, ax, ay, az);
|
||||
}
|
||||
}
|
||||
// no matches and we look at the line segments from the provided face
|
||||
// to see if x,y,z point was on or near that line. then select the
|
||||
// two faces shared by that line
|
||||
if (faces.length === 0) {
|
||||
// todo or not todo
|
||||
}
|
||||
// if no lines match, select the provided face (from min vertex index)
|
||||
if (faces.length === 0) {
|
||||
faces.push(Math.min(a,b,c) / 3);
|
||||
}
|
||||
// if the geometry has indexed faces and radians are set, find surface
|
||||
const tool = rec.tool;
|
||||
if (tool && tool.indexed && radians) {
|
||||
const match = tool.findConnectedSurface(faces, radians, filterZ);
|
||||
return { faces: match, edges, verts, point };
|
||||
}
|
||||
return { faces, edges, verts, point };
|
||||
},
|
||||
|
||||
gen_gear(data, send) {
|
||||
const { teeth, module, angle, twist, shaft, offset, height, chamfer } = data;
|
||||
const { gear, pitch } = new meshTool().generateGear(teeth, module, angle, offset);
|
||||
const points = gear.map(v => [...v, 0]).flat();
|
||||
const poly = newPolygon().addVerts(points);
|
||||
if (shaft) {
|
||||
const nump = Math.min(30 + shaft, 150);
|
||||
const srad = shaft / 2;
|
||||
poly.addInner( newPolygon().centerCircle({ x:0, y:0, z:0 }, srad, nump) );
|
||||
}
|
||||
const zh = height || 15;
|
||||
const verts = poly.extrude(zh, { chamfer });
|
||||
if (twist) {
|
||||
const rad = base.util.toRadians(twist);
|
||||
for (let i=0; i<verts.length; i += 3) {
|
||||
let [x, y] = base.util.rotate(
|
||||
verts[i],
|
||||
verts[i+1],
|
||||
rad * (verts[i+2] / zh)
|
||||
);
|
||||
verts[i] = x;
|
||||
verts[i+1] = y;
|
||||
}
|
||||
}
|
||||
send.done(verts);
|
||||
},
|
||||
|
||||
gen_threads(data, send) {
|
||||
let { height, radius, turns, depth, steps, taper } = data;
|
||||
let zstep = height / turns;
|
||||
height += zstep * 2;
|
||||
turns += 2;
|
||||
let verts = new meshTool().generateThreads(
|
||||
height,
|
||||
radius,
|
||||
turns,
|
||||
depth,
|
||||
steps,
|
||||
taper
|
||||
);
|
||||
// return send.done(verts);
|
||||
let s0 = model.split({
|
||||
z: zstep,
|
||||
pos: verts
|
||||
})
|
||||
let s1 = model.split({
|
||||
z: height - zstep,
|
||||
pos: s0.o2
|
||||
});
|
||||
send.done(s1.o1);
|
||||
},
|
||||
|
||||
add(data) {
|
||||
let { id, model } = data;
|
||||
},
|
||||
|
||||
remove(data) {
|
||||
let { id, model } = data;
|
||||
},
|
||||
|
||||
meta(data) {
|
||||
let { id, meta } = data;
|
||||
cacheUpdate(id, meta);
|
||||
},
|
||||
|
||||
create(data) {
|
||||
let { id, type } = data;
|
||||
cache[id] = { id, type };
|
||||
},
|
||||
|
||||
destroy(data) {
|
||||
delete cache[data.id];
|
||||
},
|
||||
|
||||
export(data, send) {
|
||||
let header = `# Generated By Mesh:Tool @ https://grid.space/mesh (units = millimeters)`;
|
||||
let { format, recs } = data;
|
||||
let vtot = 0;
|
||||
for (let rec of recs) {
|
||||
let { id } = rec;
|
||||
let vs = rec.varr = Array.from(translate_encode(id)).map(v => v.round(5));
|
||||
vtot += (vs.length / 3);
|
||||
}
|
||||
switch (format) {
|
||||
case "obj":
|
||||
let p = 1;
|
||||
let obj = [header];
|
||||
for (let rec of recs) {
|
||||
let { file, varr } = rec;
|
||||
obj.push(`g ${file}`);
|
||||
for (let i=0; i<varr.length; p += 3) {
|
||||
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
|
||||
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
|
||||
obj.push(`v ${varr[i++]} ${varr[i++]} ${varr[i++]}`);
|
||||
obj.push(`f ${p} ${p+1} ${p+2}`);
|
||||
}
|
||||
}
|
||||
return obj.join('\n');
|
||||
case "stl":
|
||||
let stl = new Uint8Array(80 + 4 + vtot/3 * 50);
|
||||
let dat = new DataView(stl.buffer);
|
||||
let pos = 84;
|
||||
header.split('').forEach((c,i) => {
|
||||
dat.setUint8(i, c.charCodeAt(0));
|
||||
});
|
||||
// todo put Mesh:Tool info in header
|
||||
dat.setInt32(80, vtot/3, true);
|
||||
for (let rec of recs) {
|
||||
let { id, matrix, file, varr } = rec;
|
||||
for (let i=0, l=varr.length; i<l;) {
|
||||
let p0 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
||||
let p1 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
||||
let p2 = new Vector3(varr[i++], varr[i++], varr[i++]);
|
||||
let norm = computeFaceNormal(p0, p1, p2);
|
||||
dat.setFloat32(pos + 0, norm.x, true);
|
||||
dat.setFloat32(pos + 4, norm.y, true);
|
||||
dat.setFloat32(pos + 8, norm.z, true);
|
||||
dat.setFloat32(pos + 12, p0.x, true);
|
||||
dat.setFloat32(pos + 16, p0.y, true);
|
||||
dat.setFloat32(pos + 20, p0.z, true);
|
||||
dat.setFloat32(pos + 24, p1.x, true);
|
||||
dat.setFloat32(pos + 28, p1.y, true);
|
||||
dat.setFloat32(pos + 32, p1.z, true);
|
||||
dat.setFloat32(pos + 36, p2.x, true);
|
||||
dat.setFloat32(pos + 40, p2.y, true);
|
||||
dat.setFloat32(pos + 44, p2.z, true);
|
||||
pos += 50;
|
||||
}
|
||||
}
|
||||
return stl;
|
||||
default:
|
||||
throw `invalid format "${format}"`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function debug() {
|
||||
console.log({work_cache: cache});
|
||||
}
|
||||
|
||||
export { work, log, model, debug };
|
||||
75
src2/moto/ajax.js
Normal file
75
src2/moto/ajax.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { local } from '../data/local.js';
|
||||
|
||||
function rnd() {
|
||||
return Math.round(Math.random()*0xffffffff).toString(36);
|
||||
}
|
||||
|
||||
const KEY = "moto-ajax";
|
||||
const MOID = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd());
|
||||
|
||||
local.setItem(KEY, MOID);
|
||||
|
||||
const STATES = [
|
||||
"request not initialized", // 0
|
||||
"server connection established", // 1
|
||||
"request recieved", // 2
|
||||
"processing request", // 3
|
||||
"request complete" // 4
|
||||
];
|
||||
|
||||
class Ajax {
|
||||
constructor(callback, responseType) {
|
||||
this.ajax = new XMLHttpRequest();
|
||||
this.ajax.onreadystatechange = this.onStateChange.bind(this);
|
||||
this.ajax.withCredentials = true;
|
||||
this.state = STATES[0];
|
||||
this.callback = callback;
|
||||
this.responseType = responseType;
|
||||
}
|
||||
|
||||
onStateChange() {
|
||||
this.state = STATES[this.ajax.readyState];
|
||||
if (this.ajax.readyState === 4 && this.callback) {
|
||||
let status = this.ajax.status;
|
||||
if (status >= 200 && status < 300) {
|
||||
this.callback(this.ajax.responseType ? this.ajax.response : this.ajax.responseText, this.ajax);
|
||||
} else {
|
||||
this.callback(null, this.ajax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request(url, send, headers) {
|
||||
this.ajax.open(send ? "POST" : "GET", url, true);
|
||||
if (this.responseType) this.ajax.responseType = this.responseType;
|
||||
headers = headers || {};
|
||||
headers["X-Moto-Ajax"] = MOID;
|
||||
for (let k in headers) {
|
||||
this.ajax.setRequestHeader(k, headers[k]);
|
||||
}
|
||||
if (send) {
|
||||
this.ajax.send(send);
|
||||
} else {
|
||||
this.ajax.send();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function call(url, handler) {
|
||||
return new Ajax(handler).request(url);
|
||||
}
|
||||
|
||||
function restore(id) {
|
||||
MOID = id;
|
||||
local.setItem(KEY, MOID);
|
||||
}
|
||||
|
||||
export const ajax = {
|
||||
new: (cb, rt) => new Ajax(cb, rt),
|
||||
call,
|
||||
restore,
|
||||
id: MOID
|
||||
};
|
||||
136
src2/moto/broker.js
Normal file
136
src2/moto/broker.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
class Broker {
|
||||
constructor() {
|
||||
this.topics = {};
|
||||
this.used = {};
|
||||
this.send = {};
|
||||
// using the pattern "broker.send(msg)" induces runtime errors
|
||||
// when there is not at least one registered listener for a topic
|
||||
// while also allowing for a more natural function call interface
|
||||
// and guarding against typos in topic names
|
||||
}
|
||||
|
||||
topics() {
|
||||
return Object.keys(this.topics);
|
||||
}
|
||||
|
||||
// create subscriptions for all functions in an object
|
||||
// a complement wrapObject on the subscription side
|
||||
listeners(object, root) {
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
if (typeof fn === 'function') {
|
||||
key = root ? `${root}_${key}` : key;
|
||||
this.subscribe(key, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attach function to a topic
|
||||
// creates topic if new topic
|
||||
// creates a send function if new topic
|
||||
subscribe(topic, listener) {
|
||||
if (Array.isArray(topic)) {
|
||||
for (let t of topic) {
|
||||
this.subscribe(t, listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
if (typeof topic !== 'string') {
|
||||
console.trace({invalid_topic: topic});
|
||||
return;
|
||||
}
|
||||
let topics = this.topics;
|
||||
let channel = topics[topic];
|
||||
if (!channel) {
|
||||
channel = topics[topic] = [];
|
||||
}
|
||||
if (channel.indexOf(listener) < 0) {
|
||||
let send = this.send;
|
||||
let name = topic.replace(/[\\ \.-]/g, '_');
|
||||
send[name] = send[name] = this.bind(topic);
|
||||
channel.push(listener);
|
||||
this.publish(".topic.add", topic);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unsubscribe(topic, listener) {
|
||||
let channel = this.topics[topic];
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
let index = channel.indexOf(listener);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
channel.splice(index,1);
|
||||
if (channel.length === 0) {
|
||||
delete this.topics[topic];
|
||||
this.publish(".topic.remove", topic);
|
||||
}
|
||||
}
|
||||
|
||||
publish(topic, message, options = {}) {
|
||||
if (Array.isArray(topic)) {
|
||||
for (let t of topic) {
|
||||
this.publish(t, message, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (topic !== ".topic.publish") {
|
||||
this.publish(".topic.publish", {topic, message, options});
|
||||
}
|
||||
// store last seen message on a topic
|
||||
// acts as a tracker for all used topics
|
||||
this.used[topic] = message;
|
||||
let channel = this.topics[topic];
|
||||
if (channel && channel.length) {
|
||||
for (let listener of channel) {
|
||||
try {
|
||||
listener(message, topic, options);
|
||||
} catch (e) {
|
||||
// console.log({listener_error: e, topic, options, listener});
|
||||
setTimeout(() => { throw e }, 1);
|
||||
}
|
||||
}
|
||||
} else if (topic.indexOf(".topic.") < 0) {
|
||||
if (self.dbug) {
|
||||
self.dbug.debug(`undelivered '${topic}'`, message);
|
||||
} else {
|
||||
console.log(`undelivered '${topic}'`, message);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
// return function bound to a topic for publishing
|
||||
bind(topic, message, options) {
|
||||
let broker = this;
|
||||
return function(msg, opt) {
|
||||
broker.publish(topic, message || msg, options || opt);
|
||||
}
|
||||
}
|
||||
|
||||
// return function wrapper that publishes a message to a topic
|
||||
// the published message is the return data from the function call
|
||||
wrap(topic, fn) {
|
||||
let broker = this;
|
||||
return function() {
|
||||
return broker.publish(topic, fn(...arguments));
|
||||
}
|
||||
}
|
||||
|
||||
// bind all functions in an object to a topic root
|
||||
// functions are replaced with wrappers that publish call results
|
||||
// a natural use case is wrapping api objects
|
||||
wrapObject(object, root) {
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
if (typeof fn === 'function') {
|
||||
object[key] = this.wrap(`${root}_${key}`, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const broker = new Broker();
|
||||
253
src2/moto/client.js
Normal file
253
src2/moto/client.js
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { broker } from './broker.js';
|
||||
|
||||
let ccvalue = self.navigator ? navigator.hardwareConcurrency || 1 : 1,
|
||||
ccmax = ccvalue > 3 ? ccvalue - 1 : 1,
|
||||
workcc = false, // concurrent or not
|
||||
workurl = null, // url to load worker
|
||||
worknum = 1, // number of workers to start
|
||||
workers = [], // array of all workers
|
||||
queue = [], // array of work requests
|
||||
topics = {}; // topic handler (broker lite for worker > client msgs)
|
||||
|
||||
function dispatch(topic, message) {
|
||||
for (let fn of topics[topic] || []) {
|
||||
fn(message);
|
||||
}
|
||||
}
|
||||
|
||||
// code is running in the browser / client context
|
||||
const client = {
|
||||
/** @returns [integer] total number of workers **/
|
||||
live: () => {
|
||||
return workers.length;
|
||||
},
|
||||
|
||||
/** @returns [integer] number of idle workers **/
|
||||
free: () => {
|
||||
return workers.filter(w => w.run === null).length;
|
||||
},
|
||||
|
||||
/** @returns [integer] max cpu cores available **/
|
||||
max: () => {
|
||||
return ccmax;
|
||||
},
|
||||
|
||||
/** process worker topic messages like "ready" **/
|
||||
on: (topic, fn) => {
|
||||
(topics[topic] = topics[topic] || []).addOnce(fn);
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} registered worker function map
|
||||
* auto-filled when worker process uses bind() or bindObject()
|
||||
**/
|
||||
fn: {},
|
||||
|
||||
/** binds a function endpoint to a task name **/
|
||||
bind: (task) => {
|
||||
if (!client.fn[task]) {
|
||||
client.fn[task] = (data, options) => {
|
||||
return client.call(task, data, options);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/** add a promised task call to the work queue **/
|
||||
call: (task, data, options = {}) => {
|
||||
let { direct, stream } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
let ondata, ondone, onerror = reject;
|
||||
if (!stream) {
|
||||
let accum = [];
|
||||
let simple = options.simple !== false;
|
||||
ondata = (data) => {
|
||||
accum.push(data);
|
||||
};
|
||||
ondone = () => {
|
||||
resolve(simple && accum.length === 1 ? accum[0] : accum);
|
||||
};
|
||||
} else {
|
||||
ondata = stream;
|
||||
ondone = resolve;
|
||||
}
|
||||
client.queue(task, data, { direct, ondata, ondone, onerror });
|
||||
});
|
||||
},
|
||||
|
||||
/** danger: directly add an entry to the work queue **/
|
||||
queue: (task, data, options = {}) => {
|
||||
queue.push({task, data, options});
|
||||
client.kick();
|
||||
},
|
||||
|
||||
/** check work queue and distribute tasks to free workers **/
|
||||
kick: () => {
|
||||
if (queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rec, worker;
|
||||
outer: for (let qrec of queue) {
|
||||
let { options } = qrec;
|
||||
let { index } = options;
|
||||
if (index >= 0) {
|
||||
// worker designated by index
|
||||
let mod = index % workers.length;
|
||||
if (workers[mod].run === null) {
|
||||
worker = workers[mod];
|
||||
rec = qrec;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// first available worker
|
||||
for (let i=0; i<workers.length; i++) {
|
||||
if (workers[i].run === null) {
|
||||
worker = workers[i];
|
||||
rec = qrec;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rec) {
|
||||
// nothing runnable
|
||||
return;
|
||||
}
|
||||
|
||||
// remove queue item
|
||||
let qindex = queue.indexOf(rec);
|
||||
if (qindex < 0) {
|
||||
throw "invalid queue record";
|
||||
}
|
||||
queue.splice(qindex,1);
|
||||
|
||||
let { task, data, options } = rec;
|
||||
let { direct, ondone, ondata, onerror } = options;
|
||||
|
||||
if (ondone || ondata) {
|
||||
worker.run = { ondone, ondata, onerror };
|
||||
}
|
||||
|
||||
let msg = {
|
||||
task,
|
||||
data,
|
||||
stream: ondata ? true : false
|
||||
};
|
||||
try {
|
||||
worker.postMessage(msg, direct);
|
||||
} catch (error) {
|
||||
console.trace('work send error', {data, error});
|
||||
}
|
||||
},
|
||||
|
||||
// factory can be overridden
|
||||
spawn: function(url = workurl) {
|
||||
if (self.createWorker) {
|
||||
return self.createWorker();
|
||||
} else {
|
||||
return new Worker(workurl = url);
|
||||
}
|
||||
},
|
||||
|
||||
// return number of running requests
|
||||
running: function() {
|
||||
return workers.filter(w => w.run).length;
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
for (let w of workers) {
|
||||
w.terminate();
|
||||
}
|
||||
workers = [];
|
||||
},
|
||||
|
||||
// same as restart
|
||||
start: function(url = workurl, cc = workcc || 1) {
|
||||
workcc = cc;
|
||||
worknum = Math.min(ccmax, workcc);
|
||||
workurl = url;
|
||||
client.restart(url, cc);
|
||||
},
|
||||
|
||||
restart: function() {
|
||||
client.stop();
|
||||
|
||||
// send cancel/error notification to requestors
|
||||
for (let w of workers.filter(w => w.run)) {
|
||||
if (w.run.ondone) {
|
||||
w.run.ondone({error: "cancelled operation"});
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=0; i<worknum; i++) {
|
||||
let worker = client.spawn(workurl);
|
||||
worker.run = null;
|
||||
worker.index = i;
|
||||
worker.onmessage = function(e) {
|
||||
let reply = e.data;
|
||||
|
||||
// handle special case for task binding
|
||||
if (reply.bind) {
|
||||
client.bind(reply.bind);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle special case when worker ready
|
||||
if (reply.ready) {
|
||||
dispatch('ready', reply.ready);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle special case for broker publish
|
||||
if (reply.publish) {
|
||||
broker.publish(reply.publish, reply.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (worker.run === null) {
|
||||
console.log({client_unhandled: e});
|
||||
throw "message on idle worker";
|
||||
}
|
||||
|
||||
let { done, data, error } = reply;
|
||||
let { ondone, ondata, onerror } = worker.run;
|
||||
let handled = false;
|
||||
|
||||
if (error) {
|
||||
onerror(error);
|
||||
worker.run = null;
|
||||
handled = true;
|
||||
client.kick();
|
||||
}
|
||||
|
||||
if (ondata && data) {
|
||||
ondata(data);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (done) {
|
||||
ondone();
|
||||
worker.run = null;
|
||||
handled = true;
|
||||
client.kick();
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
console.log({client_unhandled: e});
|
||||
throw "client unhandled reply";
|
||||
}
|
||||
};
|
||||
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
client.kick();
|
||||
}
|
||||
};
|
||||
|
||||
export { client };
|
||||
9
src2/moto/license.js
Normal file
9
src2/moto/license.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const is_self = typeof(self) !== 'undefined';
|
||||
|
||||
const terms = {
|
||||
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
||||
LICENSE: "See the license.md file included with the source distribution",
|
||||
VERSION: (is_self ? self : this).debug_version || "4.1.9"
|
||||
};
|
||||
|
||||
export const license = terms;
|
||||
642
src2/moto/orbit.js
Normal file
642
src2/moto/orbit.js
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
|
||||
const {
|
||||
Vector3,
|
||||
Vector2,
|
||||
Quaternion,
|
||||
EventDispatcher,
|
||||
MOUSE
|
||||
} = THREE;
|
||||
|
||||
/**
|
||||
* Adapted from THREE.OrbitControls
|
||||
*/
|
||||
class Orbit extends EventDispatcher {
|
||||
constructor(object, domElement, notify, slider) {
|
||||
super();
|
||||
|
||||
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 control orbits around
|
||||
// and where it pans with respect to.
|
||||
this.target = new Vector3();
|
||||
|
||||
// center is old, deprecated; use "target" instead
|
||||
this.center = this.target;
|
||||
|
||||
// This option actually enables dollying in and out; left as "zoom" for
|
||||
// backwards compatibility
|
||||
this.noZoom = false;
|
||||
this.zoomSpeed = 1.0;
|
||||
this.reverseZoom = false;
|
||||
|
||||
// Limits to how far you can dolly in and out
|
||||
this.minDistance = 0;
|
||||
this.maxDistance = Infinity;
|
||||
|
||||
// Set to true to disable this control
|
||||
this.noRotate = false;
|
||||
this.rotateSpeed = 1.0;
|
||||
|
||||
// Set to true to disable this control
|
||||
this.noPan = false;
|
||||
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
|
||||
|
||||
// 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 disable use of the keys
|
||||
this.noKeys = false;
|
||||
|
||||
// The four arrow keys
|
||||
this.keys = {
|
||||
UP: 38,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
BOTTOM: 40
|
||||
};
|
||||
|
||||
// default mouse buttons
|
||||
this.mouseDefault = {
|
||||
ORBIT: MOUSE.LEFT,
|
||||
ZOOM: MOUSE.MIDDLE,
|
||||
PAN: MOUSE.RIGHT
|
||||
};
|
||||
|
||||
// Onshape
|
||||
this.mouseOnshape = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
ZOOM: MOUSE.LEFT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
|
||||
this.mouseButtons = this.mouseDefault;
|
||||
|
||||
this.setMouse = function(bindings) {
|
||||
this.mouseButtons = bindings;
|
||||
};
|
||||
|
||||
let scope = this,
|
||||
domEl = scope.domElement,
|
||||
EPS = 0.000001,
|
||||
rotateStart = new Vector2(),
|
||||
rotateEnd = new Vector2(),
|
||||
rotateDelta = new Vector2(),
|
||||
panStart = new Vector2(),
|
||||
panEnd = new Vector2(),
|
||||
panDelta = new Vector2(),
|
||||
panOffset = new Vector3(),
|
||||
offset = new Vector3(),
|
||||
dollyStart = new Vector2(),
|
||||
dollyEnd = new Vector2(),
|
||||
dollyDelta = new Vector2(),
|
||||
theta,
|
||||
thetaDelta = 0,
|
||||
thetaSet = null,
|
||||
phi,
|
||||
phiDelta = 0,
|
||||
phiSet = null,
|
||||
scale = 1,
|
||||
scaleSave = 1,
|
||||
pan = new Vector3(),
|
||||
lastPosition = new Vector3(),
|
||||
lastQuaternion = new Quaternion(),
|
||||
// so camera.up is the orbit axis
|
||||
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
||||
quatInverse = quat.clone().invert(),
|
||||
// touch tracking
|
||||
firstTouch = [],
|
||||
touchSlide = false,
|
||||
lastDY = 0,
|
||||
// events
|
||||
changeEvent = { type: 'change'},
|
||||
startEvent = { type: 'start'},
|
||||
endEvent = { type: 'end'};
|
||||
|
||||
let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5},
|
||||
state = STATE.NONE,
|
||||
MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3},
|
||||
mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN;
|
||||
|
||||
// for reset
|
||||
this.target0 = this.target.clone();
|
||||
this.position0 = this.object.position.clone();
|
||||
|
||||
this.rotateLeft = function (angle) {
|
||||
thetaDelta -= angle;
|
||||
};
|
||||
|
||||
this.rotateUp = function (angle) {
|
||||
phiDelta -= angle;
|
||||
};
|
||||
|
||||
// pass in distance in world space to move left
|
||||
this.panLeft = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get X column of matrix
|
||||
panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]);
|
||||
panOffset.multiplyScalar(-distance);
|
||||
pan.add(panOffset);
|
||||
};
|
||||
|
||||
// pass in distance in world space to move up
|
||||
this.panUp = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get Y column of matrix
|
||||
panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]);
|
||||
panOffset.multiplyScalar(distance);
|
||||
pan.add(panOffset);
|
||||
};
|
||||
|
||||
// pass in distance in world space to move backward
|
||||
this.panBack = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get Y column of matrix
|
||||
panOffset.set(te[ 8 ], te[ 9 ], te[ 10 ]);
|
||||
panOffset.multiplyScalar(distance);
|
||||
pan.add(panOffset);
|
||||
};
|
||||
|
||||
// pass in x,y of change desired in pixel space,
|
||||
// right and down are positive
|
||||
this.pan = function (deltaX, deltaY) {
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
switch (mode) {
|
||||
case MODE.PERSPECTIVE:
|
||||
let position = scope.object.position;
|
||||
let offset = position.clone().sub(scope.target);
|
||||
let 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
|
||||
scope.panLeft(2 * deltaX * targetDistance / element.clientHeight);
|
||||
scope.panUp(2 * deltaY * targetDistance / element.clientHeight);
|
||||
break;
|
||||
case MODE.ORTHOGRAPHIC:
|
||||
scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth);
|
||||
scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
this.getTarget = function() {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
this.setTarget = function(t) {
|
||||
this.target.copy(t);
|
||||
};
|
||||
|
||||
function isValue(v) {
|
||||
return v !== undefined && v !== null;
|
||||
}
|
||||
|
||||
function firstValue(choices) {
|
||||
for (let v of choices) {
|
||||
if (isValue(v)) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setZoom = function(reverse, speed) {
|
||||
scope.reverseZoom = reverse;
|
||||
scope.zoomSpeed = speed || 1.0;
|
||||
};
|
||||
|
||||
this.setPosition = function(set) {
|
||||
thetaSet = firstValue([set.left, set.theta, thetaSet]);
|
||||
phiSet = firstValue([set.up, set.phi, phiSet]);
|
||||
if (set.panX) this.target.x = set.panX;
|
||||
if (set.panY) this.target.y = set.panY;
|
||||
if (set.panZ) this.target.z = set.panZ;
|
||||
if (set.scale) scale = set.scale;
|
||||
};
|
||||
|
||||
this.getPosition = function(scaled) {
|
||||
let t = this.target,
|
||||
pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 };
|
||||
return pos;
|
||||
};
|
||||
|
||||
this.dollyIn = function (factor = 1) {
|
||||
let dollyScale = getZoomScale(factor);
|
||||
scale = scale * dollyScale;
|
||||
};
|
||||
|
||||
this.dollyOut = function (factor = 1) {
|
||||
let dollyScale = getZoomScale(factor);
|
||||
scale = scale / dollyScale;
|
||||
};
|
||||
|
||||
this.update = function () {
|
||||
let position = this.object.position;
|
||||
|
||||
offset.copy(position).sub(this.target);
|
||||
|
||||
// rotate offset to "y-axis-is-up" space
|
||||
offset.applyQuaternion(quat);
|
||||
|
||||
// angle from z-axis around y-axis
|
||||
theta = isValue(thetaSet) ? thetaSet : Math.atan2(offset.x, offset.z);
|
||||
|
||||
// angle from y-axis
|
||||
phi = isValue(phiSet) ? phiSet : Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y);
|
||||
|
||||
theta += thetaDelta;
|
||||
phi += phiDelta;
|
||||
|
||||
// restrict theta to be between desired limits
|
||||
theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta));
|
||||
|
||||
// restrict phi to be between desired limits
|
||||
phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi));
|
||||
|
||||
// restrict phi to be betwee EPS and PI-EPS
|
||||
phi = Math.max(EPS, Math.min(Math.PI - EPS, phi));
|
||||
|
||||
let radius = offset.length() * (mode === MODE.PERSPECTIVE ? scale : 1);
|
||||
|
||||
// restrict radius to be between desired limits
|
||||
radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius));
|
||||
|
||||
// move target to panned location
|
||||
this.target.add(pan);
|
||||
|
||||
offset.x = radius * Math.sin(phi) * Math.sin(theta);
|
||||
offset.y = radius * Math.cos(phi);
|
||||
offset.z = radius * Math.sin(phi) * Math.cos(theta);
|
||||
|
||||
// rotate offset back to "camera-up-vector-is-up" space
|
||||
offset.applyQuaternion(quatInverse);
|
||||
|
||||
position.copy(this.target).add(offset);
|
||||
this.object.lookAt(this.target);
|
||||
|
||||
thetaDelta = 0;
|
||||
thetaSet = null;
|
||||
phiDelta = 0;
|
||||
phiSet = null;
|
||||
pan.set(0, 0, 0);
|
||||
|
||||
scaleSave *= scale;
|
||||
|
||||
if (mode === MODE.ORTHOGRAPHIC) {
|
||||
scope.object.zoom = 1/scale;
|
||||
scope.object.updateProjectionMatrix();
|
||||
} else {
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
// update condition is:
|
||||
// min(camera displacement, camera rotation in radians)^2 > EPS
|
||||
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
||||
if (lastPosition.distanceToSquared(this.object.position) > EPS
|
||||
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) {
|
||||
|
||||
this.dispatchEvent(changeEvent);
|
||||
lastPosition.copy(this.object.position);
|
||||
lastQuaternion.copy(this.object.quaternion);
|
||||
if (notify) notify(position, true);
|
||||
} else {
|
||||
if (notify) notify(position, false);
|
||||
}
|
||||
};
|
||||
|
||||
this.reset = function () {
|
||||
state = STATE.NONE;
|
||||
scale = 1;
|
||||
scaleSave = 1;
|
||||
this.target.copy(this.target0);
|
||||
this.object.position.copy(this.position0);
|
||||
};
|
||||
|
||||
this.getPolarAngle = function () {
|
||||
return phi;
|
||||
};
|
||||
|
||||
this.getAzimuthalAngle = function () {
|
||||
return theta
|
||||
};
|
||||
|
||||
function getZoomScale(factor = 1) {
|
||||
return Math.pow(0.95, factor);
|
||||
}
|
||||
|
||||
function onMouseDown(event) {
|
||||
if (scope.enabled === false) return;
|
||||
event.preventDefault();
|
||||
|
||||
switch (event.button) {
|
||||
case scope.mouseButtons.ORBIT:
|
||||
state = event.metaKey ? STATE.PAN : STATE.ROTATE;
|
||||
break;
|
||||
case scope.mouseButtons.ZOOM:
|
||||
state = STATE.DOLLY;
|
||||
break;
|
||||
case scope.mouseButtons.PAN:
|
||||
state = STATE.PAN;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case STATE.ROTATE:
|
||||
if (scope.noRotate === true) return state = STATE.NONE;
|
||||
rotateStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
case STATE.DOLLY:
|
||||
if (scope.noZoom === true) return state = STATE.NONE;
|
||||
dollyStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
case STATE.PAN:
|
||||
if (scope.noPan === true) return state = STATE.NONE;
|
||||
panStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove, false);
|
||||
document.addEventListener('mouseup', onMouseUp, false);
|
||||
scope.dispatchEvent(startEvent);
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (scope.enabled === false) return;
|
||||
event.preventDefault();
|
||||
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
if (state === STATE.ROTATE) {
|
||||
if (scope.noRotate === true) return;
|
||||
|
||||
rotateEnd.set(event.clientX, event.clientY);
|
||||
rotateDelta.subVectors(rotateEnd, rotateStart);
|
||||
|
||||
// rotating across whole screen goes 360 degrees around
|
||||
scope.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
|
||||
scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed);
|
||||
|
||||
rotateStart.copy(rotateEnd);
|
||||
|
||||
} else if (state === STATE.DOLLY) {
|
||||
if (scope.noZoom === true) return;
|
||||
|
||||
dollyEnd.set(event.clientX, event.clientY);
|
||||
dollyDelta.subVectors(dollyEnd, dollyStart);
|
||||
|
||||
if (dollyDelta.y > 0) {
|
||||
scope.dollyIn();
|
||||
} else {
|
||||
scope.dollyOut();
|
||||
}
|
||||
|
||||
dollyStart.copy(dollyEnd);
|
||||
|
||||
} else if (state === STATE.PAN) {
|
||||
if (scope.noPan === true) return;
|
||||
|
||||
panEnd.set(event.clientX, event.clientY);
|
||||
panDelta.subVectors(panEnd, panStart);
|
||||
scope.pan(panDelta.x, panDelta.y);
|
||||
panStart.copy(panEnd);
|
||||
}
|
||||
|
||||
scope.update();
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove, false);
|
||||
document.removeEventListener('mouseup', onMouseUp, false);
|
||||
scope.dispatchEvent(endEvent);
|
||||
state = STATE.NONE;
|
||||
}
|
||||
|
||||
function onMouseWheel(event) {
|
||||
if (scope.enabled === false || scope.noZoom === true) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.shiftKey && slider) {
|
||||
slider(event.deltaX || event.wheelDelta || event.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
slider(null);
|
||||
|
||||
let delta = -(event.deltaY || event.wheelDelta || event.detail || 0);
|
||||
|
||||
if (delta === 0) return;
|
||||
|
||||
if (scope.reverseZoom) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
scope.dollyOut();
|
||||
} else if (delta < 0) {
|
||||
scope.dollyIn();
|
||||
}
|
||||
|
||||
scope.update();
|
||||
scope.dispatchEvent(startEvent);
|
||||
scope.dispatchEvent(endEvent);
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return;
|
||||
|
||||
switch (event.keyCode) {
|
||||
|
||||
case scope.keys.UP:
|
||||
scope.pan(0, scope.keyPanSpeed);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.BOTTOM:
|
||||
scope.pan(0, -scope.keyPanSpeed);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.LEFT:
|
||||
scope.pan(scope.keyPanSpeed, 0);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.RIGHT:
|
||||
scope.pan(-scope.keyPanSpeed, 0);
|
||||
scope.update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function touchstart(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
const touches = [...event.touches];
|
||||
|
||||
switch (event.touches.length) {
|
||||
case 1: // one-fingered touch: rotate
|
||||
if (scope.noRotate === true) return;
|
||||
|
||||
state = STATE.TOUCH_ROTATE;
|
||||
|
||||
rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
break;
|
||||
|
||||
case 2: // two-fingered touch: dolly
|
||||
if (scope.noZoom === true) return;
|
||||
|
||||
state = STATE.TOUCH_DOLLY;
|
||||
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
touchSlide = false;
|
||||
lastDY = 0;
|
||||
|
||||
dollyStart.set(0, distance);
|
||||
break;
|
||||
|
||||
case 3: // three-fingered touch: pan
|
||||
case 4: // four-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
|
||||
state = STATE.TOUCH_PAN;
|
||||
const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length;
|
||||
const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length;
|
||||
|
||||
panStart.set(px, py);
|
||||
break;
|
||||
|
||||
default:
|
||||
state = STATE.NONE;
|
||||
}
|
||||
scope.dispatchEvent(startEvent);
|
||||
}
|
||||
|
||||
function touchmove(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
switch (event.touches.length) {
|
||||
case 1: // one-fingered touch: rotate
|
||||
if (scope.noRotate === true) return;
|
||||
if (state !== STATE.TOUCH_ROTATE) return;
|
||||
|
||||
rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
rotateDelta.subVectors(rotateEnd, rotateStart);
|
||||
|
||||
// rotating across whole screen goes 360 degrees around
|
||||
scope.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
|
||||
scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed);
|
||||
|
||||
rotateStart.copy(rotateEnd);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case 2: // two-fingered touch: dolly
|
||||
if (scope.noZoom === true) return;
|
||||
if (state !== STATE.TOUCH_DOLLY) return;
|
||||
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2;
|
||||
let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2;
|
||||
|
||||
if (scope.reverseZoom) distance = -distance;
|
||||
|
||||
dollyEnd.set(0, distance);
|
||||
dollyDelta.subVectors(dollyEnd, dollyStart);
|
||||
|
||||
if (touchSlide || (Math.abs(dy/dx) < 1 && slider && dx < 200)) {
|
||||
touchSlide = true;
|
||||
slider(lastDY ? (lastDY - ddy) : ddy);
|
||||
lastDY = ddy;
|
||||
} else {
|
||||
if (dollyDelta.y > 0) {
|
||||
scope.dollyOut(0.5);
|
||||
} else if (dollyDelta.y < 0) {
|
||||
scope.dollyIn(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
dollyStart.copy(dollyEnd);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case 3: // three-fingered touch: pan
|
||||
case 4: // four-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
if (state !== STATE.TOUCH_PAN) return;
|
||||
|
||||
const tlist = [...event.touches];
|
||||
const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length;
|
||||
const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length;
|
||||
|
||||
panEnd.set(px, py);
|
||||
panDelta.subVectors(panEnd, panStart);
|
||||
scope.pan(panDelta.x, panDelta.y);
|
||||
panStart.copy(panEnd);
|
||||
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
default:
|
||||
state = STATE.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
function touchend(/* event */) {
|
||||
if (scope.enabled === false) return;
|
||||
scope.dispatchEvent(endEvent);
|
||||
state = STATE.NONE;
|
||||
}
|
||||
|
||||
this.onMouseUp = onMouseUp;
|
||||
|
||||
domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false);
|
||||
domEl.addEventListener('mousedown', onMouseDown, false);
|
||||
domEl.addEventListener('mousewheel', onMouseWheel, false);
|
||||
domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox
|
||||
domEl.addEventListener('touchstart', touchstart, false);
|
||||
domEl.addEventListener('touchend', touchend, false);
|
||||
domEl.addEventListener('touchmove', touchmove, false);
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, false);
|
||||
}
|
||||
}
|
||||
|
||||
export { Orbit };
|
||||
159
src2/moto/service.js
Normal file
159
src2/moto/service.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// code for a ServiceWorker that provides support for running cached/offline
|
||||
|
||||
import { license } from './license.js';
|
||||
|
||||
const version = license.version;
|
||||
const origin = self.location.origin;
|
||||
const stats = {
|
||||
hit: 0,
|
||||
miss: 0,
|
||||
fetch: 0,
|
||||
cache: 0,
|
||||
bypass: 0,
|
||||
preload: 0,
|
||||
timer: undefined
|
||||
};
|
||||
|
||||
function debug() {
|
||||
console.log(`[${version}]`, ...arguments);
|
||||
}
|
||||
|
||||
function update() {
|
||||
clearTimeout(stats.timer);
|
||||
stats.timer = setTimeout(report, 1000);
|
||||
}
|
||||
|
||||
function report() {
|
||||
console.log('sw cache', Object.assign({}, stats));
|
||||
stats.hit = stats.miss = stats.fetch = stats.cache = stats.bypass = stats.preload = 0;
|
||||
}
|
||||
|
||||
debug('sw entry point');
|
||||
|
||||
const addResourcesToCache = async (resources) => {
|
||||
// debug('cache resources', resources);
|
||||
const cache = await caches.open('v'+version);
|
||||
await cache.addAll(resources);
|
||||
};
|
||||
|
||||
const putInCache = async (request, response) => {
|
||||
const cache = await caches.open('v'+version);
|
||||
// debug('cache', request.url);
|
||||
update(stats.cache++);
|
||||
await cache.put(request, response);
|
||||
};
|
||||
|
||||
const cacheFirst = async ({
|
||||
request,
|
||||
preloadResponsePromise,
|
||||
fallbackUrl
|
||||
}) => {
|
||||
// must await this promise when present to avoid errors
|
||||
const preloadResponse = preloadResponsePromise ? await preloadResponsePromise : undefined;
|
||||
|
||||
// try to get the resource from the cache
|
||||
const cache = await caches.open('v'+version);
|
||||
const responseFromCache = await cache.match(request);
|
||||
|
||||
if (responseFromCache) {
|
||||
// debug('cached', request.url);
|
||||
update(stats.hit++);
|
||||
return responseFromCache;
|
||||
} else {
|
||||
// debug('miss', request.url);
|
||||
update(stats.miss++);
|
||||
}
|
||||
|
||||
// try to use the preloaded response, if it's there
|
||||
if (preloadResponse) {
|
||||
update(stats.preload++);
|
||||
debug('using preload response', preloadResponse);
|
||||
putInCache(request, preloadResponse.clone());
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// try to get the resource from the network
|
||||
try {
|
||||
// debug('fetch', request.url);
|
||||
update(fetch.fetch++);
|
||||
const responseFromNetwork = await fetch(request);
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache and serve second one
|
||||
putInCache(request, responseFromNetwork.clone());
|
||||
return responseFromNetwork;
|
||||
} catch (error) {
|
||||
debug({fallback_error: error});
|
||||
const cache = await caches.open('v'+version);
|
||||
const fallbackResponse = await cache.match(fallbackUrl);
|
||||
if (fallbackResponse) {
|
||||
return fallbackResponse;
|
||||
}
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
status: 408
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enableNavigationPreload = async () => {
|
||||
if (self.registration.navigationPreload) {
|
||||
// Enable navigation preloads!
|
||||
// debug('sw preload');
|
||||
try {
|
||||
await self.registration.navigationPreload.enable();
|
||||
} catch (e) {
|
||||
debug('sw preload fail', e);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
const vkey = 'v'+version;
|
||||
// debug('sw activate');
|
||||
// event.waitUntil(enableNavigationPreload());
|
||||
// cleanup old caches
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then( keylist => keylist
|
||||
.filter(key => key != vkey)
|
||||
.map(key => { debug('sw delete cache', key); return caches.delete(key); }) )
|
||||
.then( deletes => Promise.all(deletes) )
|
||||
);
|
||||
event.waitUntil(new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 5000);
|
||||
}));
|
||||
debug('sw activated');
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// debug('sw install');
|
||||
// event.waitUntil( addResourcesToCache([ ]) );
|
||||
self.skipWaiting();
|
||||
debug('sw installed');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request,preloadResponse } = event;
|
||||
if (request.url.indexOf(origin) !== 0 || request.method !== 'GET') {
|
||||
debug('no-cache', request.url);
|
||||
stats.bypass++;
|
||||
event.respondWith(fetch(request));
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
cacheFirst({
|
||||
request: request,
|
||||
preloadResponsePromise: preloadResponse,
|
||||
fallbackUrl: '//static.grid.space/img/logo_gs.png',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
// debug({ onmessage: event, clients });
|
||||
});
|
||||
1513
src2/moto/space.js
Normal file
1513
src2/moto/space.js
Normal file
File diff suppressed because it is too large
Load diff
168
src2/moto/webui.js
Normal file
168
src2/moto/webui.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
|
||||
let nextid = 1;
|
||||
|
||||
function build(data, context) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
let html = [];
|
||||
for (let d of data) {
|
||||
html.appendAll(build(d, context));
|
||||
}
|
||||
return html;
|
||||
}
|
||||
let { type, attr, innr, raw } = data;
|
||||
if (raw) {
|
||||
return [ raw ];
|
||||
}
|
||||
// auto text content
|
||||
if (typeof attr === 'string') {
|
||||
attr = { _: attr };
|
||||
}
|
||||
let elid;
|
||||
let html = [];
|
||||
let text
|
||||
html.push(`<${type}`);
|
||||
let func = {};
|
||||
for (let [key, val] of Object.entries(attr || {})) {
|
||||
if (val === undefined) {
|
||||
continue;
|
||||
}
|
||||
let tov = typeof val;
|
||||
if (key === '_') {
|
||||
text = val;
|
||||
} else if (key.startsWith('_')) {
|
||||
if (val) html.push(` ${key.substring(1)}`);
|
||||
} else if (key === 'id') {
|
||||
elid = val ? (tov === 'object' ? val.join('_') : val) : undefined;
|
||||
} else if (tov === 'function') {
|
||||
func[key] = val;
|
||||
elid = elid || `_${nextid++}`;
|
||||
} else if (tov === 'object') {
|
||||
if (val.length)
|
||||
html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`);
|
||||
} else if (tov !== 'undefined') {
|
||||
html.push(` ${key}="${val}"`);
|
||||
}
|
||||
}
|
||||
if (elid) {
|
||||
html.push(` id="${elid}"`);
|
||||
context.push({elid, func});
|
||||
}
|
||||
html.push('>');
|
||||
if (innr) {
|
||||
let snips = [];
|
||||
if (innr.length) {
|
||||
for (let i of innr) {
|
||||
snips.push(build(i, context));
|
||||
}
|
||||
} else {
|
||||
snips.push(build(innr, context));
|
||||
}
|
||||
html.appendAll(snips.flat());
|
||||
}
|
||||
if (text !== undefined) {
|
||||
html.push(text);
|
||||
}
|
||||
html.push(`</${type}>`);
|
||||
return html;
|
||||
}
|
||||
|
||||
// core html builder funtions
|
||||
const h = {
|
||||
bind: (el, data, opt = {}) => {
|
||||
let ctx = [];
|
||||
let html = build(data, ctx).join('');
|
||||
if (opt.after || opt.before) {
|
||||
let tmpl = document.createElement('template');
|
||||
tmpl.innerHTML = html;
|
||||
opt.before && el.before(tmpl.content);
|
||||
opt.after && el.after(tmpl.content);
|
||||
} else if (opt.append) {
|
||||
el.innerHTML += html;
|
||||
} else {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
let map = {};
|
||||
for (let bind of ctx) {
|
||||
let { elid, func } = bind;
|
||||
let et = $(elid);
|
||||
map[elid] = et;
|
||||
for (let [name, fn] of Object.entries(func)) {
|
||||
et[name] = fn;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
|
||||
el: (type, attr, innr) => {
|
||||
if (Array.isArray(attr)) {
|
||||
innr = attr;
|
||||
attr = {};
|
||||
}
|
||||
return { type, attr, innr };
|
||||
},
|
||||
|
||||
raw: ( text ) => {
|
||||
return { raw: text }
|
||||
},
|
||||
|
||||
build: (data) => {
|
||||
return build(data, []).join('');
|
||||
}
|
||||
};
|
||||
|
||||
// window ui helpers
|
||||
const $ = (id) => {
|
||||
return document.getElementById(id);
|
||||
};
|
||||
|
||||
const $d = (id, v) => {
|
||||
$(id).style.display = v;
|
||||
};
|
||||
|
||||
const $h = (id, h) => {
|
||||
$(id).innerHTML = h;
|
||||
};
|
||||
|
||||
const $c = (id, add, del) => {
|
||||
if (add) $(id).classList.add(add);
|
||||
if (del) $(id).classList.remove(del);
|
||||
};
|
||||
|
||||
const $C = (clazz, add, del) => {
|
||||
let el = [...document.getElementsByClassName(clazz)];
|
||||
for (let e of el) {
|
||||
if (add) e.classList.add(add);
|
||||
if (del) e.classList.remove(del);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
const estop = (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
// add common element types
|
||||
[
|
||||
"a", "i", "hr", "div", "pre", "code", "span", "label", "input",
|
||||
"button", "svg", "textarea", "select", "option", "img", "canvas"
|
||||
].forEach(type => {
|
||||
h[type] = (attr, innr) => {
|
||||
return h.el(type, attr, innr);
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
$,
|
||||
$d,
|
||||
$h,
|
||||
$c,
|
||||
$C,
|
||||
h,
|
||||
estop
|
||||
};
|
||||
98
src2/moto/worker.js
Normal file
98
src2/moto/worker.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
let endpoints = {};
|
||||
|
||||
// code is running in the worker / server context
|
||||
const dispatch = {
|
||||
// bind an endpoint name to a function
|
||||
bind(name, fn) {
|
||||
endpoints[name] = fn;
|
||||
dispatch.send({bind: name});
|
||||
},
|
||||
|
||||
// bind all functions in an object
|
||||
bindObject(object, root, recurse) {
|
||||
recurse = recurse || root === undefined;
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
key = root ? `${root}_${key}` : key;
|
||||
if (typeof fn === 'function') {
|
||||
dispatch.bind(key, fn);
|
||||
} else if (recurse) {
|
||||
dispatch.bindObject(fn, key, recurse);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// allow workers to publish messages on the client side
|
||||
publish(topic, message) {
|
||||
dispatch.send({publish: topic, message});
|
||||
},
|
||||
|
||||
ready() {
|
||||
dispatch.send({ready: true});
|
||||
},
|
||||
|
||||
send(msg) {
|
||||
self.postMessage(msg);
|
||||
},
|
||||
|
||||
onmessage(e) {
|
||||
let msg = e.data,
|
||||
run = endpoints[msg.task],
|
||||
async = false,
|
||||
done = false,
|
||||
send = {
|
||||
// puts function in async mode
|
||||
// prevents auto send.done() on function return
|
||||
async() {
|
||||
async = true;
|
||||
},
|
||||
data(data, direct) {
|
||||
if (canSend(data)) {
|
||||
dispatch.send({ done: false, data: data }, direct);
|
||||
}
|
||||
},
|
||||
done(data, direct) {
|
||||
if (canSend(data)) {
|
||||
dispatch.send({ done: true, data: data }, direct);
|
||||
done = true;
|
||||
}
|
||||
},
|
||||
error(data) {
|
||||
if (canSend(data)) {
|
||||
done = true;
|
||||
dispatch.send({ error: data });
|
||||
}
|
||||
}
|
||||
},
|
||||
canSend = (data) => {
|
||||
if (done) {
|
||||
console.log("unexpected reply dropped", {data, msg});
|
||||
}
|
||||
return !done;
|
||||
};
|
||||
|
||||
if (run) {
|
||||
try {
|
||||
let data = run(msg.data, send);
|
||||
if (!done && (data || async === false)) {
|
||||
send.done(data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.stack) {
|
||||
console.trace(error.stack);
|
||||
}
|
||||
send.error(error.toString());
|
||||
// dispatch.send({ data: {error: error.toString()}, done: true });
|
||||
}
|
||||
} else {
|
||||
console.log({worker_unhandled: e});
|
||||
send.error(`no registered endpoint: ${msg.task}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// attach onmessage hanler
|
||||
self.onmessage = dispatch.onmessage;
|
||||
|
||||
export const worker = dispatch;
|
||||
4629
src2/wasm/clipper.cpp
Normal file
4629
src2/wasm/clipper.cpp
Normal file
File diff suppressed because it is too large
Load diff
404
src2/wasm/clipper.hpp
Normal file
404
src2/wasm/clipper.hpp
Normal file
|
|
@ -0,0 +1,404 @@
|
|||
/*******************************************************************************
|
||||
* *
|
||||
* Author : Angus Johnson *
|
||||
* Version : 6.4.2 *
|
||||
* Date : 27 February 2017 *
|
||||
* Website : http://www.angusj.com *
|
||||
* Copyright : Angus Johnson 2010-2017 *
|
||||
* *
|
||||
* License: *
|
||||
* Use, modification & distribution is subject to Boost Software License Ver 1. *
|
||||
* http://www.boost.org/LICENSE_1_0.txt *
|
||||
* *
|
||||
* Attributions: *
|
||||
* The code in this library is an extension of Bala Vatti's clipping algorithm: *
|
||||
* "A generic solution to polygon clipping" *
|
||||
* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. *
|
||||
* http://portal.acm.org/citation.cfm?id=129906 *
|
||||
* *
|
||||
* Computer graphics and geometric modeling: implementation and algorithms *
|
||||
* By Max K. Agoston *
|
||||
* Springer; 1 edition (January 4, 2005) *
|
||||
* http://books.google.com/books?q=vatti+clipping+agoston *
|
||||
* *
|
||||
* See also: *
|
||||
* "Polygon Offsetting by Computing Winding Numbers" *
|
||||
* Paper no. DETC2005-85513 pp. 565-575 *
|
||||
* ASME 2005 International Design Engineering Technical Conferences *
|
||||
* and Computers and Information in Engineering Conference (IDETC/CIE2005) *
|
||||
* September 24-28, 2005 , Long Beach, California, USA *
|
||||
* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
#ifndef clipper_hpp
|
||||
#define clipper_hpp
|
||||
|
||||
#define CLIPPER_VERSION "6.4.2"
|
||||
|
||||
//use_int32: When enabled 32bit ints are used instead of 64bit ints. This
|
||||
//improve performance but coordinate values are limited to the range +/- 46340
|
||||
//#define use_int32
|
||||
|
||||
//use_xyz: adds a Z member to IntPoint. Adds a minor cost to perfomance.
|
||||
//#define use_xyz
|
||||
|
||||
//use_lines: Enables line clipping. Adds a very minor cost to performance.
|
||||
#define use_lines
|
||||
|
||||
//use_deprecated: Enables temporary support for the obsolete functions
|
||||
//#define use_deprecated
|
||||
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <ostream>
|
||||
#include <functional>
|
||||
#include <queue>
|
||||
|
||||
namespace ClipperLib {
|
||||
|
||||
enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };
|
||||
enum PolyType { ptSubject, ptClip };
|
||||
//By far the most widely used winding rules for polygon filling are
|
||||
//EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32)
|
||||
//Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL)
|
||||
//see http://glprogramming.com/red/chapter11.html
|
||||
enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative };
|
||||
|
||||
#ifdef use_int32
|
||||
typedef int cInt;
|
||||
static cInt const loRange = 0x7FFF;
|
||||
static cInt const hiRange = 0x7FFF;
|
||||
#else
|
||||
typedef signed long long cInt;
|
||||
static cInt const loRange = 0x3FFFFFFF;
|
||||
static cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL;
|
||||
typedef signed long long long64; //used by Int128 class
|
||||
typedef unsigned long long ulong64;
|
||||
|
||||
#endif
|
||||
|
||||
struct IntPoint {
|
||||
cInt X;
|
||||
cInt Y;
|
||||
#ifdef use_xyz
|
||||
cInt Z;
|
||||
IntPoint(cInt x = 0, cInt y = 0, cInt z = 0): X(x), Y(y), Z(z) {};
|
||||
#else
|
||||
IntPoint(cInt x = 0, cInt y = 0): X(x), Y(y) {};
|
||||
#endif
|
||||
|
||||
friend inline bool operator== (const IntPoint& a, const IntPoint& b)
|
||||
{
|
||||
return a.X == b.X && a.Y == b.Y;
|
||||
}
|
||||
friend inline bool operator!= (const IntPoint& a, const IntPoint& b)
|
||||
{
|
||||
return a.X != b.X || a.Y != b.Y;
|
||||
}
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
typedef std::vector< IntPoint > Path;
|
||||
typedef std::vector< Path > Paths;
|
||||
|
||||
inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;}
|
||||
inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;}
|
||||
|
||||
std::ostream& operator <<(std::ostream &s, const IntPoint &p);
|
||||
std::ostream& operator <<(std::ostream &s, const Path &p);
|
||||
std::ostream& operator <<(std::ostream &s, const Paths &p);
|
||||
|
||||
struct DoublePoint
|
||||
{
|
||||
double X;
|
||||
double Y;
|
||||
DoublePoint(double x = 0, double y = 0) : X(x), Y(y) {}
|
||||
DoublePoint(IntPoint ip) : X((double)ip.X), Y((double)ip.Y) {}
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
#ifdef use_xyz
|
||||
typedef void (*ZFillCallback)(IntPoint& e1bot, IntPoint& e1top, IntPoint& e2bot, IntPoint& e2top, IntPoint& pt);
|
||||
#endif
|
||||
|
||||
enum InitOptions {ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4};
|
||||
enum JoinType {jtSquare, jtRound, jtMiter};
|
||||
enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound};
|
||||
|
||||
class PolyNode;
|
||||
typedef std::vector< PolyNode* > PolyNodes;
|
||||
|
||||
class PolyNode
|
||||
{
|
||||
public:
|
||||
PolyNode();
|
||||
virtual ~PolyNode(){};
|
||||
Path Contour;
|
||||
PolyNodes Childs;
|
||||
PolyNode* Parent;
|
||||
PolyNode* GetNext() const;
|
||||
bool IsHole() const;
|
||||
bool IsOpen() const;
|
||||
int ChildCount() const;
|
||||
private:
|
||||
//PolyNode& operator =(PolyNode& other);
|
||||
unsigned Index; //node index in Parent.Childs
|
||||
bool m_IsOpen;
|
||||
JoinType m_jointype;
|
||||
EndType m_endtype;
|
||||
PolyNode* GetNextSiblingUp() const;
|
||||
void AddChild(PolyNode& child);
|
||||
friend class Clipper; //to access Index
|
||||
friend class ClipperOffset;
|
||||
};
|
||||
|
||||
class PolyTree: public PolyNode
|
||||
{
|
||||
public:
|
||||
~PolyTree(){ Clear(); };
|
||||
PolyNode* GetFirst() const;
|
||||
void Clear();
|
||||
int Total() const;
|
||||
private:
|
||||
//PolyTree& operator =(PolyTree& other);
|
||||
PolyNodes AllNodes;
|
||||
friend class Clipper; //to access AllNodes
|
||||
};
|
||||
|
||||
bool Orientation(const Path &poly);
|
||||
double Area(const Path &poly);
|
||||
int PointInPolygon(const IntPoint &pt, const Path &path);
|
||||
|
||||
void SimplifyPolygon(const Path &in_poly, Paths &out_polys, PolyFillType fillType = pftEvenOdd);
|
||||
void SimplifyPolygons(const Paths &in_polys, Paths &out_polys, PolyFillType fillType = pftEvenOdd);
|
||||
void SimplifyPolygons(Paths &polys, PolyFillType fillType = pftEvenOdd);
|
||||
|
||||
void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415);
|
||||
void CleanPolygon(Path& poly, double distance = 1.415);
|
||||
void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance = 1.415);
|
||||
void CleanPolygons(Paths& polys, double distance = 1.415);
|
||||
|
||||
void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed);
|
||||
void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed);
|
||||
void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution);
|
||||
|
||||
void PolyTreeToPaths(const PolyTree& polytree, Paths& paths);
|
||||
void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths);
|
||||
void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths);
|
||||
|
||||
void ReversePath(Path& p);
|
||||
void ReversePaths(Paths& p);
|
||||
|
||||
struct IntRect { cInt left; cInt top; cInt right; cInt bottom; };
|
||||
|
||||
//enums that are used internally ...
|
||||
enum EdgeSide { esLeft = 1, esRight = 2};
|
||||
|
||||
//forward declarations (for stuff used internally) ...
|
||||
struct TEdge;
|
||||
struct IntersectNode;
|
||||
struct LocalMinimum;
|
||||
struct OutPt;
|
||||
struct OutRec;
|
||||
struct Join;
|
||||
|
||||
typedef std::vector < OutRec* > PolyOutList;
|
||||
typedef std::vector < TEdge* > EdgeList;
|
||||
typedef std::vector < Join* > JoinList;
|
||||
typedef std::vector < IntersectNode* > IntersectList;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
//ClipperBase is the ancestor to the Clipper class. It should not be
|
||||
//instantiated directly. This class simply abstracts the conversion of sets of
|
||||
//polygon coordinates into edge objects that are stored in a LocalMinima list.
|
||||
class ClipperBase
|
||||
{
|
||||
public:
|
||||
ClipperBase();
|
||||
virtual ~ClipperBase();
|
||||
virtual bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed);
|
||||
bool AddPaths(const Paths &ppg, PolyType PolyTyp, bool Closed);
|
||||
virtual void Clear();
|
||||
IntRect GetBounds();
|
||||
bool PreserveCollinear() {return m_PreserveCollinear;};
|
||||
void PreserveCollinear(bool value) {m_PreserveCollinear = value;};
|
||||
protected:
|
||||
void DisposeLocalMinimaList();
|
||||
TEdge* AddBoundsToLML(TEdge *e, bool IsClosed);
|
||||
virtual void Reset();
|
||||
TEdge* ProcessBound(TEdge* E, bool IsClockwise);
|
||||
void InsertScanbeam(const cInt Y);
|
||||
bool PopScanbeam(cInt &Y);
|
||||
bool LocalMinimaPending();
|
||||
bool PopLocalMinima(cInt Y, const LocalMinimum *&locMin);
|
||||
OutRec* CreateOutRec();
|
||||
void DisposeAllOutRecs();
|
||||
void DisposeOutRec(PolyOutList::size_type index);
|
||||
void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2);
|
||||
void DeleteFromAEL(TEdge *e);
|
||||
void UpdateEdgeIntoAEL(TEdge *&e);
|
||||
|
||||
typedef std::vector<LocalMinimum> MinimaList;
|
||||
MinimaList::iterator m_CurrentLM;
|
||||
MinimaList m_MinimaList;
|
||||
|
||||
bool m_UseFullRange;
|
||||
EdgeList m_edges;
|
||||
bool m_PreserveCollinear;
|
||||
bool m_HasOpenPaths;
|
||||
PolyOutList m_PolyOuts;
|
||||
TEdge *m_ActiveEdges;
|
||||
|
||||
typedef std::priority_queue<cInt> ScanbeamList;
|
||||
ScanbeamList m_Scanbeam;
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class Clipper : public virtual ClipperBase
|
||||
{
|
||||
public:
|
||||
Clipper(int initOptions = 0);
|
||||
bool Execute(ClipType clipType,
|
||||
Paths &solution,
|
||||
PolyFillType fillType = pftEvenOdd);
|
||||
bool Execute(ClipType clipType,
|
||||
Paths &solution,
|
||||
PolyFillType subjFillType,
|
||||
PolyFillType clipFillType);
|
||||
bool Execute(ClipType clipType,
|
||||
PolyTree &polytree,
|
||||
PolyFillType fillType = pftEvenOdd);
|
||||
bool Execute(ClipType clipType,
|
||||
PolyTree &polytree,
|
||||
PolyFillType subjFillType,
|
||||
PolyFillType clipFillType);
|
||||
bool ReverseSolution() { return m_ReverseOutput; };
|
||||
void ReverseSolution(bool value) {m_ReverseOutput = value;};
|
||||
bool StrictlySimple() {return m_StrictSimple;};
|
||||
void StrictlySimple(bool value) {m_StrictSimple = value;};
|
||||
//set the callback function for z value filling on intersections (otherwise Z is 0)
|
||||
#ifdef use_xyz
|
||||
void ZFillFunction(ZFillCallback zFillFunc);
|
||||
#endif
|
||||
protected:
|
||||
virtual bool ExecuteInternal();
|
||||
private:
|
||||
JoinList m_Joins;
|
||||
JoinList m_GhostJoins;
|
||||
IntersectList m_IntersectList;
|
||||
ClipType m_ClipType;
|
||||
typedef std::list<cInt> MaximaList;
|
||||
MaximaList m_Maxima;
|
||||
TEdge *m_SortedEdges;
|
||||
bool m_ExecuteLocked;
|
||||
PolyFillType m_ClipFillType;
|
||||
PolyFillType m_SubjFillType;
|
||||
bool m_ReverseOutput;
|
||||
bool m_UsingPolyTree;
|
||||
bool m_StrictSimple;
|
||||
#ifdef use_xyz
|
||||
ZFillCallback m_ZFill; //custom callback
|
||||
#endif
|
||||
void SetWindingCount(TEdge& edge);
|
||||
bool IsEvenOddFillType(const TEdge& edge) const;
|
||||
bool IsEvenOddAltFillType(const TEdge& edge) const;
|
||||
void InsertLocalMinimaIntoAEL(const cInt botY);
|
||||
void InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge);
|
||||
void AddEdgeToSEL(TEdge *edge);
|
||||
bool PopEdgeFromSEL(TEdge *&edge);
|
||||
void CopyAELToSEL();
|
||||
void DeleteFromSEL(TEdge *e);
|
||||
void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2);
|
||||
bool IsContributing(const TEdge& edge) const;
|
||||
bool IsTopHorz(const cInt XPos);
|
||||
void DoMaxima(TEdge *e);
|
||||
void ProcessHorizontals();
|
||||
void ProcessHorizontal(TEdge *horzEdge);
|
||||
void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
|
||||
OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
|
||||
OutRec* GetOutRec(int idx);
|
||||
void AppendPolygon(TEdge *e1, TEdge *e2);
|
||||
void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt);
|
||||
OutPt* AddOutPt(TEdge *e, const IntPoint &pt);
|
||||
OutPt* GetLastOutPt(TEdge *e);
|
||||
bool ProcessIntersections(const cInt topY);
|
||||
void BuildIntersectList(const cInt topY);
|
||||
void ProcessIntersectList();
|
||||
void ProcessEdgesAtTopOfScanbeam(const cInt topY);
|
||||
void BuildResult(Paths& polys);
|
||||
void BuildResult2(PolyTree& polytree);
|
||||
void SetHoleState(TEdge *e, OutRec *outrec);
|
||||
void DisposeIntersectNodes();
|
||||
bool FixupIntersectionOrder();
|
||||
void FixupOutPolygon(OutRec &outrec);
|
||||
void FixupOutPolyline(OutRec &outrec);
|
||||
bool IsHole(TEdge *e);
|
||||
bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl);
|
||||
void FixHoleLinkage(OutRec &outrec);
|
||||
void AddJoin(OutPt *op1, OutPt *op2, const IntPoint offPt);
|
||||
void ClearJoins();
|
||||
void ClearGhostJoins();
|
||||
void AddGhostJoin(OutPt *op, const IntPoint offPt);
|
||||
bool JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2);
|
||||
void JoinCommonEdges();
|
||||
void DoSimplePolygons();
|
||||
void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec);
|
||||
void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec);
|
||||
void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec);
|
||||
#ifdef use_xyz
|
||||
void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2);
|
||||
#endif
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class ClipperOffset
|
||||
{
|
||||
public:
|
||||
ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25);
|
||||
~ClipperOffset();
|
||||
void AddPath(const Path& path, JoinType joinType, EndType endType);
|
||||
void AddPaths(const Paths& paths, JoinType joinType, EndType endType);
|
||||
void Execute(Paths& solution, double delta);
|
||||
void Execute(PolyTree& solution, double delta);
|
||||
void Clear();
|
||||
double MiterLimit;
|
||||
double ArcTolerance;
|
||||
private:
|
||||
Paths m_destPolys;
|
||||
Path m_srcPoly;
|
||||
Path m_destPoly;
|
||||
std::vector<DoublePoint> m_normals;
|
||||
double m_delta, m_sinA, m_sin, m_cos;
|
||||
double m_miterLim, m_StepsPerRad;
|
||||
IntPoint m_lowest;
|
||||
PolyNode m_polyNodes;
|
||||
|
||||
void FixOrientations();
|
||||
void DoOffset(double delta);
|
||||
void OffsetPoint(int j, int& k, JoinType jointype);
|
||||
void DoSquare(int j, int k);
|
||||
void DoMiter(int j, int k, double r);
|
||||
void DoRound(int j, int k);
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
class clipperException : public std::exception
|
||||
{
|
||||
public:
|
||||
clipperException(const char* description): m_descr(description) {}
|
||||
virtual ~clipperException() throw() {}
|
||||
virtual const char* what() const throw() {return m_descr.c_str();}
|
||||
private:
|
||||
std::string m_descr;
|
||||
};
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
} //ClipperLib namespace
|
||||
|
||||
#endif //clipper_hpp
|
||||
24
src2/wasm/kiri-ani.c
Normal file
24
src2/wasm/kiri-ani.c
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#include <emscripten.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char Uint8;
|
||||
typedef unsigned short Uint16;
|
||||
typedef unsigned int Uint32;
|
||||
|
||||
extern void reportf(float a, float b);
|
||||
extern void reporti(int a, int b);
|
||||
|
||||
/**
|
||||
* m = memory base pointer
|
||||
* im = input memory location (mesh)
|
||||
* it = input memory location (tool)
|
||||
* ol = output memory location (mesh-updates)
|
||||
* returns length of update at output memory location
|
||||
*/
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
Uint32 updateMesh(unsigned char *m, Uint32 im, Uint32 it, Uint32 ol) {
|
||||
int rv = m[0] + m[100] + m[200];
|
||||
m[0] = 123;
|
||||
reporti(m[100],m[200]);
|
||||
return rv;
|
||||
}
|
||||
BIN
src2/wasm/kiri-ani.wasm
Executable file
BIN
src2/wasm/kiri-ani.wasm
Executable file
Binary file not shown.
189
src2/wasm/kiri-geo.cpp
Normal file
189
src2/wasm/kiri-geo.cpp
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
//#define use_int32
|
||||
|
||||
#include <emscripten.h>
|
||||
#include "clipper.hpp"
|
||||
|
||||
typedef unsigned char Uint8;
|
||||
typedef unsigned short Uint16;
|
||||
typedef unsigned int Uint32;
|
||||
typedef int int32;
|
||||
|
||||
using namespace ClipperLib;
|
||||
|
||||
Uint8 *mem = 0;
|
||||
|
||||
extern "C" {
|
||||
extern void debug_string(Uint32 len, char *str);
|
||||
}
|
||||
|
||||
struct length16 {
|
||||
Uint16 length;
|
||||
};
|
||||
|
||||
struct point32 {
|
||||
int32 x;
|
||||
int32 y;
|
||||
};
|
||||
|
||||
__attribute__ ((export_name("mem_get")))
|
||||
Uint32 mem_get(Uint32 size) {
|
||||
return (Uint32)malloc(size);
|
||||
}
|
||||
|
||||
__attribute__ ((export_name("mem_clr")))
|
||||
void mem_clr(Uint32 loc) {
|
||||
free((void *)loc);
|
||||
}
|
||||
|
||||
void send_string(const char *format, ...) {
|
||||
char buffer[100];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
Uint32 len = vsprintf(buffer, format, args);
|
||||
va_end(args);
|
||||
debug_string(len, buffer);
|
||||
}
|
||||
|
||||
Uint32 readPoly(Path &path, Uint32 pos) {
|
||||
struct length16 *ls = (struct length16 *)(mem + pos);
|
||||
Uint16 points = ls->length;
|
||||
pos += 2;
|
||||
while (points > 0) {
|
||||
struct point32 *ip = (struct point32 *)(mem + pos);
|
||||
pos += 8;
|
||||
path << IntPoint(ip->x, ip->y);
|
||||
points--;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
Uint32 readPolys(Paths &paths, Uint32 pos, Uint32 count) {
|
||||
Uint32 poly = 0;
|
||||
while (count > 0) {
|
||||
pos = readPoly(paths[poly++], pos);
|
||||
count--;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
Uint32 writePolys(Paths &outs, Uint32 pos) {
|
||||
for (Path po : outs) {
|
||||
struct length16 *ls = (struct length16 *)(mem + pos);
|
||||
ls->length = po.size();
|
||||
pos += 2;
|
||||
for (IntPoint pt : po) {
|
||||
struct point32 *ip = (struct point32 *)(mem + pos);
|
||||
ip->x = (int)pt.X;
|
||||
ip->y = (int)pt.Y;
|
||||
pos += 8;
|
||||
}
|
||||
}
|
||||
// null terminate
|
||||
struct length16 *ls = (struct length16 *)(mem + pos);
|
||||
ls->length = 0;
|
||||
return pos + 2;
|
||||
}
|
||||
|
||||
__attribute__ ((export_name("poly_offset")))
|
||||
Uint32 poly_offset(Uint32 memat, Uint32 polys, float offset, float clean, Uint8 simple) {
|
||||
Paths ins(polys);
|
||||
Paths outs;
|
||||
Uint32 pos = memat;
|
||||
Uint16 poly = 0;
|
||||
|
||||
pos = readPolys(ins, pos, polys);
|
||||
|
||||
if (clean > 0) {
|
||||
Paths cleans;
|
||||
CleanPolygons(ins, cleans, clean);
|
||||
ins = cleans;
|
||||
}
|
||||
|
||||
if (simple > 0) {
|
||||
Paths simples;
|
||||
SimplifyPolygons(ins, simples);
|
||||
ins = simples;
|
||||
}
|
||||
|
||||
ClipperOffset co;
|
||||
co.AddPaths(ins, jtMiter, etClosedPolygon);
|
||||
co.Execute(outs, offset);
|
||||
|
||||
Uint32 resat = pos;
|
||||
|
||||
pos = writePolys(outs, pos);
|
||||
|
||||
co.Clear();
|
||||
|
||||
return resat;
|
||||
}
|
||||
|
||||
__attribute__ ((export_name("poly_union")))
|
||||
Uint32 poly_union(Uint32 memat, Uint32 polys, float offset) {
|
||||
|
||||
Paths ins(polys);
|
||||
Paths outs;
|
||||
Uint32 pos = memat;
|
||||
Uint16 poly = 0;
|
||||
|
||||
pos = readPolys(ins, pos, polys);
|
||||
|
||||
Clipper clip;
|
||||
clip.AddPaths(ins, ptSubject, true);
|
||||
clip.Execute(ctUnion, outs);
|
||||
|
||||
Uint32 resat = pos;
|
||||
|
||||
pos = writePolys(outs, pos);
|
||||
|
||||
clip.Clear();
|
||||
|
||||
return resat;
|
||||
}
|
||||
|
||||
__attribute__ ((export_name("poly_diff")))
|
||||
Uint32 poly_diff(Uint32 memat, Uint32 polysA, Uint32 polysB, Uint8 AB, Uint8 BA, float clean) {
|
||||
|
||||
Paths inA(polysA);
|
||||
Paths inB(polysB);
|
||||
Uint32 pos = memat;
|
||||
|
||||
pos = readPolys(inA, pos, polysA);
|
||||
pos = readPolys(inB, pos, polysB);
|
||||
|
||||
Uint32 resat = pos;
|
||||
|
||||
if (AB > 0) {
|
||||
Paths outs;
|
||||
Clipper clip;
|
||||
clip.AddPaths(inA, ptSubject, true);
|
||||
clip.AddPaths(inB, ptClip, true);
|
||||
clip.Execute(ctDifference, outs, pftEvenOdd, pftEvenOdd);
|
||||
if (clean > 0) {
|
||||
// CleanPolygons(outs, clean);
|
||||
for (Path po : outs) {
|
||||
CleanPolygon(po, clean);
|
||||
}
|
||||
}
|
||||
pos = writePolys(outs, pos);
|
||||
clip.Clear();
|
||||
}
|
||||
|
||||
if (BA > 0) {
|
||||
Paths outs;
|
||||
Clipper clip;
|
||||
clip.AddPaths(inB, ptSubject, true);
|
||||
clip.AddPaths(inA, ptClip, true);
|
||||
clip.Execute(ctDifference, outs, pftEvenOdd, pftEvenOdd);
|
||||
if (clean > 0) {
|
||||
// CleanPolygons(outs, clean);
|
||||
for (Path po : outs) {
|
||||
CleanPolygon(po, clean);
|
||||
}
|
||||
}
|
||||
pos = writePolys(outs, pos);
|
||||
clip.Clear();
|
||||
}
|
||||
|
||||
return resat;
|
||||
}
|
||||
BIN
src2/wasm/kiri-geo.wasm
Executable file
BIN
src2/wasm/kiri-geo.wasm
Executable file
Binary file not shown.
217
src2/wasm/kiri-sla.c
Normal file
217
src2/wasm/kiri-sla.c
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#include <emscripten.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char Uint8;
|
||||
typedef unsigned short Uint16;
|
||||
typedef unsigned int Uint32;
|
||||
|
||||
struct info {
|
||||
Uint16 width; // image width
|
||||
Uint16 height; // image height
|
||||
Uint16 polys; // total number of polygons to render
|
||||
};
|
||||
|
||||
struct poly {
|
||||
Uint16 length; // offset to end of record in bytes
|
||||
Uint16 inners; // number of inner polygons
|
||||
Uint16 points; // number of points in polygon
|
||||
Uint16 minx;
|
||||
Uint16 maxx;
|
||||
Uint16 miny;
|
||||
Uint16 maxy;
|
||||
};
|
||||
|
||||
struct point {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
const Uint8 ph_size = sizeof(struct poly);
|
||||
const Uint8 pt_size = sizeof(struct point);
|
||||
|
||||
extern void reportf(float a, float b);
|
||||
extern void reporti(int a, int b);
|
||||
|
||||
Uint32 readoff; // read position offset
|
||||
|
||||
Uint8 check_cross(Uint32 x, Uint32 y, struct point *p1, struct point *p2) {
|
||||
return (
|
||||
((p1->y >= y) != (p2->y >= y)) &&
|
||||
(x < (p2->x - p1->x) * (y - p1->y) / (p2->y - p1->y) + p1->x)
|
||||
) ? 1 : 0;
|
||||
}
|
||||
|
||||
Uint8 is_inside_poly(Uint32 x, Uint32 y, unsigned char *m, Uint32 o) {
|
||||
struct poly *poly = (struct poly *)(m + readoff);
|
||||
|
||||
Uint32 nextpoly = readoff + poly->length;
|
||||
Uint8 side = 0; // 0=outside, 1=inside
|
||||
|
||||
// skip post header
|
||||
readoff += ph_size;
|
||||
|
||||
// save pointer to first point or last comparison
|
||||
struct point *pf = (struct point *)(m + readoff);
|
||||
struct point *p1 = pf;
|
||||
struct point *p2;
|
||||
|
||||
// skip past first point
|
||||
readoff += pt_size;
|
||||
|
||||
// read and check point pairs updating `side`
|
||||
for (Uint16 i=1; i<poly->points; i++) {
|
||||
p2 = (struct point *)(m + readoff);
|
||||
// increment read pointe
|
||||
readoff += pt_size;
|
||||
// check current/previous point crossing
|
||||
if (check_cross(x, y, p1, p2)) {
|
||||
side = 1 - side;
|
||||
}
|
||||
p1 = p2;
|
||||
}
|
||||
|
||||
// check last/first point crossing
|
||||
if (check_cross(x, y, p1, pf)) {
|
||||
side = 1 - side;
|
||||
}
|
||||
|
||||
return side;
|
||||
}
|
||||
|
||||
void rasterize_poly(unsigned char *m, Uint32 o, Uint32 height) {
|
||||
struct poly *poly = (struct poly *)(m + readoff);
|
||||
|
||||
Uint32 nextpoly = readoff + poly->length;
|
||||
|
||||
// skip post header
|
||||
readoff += ph_size;
|
||||
|
||||
// save pointer to first point or last comparison
|
||||
struct point *pf = (struct point *)(m + readoff);
|
||||
struct point *p1 = pf;
|
||||
struct point *p2;
|
||||
|
||||
// skip past first point
|
||||
readoff += pt_size;
|
||||
|
||||
// point data starts here
|
||||
Uint32 pstart = readoff;
|
||||
|
||||
// scan bounding area of polygon
|
||||
for (Uint32 x=poly->minx; x<poly->maxx; x++) {
|
||||
for (Uint32 y=poly->miny; y<poly->maxy; y++) {
|
||||
|
||||
Uint8 side = 0; // 0=outside, 1=inside
|
||||
|
||||
// re-start at point data offset for each x,y
|
||||
readoff = pstart;
|
||||
p1 = pf;
|
||||
|
||||
// read and check point pairs updating `side`
|
||||
for (Uint16 i=1; i<poly->points; i++) {
|
||||
p2 = (struct point *)(m + readoff);
|
||||
// increment read pointe
|
||||
readoff += pt_size;
|
||||
// check current/previous point crossing
|
||||
if (check_cross(x, y, p1, p2)) {
|
||||
side = 1 - side;
|
||||
}
|
||||
p1 = p2;
|
||||
}
|
||||
|
||||
// check last/first point crossing
|
||||
if (check_cross(x, y, p1, pf)) {
|
||||
side = 1 - side;
|
||||
}
|
||||
|
||||
// check inner polygons only if inside the outer polygon
|
||||
if (side)
|
||||
for (Uint16 i=0; i<poly->inners; i++) {
|
||||
// if inside an inner, then actually outside poly and term
|
||||
if (is_inside_poly(x, y, m, o)) {
|
||||
side = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (side) {
|
||||
Uint32 impos = y + (x * height);
|
||||
(m + o)[impos] = 255;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// update pointer to next polygon
|
||||
readoff = nextpoly;
|
||||
}
|
||||
|
||||
/**
|
||||
* m = memory base pointer
|
||||
* i = input memory location (polygon records)
|
||||
* o = output memory location (for raster)
|
||||
* returns last read position in memory
|
||||
*/
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
Uint32 render(unsigned char *m, Uint32 i, Uint32 o) {
|
||||
struct info *info = (struct info *)(m + i);
|
||||
Uint32 impos;
|
||||
Uint8 inside;
|
||||
Uint32 polypos = (sizeof (struct info)) + i;
|
||||
|
||||
memset(m+o, 0, info->width * info->height);
|
||||
|
||||
readoff = polypos;
|
||||
for (Uint32 p=0; p<info->polys; p++) {
|
||||
rasterize_poly(m, o, info->height);
|
||||
}
|
||||
|
||||
return readoff;
|
||||
}
|
||||
|
||||
Uint8 rle_byte(Uint8 color, Uint8 count, Uint8 type) {
|
||||
if (type == 0) {
|
||||
return (count & 0x7f) | ((color << 7) & 0x80);
|
||||
} else {
|
||||
Uint8 run = count - 1;
|
||||
return
|
||||
(run & 1 ? 128 : 0) |
|
||||
(run & 2 ? 64 : 0) |
|
||||
(run & 4 ? 32 : 0) |
|
||||
(run & 8 ? 16 : 0) |
|
||||
(run & 16 ? 8 : 0) |
|
||||
(run & 32 ? 4 : 0) |
|
||||
(run & 64 ? 2 : 0) | color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mem = memory base pointer
|
||||
* in = input memory location (raster)
|
||||
* ilen = input raster length in bytes
|
||||
* out = output memory location (for rle-encoded image)
|
||||
* type = 0=photon, 1=photons
|
||||
* returns length of rle-encoded image
|
||||
*/
|
||||
EMSCRIPTEN_KEEPALIVE
|
||||
Uint32 rle_encode(unsigned char *mem, Uint32 in, Uint32 ilen, Uint8 mask, Uint32 out, Uint8 type) {
|
||||
Uint8 color = (mem[in++] & mask) ? 1 : 0; // current color
|
||||
Uint8 count = 1; // number of color matches
|
||||
Uint8 cmax = type == 0 ? 125 : 128; // count max
|
||||
Uint8 next; // next color
|
||||
Uint32 opos = out;
|
||||
while (--ilen > 0) {
|
||||
next = (mem[in++] & mask) ? 1 : 0;
|
||||
if (color != next || count == cmax) {
|
||||
mem[opos++] = rle_byte(color, count, type);
|
||||
count = 0;
|
||||
}
|
||||
count++;
|
||||
color = next;
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
mem[opos++] = rle_byte(color, count, type);
|
||||
}
|
||||
return opos - out;
|
||||
}
|
||||
BIN
src2/wasm/kiri-sla.wasm
Executable file
BIN
src2/wasm/kiri-sla.wasm
Executable file
Binary file not shown.
13
src2/wasm/makefile
Normal file
13
src2/wasm/makefile
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
all: kiri-sla.wasm kiri-geo.wasm kiri-ani.wasm
|
||||
|
||||
kiri-sla.wasm: kiri-sla.c
|
||||
emcc --no-entry -o kiri-sla.wasm kiri-sla.c -O3 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s TOTAL_MEMORY=40mb
|
||||
|
||||
kiri-geo.wasm: kiri-geo.cpp
|
||||
emcc --no-entry -o kiri-geo.wasm clipper.cpp kiri-geo.cpp -Oz -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s TOTAL_MEMORY=40mb
|
||||
|
||||
kiri-ani.wasm: kiri-ani.c
|
||||
emcc --no-entry -o kiri-ani.wasm kiri-ani.c -O3 -s ERROR_ON_UNDEFINED_SYMBOLS=0
|
||||
|
||||
clean: kiri-*.wasm
|
||||
rm *.wasm
|
||||
1
src2/wasm/manifold.wasm
Symbolic link
1
src2/wasm/manifold.wasm
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../node_modules/manifold-3d/manifold.wasm
|
||||
23
src2/webpack/webpack-three-bundle.js
Normal file
23
src2/webpack/webpack-three-bundle.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as THREE from "../../node_modules/three/build/three.module.js";
|
||||
import * as SVGLoader from "../../node_modules/three/examples/jsm/loaders/SVGLoader.js";
|
||||
import * as BufferGeometryUtils from "../../node_modules/three/examples/jsm/utils/BufferGeometryUtils.js";
|
||||
|
||||
import { Line2 } from '../../node_modules/three/examples/jsm/lines/Line2.js';
|
||||
import { LineMaterial } from '../../node_modules/three/examples/jsm/lines/LineMaterial.js';
|
||||
import { LineGeometry } from '../../node_modules/three/examples/jsm/lines/LineGeometry.js';
|
||||
import { LineSegments2 } from '../../node_modules/three/examples/jsm/lines/LineSegments2.js';
|
||||
import { LineSegmentsGeometry } from '../../node_modules/three/examples/jsm/lines/LineSegmentsGeometry.js';
|
||||
|
||||
import * as MeshBVHLib from '../../node_modules/three-mesh-bvh/build/index.module.js';
|
||||
|
||||
export {
|
||||
THREE,
|
||||
SVGLoader,
|
||||
BufferGeometryUtils,
|
||||
LineMaterial,
|
||||
Line2,
|
||||
LineGeometry,
|
||||
LineSegments2,
|
||||
LineSegmentsGeometry,
|
||||
MeshBVHLib
|
||||
};
|
||||
38
src2/webpack/webpack-three-esm.js
Normal file
38
src2/webpack/webpack-three-esm.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
entry: path.resolve(__dirname, './webpack-three-bundle.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../ext'),
|
||||
filename: 'three.js',
|
||||
library: {
|
||||
type: 'module'
|
||||
},
|
||||
module: true
|
||||
},
|
||||
experiments: {
|
||||
outputModule: true
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js'],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
resolve: {
|
||||
fullySpecified: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optimization: {
|
||||
minimize: false
|
||||
},
|
||||
performance: {
|
||||
hints: false,
|
||||
maxAssetSize: 2 * 1024 * 1024,
|
||||
maxEntrypointSize: 2 * 1024 * 1024,
|
||||
},
|
||||
};
|
||||
31
web2/README.md
Normal file
31
web2/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Mesh:Tool Development Server
|
||||
|
||||
This is the development server setup for Mesh:Tool, a browser-based 3D mesh editor.
|
||||
|
||||
## Running the Development Server
|
||||
|
||||
1. Make sure you have Node.js installed
|
||||
2. Install serve globally (optional):
|
||||
```bash
|
||||
npm install -g serve
|
||||
```
|
||||
3. Run the development server:
|
||||
```bash
|
||||
npx serve -l 5432
|
||||
```
|
||||
4. Open your browser to http://localhost:5432/mesh
|
||||
|
||||
## Development Notes
|
||||
|
||||
- The server is configured to handle symlinks and path mappings through `serve.json`
|
||||
- CORS headers are set to allow WebAssembly modules to load properly
|
||||
- Static assets are served from the `lib` directory
|
||||
- Source code is served from the `src2` directory
|
||||
- Fonts are served from `/font` and `/fon2` directories
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter CORS or WebAssembly loading issues:
|
||||
1. Make sure you're using a modern browser that supports SharedArrayBuffer
|
||||
2. Check that the CORS headers are being applied correctly
|
||||
3. Verify that all paths in index.html are correct for your setup
|
||||
1
web2/fon2
Symbolic link
1
web2/fon2
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../node_modules/bootstrap-icons/font/
|
||||
1
web2/font
Symbolic link
1
web2/font
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../node_modules/@fortawesome/fontawesome-free/
|
||||
1
web2/lib
Symbolic link
1
web2/lib
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../src2/
|
||||
BIN
web2/mesh/favicon.ico
Normal file
BIN
web2/mesh/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
830
web2/mesh/index.css
Normal file
830
web2/mesh/index.css
Normal file
|
|
@ -0,0 +1,830 @@
|
|||
:root {
|
||||
--menu-blue: #0079ff;
|
||||
--menu-back: rgba(255,255,255,0.55);
|
||||
--dark-menu-back: rgba(80,80,80,0.75);
|
||||
--border: #888;
|
||||
--dark-border: #888;
|
||||
--selected: rgba(0,255,0,0.5);
|
||||
--selected-hover: rgba(0,255,0,0.8);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Russo One';
|
||||
src: url('/moto/russo-one.ttf');
|
||||
}
|
||||
|
||||
/** defaults */
|
||||
|
||||
a, a:hover, a:visited {
|
||||
border: none;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
body, div {
|
||||
font-family: system-ui, Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 0.5px;
|
||||
max-height: 0.5px;
|
||||
border-top: 0.5px solid rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
input {
|
||||
background-color: #eee;
|
||||
border-radius: 3px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
font-family: sans-serif;
|
||||
font-weight: normal;
|
||||
font-size: larger;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#app-body {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#app-body > div {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#curtain {
|
||||
z-index: 50000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
background-color: #fff;
|
||||
font-family: 'Russo One', sans-serif;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#container {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#container canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark #top {
|
||||
color: #eee;
|
||||
background-color: var(--dark-menu-back);
|
||||
}
|
||||
|
||||
#top {
|
||||
z-index: 50;
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
flex-direction: row;
|
||||
background-color: var(--menu-back);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.noselect {
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
:-moz-any() {
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: hidden !important;
|
||||
}
|
||||
:-moz-any() {
|
||||
overflow-x: scroll !important;
|
||||
overflow-y: scroll !important;
|
||||
margin-right: -14px !important;
|
||||
margin-bottom: -14px !important;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 2px 3px 2px 3px;
|
||||
border: 1px solid rgba(150,150,150,0.5);
|
||||
background-color: rgba(220,220,220,0.75);
|
||||
user-select: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dark button:hover {
|
||||
background-color: rgba(220,220,220,1);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgba(210,210,210,1);
|
||||
}
|
||||
|
||||
/** misc ui **/
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pad3 {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.pad5 {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
#top-left {
|
||||
margin-left: 5px;
|
||||
flex-direction: row;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.menu, #top-right > div {
|
||||
cursor: default;
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
padding: 8px 12px 8px 12px;
|
||||
border-radius: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.dark .menu hr {
|
||||
border-top: 0.5px solid rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
.menu:hover, .menu-items > div:hover, .tools i:hover {
|
||||
background-color: var(--menu-blue);
|
||||
}
|
||||
|
||||
.menu:hover .menu-items {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.dark .menu-items {
|
||||
background: #666;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
.menu-items {
|
||||
display: none;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 6px;
|
||||
background: #eee;
|
||||
padding: 4px;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.menu-items > div {
|
||||
gap: 15px;
|
||||
border-radius: 6px;
|
||||
padding: 4px 8px 4px 8px;
|
||||
}
|
||||
|
||||
.menu-items .grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.menu-items .short {
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
#top-left:not(:hover) #mode-label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#mode-label {
|
||||
display: none;
|
||||
position: absolute;
|
||||
text-transform: capitalize;
|
||||
padding: 2px 4px 2px 4px;
|
||||
background-color: rgba(0,200,0,0.35);
|
||||
border: 1px solid rgba(0,0,0,0.25);
|
||||
border-radius: 3px;
|
||||
transform: translateX(-50%);
|
||||
top: calc(100% + 2px);
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
#mode-label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -10px; /* Adjust to position the caret above the menu */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #777 transparent;
|
||||
}
|
||||
|
||||
.dark #mode-label:before {
|
||||
border-color: transparent transparent #ddd transparent;
|
||||
}
|
||||
|
||||
.dark #mode-label {
|
||||
background-color: rgba(0,200,0,0.65);
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
}
|
||||
|
||||
#top-mid {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#top-right {
|
||||
flex-direction: row;
|
||||
margin-right: 5px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#top-right > div {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/** specific modal dialogs */
|
||||
|
||||
.export {
|
||||
flex-direction: column;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.export, .export div {
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.export input, .export button {
|
||||
font-family: monospace;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.export button {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.addact {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.addact input, .additem input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.additem {
|
||||
gap: 5px 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 50px;
|
||||
}
|
||||
|
||||
/* .additem hr {
|
||||
width: 100%;
|
||||
} */
|
||||
|
||||
.additem button, .additem hr, .additem code {
|
||||
grid-column: span 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-import {
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.image-import label {
|
||||
padding-right: 5px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.image-import input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-import button {
|
||||
margin: 10px 20px 1px 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/** generic modal dialogs **/
|
||||
|
||||
#modal_page {
|
||||
background-color: rgba(0,0,0,0.25);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
z-index: 500 !important;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#modal_frame {
|
||||
border-radius: 4px !important;
|
||||
background-color: #fff !important;
|
||||
flex-direction: column;
|
||||
padding: 0 !important; /* override common below */
|
||||
/* z-index: 51; */
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#modal_frame > div {
|
||||
flex-direction: column;
|
||||
padding: 5px; /* compensate for removing padding above */
|
||||
}
|
||||
|
||||
#modal_title {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom: 1px solid gray;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
font-family: 'Russo One', monospace;
|
||||
font-size: smaller;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#modal_title_close {
|
||||
position: absolute;
|
||||
color: #555;
|
||||
right: 3px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#modal_title_close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/** welcome dialog **/
|
||||
|
||||
.welcome {
|
||||
font-size: smaller;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.welcome a {
|
||||
color: #038;
|
||||
border-radius: 3px;
|
||||
padding: 0 10px 0 10px;
|
||||
}
|
||||
|
||||
.welcome a:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.welcome .choice {
|
||||
gap: 5px
|
||||
}
|
||||
|
||||
/** settings dialog **/
|
||||
|
||||
.settings {
|
||||
gap: 5px;
|
||||
display: grid;
|
||||
background-color: #fff;
|
||||
grid: min-content 1fr / min-content 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.settings > div {
|
||||
display: grid;
|
||||
background-color: #f5f5f5;
|
||||
grid: min-content 1fr / min-content 1fr;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.settings .header {
|
||||
grid-column: 1 / 3;
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
background-color: #ccc;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings label {
|
||||
padding: 0 5px 0 3px;
|
||||
}
|
||||
|
||||
.settings input {
|
||||
font-family: monospace;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
/** spinner animation **/
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate3d(-50%, -50%, 0) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.spin::before {
|
||||
position: absolute;
|
||||
animation: 2.0s linear infinite spinner;
|
||||
animation-play-state: inherit;
|
||||
border: solid 10px #cfd0d1;
|
||||
border-bottom-color: #1c87c9;
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/** common look & feel */
|
||||
|
||||
.dark #modal_frame, .dark #actions > div, .dark #grouplist > div {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
|
||||
#modal_frame, #actions > div, #grouplist > div {
|
||||
border: 1px solid gray;
|
||||
border-radius: 3px;
|
||||
margin: 0 0 2px 0;
|
||||
padding: 3px;
|
||||
background-color: rgba(255,255,255,0.5);
|
||||
}
|
||||
|
||||
#actions .vsep, #grouplist .vsep {
|
||||
margin: 1px 0 1px 0;
|
||||
border-bottom: 1px dashed gray;
|
||||
}
|
||||
|
||||
.menu .selected {
|
||||
background-color: var(--selected);
|
||||
}
|
||||
|
||||
.menu .selected:hover {
|
||||
background-color: var(--selected-hover);
|
||||
}
|
||||
|
||||
/* .dark .head {
|
||||
color: #000 !important;
|
||||
} */
|
||||
|
||||
/** slide in/out logging window **/
|
||||
|
||||
.dark #logger {
|
||||
border-color: var(--dark-border);
|
||||
background-color: var(--dark-menu-back);
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
#logger {
|
||||
border-top: 1px solid var(--border);
|
||||
border-right: 1px solid var(--border);
|
||||
background-color: var(--menu-back);
|
||||
display: none;
|
||||
color: #555;
|
||||
min-width: 300px;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
margin: 0 0 0 1px;
|
||||
bottom: 0;
|
||||
left: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#logtext {
|
||||
flex-direction: column;
|
||||
max-height: 8em;
|
||||
min-height: 1em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#logtext > div {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#logtext::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
#logtext::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#logtext::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background-color: #ddd;
|
||||
border: 3px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
/** left menu sketch and object tool menus */
|
||||
|
||||
#tools {
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dark .tools {
|
||||
border-color: var(--dark-border);
|
||||
background-color: var(--dark-menu-back);
|
||||
}
|
||||
|
||||
.dark .tools > div {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tools {
|
||||
align-items: flex-start;
|
||||
background-color: var(--menu-back);
|
||||
}
|
||||
|
||||
.tools > div {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tools i {
|
||||
padding: 9px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.tool > div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding-left: 10px;
|
||||
left: 100%;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
place-items: center; /* Center the single grid item */
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dark .tool label {
|
||||
background-color: var(--dark-menu-back);
|
||||
border: 1px solid var(--dark-border);
|
||||
}
|
||||
|
||||
.tool label:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2px; /* Adjust to position the caret */
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #777 transparent transparent;
|
||||
}
|
||||
|
||||
.dark .tool label:before {
|
||||
border-color: transparent #ddd transparent transparent;
|
||||
}
|
||||
|
||||
.tool label {
|
||||
font-size: 12px !important;
|
||||
background-color: var(--menu-back);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px 4px 6px;
|
||||
}
|
||||
|
||||
.tool:hover > div {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
/** group/model buttons top/right of workspace **/
|
||||
|
||||
#grouplist {
|
||||
position: absolute;
|
||||
margin: 3px 3px 0 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
#grouplist .selected {
|
||||
background-color: var(--selected);
|
||||
}
|
||||
|
||||
#grouplist .selected:hover {
|
||||
background-color: var(--selected-hover);
|
||||
}
|
||||
|
||||
#grouplist, #grouplist div {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#grouplist .group {
|
||||
font-weight: bold;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
#grouplist .models {
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
#grouplist .models .square {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark #grouplist .models .square > svg {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#grouplist .models .square > svg {
|
||||
aspect-ratio: 1;
|
||||
max-height: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#grouplist .models > div {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#grouplist .models button {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/** selection info bottom/right of workspace **/
|
||||
|
||||
#selectlist {
|
||||
font-size: smaller;
|
||||
position: absolute;
|
||||
margin: 0 3px 3px 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#selectlist > .side {
|
||||
position: relative;
|
||||
width: 1.75em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#selectlist > .side > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
#selectlist > div {
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
#selectlist > button, #selectlist > div > button {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#selectlist label {
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
text-align: right;
|
||||
padding: 2px;
|
||||
background-color: rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
#selectlist .top {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: smaller;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#selectlist .side {
|
||||
font-weight: bold;
|
||||
font-size: smaller;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
#selectlist .grid {
|
||||
background-color: rgba(255,255,255,0.8);
|
||||
border: 1px solid rgba(0,0,0,0.15);
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
gap: 2px 2px;
|
||||
grid: min-content 1fr 1fr / min-content 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
#selectlist .grid0 {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#selectlist .grid1 {
|
||||
grid: min-content 1fr / min-content 1fr;
|
||||
}
|
||||
|
||||
#selectlist .grid2 {
|
||||
grid: min-content 1fr 1fr / min-content 1fr 1fr;
|
||||
}
|
||||
|
||||
.grid .ungrouped {
|
||||
background-color: rgba(128,128,128,0.5) !important;
|
||||
}
|
||||
|
||||
.grid .ungrouped:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.tempedit, .rename {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.tempedit input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.editable {
|
||||
color: #05a;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.editable:hover {
|
||||
color: #000;
|
||||
background-color: rgba(0,128,255,0.5) !important;
|
||||
}
|
||||
|
||||
#pinner-box {
|
||||
position: absolute;
|
||||
z-index: 5000;
|
||||
left: 50%;
|
||||
bottom: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
.dark #pinner {
|
||||
background-color: rgba(255,255,255,0.15);
|
||||
border-color: rgba(255,255,255,0.25);
|
||||
border-top-color: rgba(255,255,255,0.6);
|
||||
}
|
||||
|
||||
#pinner {
|
||||
z-index: 5000;
|
||||
display: none;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
border: 12px solid rgba(0,0,0,0.15);
|
||||
border-radius: 50%;
|
||||
border-top-color: rgba(0,0,0,0.6);
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg) }
|
||||
}
|
||||
38
web2/mesh/index.html
Normal file
38
web2/mesh/index.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="author" content="Stewart Allen">
|
||||
<meta name="keywords" content="3d,mesh,editor,stl,obj,3mf" />
|
||||
<meta name="copyright" content="stewart allen [sa@grid.space]">
|
||||
<meta name="description" content="Browser-based 3D Mesh Editor">
|
||||
<meta property="og:description" content="Mesh:Tool is a browser-based 3D Mesh Editor">
|
||||
<meta property="og:title" content="Browser-based Mesh Editor">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://grid.space/mesh/">
|
||||
<title>Mesh:Tool</title>
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<link rel="stylesheet" type="text/css" href="index.css">
|
||||
<link href="../font/css/all.min.css" rel="stylesheet">
|
||||
<link href="../fon2/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<script src="../lib/ext/earcut.js" type="module"></script>
|
||||
<script src="../lib/ext/tween.js"></script>
|
||||
<script src="../font/js/all.min.js" crossorigin="anonymous" async></script>
|
||||
<script src="../lib/main/mesh.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="curtain"><img id="loading"/>loading...</div>
|
||||
<div id="container"></div>
|
||||
<div id="modal_page"></div>
|
||||
<div id="top">
|
||||
<div id="top-left"></div>
|
||||
<div id="top-mid"></div>
|
||||
<div id="top-right"></div>
|
||||
</div>
|
||||
<div id="top-sep"></div>
|
||||
<div id="app-body"></div>
|
||||
<div id="pinner-box"><div id="pinner"></div></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
web2/moto/russo-one.ttf
Normal file
BIN
web2/moto/russo-one.ttf
Normal file
Binary file not shown.
17
web2/serve.json
Normal file
17
web2/serve.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "**/*",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cross-Origin-Embedder-Policy",
|
||||
"value": "require-corp"
|
||||
},
|
||||
{
|
||||
"key": "Cross-Origin-Opener-Policy",
|
||||
"value": "same-origin"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue