Skip to content
This repository was archived by the owner on Apr 9, 2026. It is now read-only.

Commit 25a23ba

Browse files
committed
v0.3.3 - Fixed Keying & Onion issues
- The addon will now assert that the active object should be of mesh type. If not, the appropriate operators will be inactive - Fixed issue that artists were not able to override keyed value in an existing keyframe - Fixed #9 that the `Initialize Frame Handler` had to be manually invoked by artists periodically to avoid issues. This has been fixed by calling the frame handler every 180 seconds, and also after a file is loaded - The panel will now display a warning if there are no active objects and also when the active object is not of MESH type - The active object's validity is now checked properly for all operators that depends on it.
1 parent 3e91384 commit 25a23ba

11 files changed

Lines changed: 82 additions & 56 deletions

stopmagic/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from typing import List, Tuple
2020
import bpy
21+
import threading
2122
import re
2223
import os.path
2324

@@ -28,7 +29,7 @@
2829
bl_info = {
2930
"name": "Stopmagic",
3031
"author": "Aldrin Mathew",
31-
"version": (0, 3, 2),
32+
"version": (0, 3, 3),
3233
"blender": (2, 91, 0),
3334
"location": "Sidebar > Stopmagic",
3435
"warning": "beta",
@@ -41,6 +42,17 @@
4142
addon_keymaps: List[Tuple[bpy.types.KeyMap, bpy.types.KeyMapItem]] = []
4243

4344

45+
@bpy.app.handlers.persistent
46+
def periodic_handler(_):
47+
functions.frame_handler(None)
48+
threading.Timer(
49+
interval=180,
50+
function=functions.frame_handler,
51+
args=[None],
52+
kwargs=None,
53+
)
54+
55+
4456
def register() -> None:
4557
global addon_keymaps
4658
icons.register()
@@ -56,7 +68,7 @@ def register() -> None:
5668
operators.keyed_frame_previous.register()
5769
operators.upgrade_addon.register()
5870
operators.contributions.register()
59-
bpy.app.handlers.load_post.append(functions.frame_handler)
71+
bpy.app.handlers.load_post.append(periodic_handler)
6072
bpy.app.handlers.frame_change_post.clear()
6173
bpy.app.handlers.frame_change_post.append(functions.update_stopmagic)
6274
bpy.app.handlers.frame_change_pre.clear()

stopmagic/functions/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from .get_object_keyframes import *
1111
from .get_object_key_values import *
1212
from .handle_onion_constraints import *
13+
from .is_candidate_object import *

stopmagic/functions/handle_onion_skin.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def handle_collections(past: bpy.types.Object, future: bpy.types.Object) -> None
2121
sm_collection.hide_select = True
2222

2323

24+
@bpy.app.handlers.persistent
2425
def handle_onion_skin(dummy):
2526
"""Automatically handles Onion Skin objects and populates them with mesh data"""
2627
if bpy.context.view_layer.objects.active is not None:
@@ -284,14 +285,15 @@ def clear_onion_data():
284285
bpy.data.materials.remove(pmaterial, do_unlink=True)
285286
if fmaterial is not None:
286287
bpy.data.materials.remove(fmaterial, do_unlink=True)
287-
if collection.get("objects") is not None:
288-
for obj in collection.objects:
289-
mesh = obj.data
290-
obj.name = "removed"
291-
bpy.context.scene.objects.unlink(obj)
292-
bpy.data.meshes.remove(mesh, do_unlink=True)
293-
bpy.data.objects.remove(obj, do_unlink=True)
294-
bpy.data.collections.remove(collection)
295-
bpy.ops.outliner.orphans_purge(
296-
do_local_ids=True, do_linked_ids=False, do_recursive=False
297-
)
288+
if collection is not None:
289+
if collection.get("objects") is not None:
290+
for obj in collection.objects:
291+
mesh = obj.data
292+
obj.name = "removed"
293+
bpy.context.scene.objects.unlink(obj)
294+
bpy.data.meshes.remove(mesh, do_unlink=True)
295+
bpy.data.objects.remove(obj, do_unlink=True)
296+
bpy.data.collections.remove(collection)
297+
bpy.ops.outliner.orphans_purge(
298+
do_local_ids=True, do_linked_ids=False, do_recursive=False
299+
)

stopmagic/functions/insert_mesh_keyframe.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,35 @@ def insert_mesh_keyframe(obj: bpy.types.Object | Any) -> None:
4040

4141

4242
def insert_mesh_keyframe_ex(obj: bpy.types.Object) -> bool:
43-
if obj.get("sm_id") is None:
44-
obj["sm_id"] = new_object_id()
45-
#
46-
object_sm_id = obj["sm_id"]
47-
ob_name_full = obj.name_full
48-
mesh_index = get_next_mesh_index(obj)
49-
mesh_name = ob_name_full + "_sm_" + str(mesh_index)
50-
del_mesh = []
51-
new_mesh = bpy.data.meshes.new_from_object(obj)
52-
new_mesh.name = mesh_name
53-
new_mesh["sm_id"] = object_sm_id
54-
new_mesh["sm_datablock"] = mesh_index
55-
obj.data = new_mesh
56-
obj.data.use_fake_user = True
57-
obj["sm_datablock"] = mesh_index
58-
obj.keyframe_insert(
59-
data_path='["sm_datablock"]', frame=bpy.context.scene.frame_current
60-
)
61-
for mesh in del_mesh:
62-
mesh.use_fake_user = False
63-
update_stopmagic(bpy.context.scene)
64-
for mesh in del_mesh:
65-
if mesh.users == 0:
66-
bpy.data.meshes.remove(mesh)
67-
#
68-
return True
43+
try:
44+
if obj.get("sm_id") is None:
45+
obj["sm_id"] = new_object_id()
46+
#
47+
object_sm_id = obj["sm_id"]
48+
ob_name_full = obj.name_full
49+
mesh_index = get_next_mesh_index(obj)
50+
mesh_name = ob_name_full + "_sm_" + str(mesh_index)
51+
del_mesh = []
52+
new_mesh = bpy.data.meshes.new_from_object(obj)
53+
new_mesh.name = mesh_name
54+
new_mesh["sm_id"] = object_sm_id
55+
new_mesh["sm_datablock"] = mesh_index
56+
obj.data = new_mesh
57+
obj.data.use_fake_user = True
58+
obj["sm_datablock"] = mesh_index
59+
obj.keyframe_insert(
60+
data_path='["sm_datablock"]', frame=bpy.context.scene.frame_current
61+
)
62+
for mesh in del_mesh:
63+
mesh.use_fake_user = False
64+
update_stopmagic(bpy.context.scene)
65+
for mesh in del_mesh:
66+
if mesh.users == 0:
67+
bpy.data.meshes.remove(mesh)
68+
#
69+
return True
70+
except:
71+
return False
6972

7073

7174
# Get the appropriate index for the mesh about to be created

stopmagic/functions/update_stopmagic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import bpy
22

33

4+
@bpy.app.handlers.persistent
45
def update_stopmagic(scene: bpy.types.Scene) -> None:
56
for object in scene.objects:
67
if object.get("sm_datablock") is None:

stopmagic/operators/add_mesh_keyframe.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import bpy
2-
from ..functions.insert_mesh_keyframe import *
2+
from stopmagic.functions import insert_mesh_keyframe, is_candidate_object
33

44

55
class AddMeshKeyframe(bpy.types.Operator):
@@ -11,7 +11,7 @@ class AddMeshKeyframe(bpy.types.Operator):
1111

1212
@classmethod
1313
def poll(cls, context):
14-
return context.view_layer.objects.active is not None
14+
return is_candidate_object(context)
1515

1616
def execute(self, context: bpy.types.Context):
1717
if bpy.context.view_layer.objects.active is not None:

stopmagic/operators/keyed_frame_next.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Set
33
import bpy
44

5-
from stopmagic.functions import get_object_keyframes
5+
from stopmagic.functions import get_object_keyframes, is_candidate_object
66

77

88
class KeyedFrameNext(bpy.types.Operator):
@@ -14,14 +14,15 @@ class KeyedFrameNext(bpy.types.Operator):
1414

1515
@classmethod
1616
def poll(cls, context: bpy.types.Context) -> bool:
17-
return context.view_layer.objects.active is not None
17+
return is_candidate_object(context)
1818

1919
def execute(self, context: bpy.types.Context) -> Set[int] | Set[str]:
20-
if bpy.context.view_layer.objects.active is not None:
20+
if context.view_layer.objects.active is not None:
2121
keyframes = get_object_keyframes(context.view_layer.objects.active)
2222
if len(keyframes) > 0:
23-
frame = bpy.context.scene.frame_current
24-
keyframes = [k for k in keyframes if k > frame]
23+
keyframes = [
24+
k for k in keyframes if k > bpy.context.scene.frame_current
25+
]
2526
if len(keyframes) > 0:
2627
lowest = keyframes[0]
2728
for num in keyframes:

stopmagic/operators/keyed_frame_previous.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import List, Set
33
import bpy
44

5-
from stopmagic.functions.get_object_keyframes import get_object_keyframes
5+
from stopmagic.functions import get_object_keyframes, is_candidate_object
66

77

88
class KeyedFramePrevious(bpy.types.Operator):
@@ -14,14 +14,15 @@ class KeyedFramePrevious(bpy.types.Operator):
1414

1515
@classmethod
1616
def poll(cls, context: bpy.types.Context) -> bool:
17-
return context.view_layer.objects.active is not None
17+
return is_candidate_object(context)
1818

1919
def execute(self, context: bpy.types.Context) -> Set[int] | Set[str]:
20-
if bpy.context.view_layer.objects.active is not None:
20+
if context.view_layer.objects.active is not None:
2121
keyframes = get_object_keyframes(context.view_layer.objects.active)
2222
if len(keyframes) > 0:
23-
frame = bpy.context.scene.frame_current
24-
keyframes = [k for k in keyframes if k < frame]
23+
keyframes = [
24+
k for k in keyframes if k < bpy.context.scene.frame_current
25+
]
2526
if len(keyframes) > 0:
2627
highest = keyframes[0]
2728
for num in keyframes:

stopmagic/operators/skip_frame_backward.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22
from typing import Set
33
import bpy
4-
from stopmagic.functions.insert_mesh_keyframe import insert_mesh_keyframe
4+
from stopmagic.functions import insert_mesh_keyframe, is_candidate_object
55

66

77
class SkipFrameBackward(bpy.types.Operator):
@@ -13,7 +13,7 @@ class SkipFrameBackward(bpy.types.Operator):
1313

1414
@classmethod
1515
def poll(cls, context: bpy.types.Context) -> bool:
16-
return context.view_layer.objects.active is not None
16+
return is_candidate_object(context)
1717

1818
def execute(self, context: bpy.types.Context) -> Set[int] | Set[str]:
1919
count = 3

stopmagic/operators/skip_frame_forward.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22
import bpy
3-
from stopmagic.functions.insert_mesh_keyframe import insert_mesh_keyframe
3+
from stopmagic.functions import insert_mesh_keyframe, is_candidate_object
44
from typing import Set
55

66

@@ -13,7 +13,7 @@ class SkipFrameForward(bpy.types.Operator):
1313

1414
@classmethod
1515
def poll(cls, context: bpy.types.Context) -> bool:
16-
return context.view_layer.objects.active is not None
16+
return is_candidate_object(context)
1717

1818
def execute(self, context: bpy.types.Context) -> Set[int] | Set[str]:
1919
count = 3

0 commit comments

Comments
 (0)