From a9603574f1af8ad25cfd67650ad037232504bee9 Mon Sep 17 00:00:00 2001 From: Michael Hemingway Date: Mon, 15 Jan 2024 18:15:56 -0500 Subject: [PATCH] fix(STLExporter): no positionAttributes + TS (#326) --- src/exporters/STLExporter.d.ts | 21 ---- src/exporters/STLExporter.js | 166 --------------------------- src/exporters/STLExporter.ts | 198 +++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 187 deletions(-) delete mode 100644 src/exporters/STLExporter.d.ts delete mode 100644 src/exporters/STLExporter.js create mode 100644 src/exporters/STLExporter.ts diff --git a/src/exporters/STLExporter.d.ts b/src/exporters/STLExporter.d.ts deleted file mode 100644 index 54e2557d..00000000 --- a/src/exporters/STLExporter.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Object3D } from 'three' - -export interface STLExporterOptionsBinary { - binary: true -} - -export interface STLExporterOptionsString { - binary?: false -} - -export interface STLExporterOptions { - binary?: boolean -} - -export class STLExporter { - constructor() - - parse(scene: Object3D, options: STLExporterOptionsBinary): DataView - parse(scene: Object3D, options?: STLExporterOptionsString): string - parse(scene: Object3D, options?: STLExporterOptions): string | DataView -} diff --git a/src/exporters/STLExporter.js b/src/exporters/STLExporter.js deleted file mode 100644 index c7113102..00000000 --- a/src/exporters/STLExporter.js +++ /dev/null @@ -1,166 +0,0 @@ -import { Vector3 } from 'three' - -/** - * Usage: - * const exporter = new STLExporter(); - * - * // second argument is a list of options - * const data = exporter.parse( mesh, { binary: true } ); - * - */ - -class STLExporter { - parse(scene, options = {}) { - options = Object.assign( - { - binary: false, - }, - options, - ) - - const binary = options.binary - - // - - const objects = [] - let triangles = 0 - - scene.traverse(function (object) { - if (object.isMesh) { - const geometry = object.geometry - - const index = geometry.index - const positionAttribute = geometry.getAttribute('position') - - triangles += index !== null ? index.count / 3 : positionAttribute.count / 3 - - objects.push({ - object3d: object, - geometry: geometry, - }) - } - }) - - let output - let offset = 80 // skip header - - if (binary === true) { - const bufferLength = triangles * 2 + triangles * 3 * 4 * 4 + 80 + 4 - const arrayBuffer = new ArrayBuffer(bufferLength) - output = new DataView(arrayBuffer) - output.setUint32(offset, triangles, true) - offset += 4 - } else { - output = '' - output += 'solid exported\n' - } - - const vA = new Vector3() - const vB = new Vector3() - const vC = new Vector3() - const cb = new Vector3() - const ab = new Vector3() - const normal = new Vector3() - - for (let i = 0, il = objects.length; i < il; i++) { - const object = objects[i].object3d - const geometry = objects[i].geometry - - const index = geometry.index - const positionAttribute = geometry.getAttribute('position') - - if (index !== null) { - // indexed geometry - - for (let j = 0; j < index.count; j += 3) { - const a = index.getX(j + 0) - const b = index.getX(j + 1) - const c = index.getX(j + 2) - - writeFace(a, b, c, positionAttribute, object) - } - } else { - // non-indexed geometry - - for (let j = 0; j < positionAttribute.count; j += 3) { - const a = j + 0 - const b = j + 1 - const c = j + 2 - - writeFace(a, b, c, positionAttribute, object) - } - } - } - - if (binary === false) { - output += 'endsolid exported\n' - } - - return output - - function writeFace(a, b, c, positionAttribute, object) { - vA.fromBufferAttribute(positionAttribute, a) - vB.fromBufferAttribute(positionAttribute, b) - vC.fromBufferAttribute(positionAttribute, c) - - if (object.isSkinnedMesh === true) { - object.applyBoneTransform(a, vA) - object.applyBoneTransform(b, vB) - object.applyBoneTransform(c, vC) - } - - vA.applyMatrix4(object.matrixWorld) - vB.applyMatrix4(object.matrixWorld) - vC.applyMatrix4(object.matrixWorld) - - writeNormal(vA, vB, vC) - - writeVertex(vA) - writeVertex(vB) - writeVertex(vC) - - if (binary === true) { - output.setUint16(offset, 0, true) - offset += 2 - } else { - output += '\t\tendloop\n' - output += '\tendfacet\n' - } - } - - function writeNormal(vA, vB, vC) { - cb.subVectors(vC, vB) - ab.subVectors(vA, vB) - cb.cross(ab).normalize() - - normal.copy(cb).normalize() - - if (binary === true) { - output.setFloat32(offset, normal.x, true) - offset += 4 - output.setFloat32(offset, normal.y, true) - offset += 4 - output.setFloat32(offset, normal.z, true) - offset += 4 - } else { - output += '\tfacet normal ' + normal.x + ' ' + normal.y + ' ' + normal.z + '\n' - output += '\t\touter loop\n' - } - } - - function writeVertex(vertex) { - if (binary === true) { - output.setFloat32(offset, vertex.x, true) - offset += 4 - output.setFloat32(offset, vertex.y, true) - offset += 4 - output.setFloat32(offset, vertex.z, true) - offset += 4 - } else { - output += '\t\t\tvertex ' + vertex.x + ' ' + vertex.y + ' ' + vertex.z + '\n' - } - } - } -} - -export { STLExporter } diff --git a/src/exporters/STLExporter.ts b/src/exporters/STLExporter.ts new file mode 100644 index 00000000..e7fafb22 --- /dev/null +++ b/src/exporters/STLExporter.ts @@ -0,0 +1,198 @@ +import { + BufferAttribute, + BufferGeometry, + InterleavedBufferAttribute, + Mesh, + Object3D, + SkinnedMesh, + Vector3, +} from 'three' + +export interface STLExporterOptionsBinary { + binary: true +} + +export interface STLExporterOptionsString { + binary?: false +} + +export interface STLExporterOptions { + binary?: boolean +} + +const isMesh = (object: unknown): object is Mesh => (object as any).isMesh + +export class STLExporter { + private binary = false + + private output: string | DataView = '' + private offset: number = 80 // skip header + + private objects: { object3d: Object3D; geometry: BufferGeometry }[] = [] + private triangles: number = 0 + + private vA = new Vector3() + private vB = new Vector3() + private vC = new Vector3() + private cb = new Vector3() + private ab = new Vector3() + private normal = new Vector3() + + parse(scene: Object3D, options: STLExporterOptionsBinary): DataView + parse(scene: Object3D, options?: STLExporterOptionsString): string + parse(scene: Object3D, options?: STLExporterOptions): string | DataView { + this.binary = options?.binary !== undefined ? options?.binary : false + + scene.traverse((object: Object3D) => { + if (isMesh(object)) { + const geometry = object.geometry + + if (!geometry.isBufferGeometry) { + throw new Error('THREE.STLExporter: Geometry is not of type THREE.BufferGeometry.') + } + + const index = geometry.index + const positionAttribute = geometry.getAttribute('position') || null + if (!positionAttribute) return + + this.triangles += index !== null ? index.count / 3 : positionAttribute.count / 3 + + this.objects.push({ + object3d: object, + geometry: geometry, + }) + } + }) + + if (this.binary) { + const bufferLength = this.triangles * 2 + this.triangles * 3 * 4 * 4 + 80 + 4 + const arrayBuffer = new ArrayBuffer(bufferLength) + this.output = new DataView(arrayBuffer) + this.output.setUint32(this.offset, this.triangles, true) + this.offset += 4 + } else { + this.output = '' + this.output += 'solid exported\n' + } + + for (let i = 0, il = this.objects.length; i < il; i++) { + const object = this.objects[i].object3d + const geometry = this.objects[i].geometry + + const index = geometry.index + const positionAttribute = geometry.getAttribute('position') + + if (index !== null) { + // indexed geometry + for (let j = 0; j < index.count; j += 3) { + const a = index.getX(j + 0) + const b = index.getX(j + 1) + const c = index.getX(j + 2) + + this.writeFace(a, b, c, positionAttribute, object as SkinnedMesh) + } + } else { + // non-indexed geometry + for (let j = 0; j < positionAttribute.count; j += 3) { + const a = j + 0 + const b = j + 1 + const c = j + 2 + + this.writeFace(a, b, c, positionAttribute, object as SkinnedMesh) + } + } + } + + if (!this.binary) { + this.output += 'endsolid exported\n' + } + + return this.output + } + + private writeFace( + a: number, + b: number, + c: number, + positionAttribute: BufferAttribute | InterleavedBufferAttribute, + object: SkinnedMesh, + ): void { + this.vA.fromBufferAttribute(positionAttribute, a) + this.vB.fromBufferAttribute(positionAttribute, b) + this.vC.fromBufferAttribute(positionAttribute, c) + + if (object.isSkinnedMesh) { + const mesh = object as Omit & + ( + | { + boneTransform(index: number, vector: Vector3): Vector3 + } + | { + applyBoneTransform(index: number, vector: Vector3): Vector3 + } + ) + + // r151 https://github.com/mrdoob/three.js/pull/25586 + if ('applyBoneTransform' in mesh) { + mesh.applyBoneTransform(a, this.vA) + mesh.applyBoneTransform(b, this.vB) + mesh.applyBoneTransform(c, this.vC) + } else { + mesh.boneTransform(a, this.vA) + mesh.boneTransform(b, this.vB) + mesh.boneTransform(c, this.vC) + } + } + + this.vA.applyMatrix4(object.matrixWorld) + this.vB.applyMatrix4(object.matrixWorld) + this.vC.applyMatrix4(object.matrixWorld) + + this.writeNormal(this.vA, this.vB, this.vC) + + this.writeVertex(this.vA) + this.writeVertex(this.vB) + this.writeVertex(this.vC) + + if (this.binary && this.output instanceof DataView) { + this.output.setUint16(this.offset, 0, true) + this.offset += 2 + } else { + this.output += '\t\tendloop\n' + this.output += '\tendfacet\n' + } + } + + private writeNormal(vA: Vector3, vB: Vector3, vC: Vector3): void { + this.cb.subVectors(vC, vB) + this.ab.subVectors(vA, vB) + this.cb.cross(this.ab).normalize() + + this.normal.copy(this.cb).normalize() + + if (this.binary && this.output instanceof DataView) { + this.output.setFloat32(this.offset, this.normal.x, true) + this.offset += 4 + this.output.setFloat32(this.offset, this.normal.y, true) + this.offset += 4 + this.output.setFloat32(this.offset, this.normal.z, true) + this.offset += 4 + } else { + this.output += `\tfacet normal ${this.normal.x} ${this.normal.y} ${this.normal.z}\n` + this.output += '\t\touter loop\n' + } + } + + private writeVertex(vertex: Vector3): void { + if (this.binary && this.output instanceof DataView) { + this.output.setFloat32(this.offset, vertex.x, true) + this.offset += 4 + this.output.setFloat32(this.offset, vertex.y, true) + this.offset += 4 + this.output.setFloat32(this.offset, vertex.z, true) + this.offset += 4 + } else { + this.output += `\t\t\tvertex ${vertex.x} ${vertex.y} ${vertex.z}\n` + } + } +}