Пример #1
0
def angleBetweenVectors(vector1: Vector, vector2: Vector) -> float:
        dot = vector1.dot(vector2)
        denom = vector1.length() * vector2.length()
        if denom > 1.e-3:
            angle = numpy.arccos(dot / denom)
            return 0.0 if numpy.isnan(angle) else angle
        return 0.0
Пример #2
0
def findOuterNormal(face):
    n = len(face)
    for i in range(n):
        for j in range(i + 1, n):
            edge = face[j] - face[i]
            if edge.length() > EPSILON:
                edge = edge.normalized()
                prev_rejection = Vector()
                is_outer = True
                for k in range(n):
                    if k != i and k != j:
                        pt = face[k] - face[i]
                        pte = pt.dot(edge)
                        rejection = pt - edge * pte
                        if rejection.dot(
                                prev_rejection
                        ) < -EPSILON:  # points on both sides of the edge - not an outer one
                            is_outer = False
                            break
                        elif rejection.length() > prev_rejection.length(
                        ):  # Pick a greater rejection for numeric stability
                            prev_rejection = rejection

                if is_outer:  # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
                    return edge.cross(prev_rejection)

    return False
Пример #3
0
def findOuterNormal(face):
    n = len(face)
    for i in range(n):
        for j in range(i + 1, n):
            edge = face[j] - face[i]
            if edge.length() > EPSILON:
                edge = edge.normalized()
                prev_rejection = Vector()
                is_outer = True
                for k in range(n):
                    if k != i and k != j:
                        pt = face[k] - face[i]
                        pte = pt.dot(edge)
                        rejection = pt - edge * pte
                        if rejection.dot(
                                prev_rejection) < -EPSILON:  # 边缘两边的点——不是外侧的点
                            is_outer = False
                            break
                        elif rejection.length() > prev_rejection.length(
                        ):  # 选择一个更大的拒绝数字稳定性
                            prev_rejection = rejection

                if is_outer:  # 找到一个外边缘,prev_rejection是面内的拒绝。生成一个正常。
                    return edge.cross(prev_rejection)

    return False
Пример #4
0
 def _scaleSelectedNodes(self, scale_vector: Vector) -> None:
     if scale_vector.length() == 0.0:
         return
     selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
     if len(selected_nodes) > 1:
         op = GroupedOperation()
         for node in selected_nodes:
             op.addOperation(ScaleOperation(node, scale_vector, scale_around_point=node.getWorldPosition()))
         op.push()
     else:
         for node in selected_nodes:
             ScaleOperation(node, scale_vector, scale_around_point=node.getWorldPosition()).push()
Пример #5
0
def findOuterNormal(face):
    n = len(face)
    for i in range(n):
        for j in range(i+1, n):
            edge = face[j] - face[i]
            if edge.length() > EPSILON:
                edge = edge.normalized()
                prev_rejection = Vector()
                is_outer = True
                for k in range(n):
                    if k != i and k != j:
                        pt = face[k] - face[i]
                        pte = pt.dot(edge)
                        rejection = pt - edge*pte
                        if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
                            is_outer = False
                            break
                        elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability 
                            prev_rejection = rejection
                        
                if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
                    return edge.cross(prev_rejection)

    return False
Пример #6
0
class TranslateTool(Tool):
    def __init__(self) -> None:
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle(
        )  #type: TranslateToolHandle.TranslateToolHandle #Because for some reason MyPy thinks this variable contains Optional[ToolHandle].
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key_T

        self._distance_update_time = None  #type: Optional[float]
        self._distance = None  #type: Optional[Vector]

        self.setExposedProperties("ToolHint", "X", "Y", "Z",
                                  SceneNodeSettings.LockPosition)

        self._update_selection_center_timer = QTimer()
        self._update_selection_center_timer.setInterval(50)
        self._update_selection_center_timer.setSingleShot(True)
        self._update_selection_center_timer.timeout.connect(
            self.propertyChanged.emit)

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(
            self._onSelectionCenterChanged)

        # CURA-5966 Make sure to render whenever objects get selected/deselected.
        Selection.selectionChanged.connect(self.propertyChanged)

    def _onSelectionCenterChanged(self):
        self._update_selection_center_timer.start()

    ##  Get the x-location of the selection bounding box center.
    #   \return X location in mm.
    def getX(self) -> float:
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    ##  Get the y-location of the selection bounding box center.
    #   \return Y location in mm.
    def getY(self) -> float:
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    ##  Get the z-location of the selection bounding box bottom
    #   The bottom is used as opposed to the center, because the biggest use
    #   case is to push the selection into the build plate.
    #   \return Z location in mm.
    def getZ(self) -> float:
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    @staticmethod
    def _parseFloat(str_value: str) -> float:
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    ##  Set the x-location of the selected object(s) by translating relative to
    #   the selection bounding box center.
    #   \param x Location in mm.
    def setX(self, x: str) -> None:
        parsed_x = self._parseFloat(x)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
                ):
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        x=parsed_x +
                        (world_position.x - bounding_box.center.x))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
                ):
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        x=parsed_x +
                        (world_position.x - bounding_box.center.x))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()

        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to
    #   the selection bounding box center.
    #   \param y Location in mm.
    def setY(self, y: str) -> None:
        parsed_y = self._parseFloat(y)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note; The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        z=parsed_y +
                        (world_position.z - bounding_box.center.z))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        z=parsed_y +
                        (world_position.z - bounding_box.center.z))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()

        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to
    #   the selection bounding box bottom.
    #   \param z Location in mm.
    def setZ(self, z: str) -> None:
        parsed_z = self._parseFloat(z)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom),
                                  DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note: The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        y=parsed_z + (world_position.y - bounding_box.bottom))
                    node_op = TranslateOperation(selected_node,
                                                 new_position,
                                                 set_position=True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(
                        y=parsed_z + (world_position.y - bounding_box.bottom))
                    TranslateOperation(selected_node,
                                       new_position,
                                       set_position=True).push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis List of axes (expressed as ToolHandle enum).
    def setEnabledAxis(self, axis: List[int]) -> None:
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Set lock setting to the object. This setting will be used to prevent
    #   model movement on the build plate.
    #   \param value The setting state.
    def setLockPosition(self, value: bool) -> None:
        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            selected_node.setSetting(SceneNodeSettings.LockPosition,
                                     str(value))

    def getLockPosition(self) -> Union[str, bool]:
        total_size = Selection.getCount()
        false_state_counter = 0
        true_state_counter = 0
        if not Selection.hasSelection():
            return False

        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            if selected_node.getSetting(SceneNodeSettings.LockPosition,
                                        "False") != "False":
                true_state_counter += 1
            else:
                false_state_counter += 1

        if total_size == false_state_counter:  # No locked positions
            return False
        elif total_size == true_state_counter:  # All selected objects are locked
            return True
        else:
            return "partially"  # At least one, but not all are locked

    ##  Handle mouse and keyboard events.
    #   \param event The event to handle.
    #   \return Whether this event has been caught by this tool (True) or should
    #   be passed on (False).
    def event(self, event: Event) -> bool:
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and cast(
                KeyEvent, event).key == KeyEvent.ShiftKey:
            return False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a translate operation
            if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
                return False

            if not self._selection_pass:
                return False
            id = self._selection_pass.getIdAtPosition(
                cast(MouseEvent, event).x,
                cast(MouseEvent, event).y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False

            camera = self._controller.getScene().getActiveCamera()
            if not camera:
                return False
            camera_direction = camera.getPosition().normalized()

            abs_x = abs(camera_direction.x)
            abs_y = abs(camera_direction.y)

            # We have to define a plane vector that is suitable for the selected toolhandle axis
            # and at the same time the camera direction should not be exactly perpendicular to the plane vector
            if id == ToolHandle.XAxis:
                plane_vector = Vector(0, camera_direction.y,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.YAxis:
                plane_vector = Vector(camera_direction.x, 0,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.ZAxis:
                plane_vector = Vector(camera_direction.x, camera_direction.y,
                                      0).normalized()
            else:
                if abs_y > DIRECTION_TOLERANCE:
                    plane_vector = Vector(0, 1, 0)
                elif abs_x > DIRECTION_TOLERANCE:
                    plane_vector = Vector(1, 0, 0)
                    self.setLockedAxis(
                        ToolHandle.ZAxis)  # Do not move y / vertical
                else:
                    plane_vector = Vector(0, 0, 1)
                    self.setLockedAxis(
                        ToolHandle.XAxis)  # Do not move y / vertical

            self.setDragPlane(Plane(plane_vector, 0))
            return True

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            x = cast(MouseEvent, event).x
            y = cast(MouseEvent, event).y

            if not self.getDragStart():
                self.setDragStart(x, y)
                return False

            drag = self.getDragVector(x, y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y=0, z=0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x=0, z=0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x=0, y=0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors(
                )
                if len(selected_nodes) > 1:
                    op = GroupedOperation()
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition,
                                           "False") == "False":
                            op.addOperation(TranslateOperation(node, drag))
                    op.push()
                else:
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition,
                                           "False") == "False":
                            TranslateOperation(node, drag).push()

                if not self._distance:
                    self._distance = Vector(0, 0, 0)
                self._distance += drag

            self.setDragStart(x, y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(ToolHandle.NoAxis)
                self.setDragPlane(None)
                self.setDragStart(
                    cast(MouseEvent, event).x,
                    cast(MouseEvent, event).y)
                return True

        return False

    ##  Return a formatted distance of the current translate operation.
    #   \return Fully formatted string showing the distance by which the
    #   mesh(es) are dragged.
    def getToolHint(self) -> Optional[str]:
        return "%.2f mm" % self._distance.length() if self._distance else None
Пример #7
0
class TranslateTool(Tool):
    def __init__(self):
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle()
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._distance_update_time = None
        self._distance = None

        self.setExposedProperties("ToolHint", "X", "Y", "Z")

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(self.propertyChanged)

    ##  Get the x-location of the selection bounding box center
    #
    #   \param x type(float) location in mm
    def getX(self):
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    ##  Get the y-location of the selection bounding box center
    #
    #   \param y type(float) location in mm
    def getY(self):
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    ##  Get the z-location of the selection bounding box bottom
    #   The bottom is used as opposed to the center, because the biggest usecase is to push the selection into the buildplate
    #
    #   \param z type(float) location in mm
    def getZ(self):
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    ##  Set the x-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param x type(float) location in mm
    def setX(self, x):
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(float(x), float(bounding_box.center.x),
                                  DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    x=float(x) + (world_position.x - bounding_box.center.x))
                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self.operationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param y type(float) location in mm
    def setY(self, y):
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(float(y), float(bounding_box.center.z),
                                  DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                # Note; The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    z=float(y) + (world_position.z - bounding_box.center.z))

                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self.operationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box bottom
    #
    #   \param z type(float) location in mm
    def setZ(self, z):
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(float(z), float(bounding_box.center.y),
                                  DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                # Note: The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    y=float(z) + (world_position.y - bounding_box.bottom))
                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self.operationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis type(list) list of axes (expressed as ToolHandle enum)
    def setEnabledAxis(self, axis):
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Handle mouse and keyboard events
    #
    #   \param event type(Event)
    def event(self, event):
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            return False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a translate operation
            if MouseEvent.LeftButton not in event.buttons:
                return False

            id = self._selection_pass.getIdAtPosition(event.x, event.y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False
            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                return False

            drag = self.getDragVector(event.x, event.y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y=0, z=0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x=0, z=0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x=0, y=0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                op = GroupedOperation()
                for node in Selection.getAllSelectedObjects():
                    op.addOperation(TranslateOperation(node, drag))
                op.push()

                self._distance += drag

            self.setDragStart(event.x, event.y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(None)
                self.setDragPlane(None)
                self.setDragStart(None, None)
                return True

        return False

    ##  Return a formatted distance of the current translate operation
    #
    #   \return type(String) fully formatted string showing the distance by which the mesh(es) are dragged
    def getToolHint(self):
        return "%.2f mm" % self._distance.length() if self._distance else None
Пример #8
0
class TranslateTool(Tool):
    def __init__(self):
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle()
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key_T

        self._distance_update_time = None
        self._distance = None

        self.setExposedProperties("ToolHint", "X", "Y", "Z",
                                  SceneNodeSettings.LockPosition)

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(self.propertyChanged)

    ##  Get the x-location of the selection bounding box center
    #
    #   \param x type(float) location in mm
    def getX(self):
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    ##  Get the y-location of the selection bounding box center
    #
    #   \param y type(float) location in mm
    def getY(self):
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    ##  Get the z-location of the selection bounding box bottom
    #   The bottom is used as opposed to the center, because the biggest usecase is to push the selection into the buildplate
    #
    #   \param z type(float) location in mm
    def getZ(self):
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    def _parseInt(self, str_value):
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    ##  Set the x-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param x type(float) location in mm
    def setX(self, x):
        Benchmark.start("Moving object in X from {start} to {end}".format(
            start=self.getX(), end=x))
        parsed_x = self._parseInt(x)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x),
                                  DIMENSION_TOLERANCE):
            for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
            ):
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    x=parsed_x + (world_position.x - bounding_box.center.x))
                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param y type(float) location in mm
    def setY(self, y):
        Benchmark.start("Moving object in Y from {start} to {end}".format(
            start=self.getY(), end=y))
        parsed_y = self._parseInt(y)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z),
                                  DIMENSION_TOLERANCE):
            for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
            ):
                # Note; The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    z=parsed_y + (world_position.z - bounding_box.center.z))

                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box bottom
    #
    #   \param z type(float) location in mm
    def setZ(self, z):
        Benchmark.start("Moving object in Z from {start} to {end}".format(
            start=self.getZ(), end=z))
        parsed_z = self._parseInt(z)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom),
                                  DIMENSION_TOLERANCE):
            for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
            ):
                # Note: The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(
                    y=parsed_z + (world_position.y - bounding_box.bottom))
                node_op = TranslateOperation(selected_node,
                                             new_position,
                                             set_position=True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis type(list) list of axes (expressed as ToolHandle enum)
    def setEnabledAxis(self, axis):
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Set lock setting to the object. This setting will be used to prevent model movement on the build plate
    #
    #   \param value type(bool) the setting state
    def setLockPosition(self, value):
        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            selected_node.setSetting(SceneNodeSettings.LockPosition, value)

    def getLockPosition(self):
        total_size = Selection.getCount()
        false_state_counter = 0
        true_state_counter = 0
        if Selection.hasSelection():
            for selected_node in self._getSelectedObjectsWithoutSelectedAncestors(
            ):

                if selected_node.getSetting(SceneNodeSettings.LockPosition,
                                            False):
                    true_state_counter += 1
                else:
                    false_state_counter += 1

            if total_size == false_state_counter:  # if no locked positions
                return False
            elif total_size == true_state_counter:  # if all selected objects are locked
                return True
            else:
                return "partially"  # if at least one is locked

        return False

    ##  Handle mouse and keyboard events
    #
    #   \param event type(Event)
    def event(self, event):
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            return False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a translate operation
            if MouseEvent.LeftButton not in event.buttons:
                return False

            id = self._selection_pass.getIdAtPosition(event.x, event.y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False

            camera_direction = self._controller.getScene().getActiveCamera(
            ).getPosition().normalized()

            abs_x = abs(camera_direction.x)
            abs_y = abs(camera_direction.y)

            # We have to define a plane vector that is suitable for the selected toolhandle axis
            # and at the same time the camera direction should not be exactly perpendicular to the plane vector
            if id == ToolHandle.XAxis:
                plane_vector = Vector(0, camera_direction.y,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.YAxis:
                plane_vector = Vector(camera_direction.x, 0,
                                      camera_direction.z).normalized()
            elif id == ToolHandle.ZAxis:
                plane_vector = Vector(camera_direction.x, camera_direction.y,
                                      0).normalized()
            else:
                if abs_y > DIRECTION_TOLERANCE:
                    plane_vector = Vector(0, 1, 0)
                elif abs_x > DIRECTION_TOLERANCE:
                    plane_vector = Vector(1, 0, 0)
                    self.setLockedAxis(
                        ToolHandle.ZAxis)  # Do not move y / vertical
                else:
                    plane_vector = Vector(0, 0, 1)
                    self.setLockedAxis(
                        ToolHandle.XAxis)  # Do not move y / vertical

            self.setDragPlane(Plane(plane_vector, 0))

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                return False

            drag = self.getDragVector(event.x, event.y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y=0, z=0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x=0, z=0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x=0, y=0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                op = GroupedOperation()
                for node in self._getSelectedObjectsWithoutSelectedAncestors():
                    if not node.getSetting(SceneNodeSettings.LockPosition,
                                           False):
                        op.addOperation(TranslateOperation(node, drag))

                op.push()

                self._distance += drag

            self.setDragStart(event.x, event.y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(ToolHandle.NoAxis)
                self.setDragPlane(None)
                self.setDragStart(None, None)
                return True

        return False

    ##  Return a formatted distance of the current translate operation
    #
    #   \return type(String) fully formatted string showing the distance by which the mesh(es) are dragged
    def getToolHint(self):
        return "%.2f mm" % self._distance.length() if self._distance else None
Пример #9
0
class TranslateTool(Tool):
    def __init__(self):
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle()
        self._enabled_axis = [ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key_Q

        self._distance_update_time = None
        self._distance = None

        self.setExposedProperties("ToolHint", "X", "Y", "Z")

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(self.propertyChanged)


    ##  Get the x-location of the selection bounding box center
    #
    #   \param x type(float) location in mm
    def getX(self):
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    ##  Get the y-location of the selection bounding box center
    #
    #   \param y type(float) location in mm
    def getY(self):
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    ##  Get the z-location of the selection bounding box bottom
    #   The bottom is used as opposed to the center, because the biggest usecase is to push the selection into the buildplate
    #
    #   \param z type(float) location in mm
    def getZ(self):
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    def _parseInt(self, str_value):
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    ##  Set the x-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param x type(float) location in mm
    def setX(self, x):
        parsed_x = self._parseInt(x)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x), DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(x=parsed_x + (world_position.x - bounding_box.center.x))
                node_op = TranslateOperation(selected_node, new_position, set_position = True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box center
    #
    #   \param y type(float) location in mm
    def setY(self, y):
        parsed_y = self._parseInt(y)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z), DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                # Note; The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(z=parsed_y + (world_position.z - bounding_box.center.z))

                node_op = TranslateOperation(selected_node, new_position, set_position = True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to the selection bounding box bottom
    #
    #   \param z type(float) location in mm
    def setZ(self, z):
        parsed_z = self._parseInt(z)
        bounding_box = Selection.getBoundingBox()

        op = GroupedOperation()
        if not Float.fuzzyCompare(parsed_z, float(bounding_box.center.y), DIMENSION_TOLERANCE):
            for selected_node in Selection.getAllSelectedObjects():
                # Note: The switching of z & y is intentional. We display z as up for the user,
                # But store the data in openGL space.
                world_position = selected_node.getWorldPosition()
                new_position = world_position.set(y=parsed_z + (world_position.y - bounding_box.bottom))
                node_op = TranslateOperation(selected_node, new_position, set_position = True)
                op.addOperation(node_op)
            op.push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis type(list) list of axes (expressed as ToolHandle enum)
    def setEnabledAxis(self, axis):
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Handle mouse and keyboard events
    #
    #   \param event type(Event)
    def event(self, event):
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            return False


        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
            # Start a translate operation
            if MouseEvent.LeftButton not in event.buttons:
                return False

            id = self._selection_pass.getIdAtPosition(event.x, event.y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False
            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                return False

            drag = self.getDragVector(event.x, event.y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y=0, z=0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x=0, z=0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x=0, y=0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                op = GroupedOperation()
                for node in Selection.getAllSelectedObjects():
                    op.addOperation(TranslateOperation(node, drag))
                op.push()

                self._distance += drag

            self.setDragStart(event.x, event.y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(None)
                self.setDragPlane(None)
                self.setDragStart(None, None)
                return True

        return False

    ##  Return a formatted distance of the current translate operation
    #
    #   \return type(String) fully formatted string showing the distance by which the mesh(es) are dragged
    def getToolHint(self):
        return "%.2f mm" % self._distance.length() if self._distance else None
Пример #10
0
class TranslateTool(Tool):
    def __init__(self):
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle()
        self._enabled_axis = [
            ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis
        ]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._distance_update_time = None
        self._distance = None

        self.setExposedProperties("ToolHint", "X", "Y", "Z")

    def getX(self):
        if Selection.hasSelection():
            return float(Selection.getSelectedObject(0).getWorldPosition().x)
        return 0.0

    def getY(self):
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getSelectedObject(0).getWorldPosition().z)
        return 0.0

    def getZ(self):
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            selected_node = Selection.getSelectedObject(0)
            try:
                bottom = selected_node.getBoundingBox().bottom
            except AttributeError:  #It can happen that there is no bounding box yet.
                bottom = 0

            return float(bottom)
        return 0.0

    def setX(self, x):
        obj = Selection.getSelectedObject(0)
        if obj:
            new_position = obj.getWorldPosition()
            new_position.setX(x)
            Selection.applyOperation(TranslateOperation,
                                     new_position,
                                     set_position=True)
            self.operationStopped.emit(self)

    def setY(self, y):
        obj = Selection.getSelectedObject(0)
        if obj:
            new_position = obj.getWorldPosition()

            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            new_position.setZ(y)
            Selection.applyOperation(TranslateOperation,
                                     new_position,
                                     set_position=True)
            self.operationStopped.emit(self)

    def setZ(self, z):
        obj = Selection.getSelectedObject(0)
        if obj:
            new_position = obj.getWorldPosition()
            selected_node = Selection.getSelectedObject(0)
            center = selected_node.getBoundingBox().center
            bottom = selected_node.getBoundingBox().bottom
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.

            new_position.setY(float(z) + (center.y - bottom))
            Selection.applyOperation(TranslateOperation,
                                     new_position,
                                     set_position=True)
            self.operationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis type(list) list of axes (expressed as ToolHandle enum)
    def setEnabledAxis(self, axis):
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Handle mouse and keyboard events
    #
    #   \param event type(Event)
    def event(self, event):
        super().event(event)

        # Make sure the displayed values are updated if the boundingbox of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in Selection.getAllSelectedObjects():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            # Snap-to-grid is turned on when pressing the shift button
            self._grid_snap = True

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            # Snap-to-grid is turned off when releasing the shift button
            self._grid_snap = False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a translate operation

            if MouseEvent.LeftButton not in event.buttons:
                return False

            id = self._selection_pass.getIdAtPosition(event.x, event.y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False
            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), 0))
            elif id == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), 0))

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                return False

            drag = self.getDragVector(event.x, event.y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag.setY(0)
                    drag.setZ(0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag.setX(0)
                    drag.setZ(0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag.setX(0)
                    drag.setY(0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                Selection.applyOperation(TranslateOperation, drag)
                self._distance += drag

            self.setDragStart(event.x, event.y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                # Force scene changed event. Some plugins choose to ignore move events when operation is in progress.
                if self._moved:
                    for node in Selection.getAllSelectedObjects():
                        Application.getInstance().getController().getScene(
                        ).sceneChanged.emit(node)
                    self._moved = False
                self.setLockedAxis(None)
                self.setDragPlane(None)
                self.setDragStart(None, None)
                return True

        return False

    ##  Return a formatted distance of the current translate operation
    #
    #   \return type(String) fully formatted string showing the distance by which the mesh(es) are dragged
    def getToolHint(self):
        return "%.2f mm" % self._distance.length() if self._distance else None
Пример #11
0
class TranslateTool(Tool):
    def __init__(self) -> None:
        super().__init__()

        self._handle = TranslateToolHandle.TranslateToolHandle() #type: TranslateToolHandle.TranslateToolHandle #Because for some reason MyPy thinks this variable contains Optional[ToolHandle].
        self._enabled_axis = [ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis]

        self._grid_snap = False
        self._grid_size = 10
        self._moved = False

        self._shortcut_key = Qt.Key_T

        self._distance_update_time = None #type: Optional[float]
        self._distance = None #type: Optional[Vector]

        self.setExposedProperties("ToolHint",
                                  "X", "Y", "Z",
                                  SceneNodeSettings.LockPosition)

        # Ensure that the properties (X, Y & Z) are updated whenever the selection center is changed.
        Selection.selectionCenterChanged.connect(self.propertyChanged)

        # CURA-5966 Make sure to render whenever objects get selected/deselected.
        Selection.selectionChanged.connect(self.propertyChanged)

    ##  Get the x-location of the selection bounding box center.
    #   \return X location in mm.
    def getX(self) -> float:
        if Selection.hasSelection():
            return float(Selection.getBoundingBox().center.x)
        return 0.0

    ##  Get the y-location of the selection bounding box center.
    #   \return Y location in mm.
    def getY(self) -> float:
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().center.z)
        return 0.0

    ##  Get the z-location of the selection bounding box bottom
    #   The bottom is used as opposed to the center, because the biggest use
    #   case is to push the selection into the build plate.
    #   \return Z location in mm.
    def getZ(self) -> float:
        # We want to display based on the bottom instead of the actual coordinate.
        if Selection.hasSelection():
            # Note; The switching of z & y is intentional. We display z as up for the user,
            # But store the data in openGL space.
            return float(Selection.getBoundingBox().bottom)
        return 0.0

    def _parseInt(self, str_value: str) -> float:
        try:
            parsed_value = float(str_value)
        except ValueError:
            parsed_value = float(0)
        return parsed_value

    ##  Set the x-location of the selected object(s) by translating relative to
    #   the selection bounding box center.
    #   \param x Location in mm.
    def setX(self, x: str) -> None:
        parsed_x = self._parseInt(x)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x), DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors():
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(x = parsed_x + (world_position.x - bounding_box.center.x))
                    node_op = TranslateOperation(selected_node, new_position, set_position = True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in self._getSelectedObjectsWithoutSelectedAncestors():
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(x = parsed_x + (world_position.x - bounding_box.center.x))
                    TranslateOperation(selected_node, new_position, set_position = True).push()

        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to
    #   the selection bounding box center.
    #   \param y Location in mm.
    def setY(self, y: str) -> None:
        parsed_y = self._parseInt(y)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z), DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note; The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(z = parsed_y + (world_position.z - bounding_box.center.z))
                    node_op = TranslateOperation(selected_node, new_position, set_position = True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(z = parsed_y + (world_position.z - bounding_box.center.z))
                    TranslateOperation(selected_node, new_position, set_position = True).push()

        self._controller.toolOperationStopped.emit(self)

    ##  Set the y-location of the selected object(s) by translating relative to
    #   the selection bounding box bottom.
    #   \param z Location in mm.
    def setZ(self, z: str) -> None:
        parsed_z = self._parseInt(z)
        bounding_box = Selection.getBoundingBox()

        if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom), DIMENSION_TOLERANCE):
            selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
            if len(selected_nodes) > 1:
                op = GroupedOperation()
                for selected_node in selected_nodes:
                    # Note: The switching of z & y is intentional. We display z as up for the user,
                    # But store the data in openGL space.
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(y = parsed_z + (world_position.y - bounding_box.bottom))
                    node_op = TranslateOperation(selected_node, new_position, set_position = True)
                    op.addOperation(node_op)
                op.push()
            else:
                for selected_node in selected_nodes:
                    world_position = selected_node.getWorldPosition()
                    new_position = world_position.set(y=parsed_z + (world_position.y - bounding_box.bottom))
                    TranslateOperation(selected_node, new_position, set_position=True).push()
        self._controller.toolOperationStopped.emit(self)

    ##  Set which axis/axes are enabled for the current translate operation
    #
    #   \param axis List of axes (expressed as ToolHandle enum).
    def setEnabledAxis(self, axis: List[int]) -> None:
        self._enabled_axis = axis
        self._handle.setEnabledAxis(axis)

    ##  Set lock setting to the object. This setting will be used to prevent
    #   model movement on the build plate.
    #   \param value The setting state.
    def setLockPosition(self, value: bool) -> None:
        for selected_node in self._getSelectedObjectsWithoutSelectedAncestors():
            selected_node.setSetting(SceneNodeSettings.LockPosition, str(value))

    def getLockPosition(self) -> Union[str, bool]:
        total_size = Selection.getCount()
        false_state_counter = 0
        true_state_counter = 0
        if Selection.hasSelection():
            for selected_node in self._getSelectedObjectsWithoutSelectedAncestors():
                if selected_node.getSetting(SceneNodeSettings.LockPosition, "False") != "False":
                    true_state_counter += 1
                else:
                    false_state_counter += 1

            if total_size == false_state_counter: # if no locked positions
                return False
            elif total_size == true_state_counter: # if all selected objects are locked
                return True
            else:
                return "partially"  # if at least one is locked

        return False

    ##  Handle mouse and keyboard events.
    #   \param event The event to handle.
    #   \return Whether this event has been caught by this tool (True) or should
    #   be passed on (False).
    def event(self, event: Event) -> bool:
        super().event(event)

        # Make sure the displayed values are updated if the bounding box of the selected mesh(es) changes
        if event.type == Event.ToolActivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.connect(self.propertyChanged)

        if event.type == Event.ToolDeactivateEvent:
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                node.boundingBoxChanged.disconnect(self.propertyChanged)

        if event.type == Event.KeyPressEvent and cast(KeyEvent, event).key == KeyEvent.ShiftKey:
            return False

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
            # Start a translate operation
            if MouseEvent.LeftButton not in cast(MouseEvent, event).buttons:
                return False

            if not self._selection_pass:
                return False
            id = self._selection_pass.getIdAtPosition(cast(MouseEvent, event).x, cast(MouseEvent, event).y)
            if not id:
                return False

            if id in self._enabled_axis:
                self.setLockedAxis(id)
            elif self._handle.isAxis(id):
                return False

            self._moved = False

            camera = self._controller.getScene().getActiveCamera()
            if not camera:
                return False
            camera_direction = camera.getPosition().normalized()

            abs_x = abs(camera_direction.x)
            abs_y = abs(camera_direction.y)

            # We have to define a plane vector that is suitable for the selected toolhandle axis
            # and at the same time the camera direction should not be exactly perpendicular to the plane vector
            if id == ToolHandle.XAxis:
                plane_vector = Vector(0, camera_direction.y, camera_direction.z).normalized()
            elif id == ToolHandle.YAxis:
                plane_vector = Vector(camera_direction.x, 0, camera_direction.z).normalized()
            elif id == ToolHandle.ZAxis:
                plane_vector = Vector(camera_direction.x, camera_direction.y, 0).normalized()
            else:
                if abs_y > DIRECTION_TOLERANCE:
                    plane_vector = Vector(0, 1, 0)
                elif abs_x > DIRECTION_TOLERANCE:
                    plane_vector = Vector(1, 0, 0)
                    self.setLockedAxis(ToolHandle.ZAxis)  # Do not move y / vertical
                else:
                    plane_vector = Vector(0, 0, 1)
                    self.setLockedAxis(ToolHandle.XAxis)  # Do not move y / vertical

            self.setDragPlane(Plane(plane_vector, 0))
            return True

        if event.type == Event.MouseMoveEvent:
            # Perform a translate operation

            if not self.getDragPlane():
                return False

            x = cast(MouseEvent, event).x
            y = cast(MouseEvent, event).y

            if not self.getDragStart():
                self.setDragStart(x, y)
                return False

            drag = self.getDragVector(x, y)
            if drag:
                if self._grid_snap and drag.length() < self._grid_size:
                    return False

                if self.getLockedAxis() == ToolHandle.XAxis:
                    drag = drag.set(y = 0, z = 0)
                elif self.getLockedAxis() == ToolHandle.YAxis:
                    drag = drag.set(x = 0, z = 0)
                elif self.getLockedAxis() == ToolHandle.ZAxis:
                    drag = drag.set(x = 0, y = 0)

                if not self._moved:
                    self._moved = True
                    self._distance = Vector(0, 0, 0)
                    self.operationStarted.emit(self)

                selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors()
                if len(selected_nodes) > 1:
                    op = GroupedOperation()
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition, "False") == "False":
                            op.addOperation(TranslateOperation(node, drag))
                    op.push()
                else:
                    for node in selected_nodes:
                        if node.getSetting(SceneNodeSettings.LockPosition, "False") == "False":
                            TranslateOperation(node, drag).push()

                if not self._distance:
                    self._distance = Vector(0, 0, 0)
                self._distance += drag

            self.setDragStart(x, y)

            # Rate-limit the angle change notification
            # This is done to prevent the UI from being flooded with property change notifications,
            # which in turn would trigger constant repaints.
            new_time = time.monotonic()
            if not self._distance_update_time or new_time - self._distance_update_time > 0.1:
                self.propertyChanged.emit()
                self._distance_update_time = new_time

            return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a translate operation
            if self.getDragPlane():
                self.operationStopped.emit(self)
                self._distance = None
                self.propertyChanged.emit()
                self.setLockedAxis(ToolHandle.NoAxis)
                self.setDragPlane(None)
                self.setDragStart(cast(MouseEvent, event).x, cast(MouseEvent, event).y)
                return True

        return False

    ##  Return a formatted distance of the current translate operation.
    #   \return Fully formatted string showing the distance by which the
    #   mesh(es) are dragged.
    def getToolHint(self) -> Optional[str]:
        return "%.2f mm" % self._distance.length() if self._distance else None