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