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

28 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +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 header = np.array(splits[0].split(), dtype=np.int64) 

40 vertex_count, face_count = header[:2] 

41 

42 vertices = np.array( 

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

44 ) 

45 

46 # will fail if incorrect number of vertices loaded 

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

48 

49 # get lines with face data 

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

51 # the first value is count 

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

53 

54 faces = triangulate_quads(faces) 

55 # save data as kwargs for a trimesh.Trimesh 

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

57 

58 return kwargs 

59 

60 

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

62 """ 

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

64 

65 Parameters 

66 ----------- 

67 mesh : trimesh.Trimesh 

68 Geometry to export 

69 digits : int 

70 Number of digits to include on floats 

71 

72 Returns 

73 ----------- 

74 export : str 

75 OFF format output 

76 """ 

77 # make sure specified digits is an int 

78 digits = int(digits) 

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

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

81 np.int64 

82 ) 

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

84 export = "\n".join( 

85 [ 

86 "OFF", 

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

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

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

90 "", 

91 ] 

92 ) 

93 

94 return export 

95 

96 

97_off_loaders = {"off": load_off} 

98_off_exporters = {"off": export_off}