io_mesh_uv_layout: speed up png export with OIIO (x7)
[blender-addons.git] / viewport_vr_preview / action_map_io.py
blob0a8c5ad85651a67eb119f208151b0bd45ba74550
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # -----------------------------------------------------------------------------
4 # Export Functions
6 __all__ = (
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",
15 def indent(levels):
16 return levels * " "
19 def round_float_32(f):
20 from struct import pack, unpack
21 return unpack("f", pack("f", f))[0]
24 def repr_f32(f):
25 f_round = round_float_32(f)
26 f_str = repr(f)
27 f_str_frac = f_str.partition(".")[2]
28 if not f_str_frac:
29 return f_str
30 for i in range(1, len(f_str_frac)):
31 f_test = round(f, i)
32 f_test_round = round_float_32(f_test)
33 if f_test_round == f_round:
34 return "%.*f" % (i, f_test)
35 return f_str
38 def ami_args_as_data(ami):
39 s = [
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:
47 sup = sup[:-2]
48 sup += "]"
49 s.append(sup)
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':
75 ami.op = args["op"]
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)):
94 return repr(value)
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):
105 lines_test = []
106 _ami_properties_to_lines_recursive(level + 2, value, lines_test)
107 if lines_test:
108 lines.append(f"(")
109 lines.append(f"\"{pname}\",\n")
110 lines.append(f"{indent(level + 3)}" "[")
111 lines.extend(lines_test)
112 lines.append("],\n")
113 lines.append(f"{indent(level + 3)}" "),\n" f"{indent(level + 2)}")
114 del lines_test
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:
122 return
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):
132 lines = []
133 _ami_properties_to_lines(level + 1, ami.op_properties, lines)
134 if not lines:
135 return None
136 return "".join(lines)
139 def amb_args_as_data(amb, type):
140 s = [
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:
148 scp = scp[:-2]
149 scp += "]"
150 s.append(scp)
152 if type == 'FLOAT' or type == 'VECTOR2D':
153 s.append(f"\"threshold\": '{amb.threshold}'")
154 if type == 'FLOAT':
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}'")
159 elif type == 'POSE':
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"])
174 if type == 'FLOAT':
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"]
179 elif type == 'POSE':
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)
196 if sort:
197 export_actionmaps.sort(key=lambda k: k.name)
199 with open(filepath, "w", encoding="utf-8") as fh:
200 fw = fh.write
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")
206 del version_file
208 fw("actionconfig_data = \\\n[")
210 for am in export_actionmaps:
211 fw("(")
212 fw(f"\"{am.name:s}\",\n")
214 fw(f"{indent(2)}" "{")
215 fw(f"\"items\":\n")
216 fw(f"{indent(3)}[")
217 for ami in am.actionmap_items:
218 fw(f"(")
219 fw(f"\"{ami.name:s}\"")
220 ami_args = ami_args_as_data(ami)
221 ami_data = _ami_attrs_or_none(4, ami)
222 if ami_data is None:
223 fw(f", ")
224 else:
225 fw(",\n" f"{indent(5)}")
227 fw(ami_args)
228 if ami_data is None:
229 fw(", None,\n")
230 else:
231 fw(",\n")
232 fw(f"{indent(5)}" "{")
233 fw(ami_data)
234 fw(f"{indent(6)}")
235 fw("}," f"{indent(5)}")
236 fw("\n")
238 fw(f"{indent(5)}" "{")
239 fw(f"\"bindings\":\n")
240 fw(f"{indent(6)}[")
241 for amb in ami.bindings:
242 fw(f"(")
243 fw(f"\"{amb.name:s}\"")
244 fw(f", ")
245 amb_args = amb_args_as_data(amb, ami.type)
246 fw(amb_args)
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)}")
256 fw("]\n")
257 fw("\n\n")
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")
268 fw(" import os\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")
273 fw(" **keywords,\n")
274 fw(" )\n")
277 # -----------------------------------------------------------------------------
278 # Import Functions
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)
285 return
287 try:
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!
343 import bpy
344 actionconfig_init_from_data(session_state, actionconfig_data, actionconfig_version)