Last active
July 5, 2020 16:27
-
-
Save oxyflour/148a029fa5eb62e9b4b3a50ef3e086ba to your computer and use it in GitHub Desktop.
unfold stl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const stl = require('stl'), | |
| fs = require('fs') | |
| const Vec = { | |
| add(a, b) { | |
| return a.map((v, i) => v + b[i]) | |
| }, | |
| sub(a, b) { | |
| return a.map((v, i) => v - b[i]) | |
| }, | |
| dot(a, b) { | |
| return a.reduce((s, v, i) => s + v * b[i], 0) | |
| }, | |
| len(s) { | |
| return Math.sqrt(this.dot(s, s)) | |
| }, | |
| angle(a, b) { | |
| return Math.acos(Vec.dot(a, b) / Vec.len(a) / Vec.len(b)) | |
| }, | |
| cross(a, b) { | |
| return [ | |
| a[1] * b[2] - a[2] * b[1], | |
| a[2] * b[0] - a[0] * b[2], | |
| a[0] * b[1] - a[1] * b[0], | |
| ] | |
| }, | |
| normal(a, b, c) { | |
| return Vec.cross(Vec.sub(a, b), Vec.sub(b, c)) | |
| } | |
| } | |
| /** | |
| * | |
| * @param { number[] } p0 | |
| * @param { number[] } p1 | |
| * @param { number[] } p2 | |
| * @param { number[] } pa | |
| * @param { number[] } pb | |
| * @returns { number[] } | |
| */ | |
| function remap(p0, p1, p2, pa, pb, flip = false) { | |
| const [a, b] = [Vec.sub(p2, p0), Vec.sub(p1, p0)], | |
| s = Vec.len(a), | |
| [x, y] = Vec.sub(pb, pa), | |
| t = Math.atan2(y, x) - Vec.angle(a, b) * (flip ? -1 : 1) | |
| return Vec.add(pa, [s * Math.cos(t), s * Math.sin(t), 0]) | |
| } | |
| /** | |
| * | |
| * @param { number[] } p0 | |
| * @param { number[] } p1 | |
| * @param { number[] } p2 | |
| * @param { number[] } p3 | |
| */ | |
| function flipped(p0, p1, p2, p3) { | |
| return Vec.dot(Vec.normal(p0, p1, p2), Vec.normal(p3, p2, p1)) < 0 | |
| } | |
| class Mesh { | |
| /** @type { { [k: string]: number } } */ | |
| idx = { } | |
| /** @type { { xyz: number[], uvw: number[], id: number }[] } */ | |
| pts = [] | |
| /** @param { number[] } xyz */ | |
| addPt(xyz) { | |
| const k = xyz.map(v => v.toFixed(5)).join(',') | |
| if (this.idx[k] === undefined) { | |
| const id = this.idx[k] = this.pts.length | |
| this.pts[id] = { xyz, id, uvw: [] } | |
| } | |
| return this.idx[k] | |
| } | |
| /** @type { { ij: number[], id: number, fcs: number[] }[] } */ | |
| eds = [] | |
| /** @param { number[] } ij */ | |
| addEd(ij) { | |
| const k = ij.sort().join(':') | |
| if (this.idx[k] === undefined) { | |
| const id = this.idx[k] = this.eds.length | |
| this.eds[id] = { ij, id, fcs: [] } | |
| } | |
| return this.idx[k] | |
| } | |
| /** @type { { eds: number[], ijk: number[], fixed?: boolean }[] } */ | |
| fcs = [] | |
| /** @param { [number[], number[], number[]] } pts */ | |
| addFc(pts) { | |
| const [i, j, k] = pts.map(xyz => this.addPt(xyz)), | |
| eds = [[i, j], [j, k], [k, i]].map(ij => this.addEd(ij)), | |
| ijk = [i, j, k], | |
| f = this.fcs.length | |
| for (const e of eds) { | |
| this.eds[e].fcs.push(f) | |
| } | |
| this.fcs.push({ eds, ijk }) | |
| } | |
| /** | |
| * @param { number } f | |
| * @param { (f: number, p: number) => void } cb | |
| */ | |
| walk(f, cb, p = -1) { | |
| if (p < 0) { | |
| for (const fc of this.fcs) { | |
| fc.fixed = false | |
| } | |
| } | |
| const fc = this.fcs[f] | |
| if (!fc.fixed) { | |
| cb(f, p) | |
| fc.fixed = true | |
| for (const e of fc.eds) { | |
| for (const n of this.eds[e].fcs) { | |
| this.walk(n, cb, f) | |
| } | |
| } | |
| } | |
| return this | |
| } | |
| unfold() { | |
| for (const pt of this.pts) { | |
| pt.uvw.length = 0 | |
| } | |
| const f = Math.floor(Math.random() * this.fcs.length), | |
| [p0, p1] = this.fcs[f].ijk.map(i => this.pts[i]) | |
| p0.uvw = p0.xyz.slice() | |
| const d = Vec.sub(p1.xyz, p0.xyz), | |
| s = Vec.len(d), | |
| t = Math.atan2(d[1], d[0]) | |
| p1.uvw = Vec.add(p0.uvw, [s * Math.cos(t), s * Math.sin(t), 0]) | |
| return this.walk(f, (f, p) => { | |
| const [p0, p1, p2] = this.fcs[f].ijk.map(i => this.pts[i]), | |
| [p3] = p >= 0 ? this.fcs[p].ijk.filter(i => !this.fcs[f].ijk.includes(i)).map(i => this.pts[i]) : [] | |
| if (!p0.uvw.length && p1.uvw.length && p2.uvw.length) { | |
| p0.uvw = remap(p1.xyz, p2.xyz, p0.xyz, p1.uvw, p2.uvw) | |
| if (p3 && p3.uvw.length && flipped(p0.uvw, p1.uvw, p2.uvw, p3.uvw)) { | |
| p0.uvw = remap(p1.xyz, p2.xyz, p0.xyz, p1.uvw, p2.uvw, true) | |
| } | |
| } else if (p0.uvw.length && !p1.uvw.length && p2.uvw.length) { | |
| p1.uvw = remap(p0.xyz, p2.xyz, p1.xyz, p0.uvw, p2.uvw) | |
| if (p3 && p3.uvw.length && flipped(p1.uvw, p0.uvw, p2.uvw, p3.uvw)) { | |
| p1.uvw = remap(p0.xyz, p2.xyz, p1.xyz, p0.uvw, p2.uvw, true) | |
| } | |
| } else if (p0.uvw.length && p1.uvw.length && !p2.uvw.length) { | |
| p2.uvw = remap(p0.xyz, p1.xyz, p2.xyz, p0.uvw, p1.uvw) | |
| if (p3 && p3.uvw.length && flipped(p2.uvw, p1.uvw, p0.uvw, p3.uvw)) { | |
| p2.uvw = remap(p0.xyz, p1.xyz, p2.xyz, p0.uvw, p1.uvw, true) | |
| } | |
| } else { | |
| console.warn(`[WARN] ignore face`) | |
| console.warn(p0) | |
| console.warn(p1) | |
| console.warn(p2) | |
| } | |
| }) | |
| } | |
| /** @param { string } file */ | |
| write(file, description = '...') { | |
| const facets = this.fcs | |
| .filter(fc => fc.ijk.every(i => this.pts[i].uvw.length)) | |
| .map(fc => ({ verts: fc.ijk.map(i => this.pts[i]).map(p => p.uvw.length ? p.uvw : p.xyz) })) | |
| fs.writeFileSync(file, stl.fromObject({ description, facets })) | |
| } | |
| } | |
| const mesh = new Mesh() | |
| for (const face of stl.toObject(fs.readFileSync('faces.stl')).facets) { | |
| mesh.addFc(face.verts) | |
| } | |
| mesh.unfold().write('unfold.stl') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment