Skip to content

Commit

Permalink
Fix Connector for more robust lofting
Browse files Browse the repository at this point in the history
  • Loading branch information
FranzBangar committed Dec 15, 2023
1 parent c11be42 commit 120247a
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 27 deletions.
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.wordBasedSuggestions": false,
"editor.wordBasedSuggestions": "off",
"gitlens.codeLens.symbolScopes": [
"!Module"
],
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.organizeImports": "explicit"
},
},
"python.linting.enabled": true,
Expand Down
56 changes: 39 additions & 17 deletions src/classy_blocks/construct/operations/connector.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
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.types import NPPointType
from classy_blocks.util import functions as f
from classy_blocks.util.constants import FACE_MAP


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 that are closest together
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
Expand All @@ -39,26 +58,29 @@ class Connector(Operation):
recommended to chop operation 1 or 2 in axes 0 and 1 and
only provide chopping for axis 2 of connector."""

@staticmethod
def _find_closest_face(point: NPPointType, op: Operation):
"""Finds a face in operation that is closest to a given point"""
faces = [op.get_face(side) for side in FACE_MAP.keys()]
distances = [f.norm(face.center - point) for face in faces]
return faces[np.argmin(distances)]

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

start_face = self._find_closest_face(self.operation_2.center, operation_1)
end_face = self._find_closest_face(start_face.center, self.operation_2)
start_face = self._find_closest_face(end_face.center, self.operation_1)
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 = self.operation_1.center + 10 * (
self.operation_1.top_face.center - self.operation_1.bottom_face.center
)
ceiling = self.operation_1.center + 10 * (self.operation_2.center - self.operation_1.center)
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

0 comments on commit 120247a

Please sign in to comment.