Merge branch 'blender-v4.4-release'
[blender.git] / source / blender / python / intern / bpy_utils_units.cc
blobb7a8c9a1fec57de02a5a37312c6da9a7c960c4a6
1 /* SPDX-FileCopyrightText: 2023 Blender Authors
3 * SPDX-License-Identifier: GPL-2.0-or-later */
5 /** \file
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
15 #include <Python.h>
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[] = {
35 "NONE",
36 "METRIC",
37 "IMPERIAL",
38 nullptr,
41 static 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,
61 BLI_STATIC_ASSERT(
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`")
65 /**
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,
86 /**
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;
94 int pos = 0;
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;
103 desc->doc = nullptr;
105 /* end sentinel */
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);
124 if (*r_usys < 0) {
125 PyErr_Format(PyExc_ValueError, "Unknown unit system specified: %.200s.", usys_str);
126 return false;
129 *r_ucat = BLI_str_index_in_array(ucat_str, bpyunits_ucategories_items);
130 if (*r_ucat < 0) {
131 PyErr_Format(PyExc_ValueError, "Unknown unit category specified: %.200s.", ucat_str);
132 return false;
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;
143 return true;
146 PyDoc_STRVAR(
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");
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;
172 char *str;
173 Py_ssize_t str_len;
174 double result;
175 int usys, ucat;
176 PyObject *ret;
178 static const char *_keywords[] = {
179 "unit_system",
180 "unit_category",
181 "str_input",
182 "str_ref_unit",
183 nullptr,
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` */
192 ":to_value",
193 _keywords,
194 nullptr,
196 if (!_PyArg_ParseTupleAndKeywordsFast(
197 args, kw, &_parser, &usys_str, &ucat_str, &inpt, &str_len, &uref))
199 return nullptr;
202 if (!bpyunits_validate(usys_str, ucat_str, &usys, &ucat)) {
203 return nullptr;
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()) {
214 PyErr_Print();
215 PyErr_Clear();
218 PyErr_Format(
219 PyExc_ValueError, "'%.200s' (converted as '%s') could not be evaluated.", inpt, str);
220 ret = nullptr;
222 else {
223 ret = PyFloat_FromDouble(result);
226 PyMem_FREE(str);
227 return ret;
230 PyDoc_STRVAR(
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");
257 static PyObject *bpyunits_to_string(PyObject * /*self*/, PyObject *args, PyObject *kw)
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;
264 int usys, ucat;
266 static const char *_keywords[] = {
267 "unit_system",
268 "unit_category",
269 "value",
270 "precision",
271 "split_unit",
272 "compatible_unit",
273 nullptr,
275 static _PyArg_Parser _parser = {
276 PY_ARG_PARSER_HEAD_COMPAT()
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,
288 if (!_PyArg_ParseTupleAndKeywordsFast(args,
290 &_parser,
291 &usys_str,
292 &ucat_str,
293 &value,
294 &precision,
295 PyC_ParseBool,
296 &split_unit,
297 PyC_ParseBool,
298 &compatible_unit))
300 return nullptr;
303 if (!bpyunits_validate(usys_str, ucat_str, &usys, &ucat)) {
304 return nullptr;
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];
317 const char *str;
318 PyObject *result;
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);
325 str = buf2;
327 else {
328 str = buf1;
331 result = PyUnicode_FromString(str);
333 return result;
337 #if (defined(__GNUC__) && !defined(__clang__))
338 # pragma GCC diagnostic push
339 # pragma GCC diagnostic ignored "-Wcast-function-type"
340 #endif
342 static 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},
354 #if (defined(__GNUC__) && !defined(__clang__))
355 # pragma GCC diagnostic pop
356 #endif
358 PyDoc_STRVAR(
359 /* Wrap. */
360 bpyunits_doc,
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,
369 /*m_slots*/ nullptr,
370 /*m_traverse*/ nullptr,
371 /*m_clear*/ nullptr,
372 /*m_free*/ 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 */
394 return submodule;