From 083b84673f1beba438f92085f454782b39759300 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Tue, 14 Jul 2026 09:45:50 -0500 Subject: [PATCH 1/4] Added experimental option to turn off batched evaluations --- pyproject.toml | 2 +- src/addons/character_dna/__init__.py | 2 +- src/addons/character_dna/blender_manifest.toml | 2 +- src/addons/character_dna/properties.py | 10 ++++++++++ src/addons/character_dna/release_notes.md | 11 ++--------- src/addons/character_dna/rig_instance.py | 18 ++++++++++++++++++ src/addons/character_dna/typing.py | 1 + .../character_dna/ui/addon_preferences.py | 10 ++++++++++ uv.lock | 2 +- 9 files changed, 45 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 41cd2e9..a7f06c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = ["blender", "metahuman", "dna", "riglogic", "animation", "3d"] name = "character-dna-addon" readme = "README.md" requires-python = ">=3.11,<3.14" -version = "0.9.8" +version = "0.9.10" [project.optional-dependencies] dev = [ diff --git a/src/addons/character_dna/__init__.py b/src/addons/character_dna/__init__.py index 8e93ce6..2627dcf 100644 --- a/src/addons/character_dna/__init__.py +++ b/src/addons/character_dna/__init__.py @@ -17,7 +17,7 @@ bl_info = { "name": "Character DNA", "author": "Poly Hammer", - "version": (0, 9, 8), + "version": (0, 9, 10), "blender": (4, 5, 0), "location": "File > Import > MetaHuman DNA", "description": ( diff --git a/src/addons/character_dna/blender_manifest.toml b/src/addons/character_dna/blender_manifest.toml index 06d9b3a..b26f149 100644 --- a/src/addons/character_dna/blender_manifest.toml +++ b/src/addons/character_dna/blender_manifest.toml @@ -11,7 +11,7 @@ schema_version = "1.0.0" id = "character_dna" name = "Character DNA" tagline = "Customize MetaHuman DNA files with RigLogic evaluation support" -version = "0.9.8" +version = "0.9.10" type = "add-on" # ============================================================================= diff --git a/src/addons/character_dna/properties.py b/src/addons/character_dna/properties.py index 22d411f..d5e095a 100644 --- a/src/addons/character_dna/properties.py +++ b/src/addons/character_dna/properties.py @@ -328,6 +328,16 @@ class CharacterAddonProperties: next_metrics_consent_timestamp: bpy.props.FloatProperty(default=0.0) # pyright: ignore[reportInvalidTypeForm] extra_dna_folder_list: bpy.props.CollectionProperty(type=ExtraDnaFolder) # pyright: ignore[reportInvalidTypeForm] extra_dna_folder_list_active_index: bpy.props.IntProperty() # pyright: ignore[reportInvalidTypeForm] + batched_evaluations: bpy.props.BoolProperty( + name="Batched Evaluations", + default=True, + description=( + "Batch all rig evaluations onto a the next draw call so all updates from a single scene change are " + "applied together. This is optimized for viewport better performance (FPS) and stability. Disable this to " + "evaluate the rig immediately when the scene updates, this can remove jittering when rendering but can " + "cause instability and crashes." + ), + ) # pyright: ignore[reportInvalidTypeForm] show_pro_features: bpy.props.BoolProperty( name="Show Pro Features", default=True, diff --git a/src/addons/character_dna/release_notes.md b/src/addons/character_dna/release_notes.md index a14aa9d..2591909 100644 --- a/src/addons/character_dna/release_notes.md +++ b/src/addons/character_dna/release_notes.md @@ -1,14 +1,7 @@ -## Minor Changes - -* Added transfer to shape key operator in Raw Editor - ## Patch Changes -* Fixed Body Baking issue when animating with control rig [#342](https://github.com/poly-hammer/character-dna-addon/issues/342) -* Made DNA component type detection more robust [#337](https://github.com/poly-hammer/character-dna-addon/issues/337) -* Fixed body animation pose mode switching bug -* Fixed view options panel lagging draw calls -* Fixed operator for adding [Texture Logic Node](https://docs.polyhammer.com/character-dna-addon/terminology/#texture-logic) now under `Add > Rig Logic > Head Texture Logic` in material graph +* Added experimental option to turn off batched evaluations +* Fixed center eye control baking bug [#308](https://github.com/poly-hammer/character-dna-addon/issues/308) ## Tests Passing On diff --git a/src/addons/character_dna/rig_instance.py b/src/addons/character_dna/rig_instance.py index f30c25a..9c46e97 100644 --- a/src/addons/character_dna/rig_instance.py +++ b/src/addons/character_dna/rig_instance.py @@ -274,6 +274,24 @@ def rig_instance_listener(_: "Scene", dependency_graph: bpy.types.Depsgraph, is_ else: final_instance_updates.add((instance, component)) + if not final_instance_updates: + return + + # When batched evaluations are disabled, apply the evaluations directly here instead of + # deferring them to the timer. This reduces latency (no batching jitter/lag) at the cost of + # potential instability while rendering, since handlers can run in a restricted context. + addon_preferences = utilities.get_addon_preferences() + if addon_preferences and not addon_preferences.batched_evaluations: + for instance, component in final_instance_updates: + try: + instance.evaluate(component=component) + except ReferenceError: + # The underlying data was freed out from under us; skip it. + continue + except Exception as error: + logger.exception(f"Error evaluating rig instance '{instance.name}': {error}") + return + # Defer evaluation to a timer callback where Blender allows writing to ID data. # Queue instances by name so an undo that reallocates the collection can't leave us # holding a dangling PropertyGroup wrapper. diff --git a/src/addons/character_dna/typing.py b/src/addons/character_dna/typing.py index 8b24bf6..c1405a5 100644 --- a/src/addons/character_dna/typing.py +++ b/src/addons/character_dna/typing.py @@ -123,6 +123,7 @@ class CharacterAddonPreferences(CharacterAddonProperties, bpy.types.AddonPrefere bl_idname: str metrics_collection: bool + batched_evaluations: bool rbf_editor: RBFEditorPreferences raw_control_editor: RawControlEditorPreferences shape_key_editor: ShapeKeyEditorPreferences diff --git a/src/addons/character_dna/ui/addon_preferences.py b/src/addons/character_dna/ui/addon_preferences.py index 5b67249..6fb548e 100644 --- a/src/addons/character_dna/ui/addon_preferences.py +++ b/src/addons/character_dna/ui/addon_preferences.py @@ -40,6 +40,16 @@ def draw(self, context: "Context"): row = layout.row() row.prop(self, "metrics_collection", text="Allow Metrics Collection") + row = layout.row() + row.prop(self, "batched_evaluations", text="Batched Evaluations") + if not self.batched_evaluations: + row = layout.row() + row.label( + text="(Experimental) Disabling Batched Evaluations can cause instability and crashes. Please report " + "any issues and provide your crash logs.", + icon="ERROR", + ) + # Editor Settings (Pro only). The ``show_pro_features`` toggle lets Pro # users preview what the free edition's UI looks like. When the editors # submodule is absent (free edition), show a note advertising Pro instead. diff --git a/uv.lock b/uv.lock index 177d53c..e47d855 100644 --- a/uv.lock +++ b/uv.lock @@ -193,7 +193,7 @@ wheels = [ [[package]] name = "character-dna-addon" -version = "0.9.8" +version = "0.9.10" source = { editable = "." } dependencies = [ { name = "sentry-sdk" }, From f5a57e66225875ce5133c0c75a454ee1bd04a108 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Tue, 14 Jul 2026 11:16:28 -0500 Subject: [PATCH 2/4] fixed eye aim bug and fix event notifications on rig instance add/remove --- pyproject.toml | 2 +- src/addons/character_dna/__init__.py | 9 +- .../character_dna/blender_manifest.toml | 2 +- src/addons/character_dna/components/base.py | 4 + src/addons/character_dna/operators.py | 2 + src/addons/character_dna/release_notes.md | 1 + src/addons/character_dna/rig_instance.py | 142 ++++++++++++------ src/addons/character_dna/utilities/action.py | 17 ++- src/addons/character_dna/utilities/misc.py | 23 ++- uv.lock | 2 +- 10 files changed, 143 insertions(+), 61 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a7f06c2..b0fa1ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = ["blender", "metahuman", "dna", "riglogic", "animation", "3d"] name = "character-dna-addon" readme = "README.md" requires-python = ">=3.11,<3.14" -version = "0.9.10" +version = "0.9.12" [project.optional-dependencies] dev = [ diff --git a/src/addons/character_dna/__init__.py b/src/addons/character_dna/__init__.py index 2627dcf..f1e46c8 100644 --- a/src/addons/character_dna/__init__.py +++ b/src/addons/character_dna/__init__.py @@ -17,7 +17,7 @@ bl_info = { "name": "Character DNA", "author": "Poly Hammer", - "version": (0, 9, 10), + "version": (0, 9, 12), "blender": (4, 5, 0), "location": "File > Import > MetaHuman DNA", "description": ( @@ -29,9 +29,10 @@ "category": "Poly Hammer", } -# Callbacks that other addons can register to be notified when a rig instance is set up -# in the scene. -post_setup_scene_callbacks: list[Callable[[rig_instance.RigInstance], None]] = [] +# Callbacks that other addons can register to be notified when the rig instance list changes +# (a rig instance is set up on load, added, or removed). The argument is the affected rig +# instance for additions/setup, or ``None`` for removals. +post_setup_scene_callbacks: list[Callable[[rig_instance.RigInstance | None], None]] = [] # Main Addon. Editor operators/panels are registered separately via the optional # ``editors`` submodule registry (see ``utilities.get_editors``), so this list diff --git a/src/addons/character_dna/blender_manifest.toml b/src/addons/character_dna/blender_manifest.toml index b26f149..e9c7013 100644 --- a/src/addons/character_dna/blender_manifest.toml +++ b/src/addons/character_dna/blender_manifest.toml @@ -11,7 +11,7 @@ schema_version = "1.0.0" id = "character_dna" name = "Character DNA" tagline = "Customize MetaHuman DNA files with RigLogic evaluation support" -version = "0.9.10" +version = "0.9.12" type = "add-on" # ============================================================================= diff --git a/src/addons/character_dna/components/base.py b/src/addons/character_dna/components/base.py index a8f99d7..4258aad 100644 --- a/src/addons/character_dna/components/base.py +++ b/src/addons/character_dna/components/base.py @@ -98,6 +98,8 @@ def __init__( self.rig_instance.name = name # set the active rig instance self.scene_properties.rig_instance_list_active_index = len(self.scene_properties.rig_instance_list) - 1 + # notify registered callbacks that a rig instance was added to the list + utilities.notify_rig_instances_changed(self.rig_instance) if component_type == "head": self.rig_instance.head_dna_file_path = str(dna_file_path) @@ -453,6 +455,8 @@ def _delete_rig_instance(self): my_list.remove(active_index) to_index = min(active_index, len(my_list) - 1) self.scene_properties.rig_instance_list_active_index = to_index + # notify registered callbacks that a rig instance was removed from the list + utilities.notify_rig_instances_changed() def import_materials(self) -> list[bpy.types.Material] | None: # noqa: PLR0912 if self.dna_import_properties and not self.dna_import_properties.import_materials: diff --git a/src/addons/character_dna/operators.py b/src/addons/character_dna/operators.py index e1d35a0..c5faa8d 100644 --- a/src/addons/character_dna/operators.py +++ b/src/addons/character_dna/operators.py @@ -1678,6 +1678,8 @@ def execute(self, context: "Context") -> set[str]: my_list.remove(self.active_index) to_index = min(self.active_index, len(my_list) - 1) addon_scene_properties.rig_instance_list_active_index = to_index + # notify registered callbacks that a rig instance was removed from the list + utilities.notify_rig_instances_changed() return {"FINISHED"} def invoke(self, context: "Context", event: bpy.types.Event) -> set[str] | None: diff --git a/src/addons/character_dna/release_notes.md b/src/addons/character_dna/release_notes.md index 2591909..e489497 100644 --- a/src/addons/character_dna/release_notes.md +++ b/src/addons/character_dna/release_notes.md @@ -2,6 +2,7 @@ * Added experimental option to turn off batched evaluations * Fixed center eye control baking bug [#308](https://github.com/poly-hammer/character-dna-addon/issues/308) +* Fixed bug with eye aim control when eyes follow head is false [#309](https://github.com/poly-hammer/character-dna-addon/issues/309) ## Tests Passing On diff --git a/src/addons/character_dna/rig_instance.py b/src/addons/character_dna/rig_instance.py index 9c46e97..3843471 100644 --- a/src/addons/character_dna/rig_instance.py +++ b/src/addons/character_dna/rig_instance.py @@ -1421,58 +1421,80 @@ def update_head_switch_values(self): # noqa: PLR0912 def get_head_gui_control_values_from_eye_aim(self) -> dict[str, dict[str, float]]: values = {} - if not self.face_board: + if not self.face_board or not self.head_rig: + return values + + # Read the *evaluated* objects so the posed bone matrices reflect the current head-bone + # rotation and the eye aim follow-head constraint. This is taken fresh from the current + # dependency graph (rather than the cached evaluated rig) so it is also correct per-frame + # during baking, where the scene frame is stepped just before this is called. + depsgraph = bpy.context.evaluated_depsgraph_get() + head_rig = self.head_rig.evaluated_get(depsgraph) + face_board = self.face_board.evaluated_get(depsgraph) + if not head_rig.pose or not face_board.pose: return values for target_name, eye_bone_name, control_name in [ ("CTRL_L_eyeAim", "FACIAL_L_Eye", "CTRL_L_eye"), ("CTRL_R_eyeAim", "FACIAL_R_Eye", "CTRL_R_eye"), ]: - target = self.face_board.pose.bones.get(target_name) - eye = self.head_rig.pose.bones.get(eye_bone_name) - if target and eye: - eye_rest_matrix = self.face_board.matrix_world @ eye.bone.matrix_local + target = face_board.pose.bones.get(target_name) + eye = head_rig.pose.bones.get(eye_bone_name) + if not (target and eye): + continue - # Current eye-to-target direction in world space - eye_pos = self.face_board.matrix_world @ eye.head - target_pos = self.face_board.matrix_world @ target.head - look_direction = target_pos - eye_pos + # The eye control values are expressed relative to the eye's neutral (control = 0) + # orientation. That neutral orientation is posed by the eye's parent chain, so it + # rotates with the head. Building the reference frame from the parent's *current* pose + # matrix (instead of the static rest matrix) is what lets the eyes keep aiming at a + # world-fixed target when the head bone is turned (the eyes-follow-head-off case). + eye_parent = eye.parent + if eye_parent: + rest_relative_to_parent = eye_parent.bone.matrix_local.inverted_safe() @ eye.bone.matrix_local + eye_reference_matrix = head_rig.matrix_world @ eye_parent.matrix @ rest_relative_to_parent + else: + eye_reference_matrix = head_rig.matrix_world @ eye.bone.matrix_local - if look_direction.length < FLOATING_POINT_PRECISION: - continue + # The eye and the aim target live on different objects, so map each into true world + # space with its own object matrix (head rig for the eye, face board for the target). + eye_pos = head_rig.matrix_world @ eye.head + target_pos = face_board.matrix_world @ target.head + look_direction = target_pos - eye_pos + + if look_direction.length < FLOATING_POINT_PRECISION: + continue - look_direction.normalize() + look_direction.normalize() - # Convert look direction to eye's local space - eye_matrix_inv = eye_rest_matrix.inverted() - local_look_direction = (eye_matrix_inv.to_3x3() @ look_direction).normalized() + # Convert the world look direction into the eye's reference (local) space. + local_look_direction = (eye_reference_matrix.to_3x3().inverted_safe() @ look_direction).normalized() - # Calculate horizontal distance (projection onto XZ plane) - horizontal_dist = math.sqrt(local_look_direction.x**2 + local_look_direction.z**2) + # Calculate horizontal distance (projection onto XZ plane, forward is local -Z) + horizontal_dist = math.sqrt(local_look_direction.x**2 + local_look_direction.z**2) - if horizontal_dist > FLOATING_POINT_PRECISION: - # Remap yaw to continuous range centered on forward direction (-Z) - # Instead of atan2(x, -z), we use the normalized x component directly - # This gives us a smooth -1 to 1 range for horizontal movement - x_normalized = local_look_direction.x / horizontal_dist + if horizontal_dist > FLOATING_POINT_PRECISION: + # Remap yaw to continuous range centered on forward direction (-Z) + # Instead of atan2(x, -z), we use the normalized x component directly + # This gives us a smooth -1 to 1 range for horizontal movement + x_normalized = local_look_direction.x / horizontal_dist - # For better control, we can use asin which gives -90° to 90° range - yaw = math.asin(max(-1.0, min(1.0, x_normalized))) - else: - # Looking straight up/down, yaw is undefined - yaw = 0.0 + # For better control, we can use asin which gives -90° to 90° range + yaw = math.asin(max(-1.0, min(1.0, x_normalized))) + else: + # Looking straight up/down, yaw is undefined + yaw = 0.0 - # Pitch is the angle from the horizontal plane - pitch = math.atan2(local_look_direction.y, horizontal_dist) + # Pitch is the angle from the horizontal plane + pitch = math.atan2(local_look_direction.y, horizontal_dist) - # Map angles to -1..1 range based on max rotation - x_max_rad = math.radians(60.0) - y_max_rad = math.radians(30.0) + # Map angles to -1..1 range based on max rotation + x_max_rad = math.radians(60.0) + y_max_rad = math.radians(30.0) - x_control = max(-1.0, min(1.0, yaw / x_max_rad)) - y_control = max(-1.0, min(1.0, pitch / y_max_rad)) + x_control = max(-1.0, min(1.0, yaw / x_max_rad)) + y_control = max(-1.0, min(1.0, pitch / y_max_rad)) - values[control_name] = {"x": x_control, "y": y_control} + values[control_name] = {"x": x_control, "y": y_control} return values @@ -1528,6 +1550,33 @@ def update_head_raw_control_values(self, override_values: dict[str, dict[str, fl ) self.data[self.cache_key("head", "logged_missing_raw_controls")] = True + def _resolve_eye_control_value( + self, + value: float | None, + control_name: str, + axis: str, + eye_aim_override_values: dict[str, dict[str, float]], + center_value: float | None, + ) -> float | None: + """Apply the eye aim / master center eye override to an individual L/R eye control value. + + The eye aim (when active) takes priority; otherwise the master ``CTRL_C_eye`` value + (``center_value``, read from either the live face board bone or the baked override + values) overrides the individual ``CTRL_L_eye`` / ``CTRL_R_eye`` value. Any control that + is not an eye control is returned unchanged. This is shared by the live and bake paths. + """ + if control_name not in ("CTRL_L_eye", "CTRL_R_eye"): + return value + + eye_aim_value = eye_aim_override_values.get(control_name, {}).get(axis) + if eye_aim_value is not None: + if abs(eye_aim_value) > FLOATING_POINT_PRECISION: + return eye_aim_value + elif center_value is not None and abs(center_value) > FLOATING_POINT_PRECISION: + return center_value + + return value + def update_head_gui_control_values(self, override_values: dict[str, dict[str, float]] | None = None): # skip if the face board is not set if not self.face_board or not self.head_dna_reader: @@ -1548,24 +1597,23 @@ def update_head_gui_control_values(self, override_values: dict[str, dict[str, fl # bone locations. This can be used for baking the values to an action. if override_values: value = override_values.get(control_name, {}).get(axis) + # Mirror the live path's eye handling so baking respects the eye aim and the + # master center eye control (read from the baked CTRL_C_eye override values). + value = self._resolve_eye_control_value( + value, control_name, axis, eye_aim_override_values, override_values.get("CTRL_C_eye", {}).get(axis) + ) if value is not None: head_instance.setGUIControl(index, value) else: pose_bone = face_pose_bones.get(control_name) if pose_bone: value = getattr(pose_bone.location, axis) - # special case for the eye controls, if the center eye control is above 0, - # use that value instead - if control_name in ("CTRL_L_eye", "CTRL_R_eye"): - center_value = eye_aim_override_values.get(control_name, {}).get(axis) - if center_value is not None: - if abs(center_value) > FLOATING_POINT_PRECISION: - value = center_value - elif center_eye_control: - center_value = getattr(center_eye_control.location, axis) - if abs(center_value) > FLOATING_POINT_PRECISION: - value = center_value - + # special case for the eye controls: the eye aim and the master center eye + # control override the individual L/R eye controls. + center_value = getattr(center_eye_control.location, axis) if center_eye_control else None + value = self._resolve_eye_control_value( + value, control_name, axis, eye_aim_override_values, center_value + ) head_instance.setGUIControl(index, value) else: missing_gui_controls.append(control_name) diff --git a/src/addons/character_dna/utilities/action.py b/src/addons/character_dna/utilities/action.py index 7c8eeb0..a693d02 100644 --- a/src/addons/character_dna/utilities/action.py +++ b/src/addons/character_dna/utilities/action.py @@ -531,7 +531,7 @@ def bake_control_curve_values_for_frame( # noqa: PLR0912 if not channel_bag: return - for fcurve in channel_bag.fcurves: + for fcurve in channel_bag.fcurves: # pyright: ignore[reportAttributeAccessIssue] control_curve_name, transform = fcurve.data_path.split('"].') if transform == "location" and fcurve.array_index != 2: control_curve_name = control_curve_name.replace('pose.bones["', "") @@ -540,6 +540,16 @@ def bake_control_curve_values_for_frame( # noqa: PLR0912 control_curve_values[control_curve_name] = control_curve_values.get(control_curve_name, {}) control_curve_values[control_curve_name].update({axis: fcurve.evaluate(frame)}) + # When the eye aim is active the eye control values are derived from the posed aim target + # geometry (aim target and eye bone positions), so step the scene to this frame first so + # those reads reflect the frame being baked. This is gated on the head component and the eye + # aim actually being active to avoid a scene update on every baked frame. + if component == "head" and instance.face_board and bpy.context.scene: + look_at_value = control_curve_values.get("CTRL_lookAtSwitch", {}).get("y") + eye_aim_active = look_at_value >= 0.99 if look_at_value is not None else instance.head_use_eye_aim + if eye_aim_active: + bpy.context.scene.frame_set(frame) + # set and update the control curve values based on the fcurve values instance.update_head_gui_control_values(override_values=control_curve_values) @@ -872,6 +882,7 @@ def bake_face_board_to_action( bpy.context.window_manager, ToolInfo.NAME ) window_manager_properties.evaluate_dependency_graph = False + original_frame = bpy.context.scene.frame_current if bpy.context.scene else None # create or replace the target action for bone keyframes if replace_action: @@ -931,6 +942,10 @@ def bake_face_board_to_action( clean_curves=clean_curves, ) + # restore the original frame if the eye aim baking stepped the scene + if original_frame is not None and bpy.context.scene and bpy.context.scene.frame_current != original_frame: + bpy.context.scene.frame_set(original_frame) + window_manager_properties.evaluate_dependency_graph = True diff --git a/src/addons/character_dna/utilities/misc.py b/src/addons/character_dna/utilities/misc.py index ef0f907..8a29b4e 100644 --- a/src/addons/character_dna/utilities/misc.py +++ b/src/addons/character_dna/utilities/misc.py @@ -289,9 +289,24 @@ def get_addon_version() -> str: return addon_version -def setup_scene(*_: Any) -> None: +def notify_rig_instances_changed(instance: "RigInstance | None" = None) -> None: + """Fire the registered post-setup callbacks after the rig instance list changes. + + Call this whenever a rig instance is added or removed at runtime (import, duplicate, + delete) so external integrations -- e.g. the Character Control Rig addon -- can resync + their own state. ``instance`` is the affected rig instance for additions, or ``None`` for + removals (where the callback should simply re-derive its state from the current list). + """ from .. import post_setup_scene_callbacks + for callback in post_setup_scene_callbacks: + try: + callback(instance) + except Exception as error: + logger.exception(f"Error in post_setup_scene_callbacks: {error}") + + +def setup_scene(*_: Any) -> None: # Auto-migrate rig-instance data saved by a different addon edition (Free vs # Pro) or an older version before initializing, so reopening a .blend always # yields a correctly populated rig-instance list. Guard against failures so a @@ -310,11 +325,7 @@ def setup_scene(*_: Any) -> None: # notify any registered callbacks that a rig instance has been set up in the scene, so they can perform # any necessary actions - for callback in post_setup_scene_callbacks: - try: - callback(instance) - except Exception as error: - logger.exception(f"Error in post_setup_scene_callbacks: {error}") + notify_rig_instances_changed(instance) start_listening() diff --git a/uv.lock b/uv.lock index e47d855..231c58b 100644 --- a/uv.lock +++ b/uv.lock @@ -193,7 +193,7 @@ wheels = [ [[package]] name = "character-dna-addon" -version = "0.9.10" +version = "0.9.12" source = { editable = "." } dependencies = [ { name = "sentry-sdk" }, From 41f89f38ac17116f3a4a82714f883414e463aefe Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Tue, 14 Jul 2026 12:22:48 -0500 Subject: [PATCH 3/4] fixed naming conventions when baking shape keys --- docs/faq.md | 3 ++ pyproject.toml | 2 +- src/addons/character_dna/__init__.py | 2 +- .../character_dna/blender_manifest.toml | 2 +- src/addons/character_dna/properties.py | 6 +-- src/addons/character_dna/release_notes.md | 4 ++ src/addons/character_dna/utilities/action.py | 40 +++++++++++++++---- tests/test_animation.py | 25 ++++++------ uv.lock | 2 +- 9 files changed, 59 insertions(+), 27 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index f80b4a0..b004d20 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -19,6 +19,9 @@ Check these in order: Live RigLogic evaluation doesn't run before each frame when rendering. **Bake** the face board animation to keyframes first so the pose bones, shape keys, and mask values are stored on the timeline. See [Animation](./free-features/animation.md). +!!! note + It is now possible to disable `Batched Evaluations` in the addon preferences. This is currently experimental. Please report any issues with evaluation in the scene or while rendering with this option on. + ## Still stuck? Browse or search existing reports and open a new issue here: diff --git a/pyproject.toml b/pyproject.toml index b0fa1ea..ed31c5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = ["blender", "metahuman", "dna", "riglogic", "animation", "3d"] name = "character-dna-addon" readme = "README.md" requires-python = ">=3.11,<3.14" -version = "0.9.12" +version = "0.10.1" [project.optional-dependencies] dev = [ diff --git a/src/addons/character_dna/__init__.py b/src/addons/character_dna/__init__.py index f1e46c8..0df6723 100644 --- a/src/addons/character_dna/__init__.py +++ b/src/addons/character_dna/__init__.py @@ -17,7 +17,7 @@ bl_info = { "name": "Character DNA", "author": "Poly Hammer", - "version": (0, 9, 12), + "version": (0, 10, 1), "blender": (4, 5, 0), "location": "File > Import > MetaHuman DNA", "description": ( diff --git a/src/addons/character_dna/blender_manifest.toml b/src/addons/character_dna/blender_manifest.toml index e9c7013..7f04257 100644 --- a/src/addons/character_dna/blender_manifest.toml +++ b/src/addons/character_dna/blender_manifest.toml @@ -11,7 +11,7 @@ schema_version = "1.0.0" id = "character_dna" name = "Character DNA" tagline = "Customize MetaHuman DNA files with RigLogic evaluation support" -version = "0.9.12" +version = "0.10.1" type = "add-on" # ============================================================================= diff --git a/src/addons/character_dna/properties.py b/src/addons/character_dna/properties.py index d5e095a..2179a0b 100644 --- a/src/addons/character_dna/properties.py +++ b/src/addons/character_dna/properties.py @@ -332,10 +332,8 @@ class CharacterAddonProperties: name="Batched Evaluations", default=True, description=( - "Batch all rig evaluations onto a the next draw call so all updates from a single scene change are " - "applied together. This is optimized for viewport better performance (FPS) and stability. Disable this to " - "evaluate the rig immediately when the scene updates, this can remove jittering when rendering but can " - "cause instability and crashes." + "Batch all rig evaluations onto the next draw call so all updates from a single scene change are " + "applied together. Disable this to evaluate each rig immediately when the scene's dependency graph updates" ), ) # pyright: ignore[reportInvalidTypeForm] show_pro_features: bpy.props.BoolProperty( diff --git a/src/addons/character_dna/release_notes.md b/src/addons/character_dna/release_notes.md index e489497..5bd7811 100644 --- a/src/addons/character_dna/release_notes.md +++ b/src/addons/character_dna/release_notes.md @@ -1,3 +1,7 @@ +## Minor Changes + +* It is now possible to optionally disable `Batched Evaluations` in the addon preferences. This is currently experimental. This removes the need to bake before rendering. Please report any issues with evaluation in the scene or while rendering with this option on. + ## Patch Changes * Added experimental option to turn off batched evaluations diff --git a/src/addons/character_dna/utilities/action.py b/src/addons/character_dna/utilities/action.py index a693d02..ebd7ce3 100644 --- a/src/addons/character_dna/utilities/action.py +++ b/src/addons/character_dna/utilities/action.py @@ -736,17 +736,25 @@ def _bulk_write_scalar_keyframes( fcurve.update() -def flush_shape_key_keyframes_to_action( +def flush_shape_key_keyframes_to_action( # noqa: PLR0912 buffer: dict[bpy.types.ShapeKey, list[tuple[float, float]]], + action_name: str, + instance_name: str = "", clean_curves: bool = True, ) -> None: """Bulk-write accumulated shape key keyframes using foreach_set for performance. Groups shape keys by their owning Key data-block and writes all fcurves - in batch rather than using per-frame keyframe_insert calls. + in batch rather than using per-frame keyframe_insert calls. Each mesh's Key + data-block gets its own action named ``__shape_keys``, + where the mesh name is the Key data-block's name (which the importer sets to the + mesh object name) with the ``_`` prefix stripped so the instance + name is not duplicated. Args: buffer: Maps ShapeKey references to their accumulated (frame, value) keyframes. + action_name: The base baked action name used as the shape-key action name prefix. + instance_name: The rig instance name, stripped from the mesh name prefix when present. clean_curves: Whether to clean redundant keyframes from curves after writing. """ if not buffer: @@ -769,10 +777,20 @@ def flush_shape_key_keyframes_to_action( if not key_data.animation_data: continue + # the Key data-block is named after its mesh object (see DNAImporter.set_shape_key); + # strip the instance prefix so the action name isn't "_...__mesh...". + mesh_name = key_data.name + if instance_name and mesh_name.startswith(f"{instance_name}_"): + mesh_name = mesh_name[len(instance_name) + 1 :] + shape_key_action_name = f"{action_name}_{mesh_name}_shape_keys" + action = key_data.animation_data.action if not action: - action = bpy.data.actions.new(name=f"{key_data.name}Action") + action = bpy.data.actions.new(name=shape_key_action_name) key_data.animation_data.action = action + if not anim_utils: + # blender 4.5: animation_data.action assignment does not set id_root + action.id_root = "KEY" if anim_utils: if len(action.slots) == 0: @@ -790,6 +808,9 @@ def flush_shape_key_keyframes_to_action( data_path = shape_key.path_from_id("value") _bulk_write_scalar_keyframes(channel_bag, data_path, keyframes, clean_curves) + # ensure the name follows the convention even when reusing an existing action + action.name = shape_key_action_name + def flush_texture_mask_keyframes_to_action( texture_logic_node: bpy.types.ShaderNodeGroup, @@ -826,7 +847,7 @@ def flush_texture_mask_keyframes_to_action( node_tree.animation_data.action = action if not anim_utils: # blender 4.5: animation_data.action assignment does not set id_root - action.id_root = "NODETREE" + action.id_root = "NODETREE" # pyright: ignore[reportAttributeAccessIssue] if anim_utils: if len(action.slots) == 0: @@ -896,7 +917,7 @@ def bake_face_board_to_action( elif not anim_utils: # blender 4.5: animation_data.action assignment does not set id_root, # so set it explicitly so downstream code/tests can filter by it. - target_action.id_root = "OBJECT" + target_action.id_root = "OBJECT" # pyright: ignore[reportAttributeAccessIssue] # assign the new action to the armature if not armature_object.animation_data: @@ -932,13 +953,18 @@ def bake_face_board_to_action( flush_bone_keyframes_to_action(target_action, bone_keyframe_buffer, clean_curves=clean_curves) if shape_keys: - flush_shape_key_keyframes_to_action(shape_key_buffer, clean_curves=clean_curves) + flush_shape_key_keyframes_to_action( + shape_key_buffer, + action_name=action_name, + instance_name=instance.name, + clean_curves=clean_curves, + ) if texture_logic_node and masks: flush_texture_mask_keyframes_to_action( texture_logic_node, mask_buffer, - action_name=f"{action_name}_shader", + action_name=f"{action_name}_texture_logic_node", clean_curves=clean_curves, ) diff --git a/tests/test_animation.py b/tests/test_animation.py index 24241ce..d017518 100644 --- a/tests/test_animation.py +++ b/tests/test_animation.py @@ -87,9 +87,9 @@ def test_bake_component_animation( if not replace_action: assert len(new_object_actions) == 1, "A new action should be created when not replacing an existing action." - assert ( - new_object_actions.pop() == f"{instance.name}_{component}_{action_name}" - ), "The baked action name is not as expected." + assert new_object_actions.pop() == f"{instance.name}_{component}_{action_name}", ( + "The baked action name is not as expected." + ) @pytest.mark.parametrize( @@ -117,7 +117,8 @@ def test_bake_face_board_animation( previous_node_tree_action_names = [ a.name for a in bpy.data.actions - if a.slots[0].target_id_type == "NODETREE" and a.name != f"{instance.name}_head_{action_name}_shader" + if a.slots[0].target_id_type == "NODETREE" + and a.name != f"{instance.name}_head_{action_name}_texture_logic_node" ] else: previous_object_action_names = [ @@ -128,7 +129,7 @@ def test_bake_face_board_animation( previous_node_tree_action_names = [ a.name for a in bpy.data.actions - if a.id_root == "NODETREE" and a.name != f"{instance.name}_head_{action_name}_shader" + if a.id_root == "NODETREE" and a.name != f"{instance.name}_head_{action_name}_texture_logic_node" ] bpy.ops.character_dna.bake_face_board_animation( @@ -153,13 +154,13 @@ def test_bake_face_board_animation( if not replace_action: assert len(new_object_actions) == 1, "A new action should be created when not replacing an existing action." - assert ( - new_object_actions.pop() == f"{instance.name}_head_{action_name}" - ), "The baked action name is not as expected." + assert new_object_actions.pop() == f"{instance.name}_head_{action_name}", ( + "The baked action name is not as expected." + ) - assert ( - len(new_node_tree_action_names) == 1 - ), "A new node tree action should always be created for face board baking." + assert len(new_node_tree_action_names) == 1, ( + "A new node tree action should always be created for face board baking." + ) assert any( - name == f"{instance.name}_head_{action_name}_shader" for name in expected_node_tree_action_names + name == f"{instance.name}_head_{action_name}_texture_logic_node" for name in expected_node_tree_action_names ), "The baked node tree action name is not as expected." diff --git a/uv.lock b/uv.lock index 231c58b..e6a55f0 100644 --- a/uv.lock +++ b/uv.lock @@ -193,7 +193,7 @@ wheels = [ [[package]] name = "character-dna-addon" -version = "0.9.12" +version = "0.10.1" source = { editable = "." } dependencies = [ { name = "sentry-sdk" }, From ccab172474d6aeba2f69fa8790240d3e4df36af6 Mon Sep 17 00:00:00 2001 From: Jack Yao Date: Tue, 14 Jul 2026 19:35:32 -0500 Subject: [PATCH 4/4] Added blender 5.2 support --- .github/workflows/on-pr.yaml | 26 +++++------ .github/workflows/release.yaml | 26 +++++------ .vscode/tasks.json | 10 +++-- docs/contributing/development.md | 2 +- pyproject.toml | 8 ++-- src/addons/character_dna/__init__.py | 2 +- .../character_dna/blender_manifest.toml | 4 +- src/addons/character_dna/editors | 2 +- src/addons/character_dna/release_notes.md | 4 +- .../character_dna/utilities/material.py | 5 ++- .../test_transfer_shape_from_selected_mesh.py | 28 ++++++++++++ uv.lock | 45 ++++++++++++++----- 12 files changed, 111 insertions(+), 51 deletions(-) diff --git a/.github/workflows/on-pr.yaml b/.github/workflows/on-pr.yaml index dcd85fd..42dd8a7 100644 --- a/.github/workflows/on-pr.yaml +++ b/.github/workflows/on-pr.yaml @@ -62,40 +62,40 @@ jobs: base-branch: ${{ github.head_ref }} # Test with Blender 5.1 - test-blender-5-1-ubuntu-latest: + test-blender-5-2-ubuntu-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "ubuntu-internal" addon-id: "character_dna" base: "character-dna" base-branch: ${{ github.head_ref }} - test-blender-5-1-windows-latest: + test-blender-5-2-windows-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "windows-latest" addon-id: "character_dna" base: "character-dna" base-branch: ${{ github.head_ref }} - test-blender-5-1-macos-latest: + test-blender-5-2-macos-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "macos-latest" addon-id: "character_dna" base: "character-dna" @@ -116,14 +116,14 @@ jobs: is-push: ${{ github.event_name == 'push' }} is-pull-request: ${{ github.event_name == 'pull_request' }} - benchmark-blender-5-1: - needs: [test-blender-5-1-ubuntu-latest, test-blender-5-1-windows-latest, test-blender-5-1-macos-latest] - name: Blender 5.1 Benchmark + benchmark-blender-5-2: + needs: [test-blender-5-2-ubuntu-latest, test-blender-5-2-windows-latest, test-blender-5-2-macos-latest] + name: Blender 5.2 Benchmark uses: poly-hammer/character-dna-addon/.github/workflows/re-use-benchmark.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "ubuntu-internal" iterations: 50 warmup: 10 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index df55cba..6d1d96b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -64,39 +64,39 @@ jobs: addon-id: "character_dna" base: "character-dna" - # Test with Blender 5.1 - test-blender-5-1-ubuntu-latest: + # Test with Blender 5.2 + test-blender-5-2-ubuntu-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "ubuntu-internal" addon-id: "character_dna" base: "character-dna" - test-blender-5-1-windows-latest: + test-blender-5-2-windows-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "windows-latest" addon-id: "character_dna" base: "character-dna" - test-blender-5-1-macos-latest: + test-blender-5-2-macos-latest: needs: [lint] - name: Blender 5.1 Tests + name: Blender 5.2 Tests uses: poly-hammer/poly-hammer-workflows/.github/workflows/re-use-blender-tests.yaml@main secrets: inherit with: python-version: "3.13" - blender-version: "5.1" + blender-version: "5.2" runner-os: "macos-latest" addon-id: "character_dna" base: "character-dna" @@ -104,9 +104,9 @@ jobs: release: needs: [ - test-blender-5-1-ubuntu-latest, - test-blender-5-1-windows-latest, - test-blender-5-1-macos-latest, + test-blender-5-2-ubuntu-latest, + test-blender-5-2-windows-latest, + test-blender-5-2-macos-latest, # test-blender-4-5-ubuntu-latest, test-blender-4-5-windows-latest, test-blender-4-5-macos-latest, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 070899a..331bf32 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -157,21 +157,23 @@ { "id": "blender-version", "type": "pickString", - "default": "4.5", + "default": "5.2", "options": [ "4.5", "5.0", - "5.1" + "5.1", + "5.2" ], "description": "What blender version to launch" }, { "id": "unreal-version", "type": "pickString", - "default": "5.6", + "default": "5.8", "options": [ "5.6", - "5.7" + "5.7", + "5.8" ], "description": "What unreal version to launch" }, diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 9ada544..4895c3d 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -18,7 +18,7 @@ Contributions are welcome! Please create an issue to discuss significant work be - [Python 3.13](https://www.python.org/downloads/release/python-31314/) - [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python package manager) - [Git](https://git-scm.com/download/win) with Git LFS -- [Blender 5.1+](https://www.blender.org/download/) +- [Blender 5.2+](https://www.blender.org/download/) ## Setup diff --git a/pyproject.toml b/pyproject.toml index ed31c5f..3a87bcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,12 +24,13 @@ keywords = ["blender", "metahuman", "dna", "riglogic", "animation", "3d"] name = "character-dna-addon" readme = "README.md" requires-python = ">=3.11,<3.14" -version = "0.10.1" +version = "0.10.3" [project.optional-dependencies] dev = [ "debugpy>=1.8.13", - "bpy>=4.5.1", + "bpy>=4.5.1,<5.2; python_version < '3.13'", + "bpy>=5.2; python_version >= '3.13'", "pytest>=8.3.3", "pytest-dotenv>=0.5.2", "pytest-cov>=6.0.0", @@ -399,7 +400,8 @@ max-statements = 60 [dependency-groups] dev = [ "debugpy>=1.8.13", - "bpy>=4.5.1", + "bpy>=4.5.1,<5.2; python_version < '3.13'", + "bpy>=5.2; python_version >= '3.13'", "pytest>=8.3.3", "pytest-dotenv>=0.5.2", "pytest-cov>=6.0.0", diff --git a/src/addons/character_dna/__init__.py b/src/addons/character_dna/__init__.py index 0df6723..a8168a1 100644 --- a/src/addons/character_dna/__init__.py +++ b/src/addons/character_dna/__init__.py @@ -17,7 +17,7 @@ bl_info = { "name": "Character DNA", "author": "Poly Hammer", - "version": (0, 10, 1), + "version": (0, 10, 3), "blender": (4, 5, 0), "location": "File > Import > MetaHuman DNA", "description": ( diff --git a/src/addons/character_dna/blender_manifest.toml b/src/addons/character_dna/blender_manifest.toml index 7f04257..2e4d790 100644 --- a/src/addons/character_dna/blender_manifest.toml +++ b/src/addons/character_dna/blender_manifest.toml @@ -11,7 +11,7 @@ schema_version = "1.0.0" id = "character_dna" name = "Character DNA" tagline = "Customize MetaHuman DNA files with RigLogic evaluation support" -version = "0.10.1" +version = "0.10.3" type = "add-on" # ============================================================================= @@ -26,7 +26,7 @@ website = "https://polyhammer.com" # Minimum Blender version required blender_version_min = "4.5.0" # Maximum Blender version supported -blender_version_max = "5.2.0" +blender_version_max = "5.3.0" # ============================================================================= # License Information diff --git a/src/addons/character_dna/editors b/src/addons/character_dna/editors index 62135e2..760cf20 160000 --- a/src/addons/character_dna/editors +++ b/src/addons/character_dna/editors @@ -1 +1 @@ -Subproject commit 62135e29697a4427c6ae3b90311592abb4622b00 +Subproject commit 760cf2098ec90ee70ef7718d9bd2e247450b90f2 diff --git a/src/addons/character_dna/release_notes.md b/src/addons/character_dna/release_notes.md index 5bd7811..e2f91af 100644 --- a/src/addons/character_dna/release_notes.md +++ b/src/addons/character_dna/release_notes.md @@ -1,5 +1,7 @@ ## Minor Changes +* Added Blender `5.2` support + * It is now possible to optionally disable `Batched Evaluations` in the addon preferences. This is currently experimental. This removes the need to bake before rendering. Please report any issues with evaluation in the scene or while rendering with this option on. ## Patch Changes @@ -10,5 +12,5 @@ ## Tests Passing On -* Blender `4.5`, `5.1` (installed from blender.org) +* Blender `4.5`, `5.1`, `5.2` (installed from blender.org) * Unreal `5.6`, `5.7`, `5.8` diff --git a/src/addons/character_dna/utilities/material.py b/src/addons/character_dna/utilities/material.py index 29b571c..06561fe 100644 --- a/src/addons/character_dna/utilities/material.py +++ b/src/addons/character_dna/utilities/material.py @@ -73,8 +73,9 @@ def create_new_material( name: str, color: tuple[float, float, float, float] | None = None, alpha: float | None = None ) -> bpy.types.Material: material = bpy.data.materials.new(name=name) - if hasattr(material, "use_nodes"): - material.use_nodes = True + # `Material.use_nodes` is deprecated (removed in Blender 6.0); every material + # already has a node tree by default on the addon's supported Blender range + # (4.5+), so no explicit opt-in is needed. if not material.node_tree: logger.error(f"Material {name} has no node tree.") return material diff --git a/tests/raw_control_editor/test_transfer_shape_from_selected_mesh.py b/tests/raw_control_editor/test_transfer_shape_from_selected_mesh.py index d95cca7..032be89 100644 --- a/tests/raw_control_editor/test_transfer_shape_from_selected_mesh.py +++ b/tests/raw_control_editor/test_transfer_shape_from_selected_mesh.py @@ -221,3 +221,31 @@ def test_resolve_donor_errors_when_several_and_none_active() -> None: with pytest.raises(RawControlEditorError, match="select only one"): resolve_donor_mesh(instance, editor) + + +def test_resolve_donor_allows_deformed_head_mesh() -> None: + """The rig's live head mesh is re-allowed as a donor so its deformed + shape can be copied onto the target, even though it is also listed as + the ``head_lod0_mesh`` output item.""" + head_mesh = _flat_quad("head_lod0_mesh") + target_mesh = _flat_quad("target_quad") + # The head mesh is registered both as ``head_mesh`` and as its output item. + instance = _FakeInstance(head=[_Row(head_mesh)], body=[], head_mesh=head_mesh) + editor = _FakeEditor(target_rows=[_Row(target_mesh)]) + _select_only(head_mesh, active=head_mesh) + + assert selected_donor_mesh_candidates(instance, editor) == [head_mesh] + assert resolve_donor_mesh(instance, editor) is head_mesh + + +def test_head_mesh_excluded_when_it_is_also_the_target() -> None: + """If the head mesh is itself the active target row it stays excluded + -- a mesh cannot be its own donor.""" + head_mesh = _flat_quad("head_lod0_mesh") + instance = _FakeInstance(head=[_Row(head_mesh)], body=[], head_mesh=head_mesh) + editor = _FakeEditor(target_rows=[_Row(head_mesh)]) + _select_only(head_mesh, active=head_mesh) + + assert selected_donor_mesh_candidates(instance, editor) == [] + with pytest.raises(RawControlEditorError, match="Select one donor mesh"): + resolve_donor_mesh(instance, editor) diff --git a/uv.lock b/uv.lock index e6a55f0..fd7040a 100644 --- a/uv.lock +++ b/uv.lock @@ -27,6 +27,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "boto3" version = "1.42.81" @@ -78,23 +87,24 @@ wheels = [ [[package]] name = "bpy" -version = "5.1.0" +version = "5.2.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.13' and platform_machine != 's390x'", "python_full_version >= '3.13' and platform_machine == 's390x'", ] dependencies = [ + { name = "cattrs", marker = "python_full_version >= '3.13'" }, { name = "cython", marker = "python_full_version >= '3.13'" }, { name = "numpy", version = "2.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "requests", marker = "python_full_version >= '3.13'" }, { name = "zstandard", marker = "python_full_version >= '3.13'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/58/e311ab21cd4347685035cb43def43e9aff32e870f65da791bb8ed666b8d7/bpy-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79cb379d32432031b31aaa740b2af1cbd6fab14ba3aa25db1500b19ea332bf8a", size = 238523944, upload-time = "2026-03-17T14:44:24.527Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b9/bcd380708380baf759bd9c422f370294ab750535c869f809b447e7836917/bpy-5.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5b557f65902e4c84caf8b413d9bc6aceb660a5ca934e65f6c16e4ee91dc4cd4f", size = 390716200, upload-time = "2026-03-17T14:45:55.547Z" }, - { url = "https://files.pythonhosted.org/packages/f3/65/a75513cbdc117238bd2dcf1fe655e05fca379577087e03144cca6b6718a5/bpy-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:6ee14d8b28989b911b2e921b4592d1801738d550fedbcc57f773dae589f41bb2", size = 350105048, upload-time = "2026-03-17T14:45:25.828Z" }, - { url = "https://files.pythonhosted.org/packages/76/55/970c78b4046b3020bee271f17e1cd769f4edfcd9c29d8ab307e908eeddf3/bpy-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:7dbd29e684dd6fa6acdc11c96bdbfb43ca95576a6b00ece413d3f80b8717fc08", size = 205493452, upload-time = "2026-03-17T14:44:51.573Z" }, + { url = "https://files.pythonhosted.org/packages/85/3a/ebdfeedbefe18641ac541c44bbe433fa363d377fedbabf4fbabb571faf5e/bpy-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b5f9d4e02de90be7100494807a021b60c194c5d5315daef089213b2cdc8cd9d", size = 245126219, upload-time = "2026-07-14T12:14:33.631Z" }, + { url = "https://files.pythonhosted.org/packages/53/74/827c0930eec8d1b74b8c67d4066bf922f1e15a3d80f0f50ed4a48a2692b1/bpy-5.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:daae68954f1ab33996aa582861323eff815210e5c56641ac62f855a8365201c2", size = 401306561, upload-time = "2026-07-14T12:13:25.07Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d5/dd33167ec5540244b170a9f6ee1436e453ee89f0914401831268f69ea0bd/bpy-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:d657ee262bb01217343d19156c869819441b2ec436a24b302de8f1fa3a0ef1eb", size = 338722862, upload-time = "2026-07-14T12:14:14.093Z" }, + { url = "https://files.pythonhosted.org/packages/82/cb/a8d985a59e8e31cf55f6b62144305379a4c3d795633186933059bc91c16a/bpy-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:925ae162a1bcde29a514804a5ef3afc5516fccc5ce568e7c8dee95d66a06215a", size = 210627206, upload-time = "2026-07-14T12:13:44.824Z" }, ] [[package]] @@ -125,6 +135,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/e0/5011747466414c12cac8a8df77aa235068669a6a5a5df301a96209db6054/cairosvg-2.9.0-py3-none-any.whl", hash = "sha256:4b82d07d145377dffdfc19d9791bd5fb65539bb4da0adecf0bdbd9cd4ffd7c68", size = 45962, upload-time = "2026-03-14T13:56:33.512Z" }, ] +[[package]] +name = "cattrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs", marker = "python_full_version >= '3.13'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a0/ec/ba18945e7d6e55a58364d9fb2e46049c1c2998b3d805f19b703f14e81057/cattrs-26.1.0.tar.gz", hash = "sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40", size = 495672, upload-time = "2026-02-18T22:15:19.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/56/60547f7801b97c67e97491dc3d9ade9fbccbd0325058fd3dfcb2f5d98d90/cattrs-26.1.0-py3-none-any.whl", hash = "sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096", size = 73054, upload-time = "2026-02-18T22:15:17.958Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -193,7 +216,7 @@ wheels = [ [[package]] name = "character-dna-addon" -version = "0.10.1" +version = "0.10.3" source = { editable = "." } dependencies = [ { name = "sentry-sdk" }, @@ -202,7 +225,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "bpy", version = "5.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "bpy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "bpy", version = "5.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "debugpy" }, { name = "genbadge", extra = ["all"] }, { name = "mkdocs" }, @@ -221,7 +244,7 @@ stubs = [ [package.dev-dependencies] dev = [ { name = "bpy", version = "5.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "bpy", version = "5.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "bpy", version = "5.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, { name = "cairosvg" }, { name = "debugpy" }, { name = "genbadge", extra = ["all"] }, @@ -238,7 +261,8 @@ dev = [ [package.metadata] requires-dist = [ - { name = "bpy", marker = "extra == 'dev'", specifier = ">=4.5.1" }, + { name = "bpy", marker = "python_full_version >= '3.13' and extra == 'dev'", specifier = ">=5.2" }, + { name = "bpy", marker = "python_full_version < '3.13' and extra == 'dev'", specifier = ">=4.5.1,<5.2" }, { name = "debugpy", marker = "extra == 'dev'", specifier = ">=1.8.13" }, { name = "fake-bpy-module-latest", marker = "extra == 'stubs'" }, { name = "genbadge", extras = ["all"], marker = "extra == 'dev'", specifier = ">=1.1.2" }, @@ -256,7 +280,8 @@ provides-extras = ["dev", "stubs"] [package.metadata.requires-dev] dev = [ - { name = "bpy", specifier = ">=4.5.1" }, + { name = "bpy", marker = "python_full_version < '3.13'", specifier = ">=4.5.1,<5.2" }, + { name = "bpy", marker = "python_full_version >= '3.13'", specifier = ">=5.2" }, { name = "cairosvg", specifier = ">=2.9.0" }, { name = "debugpy", specifier = ">=1.8.13" }, { name = "genbadge", extras = ["all"], specifier = ">=1.1.2" },