Skip to content

Commit

Permalink
Merge branch 'development' of github.com:damogranlabs/classy_blocks i…
Browse files Browse the repository at this point in the history
…nto development
  • Loading branch information
FranzBangar committed Jan 22, 2024
2 parents 0ffb97f + 120247a commit 7c594c4
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 64 deletions.
24 changes: 24 additions & 0 deletions examples/operation/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import os

import numpy as np

import classy_blocks as cb

box_1 = cb.Box([-1, -1, -1], [1, 1, 1])
box_2 = box_1.copy().rotate(np.pi / 4, [1, 1, 1], [0, 0, 0]).translate([4, 2, 0])

for i in range(3):
box_1.chop(i, count=10)
box_2.chop(i, count=10)


connector = cb.Connector(box_1, box_2)
connector.chop(2, count=10)

mesh = cb.Mesh()
mesh.add(box_1)
mesh.add(box_2)
mesh.add(connector)
mesh.set_default_patch("walls", "wall")

mesh.write(os.path.join("..", "case", "system", "blockMeshDict"), debug_path="debug.vtk")
2 changes: 2 additions & 0 deletions src/classy_blocks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from .construct.edges import Angle, Arc, OnCurve, Origin, PolyLine, Project, Spline
from .construct.flat.face import Face
from .construct.operations.box import Box
from .construct.operations.connector import Connector
from .construct.operations.extrude import Extrude
from .construct.operations.loft import Loft
from .construct.operations.revolve import Revolve
Expand Down Expand Up @@ -46,6 +47,7 @@
"Revolve",
"Box",
"Wedge",
"Connector",
# construct shapes
"Elbow",
"Frustum",
Expand Down
86 changes: 86 additions & 0 deletions src/classy_blocks/construct/operations/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from typing import List

import numpy as np

from classy_blocks.construct.flat.face import Face
from classy_blocks.construct.operations.operation import Operation
from classy_blocks.modify.reorient.viewpoint import ViewpointReorienter
from classy_blocks.util import functions as f


class FacePair:
def __init__(self, face_1: Face, face_2: Face):
self.face_1 = face_1
self.face_2 = face_2

@property
def distance(self) -> float:
"""Returns distance between two faces' centers"""
return f.norm(self.face_1.center - self.face_2.center)

@property
def alignment(self) -> float:
"""Returns a scalar number that is a measure of how well the
two faces are aligned, a.k.a. how well their normals align"""
vconn = f.unit_vector(self.face_2.center - self.face_1.center)
return np.dot(vconn, self.face_1.normal) ** 3 + np.dot(-vconn, self.face_2.normal) ** 3


class Connector(Operation):
"""A normal Loft but automatically finds and reorders appropriate faces between
two arbitrary given blocks.
The recipe is as follows:
1. Find a pair of faces whose normals are most nicely aligned
2. Create a loft that connects them
3. Reorder the loft so that is is properly oriented
The following limitations apply:
"Closest faces" might be an ill-defined term; for example,
imagine two boxes:
___
| 2 |
|___|
___
| 1 |
|___|
Here, multiple different faces can be found.
Reordering relies on ViewpointReorienter; see the documentation on that
for its limitations.
Resulting loft will have the bottom face coincident with operation_1
and top face with operation_2.
Axis 2 is always between the two operations but axes 0 and 1
depend on positions of operations and is not exactly defined.
To somewhat alleviate this confusion it is
recommended to chop operation 1 or 2 in axes 0 and 1 and
only provide chopping for axis 2 of connector."""

def __init__(self, operation_1: Operation, operation_2: Operation):
self.operation_1 = operation_1
self.operation_2 = operation_2

all_pairs: List[FacePair] = []
for orient_1, face_1 in operation_1.get_all_faces().items():
if orient_1 in ("bottom", "left", "front"):
face_1.invert()
for orient_2, face_2 in operation_2.get_all_faces().items():
if orient_2 in ("bottom", "left", "front"):
face_2.invert()
all_pairs.append(FacePair(face_1, face_2))

all_pairs.sort(key=lambda pair: pair.distance)
all_pairs = all_pairs[:9]
all_pairs.sort(key=lambda pair: pair.alignment)

start_face = all_pairs[-1].face_1
end_face = all_pairs[-1].face_2

super().__init__(start_face, end_face)

viewpoint = operation_1.center + 2 * (operation_1.top_face.center - operation_1.bottom_face.center)
ceiling = operation_1.center + 2 * (operation_2.center - operation_1.center)
reorienter = ViewpointReorienter(viewpoint, ceiling)
reorienter.reorient(self)
33 changes: 30 additions & 3 deletions src/classy_blocks/construct/operations/operation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, List, Optional, TypeVar, Union
from typing import Dict, List, Optional, TypeVar, Union, get_args

import numpy as np

Expand All @@ -8,8 +8,9 @@
from classy_blocks.construct.flat.face import Face
from classy_blocks.construct.point import Point
from classy_blocks.grading.chop import Chop
from classy_blocks.types import AxisType, NPPointType, OrientType, ProjectToType
from classy_blocks.types import AxisType, NPPointType, OrientType, PointType, ProjectToType
from classy_blocks.util import constants
from classy_blocks.util import functions as f
from classy_blocks.util.constants import SIDES_MAP
from classy_blocks.util.frame import Frame
from classy_blocks.util.tools import edge_map
Expand Down Expand Up @@ -216,9 +217,35 @@ def get_patches_at_corner(self, corner: int) -> set:
def get_face(self, side: OrientType) -> Face:
"""Returns a new Face on specified side of the Operation.
Warning: bottom, left and front faces must be inverted prior
to using them for a loft/extrude etc."""
to using them for a loft/extrude etc (they point inside the operation by default)."""
return Face([self.point_array[i] for i in constants.FACE_MAP[side]])

def get_all_faces(self) -> Dict[OrientType, Face]:
"""Returns a list of all faces"""
return {orient: self.get_face(orient) for orient in get_args(OrientType)}

def get_closest_face(self, point: PointType) -> Face:
"""Returns a Face that has a center nearest to given point"""
point = np.array(point)
faces = list(self.get_all_faces().values())
centers = [f.norm(point - face.center) for face in faces]

return faces[np.argmin(centers)]

def get_normal_face(self, point: PointType) -> Face:
"""Returns a Face that has normal closest to
vector that connects returned face and 'point' (viewer)."""
point = np.array(point)
faces = self.get_all_faces()
orients: List[OrientType] = ["bottom", "left", "front"]

for orient in orients:
faces[orient].invert()
face_list = list(faces.values())

dotps = [np.dot(f.unit_vector(point - face.center), face.normal) for face in face_list]
return face_list[np.argmax(dotps)]

@property
def patch_names(self) -> Dict[OrientType, str]:
"""Returns patches names on sides where they are specified"""
Expand Down
22 changes: 18 additions & 4 deletions src/classy_blocks/construct/shapes/cylinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,25 @@

from classy_blocks.base import transforms as tr
from classy_blocks.base.exceptions import CylinderCreationError
from classy_blocks.construct.flat.sketches.disk import Disk
from classy_blocks.construct.flat.sketches.disk import Disk, HalfDisk
from classy_blocks.construct.shapes.rings import ExtrudedRing
from classy_blocks.construct.shapes.round import RoundSolidShape
from classy_blocks.types import PointType
from classy_blocks.util import functions as f
from classy_blocks.util.constants import TOL


class Cylinder(RoundSolidShape):
"""A Cylinder.
class SemiCylinder(RoundSolidShape):
"""Half of a cylinder; it is constructed from
given point and axis in a positive sense - right-hand rule.
Args:
axis_point_1: position of start face
axis_point_2: position of end face
radius_point_1: defines starting point and radius"""

sketch_class = HalfDisk

def __init__(self, axis_point_1: PointType, axis_point_2: PointType, radius_point_1: PointType):
axis_point_1 = np.asarray(axis_point_1)
axis = np.asarray(axis_point_2) - axis_point_1
Expand All @@ -33,7 +36,18 @@ def __init__(self, axis_point_1: PointType, axis_point_2: PointType, radius_poin

transform_2: List[tr.Transformation] = [tr.Translation(axis)]

super().__init__(Disk(axis_point_1, radius_point_1, axis), transform_2, None)
super().__init__(self.sketch_class(axis_point_1, radius_point_1, axis), transform_2, None)


class Cylinder(SemiCylinder):
"""A Cylinder.
Args:
axis_point_1: position of start face
axis_point_2: position of end face
radius_point_1: defines starting point and radius"""

sketch_class = Disk

@classmethod
def chain(cls, source: RoundSolidShape, length: float, start_face: bool = False) -> "Cylinder":
Expand Down
8 changes: 7 additions & 1 deletion src/classy_blocks/modify/reorient/viewpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def flip(self):
"""Flips the triangle so that its normal points the other way"""
self.points = np.flip(self.points, axis=0)

def orient(self, hull_center: NPPointType):
def orient(self, hull_center: NPPointType) -> None:
"""Flips the triangle around (if needed) so that
normal always points away from the provided hull center"""
if np.dot(self.center - hull_center, self.normal) < 0:
Expand Down Expand Up @@ -135,6 +135,12 @@ def _make_triangles(self, points: NPPointListType) -> List[Triangle]:
def _get_normals(self, center: NPPointType) -> Dict[OrientType, NPVectorType]:
v_observer = f.unit_vector(np.array(self.observer) - center)
v_ceiling = f.unit_vector(np.array(self.ceiling) - center)

# correct ceiling so that it's always at right angle with observer
correction = np.dot(v_ceiling, v_observer) * v_observer
v_ceiling -= correction
v_ceiling = f.unit_vector(v_ceiling)

v_left = f.unit_vector(np.cross(v_observer, v_ceiling))

return {
Expand Down
64 changes: 8 additions & 56 deletions tests/test_construct/test_operation/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,14 @@ class BoxTests(unittest.TestCase):

@parameterized.expand(
(
(
[
1,
1,
1,
],
),
(
[
-1,
1,
1,
],
),
(
[
-1,
-1,
1,
],
),
(
[
1,
-1,
1,
],
),
(
[
1,
1,
-1,
],
),
(
[
-1,
1,
-1,
],
),
(
[
-1,
-1,
-1,
],
),
(
[
1,
-1,
-1,
],
),
([1, 1, 1],),
([-1, 1, 1],),
([-1, -1, 1],),
([1, -1, 1],),
([1, 1, -1],),
([-1, 1, -1],),
([-1, -1, -1],),
([1, -1, -1],),
)
)
def test_create_box(self, diagonal_point):
Expand Down
38 changes: 38 additions & 0 deletions tests/test_construct/test_operation/test_connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import unittest

import numpy as np

from classy_blocks.construct.operations.box import Box
from classy_blocks.construct.operations.connector import Connector


class ConnectorTests(unittest.TestCase):
def setUp(self):
# basic box, center at origin
self.box_1 = Box([-0.5, -0.5, -0.5], [0.5, 0.5, 0.5])
# a 'nicely' positioned box
self.box_2 = self.box_1.copy().rotate(np.pi / 2, [0, 0, 1], [0, 0, 0]).translate([2, 0, 0])
# an ugly box a.k.a. border case
# self.box_3

def test_create_normal(self):
_ = Connector(self.box_1, self.box_2)

def test_create_inverted(self):
_ = Connector(self.box_2, self.box_1)

def test_direction(self):
connector = Connector(self.box_1, self.box_2)

box_vector = self.box_2.center - self.box_1.center
connector_vector = connector.top_face.center - connector.bottom_face.center

self.assertGreater(np.dot(box_vector, connector_vector), 0)

def test_direction_inverted(self):
connector = Connector(self.box_2, self.box_1)

box_vector = self.box_2.center - self.box_1.center
connector_vector = connector.top_face.center - connector.bottom_face.center

self.assertLess(np.dot(box_vector, connector_vector), 0)
1 change: 1 addition & 0 deletions tests/test_util/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_import_operations(self):
_ = cb.Extrude
_ = cb.Revolve
_ = cb.Wedge
_ = cb.Connector

def test_import_shapes(self):
"""Import Shapes"""
Expand Down

0 comments on commit 7c594c4

Please sign in to comment.