Blender V5.0
bpy_utils_units.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2023 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
11
12/* Future-proof, See https://docs.python.org/3/c-api/arg.html#strings-and-buffers */
13#define PY_SSIZE_T_CLEAN
14
15#include <Python.h>
16#include <structmember.h>
17
18#include "BLI_string.h"
19#include "BLI_utildefines.h"
20
21#include "bpy_utils_units.hh"
22
24#include "../generic/python_compat.hh" /* IWYU pragma: keep. */
25
26#include "BKE_unit.hh"
27
28/***** C-defined systems and types *****/
29
30static PyTypeObject BPyUnitsSystemsType;
31static PyTypeObject BPyUnitsCategoriesType;
32
33/* XXX: Maybe better as `extern` of `BKE_unit.hh` ? */
34static const char *bpyunits_usystem_items[] = {
35 "NONE",
36 "METRIC",
37 "IMPERIAL",
38 nullptr,
39};
40
41static const char *bpyunits_ucategories_items[] = {
42 "NONE",
43 "LENGTH",
44 "AREA",
45 "VOLUME",
46 "MASS",
47 "ROTATION",
48 "TIME",
49 "TIME_ABSOLUTE",
50 "VELOCITY",
51 "ACCELERATION",
52 "CAMERA",
53 "POWER",
54 "TEMPERATURE",
55 "WAVELENGTH",
56 "COLOR_TEMPERATURE",
57 "FREQUENCY",
58 nullptr,
59};
60
63 "`bpyunits_ucategories_items` should match `B_UNIT_` enum items in `BKE_units.h`")
64
65
70static PyStructSequence_Field bpyunits_systems_fields[ARRAY_SIZE(bpyunits_usystem_items)];
72
73static PyStructSequence_Desc bpyunits_systems_desc = {
74 /*name*/ "bpy.utils.units.systems",
75 /*doc*/ "This named tuple contains all predefined unit systems",
76 /*fields*/ bpyunits_systems_fields,
77 /*n_in_sequence*/ ARRAY_SIZE(bpyunits_systems_fields) - 1,
78};
79static PyStructSequence_Desc bpyunits_categories_desc = {
80 /*name*/ "bpy.utils.units.categories",
81 /*doc*/ "This named tuple contains all predefined unit names",
83 /*n_in_sequence*/ ARRAY_SIZE(bpyunits_categories_fields) - 1,
84};
85
89static PyObject *py_structseq_from_strings(PyTypeObject *py_type,
90 PyStructSequence_Desc *py_sseq_desc,
91 const char **str_items)
92{
93 PyObject *py_struct_seq;
94 int pos = 0;
95
96 const char **str_iter;
97 PyStructSequence_Field *desc;
98
99 /* Initialize array. */
100 /* We really populate the contexts' fields here! */
101 for (str_iter = str_items, desc = py_sseq_desc->fields; *str_iter; str_iter++, desc++) {
102 desc->name = (char *)*str_iter;
103 desc->doc = nullptr;
104 }
105 /* end sentinel */
106 desc->name = desc->doc = nullptr;
107
108 PyStructSequence_InitType(py_type, py_sseq_desc);
109
110 /* Initialize the Python type. */
111 py_struct_seq = PyStructSequence_New(py_type);
112 BLI_assert(py_struct_seq != nullptr);
113
114 for (str_iter = str_items; *str_iter; str_iter++) {
115 PyStructSequence_SET_ITEM(py_struct_seq, pos++, PyUnicode_FromString(*str_iter));
116 }
117
118 return py_struct_seq;
119}
120
121static bool bpyunits_validate(const char *usys_str, const char *ucat_str, int *r_usys, int *r_ucat)
122{
124 if (*r_usys < 0) {
125 PyErr_Format(PyExc_ValueError, "Unknown unit system specified: %.200s.", usys_str);
126 return false;
127 }
128
130 if (*r_ucat < 0) {
131 PyErr_Format(PyExc_ValueError, "Unknown unit category specified: %.200s.", ucat_str);
132 return false;
133 }
134
135 if (!BKE_unit_is_valid(*r_usys, *r_ucat)) {
136 PyErr_Format(PyExc_ValueError,
137 "%.200s / %.200s unit system/category combination is not valid.",
138 usys_str,
139 ucat_str);
140 return false;
141 }
142
143 return true;
144}
145
147 /* Wrap. */
148 bpyunits_to_value_doc,
149 ".. method:: to_value(unit_system, unit_category, str_input, *, str_ref_unit=None)\n"
150 "\n"
151 " Convert a given input string into a float value.\n"
152 "\n"
153 " :arg unit_system: The unit system, from :attr:`bpy.utils.units.systems`.\n"
154 " :type unit_system: str\n"
155 " :arg unit_category: The category of data we are converting (length, area, rotation, "
156 "etc.),\n"
157 " from :attr:`bpy.utils.units.categories`.\n"
158 " :type unit_category: str\n"
159 " :arg str_input: The string to convert to a float value.\n"
160 " :type str_input: str\n"
161 " :arg str_ref_unit: A reference string from which to extract a default unit, if none is "
162 "found in ``str_input``.\n"
163 " :type str_ref_unit: str | None\n"
164 " :return: The converted/interpreted value.\n"
165 " :rtype: float\n"
166 " :raises ValueError: if conversion fails to generate a valid Python float value.\n");
167static PyObject *bpyunits_to_value(PyObject * /*self*/, PyObject *args, PyObject *kw)
168{
169 char *usys_str = nullptr, *ucat_str = nullptr, *inpt = nullptr, *uref = nullptr;
170 const float scale = 1.0f;
171
172 char *str;
173 Py_ssize_t str_len;
174 double result;
175 int usys, ucat;
176 PyObject *ret;
177
178 static const char *_keywords[] = {
179 "unit_system",
180 "unit_category",
181 "str_input",
182 "str_ref_unit",
183 nullptr,
184 };
185 static _PyArg_Parser _parser = {
187 "s" /* `unit_system` */
188 "s" /* `unit_category` */
189 "s#" /* `str_input` */
190 "|$" /* Optional keyword only arguments. */
191 "z" /* `str_ref_unit` */
192 ":to_value",
193 _keywords,
194 nullptr,
195 };
196 if (!_PyArg_ParseTupleAndKeywordsFast(
197 args, kw, &_parser, &usys_str, &ucat_str, &inpt, &str_len, &uref))
198 {
199 return nullptr;
200 }
201
202 if (!bpyunits_validate(usys_str, ucat_str, &usys, &ucat)) {
203 return nullptr;
204 }
205
206 const size_t str_maxncpy = str_len * 2 + 64;
207 str = static_cast<char *>(PyMem_MALLOC(sizeof(*str) * str_maxncpy));
208 BLI_strncpy(str, inpt, str_maxncpy);
209
210 BKE_unit_replace_string(str, int(str_maxncpy), uref, scale, usys, ucat);
211
212 if (!PyC_RunString_AsNumber(nullptr, str, "<bpy_units_api>", &result)) {
213 if (PyErr_Occurred()) {
214 PyErr_Print();
215 }
216
217 PyErr_Format(
218 PyExc_ValueError, "'%.200s' (converted as '%s') could not be evaluated.", inpt, str);
219 ret = nullptr;
220 }
221 else {
222 ret = PyFloat_FromDouble(result);
223 }
224
225 PyMem_FREE(str);
226 return ret;
227}
228
230 /* Wrap. */
231 bpyunits_to_string_doc,
232 ".. method:: to_string(unit_system, unit_category, value, *, precision=3, "
233 "split_unit=False, compatible_unit=False)\n"
234 "\n"
235 " Convert a given input float value into a string with units.\n"
236 "\n"
237 " :arg unit_system: The unit system, from :attr:`bpy.utils.units.systems`.\n"
238 " :type unit_system: str\n"
239 " :arg unit_category: The category of data we are converting (length, area, "
240 "rotation, etc.),\n"
241 " from :attr:`bpy.utils.units.categories`.\n"
242 " :type unit_category: str\n"
243 " :arg value: The value to convert to a string.\n"
244 " :type value: float\n"
245 " :arg precision: Number of digits after the comma.\n"
246 " :type precision: int\n"
247 " :arg split_unit: Whether to use several units if needed (1m1cm), or always only "
248 "one (1.01m).\n"
249 " :type split_unit: bool\n"
250 " :arg compatible_unit: Whether to use keyboard-friendly units (1m2) or nicer "
251 "UTF8 ones (1m²).\n"
252 " :type compatible_unit: bool\n"
253 " :return: The converted string.\n"
254 " :rtype: str\n"
255 " :raises ValueError: if conversion fails to generate a valid Python string.\n");
256static PyObject *bpyunits_to_string(PyObject * /*self*/, PyObject *args, PyObject *kw)
257{
258 char *usys_str = nullptr, *ucat_str = nullptr;
259 double value = 0.0;
260 int precision = 3;
261 bool split_unit = false, compatible_unit = false;
262
263 int usys, ucat;
264
265 static const char *_keywords[] = {
266 "unit_system",
267 "unit_category",
268 "value",
269 "precision",
270 "split_unit",
271 "compatible_unit",
272 nullptr,
273 };
274 static _PyArg_Parser _parser = {
276 "s" /* `unit_system` */
277 "s" /* `unit_category` */
278 "d" /* `value` */
279 "|$" /* Optional keyword only arguments. */
280 "i" /* `precision` */
281 "O&" /* `split_unit` */
282 "O&" /* `compatible_unit` */
283 ":to_string",
284 _keywords,
285 nullptr,
286 };
287 if (!_PyArg_ParseTupleAndKeywordsFast(args,
288 kw,
289 &_parser,
290 &usys_str,
291 &ucat_str,
292 &value,
293 &precision,
295 &split_unit,
297 &compatible_unit))
298 {
299 return nullptr;
300 }
301
302 if (!bpyunits_validate(usys_str, ucat_str, &usys, &ucat)) {
303 return nullptr;
304 }
305
306 {
307 /* Maximum expected length of string result:
308 * - Number itself: precision + decimal dot + up to four 'above dot' digits.
309 * - Unit: up to ten chars
310 * (six currently, let's be conservative, also because we use some UTF8 chars).
311 * This can be repeated twice (e.g. `1m20cm`), and we add ten more spare chars
312 * (spaces, trailing '\0'...).
313 * So in practice, 64 should be more than enough.
314 */
315 char buf1[64], buf2[64];
316 const char *str;
317 PyObject *result;
318
320 buf1, sizeof(buf1), value, precision, usys, ucat, split_unit, false);
321
322 if (compatible_unit) {
323 BKE_unit_name_to_alt(buf2, sizeof(buf2), buf1, usys, ucat);
324 str = buf2;
325 }
326 else {
327 str = buf1;
328 }
329
330 result = PyUnicode_FromString(str);
331
332 return result;
333 }
334}
335
336#ifdef __GNUC__
337# ifdef __clang__
338# pragma clang diagnostic push
339# pragma clang diagnostic ignored "-Wcast-function-type"
340# else
341# pragma GCC diagnostic push
342# pragma GCC diagnostic ignored "-Wcast-function-type"
343# endif
344#endif
345
346static PyMethodDef bpyunits_methods[] = {
347 {"to_value",
348 (PyCFunction)bpyunits_to_value,
349 METH_VARARGS | METH_KEYWORDS,
350 bpyunits_to_value_doc},
351 {"to_string",
352 (PyCFunction)bpyunits_to_string,
353 METH_VARARGS | METH_KEYWORDS,
354 bpyunits_to_string_doc},
355 {nullptr, nullptr, 0, nullptr},
356};
357
358#ifdef __GNUC__
359# ifdef __clang__
360# pragma clang diagnostic pop
361# else
362# pragma GCC diagnostic pop
363# endif
364#endif
365
367 /* Wrap. */
368 bpyunits_doc,
369 "This module contains some data/methods regarding units handling.\n");
370static PyModuleDef bpyunits_module = {
371 /*m_base*/ PyModuleDef_HEAD_INIT,
372 /*m_name*/ "bpy.utils.units",
373 /*m_doc*/ bpyunits_doc,
374 /*m_size*/ -1, /* multiple "initialization" just copies the module dict. */
375 /*m_methods*/ bpyunits_methods,
376 /*m_slots*/ nullptr,
377 /*m_traverse*/ nullptr,
378 /*m_clear*/ nullptr,
379 /*m_free*/ nullptr,
380};
381
383{
384 PyObject *submodule, *item;
385
386 submodule = PyModule_Create(&bpyunits_module);
387 PyDict_SetItemString(PyImport_GetModuleDict(), bpyunits_module.m_name, submodule);
388
389 /* Finalize our unit systems and types structseq definitions! */
390
391 /* bpy.utils.units.system */
394 PyModule_AddObject(submodule, "systems", item); /* steals ref */
395
396 /* bpy.utils.units.categories */
399 PyModule_AddObject(submodule, "categories", item); /* steals ref */
400
401 return submodule;
402}
void BKE_unit_name_to_alt(char *str, int str_maxncpy, const char *orig_str, int system, int type)
Definition unit.cc:2447
@ B_UNIT_TYPE_TOT
Definition BKE_unit.hh:152
bool BKE_unit_is_valid(int system, int type)
Definition unit.cc:2514
bool BKE_unit_replace_string(char *str, int str_maxncpy, const char *str_prev, double scale_pref, int system, int type)
Definition unit.cc:2357
size_t BKE_unit_value_as_string_adaptive(char *str, int str_maxncpy, double value, int prec, int system, int type, bool split, bool pad)
Definition unit.cc:1869
#define BLI_STATIC_ASSERT(a, msg)
Definition BLI_assert.h:83
#define BLI_assert(a)
Definition BLI_assert.h:46
int BLI_str_index_in_array(const char *__restrict str, const char **__restrict str_array) ATTR_WARN_UNUSED_RESULT ATTR_NONNULL(1
char * BLI_strncpy(char *__restrict dst, const char *__restrict src, size_t dst_maxncpy) ATTR_NONNULL(1
#define ARRAY_SIZE(arr)
static const char * bpyunits_usystem_items[]
PyObject * BPY_utils_units()
static PyObject * bpyunits_to_string(PyObject *, PyObject *args, PyObject *kw)
static PyObject * py_structseq_from_strings(PyTypeObject *py_type, PyStructSequence_Desc *py_sseq_desc, const char **str_items)
PyDoc_STRVAR(bpyunits_to_value_doc, ".. method:: to_value(unit_system, unit_category, str_input, *, str_ref_unit=None)\n" "\n" " Convert a given input string into a float value.\n" "\n" " :arg unit_system: The unit system, from :attr:`bpy.utils.units.systems`.\n" " :type unit_system: str\n" " :arg unit_category: The category of data we are converting (length, area, rotation, " "etc.),\n" " from :attr:`bpy.utils.units.categories`.\n" " :type unit_category: str\n" " :arg str_input: The string to convert to a float value.\n" " :type str_input: str\n" " :arg str_ref_unit: A reference string from which to extract a default unit, if none is " "found in ``str_input``.\n" " :type str_ref_unit: str | None\n" " :return: The converted/interpreted value.\n" " :rtype: float\n" " :raises ValueError: if conversion fails to generate a valid Python float value.\n")
static PyModuleDef bpyunits_module
static PyStructSequence_Desc bpyunits_categories_desc
static PyMethodDef bpyunits_methods[]
static PyStructSequence_Field bpyunits_categories_fields[ARRAY_SIZE(bpyunits_ucategories_items)]
static const char * bpyunits_ucategories_items[]
static PyTypeObject BPyUnitsCategoriesType
static bool bpyunits_validate(const char *usys_str, const char *ucat_str, int *r_usys, int *r_ucat)
static PyObject * bpyunits_to_value(PyObject *, PyObject *args, PyObject *kw)
static PyStructSequence_Desc bpyunits_systems_desc
static PyTypeObject BPyUnitsSystemsType
#define str(s)
uint pos
bool PyC_RunString_AsNumber(const char *imports[], const char *expr, const char *filename, double *r_value)
int PyC_ParseBool(PyObject *o, void *p)
header-only compatibility defines.
#define PY_ARG_PARSER_HEAD_COMPAT()
return ret