1 /* SPDX-FileCopyrightText: 2023 Blender Authors
3 * SPDX-License-Identifier: GPL-2.0-or-later */
6 * \ingroup pythonintern
8 * This file defines a singleton py object accessed via 'bpy.utils.units',
9 * which exposes various data and functions useful in units handling.
12 /* Future-proof, See https://docs.python.org/3/c-api/arg.html#strings-and-buffers */
13 #define PY_SSIZE_T_CLEAN
16 #include <structmember.h>
18 #include "BLI_string.h"
19 #include "BLI_utildefines.h"
21 #include "bpy_utils_units.hh"
23 #include "../generic/py_capi_utils.hh"
24 #include "../generic/python_compat.hh"
26 #include "BKE_unit.hh"
28 /***** C-defined systems and types *****/
30 static PyTypeObject BPyUnitsSystemsType
;
31 static PyTypeObject BPyUnitsCategoriesType
;
33 /* XXX: Maybe better as `extern` of `BKE_unit.hh` ? */
34 static const char *bpyunits_usystem_items
[] = {
41 static const char *bpyunits_ucategories_items
[] = {
62 ARRAY_SIZE(bpyunits_ucategories_items
) == B_UNIT_TYPE_TOT
+ 1,
63 "`bpyunits_ucategories_items` should match `B_UNIT_` enum items in `BKE_units.h`")
66 * These fields are just empty placeholders, actual values get set in initializations functions.
67 * This allows us to avoid many handwriting, and above all,
68 * to keep all systems/categories definition stuff in `BKE_unit.hh`.
70 static PyStructSequence_Field bpyunits_systems_fields
[ARRAY_SIZE(bpyunits_usystem_items
)];
71 static PyStructSequence_Field bpyunits_categories_fields
[ARRAY_SIZE(bpyunits_ucategories_items
)];
73 static 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,
79 static PyStructSequence_Desc bpyunits_categories_desc
= {
80 /*name*/ "bpy.utils.units.categories",
81 /*doc*/ "This named tuple contains all predefined unit names",
82 /*fields*/ bpyunits_categories_fields
,
83 /*n_in_sequence*/ ARRAY_SIZE(bpyunits_categories_fields
) - 1,
87 * Simple utility function to initialize #PyStructSequence_Desc
89 static PyObject
*py_structseq_from_strings(PyTypeObject
*py_type
,
90 PyStructSequence_Desc
*py_sseq_desc
,
91 const char **str_items
)
93 PyObject
*py_struct_seq
;
96 const char **str_iter
;
97 PyStructSequence_Field
*desc
;
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
;
106 desc
->name
= desc
->doc
= nullptr;
108 PyStructSequence_InitType(py_type
, py_sseq_desc
);
110 /* Initialize the Python type. */
111 py_struct_seq
= PyStructSequence_New(py_type
);
112 BLI_assert(py_struct_seq
!= nullptr);
114 for (str_iter
= str_items
; *str_iter
; str_iter
++) {
115 PyStructSequence_SET_ITEM(py_struct_seq
, pos
++, PyUnicode_FromString(*str_iter
));
118 return py_struct_seq
;
121 static bool bpyunits_validate(const char *usys_str
, const char *ucat_str
, int *r_usys
, int *r_ucat
)
123 *r_usys
= BLI_str_index_in_array(usys_str
, bpyunits_usystem_items
);
125 PyErr_Format(PyExc_ValueError
, "Unknown unit system specified: %.200s.", usys_str
);
129 *r_ucat
= BLI_str_index_in_array(ucat_str
, bpyunits_ucategories_items
);
131 PyErr_Format(PyExc_ValueError
, "Unknown unit category specified: %.200s.", ucat_str
);
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.",
148 bpyunits_to_value_doc
,
149 ".. method:: to_value(unit_system, unit_category, str_input, str_ref_unit=None)\n"
151 " Convert a given input string into a float value.\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, "
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"
166 " :raises ValueError: if conversion fails to generate a valid Python float value.\n");
167 static PyObject
*bpyunits_to_value(PyObject
* /*self*/, PyObject
*args
, PyObject
*kw
)
169 char *usys_str
= nullptr, *ucat_str
= nullptr, *inpt
= nullptr, *uref
= nullptr;
170 const float scale
= 1.0f
;
178 static const char *_keywords
[] = {
185 static _PyArg_Parser _parser
= {
186 PY_ARG_PARSER_HEAD_COMPAT()
187 "s" /* `unit_system` */
188 "s" /* `unit_category` */
189 "s#" /* `str_input` */
190 "|$" /* Optional keyword only arguments. */
191 "z" /* `str_ref_unit` */
196 if (!_PyArg_ParseTupleAndKeywordsFast(
197 args
, kw
, &_parser
, &usys_str
, &ucat_str
, &inpt
, &str_len
, &uref
))
202 if (!bpyunits_validate(usys_str
, ucat_str
, &usys
, &ucat
)) {
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
));
210 BKE_unit_replace_string(str
, int(str_len
), uref
, scale
, usys
, ucat
);
212 if (!PyC_RunString_AsNumber(nullptr, str
, "<bpy_units_api>", &result
)) {
213 if (PyErr_Occurred()) {
219 PyExc_ValueError
, "'%.200s' (converted as '%s') could not be evaluated.", inpt
, str
);
223 ret
= PyFloat_FromDouble(result
);
232 bpyunits_to_string_doc
,
233 ".. method:: to_string(unit_system, unit_category, value, precision=3, "
234 "split_unit=False, compatible_unit=False)\n"
236 " Convert a given input float value into a string with units.\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, "
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 "
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"
256 " :raises ValueError: if conversion fails to generate a valid Python string.\n");
257 static PyObject
*bpyunits_to_string(PyObject
* /*self*/, PyObject
*args
, PyObject
*kw
)
259 char *usys_str
= nullptr, *ucat_str
= nullptr;
262 bool split_unit
= false, compatible_unit
= false;
266 static const char *_keywords
[] = {
275 static _PyArg_Parser _parser
= {
276 PY_ARG_PARSER_HEAD_COMPAT()
277 "s" /* `unit_system` */
278 "s" /* `unit_category` */
280 "|$" /* Optional keyword only arguments. */
281 "i" /* `precision` */
282 "O&" /* `split_unit` */
283 "O&" /* `compatible_unit` */
288 if (!_PyArg_ParseTupleAndKeywordsFast(args
,
303 if (!bpyunits_validate(usys_str
, ucat_str
, &usys
, &ucat
)) {
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.
316 char buf1
[64], buf2
[64];
320 BKE_unit_value_as_string_adaptive(
321 buf1
, sizeof(buf1
), value
, precision
, usys
, ucat
, split_unit
, false);
323 if (compatible_unit
) {
324 BKE_unit_name_to_alt(buf2
, sizeof(buf2
), buf1
, usys
, ucat
);
331 result
= PyUnicode_FromString(str
);
337 #if (defined(__GNUC__) && !defined(__clang__))
338 # pragma GCC diagnostic push
339 # pragma GCC diagnostic ignored "-Wcast-function-type"
342 static PyMethodDef bpyunits_methods
[] = {
344 (PyCFunction
)bpyunits_to_value
,
345 METH_VARARGS
| METH_KEYWORDS
,
346 bpyunits_to_value_doc
},
348 (PyCFunction
)bpyunits_to_string
,
349 METH_VARARGS
| METH_KEYWORDS
,
350 bpyunits_to_string_doc
},
351 {nullptr, nullptr, 0, nullptr},
354 #if (defined(__GNUC__) && !defined(__clang__))
355 # pragma GCC diagnostic pop
361 "This module contains some data/methods regarding units handling.");
363 static 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
,
370 /*m_traverse*/ nullptr,
375 PyObject
*BPY_utils_units()
377 PyObject
*submodule
, *item
;
379 submodule
= PyModule_Create(&bpyunits_module
);
380 PyDict_SetItemString(PyImport_GetModuleDict(), bpyunits_module
.m_name
, submodule
);
382 /* Finalize our unit systems and types structseq definitions! */
384 /* bpy.utils.units.system */
385 item
= py_structseq_from_strings(
386 &BPyUnitsSystemsType
, &bpyunits_systems_desc
, bpyunits_usystem_items
);
387 PyModule_AddObject(submodule
, "systems", item
); /* steals ref */
389 /* bpy.utils.units.categories */
390 item
= py_structseq_from_strings(
391 &BPyUnitsCategoriesType
, &bpyunits_categories_desc
, bpyunits_ucategories_items
);
392 PyModule_AddObject(submodule
, "categories", item
); /* steals ref */