package mesh import ( "bufio" "fmt" "io" "os" "strconv" "strings" ) // ReadOBJ parses a Wavefront OBJ file. Vertices (v), objects/groups // (o/g) and faces (f) are honored; polygons are fan-triangulated; // normals, texture coords and materials are ignored (they are // recomputed or irrelevant for geometry editing). func ReadOBJ(r io.Reader) (*Scene, error) { var verts []Vec3 type objFaces struct { name string tris []Triangle // indices into the global vert list } objs := []*objFaces{} current := func() *objFaces { if len(objs) == 0 { objs = append(objs, &objFaces{name: "default"}) } return objs[len(objs)-1] } sc := bufio.NewScanner(r) sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024) lineNo := 0 for sc.Scan() { lineNo++ line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } fields := strings.Fields(line) switch fields[0] { case "v": if len(fields) < 4 { return nil, fmt.Errorf("obj line %d: vertex needs x y z", lineNo) } var v Vec3 var err error if v.X, err = strconv.ParseFloat(fields[1], 64); err == nil { if v.Y, err = strconv.ParseFloat(fields[2], 64); err == nil { v.Z, err = strconv.ParseFloat(fields[3], 64) } } if err != nil { return nil, fmt.Errorf("obj line %d: bad vertex: %v", lineNo, err) } verts = append(verts, v) case "o", "g": name := "unnamed" if len(fields) > 1 { name = strings.Join(fields[1:], " ") } // only open a new object if the current one has faces if len(objs) > 0 && len(objs[len(objs)-1].tris) == 0 { objs[len(objs)-1].name = name } else { objs = append(objs, &objFaces{name: name}) } case "f": if len(fields) < 4 { return nil, fmt.Errorf("obj line %d: face needs at least 3 vertices", lineNo) } idx := make([]int, 0, len(fields)-1) for _, f := range fields[1:] { // "v", "v/vt", "v//vn", "v/vt/vn" — we only need v vs := strings.SplitN(f, "/", 2)[0] i, err := strconv.Atoi(vs) if err != nil { return nil, fmt.Errorf("obj line %d: bad face index %q", lineNo, f) } if i < 0 { i = len(verts) + i // negative = relative to current count } else { i-- // obj is 1-based } if i < 0 || i >= len(verts) { return nil, fmt.Errorf("obj line %d: face index %q out of range (have %d vertices)", lineNo, f, len(verts)) } idx = append(idx, i) } o := current() for k := 1; k+1 < len(idx); k++ { // fan triangulation o.tris = append(o.tris, Triangle{idx[0], idx[k], idx[k+1]}) } } } if err := sc.Err(); err != nil { return nil, err } // Compact the global vertex list into per-mesh local lists. scene := &Scene{} for _, o := range objs { if len(o.tris) == 0 { continue } m := &Mesh{Name: o.name} remap := map[int]int{} for _, t := range o.tris { var lt Triangle for k, gi := range t { li, ok := remap[gi] if !ok { li = len(m.Verts) m.Verts = append(m.Verts, verts[gi]) remap[gi] = li } lt[k] = li } m.Tris = append(m.Tris, lt) } scene.Meshes = append(scene.Meshes, m) } if len(scene.Meshes) == 0 { return nil, fmt.Errorf("obj contains no faces") } return scene, nil } // WriteOBJ writes the scene as OBJ, one "o" object per mesh. func WriteOBJ(w io.Writer, s *Scene) error { bw := bufio.NewWriter(w) fmt.Fprintln(bw, "# exported by mesht (agent-tools)") offset := 1 // obj indices are global and 1-based for _, m := range s.Meshes { fmt.Fprintf(bw, "o %s\n", m.Name) for _, v := range m.Verts { fmt.Fprintf(bw, "v %g %g %g\n", v.X, v.Y, v.Z) } for _, t := range m.Tris { fmt.Fprintf(bw, "f %d %d %d\n", t[0]+offset, t[1]+offset, t[2]+offset) } offset += len(m.Verts) } return bw.Flush() } // ReadFile loads a scene, picking the format from the file extension // (.obj, .stl). func ReadFile(path string) (*Scene, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() switch ext(path) { case "obj": s, err := ReadOBJ(f) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } return s, nil case "stl": s, err := ReadSTL(f) if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } return s, nil } return nil, fmt.Errorf("%s: unsupported format (use .obj or .stl)", path) } // WriteFile saves a scene, picking the format from the file extension. // asciiSTL selects text STL instead of the default binary. func WriteFile(path string, s *Scene, asciiSTL bool) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() switch ext(path) { case "obj": err = WriteOBJ(f, s) case "stl": if asciiSTL { err = WriteSTLAscii(f, s) } else { err = WriteSTLBinary(f, s) } default: err = fmt.Errorf("unsupported output format (use .obj or .stl)") } if err != nil { return fmt.Errorf("%s: %w", path, err) } return f.Close() } func ext(path string) string { i := strings.LastIndex(path, ".") if i < 0 { return "" } return strings.ToLower(path[i+1:]) }