Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete geometry.py module: handling sphere and cylinder #7

Merged
merged 51 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
7baf0d3
add protocol for general blender mesh interface
skim0119 May 31, 2024
1e7400d
doc: sphere object
skim0119 May 31, 2024
10fc242
Merge branch 'main' into wip/geometry
skim0119 Jun 4, 2024
d32710a
Merge branch 'main' into wip/geometry
skim0119 Jun 5, 2024
f295da7
Merge branch 'main' into wip/geometry
skim0119 Jun 5, 2024
8e8b879
test: add test template for geometry module
skim0119 Jun 5, 2024
2e86b58
fix: scale and radius relation for mesh tranformation
skim0119 Jun 7, 2024
9780e54
Merge branch 'main' into wip/geometry
skim0119 Jun 7, 2024
07c183d
test: add geometry shape checker
skim0119 Jun 7, 2024
7d8b062
removed: blender mesh interface state getter
skim0119 Jun 7, 2024
61e7d41
Completed Cylinder class. Would like review on depth parameter
Rohar10 Jun 8, 2024
ed4d809
Finished Cylinder class; will start writing test cases
Rohar10 Jun 8, 2024
ea5277e
Removed depth from Cylinder parameters
Rohar10 Jun 8, 2024
4eea83b
refactor: protocol defined in separate file
skim0119 Jun 8, 2024
5d6be80
rename
skim0119 Jun 8, 2024
99cf8af
feat: keyframe manipulator mixin
skim0119 Jun 9, 2024
3def8a9
formatting
skim0119 Jun 9, 2024
170350d
test: fix utility import
skim0119 Jun 9, 2024
a2fb430
Update src/bsr/geometry.py
skim0119 Jun 9, 2024
bcfe9d2
fix: correct naming for internal usage of geometry object
skim0119 Jun 9, 2024
c3b95e0
Merge pull request #18 from GazzolaLab/wip/geometry-group
skim0119 Jun 10, 2024
c65d379
Working on test cases
Rohar10 Jun 10, 2024
b51b921
Update tests/test_blender_mesh_interface.py
Rohar10 Jun 11, 2024
a0a2514
Update tests/test_blender_mesh_interface.py
Rohar10 Jun 11, 2024
6b4671f
Merge branch 'main' into wip/geometry
skim0119 Jun 12, 2024
75e9f7b
init: typing alias collection
skim0119 Jun 11, 2024
6566751
update: stacking type
skim0119 Jun 11, 2024
17cb7a2
update npz2blend operator to match updated implementation
skim0119 Jun 11, 2024
b88dd2e
test: stacking base
skim0119 Jun 11, 2024
54a38e6
update stacking protocol
skim0119 Jun 11, 2024
6a90611
test: add property test for stack module
skim0119 Jun 11, 2024
2e7e498
mypy: pass test for stack module
skim0119 Jun 11, 2024
125519c
doc: api for collective geometry
skim0119 Jun 11, 2024
b3e2ee2
test: add frustum in primitive testing
skim0119 Jun 12, 2024
3cc7e35
refactor: single-implementation for cylinder orientation function
skim0119 Jun 12, 2024
426a4d4
Written initial test cases; will test later. Please review if possibl…
Rohar10 Jun 12, 2024
5013095
postpone frustum to be future task
skim0119 Jun 12, 2024
9935295
Merge branch 'wip/geometry' of https://github.com/GazzolaLab/Blender_…
skim0119 Jun 12, 2024
9598fc3
test: extend geometry unittests
skim0119 Jun 12, 2024
a541f13
test: finalize geometry test cases
skim0119 Jun 12, 2024
0e3601c
added poetry.lock
Rohar10 Jun 12, 2024
7472b8c
small changes in geometry.py file
Rohar10 Jun 14, 2024
326ea89
Was passing 197/199 Test Cases; Now having errors when running make t…
Rohar10 Jun 17, 2024
84f0fd2
passed all test cases; awaiting documentation
Rohar10 Jun 17, 2024
b4cd0b3
Completed function docstrings, passed all test cases
Rohar10 Jun 18, 2024
f78b16b
Completed function docstrings, passed all test cases
Rohar10 Jun 18, 2024
dd5ab9c
Delete .vscode/settings.json
skim0119 Jun 18, 2024
20792eb
update geometry
skim0119 Jun 18, 2024
d4fbf09
Merge branch 'wip/geometry' of https://github.com/GazzolaLab/Blender_…
skim0119 Jun 18, 2024
100fd5e
pass mypy
skim0119 Jun 18, 2024
c817f2b
fix test segfault
skim0119 Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 132 additions & 43 deletions src/bsr/geometry.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,127 @@
__doc__ = """
This module provides a set of geometry-mesh interfaces for blender objects.
"""
__all__ = ["BlenderMeshInterfaceProtocol", "Sphere", "Cylinder"]

from typing import (
TYPE_CHECKING,
Any,
ParamSpec,
Protocol,
Type,
TypedDict,
TypeVar,
)
from typing_extensions import Self

import colorsys

import bpy
import numpy as np

MeshDataType = dict[str, Any]

S = TypeVar("S", bound="BlenderMeshInterfaceProtocol")
P = ParamSpec("P")


class BlenderMeshInterfaceProtocol(Protocol):
"""
This protocol defines the interface for Blender mesh objects.
"""

# TODO: For future implementation
# @property
# def data(self): ...

@property
def object(self) -> bpy.types.Object:
"""Returns associated Blender object."""

@classmethod
def create(cls: Type[S], states: MeshDataType) -> S:
"""Creates a new mesh object with the given states."""

def update_states(self, *args: Any) -> bpy.types.Object:
"""Updates the mesh object with the given states."""

# def update_material(self, material) -> None: ... # TODO: For future implementation


class Sphere:
def __init__(self, location, radius=0.005):
self.obj = self.create_sphere(location, radius)
"""
This class provides a mesh interface for Blender Sphere objects.
Sphere objects are created with the given position and radius.

def create_sphere(self, location, radius):
bpy.ops.mesh.primitive_uv_sphere_add(radius=radius, location=location)
return bpy.context.active_object
Parameters
----------
position : np.ndarray
The position of the sphere object.
radius : float
The radius of the sphere object.
"""

def __init__(self, position: np.ndarray, radius: float) -> None:
self._obj = self._create_sphere()
self.update_states(position, radius)

def update_position(self, location):
self.obj.location.z = location[2]
self.obj.location.y = location[1]
self.obj.location.x = location[0]
@classmethod
def create(cls, states: MeshDataType) -> "Sphere":
return cls(states["position"], states["radius"])

@property
def object(self) -> bpy.types.Object:
return self._obj

def update_states(
self, position: np.ndarray | None = None, radius: float | None = None
) -> bpy.types.Object:
if position is not None:
self.object.location.x = position[0]
self.object.location.y = position[1]
self.object.location.z = position[2]
if radius is not None:
self.object.scale = (radius, radius, radius)
return self.object

def _create_sphere(self) -> bpy.types.Object:
"""
Creates a new sphere object with the given position and radius.
"""
bpy.ops.mesh.primitive_uv_sphere_add()
return bpy.context.active_object


# FIXME: This class needs to be modified to conform to the BlenderMeshInterfaceProtocol
class Cylinder:
def __init__(self, pos1, pos2):
self.obj = self.create_cylinder(pos1, pos2)
self.mat = bpy.data.materials.new(name="cyl_mat")
self.obj.active_material = self.mat
"""
TODO: Add documentation
"""

def create_cylinder(self, pos1, pos2):
depth, center, angles = self.calc_cyl_orientation(pos1, pos2)
bpy.ops.mesh.primitive_cylinder_add(
radius=0.005, depth=1, location=center
def __init__(
self, pos1: np.ndarray, pos2: np.ndarray, radius: float, depth: float
):
self.pos1 = pos1
skim0119 marked this conversation as resolved.
Show resolved Hide resolved
self.pos2 = pos2
self.obj = self._create_cylinder(pos1, pos2, radius, depth)

@classmethod
def create(cls, states: MeshDataType) -> "Cylinder":
return cls(
states["pos1"], states["pos2"], states["radius"], states["depth"]
)
cylinder = bpy.context.active_object
cylinder.rotation_euler = (0, angles[1], angles[0])
cylinder.scale[2] = depth
return cylinder

@property
def object(self) -> bpy.types.Object:
return self._obj

def update_states(self, pos1, pos2, radius):
depth, center, angles = self.calc_cyl_orientation(pos1, pos2)
self.obj.location = center
self.obj.rotation_euler = (0, angles[1], angles[0])
self.obj.scale[2] = depth
self.obj.scale[0] = radius
self.obj.scale[1] = radius

def calc_cyl_orientation(self, pos1, pos2):
pos1 = np.array(pos1)
Expand All @@ -47,29 +136,29 @@ def calc_cyl_orientation(self, pos1, pos2):
angles = np.array([phi, theta])
return depth, center, angles

def update_position(self, pos1, pos2):
def _create_cylinder(
self, pos1: np.ndarray, pos2: np.ndarray, radius: float, depth: float
) -> bpy.types.Object:
"""
Creates a new cylinder object with the given end positions, radius, centerpoint and depth.
"""
depth, center, angles = self.calc_cyl_orientation(pos1, pos2)
self.obj.location = (center[0], center[1], center[2])
self.obj.rotation_euler = (0, angles[1], angles[0])
self.obj.scale[2] = depth

# computing deformation heat-map
max_def = 0.07

h = (
-np.sqrt(self.obj.location[0] ** 2 + self.obj.location[2] ** 2)
/ max_def
+ 240 / 360
)
v = (
np.sqrt(self.obj.location[0] ** 2 + self.obj.location[2] ** 2)
/ max_def
* 0.5
+ 0.5
bpy.ops.mesh.primitive_uv_cylinder_add(
radius=radius, location=center, depth=depth
)
cylinder = bpy.context.active_object
cylinder.rotation_euler = (0, angles[1], angles[0])
cylinder.scale[2] = depth
return cylinder

r, g, b = colorsys.hsv_to_rgb(h, 1, v)
self.update_color(r, g, b, 1)

def update_color(self, r, g, b, a):
self.mat.diffuse_color = (r, g, b, a)
if TYPE_CHECKING:
# This is required for explicit type-checking
data = {"position": np.array([0, 0, 0]), "radius": 1.0}
_: BlenderMeshInterfaceProtocol = Sphere.create(data)
data = {
"position_1": np.array([0, 0, 0]),
"position_2": np.array([1, 1, 1]),
"radius": 1.0,
}
_: BlenderMeshInterfaceProtocol = Cylinder.create(data)
10 changes: 10 additions & 0 deletions src/bsr/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,13 @@ def clear_mesh_objects() -> None:
bpy.ops.object.select_all(action="DESELECT")
bpy.ops.object.select_by_type(type="MESH")
bpy.ops.object.delete()


def scene_update() -> None:
"""
Update the scene

Used to update object's matrix_world after transformations
(https://blender.stackexchange.com/questions/27667/incorrect-matrix-world-after-transformation)
"""
bpy.context.view_layer.update()
138 changes: 138 additions & 0 deletions tests/geometry/test_primitive_geometry_mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import numpy as np
import pytest

from bsr.geometry import Cylinder, Sphere
from tests.geometry.utils import get_mesh_limit

# Visual tolerance for the mesh limit
_VISUAL_ATOL = 1e-7
_VISUAL_RTOL = 1e-4


@pytest.mark.parametrize(
"center",
[
np.array([10, 10, 10]),
np.array([10, 11, 10]),
np.array([10, 11, 11]),
np.array([11, 11, 11]),
],
)
@pytest.mark.parametrize("radius", [1, 2, 3, 5.5])
def test_sphere_radius_and_position(center, radius):
x_min, x_max = center[0] - radius, center[0] + radius
y_min, y_max = center[1] - radius, center[1] + radius
z_min, z_max = center[2] - radius, center[2] + radius

sphere = Sphere(position=center, radius=radius)

mesh_limit = get_mesh_limit(sphere)

np.testing.assert_allclose(
(x_min, x_max, y_min, y_max, z_min, z_max),
mesh_limit,
rtol=_VISUAL_RTOL,
atol=_VISUAL_ATOL,
)


@pytest.mark.parametrize(
"position_one",
[
np.array([10, 10, 10]),
np.array([10, 11, -10]),
np.array([-10, 11, 11]),
],
)
@pytest.mark.parametrize("length", [1, 10.5, -1, -10.5])
@pytest.mark.parametrize("radius", [1, 3, 5.5])
def test_x_cylinder_radius_and_positions(position_one, length, radius):
position_two = position_one + np.array([length, 0, 0])
y, z = position_one[1], position_one[2]

x_min, x_max = min(position_one[0], position_two[0]), max(
position_one[0], position_two[0]
)
y_min, y_max = y - radius, y + radius
z_min, z_max = z - radius, z + radius

cylinder = Cylinder(
position_1=position_one, position_2=position_two, radius=radius
)

mesh_limit = get_mesh_limit(cylinder)

np.testing.assert_allclose(
(x_min, x_max, y_min, y_max, z_min, z_max),
mesh_limit,
rtol=_VISUAL_RTOL,
atol=_VISUAL_ATOL,
)


@pytest.mark.parametrize(
"position_one",
[
np.array([10, 10, 10]),
np.array([10, 11, -10]),
np.array([-10, 11, 11]),
],
)
@pytest.mark.parametrize("length", [1, 10.5, -1, -10.5])
@pytest.mark.parametrize("radius", [1, 3, 5.5])
def test_y_cylinder_radius_and_positions(position_one, length, radius):
position_two = position_one + np.array([0, length, 0])
x, z = position_one[0], position_one[2]

x_min, x_max = x - radius, x + radius
y_min, y_max = min(position_one[1], position_two[1]), max(
position_one[1], position_two[1]
)
z_min, z_max = z - radius, z + radius

cylinder = Cylinder(
position_1=position_one, position_2=position_two, radius=radius
)

mesh_limit = get_mesh_limit(cylinder)

np.testing.assert_allclose(
(x_min, x_max, y_min, y_max, z_min, z_max),
mesh_limit,
rtol=_VISUAL_RTOL,
atol=_VISUAL_ATOL,
)


@pytest.mark.parametrize(
"position_one",
[
np.array([10, 10, 10]),
np.array([10, 11, -10]),
np.array([-10, 11, 11]),
],
)
@pytest.mark.parametrize("length", [1, 10.5, -1, -10.5])
@pytest.mark.parametrize("radius", [1, 3, 5.5])
def test_z_cylinder_radius_and_positions(position_one, length, radius):
position_two = position_one + np.array([0, 0, length])
x, y = position_one[0], position_one[1]

x_min, x_max = x - radius, x + radius
y_min, y_max = y - radius, y + radius
z_min, z_max = min(position_one[2], position_two[2]), max(
position_one[2], position_two[2]
)

cylinder = Cylinder(
position_1=position_one, position_2=position_two, radius=radius
)

mesh_limit = get_mesh_limit(cylinder)

np.testing.assert_allclose(
(x_min, x_max, y_min, y_max, z_min, z_max),
mesh_limit,
rtol=_VISUAL_RTOL,
atol=_VISUAL_ATOL,
)
22 changes: 22 additions & 0 deletions tests/geometry/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import bpy
import numpy as np

from bsr.geometry import BlenderMeshInterfaceProtocol
from bsr.macros import scene_update


def get_mesh_limit(interface: BlenderMeshInterfaceProtocol):
"""(For testing) Given blender mesh object, return xyz limit"""

obj = interface.object
scene_update()

vertices_coords = []
for v in obj.data.vertices:
global_coord = obj.matrix_world @ v.co
vertices_coords.append(list(global_coord))
vertices_coords = np.array(vertices_coords)

x_min, y_min, z_min = np.min(vertices_coords, axis=0)
x_max, y_max, z_max = np.max(vertices_coords, axis=0)
return x_min, x_max, y_min, y_max, z_min, z_max
Loading
Loading