1/**
2 * Dependency-free writers from a merged `TriMesh` to common mesh files.
3 * Positions and normals only — the tessellated approximation of the model.
4 */
5import type { TriMesh } from '../model/types'
7export function toOBJ(mesh: TriMesh): Uint8Array {
8 const { positions, normals, indices } = mesh
9 const lines: string[] = ['# mesh-studio export']
10 for (let i = 0; i < positions.length; i += 3) {
11 lines.push(`v ${positions[i]} ${positions[i + 1]} ${positions[i + 2]}`)
12 }
13 for (let i = 0; i < normals.length; i += 3) {
14 lines.push(`vn ${normals[i]} ${normals[i + 1]} ${normals[i + 2]}`)
15 }
16 for (let i = 0; i < indices.length; i += 3) {
17 const a = indices[i] + 1
18 const b = indices[i + 1] + 1
19 const c = indices[i + 2] + 1
20 lines.push(`f ${a}//${a} ${b}//${b} ${c}//${c}`)
21 }
22 return new TextEncoder().encode(lines.join('\n') + '\n')
23}
25export function toPLY(mesh: TriMesh): Uint8Array {
26 const { positions, normals, indices } = mesh
27 const nVerts = positions.length / 3
28 const nFaces = indices.length / 3
29 const lines: string[] = [
30 'ply',
31 'format ascii 1.0',
32 'comment mesh-studio export',
33 `element vertex ${nVerts}`,
34 'property float x',
35 'property float y',
36 'property float z',
37 'property float nx',
38 'property float ny',
39 'property float nz',
40 `element face ${nFaces}`,
41 'property list uchar int vertex_index',
42 'end_header',
43 ]
44 for (let i = 0; i < nVerts; i++) {
45 const p = i * 3
46 lines.push(
47 `${positions[p]} ${positions[p + 1]} ${positions[p + 2]} ` +
48 `${normals[p]} ${normals[p + 1]} ${normals[p + 2]}`,
49 )
50 }
51 for (let i = 0; i < nFaces; i++) {
52 const f = i * 3
53 lines.push(`3 ${indices[f]} ${indices[f + 1]} ${indices[f + 2]}`)
54 }
55 return new TextEncoder().encode(lines.join('\n') + '\n')
56}
58export function toSTL(mesh: TriMesh): Uint8Array {
59 const { positions, normals, indices } = mesh
60 const nTri = indices.length / 3
61 const buffer = new ArrayBuffer(84 + nTri * 50)
62 const view = new DataView(buffer)
63 // 80-byte header left as zeros, then triangle count
64 view.setUint32(80, nTri, true)
65 let off = 84
66 const faceNormal = (a: number, b: number, c: number) => {
67 // average the vertex normals, fall back to geometric normal
68 let nx = normals[a] + normals[b] + normals[c]
69 let ny = normals[a + 1] + normals[b + 1] + normals[c + 1]
70 let nz = normals[a + 2] + normals[b + 2] + normals[c + 2]
71 const len = Math.hypot(nx, ny, nz)
72 if (len < 1e-9) {
73 const ux = positions[b] - positions[a]
74 const uy = positions[b + 1] - positions[a + 1]
75 const uz = positions[b + 2] - positions[a + 2]
76 const vx = positions[c] - positions[a]
77 const vy = positions[c + 1] - positions[a + 1]
78 const vz = positions[c + 2] - positions[a + 2]
79 nx = uy * vz - uz * vy
80 ny = uz * vx - ux * vz
81 nz = ux * vy - uy * vx
82 }
83 const l = Math.hypot(nx, ny, nz) || 1
84 return [nx / l, ny / l, nz / l] as const
85 }
86 for (let t = 0; t < nTri; t++) {
87 const ia = indices[t * 3] * 3
88 const ib = indices[t * 3 + 1] * 3
89 const ic = indices[t * 3 + 2] * 3
90 const [nx, ny, nz] = faceNormal(ia, ib, ic)
91 view.setFloat32(off, nx, true)
92 view.setFloat32(off + 4, ny, true)
93 view.setFloat32(off + 8, nz, true)
94 off += 12
95 for (const iv of [ia, ib, ic]) {
96 view.setFloat32(off, positions[iv], true)
97 view.setFloat32(off + 4, positions[iv + 1], true)
98 view.setFloat32(off + 8, positions[iv + 2], true)
99 off += 12
100 }
101 view.setUint16(off, 0, true)
102 off += 2
103 }
104 return new Uint8Array(buffer)
105}