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
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
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
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()
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
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
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
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
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
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
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