1 # SPDX-FileCopyrightText: 2021-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # -----------------------------------------------------------------------------
9 "actionconfig_export_as_data",
10 "actionconfig_import_from_data",
11 "actionconfig_init_from_data",
12 "actionmap_init_from_data",
13 "actionmap_item_init_from_data",
21 def round_float_32(f
):
22 from struct
import pack
, unpack
23 return unpack("f", pack("f", f
))[0]
27 f_round
= round_float_32(f
)
29 f_str_frac
= f_str
.partition(".")[2]
32 for i
in range(1, len(f_str_frac
)):
34 f_test_round
= round_float_32(f_test
)
35 if f_test_round
== f_round
:
36 return "%.*f" % (i
, f_test
)
40 def ami_args_as_data(ami
):
42 f
"\"type\": '{ami.type}'",
45 sup
= f
"\"user_paths\": ["
46 for user_path
in ami
.user_paths
:
47 sup
+= f
"'{user_path.path}', "
48 if len(ami
.user_paths
) > 0:
53 if ami
.type == 'FLOAT' or ami
.type == 'VECTOR2D':
54 s
.append(f
"\"op\": '{ami.op}'")
55 s
.append(f
"\"op_mode\": '{ami.op_mode}'")
56 s
.append(f
"\"bimanual\": '{ami.bimanual}'")
57 s
.append(f
"\"haptic_name\": '{ami.haptic_name}'")
58 s
.append(f
"\"haptic_match_user_paths\": '{ami.haptic_match_user_paths}'")
59 s
.append(f
"\"haptic_duration\": '{ami.haptic_duration}'")
60 s
.append(f
"\"haptic_frequency\": '{ami.haptic_frequency}'")
61 s
.append(f
"\"haptic_amplitude\": '{ami.haptic_amplitude}'")
62 s
.append(f
"\"haptic_mode\": '{ami.haptic_mode}'")
63 elif ami
.type == 'POSE':
64 s
.append(f
"\"pose_is_controller_grip\": '{ami.pose_is_controller_grip}'")
65 s
.append(f
"\"pose_is_controller_aim\": '{ami.pose_is_controller_aim}'")
67 return "{" + ", ".join(s
) + "}"
70 def ami_data_from_args(ami
, args
):
71 ami
.type = args
["type"]
73 for path
in args
["user_paths"]:
74 ami
.user_paths
.new(path
)
76 if ami
.type == 'FLOAT' or ami
.type == 'VECTOR2D':
78 ami
.op_mode
= args
["op_mode"]
79 ami
.bimanual
= True if (args
["bimanual"] == 'True') else False
80 ami
.haptic_name
= args
["haptic_name"]
81 ami
.haptic_match_user_paths
= True if (args
["haptic_match_user_paths"] == 'True') else False
82 ami
.haptic_duration
= float(args
["haptic_duration"])
83 ami
.haptic_frequency
= float(args
["haptic_frequency"])
84 ami
.haptic_amplitude
= float(args
["haptic_amplitude"])
85 ami
.haptic_mode
= args
["haptic_mode"]
86 elif ami
.type == 'POSE':
87 ami
.pose_is_controller_grip
= True if (args
["pose_is_controller_grip"] == 'True') else False
88 ami
.pose_is_controller_aim
= True if (args
["pose_is_controller_aim"] == 'True') else False
91 def _ami_properties_to_lines_recursive(level
, properties
, lines
):
92 from bpy
.types
import OperatorProperties
94 def string_value(value
):
95 if isinstance(value
, (str, bool, int, set)):
97 elif isinstance(value
, float):
98 return repr_f32(value
)
99 elif getattr(value
, '__len__', False):
100 return repr(tuple(value
))
101 raise Exception(f
"Export action configuration: can't write {value!r}")
103 for pname
in properties
.bl_rna
.properties
.keys():
104 if pname
!= "rna_type":
105 value
= getattr(properties
, pname
)
106 if isinstance(value
, OperatorProperties
):
108 _ami_properties_to_lines_recursive(level
+ 2, value
, lines_test
)
111 lines
.append(f
"\"{pname}\",\n")
112 lines
.append(f
"{indent(level + 3)}" "[")
113 lines
.extend(lines_test
)
115 lines
.append(f
"{indent(level + 3)}" "),\n" f
"{indent(level + 2)}")
117 elif properties
.is_property_set(pname
):
118 value
= string_value(value
)
119 lines
.append((f
"(\"{pname}\", {value:s}),\n" f
"{indent(level + 2)}"))
122 def _ami_properties_to_lines(level
, ami_props
, lines
):
123 if ami_props
is None:
126 lines_test
= [f
"\"op_properties\":\n" f
"{indent(level + 1)}" "["]
127 _ami_properties_to_lines_recursive(level
, ami_props
, lines_test
)
128 if len(lines_test
) > 1:
129 lines_test
.append("],\n")
130 lines
.extend(lines_test
)
133 def _ami_attrs_or_none(level
, ami
):
135 _ami_properties_to_lines(level
+ 1, ami
.op_properties
, lines
)
138 return "".join(lines
)
141 def amb_args_as_data(amb
, type):
143 f
"\"profile\": '{amb.profile}'",
146 scp
= f
"\"component_paths\": ["
147 for component_path
in amb
.component_paths
:
148 scp
+= f
"'{component_path.path}', "
149 if len(amb
.component_paths
) > 0:
154 if type == 'FLOAT' or type == 'VECTOR2D':
155 s
.append(f
"\"threshold\": '{amb.threshold}'")
157 s
.append(f
"\"axis_region\": '{amb.axis0_region}'")
158 else: # type == 'VECTOR2D':
159 s
.append(f
"\"axis0_region\": '{amb.axis0_region}'")
160 s
.append(f
"\"axis1_region\": '{amb.axis1_region}'")
162 s
.append(f
"\"pose_location\": '{amb.pose_location.x, amb.pose_location.y, amb.pose_location.z}'")
163 s
.append(f
"\"pose_rotation\": '{amb.pose_rotation.x, amb.pose_rotation.y, amb.pose_rotation.z}'")
165 return "{" + ", ".join(s
) + "}"
168 def amb_data_from_args(amb
, args
, type):
169 amb
.profile
= args
["profile"]
171 for path
in args
["component_paths"]:
172 amb
.component_paths
.new(path
)
174 if type == 'FLOAT' or type == 'VECTOR2D':
175 amb
.threshold
= float(args
["threshold"])
177 amb
.axis0_region
= args
["axis_region"]
178 else: # type == 'VECTOR2D':
179 amb
.axis0_region
= args
["axis0_region"]
180 amb
.axis1_region
= args
["axis1_region"]
182 l
= args
["pose_location"].strip(')(').split(', ')
183 amb
.pose_location
.x
= float(l
[0])
184 amb
.pose_location
.y
= float(l
[1])
185 amb
.pose_location
.z
= float(l
[2])
186 l
= args
["pose_rotation"].strip(')(').split(', ')
187 amb
.pose_rotation
.x
= float(l
[0])
188 amb
.pose_rotation
.y
= float(l
[1])
189 amb
.pose_rotation
.z
= float(l
[2])
192 def actionconfig_export_as_data(session_state
, filepath
, *, sort
=False):
193 export_actionmaps
= []
195 for am
in session_state
.actionmaps
:
196 export_actionmaps
.append(am
)
199 export_actionmaps
.sort(key
=lambda k
: k
.name
)
201 with
open(filepath
, "w", encoding
="utf-8") as fh
:
204 # Use the file version since it includes the sub-version
205 # which we can bump multiple times between releases.
206 from bpy
.app
import version_file
207 fw(f
"actionconfig_version = {version_file!r}\n")
210 fw("actionconfig_data = \\\n[")
212 for am
in export_actionmaps
:
214 fw(f
"\"{am.name:s}\",\n")
216 fw(f
"{indent(2)}" "{")
219 for ami
in am
.actionmap_items
:
221 fw(f
"\"{ami.name:s}\"")
222 ami_args
= ami_args_as_data(ami
)
223 ami_data
= _ami_attrs_or_none(4, ami
)
227 fw(",\n" f
"{indent(5)}")
234 fw(f
"{indent(5)}" "{")
237 fw("}," f
"{indent(5)}")
240 fw(f
"{indent(5)}" "{")
241 fw(f
"\"bindings\":\n")
243 for amb
in ami
.bindings
:
245 fw(f
"\"{amb.name:s}\"")
247 amb_args
= amb_args_as_data(amb
, ami
.type)
249 fw("),\n" f
"{indent(7)}")
250 fw("],\n" f
"{indent(6)}")
251 fw("},\n" f
"{indent(5)}")
252 fw("),\n" f
"{indent(4)}")
254 fw("],\n" f
"{indent(3)}")
255 fw("},\n" f
"{indent(2)}")
256 fw("),\n" f
"{indent(1)}")
260 fw("if __name__ == \"__main__\":\n")
262 # We could remove this in the future, as loading new action-maps in older Blender versions
263 # makes less and less sense as Blender changes.
264 fw(" # Only add keywords that are supported.\n")
265 fw(" from bpy.app import version as blender_version\n")
266 fw(" keywords = {}\n")
267 fw(" if blender_version >= (3, 0, 0):\n")
268 fw(" keywords[\"actionconfig_version\"] = actionconfig_version\n")
271 fw(" from viewport_vr_preview.io import actionconfig_import_from_data\n")
272 fw(" actionconfig_import_from_data(\n")
273 fw(" os.path.splitext(os.path.basename(__file__))[0],\n")
274 fw(" actionconfig_data,\n")
279 # -----------------------------------------------------------------------------
282 def _ami_props_setattr(ami_name
, ami_props
, attr
, value
):
283 if type(value
) is list:
284 ami_subprop
= getattr(ami_props
, attr
)
285 for subattr
, subvalue
in value
:
286 _ami_props_setattr(ami_subprop
, subattr
, subvalue
)
290 setattr(ami_props
, attr
, value
)
291 except AttributeError:
292 print(f
"Warning: property '{attr}' not found in action map item '{ami_name}'")
293 except Exception as ex
:
294 print(f
"Warning: {ex!r}")
297 def actionmap_item_init_from_data(ami
, ami_bindings
):
298 new_fn
= getattr(ami
.bindings
, "new")
299 for (amb_name
, amb_args
) in ami_bindings
:
300 amb
= new_fn(amb_name
, True)
301 amb_data_from_args(amb
, amb_args
, ami
.type)
304 def actionmap_init_from_data(am
, am_items
):
305 new_fn
= getattr(am
.actionmap_items
, "new")
306 for (ami_name
, ami_args
, ami_data
, ami_content
) in am_items
:
307 ami
= new_fn(ami_name
, True)
308 ami_data_from_args(ami
, ami_args
)
309 if ami_data
is not None:
310 ami_props_data
= ami_data
.get("op_properties", None)
311 if ami_props_data
is not None:
312 ami_props
= ami
.op_properties
313 assert type(ami_props_data
) is list
314 for attr
, value
in ami_props_data
:
315 _ami_props_setattr(ami_name
, ami_props
, attr
, value
)
316 ami_bindings
= ami_content
["bindings"]
317 assert type(ami_bindings
) is list
318 actionmap_item_init_from_data(ami
, ami_bindings
)
321 def actionconfig_init_from_data(session_state
, actionconfig_data
, actionconfig_version
):
322 # Load data in the format defined above.
324 # Runs at load time, keep this fast!
325 if actionconfig_version
is not None:
326 from .versioning
import actionconfig_update
327 actionconfig_data
= actionconfig_update(actionconfig_data
, actionconfig_version
)
329 for (am_name
, am_content
) in actionconfig_data
:
330 am
= session_state
.actionmaps
.new(session_state
, am_name
, True)
331 am_items
= am_content
["items"]
332 # Check here instead of inside 'actionmap_init_from_data'
333 # because we want to allow both tuple & list types in that case.
335 # For full action maps, ensure these are always lists to allow for extending them
336 # in a generic way that doesn't have to check for the type each time.
337 assert type(am_items
) is list
338 actionmap_init_from_data(am
, am_items
)
341 def actionconfig_import_from_data(session_state
, actionconfig_data
, *, actionconfig_version
=(0, 0, 0)):
342 # Load data in the format defined above.
344 # Runs at load time, keep this fast!
346 actionconfig_init_from_data(session_state
, actionconfig_data
, actionconfig_version
)