AnimAll: rename the "Animate" tab back to "Animation"
[blender-addons.git] / viewport_vr_preview / action_map_io.py
blob02183fb95e168035a70123fd52e6429910bd5da3
1 # SPDX-FileCopyrightText: 2021-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # -----------------------------------------------------------------------------
6 # Export Functions
8 __all__ = (
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",
17 def indent(levels):
18 return levels * " "
21 def round_float_32(f):
22 from struct import pack, unpack
23 return unpack("f", pack("f", f))[0]
26 def repr_f32(f):
27 f_round = round_float_32(f)
28 f_str = repr(f)
29 f_str_frac = f_str.partition(".")[2]
30 if not f_str_frac:
31 return f_str
32 for i in range(1, len(f_str_frac)):
33 f_test = round(f, i)
34 f_test_round = round_float_32(f_test)
35 if f_test_round == f_round:
36 return "%.*f" % (i, f_test)
37 return f_str
40 def ami_args_as_data(ami):
41 s = [
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:
49 sup = sup[:-2]
50 sup += "]"
51 s.append(sup)
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':
77 ami.op = args["op"]
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)):
96 return repr(value)
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):
107 lines_test = []
108 _ami_properties_to_lines_recursive(level + 2, value, lines_test)
109 if lines_test:
110 lines.append(f"(")
111 lines.append(f"\"{pname}\",\n")
112 lines.append(f"{indent(level + 3)}" "[")
113 lines.extend(lines_test)
114 lines.append("],\n")
115 lines.append(f"{indent(level + 3)}" "),\n" f"{indent(level + 2)}")
116 del lines_test
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:
124 return
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):
134 lines = []
135 _ami_properties_to_lines(level + 1, ami.op_properties, lines)
136 if not lines:
137 return None
138 return "".join(lines)
141 def amb_args_as_data(amb, type):
142 s = [
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:
150 scp = scp[:-2]
151 scp += "]"
152 s.append(scp)
154 if type == 'FLOAT' or type == 'VECTOR2D':
155 s.append(f"\"threshold\": '{amb.threshold}'")
156 if type == 'FLOAT':
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}'")
161 elif type == 'POSE':
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"])
176 if type == 'FLOAT':
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"]
181 elif type == 'POSE':
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)
198 if sort:
199 export_actionmaps.sort(key=lambda k: k.name)
201 with open(filepath, "w", encoding="utf-8") as fh:
202 fw = fh.write
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")
208 del version_file
210 fw("actionconfig_data = \\\n[")
212 for am in export_actionmaps:
213 fw("(")
214 fw(f"\"{am.name:s}\",\n")
216 fw(f"{indent(2)}" "{")
217 fw(f"\"items\":\n")
218 fw(f"{indent(3)}[")
219 for ami in am.actionmap_items:
220 fw(f"(")
221 fw(f"\"{ami.name:s}\"")
222 ami_args = ami_args_as_data(ami)
223 ami_data = _ami_attrs_or_none(4, ami)
224 if ami_data is None:
225 fw(f", ")
226 else:
227 fw(",\n" f"{indent(5)}")
229 fw(ami_args)
230 if ami_data is None:
231 fw(", None,\n")
232 else:
233 fw(",\n")
234 fw(f"{indent(5)}" "{")
235 fw(ami_data)
236 fw(f"{indent(6)}")
237 fw("}," f"{indent(5)}")
238 fw("\n")
240 fw(f"{indent(5)}" "{")
241 fw(f"\"bindings\":\n")
242 fw(f"{indent(6)}[")
243 for amb in ami.bindings:
244 fw(f"(")
245 fw(f"\"{amb.name:s}\"")
246 fw(f", ")
247 amb_args = amb_args_as_data(amb, ami.type)
248 fw(amb_args)
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)}")
258 fw("]\n")
259 fw("\n\n")
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")
270 fw(" import os\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")
275 fw(" **keywords,\n")
276 fw(" )\n")
279 # -----------------------------------------------------------------------------
280 # Import Functions
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)
287 return
289 try:
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!
345 import bpy
346 actionconfig_init_from_data(session_state, actionconfig_data, actionconfig_version)