diff --git a/.vscode/settings.json b/.vscode/settings.json index 32224f19..7b5c84ac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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, diff --git a/src/classy_blocks/construct/operations/connector.py b/src/classy_blocks/construct/operations/connector.py index 094abe20..d3e1718a 100644 --- a/src/classy_blocks/construct/operations/connector.py +++ b/src/classy_blocks/construct/operations/connector.py @@ -1,10 +1,29 @@ +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): @@ -12,7 +31,7 @@ class Connector(Operation): 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 @@ -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) diff --git a/src/classy_blocks/construct/operations/operation.py b/src/classy_blocks/construct/operations/operation.py index 14d4af24..10f26c93 100644 --- a/src/classy_blocks/construct/operations/operation.py +++ b/src/classy_blocks/construct/operations/operation.py @@ -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 @@ -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 @@ -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""" diff --git a/src/classy_blocks/construct/shapes/cylinder.py b/src/classy_blocks/construct/shapes/cylinder.py index 2983458b..ef975fe6 100644 --- a/src/classy_blocks/construct/shapes/cylinder.py +++ b/src/classy_blocks/construct/shapes/cylinder.py @@ -4,7 +4,7 @@ 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 @@ -12,14 +12,17 @@ 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 @@ -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": diff --git a/src/classy_blocks/modify/reorient/viewpoint.py b/src/classy_blocks/modify/reorient/viewpoint.py index 455b526b..2b9d695b 100644 --- a/src/classy_blocks/modify/reorient/viewpoint.py +++ b/src/classy_blocks/modify/reorient/viewpoint.py @@ -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: @@ -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 {