Coverage for trimesh/exchange/off.py: 97%

32 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-08-31 18:21 +0000

1import re 

2 

3import numpy as np 

4 

5from ..geometry import triangulate_quads 

6from ..util import array_to_string, comment_strip, decode_text 

7 

8 

9def load_off(file_obj, **kwargs) -> dict: 

10 """ 

11 Load an OFF file into the kwargs for a Trimesh constructor. 

12 

13 Parameters 

14 ---------- 

15 file_obj : file object 

16 Contains an OFF file 

17 

18 Returns 

19 ---------- 

20 loaded : dict 

21 kwargs for Trimesh constructor 

22 """ 

23 text = file_obj.read() 

24 # will magically survive weird encoding sometimes 

25 # comment strip will handle all cases of commenting 

26 text = comment_strip(decode_text(text)).strip() 

27 

28 # split the first key 

29 _, header, raw = re.split("(COFF|OFF)", text, maxsplit=1) 

30 if header.upper() not in ["OFF", "COFF"]: 

31 raise NameError(f"Not an OFF file! Header was: `{header}`") 

32 

33 # split into lines and remove whitespace 

34 splits = [i.strip() for i in str.splitlines(str(raw))] 

35 # remove empty lines 

36 splits = [i for i in splits if len(i) > 0] 

37 

38 # the first non-comment line should be the counts 

39 if not splits: 

40 raise ValueError("OFF file is missing the vertex/face count line") 

41 header = np.array(splits[0].split(), dtype=np.int64) 

42 if header.size < 2: 

43 raise ValueError("OFF file has a malformed vertex/face count line") 

44 vertex_count, face_count = header[:2] 

45 

46 vertices = np.array( 

47 [i.split()[:3] for i in splits[1 : vertex_count + 1]], dtype=np.float64 

48 ) 

49 

50 # will fail if incorrect number of vertices loaded 

51 vertices = vertices.reshape((vertex_count, 3)) 

52 

53 # get lines with face data 

54 faces = [i.split() for i in splits[vertex_count + 1 : vertex_count + face_count + 1]] 

55 # the first value is count 

56 faces = [line[1 : int(line[0]) + 1] for line in faces] 

57 

58 faces = triangulate_quads(faces) 

59 # save data as kwargs for a trimesh.Trimesh 

60 kwargs = {"vertices": vertices, "faces": faces} 

61 

62 return kwargs 

63 

64 

65def export_off(mesh, digits=10) -> str: 

66 """ 

67 Export a mesh as an OFF file, a simple text format 

68 

69 Parameters 

70 ----------- 

71 mesh : trimesh.Trimesh 

72 Geometry to export 

73 digits : int 

74 Number of digits to include on floats 

75 

76 Returns 

77 ----------- 

78 export : str 

79 OFF format output 

80 """ 

81 # make sure specified digits is an int 

82 digits = int(digits) 

83 # prepend a 3 (face count) to each face 

84 faces_stacked = np.column_stack((np.ones(len(mesh.faces)) * 3, mesh.faces)).astype( 

85 np.int64 

86 ) 

87 # the header is vertex count, face count, another number 

88 export = "\n".join( 

89 [ 

90 "OFF", 

91 str(len(mesh.vertices)) + " " + str(len(mesh.faces)) + " 0", 

92 array_to_string(mesh.vertices, col_delim=" ", row_delim="\n", digits=digits), 

93 array_to_string(faces_stacked, col_delim=" ", row_delim="\n"), 

94 "", 

95 ] 

96 ) 

97 

98 return export 

99 

100 

101_off_loaders = {"off": load_off} 

102_off_exporters = {"off": export_off}