1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # -----------------------------------------------------------------------------
7 "actionconfig_export_as_data",
8 "actionconfig_import_from_data",
9 "actionconfig_init_from_data",
10 "actionmap_init_from_data",
11 "actionmap_item_init_from_data",
19 def round_float_32(f
):
20 from struct
import pack
, unpack
21 return unpack("f", pack("f", f
))[0]
25 f_round
= round_float_32(f
)
27 f_str_frac
= f_str
.partition(".")[2]
30 for i
in range(1, len(f_str_frac
)):
32 f_test_round
= round_float_32(f_test
)
33 if f_test_round
== f_round
:
34 return "%.*f" % (i
, f_test
)
38 def ami_args_as_data(ami
):
40 f
"\"type\": '{ami.type}'",
43 sup
= f
"\"user_paths\": ["
44 for user_path
in ami
.user_paths
:
45 sup
+= f
"'{user_path.path}', "
46 if len(ami
.user_paths
) > 0:
51 if ami
.type == 'FLOAT' or ami
.type == 'VECTOR2D':
52 s
.append(f
"\"op\": '{ami.op}'")
53 s
.append(f
"\"op_mode\": '{ami.op_mode}'")
54 s
.append(f
"\"bimanual\": '{ami.bimanual}'")
55 s
.append(f
"\"haptic_name\": '{ami.haptic_name}'")
56 s
.append(f
"\"haptic_match_user_paths\": '{ami.haptic_match_user_paths}'")
57 s
.append(f
"\"haptic_duration\": '{ami.haptic_duration}'")
58 s
.append(f
"\"haptic_frequency\": '{ami.haptic_frequency}'")
59 s
.append(f
"\"haptic_amplitude\": '{ami.haptic_amplitude}'")
60 s
.append(f
"\"haptic_mode\": '{ami.haptic_mode}'")
61 elif ami
.type == 'POSE':
62 s
.append(f
"\"pose_is_controller_grip\": '{ami.pose_is_controller_grip}'")
63 s
.append(f
"\"pose_is_controller_aim\": '{ami.pose_is_controller_aim}'")
65 return "{" + ", ".join(s
) + "}"
68 def ami_data_from_args(ami
, args
):
69 ami
.type = args
["type"]
71 for path
in args
["user_paths"]:
72 ami
.user_paths
.new(path
)
74 if ami
.type == 'FLOAT' or ami
.type == 'VECTOR2D':
76 ami
.op_mode
= args
["op_mode"]
77 ami
.bimanual
= True if (args
["bimanual"] == 'True') else False
78 ami
.haptic_name
= args
["haptic_name"]
79 ami
.haptic_match_user_paths
= True if (args
["haptic_match_user_paths"] == 'True') else False
80 ami
.haptic_duration
= float(args
["haptic_duration"])
81 ami
.haptic_frequency
= float(args
["haptic_frequency"])
82 ami
.haptic_amplitude
= float(args
["haptic_amplitude"])
83 ami
.haptic_mode
= args
["haptic_mode"]
84 elif ami
.type == 'POSE':
85 ami
.pose_is_controller_grip
= True if (args
["pose_is_controller_grip"] == 'True') else False
86 ami
.pose_is_controller_aim
= True if (args
["pose_is_controller_aim"] == 'True') else False
89 def _ami_properties_to_lines_recursive(level
, properties
, lines
):
90 from bpy
.types
import OperatorProperties
92 def string_value(value
):
93 if isinstance(value
, (str, bool, int, set)):
95 elif isinstance(value
, float):
96 return repr_f32(value
)
97 elif getattr(value
, '__len__', False):
98 return repr(tuple(value
))
99 raise Exception(f
"Export action configuration: can't write {value!r}")
101 for pname
in properties
.bl_rna
.properties
.keys():
102 if pname
!= "rna_type":
103 value
= getattr(properties
, pname
)
104 if isinstance(value
, OperatorProperties
):
106 _ami_properties_to_lines_recursive(level
+ 2, value
, lines_test
)
109 lines
.append(f
"\"{pname}\",\n")
110 lines
.append(f
"{indent(level + 3)}" "[")
111 lines
.extend(lines_test
)
113 lines
.append(f
"{indent(level + 3)}" "),\n" f
"{indent(level + 2)}")
115 elif properties
.is_property_set(pname
):
116 value
= string_value(value
)
117 lines
.append((f
"(\"{pname}\", {value:s}),\n" f
"{indent(level + 2)}"))
120 def _ami_properties_to_lines(level
, ami_props
, lines
):
121 if ami_props
is None:
124 lines_test
= [f
"\"op_properties\":\n" f
"{indent(level + 1)}" "["]
125 _ami_properties_to_lines_recursive(level
, ami_props
, lines_test
)
126 if len(lines_test
) > 1:
127 lines_test
.append("],\n")
128 lines
.extend(lines_test
)
131 def _ami_attrs_or_none(level
, ami
):
133 _ami_properties_to_lines(level
+ 1, ami
.op_properties
, lines
)
136 return "".join(lines
)
139 def amb_args_as_data(amb
, type):
141 f
"\"profile\": '{amb.profile}'",
144 scp
= f
"\"component_paths\": ["
145 for component_path
in amb
.component_paths
:
146 scp
+= f
"'{component_path.path}', "
147 if len(amb
.component_paths
) > 0:
152 if type == 'FLOAT' or type == 'VECTOR2D':
153 s
.append(f
"\"threshold\": '{amb.threshold}'")
155 s
.append(f
"\"axis_region\": '{amb.axis0_region}'")
156 else: # type == 'VECTOR2D':
157 s
.append(f
"\"axis0_region\": '{amb.axis0_region}'")
158 s
.append(f
"\"axis1_region\": '{amb.axis1_region}'")
160 s
.append(f
"\"pose_location\": '{amb.pose_location.x, amb.pose_location.y, amb.pose_location.z}'")
161 s
.append(f
"\"pose_rotation\": '{amb.pose_rotation.x, amb.pose_rotation.y, amb.pose_rotation.z}'")
163 return "{" + ", ".join(s
) + "}"
166 def amb_data_from_args(amb
, args
, type):
167 amb
.profile
= args
["profile"]
169 for path
in args
["component_paths"]:
170 amb
.component_paths
.new(path
)
172 if type == 'FLOAT' or type == 'VECTOR2D':
173 amb
.threshold
= float(args
["threshold"])
175 amb
.axis0_region
= args
["axis_region"]
176 else: # type == 'VECTOR2D':
177 amb
.axis0_region
= args
["axis0_region"]
178 amb
.axis1_region
= args
["axis1_region"]
180 l
= args
["pose_location"].strip(')(').split(', ')
181 amb
.pose_location
.x
= float(l
[0])
182 amb
.pose_location
.y
= float(l
[1])
183 amb
.pose_location
.z
= float(l
[2])
184 l
= args
["pose_rotation"].strip(')(').split(', ')
185 amb
.pose_rotation
.x
= float(l
[0])
186 amb
.pose_rotation
.y
= float(l
[1])
187 amb
.pose_rotation
.z
= float(l
[2])
190 def actionconfig_export_as_data(session_state
, filepath
, *, sort
=False):
191 export_actionmaps
= []
193 for am
in session_state
.actionmaps
:
194 export_actionmaps
.append(am
)
197 export_actionmaps
.sort(key
=lambda k
: k
.name
)
199 with
open(filepath
, "w", encoding
="utf-8") as fh
:
202 # Use the file version since it includes the sub-version
203 # which we can bump multiple times between releases.
204 from bpy
.app
import version_file
205 fw(f
"actionconfig_version = {version_file!r}\n")
208 fw("actionconfig_data = \\\n[")
210 for am
in export_actionmaps
:
212 fw(f
"\"{am.name:s}\",\n")
214 fw(f
"{indent(2)}" "{")
217 for ami
in am
.actionmap_items
:
219 fw(f
"\"{ami.name:s}\"")
220 ami_args
= ami_args_as_data(ami
)
221 ami_data
= _ami_attrs_or_none(4, ami
)
225 fw(",\n" f
"{indent(5)}")
232 fw(f
"{indent(5)}" "{")
235 fw("}," f
"{indent(5)}")
238 fw(f
"{indent(5)}" "{")
239 fw(f
"\"bindings\":\n")
241 for amb
in ami
.bindings
:
243 fw(f
"\"{amb.name:s}\"")
245 amb_args
= amb_args_as_data(amb
, ami
.type)
247 fw("),\n" f
"{indent(7)}")
248 fw("],\n" f
"{indent(6)}")
249 fw("},\n" f
"{indent(5)}")
250 fw("),\n" f
"{indent(4)}")
252 fw("],\n" f
"{indent(3)}")
253 fw("},\n" f
"{indent(2)}")
254 fw("),\n" f
"{indent(1)}")
258 fw("if __name__ == \"__main__\":\n")
260 # We could remove this in the future, as loading new action-maps in older Blender versions
261 # makes less and less sense as Blender changes.
262 fw(" # Only add keywords that are supported.\n")
263 fw(" from bpy.app import version as blender_version\n")
264 fw(" keywords = {}\n")
265 fw(" if blender_version >= (3, 0, 0):\n")
266 fw(" keywords[\"actionconfig_version\"] = actionconfig_version\n")
269 fw(" from viewport_vr_preview.io import actionconfig_import_from_data\n")
270 fw(" actionconfig_import_from_data(\n")
271 fw(" os.path.splitext(os.path.basename(__file__))[0],\n")
272 fw(" actionconfig_data,\n")
277 # -----------------------------------------------------------------------------
280 def _ami_props_setattr(ami_name
, ami_props
, attr
, value
):
281 if type(value
) is list:
282 ami_subprop
= getattr(ami_props
, attr
)
283 for subattr
, subvalue
in value
:
284 _ami_props_setattr(ami_subprop
, subattr
, subvalue
)
288 setattr(ami_props
, attr
, value
)
289 except AttributeError:
290 print(f
"Warning: property '{attr}' not found in action map item '{ami_name}'")
291 except Exception as ex
:
292 print(f
"Warning: {ex!r}")
295 def actionmap_item_init_from_data(ami
, ami_bindings
):
296 new_fn
= getattr(ami
.bindings
, "new")
297 for (amb_name
, amb_args
) in ami_bindings
:
298 amb
= new_fn(amb_name
, True)
299 amb_data_from_args(amb
, amb_args
, ami
.type)
302 def actionmap_init_from_data(am
, am_items
):
303 new_fn
= getattr(am
.actionmap_items
, "new")
304 for (ami_name
, ami_args
, ami_data
, ami_content
) in am_items
:
305 ami
= new_fn(ami_name
, True)
306 ami_data_from_args(ami
, ami_args
)
307 if ami_data
is not None:
308 ami_props_data
= ami_data
.get("op_properties", None)
309 if ami_props_data
is not None:
310 ami_props
= ami
.op_properties
311 assert type(ami_props_data
) is list
312 for attr
, value
in ami_props_data
:
313 _ami_props_setattr(ami_name
, ami_props
, attr
, value
)
314 ami_bindings
= ami_content
["bindings"]
315 assert type(ami_bindings
) is list
316 actionmap_item_init_from_data(ami
, ami_bindings
)
319 def actionconfig_init_from_data(session_state
, actionconfig_data
, actionconfig_version
):
320 # Load data in the format defined above.
322 # Runs at load time, keep this fast!
323 if actionconfig_version
is not None:
324 from .versioning
import actionconfig_update
325 actionconfig_data
= actionconfig_update(actionconfig_data
, actionconfig_version
)
327 for (am_name
, am_content
) in actionconfig_data
:
328 am
= session_state
.actionmaps
.new(session_state
, am_name
, True)
329 am_items
= am_content
["items"]
330 # Check here instead of inside 'actionmap_init_from_data'
331 # because we want to allow both tuple & list types in that case.
333 # For full action maps, ensure these are always lists to allow for extending them
334 # in a generic way that doesn't have to check for the type each time.
335 assert type(am_items
) is list
336 actionmap_init_from_data(am
, am_items
)
339 def actionconfig_import_from_data(session_state
, actionconfig_data
, *, actionconfig_version
=(0, 0, 0)):
340 # Load data in the format defined above.
342 # Runs at load time, keep this fast!
344 actionconfig_init_from_data(session_state
, actionconfig_data
, actionconfig_version
)