Coverage for trimesh/units.py: 91%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-24 04:40 +0000
« 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)
6Very basic conversions, and no requirement for
7sympy.physics.units or pint.
8"""
10from . import resources
11from .constants import log
12from .parent import Geometry
14# scaling factors from various unit systems to inches
15_lookup = resources.get_json("units_to_inches.json")
18def unit_conversion(current: str, desired: str) -> float:
19 """
20 Calculate the conversion from one set of units to another.
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')
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())
38def keys() -> set:
39 """
40 Return a set containing all currently valid units.
42 Returns
43 --------
44 keys
45 All units with conversions i.e. {'in', 'm', ...}
46 """
47 return set(_lookup.keys())
50def to_inch(unit: str) -> float:
51 """
52 Calculate the conversion to an arbitrary common unit.
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
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
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 )
81 raise ValueError(message)
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.
90 Parameters
91 ------------
92 obj
93 A geometry object.
94 guess
95 If metadata doesn't have units make a "best guess"
97 Returns
98 ------------
99 units
100 A guess of what the units might be
101 """
103 hints = [obj.metadata.get("name", None)]
104 if obj.source is not None:
105 hints.append(obj.source.file_name)
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
122 if not guess:
123 raise ValueError("No units and not allowed to guess!")
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"
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`.
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)
156 log.debug("converting units from %s to %s", obj.units, desired)
157 # float, conversion factor
158 conversion = unit_conversion(obj.units, desired)
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