/** Copyright Stewart Allen -- All Rights Reserved */ import "../ext/earcut.js"; import '../add/array.js'; import '../add/class.js'; import '../add/three.js'; import { version } from '../moto/license.js'; import { client as motoClient } from '../moto/client.js'; import { worker } from '../moto/worker.js'; import { sliceConnect } from '../geo/slicer.js'; import { newPolygon } from '../geo/polygon.js'; import { polygons } from '../geo/polygons.js'; import { CSG } from '../geo/csg.js'; import { util } from './util.js'; import { tool as meshTool } from './tool.js'; import { THREE } from '../ext/three.js'; const { Triangle, Vector3, BufferGeometry, BufferAttribute, computeFaceNormal } = THREE; const cache = {}; // add scoped access to cache const work = { cache }; // start worker pool (disabled for now with *0) motoClient.start(`/v2/lib/mesh/pool.js?${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 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 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 [...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 = util.toRadians(twist); for (let i=0; i 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 { 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 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 { 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