Blender V4.3
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
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
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 str_len = str_len * 2 + 64;
207 str = static_cast<char *>(PyMem_MALLOC(sizeof(*str) * size_t(str_len)));
208 BLI_strncpy(str, inpt, size_t(str_len));
209
210 BKE_unit_replace_string(str, int(str_len), uref, scale, usys, ucat);
211
212 if (!PyC_RunString_AsNumber(nullptr, str, "<bpy_units_api>", &result)) {
213 if (PyErr_Occurred()) {
214 PyErr_Print();
215 PyErr_Clear();
216 }
217
218 PyErr_Format(
219 PyExc_ValueError, "'%.200s' (converted as '%s') could not be evaluated.", inpt, str);
220 ret = nullptr;
221 }
222 else {
223 ret = PyFloat_FromDouble(result);
224 }
225
226 PyMem_FREE(str);
227 return ret;
228}
229
231 /* Wrap. */
232 bpyunits_to_string_doc,
233 ".. method:: to_string(unit_system, unit_category, value, precision=3, "
234 "split_unit=False, compatible_unit=False)\n"
235 "\n"
236 " Convert a given input float value into a string with units.\n"
237 "\n"
238 " :arg unit_system: The unit system, from :attr:`bpy.utils.units.systems`.\n"
239 " :type unit_system: str\n"
240 " :arg unit_category: The category of data we are converting (length, area, "
241 "rotation, etc.),\n"
242 " from :attr:`bpy.utils.units.categories`.\n"
243 " :type unit_category: str\n"
244 " :arg value: The value to convert to a string.\n"
245 " :type value: float\n"
246 " :arg precision: Number of digits after the comma.\n"
247 " :type precision: int\n"
248 " :arg split_unit: Whether to use several units if needed (1m1cm), or always only "
249 "one (1.01m).\n"
250 " :type split_unit: bool\n"
251 " :arg compatible_unit: Whether to use keyboard-friendly units (1m2) or nicer "
252 "utf-8 ones (1m²).\n"
253 " :type compatible_unit: bool\n"
254 " :return: The converted string.\n"
255 " :rtype: str\n"
256 " :raises ValueError: if conversion fails to generate a valid Python string.\n");
257static PyObject *bpyunits_to_string(PyObject * /*self*/, PyObject *args, PyObject *kw)
258{
259 char *usys_str = nullptr, *ucat_str = nullptr;
260 double value = 0.0;
261 int precision = 3;
262 bool split_unit = false, compatible_unit = false;
263
264 int usys, ucat;
265
266 static const char *_keywords[] = {
267 "unit_system",
268 "unit_category",
269 "value",
270 "precision",
271 "split_unit",
272 "compatible_unit",
273 nullptr,
274 };
275 static _PyArg_Parser _parser = {
277 "s" /* `unit_system` */
278 "s" /* `unit_category` */
279 "d" /* `value` */
280 "|$" /* Optional keyword only arguments. */
281 "i" /* `precision` */
282 "O&" /* `split_unit` */
283 "O&" /* `compatible_unit` */
284 ":to_string",
285 _keywords,
286 nullptr,
287 };
288 if (!_PyArg_ParseTupleAndKeywordsFast(args,
289 kw,
290 &_parser,
291 &usys_str,
292 &ucat_str,
293 &value,
294 &precision,
296 &split_unit,
298 &compatible_unit))
299 {
300 return nullptr;
301 }
302
303 if (!bpyunits_validate(usys_str, ucat_str, &usys, &ucat)) {
304 return nullptr;
305 }
306
307 {
308 /* Maximum expected length of string result:
309 * - Number itself: precision + decimal dot + up to four 'above dot' digits.
310 * - Unit: up to ten chars
311 * (six currently, let's be conservative, also because we use some utf8 chars).
312 * This can be repeated twice (e.g. 1m20cm), and we add ten more spare chars
313 * (spaces, trailing '\0'...).
314 * So in practice, 64 should be more than enough.
315 */
316 char buf1[64], buf2[64];
317 const char *str;
318 PyObject *result;
319
321 buf1, sizeof(buf1), value, precision, usys, ucat, bool(split_unit), false);
322
323 if (compatible_unit) {
324 BKE_unit_name_to_alt(buf2, sizeof(buf2), buf1, usys, ucat);
325 str = buf2;
326 }
327 else {
328 str = buf1;
329 }
330
331 result = PyUnicode_FromString(str);
332
333 return result;
334 }
335}
336
337#if (defined(__GNUC__) && !defined(__clang__))
338# pragma GCC diagnostic push
339# pragma GCC diagnostic ignored "-Wcast-function-type"
340#endif
341
342static PyMethodDef bpyunits_methods[] = {
343 {"to_value",
344 (PyCFunction)bpyunits_to_value,
345 METH_VARARGS | METH_KEYWORDS,
346 bpyunits_to_value_doc},
347 {"to_string",
348 (PyCFunction)bpyunits_to_string,
349 METH_VARARGS | METH_KEYWORDS,
350 bpyunits_to_string_doc},
351 {nullptr, nullptr, 0, nullptr},
352};
353
354#if (defined(__GNUC__) && !defined(__clang__))
355# pragma GCC diagnostic pop
356#endif
357
359 /* Wrap. */
360 bpyunits_doc,
361 "This module contains some data/methods regarding units handling.");
362
363static PyModuleDef bpyunits_module = {
364 /*m_base*/ PyModuleDef_HEAD_INIT,
365 /*m_name*/ "bpy.utils.units",
366 /*m_doc*/ bpyunits_doc,
367 /*m_size*/ -1, /* multiple "initialization" just copies the module dict. */
368 /*m_methods*/ bpyunits_methods,
369 /*m_slots*/ nullptr,
370 /*m_traverse*/ nullptr,
371 /*m_clear*/ nullptr,
372 /*m_free*/ nullptr,
373};
374
376{
377 PyObject *submodule, *item;
378
379 submodule = PyModule_Create(&bpyunits_module);
380 PyDict_SetItemString(PyImport_GetModuleDict(), bpyunits_module.m_name, submodule);
381
382 /* Finalize our unit systems and types structseq definitions! */
383
384 /* bpy.utils.units.system */
387 PyModule_AddObject(submodule, "systems", item); /* steals ref */
388
389 /* bpy.utils.units.categories */
392 PyModule_AddObject(submodule, "categories", item); /* steals ref */
393
394 return submodule;
395}
void BKE_unit_name_to_alt(char *str, int str_maxncpy, const char *orig_str, int system, int type)
Definition unit.cc:2405
bool BKE_unit_is_valid(int system, int type)
Definition unit.cc:2472
@ B_UNIT_TYPE_TOT
Definition BKE_unit.hh:122
bool BKE_unit_replace_string(char *str, int str_maxncpy, const char *str_prev, double scale_pref, int system, int type)
Definition unit.cc:2315
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:1863
#define BLI_STATIC_ASSERT(a, msg)
Definition BLI_assert.h:87
#define BLI_assert(a)
Definition BLI_assert.h:50
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)
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)
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 PyObject * bpyunits_to_value(PyObject *, PyObject *args, PyObject *kw)
static PyStructSequence_Desc bpyunits_systems_desc
static PyTypeObject BPyUnitsSystemsType
#define str(s)
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