diff --git a/.gitignore b/.gitignore index 5ae4121..3b9d99c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -# image files +# rendered files *.png +*.mov # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/examples/br2/callbacks.py b/examples/br2/callbacks.py index 3652e64..957dc58 100644 --- a/examples/br2/callbacks.py +++ b/examples/br2/callbacks.py @@ -142,9 +142,9 @@ def update_states( ) def set_keyframe(self, keyframe: int) -> None: - self.bending_actuation.set_keyframe(keyframe) - self.rotation_CW_actuation.set_keyframe(keyframe) - self.rotation_CCW_actuation.set_keyframe(keyframe) + self.bending_actuation.update_keyframe(keyframe) + self.rotation_CW_actuation.update_keyframe(keyframe) + self.rotation_CCW_actuation.update_keyframe(keyframe) class BlenderBR2CallBack(BasicCallBackBaseClass): diff --git a/examples/camera_movement.py b/examples/camera_movement.py index 7fec35a..ca5311a 100644 --- a/examples/camera_movement.py +++ b/examples/camera_movement.py @@ -12,33 +12,40 @@ def main( camera_orbiting_radius: float = 1.0, ): - # Create a new scene + # Clear all mesh objects in the new scene bsr.clear_mesh_objects() # Set the camera film background to transparent bsr.camera.set_film_transparent() + # Set the render file path + bsr.camera.set_file_path(filename + "/frame") + + # Set resolution + bsr.camera.set_resolution(1920, 1080) + # Set the camera look at location bsr.camera.look_at = np.array([0.0, 0.0, 0.0]) - # Set a frame at the origin + # Set a pose at the origin _ = Pose( positions=np.zeros(3), directors=np.identity(3), unit_length=0.25, ) - # Set the current frame number - bsr.frame_manager.frame_current = 0 - - # Set the initial keyframe number - bsr.frame_manager.set_frame_start() - - # Set the camera orbiting keyframes + # Set the camera orbiting angles angles = np.linspace( 0.0, 360.0, int(frame_rate * total_time), endpoint=False ) - for k, angle in enumerate(angles): + + # Set the initial frame + frame_start = 0 + bsr.frame_manager.frame_start = frame_start + + for frame_current, angle in bsr.frame_manager.enumerate( + angles, frame_current_init=frame_start + ): # Set the camera location bsr.camera.location = np.array( @@ -49,35 +56,22 @@ def main( ] ) - # Update the keyframe - bsr.camera.set_keyframe(bsr.frame_manager.frame_current) - - if k != len(angles) - 1: - # Update the keyframe number - bsr.frame_manager.update() - else: - # Set the final keyframe number - bsr.frame_manager.set_frame_end() + # Set and update the camera in current frame + bsr.camera.update_keyframe(frame_current) # Set the frame rate - bsr.frame_manager.set_frame_rate(fps=frame_rate) + bsr.frame_manager.frame_rate = frame_rate # Set the view distance bsr.set_view_distance(distance=5) - # Deslect all objects + # Deselect all objects bsr.deselect_all() # Select the camera object bsr.camera.select() - # Set the render file path - bsr.camera.set_file_path("render/" + filename) - - # set resolution - bsr.camera.set_resolution(1920, 1080) - - # render the scene + # Render the scene bsr.camera.render( frames=np.arange( bsr.frame_manager.frame_start, bsr.frame_manager.frame_end + 1 @@ -90,3 +84,7 @@ def main( if __name__ == "__main__": main() + print("\n\nTo convert the frames into a video, run the following command:") + print( + r"ffmpeg -threads 8 -r 60 -i camera_movement/frame_%03d.png -b:v 90M -c:v prores -pix_fmt yuva444p10le camera_movement.mov" + ) diff --git a/examples/elastica-timoshenko.py b/examples/elastica_timoshenko.py similarity index 100% rename from examples/elastica-timoshenko.py rename to examples/elastica_timoshenko.py diff --git a/examples/pose_demo.py b/examples/pose_demo.py index 64874e6..1a41e58 100644 --- a/examples/pose_demo.py +++ b/examples/pose_demo.py @@ -6,7 +6,21 @@ def angle_to_color(angle: float) -> np.ndarray: - # Normalize angle to 0-360 range + """ + Convert angle to RGB color value + + Parameters + ---------- + angle : float + The angle in degrees + + Returns + ------- + np.ndarray + The RGBA color value + """ + + # Reset angle range to 0-360 degrees angle = angle % 360 # Convert angle to radians @@ -14,79 +28,85 @@ def angle_to_color(angle: float) -> np.ndarray: # Calculate RGB values if angle < 120: - r = np.cos(rad) / 2 + 0.5 - g = np.sin(rad) / 2 + 0.5 - b = 0 + rad = 3 * rad / 2 + r = 0.5 + np.sin(rad) / 2 + g = 0.5 - np.sin(rad) / 2 + b = 0.5 - np.sin(rad) / 2 elif angle < 240: - r = 0 - g = np.cos(rad - 2 * np.pi / 3) / 2 + 0.5 - b = np.sin(rad - 2 * np.pi / 3) / 2 + 0.5 + rad = rad - 2 * np.pi / 3 + rad = 3 * rad / 2 + r = 0.5 - np.sin(rad) / 2 + g = 0.5 + np.sin(rad) / 2 + b = 0.5 - np.sin(rad) / 2 else: - r = np.sin(rad - 4 * np.pi / 3) / 2 + 0.5 - g = 0 - b = np.cos(rad - 4 * np.pi / 3) / 2 + 0.5 + rad = rad - 2 * np.pi / 3 * 2 + rad = 3 * rad / 2 + r = 0.5 - np.sin(rad) / 2 + g = 0.5 - np.sin(rad) / 2 + b = 0.5 + np.sin(rad) / 2 # Return RGBA numpy array return np.array([r, g, b, 1.0]) -def main(filename: str = "pose_demo5"): - - # initial values for frame rate and period - frame_rate = 60 - total_time = 5 - - # calculates total number of frames in the visualization - total_frames = frame_rate * total_time - - # clears all mesh objects +def main( + filename: str = "pose_demo", + frame_rate: int = 60, + total_time: float = 5.0, +): + # Clear all mesh objects in the new scene bsr.clear_mesh_objects() - bsr.frame_manager.set_frame_start() - # intializes pose instance and angle - pose_object = Pose( - positions=np.array([1, 0, 0]), directors=np.eye(3), thickness_ratio=0.1 + # Initialize pose instance + pose = Pose( + positions=np.array([1, 0, 0]), + directors=np.eye(3), + thickness_ratio=0.1, + ) + + # Create an array of angles from 0 to 360 degrees + angles = np.linspace( + 0.0, 360.0, int(frame_rate * total_time), endpoint=False ) - theta = 0 - # iterates through each frame in total time duration - for frame in range(total_frames): - theta = 2 * np.pi * frame / total_frames + # Set the initial frame + frame_start = 0 + bsr.frame_manager.frame_start = frame_start - # defines path of of motion for positions of pose object - new_positions = np.array([np.cos(theta), np.sin(theta), 0]) - pose_object.positions = new_positions + for frame_current, angle in bsr.frame_manager.enumerate( + angles, frame_current_init=frame_start + ): + # Define path of of motion for positions of pose object + positions = [np.cos(np.radians(angle)), np.sin(np.radians(angle)), 0.0] - # defines directors of pose object - d2 = np.array([-np.sin(theta), np.cos(theta), 0]) - d3 = np.array([0, 0, 1]) + # Define directors of pose object + d2 = [-np.sin(np.radians(angle)), np.cos(np.radians(angle)), 0.0] + d3 = [0, 0, 1] d1 = np.cross(d2, d3) - new_directors = np.column_stack((d1, d2, d3)) - pose_object.directors = new_directors - - # updates positions and directors of pose object at each keyframe - pose_object.update_states(new_positions, new_directors) + directors = np.column_stack((d1, d2, d3)) - # converts angle to rgb color value at each frame - color = angle_to_color(np.degrees(theta)) + # Update positions and directors of pose object at each keyframe + pose.update_states( + positions=np.array(positions), + directors=directors, + ) - # updates pose object's colors - pose_object.update_material(color=color) + # Convert angle to rgb color value at each frame + color = angle_to_color(angle) - # sets and updates keyframes - pose_object.set_keyframe(frame) - bsr.frame_manager.update() + # Update pose object's colors + pose.update_material(color=color) - # Set the final keyframe number - bsr.frame_manager.set_frame_end() + # Set and update the pose in current frame + pose.update_keyframe(frame_current) # Set the frame rate - bsr.frame_manager.set_frame_rate(fps=frame_rate) + bsr.frame_manager.frame_rate = frame_rate # Set the view distance bsr.set_view_distance(distance=5) - # Deslect all objects + # Deselect all objects bsr.deselect_all() # Select the camera object diff --git a/examples/single_rigid_rod_spring_action.py b/examples/single_rigid_rod_spring_action.py index 0e49a66..de97a70 100644 --- a/examples/single_rigid_rod_spring_action.py +++ b/examples/single_rigid_rod_spring_action.py @@ -61,7 +61,7 @@ def analytical_solution(t, y0: float, v0: float): # update the rod keyframe = int(time_index / simulation_ratio) + 1 rod.update_states(positions=positions, radii=radii) - rod.set_keyframe(keyframe) + rod.update_keyframe(keyframe) bsr.save("single_rigid_rod_spring_action.blend") diff --git a/poetry.lock b/poetry.lock index a3d0b1b..f745d0d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -111,13 +111,13 @@ zstandard = "*" [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -1548,6 +1548,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1555,8 +1556,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1573,6 +1582,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1580,6 +1590,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, diff --git a/src/bsr/_camera.py b/src/bsr/_camera.py index e05e941..3ff2cae 100644 --- a/src/bsr/_camera.py +++ b/src/bsr/_camera.py @@ -39,7 +39,7 @@ def select(self) -> None: """ bpy.context.view_layer.objects.active = self._camera - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Sets a keyframe at the given frame. diff --git a/src/bsr/_light.py b/src/bsr/_light.py index a3f9fcd..d7f6ec3 100644 --- a/src/bsr/_light.py +++ b/src/bsr/_light.py @@ -46,7 +46,7 @@ def location(self, location: np.ndarray) -> None: """ self._light.location = location - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Sets a keyframe at the given frame. diff --git a/src/bsr/frame.py b/src/bsr/frame.py index d519545..248e7b1 100644 --- a/src/bsr/frame.py +++ b/src/bsr/frame.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Iterable, Optional import bpy @@ -8,29 +8,30 @@ class FrameManager(metaclass=SingletonMeta): """ This class provides methods for manipulating the frame of the scene. - Only one instance exist, which you can access by: bsr.frame. + Only one instance exist, which you can access by: bsr.frame_manager. """ - def update(self, forwardframe: int = 1) -> None: + def update(self, frame_forward: int = 1) -> None: """ Update the current frame number of the scene. Parameters ---------- - forwardframe : int, optional + frame_forward : int, optional The number of frames to move forward. The default is 1. """ assert ( - isinstance(forwardframe, int) and forwardframe > 0 - ), "forwardframe must be a positive integer" - bpy.context.scene.frame_current += forwardframe + isinstance(frame_forward, int) and frame_forward > 0 + ), "frame_forward must be a positive integer" + bpy.context.scene.frame_current += frame_forward @property def frame_current(self) -> int: """ Return the current frame number of the scene. """ - return int(bpy.context.scene.frame_current) + frame_current = int(bpy.context.scene.frame_current) + return frame_current @frame_current.setter def frame_current(self, frame: int) -> None: @@ -51,53 +52,75 @@ def frame_current(self, frame: int) -> None: def frame_start(self) -> int: """ Return the start frame number of the scene. - """ - return int(bpy.context.scene.frame_start) - @property - def frame_end(self) -> int: - """ - Return the end frame number of the scene. + Returns + ------- + int + The start frame number of the scene. """ - return int(bpy.context.scene.frame_end) + frame_start = int(bpy.context.scene.frame_start) + return frame_start - def set_frame_start(self, frame: Optional[int] = None) -> None: + @frame_start.setter + def frame_start(self, frame: int) -> None: """ Set the start frame number of the scene. Parameters ---------- - frame : int, optional - The start frame number of the scene. The default is None. - If None, the current frame number is used. + frame : int + The start frame number of the scene. """ - if frame is None: - frame = bpy.context.scene.frame_current - else: - assert ( - isinstance(frame, int) and frame >= 0 - ), "frame must be a positive integer or 0" + assert ( + isinstance(frame, int) and frame >= 0 + ), "frame must be a nonnegative integer" bpy.context.scene.frame_start = frame - def set_frame_end(self, frame: Optional[int] = None) -> None: + @property + def frame_end(self) -> int: + """ + Return the end frame number of the scene. + + Returns + ------- + int + The end frame number of the scene. + """ + frame_end = int(bpy.context.scene.frame_end) + return frame_end + + @frame_end.setter + def frame_end(self, frame: int) -> None: """ Set the end frame number of the scene. Parameters ---------- - frame : int, optional - The end frame number of the scene. The default is None. - If None, the current frame number is used. + frame : int + The end frame number of the scene. """ - if frame is None: - frame = bpy.context.scene.frame_current - else: - assert ( - isinstance(frame, int) and frame >= 0 - ), "frame must be a positive integer or 0" + assert ( + isinstance(frame, int) and frame >= 0 + ), "frame must be a nonnegative integer" bpy.context.scene.frame_end = frame - def set_frame_rate(self, fps: int | float) -> None: + @property + def frame_rate(self) -> float: + """ + Return the frame rate of the scene. + + Returns + ------- + float + The frame rate of the scene. (Frame per second) + """ + fps: float = ( + bpy.context.scene.render.fps / bpy.context.scene.render.fps_base + ) + return fps + + @frame_rate.setter + def frame_rate(self, fps: int | float) -> None: """ Set the frame rate of the scene. @@ -111,16 +134,26 @@ def set_frame_rate(self, fps: int | float) -> None: bpy.context.scene.render.fps = int(fps) bpy.context.scene.render.fps_base = int(fps) / fps - def get_frame_rate(self) -> float: + def enumerate( + self, iterable: Iterable, frame_current_init: Optional[int] = None + ) -> Iterable: """ - Get the frame rate of the scene. + Enumerate through the frames of the scene. - Returns - ------- - float - The frame rate of the scene. (Frame per second) + Parameters + ---------- + iterable : Iterable + An iterable object to enumerate. + frame_current_init : int, optional + The initial current frame number of the scene. The default is None. + If None, the number self.frame_current is used. """ - fps: float = ( - bpy.context.scene.render.fps / bpy.context.scene.render.fps_base - ) - return fps + if frame_current_init is not None: + assert ( + isinstance(frame_current_init, int) and frame_current_init >= 0 + ), "frame_current_init must be a nonnegative integer" + self.frame_current = frame_current_init + for item in iterable: + yield self.frame_current, item + self.update() # Update the frame number + self.frame_end = self.frame_current - 1 # Set the final frame number diff --git a/src/bsr/geometry/composite/pose.py b/src/bsr/geometry/composite/pose.py index 076b63b..b625153 100644 --- a/src/bsr/geometry/composite/pose.py +++ b/src/bsr/geometry/composite/pose.py @@ -168,15 +168,15 @@ def update_material(self, **kwargs: dict[str, Any]) -> None: for cylinder in self.cylinders: cylinder.update_material(**kwargs) - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Set the keyframe for the pose object """ for shperes in self.spheres: - shperes.set_keyframe(keyframe) + shperes.update_keyframe(keyframe) for cylinder in self.cylinders: - cylinder.set_keyframe(keyframe) + cylinder.update_keyframe(keyframe) if TYPE_CHECKING: diff --git a/src/bsr/geometry/composite/rod.py b/src/bsr/geometry/composite/rod.py index bfc9c12..d90fd86 100644 --- a/src/bsr/geometry/composite/rod.py +++ b/src/bsr/geometry/composite/rod.py @@ -162,15 +162,15 @@ def update_material(self, **kwargs: dict[str, Any]) -> None: for cylinder in self.cylinders: cylinder.update_material(**kwargs) - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Set keyframe for the rod object """ for idx, sphere in enumerate(self.spheres): - sphere.set_keyframe(keyframe) + sphere.update_keyframe(keyframe) for idx, cylinder in enumerate(self.cylinders): - cylinder.set_keyframe(keyframe) + cylinder.update_keyframe(keyframe) # Alias diff --git a/src/bsr/geometry/composite/stack.py b/src/bsr/geometry/composite/stack.py index 9ca45e9..3990412 100644 --- a/src/bsr/geometry/composite/stack.py +++ b/src/bsr/geometry/composite/stack.py @@ -75,12 +75,12 @@ def object(self) -> list[BlenderMeshInterfaceProtocol]: """ return self._objs - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Sets a keyframe at the given frame. """ for obj in self._objs: - obj.set_keyframe(keyframe) + obj.update_keyframe(keyframe) @classmethod def create( diff --git a/src/bsr/geometry/primitives/simple.py b/src/bsr/geometry/primitives/simple.py index e5a38c2..67301c4 100644 --- a/src/bsr/geometry/primitives/simple.py +++ b/src/bsr/geometry/primitives/simple.py @@ -54,7 +54,7 @@ def _validate_position(position: NDArray) -> None: """ Checks if inputted position values are valid - Paramters + Parameters --------- position: NDArray Position input (endpoint or centerpoint depending on Object type) @@ -226,7 +226,7 @@ def _create_sphere(self) -> bpy.types.Object: bpy.ops.mesh.primitive_uv_sphere_add() return bpy.context.active_object - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Sets a keyframe at the given frame. @@ -248,7 +248,7 @@ class Cylinder(KeyFrameControlMixin): position_1 : NDArray The first endpoint position of the cylinder object. (3D) position_2 : NDArray - The second enspoint position of the cylinder object. (3D) + The second endpoint position of the cylinder object. (3D) radius : float The radius of the cylinder object. """ @@ -416,7 +416,7 @@ def _create_cylinder( cylinder = bpy.context.active_object return cylinder - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: """ Sets a keyframe at the given frame. @@ -509,7 +509,7 @@ def update_states( ) -> None: raise NotImplementedError - def set_keyframe(self, keyframe: int) -> None: + def update_keyframe(self, keyframe: int) -> None: raise NotImplementedError diff --git a/src/bsr/tools/keyframe_mixin.py b/src/bsr/tools/keyframe_mixin.py index 72824d3..630afdc 100644 --- a/src/bsr/tools/keyframe_mixin.py +++ b/src/bsr/tools/keyframe_mixin.py @@ -21,5 +21,7 @@ def clear_animation(self: BlenderMeshInterfaceProtocol) -> None: self.object.animation_data_create() @abstractmethod - def set_keyframe(self: BlenderMeshInterfaceProtocol, keyframe: int) -> None: + def update_keyframe( + self: BlenderMeshInterfaceProtocol, keyframe: int + ) -> None: raise NotImplementedError diff --git a/src/bsr/tools/protocol.py b/src/bsr/tools/protocol.py index 914dc94..e9481ce 100644 --- a/src/bsr/tools/protocol.py +++ b/src/bsr/tools/protocol.py @@ -4,4 +4,4 @@ class BlenderKeyframeManipulateProtocol(Protocol): def clear_animation(self) -> None: ... - def set_keyframe(self, keyframe: int) -> None: ... + def update_keyframe(self, keyframe: int) -> None: ... diff --git a/src/elastica_blender/converter/npz2blend.py b/src/elastica_blender/converter/npz2blend.py index 859dcbb..9f2cdac 100644 --- a/src/elastica_blender/converter/npz2blend.py +++ b/src/elastica_blender/converter/npz2blend.py @@ -63,7 +63,7 @@ def construct_blender_file( rods.update_states( position_history[:, tidx, ...], radius_history[:, tidx, ...] ) - rods.set_keyframe(tidx) + rods.update_keyframe(tidx) else: for tag in tags: position_history = data[tag + "_position_history"] @@ -77,7 +77,7 @@ def construct_blender_file( rods.update_states( position_history[:, tidx, ...], radius_history[:, tidx, ...] ) - rods.set_keyframe(tidx) + rods.update_keyframe(tidx) bsr.save(output) diff --git a/src/elastica_blender/rod_callback.py b/src/elastica_blender/rod_callback.py index 48ccff9..785cbb9 100644 --- a/src/elastica_blender/rod_callback.py +++ b/src/elastica_blender/rod_callback.py @@ -41,5 +41,5 @@ def make_callback( positions=system.position_collection, radii=system.radius, ) - self.bpy_objs.set_keyframe(self.keyframe) + self.bpy_objs.update_keyframe(self.keyframe) self.keyframe += 1 diff --git a/tests/geometry/stack/test_stack_properties.py b/tests/geometry/stack/test_stack_properties.py index 279df3f..5caaf55 100644 --- a/tests/geometry/stack/test_stack_properties.py +++ b/tests/geometry/stack/test_stack_properties.py @@ -9,14 +9,14 @@ def test_object_property(): assert stack.object == [1, 2, 3] -def test_set_keyframe(mocker): +def test_update_keyframe(mocker): stack = BaseStack() mock_rod = mocker.Mock() n_repeat = 3 val = 5 stack._objs = [mock_rod] * n_repeat - stack.set_keyframe(val) + stack.update_keyframe(val) - mock_rod.set_keyframe.assert_called() - assert mock_rod.set_keyframe.call_count == n_repeat - mock_rod.set_keyframe.assert_called_with(val) + mock_rod.update_keyframe.assert_called() + assert mock_rod.update_keyframe.call_count == n_repeat + mock_rod.update_keyframe.assert_called_with(val) diff --git a/tests/geometry/test_interface_keyframe_setting.py b/tests/geometry/test_interface_keyframe_setting.py index fbdaf26..ce5e91d 100644 --- a/tests/geometry/test_interface_keyframe_setting.py +++ b/tests/geometry/test_interface_keyframe_setting.py @@ -28,50 +28,50 @@ def count_number_of_keyframes_action(obj): return len(action.fcurves[0].keyframe_points) -def test_set_keyframe_count_for_primitive_sphere(): +def test_update_keyframe_count_for_primitive_sphere(): primitive = Sphere(position=np.array([0, 0, 0]), radius=1.0) - primitive.set_keyframe(1) + primitive.update_keyframe(1) assert count_number_of_keyframes_action(primitive.object) == 1 - primitive.set_keyframe(2) + primitive.update_keyframe(2) assert count_number_of_keyframes_action(primitive.object) == 2 - # Setting keyfrome at the same frame should not increase the number of keyframes: - primitive.set_keyframe(2) + # Setting keyframe at the same frame should not increase the number of keyframes: + primitive.update_keyframe(2) assert count_number_of_keyframes_action(primitive.object) == 2 primitive.clear_animation() assert count_number_of_keyframes_action(primitive.object) == 0 - primitive.set_keyframe(1) + primitive.update_keyframe(1) assert count_number_of_keyframes_action(primitive.object) == 1 # Clear the test primitive.clear_animation() -def test_set_keyframe_count_for_primitive_cylinder(): +def test_update_keyframe_count_for_primitive_cylinder(): primitive = Cylinder( position_1=np.array([0, 0, 0]), position_2=np.array([0, 0, 1]), radius=1.0, ) - primitive.set_keyframe(1) + primitive.update_keyframe(1) assert count_number_of_keyframes_action(primitive.object) == 1 - primitive.set_keyframe(2) + primitive.update_keyframe(2) assert count_number_of_keyframes_action(primitive.object) == 2 # Setting keyfrome at the same frame should not increase the number of keyframes: - primitive.set_keyframe(2) + primitive.update_keyframe(2) assert count_number_of_keyframes_action(primitive.object) == 2 primitive.clear_animation() assert count_number_of_keyframes_action(primitive.object) == 0 - primitive.set_keyframe(1) + primitive.update_keyframe(1) assert count_number_of_keyframes_action(primitive.object) == 1 # Clear the test diff --git a/tests/test_frame.py b/tests/test_frame.py index cdde270..8e60fd2 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -27,33 +27,58 @@ def test_frame_manager_update(self): frame_manager.update(10) assert frame_manager.frame_current == 10 - def test_frame_manager_update_with_wrong_forwardframe(self): + def test_frame_manager_update_with_wrong_frame_forward(self): frame_manager = FrameManager() with pytest.raises(AssertionError): frame_manager.update(-1) - def test_frame_manager_set_frame_end(self): + def test_frame_manager_get_set_frame_start(self): frame_manager = FrameManager() - frame_manager.set_frame_end(100) - assert bpy.context.scene.frame_end == 100 + frame_manager.frame_start = 10 + assert bpy.context.scene.frame_start == 10 + assert frame_manager.frame_start == 10 - def test_frame_manager_set_frame_end_with_none(self): + def test_frame_manager_set_frame_start_with_wrong_frame(self): frame_manager = FrameManager() - frame_manager.frame_current = 0 - frame_manager.update(250) - frame_manager.set_frame_end() - assert bpy.context.scene.frame_end == 250 + frame_start = frame_manager.frame_start + with pytest.raises(AssertionError): + frame_manager.frame_start = -1 + assert frame_manager.frame_start == frame_start + + def test_frame_manager_get_set_frame_end(self): + frame_manager = FrameManager() + frame_manager.frame_end = 100 + assert bpy.context.scene.frame_end == 100 + assert frame_manager.frame_end == 100 def test_frame_manager_set_frame_end_with_wrong_frame(self): frame_manager = FrameManager() + frame_end = frame_manager.frame_end with pytest.raises(AssertionError): - frame_manager.set_frame_end(-1) + frame_manager.frame_end = -1 + assert frame_manager.frame_end == frame_end def test_frame_manager_get_set_frame_rate(self): frame_manager = FrameManager() - frame_manager.set_frame_rate(30) - assert frame_manager.get_frame_rate() == 30 - frame_manager.set_frame_rate(29.97) - assert math.isclose( - frame_manager.get_frame_rate(), 29.97, abs_tol=0.001 - ) + frame_manager.frame_rate = 30 + assert frame_manager.frame_rate == 30 + frame_manager.frame_rate = 29.97 + assert math.isclose(frame_manager.frame_rate, 29.97, abs_tol=0.001) + + def test_frame_manager_set_frame_rate_with_wrong_frame_rate(self): + frame_manager = FrameManager() + frame_rate = frame_manager.frame_rate + with pytest.raises(AssertionError): + frame_manager.frame_rate = 0 + assert frame_manager.frame_rate == frame_rate + + def test_frame_manager_enumerate(self): + frame_manager = FrameManager() + frame_init = 10 + for frame_current, step in frame_manager.enumerate( + range(5), frame_current_init=frame_init + ): + assert frame_current == (step + frame_init) + assert frame_manager.frame_current == frame_current + assert frame_manager.frame_end == 14 + assert frame_manager.frame_current == 15