379 lines
12 KiB
JavaScript
379 lines
12 KiB
JavaScript
|
const { CSG } = require('@jscad/csg')
|
||
|
const { vt2jscad } = require('./vt2jscad')
|
||
|
const { BinaryReader } = require('@jscad/io-utils')
|
||
|
|
||
|
// STL function from http://jsfiddle.net/Riham/yzvGD/35/
|
||
|
// CC BY-SA by Riham
|
||
|
// changes by Rene K. Mueller <spiritdude@gmail.com>
|
||
|
// changes by Mark 'kaosat-dev' Moissette
|
||
|
// 2017/10/14: refactoring, added support for CSG output etc
|
||
|
// 2013/03/28: lot of rework and debugging included, and error handling
|
||
|
// 2013/03/18: renamed functions, creating .jscad source direct via polyhedron()
|
||
|
const echo = console.info
|
||
|
|
||
|
function deserialize (stl, filename, options) {
|
||
|
const defaults = {version: '0.0.0', addMetaData: true, output: 'jscad'}
|
||
|
options = Object.assign({}, defaults, options)
|
||
|
const {version, output, addMetaData} = options
|
||
|
|
||
|
const isBinary = isDataBinaryRobust(stl)
|
||
|
|
||
|
stl = isBinary && isBuffer(stl) ? bufferToBinaryString(stl) : stl
|
||
|
|
||
|
const elementFormatterJscad = ({vertices, triangles, normals, colors, index}) => `// object #${index}: triangles: ${triangles.length}\n${vt2jscad(vertices, triangles, null)}`
|
||
|
const elementFormatterCSG = ({vertices, triangles, normals, colors}) => polyhedron({ points: vertices, polygons: triangles })
|
||
|
|
||
|
const deserializer = isBinary ? deserializeBinarySTL : deserializeAsciiSTL
|
||
|
const elementFormatter = output === 'jscad' ? elementFormatterJscad : elementFormatterCSG
|
||
|
const outputFormatter = output === 'jscad' ? formatAsJscad : formatAsCsg
|
||
|
|
||
|
return outputFormatter(deserializer(stl, filename, version, elementFormatter), addMetaData, version, filename)
|
||
|
|
||
|
/*
|
||
|
if (err) src += '// WARNING: import errors: ' + err + ' (some triangles might be misaligned or missing)\n'
|
||
|
src += '// objects: 1\n// object #1: triangles: ' + totalTriangles + '\n\n'
|
||
|
src += 'function main() { return '
|
||
|
src += vt2jscad(vertices, triangles, normals, colors)
|
||
|
src += '; }' */
|
||
|
}
|
||
|
|
||
|
function bufferToBinaryString (buffer) {
|
||
|
let binary = ''
|
||
|
const bytes = new Uint8Array(buffer)
|
||
|
let length = bytes.byteLength
|
||
|
for (let i = 0; i < length; i++) {
|
||
|
binary += String.fromCharCode(bytes[i])
|
||
|
}
|
||
|
return binary
|
||
|
}
|
||
|
|
||
|
// taken from https://github.com/feross/is-buffer if we need it more than once, add as dep
|
||
|
function isBuffer (obj) {
|
||
|
return !!obj.constructor && typeof obj.constructor.isBuffer === 'function' && obj.constructor.isBuffer(obj)
|
||
|
}
|
||
|
|
||
|
// transforms input to string if it was not already the case
|
||
|
function ensureString (buf) {
|
||
|
if (typeof buf !== 'string') {
|
||
|
let arrayBuffer = new Uint8Array(buf)
|
||
|
let str = ''
|
||
|
for (let i = 0; i < buf.byteLength; i++) {
|
||
|
str += String.fromCharCode(arrayBuffer[i]) // implicitly assumes little-endian
|
||
|
}
|
||
|
return str
|
||
|
} else {
|
||
|
return buf
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// reliable binary detection
|
||
|
function isDataBinaryRobust (data) {
|
||
|
// console.log('data is binary ?')
|
||
|
const 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
|
||
|
const text = ensureString(data)
|
||
|
const isBinary = patternVertex.exec(text) === null
|
||
|
return isBinary
|
||
|
}
|
||
|
|
||
|
function formatAsJscad (data, addMetaData, version, filename) {
|
||
|
let code = addMetaData ? `//
|
||
|
// producer: OpenJSCAD.org Compatibility${version} STL Binary Importer
|
||
|
// date: ${new Date()}
|
||
|
// source: ${filename}
|
||
|
//
|
||
|
` : ''
|
||
|
|
||
|
return code + `function main() { return union(
|
||
|
// objects: ${data.length}
|
||
|
${data.join('\n')}); }
|
||
|
`
|
||
|
}
|
||
|
|
||
|
function formatAsCsg (data) {
|
||
|
return new CSG().union(data)
|
||
|
}
|
||
|
|
||
|
function deserializeBinarySTL (stl, filename, version, elementFormatter, debug = false) {
|
||
|
// -- This makes more sense if you read http://en.wikipedia.org/wiki/STL_(file_format)#Binary_STL
|
||
|
let vertices = []
|
||
|
let triangles = []
|
||
|
let normals = []
|
||
|
let colors = []
|
||
|
let converted = 0
|
||
|
let vertexIndex = 0
|
||
|
let err = 0
|
||
|
let mcolor = null
|
||
|
let umask = parseInt('01000000000000000', 2)
|
||
|
let rmask = parseInt('00000000000011111', 2)
|
||
|
let gmask = parseInt('00000001111100000', 2)
|
||
|
let bmask = parseInt('00111110000000000', 2)
|
||
|
let br = new BinaryReader(stl)
|
||
|
|
||
|
let m = 0
|
||
|
let c = 0
|
||
|
let r = 0
|
||
|
let g = 0
|
||
|
let b = 0
|
||
|
let a = 0
|
||
|
for (let i = 0; i < 80; i++) {
|
||
|
switch (m) {
|
||
|
case 6:
|
||
|
r = br.readUInt8()
|
||
|
m += 1
|
||
|
continue
|
||
|
case 7:
|
||
|
g = br.readUInt8()
|
||
|
m += 1
|
||
|
continue
|
||
|
case 8:
|
||
|
b = br.readUInt8()
|
||
|
m += 1
|
||
|
continue
|
||
|
case 9:
|
||
|
a = br.readUInt8()
|
||
|
m += 1
|
||
|
continue
|
||
|
default:
|
||
|
c = br.readChar()
|
||
|
switch (c) {
|
||
|
case 'C':
|
||
|
case 'O':
|
||
|
case 'L':
|
||
|
case 'R':
|
||
|
case '=':
|
||
|
m += 1
|
||
|
break
|
||
|
default:
|
||
|
break
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if (m === 10) { // create the default color
|
||
|
mcolor = [r / 255, g / 255, b / 255, a / 255]
|
||
|
}
|
||
|
|
||
|
let totalTriangles = br.readUInt32() // Read # triangles
|
||
|
|
||
|
for (let tr = 0; tr < totalTriangles; tr++) {
|
||
|
if (debug) {
|
||
|
if (tr % 100 === 0) console.info(`stl importer: converted ${converted} out of ${totalTriangles} triangles`)
|
||
|
}
|
||
|
/*
|
||
|
REAL32[3] . Normal vector
|
||
|
REAL32[3] . Vertex 1
|
||
|
REAL32[3] . Vertex 2
|
||
|
REAL32[3] . Vertex 3
|
||
|
UINT16 . Attribute byte count */
|
||
|
// -- Parse normal
|
||
|
let no = []; no.push(br.readFloat()); no.push(br.readFloat()); no.push(br.readFloat())
|
||
|
|
||
|
// -- Parse every 3 subsequent floats as a vertex
|
||
|
let v1 = []; v1.push(br.readFloat()); v1.push(br.readFloat()); v1.push(br.readFloat())
|
||
|
let v2 = []; v2.push(br.readFloat()); v2.push(br.readFloat()); v2.push(br.readFloat())
|
||
|
let v3 = []; v3.push(br.readFloat()); v3.push(br.readFloat()); v3.push(br.readFloat())
|
||
|
|
||
|
let skip = 0
|
||
|
|
||
|
for (let i = 0; i < 3; i++) {
|
||
|
if (isNaN(v1[i])) skip++
|
||
|
if (isNaN(v2[i])) skip++
|
||
|
if (isNaN(v3[i])) skip++
|
||
|
if (isNaN(no[i])) skip++
|
||
|
}
|
||
|
if (skip > 0) {
|
||
|
echo('bad triangle vertice coords/normal: ', skip)
|
||
|
}
|
||
|
|
||
|
err += skip
|
||
|
// -- every 3 vertices create a triangle.
|
||
|
let triangle = []; triangle.push(vertexIndex++); triangle.push(vertexIndex++); triangle.push(vertexIndex++)
|
||
|
|
||
|
let abc = br.readUInt16()
|
||
|
let color = null
|
||
|
if (m === 10) {
|
||
|
let u = (abc & umask) // 0 if color is unique for this triangle
|
||
|
let r = (abc & rmask) / 31
|
||
|
let g = ((abc & gmask) >>> 5) / 31
|
||
|
let b = ((abc & bmask) >>> 10) / 31
|
||
|
let a = 255
|
||
|
if (u === 0) {
|
||
|
color = [r, g, b, a]
|
||
|
} else {
|
||
|
color = mcolor
|
||
|
}
|
||
|
colors.push(color)
|
||
|
}
|
||
|
|
||
|
// -- Add 3 vertices for every triangle
|
||
|
// -- TODO: OPTIMIZE: Check if the vertex is already in the array, if it is just reuse the index
|
||
|
if (skip === 0) { // checking cw vs ccw, given all normal/vertice are valid
|
||
|
// E1 = B - A
|
||
|
// E2 = C - A
|
||
|
// test = dot( Normal, cross( E1, E2 ) )
|
||
|
// test > 0: cw, test < 0 : ccw
|
||
|
let w1 = new CSG.Vector3D(v1)
|
||
|
let w2 = new CSG.Vector3D(v2)
|
||
|
let w3 = new CSG.Vector3D(v3)
|
||
|
let e1 = w2.minus(w1)
|
||
|
let e2 = w3.minus(w1)
|
||
|
let t = new CSG.Vector3D(no).dot(e1.cross(e2))
|
||
|
if (t > 0) { // 1,2,3 -> 3,2,1
|
||
|
let tmp = v3
|
||
|
v3 = v1
|
||
|
v1 = tmp
|
||
|
}
|
||
|
}
|
||
|
vertices.push(v1)
|
||
|
vertices.push(v2)
|
||
|
vertices.push(v3)
|
||
|
triangles.push(triangle)
|
||
|
normals.push(no)
|
||
|
converted++
|
||
|
}
|
||
|
|
||
|
if (err) {
|
||
|
console.warn(`WARNING: import errors: ${err} (some triangles might be misaligned or missing)`)
|
||
|
// FIXME: this used to be added to the output script, which makes more sense
|
||
|
}
|
||
|
|
||
|
return [elementFormatter({vertices, triangles, normals, colors})]
|
||
|
}
|
||
|
|
||
|
function deserializeAsciiSTL (stl, filename, version, elementFormatter) {
|
||
|
let converted = 0
|
||
|
let o
|
||
|
|
||
|
// -- Find all models
|
||
|
const objects = stl.split('endsolid')
|
||
|
// src += '// objects: ' + (objects.length - 1) + '\n'
|
||
|
let elements = []
|
||
|
for (o = 1; o < objects.length; o++) {
|
||
|
// -- Translation: a non-greedy regex for facet {...} endloop pattern
|
||
|
let patt = /\bfacet[\s\S]*?endloop/mgi
|
||
|
let vertices = []
|
||
|
let triangles = []
|
||
|
let normals = []
|
||
|
let vertexIndex = 0
|
||
|
let err = 0
|
||
|
|
||
|
let match = stl.match(patt)
|
||
|
if (match == null) continue
|
||
|
for (let i = 0; i < match.length; i++) {
|
||
|
// if(converted%100==0) status('stl to jscad: converted '+converted+' out of '+match.length+ ' facets');
|
||
|
// -- 1 normal with 3 numbers, 3 different vertex objects each with 3 numbers:
|
||
|
// let vpatt = /\bfacet\s+normal\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s*outer\s+loop\s+vertex\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s*vertex\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s*vertex\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)/mgi;
|
||
|
// (-?\d+\.?\d*) -1.21223
|
||
|
// (-?\d+\.?\d*[Ee]?[-+]?\d*)
|
||
|
let vpatt = /\bfacet\s+normal\s+(\S+)\s+(\S+)\s+(\S+)\s+outer\s+loop\s+vertex\s+(\S+)\s+(\S+)\s+(\S+)\s+vertex\s+(\S+)\s+(\S+)\s+(\S+)\s+vertex\s+(\S+)\s+(\S+)\s+(\S+)\s*/mgi
|
||
|
let v = vpatt.exec(match[i])
|
||
|
if (v == null) continue
|
||
|
if (v.length !== 13) {
|
||
|
echo('Failed to parse ' + match[i])
|
||
|
break
|
||
|
}
|
||
|
let skip = 0
|
||
|
for (let k = 0; k < v.length; k++) {
|
||
|
if (v[k] === 'NaN') {
|
||
|
echo('bad normal or triangle vertice #' + converted + ' ' + k + ": '" + v[k] + "', skipped")
|
||
|
skip++
|
||
|
}
|
||
|
}
|
||
|
err += skip
|
||
|
if (skip) {
|
||
|
continue
|
||
|
}
|
||
|
if (0 && skip) {
|
||
|
let j = 1 + 3
|
||
|
let v1 = []; v1.push(parseFloat(v[j++])); v1.push(parseFloat(v[j++])); v1.push(parseFloat(v[j++]))
|
||
|
let v2 = []; v2.push(parseFloat(v[j++])); v2.push(parseFloat(v[j++])); v2.push(parseFloat(v[j++]))
|
||
|
let v3 = []; v3.push(parseFloat(v[j++])); v3.push(parseFloat(v[j++])); v3.push(parseFloat(v[j++]))
|
||
|
echo('recalculate norm', v1, v2, v3)
|
||
|
let w1 = new CSG.Vector3D(v1)
|
||
|
let w2 = new CSG.Vector3D(v2)
|
||
|
let w3 = new CSG.Vector3D(v3)
|
||
|
let _u = w1.minus(w3)
|
||
|
let _v = w1.minus(w2)
|
||
|
let norm = _u.cross(_v).unit()
|
||
|
j = 1
|
||
|
v[j++] = norm._x
|
||
|
v[j++] = norm._y
|
||
|
v[j++] = norm._z
|
||
|
skip = false
|
||
|
}
|
||
|
let j = 1
|
||
|
let no = []; no.push(parseFloat(v[j++])); no.push(parseFloat(v[j++])); no.push(parseFloat(v[j++]))
|
||
|
let v1 = []; v1.push(parseFloat(v[j++])); v1.push(parseFloat(v[j++])); v1.push(parseFloat(v[j++]))
|
||
|
let v2 = []; v2.push(parseFloat(v[j++])); v2.push(parseFloat(v[j++])); v2.push(parseFloat(v[j++]))
|
||
|
let v3 = []; v3.push(parseFloat(v[j++])); v3.push(parseFloat(v[j++])); v3.push(parseFloat(v[j++]))
|
||
|
let triangle = []; triangle.push(vertexIndex++); triangle.push(vertexIndex++); triangle.push(vertexIndex++)
|
||
|
|
||
|
// -- Add 3 vertices for every triangle
|
||
|
// TODO: OPTIMIZE: Check if the vertex is already in the array, if it is just reuse the index
|
||
|
if (skip === 0) { // checking cw vs ccw
|
||
|
// E1 = B - A
|
||
|
// E2 = C - A
|
||
|
// test = dot( Normal, cross( E1, E2 ) )
|
||
|
// test > 0: cw, test < 0: ccw
|
||
|
let w1 = new CSG.Vector3D(v1)
|
||
|
let w2 = new CSG.Vector3D(v2)
|
||
|
let w3 = new CSG.Vector3D(v3)
|
||
|
let e1 = w2.minus(w1)
|
||
|
let e2 = w3.minus(w1)
|
||
|
let t = new CSG.Vector3D(no).dot(e1.cross(e2))
|
||
|
if (t > 0) { // 1,2,3 -> 3,2,1
|
||
|
let tmp = v3
|
||
|
v3 = v1
|
||
|
v1 = tmp
|
||
|
}
|
||
|
}
|
||
|
vertices.push(v1)
|
||
|
vertices.push(v2)
|
||
|
vertices.push(v3)
|
||
|
normals.push(no)
|
||
|
triangles.push(triangle)
|
||
|
converted++
|
||
|
}
|
||
|
if (err) {
|
||
|
console.warn(`WARNING: import errors: ${err} (some triangles might be misaligned or missing)`)
|
||
|
// FIXME: this used to be added to the output script, which makes more sense
|
||
|
}
|
||
|
|
||
|
elements.push(
|
||
|
elementFormatter({vertices, triangles, index: o})
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return elements
|
||
|
}
|
||
|
|
||
|
// FIXME : just a stand in for now from scad-api, not sure if we should rely on scad-api from here ?
|
||
|
function polyhedron (p) {
|
||
|
let pgs = []
|
||
|
let ref = p.triangles || p.polygons
|
||
|
let colors = p.colors || null
|
||
|
|
||
|
for (let i = 0; i < ref.length; i++) {
|
||
|
let pp = []
|
||
|
for (let j = 0; j < ref[i].length; j++) {
|
||
|
pp[j] = p.points[ref[i][j]]
|
||
|
}
|
||
|
|
||
|
let v = []
|
||
|
for (let j = ref[i].length - 1; j >= 0; j--) { // --- we reverse order for examples of OpenSCAD work
|
||
|
v.push(new CSG.Vertex(new CSG.Vector3D(pp[j][0], pp[j][1], pp[j][2])))
|
||
|
}
|
||
|
let s = CSG.Polygon.defaultShared
|
||
|
if (colors && colors[i]) {
|
||
|
s = CSG.Polygon.Shared.fromColor(colors[i])
|
||
|
}
|
||
|
pgs.push(new CSG.Polygon(v, s))
|
||
|
}
|
||
|
let r = CSG.fromPolygons(pgs)
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
module.exports = {
|
||
|
deserialize
|
||
|
}
|