Coverage for trimesh/units.py: 91%

45 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-24 04:40 +0000

1""" 

2units.py 

3-------------- 

4Deal with physical unit systems (i.e. inches, mm) 

5 

6Very basic conversions, and no requirement for 

7sympy.physics.units or pint. 

8""" 

9 

10from . import resources 

11from .constants import log 

12from .parent import Geometry 

13 

14# scaling factors from various unit systems to inches 

15_lookup = resources.get_json("units_to_inches.json") 

16 

17 

18def unit_conversion(current: str, desired: str) -> float: 

19 """ 

20 Calculate the conversion from one set of units to another. 

21 

22 Parameters 

23 --------- 

24 current : str 

25 Unit system values are in now (eg 'millimeters') 

26 desired : str 

27 Unit system we'd like values in (eg 'inches') 

28 

29 Returns 

30 --------- 

31 conversion : float 

32 Number to multiply by to put values into desired units 

33 """ 

34 # convert to common system then return ratio between current and desired 

35 return to_inch(current.strip().lower()) / to_inch(desired.strip().lower()) 

36 

37 

38def keys() -> set: 

39 """ 

40 Return a set containing all currently valid units. 

41 

42 Returns 

43 -------- 

44 keys 

45 All units with conversions i.e. {'in', 'm', ...} 

46 """ 

47 return set(_lookup.keys()) 

48 

49 

50def to_inch(unit: str) -> float: 

51 """ 

52 Calculate the conversion to an arbitrary common unit. 

53 

54 Parameters 

55 ------------ 

56 unit 

57 Either a key in `units_to_inches.json` or in the simple 

58 `{float} * {str}` form, i.e. "1.2 * meters". We don't 

59 support arbitrary `eval` of any math string 

60 

61 Returns 

62 ---------- 

63 conversion 

64 Factor to multiply by to get to an `inch` system. 

65 """ 

66 # see if the units are just in our lookup table 

67 lookup = _lookup.get(unit.strip().lower(), None) 

68 if lookup is not None: 

69 return lookup 

70 

71 try: 

72 # otherwise check to see if they are in the factor * unit form 

73 value, key = unit.split("*") 

74 return _lookup[key.strip()] * float(value) 

75 except BaseException as E: 

76 # add a helpful error message 

77 message = ( 

78 f'arbitrary units must be in the form "1.21 * meters", not "{unit}" ({E})' 

79 ) 

80 

81 raise ValueError(message) 

82 

83 

84def units_from_metadata(obj: Geometry, guess: bool = True) -> str: 

85 """ 

86 Try to extract hints from metadata and if that fails 

87 guess based on the object scale. 

88 

89 

90 Parameters 

91 ------------ 

92 obj 

93 A geometry object. 

94 guess 

95 If metadata doesn't have units make a "best guess" 

96 

97 Returns 

98 ------------ 

99 units 

100 A guess of what the units might be 

101 """ 

102 

103 hints = [obj.metadata.get("name", None)] 

104 if obj.source is not None: 

105 hints.append(obj.source.file_name) 

106 

107 # try to guess from metadata 

108 for hint in hints: 

109 if hint is None: 

110 continue 

111 hint = hint.lower().replace("units", " ").replace("unit", " ") 

112 # replace all delimiter options with white space 

113 for delim in "_-.": 

114 hint = hint.replace(delim, " ") 

115 hint = "".join(c for c in hint if c not in "0123456789") 

116 # loop through each hint 

117 for h in hint.strip().split(): 

118 # if the hint is a valid unit return it 

119 if h in _lookup: 

120 return h 

121 

122 if not guess: 

123 raise ValueError("No units and not allowed to guess!") 

124 

125 # we made it to the wild ass guess section 

126 # if the scale is larger than 100 mystery units 

127 # declare the model to be millimeters, otherwise inches 

128 log.debug("no units: guessing from scale") 

129 if float(obj.scale) > 100.0: 

130 return "millimeters" 

131 else: 

132 return "inches" 

133 

134 

135def _convert_units(obj: Geometry, desired: str, guess=False) -> None: 

136 """ 

137 Given an object with scale and units try to scale 

138 to different units via the object's `apply_scale`. 

139 

140 Parameters 

141 --------- 

142 obj : object 

143 With apply_scale method (i.e. Trimesh, Path2D, etc) 

144 desired : str 

145 Units desired (eg 'inches') 

146 guess: bool 

147 Whether we are allowed to guess the units 

148 if they are not specified. 

149 """ 

150 if obj.units is None: 

151 # try to extract units from metadata 

152 # if nothing specified in metadata and not allowed 

153 # to guess will raise a ValueError 

154 obj.units = units_from_metadata(obj, guess=guess) 

155 

156 log.debug("converting units from %s to %s", obj.units, desired) 

157 # float, conversion factor 

158 conversion = unit_conversion(obj.units, desired) 

159 

160 # apply scale uses transforms which preserve 

161 # cached properties rather than just multiplying vertices 

162 obj.apply_scale(conversion) 

163 # units are now desired units 

164 obj.units = desired