1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
|
package main
import (
"bufio"
"bytes"
"encoding/binary"
"fmt"
"math"
"strconv"
"strings"
)
// STL parsing
// ============================================================================
//
// STL is a triangle soup: no materials, no textures, no scene graph, no
// indices — just a flat list of independent triangles. That is why a viewer
// for it does not need a 3D engine.
//
// The file is parsed here, in Go, and stored as a packed vertex buffer that the
// browser can hand to WebGL unmodified. The alternative — shipping the raw STL
// and parsing it in JavaScript — would mean writing the same parser a second
// time in a language with no binary types worth the name, and re-running it on
// every page view.
//
// Two on-disk formats exist and both are supported:
//
// binary: 80-byte header, uint32 triangle count, then 50 bytes per triangle
// (12 little-endian float32 = normal + 3 vertices, plus a uint16
// attribute count that is almost always zero).
// ascii: "solid … facet normal x y z / outer loop / vertex x y z ×3 /
// endloop / endfacet … endsolid".
// vertexStride is the number of float32s per vertex in the packed buffer:
// position (3) followed by normal (3).
const vertexStride = 6
// mesh is a parsed STL, normalised and ready to upload to the GPU.
type mesh struct {
// Verts is interleaved [px,py,pz, nx,ny,nz] per vertex, three consecutive
// vertices per triangle.
Verts []float32
Triangles int
}
// parseSTL parses either STL flavour and returns a normalised mesh.
//
// The result is centred on the origin and scaled to fit a unit box, so the
// viewer's camera does not need to know anything about the model: a 0.5mm screw
// and a 200mm bracket both arrive framed identically.
func parseSTL(data []byte) (*mesh, error) {
m, err := parseSTLRaw(data)
if err != nil {
return nil, err
}
if m.Triangles == 0 {
return nil, fmt.Errorf("STL contains no triangles")
}
normalizeMesh(m)
return m, nil
}
func parseSTLRaw(data []byte) (*mesh, error) {
if isBinarySTL(data) {
return parseBinarySTL(data)
}
return parseASCIISTL(data)
}
// isBinarySTL decides which flavour a file is.
//
// The "solid" prefix is NOT a reliable signal: several CAD exporters write
// binary files whose 80-byte header happens to begin with "solid". The size
// check is authoritative instead, because a binary STL's length is fully
// determined by its triangle count.
func isBinarySTL(data []byte) bool {
if len(data) < 84 {
return false
}
count := binary.LittleEndian.Uint32(data[80:84])
// Guard against a bogus count overflowing the multiplication.
if count > (1<<32-1)/50 {
return false
}
return len(data) == 84+int(count)*50
}
func parseBinarySTL(data []byte) (*mesh, error) {
if len(data) < 84 {
return nil, fmt.Errorf("binary STL too short: %d bytes", len(data))
}
count := int(binary.LittleEndian.Uint32(data[80:84]))
need := 84 + count*50
if len(data) < need {
return nil, fmt.Errorf("binary STL truncated: have %d bytes, need %d for %d triangles",
len(data), need, count)
}
m := &mesh{Verts: make([]float32, 0, count*3*vertexStride), Triangles: count}
for i := 0; i < count; i++ {
off := 84 + i*50
f := func(n int) float32 {
return math.Float32frombits(binary.LittleEndian.Uint32(data[off+n*4 : off+n*4+4]))
}
nx, ny, nz := f(0), f(1), f(2)
var tri [9]float32
for v := 0; v < 9; v++ {
tri[v] = f(3 + v)
}
appendTriangle(m, tri, nx, ny, nz)
}
return m, nil
}
// parseASCIISTL reads the textual flavour.
//
// Rather than matching the full grammar, this scans for the two keywords that
// carry data ("facet normal" and "vertex") and ignores the structural noise.
// Real-world ASCII STL files vary in whitespace and capitalisation, and a
// strict parser buys nothing here.
func parseASCIISTL(data []byte) (*mesh, error) {
m := &mesh{}
sc := bufio.NewScanner(bytes.NewReader(data))
// Vertex lines are short, but a file with no newlines at all would
// otherwise overflow the default 64KiB limit before failing usefully.
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
var normal [3]float32
var tri [9]float32
nVerts := 0
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" {
continue
}
lower := strings.ToLower(line)
switch {
case strings.HasPrefix(lower, "facet normal"):
fields := strings.Fields(line)
if len(fields) >= 5 {
for i := 0; i < 3; i++ {
v, err := strconv.ParseFloat(fields[2+i], 32)
if err != nil {
return nil, fmt.Errorf("ascii STL: bad normal %q: %w", line, err)
}
normal[i] = float32(v)
}
}
nVerts = 0
case strings.HasPrefix(lower, "vertex"):
fields := strings.Fields(line)
if len(fields) < 4 {
return nil, fmt.Errorf("ascii STL: bad vertex %q", line)
}
if nVerts >= 3 {
return nil, fmt.Errorf("ascii STL: more than three vertices in a facet")
}
for i := 0; i < 3; i++ {
v, err := strconv.ParseFloat(fields[1+i], 32)
if err != nil {
return nil, fmt.Errorf("ascii STL: bad vertex %q: %w", line, err)
}
tri[nVerts*3+i] = float32(v)
}
nVerts++
case strings.HasPrefix(lower, "endfacet"):
if nVerts != 3 {
return nil, fmt.Errorf("ascii STL: facet with %d vertices", nVerts)
}
appendTriangle(m, tri, normal[0], normal[1], normal[2])
m.Triangles++
nVerts = 0
normal = [3]float32{}
}
}
if err := sc.Err(); err != nil {
return nil, fmt.Errorf("ascii STL: %w", err)
}
return m, nil
}
// appendTriangle writes one triangle's three vertices into the packed buffer.
//
// The stored normal is recomputed from the vertices whenever the file's own
// normal is absent or degenerate (a zero vector). Many exporters write zeroed
// normals and expect the consumer to derive them, and a zero normal would leave
// the face unlit and invisible.
func appendTriangle(m *mesh, tri [9]float32, nx, ny, nz float32) {
if nx == 0 && ny == 0 && nz == 0 {
nx, ny, nz = faceNormal(tri)
}
for v := 0; v < 3; v++ {
m.Verts = append(m.Verts,
tri[v*3], tri[v*3+1], tri[v*3+2],
nx, ny, nz)
}
}
// faceNormal computes a unit normal from a triangle's winding order, using the
// right-hand rule that STL specifies.
func faceNormal(t [9]float32) (float32, float32, float32) {
ux, uy, uz := t[3]-t[0], t[4]-t[1], t[5]-t[2]
vx, vy, vz := t[6]-t[0], t[7]-t[1], t[8]-t[2]
nx := uy*vz - uz*vy
ny := uz*vx - ux*vz
nz := ux*vy - uy*vx
l := float32(math.Sqrt(float64(nx*nx + ny*ny + nz*nz)))
if l == 0 {
// A degenerate (zero-area) triangle has no meaningful normal. Point it
// at the viewer so it is at worst a flat shaded speck.
return 0, 0, 1
}
return nx / l, ny / l, nz / l
}
// normalizeMesh centres the model on the origin and scales it to fit within a
// box of side 1, so the viewer can use a fixed camera for any model.
func normalizeMesh(m *mesh) {
minv := [3]float32{math.MaxFloat32, math.MaxFloat32, math.MaxFloat32}
maxv := [3]float32{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32}
for i := 0; i < len(m.Verts); i += vertexStride {
for a := 0; a < 3; a++ {
v := m.Verts[i+a]
if v < minv[a] {
minv[a] = v
}
if v > maxv[a] {
maxv[a] = v
}
}
}
var center [3]float32
var extent float32
for a := 0; a < 3; a++ {
center[a] = (minv[a] + maxv[a]) / 2
if d := maxv[a] - minv[a]; d > extent {
extent = d
}
}
// A completely flat or single-point model has zero extent; leave the scale
// alone rather than dividing by zero.
scale := float32(1)
if extent > 0 {
scale = 1 / extent
}
for i := 0; i < len(m.Verts); i += vertexStride {
for a := 0; a < 3; a++ {
m.Verts[i+a] = (m.Verts[i+a] - center[a]) * scale
}
}
}
// packMesh serialises the vertex buffer as little-endian float32s.
//
// This is what the browser fetches: it goes straight into a WebGL buffer with
// no parsing at all, because little-endian float32 is exactly what a typed
// array expects on every platform a browser runs on today.
func packMesh(m *mesh) []byte {
out := make([]byte, len(m.Verts)*4)
for i, v := range m.Verts {
binary.LittleEndian.PutUint32(out[i*4:], math.Float32bits(v))
}
return out
}
|