コード例 #1
0
ファイル: ReadMeshJob.py プロジェクト: johntron/Uranium
    def run(self):
        loading_message = Message(i18n_catalog.i18nc("Loading mesh message, {0} is file name", "Loading {0}").format(self._filename), lifetime = 0, dismissable = False)
        loading_message.setProgress(-1)
        loading_message.show()

        mesh = self._handler.read(self._filename, self._device)

        # Scale down to maximum bounds size if that is available
        if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"):
            max_bounds = Application.getInstance().getController().getScene()._maximum_bounds
            bbox = mesh.getExtents()

            if max_bounds.width < bbox.width or max_bounds.height < bbox.height or max_bounds.depth < bbox.depth:
                largest_dimension = max(bbox.width, bbox.height, bbox.depth)

                scale_factor = 1.0
                if largest_dimension == bbox.width:
                    scale_factor = max_bounds.width / bbox.width
                elif largest_dimension == bbox.height:
                    scale_factor = max_bounds.height / bbox.height
                else:
                    scale_factor = max_bounds.depth / bbox.depth

                matrix = Matrix()
                matrix.setByScaleFactor(scale_factor)
                mesh = mesh.getTransformed(matrix)

        self.setResult(mesh)

        loading_message.hide()
        result_message = Message(i18n_catalog.i18nc("Finished loading mesh message, {0} is file name", "Loaded {0}").format(self._filename))
        result_message.show()
コード例 #2
0
ファイル: BackendProxy.py プロジェクト: kelvinfang/Uranium
class BackendProxy(QObject):
    def __init__(self, parent = None):
        super().__init__(parent)
        self._backend = Application.getInstance().getBackend()
        self._progress = -1;
        self._messageDisplayed = False
        self._message = None
        if self._backend:
            self._backend.processingProgress.connect(self._onProcessingProgress)

    processingProgress = pyqtSignal(float, arguments = ["amount"])
    
    @pyqtProperty(float, notify = processingProgress)
    def progress(self):
        if self._progress > 0 and self._progress < 1 and self._messageDisplayed == False:
            self._message = Message(i18n_catalog.i18n("Slicing in Process: "), 0, False, self._progress)
            self._message.show()
            self._messageDisplayed = True
        if self._progress >= 1 and self._messageDisplayed == True:
            self._message.hide()
            self._messageDisplayed = False
        return self._progress

    def _onProcessingProgress(self, amount):
        self._progress = amount
        self.processingProgress.emit(amount)
コード例 #3
0
ファイル: ReadMeshJob.py プロジェクト: fieldOfView/Uranium
    def run(self):
        loading_message = Message(i18n_catalog.i18nc("@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime = 0, dismissable = False)
        loading_message.setProgress(-1)
        loading_message.show()

        Job.yieldThread() # Yield to any other thread that might want to do something else.

        try:
            begin_time = time.time()
            node = self._handler.read(self._filename)
            end_time = time.time()
            Logger.log("d", "Loading mesh took %s seconds", end_time - begin_time)
        except Exception as e:
            print(e)
        if not node:
            loading_message.hide()

            result_message = Message(i18n_catalog.i18nc("@info:status", "Failed to load <filename>{0}</filename>", self._filename))
            result_message.show()
            return
        if node.getMeshData():
            node.getMeshData().setFileName(self._filename)
        # Scale down to maximum bounds size if that is available
        if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"):
            max_bounds = Application.getInstance().getController().getScene()._maximum_bounds
            node._resetAABB()
            bounding_box = node.getBoundingBox()
            timeout_counter = 0
            #As the calculation of the bounding box is in a seperate thread it might be that it's not done yet.
            while bounding_box.width == 0 or bounding_box.height == 0 or bounding_box.depth == 0:
                bounding_box = node.getBoundingBox()
                time.sleep(0.1)
                timeout_counter += 1
                if timeout_counter > 10:
                    break
            if max_bounds.width < bounding_box.width or max_bounds.height < bounding_box.height or max_bounds.depth < bounding_box.depth:
                scale_factor_width = max_bounds.width / bounding_box.width
                scale_factor_height = max_bounds.height / bounding_box.height
                scale_factor_depth = max_bounds.depth / bounding_box.depth
                scale_factor = min(scale_factor_width,scale_factor_height,scale_factor_depth)

                scale_vector = Vector(scale_factor, scale_factor, scale_factor)
                display_scale_factor = scale_factor * 100

                if Preferences.getInstance().getValue("mesh/scale_to_fit") == True:
                    scale_message = Message(i18n_catalog.i18nc("@info:status", "Auto scaled object to {0}% of original size", ("%i" % display_scale_factor)))

                    try:
                        node.scale(scale_vector)
                        scale_message.show()
                    except Exception as e:
                        print(e)

        self.setResult(node)

        loading_message.hide()
        result_message = Message(i18n_catalog.i18nc("@info:status", "Loaded <filename>{0}</filename>", self._filename))
        result_message.show()
コード例 #4
0
ファイル: ArrangeObjectsJob.py プロジェクト: daid/Cura
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0)
        status_message.show()
        arranger = Arrange.create(fixed_nodes = self._fixed_nodes)

        # Collect nodes to be placed
        nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
        for node in self._nodes:
            offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
            nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))

        # Sort the nodes with the biggest area first.
        nodes_arr.sort(key=lambda item: item[0])
        nodes_arr.reverse()

        # Place nodes one at a time
        start_priority = 0
        last_priority = start_priority
        last_size = None
        grouped_operation = GroupedOperation()
        found_solution_for_all = True
        for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
            # For performance reasons, we assume that when a location does not fit,
            # it will also not fit for the next object (while what can be untrue).
            # We also skip possibilities by slicing through the possibilities (step = 10)
            if last_size == size:  # This optimization works if many of the objects have the same size
                start_priority = last_priority
            else:
                start_priority = 0
            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
            x, y = best_spot.x, best_spot.y
            node.removeDecorator(ZOffsetDecorator)
            if node.getBoundingBox():
                center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
            else:
                center_y = 0
            if x is not None:  # We could find a place
                last_size = size
                last_priority = best_spot.priority

                arranger.place(x, y, hull_shape_arr)  # take place before the next one

                grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
            else:
                Logger.log("d", "Arrange all: could not find spot!")
                found_solution_for_all = False
                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))

            status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
            Job.yieldThread()

        grouped_operation.push()

        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"))
            no_full_solution_message.show()
コード例 #5
0
ファイル: ReadFileJob.py プロジェクト: Ultimaker/Uranium
class ReadFileJob(Job):
    def __init__(self, filename: str, handler: Optional[FileHandler] = None) -> None:
        super().__init__()
        self._filename = filename
        self._handler = handler
        self._loading_message = None  # type: Optional[Message]

    def getFileName(self):
        return self._filename

    def run(self) -> None:
        if self._handler is None:
            Logger.log("e", "FileHandler was not set.")
            return None
        reader = self._handler.getReaderForFile(self._filename)
        if not reader:
            result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Cannot open files of the type of <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File"))
            result_message.show()
            return

        # Give the plugin a chance to display a dialog before showing the loading UI
        try:
            pre_read_result = reader.preRead(self._filename)
        except:
            Logger.logException("e", "Failed to pre-read the file %s", self._filename)
            pre_read_result = MeshReader.PreReadResult.failed

        if pre_read_result != MeshReader.PreReadResult.accepted:
            if pre_read_result == MeshReader.PreReadResult.failed:
                result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename),
                                         lifetime=0,
                                         title = i18n_catalog.i18nc("@info:title", "Invalid File"))
                result_message.show()
            return

        self._loading_message = Message(self._filename,
                                        lifetime=0,
                                        dismissable=False,
                                        title = i18n_catalog.i18nc("@info:title", "Loading"))
        self._loading_message.setProgress(-1)
        self._loading_message.show()

        Job.yieldThread()  # Yield to any other thread that might want to do something else.
        begin_time = time.time()
        try:
            self.setResult(self._handler.readerRead(reader, self._filename))
        except:
            Logger.logException("e", "Exception occurred while loading file %s", self._filename)
        finally:
            end_time = time.time()
            Logger.log("d", "Loading file took %0.1f seconds", end_time - begin_time)
            if self._result is None:
                self._loading_message.hide()
                result_message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to load <filename>{0}</filename>", self._filename), lifetime=0, title = i18n_catalog.i18nc("@info:title", "Invalid File"))
                result_message.show()
                return
            self._loading_message.hide()
コード例 #6
0
ファイル: ReadMeshJob.py プロジェクト: kelvinfang/Uranium
    def run(self):
        loading_message = Message(i18n_catalog.i18nc("@info:status", "Loading <filename>{0}</filename>", self._filename), lifetime = 0, dismissable = False)
        loading_message.setProgress(-1)
        loading_message.show()

        try:
            node = self._handler.read(self._filename)
        except Exception as e:
            print(e)
        if not node:
            loading_message.hide()

            result_message = Message(i18n_catalog.i18nc("@info:status", "Failed to load <filename>{0}</filename>", self._filename))
            result_message.show()
            return

        # Scale down to maximum bounds size if that is available
        if hasattr(Application.getInstance().getController().getScene(), "_maximum_bounds"):
            max_bounds = Application.getInstance().getController().getScene()._maximum_bounds
            node._resetAABB()
            bounding_box = node.getBoundingBox()
            timeout_counter = 0
            #As the calculation of the bounding box is in a seperate thread it might be that it's not done yet.
            while bounding_box.width == 0 or bounding_box.height == 0 or bounding_box.depth == 0:
                bounding_box = node.getBoundingBox()
                time.sleep(0.1)
                timeout_counter += 1
                if timeout_counter > 10:
                    break
            if max_bounds.width < bounding_box.width or max_bounds.height < bounding_box.height or max_bounds.depth < bounding_box.depth:
                largest_dimension = max(bounding_box.width, bounding_box.height, bounding_box.depth)

                scale_factor = 1.0
                if largest_dimension == bounding_box.width:
                    scale_factor = max_bounds.width / bounding_box.width
                elif largest_dimension == bounding_box.height:
                    scale_factor = max_bounds.height / bounding_box.height
                else:
                    scale_factor = max_bounds.depth / bounding_box.depth

                scale_vector = Vector(scale_factor, scale_factor, scale_factor)
                scale_message = Message(i18n_catalog.i18nc("", "Auto scaled object to {0} % of original size", ("%.2f" % scale_factor)))

                try:
                    node.scale(scale_vector)
                    scale_message.show()
                except Exception as e:
                    print(e)

        self.setResult(node)

        loading_message.hide()
        result_message = Message(i18n_catalog.i18nc("@info:status", "Loaded <filename>{0}</filename>", self._filename))
        result_message.show()
コード例 #7
0
ファイル: MultiplyObjectsJob.py プロジェクト: olemis/Cura
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
                                 dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object"))
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        root = scene.getRoot()
        arranger = Arrange.create(scene_root=root)
        nodes = []
        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
                current_node = current_node.getParent()

            node_too_big = False
            if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
            else:
                node_too_big = True

            found_solution_for_all = True
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                if not node_too_big:
                    node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = node.getPosition()
                    new_location = new_location.set(z = 100 - i * 20)
                    node.setPosition(new_location)

                nodes.append(node)
                current_progress += 1
                status_message.setProgress((current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object"))
            no_full_solution_message.show()
コード例 #8
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
                                 dismissable=False, progress=0)
        status_message.show()
        scene = Application.getInstance().getController().getScene()
        node = scene.findObject(self._object_id)

        if not node and self._object_id != 0:  # Workaround for tool handles overlapping the selected object
            node = Selection.getSelectedObject(0)

        # If object is part of a group, multiply group
        current_node = node
        while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
            current_node = current_node.getParent()

        root = scene.getRoot()
        arranger = Arrange.create(scene_root=root)
        offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
        nodes = []
        found_solution_for_all = True
        for i in range(self._count):
            # We do place the nodes one by one, as we want to yield in between.
            node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
            if not solution_found:
                found_solution_for_all = False
                new_location = node.getPosition()
                new_location = new_location.set(z = 100 - i * 20)
                node.setPosition(new_location)

            nodes.append(node)
            Job.yieldThread()
            status_message.setProgress((i + 1) / self._count * 100)

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"))
            no_full_solution_message.show()
コード例 #9
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
                                 dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        machine_width = global_container_stack.getProperty("machine_width", "value")
        machine_depth = global_container_stack.getProperty("machine_depth", "value")

        root = scene.getRoot()
        scale = 0.5
        arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
        processed_nodes = []
        nodes = []

        not_fit_count = 0

        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and (current_node.getParent().callDecoration("isGroup") or current_node.getParent().callDecoration("isSliceable")):
                current_node = current_node.getParent()

            if current_node in processed_nodes:
                continue
            processed_nodes.append(current_node)

            node_too_big = False
            if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
            else:
                node_too_big = True

            found_solution_for_all = True
            arranger.resetLastPriority()
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                new_node = copy.deepcopy(node)
                solution_found = False
                if not node_too_big:
                    solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)

                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = new_node.getPosition()
                    new_location = new_location.set(z = - not_fit_count * 20)
                    new_node.setPosition(new_location)
                    not_fit_count += 1

                # Same build plate
                build_plate_number = current_node.callDecoration("getBuildPlateNumber")
                new_node.callDecoration("setBuildPlateNumber", build_plate_number)
                for child in new_node.getChildren():
                    child.callDecoration("setBuildPlateNumber", build_plate_number)

                nodes.append(new_node)
                current_progress += 1
                status_message.setProgress((current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object"))
            no_full_solution_message.show()
コード例 #10
0
ファイル: RotateTool.py プロジェクト: dreamsxin/Uranium
class RotateTool(Tool):
    """Provides the tool to rotate meshes and groups
    
    The tool exposes a ToolHint to show the rotation angle of the current operation
    """
    def __init__(self):
        super().__init__()
        self._handle = RotateToolHandle.RotateToolHandle()

        self._snap_rotation = True
        self._snap_angle = math.radians(15)

        self._angle = None
        self._angle_update_time = None

        self._shortcut_key = Qt.Key_R

        self._progress_message = None
        self._iterations = 0
        self._total_iterations = 0
        self._rotating = False
        self.setExposedProperties("ToolHint", "RotationSnap",
                                  "RotationSnapAngle", "SelectFaceSupported",
                                  "SelectFaceToLayFlatMode")
        self._saved_node_positions = []

        self._active_widget = None  # type: Optional[RotateToolHandle.ExtraWidgets]
        self._widget_click_start = 0

        self._select_face_mode = False
        Selection.selectedFaceChanged.connect(self._onSelectedFaceChanged)

    def event(self, event):
        """Handle mouse and keyboard events
        
        :param event: type(Event)
        """

        super().event(event)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            # Snap is toggled when pressing the shift button
            self.setRotationSnap(not self._snap_rotation)

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            # Snap is "toggled back" when releasing the shift button
            self.setRotationSnap(not self._snap_rotation)

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a rotate 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._handle.getExtraWidgetsColorMap():
                self._active_widget = self._handle.ExtraWidgets[id]
                self._widget_click_start = time.monotonic()
                # Continue as if the picked widget is the appropriate axis
                id = math.floor(
                    self._active_widget.value / 2) + self._handle.XAxis

            if self._handle.isAxis(id):
                self.setLockedAxis(id)
            else:
                # Not clicked on an axis: do nothing.
                return False

            handle_position = self._handle.getWorldPosition()

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in self._getSelectedObjectsWithoutSelectedAncestors():
                self._saved_node_positions.append((node, node.getPosition()))

            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))
            elif self._locked_axis == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))

            self.setDragStart(event.x, event.y)
            self._rotating = False
            self._angle = 0
            return True

        if event.type == Event.MouseMoveEvent:
            # Perform a rotate operation
            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                if not self.getDragStart():  #May have set it to None.
                    return False

            if not self._rotating:
                self._rotating = True
                self.operationStarted.emit(self)

            handle_position = self._handle.getWorldPosition()

            drag_start = (self.getDragStart() - handle_position).normalized()
            drag_position = self.getDragPosition(event.x, event.y)
            if not drag_position:
                return False
            drag_end = (drag_position - handle_position).normalized()

            try:
                angle = math.acos(drag_start.dot(drag_end))
            except ValueError:
                angle = 0

            if self._snap_rotation:
                angle = int(angle / self._snap_angle) * self._snap_angle
                if angle == 0:
                    return False

            rotation = Quaternion()
            if self.getLockedAxis() == ToolHandle.XAxis:
                direction = 1 if Vector.Unit_X.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_X)
            elif self.getLockedAxis() == ToolHandle.YAxis:
                direction = 1 if Vector.Unit_Y.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Y)
            elif self.getLockedAxis() == ToolHandle.ZAxis:
                direction = 1 if Vector.Unit_Z.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Z)
            else:
                direction = -1

            # 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._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time
                self._angle += direction * angle
                self.propertyChanged.emit()

                # Rotate around the saved centeres of all selected nodes
                if len(self._saved_node_positions) > 1:
                    op = GroupedOperation()
                    for node, position in self._saved_node_positions:
                        op.addOperation(
                            RotateOperation(node,
                                            rotation,
                                            rotate_around_point=position))
                    op.push()
                else:
                    for node, position in self._saved_node_positions:
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position).push()

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

        if event.type == Event.MouseReleaseEvent:
            if self._active_widget != None and time.monotonic(
            ) - self._widget_click_start < 0.2:
                id = self._selection_pass.getIdAtPosition(event.x, event.y)

                if id in self._handle.getExtraWidgetsColorMap(
                ) and self._active_widget == self._handle.ExtraWidgets[id]:
                    axis = math.floor(self._active_widget.value / 2)
                    angle = math.radians(-90 if self._active_widget.value -
                                         2 * axis else 90)
                    axis += self._handle.XAxis

                    rotation = Quaternion()
                    if axis == ToolHandle.XAxis:
                        rotation = Quaternion.fromAngleAxis(
                            angle, Vector.Unit_X)
                    elif axis == ToolHandle.YAxis:
                        rotation = Quaternion.fromAngleAxis(
                            angle, Vector.Unit_Y)
                    else:
                        rotation = Quaternion.fromAngleAxis(
                            angle, Vector.Unit_Z)

                    # Rotate around the saved centeres of all selected nodes
                    if len(self._saved_node_positions) > 1:
                        op = GroupedOperation()
                        for node, position in self._saved_node_positions:
                            op.addOperation(
                                RotateOperation(node,
                                                rotation,
                                                rotate_around_point=position))
                        op.push()
                    else:
                        for node, position in self._saved_node_positions:
                            RotateOperation(
                                node, rotation,
                                rotate_around_point=position).push()

            self._active_widget = None  # type: Optional[RotateToolHandle.ExtraWidgets]

            # Finish a rotate operation
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(ToolHandle.NoAxis)
                self._angle = None
                self.propertyChanged.emit()
                if self._rotating:
                    self.operationStopped.emit(self)
                return True

    def _onSelectedFaceChanged(self):
        if not self._select_face_mode:
            return

        self._handle.setEnabled(not Selection.getFaceSelectMode())

        selected_face = Selection.getSelectedFace()
        if not Selection.getSelectedFace() or not (
                Selection.hasSelection() and Selection.getFaceSelectMode()):
            return

        original_node, face_id = selected_face
        meshdata = original_node.getMeshDataTransformed()
        if not meshdata or face_id < 0:
            return
        if face_id > (meshdata.getVertexCount() / 3 if
                      not meshdata.hasIndices() else meshdata.getFaceCount()):
            return

        face_mid, face_normal = meshdata.getFacePlane(face_id)
        object_mid = original_node.getBoundingBox().center
        rotation_point_vector = Vector(object_mid.x, object_mid.y, face_mid[2])
        face_normal_vector = Vector(face_normal[0], face_normal[1],
                                    face_normal[2])
        rotation_quaternion = Quaternion.rotationTo(
            face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0))

        operation = GroupedOperation()
        current_node = None  # type: Optional[SceneNode]
        for node in Selection.getAllSelectedObjects():
            current_node = node
            parent_node = current_node.getParent()
            while parent_node and parent_node.callDecoration("isGroup"):
                current_node = parent_node
                parent_node = current_node.getParent()
        if current_node is None:
            return

        rotate_operation = RotateOperation(current_node, rotation_quaternion,
                                           rotation_point_vector)
        operation.addOperation(rotate_operation)
        operation.push()

        # NOTE: We might want to consider unchecking the select-face button afterthe operation is done.

    def getToolHint(self):
        """Return a formatted angle of the current rotate operation
        
        :return: type(String) fully formatted string showing the angle by which the mesh(es) are rotated
        """

        return "%d°" % round(math.degrees(
            self._angle)) if self._angle else None

    def getSelectFaceSupported(self) -> bool:
        """Get whether the select face feature is supported.
        
        :return: True if it is supported, or False otherwise.
        """
        # Use a dummy postfix, since an equal version with a postfix is considered smaller normally.
        return Version(OpenGL.getInstance().getOpenGLVersion()) >= Version(
            "4.1 dummy-postfix")

    def getRotationSnap(self):
        """Get the state of the "snap rotation to N-degree increments" option
        
        :return: type(Boolean)
        """

        return self._snap_rotation

    def setRotationSnap(self, snap):
        """Set the state of the "snap rotation to N-degree increments" option
        
        :param snap: type(Boolean)
        """

        if snap != self._snap_rotation:
            self._snap_rotation = snap
            self.propertyChanged.emit()

    def getRotationSnapAngle(self):
        """Get the number of degrees used in the "snap rotation to N-degree increments" option"""

        return self._snap_angle

    def setRotationSnapAngle(self, angle):
        """Set the number of degrees used in the "snap rotation to N-degree increments" option"""

        if angle != self._snap_angle:
            self._snap_angle = angle
            self.propertyChanged.emit()

    def getSelectFaceToLayFlatMode(self) -> bool:
        """Whether the rotate tool is in 'Lay flat by face'-Mode."""

        if not Selection.getFaceSelectMode():
            self._select_face_mode = False  # .. but not the other way around!
        return self._select_face_mode

    def setSelectFaceToLayFlatMode(self, select: bool) -> None:
        """Set the rotate tool to/from 'Lay flat by face'-Mode."""

        if select != self._select_face_mode or select != Selection.getFaceSelectMode(
        ):
            self._select_face_mode = select
            if not select:
                Selection.clearFace()
            Selection.setFaceSelectMode(self._select_face_mode)
            self.propertyChanged.emit()

    def resetRotation(self):
        """Reset the orientation of the mesh(es) to their original orientation(s)"""

        for node in self._getSelectedObjectsWithoutSelectedAncestors():
            node.setMirror(Vector(1, 1, 1))

        Selection.applyOperation(SetTransformOperation, None, Quaternion(),
                                 None)

    def layFlat(self):
        """Initialise and start a LayFlatOperation
        
        Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project
        """

        self.operationStarted.emit(self)
        self._progress_message = Message(
            i18n_catalog.i18nc("@label",
                               "Laying object flat on buildplate..."),
            lifetime=0,
            dismissable=False,
            title=i18n_catalog.i18nc("@title", "Object Rotation"))
        self._progress_message.setProgress(0)

        self._iterations = 0
        self._total_iterations = 0
        for selected_object in self._getSelectedObjectsWithoutSelectedAncestors(
        ):
            self._layObjectFlat(selected_object)
        self._progress_message.show()

        operations = Selection.applyOperation(LayFlatOperation)
        for op in operations:
            op.progress.connect(self._layFlatProgress)

        job = LayFlatJob(operations)
        job.finished.connect(self._layFlatFinished)
        job.start()

    def _layObjectFlat(self, selected_object):
        """Lays the given object flat. The given object can be a group or not."""

        if not selected_object.callDecoration("isGroup"):
            self._total_iterations += selected_object.getMeshData(
            ).getVertexCount() * 2
        else:
            for child in selected_object.getChildren():
                self._layObjectFlat(child)

    def _layFlatProgress(self, iterations: int):
        """Called while performing the LayFlatOperation so progress can be shown
        
        Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications,
        :param iterations: type(int) number of iterations performed since the last callback
        """

        self._iterations += iterations
        if self._progress_message:
            self._progress_message.setProgress(
                min(100 * (self._iterations / self._total_iterations), 100))

    def _layFlatFinished(self, job):
        """Called when the LayFlatJob is done running all of its LayFlatOperations
        
        :param job: type(LayFlatJob)
        """

        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self.operationStopped.emit(self)
コード例 #11
0
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, properties):
        super().__init__(key)

        self._address = address
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None

        ##  Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint.
        self._num_extruders_set = False
        self._num_extruders = 1

        self._api_version = "1"
        self._api_prefix = "/api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        self.setPriority(2) # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")
        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._command_request = None
        self._command_reply = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_timer = QTimer()
        self._camera_timer.setInterval(500)  # Todo: Add preference for camera update interval
        self._camera_timer.setSingleShot(False)
        self._camera_timer.timeout.connect(self._update_camera)

        self._camera_image_id = 0

        self._camera_image = QImage()

        self._connection_state_before_timeout = None

        self._last_response_time = None
        self._response_timeout_time = 5

    def getProperties(self):
        return self._properties

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result = str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant = True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    def _update_camera(self):
        ## Request new image
        url = QUrl("http://" + self._address + ":8080/?action=snapshot")
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)

    def _update(self):
        # Check that we aren't in a timeout state
        if self._last_response_time and not self._connection_state_before_timeout:
            if time() - self._last_response_time > self._response_timeout_time:
                # Go into timeout state.
                Logger.log("d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections."))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        url = QUrl("http://" + self._address + self._api_prefix + "printer")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()
        self._camera_timer.stop()
        self._camera_image = QImage()
        self.newImage.emit()

    def requestWrite(self, node, file_name = None, filter_by_machine = False):
        Application.getInstance().showPrintMonitor.emit(True)
        self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        self.setConnectionState(ConnectionState.connecting)
        self._update()  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        self._update_camera()
        Logger.log("d", "Connection with instance %s with ip %s started", self._key, self._address)
        self._update_timer.start()
        self._camera_timer.start()
        self._last_response_time = None
        self.setAcceptsCommands(False)

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with ip %s stopped", self._key, self._address)
        self.close()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify = newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"

        if command:
            self._sendCommand(command)

    def startPrint(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if not global_container_stack:
            return

        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            for line in self._gcode:
                single_string_file_data += line

            file_name = "%s.gcode" % Application.getInstance().getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            if global_container_stack.getMetaDataEntry("octoprint_auto_print", True):
                self._post_part = QHttpPart()
                self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
                self._post_part.setBody(b"true")
                self._post_multi_part.append(self._post_part)

                self._post_part = QHttpPart()
                self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name)
                self._post_part.setBody(single_string_file_data.encode())
                self._post_multi_part.append(self._post_part)

            url = QUrl("http://" + self._address + self._api_prefix + "files/local")

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(), self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request, self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log("e", "An exception occurred in network connection: %s" % str(e))

    def _sendCommand(self, command):
        url = QUrl("http://" + self._address + self._api_prefix + "job")
        self._command_request = QNetworkRequest(url)
        self._command_request.setRawHeader(self._api_header.encode(), self._api_key.encode())
        self._command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")

        data = "{\"command\": \"%s\"}" % command
        self._command_reply = self._manager.post(self._command_request, data.encode())
        Logger.log("d", "Sent command to OctoPrint instance: %s", data)

    def _setTargetBedTemperature(self, temperature):
        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    def _setTargetHotendTemperature(self, index, temperature):
        Logger.log("d", "Setting hotend %s temperature to %s", index, temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    def _setHeadPosition(self, x, y , z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Y%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
        self._sendCommand("G90")

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "printer" in reply.url().toString():  # Status update from /printer.
                if http_status_code == 200:
                    self.setAcceptsCommands(True)

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    if not self._num_extruders_set:
                        self._num_extruders = 0
                        while "tool%d" % self._num_extruders in json_data["temperature"]:
                            self._num_extruders = self._num_extruders + 1

                        # Reinitialise from PrinterOutputDevice to match the new _num_extruders
                        self._hotend_temperatures = [0] * self._num_extruders
                        self._target_hotend_temperatures = [0] * self._num_extruders

                        self._num_extruders_set = True

                    # Check for hotend temperatures
                    for index in range(0, self._num_extruders):
                        temperature = json_data["temperature"]["tool%d" % index]["actual"]
                        self._setHotendTemperature(index, temperature)

                    bed_temperature = json_data["temperature"]["bed"]["actual"]
                    self._setBedTemperature(bed_temperature)

                    job_state = "offline"
                    if json_data["state"]["flags"]["error"]:
                        job_state = "error"
                    elif json_data["state"]["flags"]["paused"]:
                        job_state = "paused"
                    elif json_data["state"]["flags"]["printing"]:
                        job_state = "printing"
                    elif json_data["state"]["flags"]["ready"]:
                        job_state = "ready"
                    self._updateJobState(job_state)
                elif http_status_code == 401:
                    self.setAcceptsCommands(False)
                else:
                    pass  # TODO: Handle errors

            elif "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
                        elif progress > 0:
                            self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

            elif "snapshot" in reply.url().toString():  # Update from camera:
                if http_status_code == 200:
                    self._camera_image.loadFromData(reply.readAll())
                    self.newImage.emit()
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "files" in reply.url().toString():  # Result from /files command:
                if http_status_code == 201:
                    Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()
                global_container_stack = Application.getInstance().getGlobalContainerStack()
                if global_container_stack and not global_container_stack.getMetaDataEntry("octoprint_auto_print", True):
                    message = Message(catalog.i18nc("@info:status", "Saved to OctoPrint as {1}").format(reply.header(QNetworkRequest.LocationHeader).toString()))
                    message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to OctoPrint."))
                    message.actionTriggered.connect(self._onMessageActionTriggered)
                    message.show()

            elif "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation())

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl("http://" + self._address))
コード例 #12
0
 def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
     if sync_message_action == "sync":
         self.openSyncAllWindow()
         sync_message.hide()
     elif sync_message_action == "learn_more":
         QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
コード例 #13
0
class ProcessSlicedLayersJob(Job):
    def __init__(self, layers):
        super().__init__()
        self._layers = layers
        self._scene = Application.getInstance().getController().getScene()
        self._progress_message = Message(
            catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
        self._abort_requested = False
        self._build_plate_number = None

    ##  Aborts the processing of layers.
    #
    #   This abort is made on a best-effort basis, meaning that the actual
    #   job thread will check once in a while to see whether an abort is
    #   requested and then stop processing by itself. There is no guarantee
    #   that the abort will stop the job any time soon or even at all.
    def abort(self):
        self._abort_requested = True

    def setBuildPlate(self, new_value):
        self._build_plate_number = new_value

    def getBuildPlate(self):
        return self._build_plate_number

    def run(self):
        Logger.log(
            "d", "Processing new layer for build plate %s..." %
            self._build_plate_number)
        start_time = time()
        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "SimulationView":
            view.resetLayerData()
            self._progress_message.show()
            Job.yieldThread()
            if self._abort_requested:
                if self._progress_message:
                    self._progress_message.hide()
                return

        Application.getInstance().getController().activeViewChanged.connect(
            self._onActiveViewChanged)

        # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice
        new_node = CuraSceneNode(no_setting_override=True)
        new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))

        # Force garbage collection.
        # For some reason, Python has a tendency to keep the layer data
        # in memory longer than needed. Forcing the GC to run here makes
        # sure any old layer data is really cleaned up before adding new.
        gc.collect()

        mesh = MeshData()
        layer_data = LayerDataBuilder.LayerDataBuilder()
        layer_count = len(self._layers)

        # Find the minimum layer number
        # When disabling the remove empty first layers setting, the minimum layer number will be a positive
        # value. In that case the first empty layers will be discarded and start processing layers from the
        # first layer with data.
        # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
        # simply offset all other layers so the lowest layer is always 0. It could happens that the first
        # raft layer has value -8 but there are just 4 raft (negative) layers.
        min_layer_number = sys.maxsize
        negative_layers = 0
        for layer in self._layers:
            if layer.repeatedMessageCount("path_segment") > 0:
                if layer.id < min_layer_number:
                    min_layer_number = layer.id
                if layer.id < 0:
                    negative_layers += 1

        current_layer = 0

        for layer in self._layers:
            # If the layer is below the minimum, it means that there is no data, so that we don't create a layer
            # data. However, if there are empty layers in between, we compute them.
            if layer.id < min_layer_number:
                continue

            # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used,
            # then the absolute layer number is adjusted by removing the empty layers that can be in between raft
            # and the model
            abs_layer_number = layer.id - min_layer_number
            if layer.id >= 0 and negative_layers != 0:
                abs_layer_number += (min_layer_number + negative_layers)

            layer_data.addLayer(abs_layer_number)
            this_layer = layer_data.getLayer(abs_layer_number)
            layer_data.setLayerHeight(abs_layer_number, layer.height)
            layer_data.setLayerThickness(abs_layer_number, layer.thickness)

            for p in range(layer.repeatedMessageCount("path_segment")):
                polygon = layer.getRepeatedMessage("path_segment", p)

                extruder = polygon.extruder

                line_types = numpy.fromstring(
                    polygon.line_type,
                    dtype="u1")  # Convert bytearray to numpy array

                line_types = line_types.reshape((-1, 1))

                points = numpy.fromstring(
                    polygon.points,
                    dtype="f4")  # Convert bytearray to numpy array
                if polygon.point_type == 0:  # Point2D
                    points = points.reshape(
                        (-1, 2)
                    )  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
                else:  # Point3D
                    points = points.reshape((-1, 3))

                line_widths = numpy.fromstring(
                    polygon.line_width,
                    dtype="f4")  # Convert bytearray to numpy array
                line_widths = line_widths.reshape(
                    (-1, 1)
                )  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                line_thicknesses = numpy.fromstring(
                    polygon.line_thickness,
                    dtype="f4")  # Convert bytearray to numpy array
                line_thicknesses = line_thicknesses.reshape(
                    (-1, 1)
                )  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                line_feedrates = numpy.fromstring(
                    polygon.line_feedrate,
                    dtype="f4")  # Convert bytearray to numpy array
                line_feedrates = line_feedrates.reshape(
                    (-1, 1)
                )  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                # Create a new 3D-array, copy the 2D points over and insert the right height.
                # This uses manual array creation + copy rather than numpy.insert since this is
                # faster.
                new_points = numpy.empty((len(points), 3), numpy.float32)
                if polygon.point_type == 0:  # Point2D
                    new_points[:, 0] = points[:, 0]
                    new_points[:,
                               1] = layer.height / 1000  # layer height value is in backend representation
                    new_points[:, 2] = -points[:, 1]
                else:  # Point3D
                    new_points[:, 0] = points[:, 0]
                    new_points[:, 1] = points[:, 2]
                    new_points[:, 2] = -points[:, 1]

                this_poly = LayerPolygon.LayerPolygon(extruder, line_types,
                                                      new_points, line_widths,
                                                      line_thicknesses,
                                                      line_feedrates)
                this_poly.buildCache()

                this_layer.polygons.append(this_poly)

                Job.yieldThread()
            Job.yieldThread()
            current_layer += 1
            progress = (current_layer / layer_count) * 99
            # TODO: Rebuild the layer data mesh once the layer has been processed.
            # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.

            if self._abort_requested:
                if self._progress_message:
                    self._progress_message.hide()
                return
            if self._progress_message:
                self._progress_message.setProgress(progress)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data

        # Find out colors per extruder
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        manager = ExtruderManager.getInstance()
        extruders = manager.getActiveExtruderStacks()
        if extruders:
            material_color_map = numpy.zeros((len(extruders), 4),
                                             dtype=numpy.float32)
            for extruder in extruders:
                position = int(
                    extruder.getMetaDataEntry("position", default="0"))
                try:
                    default_color = ExtrudersModel.defaultColors[position]
                except IndexError:
                    default_color = "#e0e000"
                color_code = extruder.material.getMetaDataEntry(
                    "color_code", default=default_color)
                color = colorCodeToRGBA(color_code)
                material_color_map[position, :] = color
        else:
            # Single extruder via global stack.
            material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
            color_code = global_container_stack.material.getMetaDataEntry(
                "color_code", default="#e0e000")
            color = colorCodeToRGBA(color_code)
            material_color_map[0, :] = color

        # We have to scale the colors for compatibility mode
        if OpenGLContext.isLegacyOpenGL() or bool(
                Application.getInstance().getPreferences().getValue(
                    "view/force_layer_view_compatibility_mode")):
            line_type_brightness = 0.5  # for compatibility mode
        else:
            line_type_brightness = 1.0
        layer_mesh = layer_data.build(material_color_map, line_type_brightness)
        if self._abort_requested:
            if self._progress_message:
                self._progress_message.hide()
            return
        # Add LayerDataDecorator to scene node to indicate that the node has layer data
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        new_node.addDecorator(decorator)

        new_node.setMeshData(mesh)
        # Set build volume as parent, the build volume can move as a result of raft settings.
        # It makes sense to set the build volume as parent: the print is actually printed on it.
        new_node_parent = Application.getInstance().getBuildVolume()
        new_node.setParent(
            new_node_parent)  # Note: After this we can no longer abort!

        settings = Application.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            new_node.setPosition(
                Vector(-settings.getProperty("machine_width", "value") / 2,
                       0.0,
                       settings.getProperty("machine_depth", "value") / 2))

        if self._progress_message:
            self._progress_message.setProgress(100)

        if self._progress_message:
            self._progress_message.hide()

        # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
        self._layers = None

        Logger.log("d", "Processing layers took %s seconds",
                   time() - start_time)

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView(
            ).getPluginId() == "SimulationView":
                if not self._progress_message:
                    self._progress_message = Message(
                        catalog.i18nc("@info:status", "Processing Layers"), 0,
                        False, 0, catalog.i18nc("@info:title", "Information"))
                if self._progress_message.getProgress() != 100:
                    self._progress_message.show()
            else:
                if self._progress_message:
                    self._progress_message.hide()
コード例 #14
0
ファイル: QtApplication.py プロジェクト: Ultimaker/Uranium
 def hideMessage(self, message: Message) -> None:
     with self._message_lock:
         if message in self._visible_messages:
             message.hide(send_signal = False)  # we're in handling hideMessageSignal so we don't want to resend it
             self._visible_messages.remove(message)
             self.visibleMessageRemoved.emit(message)
コード例 #15
0
ファイル: FlavorParser.py プロジェクト: rwreynolds/Cura
class FlavorParser:

    def __init__(self) -> None:
        CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
        self._cancelled = False
        self._message = None
        self._layer_number = 0
        self._extruder_number = 0
        self._clearValues()
        self._scene_node = None
        # X, Y, Z position, F feedrate and E extruder values are stored
        self._position = Position
        self._is_layers_in_file = False  # Does the Gcode have the layers comment?
        self._extruder_offsets = {}  # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._current_layer_thickness = 0.2  # default
        self._filament_diameter = 2.85       # default
        self._previous_extrusion_value = 0.0  # keep track of the filament retractions

        CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)

    def _clearValues(self) -> None:
        self._extruder_number = 0
        self._extrusion_length_offset = [0] # type: List[float]
        self._layer_type = LayerPolygon.Inset0Type
        self._layer_number = 0
        self._previous_z = 0 # type: float
        self._layer_data_builder = LayerDataBuilder()
        self._is_absolute_positioning = True    # It can be absolute (G90) or relative (G91)
        self._is_absolute_extrusion = True  # It can become absolute (M82, default) or relative (M83)

    @staticmethod
    def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
        n = line.find(code)
        if n < 0:
            return None
        n += len(code)
        pattern = re.compile("[;\s]")
        match = pattern.search(line, n)
        m = match.start() if match is not None else -1
        try:
            if m < 0:
                return line[n:]
            return line[n:m]
        except:
            return None

    def _getInt(self, line: str, code: str) -> Optional[int]:
        value = self._getValue(line, code)
        try:
            return int(value)   # type: ignore
        except:
            return None

    def _getFloat(self, line: str, code: str) -> Optional[float]:
        value = self._getValue(line, code)
        try:
            return float(value) # type: ignore
        except:
            return None

    def _onHideMessage(self, message: str) -> None:
        if message == self._message:
            self._cancelled = True

    def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[5] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
            self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 3), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:
            points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
            extrusion_values[i] = point[4]
            if i > 0:
                line_feedrates[i - 1] = point[3]
                line_types[i - 1] = point[5]
                if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
                    line_widths[i - 1] = 0.1
                    line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
                else:
                    line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _createEmptyLayer(self, layer_number: int) -> None:
        self._layer_data_builder.addLayer(layer_number)
        self._layer_data_builder.setLayerHeight(layer_number, 0)
        self._layer_data_builder.setLayerThickness(layer_number, 0)

    def _calculateLineWidth(self, current_point: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
        # Area of the filament
        Af = (self._filament_diameter / 2) ** 2 * numpy.pi
        # Length of the extruded filament
        de = current_extrusion - previous_extrusion
        # Volumne of the extruded filament
        dVe = de * Af
        # Length of the printed line
        dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
        # When the extruder recovers from a retraction, we get zero distance
        if dX == 0:
            return 0.1
        # Area of the printed line. This area is a rectangle
        Ae = dVe / dX
        # This area is a rectangle with area equal to layer_thickness * layer_width
        line_width = Ae / layer_thickness

        # A threshold is set to avoid weird paths in the GCode
        if line_width > 1.2:
            return 0.35
        return line_width

    def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        x, y, z, f, e = position

        if self._is_absolute_positioning:
            x = params.x if params.x is not None else x
            y = params.y if params.y is not None else y
            z = params.z if params.z is not None else z
        else:
            x += params.x if params.x is not None else 0
            y += params.y if params.y is not None else 0
            z += params.z if params.z is not None else 0

        f = params.f if params.f is not None else f

        if params.e is not None:
            new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
            if new_extrusion_value > e[self._extruder_number]:
                path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type])  # extrusion
                self._previous_extrusion_value = new_extrusion_value
            else:
                path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])  # retraction
            e[self._extruder_number] = new_extrusion_value

            # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
            # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
            if z > self._previous_z and (z - self._previous_z < 1.5):
                self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
                self._previous_z = z
        elif self._previous_extrusion_value > e[self._extruder_number]:
            path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
        else:
            path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
        return self._position(x, y, z, f, e)


    # G0 and G1 should be handled exactly the same.
    _gCode1 = _gCode0

    ##  Home the head.
    def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        return self._position(
            params.x if params.x is not None else position.x,
            params.y if params.y is not None else position.y,
            params.z if params.z is not None else position.z,
            position.f,
            position.e)

    ##  Set the absolute positioning
    def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        self._is_absolute_positioning = True
        self._is_absolute_extrusion = True
        return position

    ##  Set the relative positioning
    def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        self._is_absolute_positioning = False
        self._is_absolute_extrusion = False
        return position

    ##  Reset the current position to the values specified.
    #   For example: G92 X10 will set the X to 10 without any physical motion.
    def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        if params.e is not None:
            # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
            self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
            position.e[self._extruder_number] = params.e
            self._previous_extrusion_value = params.e
        else:
            self._previous_extrusion_value = 0.0
        return self._position(
            params.x if params.x is not None else position.x,
            params.y if params.y is not None else position.y,
            params.z if params.z is not None else position.z,
            params.f if params.f is not None else position.f,
            position.e)

    def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        func = getattr(self, "_gCode%s" % G, None)
        line = line.split(";", 1)[0]  # Remove comments (if any)
        if func is not None:
            s = line.upper().split(" ")
            x, y, z, f, e = None, None, None, None, None
            for item in s[1:]:
                if len(item) <= 1:
                    continue
                if item.startswith(";"):
                    continue
                if item[0] == "X":
                    x = float(item[1:])
                if item[0] == "Y":
                    y = float(item[1:])
                if item[0] == "Z":
                    z = float(item[1:])
                if item[0] == "F":
                    f = float(item[1:]) / 60
                if item[0] == "E":
                    e = float(item[1:])
            params = PositionOptional(x, y, z, f, e)
            return func(position, params, path)
        return position

    def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        self._extruder_number = T
        if self._extruder_number + 1 > len(position.e):
            self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
            position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
        return position

    def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        pass

    _type_keyword = ";TYPE:"
    _layer_keyword = ";LAYER:"

    ##  For showing correct x, y offsets for each extruder
    def _extruderOffsets(self) -> Dict[int, List[float]]:
        result = {}
        for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
            result[int(extruder.getMetaData().get("position", "0"))] = [
                extruder.getProperty("machine_nozzle_offset_x", "value"),
                extruder.getProperty("machine_nozzle_offset_y", "value")]
        return result

    def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
        Logger.log("d", "Preparing to load GCode")
        self._cancelled = False
        # We obtain the filament diameter from the selected extruder to calculate line widths
        global_stack = CuraApplication.getInstance().getGlobalContainerStack()

        if not global_stack:
            return None

        self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")

        scene_node = CuraSceneNode()

        gcode_list = []
        self._is_layers_in_file = False

        self._extruder_offsets = self._extruderOffsets()  # dict with index the extruder number. can be empty

        ##############################################################################################
        ##  This part is where the action starts
        ##############################################################################################
        file_lines = 0
        current_line = 0
        for line in stream.split("\n"):
            file_lines += 1
            gcode_list.append(line + "\n")
            if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
                self._is_layers_in_file = True

        file_step = max(math.floor(file_lines / 100), 1)

        self._clearValues()

        self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
                                lifetime=0,
                                title = catalog.i18nc("@info:title", "G-code Details"))

        assert(self._message is not None) # use for typing purposes
        self._message.setProgress(0)
        self._message.show()

        Logger.log("d", "Parsing Gcode...")

        current_position = Position(0, 0, 0, 0, [0])
        current_path = [] #type: List[List[float]]
        min_layer_number = 0
        negative_layers = 0
        previous_layer = 0
        self._previous_extrusion_value = 0.0

        for line in stream.split("\n"):
            if self._cancelled:
                Logger.log("d", "Parsing Gcode file cancelled")
                return None
            current_line += 1

            if current_line % file_step == 0:
                self._message.setProgress(math.floor(current_line / file_lines * 100))
                Job.yieldThread()
            if len(line) == 0:
                continue

            if line.find(self._type_keyword) == 0:
                type = line[len(self._type_keyword):].strip()
                if type == "WALL-INNER":
                    self._layer_type = LayerPolygon.InsetXType
                elif type == "WALL-OUTER":
                    self._layer_type = LayerPolygon.Inset0Type
                elif type == "SKIN":
                    self._layer_type = LayerPolygon.SkinType
                elif type == "SKIRT":
                    self._layer_type = LayerPolygon.SkirtType
                elif type == "SUPPORT":
                    self._layer_type = LayerPolygon.SupportType
                elif type == "FILL":
                    self._layer_type = LayerPolygon.InfillType
                elif type == "SUPPORT-INTERFACE":
                    self._layer_type = LayerPolygon.SupportInterfaceType
                else:
                    Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)

            # When the layer change is reached, the polygon is computed so we have just one layer per extruder
            if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
                try:
                    layer_number = int(line[len(self._layer_keyword):])
                    self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
                    current_path.clear()
                    # Start the new layer at the end position of the last layer
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])

                    # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
                    # as in ProcessSlicedLayersJob
                    if layer_number < min_layer_number:
                        min_layer_number = layer_number
                    if layer_number < 0:
                        layer_number += abs(min_layer_number)
                        negative_layers += 1
                    else:
                        layer_number += negative_layers

                    # In case there is a gap in the layer count, empty layers are created
                    for empty_layer in range(previous_layer + 1, layer_number):
                        self._createEmptyLayer(empty_layer)

                    self._layer_number = layer_number
                    previous_layer = layer_number
                except:
                    pass

            # This line is a comment. Ignore it (except for the layer_keyword)
            if line.startswith(";"):
                continue

            G = self._getInt(line, "G")
            if G is not None:
                # When find a movement, the new posistion is calculated and added to the current_path, but
                # don't need to create a polygon until the end of the layer
                current_position = self.processGCode(G, line, current_position, current_path)
                continue

            # When changing the extruder, the polygon with the stored paths is computed
            if line.startswith("T"):
                T = self._getInt(line, "T")
                if T is not None:
                    self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
                    current_path.clear()

                    # When changing tool, store the end point of the previous path, then process the code and finally
                    # add another point with the new position of the head.
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
                    current_position = self.processTCode(T, line, current_position, current_path)
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])

            if line.startswith("M"):
                M = self._getInt(line, "M")
                self.processMCode(M, line, current_position, current_path)

        # "Flush" leftovers. Last layer paths are still stored
        if len(current_path) > 1:
            if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
                self._layer_number += 1
                current_path.clear()

        material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
        material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
        material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
        material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
        material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
        material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        # gcode_dict stores gcode_lists for a number of build plates.
        active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.

        Logger.log("d", "Finished parsing Gcode")
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w", "File doesn't contain any valid layers")

        settings = CuraApplication.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            machine_width = settings.getProperty("machine_width", "value")
            machine_depth = settings.getProperty("machine_depth", "value")
            scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "GCode loading finished")

        if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
                lifetime=0,
                title = catalog.i18nc("@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = CuraApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
コード例 #16
0
class ReadFileJob(Job):
    """A Job subclass that performs file loading."""
    def __init__(self,
                 filename: str,
                 handler: Optional[FileHandler] = None,
                 add_to_recent_files: bool = True) -> None:
        super().__init__()
        self._filename = filename
        self._handler = handler
        self._loading_message = None  # type: Optional[Message]
        self._add_to_recent_files = add_to_recent_files

    def getFileName(self):
        return self._filename

    def getAddToRecentFiles(self):
        return self._add_to_recent_files

    def run(self) -> None:
        from UM.Mesh.MeshReader import MeshReader
        if self._handler is None:
            Logger.log("e", "FileHandler was not set.")
            return None
        reader = self._handler.getReaderForFile(self._filename)
        if not reader:
            result_message = Message(i18n_catalog.i18nc(
                "@info:status Don't translate the XML tag <filename>!",
                "Cannot open files of the type of <filename>{0}</filename>",
                self._filename),
                                     lifetime=0,
                                     title=i18n_catalog.i18nc(
                                         "@info:title", "Invalid File"),
                                     message_type=Message.MessageType.ERROR)
            result_message.show()
            return

        # Give the plugin a chance to display a dialog before showing the loading UI
        try:
            pre_read_result = reader.preRead(self._filename)
        except:
            Logger.logException("e", "Failed to pre-read the file %s",
                                self._filename)
            pre_read_result = MeshReader.PreReadResult.failed

        if pre_read_result != MeshReader.PreReadResult.accepted:
            if pre_read_result == MeshReader.PreReadResult.failed:
                result_message = Message(i18n_catalog.i18nc(
                    "@info:status Don't translate the XML tag <filename>!",
                    "Failed to load <filename>{0}</filename>. The file could be corrupt or inaccessible.",
                    self._filename),
                                         lifetime=0,
                                         title=i18n_catalog.i18nc(
                                             "@info:title",
                                             "Unable to Open File"),
                                         message_type=Message.MessageType.ERROR
                                         )
                result_message.show()
            return

        self._loading_message = Message(self._filename,
                                        lifetime=0,
                                        progress=0,
                                        dismissable=False,
                                        title=i18n_catalog.i18nc(
                                            "@info:title", "Loading"))
        self._loading_message.setProgress(-1)
        self._loading_message.show()

        Job.yieldThread(
        )  # Yield to any other thread that might want to do something else.
        begin_time = time.time()
        try:
            self.setResult(self._handler.readerRead(reader, self._filename))
        except:
            Logger.logException("e",
                                "Exception occurred while loading file %s",
                                self._filename)
        finally:
            end_time = time.time()
            Logger.log("d", "Loading file took %0.1f seconds",
                       end_time - begin_time)
            self._loading_message.hide()
            if reader.emptyFileHintSet():
                result_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status Don't translate the XML tag <filename>!",
                        "There where no models in <filename>{0}</filename>.",
                        self._filename),
                    lifetime=0,
                    title=i18n_catalog.i18nc("@info:title",
                                             "No Models in File"),
                    message_type=Message.MessageType.WARNING)
                result_message.show()
            elif not self._result:
                result_message = Message(i18n_catalog.i18nc(
                    "@info:status Don't translate the XML tag <filename>!",
                    "Failed to load <filename>{0}</filename>. The file could be corrupt, inaccessible or it did not contain any models.",
                    self._filename),
                                         lifetime=0,
                                         title=i18n_catalog.i18nc(
                                             "@info:title",
                                             "Unable to Open File"),
                                         message_type=Message.MessageType.ERROR
                                         )
                result_message.show()
コード例 #17
0
ファイル: CuraEngineBackend.py プロジェクト: eyal0/Cura
class CuraEngineBackend(Backend):
    def __init__(self):
        super().__init__()

        # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
        default_engine_location = os.path.join(Application.getInstallPrefix(),
                                               "bin", "CuraEngine")
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(
                os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
        if sys.platform == "win32":
            default_engine_location += ".exe"
        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location",
                                                default_engine_location)

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(
            self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = []

        # When there are current settings and machine instance is changed, there is no profile changed event. We should
        # pretend there is though.
        Application.getInstance().getMachineManager(
        ).activeMachineInstanceChanged.connect(self._onActiveProfileChanged)

        self._profile = None
        Application.getInstance().getMachineManager(
        ).activeProfileChanged.connect(self._onActiveProfileChanged)
        self._onActiveProfileChanged()

        self._change_timer = QTimer()
        self._change_timer.setInterval(500)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self.slice)

        self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
        self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
        self._message_handlers[
            "cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
        self._message_handlers[
            "cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
        self._message_handlers[
            "cura.proto.ObjectPrintTime"] = self._onObjectPrintTimeMessage
        self._message_handlers[
            "cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage

        self._slicing = False
        self._restart = False
        self._enabled = True
        self._always_restart = True
        self._process_layers_job = None  #The currently active job to process layers, or None if it is not processing layers.

        self._message = None

        self.backendQuit.connect(self._onBackendQuit)

        self.backendConnected.connect(self._onBackendConnected)
        Application.getInstance().getController().toolOperationStarted.connect(
            self._onToolOperationStarted)
        Application.getInstance().getController().toolOperationStopped.connect(
            self._onToolOperationStopped)

        Application.getInstance().getMachineManager(
        ).activeMachineInstanceChanged.connect(self._onInstanceChanged)

    def close(self):
        # Terminate CuraEngine if it is still running at this point
        self._terminate()
        super().close()

    ##  Get the command that is used to call the engine.
    #   This is usefull for debugging and used to actually start the engine
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        active_machine = Application.getInstance().getMachineManager(
        ).getActiveMachineInstance()
        json_path = ""
        if not active_machine:
            json_path = Resources.getPath(Resources.MachineDefinitions,
                                          "fdmprinter.json")
        else:
            json_path = active_machine.getMachineDefinition().getPath()

        return [
            Preferences.getInstance().getValue("backend/location"), "connect",
            "127.0.0.1:{0}".format(self._port), "-j", json_path, "-vv"
        ]

    ##  Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted whne the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    ##  Perform a slice of the scene.
    def slice(self):

        if not self._enabled:
            return

        if self._slicing:
            self._terminate()

            if self._message:
                self._message.hide()
                self._message = None

            return

        if self._process_layers_job:
            self._process_layers_job.abort()
            self._process_layers_job = None

        if self._profile.hasErrorValue():
            Logger.log("w", "Profile has error values. Aborting slicing")
            if self._message:
                self._message.hide()
                self._message = None
            self._message = Message(
                catalog.i18nc(
                    "@info:status",
                    "Unable to slice. Please check your setting values for errors."
                ))
            self._message.show()
            return  #No slicing if we have error values since those are by definition illegal values.

        self.processingProgress.emit(0.0)
        self.backendStateChange.emit(BackendState.NOT_STARTED)
        if self._message:
            self._message.setProgress(-1)
        #else:
        #    self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
        #    self._message.show()

        self._scene.gcode_list = []
        self._slicing = True
        self.slicingStarted.emit()

        job = StartSliceJob.StartSliceJob(self._profile, self._socket)
        job.start()
        job.finished.connect(self._onStartSliceCompleted)

    def _terminate(self):
        self._slicing = False
        self._restart = True
        self._stored_layer_data = []
        self.slicingCancelled.emit()
        self.processingProgress.emit(0)
        Logger.log("d", "Attempting to kill the engine process")
        if self._process is not None:
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()
                Logger.log(
                    "d", "Engine process is killed. Received return code %s",
                    self._process.wait())
                self._process = None
                #self._createSocket() # Re create the socket
            except Exception as e:  # terminating a process that is already terminating causes an exception, silently ignore this.
                Logger.log(
                    "d",
                    "Exception occured while trying to kill the engine %s",
                    str(e))

    def _onStartSliceCompleted(self, job):
        if job.getError() or job.getResult() != True:
            if self._message:
                self._message.hide()
                self._message = None
            return

    def _onSceneChanged(self, source):
        if type(source) is not SceneNode:
            return

        if source is self._scene.getRoot():
            return

        if source.getMeshData() is None:
            return

        if source.getMeshData().getVertices() is None:
            return

        self._onChanged()

    def _onSocketError(self, error):
        if Application.getInstance().isShuttingDown():
            return

        super()._onSocketError(error)
        self._terminate()

        if error.getErrorCode() not in [
                Arcus.ErrorCode.BindFailedError,
                Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug
        ]:
            Logger.log("e", "A socket error caused the connection to be reset")

    def _onActiveProfileChanged(self):
        if self._profile:
            self._profile.settingValueChanged.disconnect(
                self._onSettingChanged)

        self._profile = Application.getInstance().getMachineManager(
        ).getWorkingProfile()
        if self._profile:
            self._profile.settingValueChanged.connect(self._onSettingChanged)
            self._onChanged()

    def _onSettingChanged(self, setting):
        self._onChanged()

    def _onLayerMessage(self, message):
        self._stored_layer_data.append(message)

    def _onProgressMessage(self, message):
        if self._message:
            self._message.setProgress(round(message.amount * 100))

        self.processingProgress.emit(message.amount)
        self.backendStateChange.emit(BackendState.PROCESSING)

    def _onSlicingFinishedMessage(self, message):
        self.backendStateChange.emit(BackendState.DONE)
        self.processingProgress.emit(1.0)

        self._slicing = False

        if self._message:
            self._message.setProgress(100)
            self._message.hide()
            self._message = None

        if self._layer_view_active and (
                self._process_layers_job is None
                or not self._process_layers_job.isRunning()):
            self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(
                self._stored_layer_data)
            self._process_layers_job.start()
            self._stored_layer_data = []

    def _onGCodeLayerMessage(self, message):
        self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))

    def _onGCodePrefixMessage(self, message):
        self._scene.gcode_list.insert(0,
                                      message.data.decode("utf-8", "replace"))

    def _onObjectPrintTimeMessage(self, message):
        self.printDurationMessage.emit(message.time, message.material_amount)

    def _createSocket(self):
        super()._createSocket(
            os.path.abspath(
                os.path.join(
                    PluginRegistry.getInstance().getPluginPath(
                        self.getPluginId()), "Cura.proto")))

    ##  Manually triggers a reslice
    def forceSlice(self):
        self._change_timer.start()

    def _onChanged(self):
        if not self._profile:
            return

        self._change_timer.start()

    def _onBackendConnected(self):
        if self._restart:
            self._onChanged()
            self._restart = False

    def _onToolOperationStarted(self, tool):
        self._terminate()  # Do not continue slicing once a tool has started
        self._enabled = False  # Do not reslice when a tool is doing it's 'thing'

    def _onToolOperationStopped(self, tool):
        self._enabled = True  # Tool stop, start listening for changes again.

    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "LayerView":
                self._layer_view_active = True
                # There is data and we're not slicing at the moment
                # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
                if self._stored_layer_data and not self._slicing:
                    self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(
                        self._stored_layer_data)
                    self._process_layers_job.start()
                    self._stored_layer_data = []
            else:
                self._layer_view_active = False

    def _onInstanceChanged(self):
        self._terminate()

    def _onBackendQuit(self):
        if not self._restart and self._process:
            Logger.log(
                "d",
                "Backend quit with return code %s. Resetting process and socket.",
                self._process.wait())
            self._process = None
            self._createSocket()
コード例 #18
0
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, port, properties):
        super().__init__(key)

        self._address = address
        self._port = port
        self._path = properties.get(b"path", b"/").decode("utf-8")
        if self._path[-1:] != "/":
            self._path += "/"
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None
        self._auto_print = True

        ##  Todo: Hardcoded value now; we should probably read this from the machine definition and octoprint.
        self._num_extruders_set = False
        self._num_extruders = 1

        self._api_version = "1"
        self._api_prefix = "api/"
        self._api_header = "X-Api-Key"
        self._api_key = None

        protocol = "https" if properties.get(
            b'useHttps') == b"true" else "http"
        self._base_url = "%s://%s:%d%s" % (protocol, self._address, self._port,
                                           self._path)
        self._api_url = self._base_url + self._api_prefix
        self._camera_url = "%s://%s:8080/?action=stream" % (protocol,
                                                            self._address)

        self.setPriority(
            2
        )  # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connected to OctoPrint on {0}").format(
                                   self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Hack to ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._printer_request = None
        self._printer_reply = None

        self._print_job_request = None
        self._print_job_reply = None

        self._image_request = None
        self._image_reply = None
        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._post_request = None
        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._job_request = None
        self._job_reply = None

        self._command_request = None
        self._command_reply = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(
            2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_image_id = 0
        self._camera_image = QImage()

        self._connection_state_before_timeout = None

        self._last_response_time = None
        self._last_request_time = None
        self._response_timeout_time = 5
        self._recreate_network_manager_time = 30  # If we have no connection, re-create network manager every 30 sec.
        self._recreate_network_manager_count = 1

    def getProperties(self):
        return self._properties

    @pyqtSlot(str, result=str)
    def getProperty(self, key):
        key = key.encode("utf-8")
        if key in self._properties:
            return self._properties.get(key, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result=str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self):
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self):
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self):
        return self._base_url

    def _startCamera(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack or not parseBool(
                global_container_stack.getMetaDataEntry(
                    "octoprint_show_camera", False)):
            return

        # Start streaming mjpg stream
        url = QUrl(self._camera_url)
        self._image_request = QNetworkRequest(url)
        self._image_reply = self._manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(
            self._onStreamDownloadProgress)

    def _stopCamera(self):
        if self._image_reply:
            self._image_reply.abort()
            self._image_reply.downloadProgress.disconnect(
                self._onStreamDownloadProgress)
            self._image_reply = None
        self._image_request = None

        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._camera_image = QImage()
        self.newImage.emit()

    def _update(self):
        if self._last_response_time:
            time_since_last_response = time() - self._last_response_time
        else:
            time_since_last_response = 0
        if self._last_request_time:
            time_since_last_request = time() - self._last_request_time
        else:
            time_since_last_request = float(
                "inf")  # An irrelevantly large number of seconds

        # Connection is in timeout, check if we need to re-start the connection.
        # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
        # Re-creating the QNetworkManager seems to fix this issue.
        if self._last_response_time and self._connection_state_before_timeout:
            if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
                self._recreate_network_manager_count += 1
                # It can happen that we had a very long timeout (multiple times the recreate time).
                # In that case we should jump through the point that the next update won't be right away.
                while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
                    self._recreate_network_manager_count += 1
                Logger.log(
                    "d",
                    "Timeout lasted over 30 seconds (%.1fs), re-checking connection.",
                    time_since_last_response)
                self._createNetworkManager()
                return

        # Check if we have an connection in the first place.
        if not self._manager.networkAccessible():
            if not self._connection_state_before_timeout:
                Logger.log(
                    "d",
                    "The network connection seems to be disabled. Going into timeout mode"
                )
                self._connection_state_before_timeout = self._connection_state
                self.setConnectionState(ConnectionState.error)
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with the network was lost."))
                self._connection_message.show()
                # Check if we were uploading something. Abort if this is the case.
                # Some operating systems handle this themselves, others give weird issues.
                try:
                    if self._post_reply:
                        Logger.log(
                            "d",
                            "Stopping post upload because the connection was lost."
                        )
                        try:
                            self._post_reply.uploadProgress.disconnect(
                                self._onUploadProgress)
                        except TypeError:
                            pass  # The disconnection can fail on mac in some cases. Ignore that.

                        self._post_reply.abort()
                        self._progress_message.hide()
                except RuntimeError:
                    self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
            return
        else:
            if not self._connection_state_before_timeout:
                self._recreate_network_manager_count = 1

        # Check that we aren't in a timeout state
        if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
            if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
                # Go into timeout state.
                Logger.log(
                    "d",
                    "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.",
                    time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The connection with OctoPrint was lost. Check your network-connections."
                    ))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        url = QUrl(self._api_url + "printer")
        self._printer_request = QNetworkRequest(url)
        self._printer_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._printer_reply = self._manager.get(self._printer_request)

        ## Request print_job data
        url = QUrl(self._api_url + "job")
        self._job_request = QNetworkRequest(url)
        self._job_request.setRawHeader(self._api_header.encode(),
                                       self._api_key.encode())
        self._job_reply = self._manager.get(self._job_request)

    def _createNetworkManager(self):
        if self._manager:
            self._manager.finished.disconnect(self._onRequestFinished)

        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()

        self._stopCamera()

    def requestWrite(self,
                     node,
                     file_name=None,
                     filter_by_machine=False,
                     file_handler=None):
        self.writeStarted.emit(self)
        self._gcode = getattr(
            Application.getInstance().getController().getScene(), "gcode_list")

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        self._createNetworkManager()

        self.setConnectionState(ConnectionState.connecting)
        self._update(
        )  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        Logger.log("d", "Connection with instance %s with url %s started",
                   self._key, self._base_url)
        self._update_timer.start()

        self._last_response_time = None
        self.setAcceptsCommands(False)
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connecting to OctoPrint on {0}").format(
                                   self._key))

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with url %s stopped",
                   self._key, self._base_url)
        self.close()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify=newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"

        if command:
            self._sendCommand(command)

    def startPrint(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        self._auto_print = parseBool(
            global_container_stack.getMetaDataEntry("octoprint_auto_print",
                                                    True))
        if self._auto_print:
            Application.getInstance().showPrintMonitor.emit(True)

        if self.jobState != "ready" and self.jobState != "":
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "OctoPrint is printing. Unable to start a new job."))
            self._error_message.show()
            return
        try:
            self._progress_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending data to OctoPrint"), 0, False, -1)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            last_process_events = time()
            for line in self._gcode:
                single_string_file_data += line
                if time() > last_process_events + 0.05:
                    # Ensure that the GUI keeps updated at least 20 times per second.
                    QCoreApplication.processEvents()
                    last_process_events = time()

            file_name = "%s.gcode" % Application.getInstance(
            ).getPrintInformation().jobName

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                      "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            if self._auto_print:
                self._post_part = QHttpPart()
                self._post_part.setHeader(
                    QNetworkRequest.ContentDispositionHeader,
                    "form-data; name=\"print\"")
                self._post_part.setBody(b"true")
                self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(
                QNetworkRequest.ContentDispositionHeader,
                "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            destination = "local"
            if parseBool(
                    global_container_stack.getMetaDataEntry(
                        "octoprint_store_sd", False)):
                destination = "sdcard"

            url = QUrl(self._api_url + "files/" + destination)

            ##  Create the QT request
            self._post_request = QNetworkRequest(url)
            self._post_request.setRawHeader(self._api_header.encode(),
                                            self._api_key.encode())

            ##  Post request + data
            self._post_reply = self._manager.post(self._post_request,
                                                  self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Unable to send data to OctoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log(
                "e",
                "An exception occurred in network connection: %s" % str(e))

    def _sendCommand(self, command):
        url = QUrl(self._api_url + "job")
        self._command_request = QNetworkRequest(url)
        self._command_request.setRawHeader(self._api_header.encode(),
                                           self._api_key.encode())
        self._command_request.setHeader(QNetworkRequest.ContentTypeHeader,
                                        "application/json")

        data = "{\"command\": \"%s\"}" % command
        self._command_reply = self._manager.post(self._command_request,
                                                 data.encode())
        Logger.log("d", "Sent command to OctoPrint instance: %s", data)

    def _setTargetBedTemperature(self, temperature):
        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    def _setTargetHotendTemperature(self, index, temperature):
        Logger.log("d", "Setting hotend %s temperature to %s", index,
                   temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    def _setHeadPosition(self, x, y, z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Y%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand("G91")
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
        self._sendCommand("G90")

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error(
        ) == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log(
                    "d",
                    "We got a response from the instance after %s of silence",
                    time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "printer" in reply.url().toString(
            ):  # Status update from /printer.
                if http_status_code == 200:
                    if not self.acceptsCommands:
                        self.setAcceptsCommands(True)
                        self.setConnectionText(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Connected to OctoPrint on {0}").format(
                                    self._key))

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    if not self._num_extruders_set:
                        self._num_extruders = 0
                        while "tool%d" % self._num_extruders in json_data[
                                "temperature"]:
                            self._num_extruders = self._num_extruders + 1

                        # Reinitialise from PrinterOutputDevice to match the new _num_extruders
                        self._hotend_temperatures = [0] * self._num_extruders
                        self._target_hotend_temperatures = [
                            0
                        ] * self._num_extruders

                        self._num_extruders_set = True

                    # Check for hotend temperatures
                    for index in range(0, self._num_extruders):
                        temperature = json_data["temperature"]["tool%d" %
                                                               index]["actual"]
                        self._setHotendTemperature(index, temperature)

                    bed_temperature = json_data["temperature"]["bed"]["actual"]
                    self._setBedTemperature(bed_temperature)

                    job_state = "offline"
                    if json_data["state"]["flags"]["error"]:
                        job_state = "error"
                    elif json_data["state"]["flags"]["paused"]:
                        job_state = "paused"
                    elif json_data["state"]["flags"]["printing"]:
                        job_state = "printing"
                    elif json_data["state"]["flags"]["ready"]:
                        job_state = "ready"
                    self._updateJobState(job_state)
                elif http_status_code == 401:
                    self.setAcceptsCommands(False)
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "OctoPrint on {0} does not allow access to print").
                        format(self._key))
                else:
                    pass  # TODO: Handle errors

            elif "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    json_data = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(
                                json_data["progress"]["printTime"] +
                                json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(
                                max(json_data["job"]["estimatedPrintTime"],
                                    json_data["progress"]["printTime"]))
                        elif progress > 0:
                            self.setTimeTotal(
                                json_data["progress"]["printTime"] /
                                (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if "files" in reply.url().toString(
            ):  # Result from /files command:
                if http_status_code == 201:
                    Logger.log(
                        "d", "Resource created on OctoPrint instance: %s",
                        reply.header(
                            QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()
                global_container_stack = Application.getInstance(
                ).getGlobalContainerStack()
                if not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(
                            reply.header(QNetworkRequest.LocationHeader).
                            toString()).fileName()
                        message = Message(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Saved to OctoPrint as {0}").format(file_name))
                    else:
                        message = Message(
                            i18n_catalog.i18nc("@info:status",
                                               "Saved to OctoPrint"))
                    message.addAction(
                        "open_browser",
                        i18n_catalog.i18nc("@action:button",
                                           "Open OctoPrint..."), "globe",
                        i18n_catalog.i18nc("@info:tooltip",
                                           "Open the OctoPrint web interface"))
                    message.actionTriggered.connect(
                        self._onMessageActionTriggered)
                    message.show()

            elif "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d",
                       "OctoPrintOutputDevice got an unhandled operation %s",
                       reply.operation())

    def _onStreamDownloadProgress(self, bytes_received, bytes_total):
        self._stream_buffer += self._image_reply.readAll()

        if self._stream_buffer_start_index == -1:
            self._stream_buffer_start_index = self._stream_buffer.indexOf(
                b'\xff\xd8')
        stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')

        if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
            jpg_data = self._stream_buffer[
                self._stream_buffer_start_index:stream_buffer_end_index + 2]
            self._stream_buffer = self._stream_buffer[stream_buffer_end_index +
                                                      2:]
            self._stream_buffer_start_index = -1

            self._camera_image.loadFromData(jpg_data)
            self.newImage.emit()

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                if progress > self._progress_message.getProgress():
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(
                    i18n_catalog.i18nc("@info:status",
                                       "Storing data on OctoPrint"), 0, False,
                    -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._base_url))
コード例 #19
0
    def run(self):
        status_message = Message(
            i18n_catalog.i18nc("@info:status",
                               "Multiplying and placing objects"),
            lifetime=0,
            dismissable=False,
            progress=0,
            title=i18n_catalog.i18nc("@info:title", "Placing Object"))
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        machine_width = global_container_stack.getProperty(
            "machine_width", "value")
        machine_depth = global_container_stack.getProperty(
            "machine_depth", "value")

        root = scene.getRoot()
        scale = 0.5
        arranger = Arrange.create(x=machine_width,
                                  y=machine_depth,
                                  scene_root=root,
                                  scale=scale,
                                  min_offset=self._min_offset)
        processed_nodes = []
        nodes = []

        not_fit_count = 0

        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and (
                    current_node.getParent().callDecoration("isGroup")
                    or current_node.getParent().callDecoration("isSliceable")):
                current_node = current_node.getParent()

            if current_node in processed_nodes:
                continue
            processed_nodes.append(current_node)

            node_too_big = False
            if node.getBoundingBox(
            ).width < machine_width or node.getBoundingBox(
            ).depth < machine_depth:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(
                    current_node, min_offset=self._min_offset, scale=scale)
            else:
                node_too_big = True

            found_solution_for_all = True
            arranger.resetLastPriority()
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                new_node = copy.deepcopy(node)
                solution_found = False
                if not node_too_big:
                    solution_found = arranger.findNodePlacement(
                        new_node, offset_shape_arr, hull_shape_arr)

                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = new_node.getPosition()
                    new_location = new_location.set(z=-not_fit_count * 20)
                    new_node.setPosition(new_location)
                    not_fit_count += 1

                # Same build plate
                build_plate_number = current_node.callDecoration(
                    "getBuildPlateNumber")
                new_node.callDecoration("setBuildPlateNumber",
                                        build_plate_number)
                for child in new_node.getChildren():
                    child.callDecoration("setBuildPlateNumber",
                                         build_plate_number)

                nodes.append(new_node)
                current_progress += 1
                status_message.setProgress(
                    (current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                op.addOperation(
                    AddSceneNodeOperation(new_node, current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc(
                "@info:status",
                "Unable to find a location within the build volume for all objects"
            ),
                                               title=i18n_catalog.i18nc(
                                                   "@info:title",
                                                   "Placing Object"))
            no_full_solution_message.show()
コード例 #20
0
class NautilusOutputDevice(OutputDevice):
    def __init__(self, name, url, duet_password, http_user, http_password, device_type):
        self._device_type = device_type
        if device_type == DeviceType.upload:
            description = catalog.i18nc("@action:button", "Send to {0}").format(name)
            name_id = name + "-upload"
            priority = 10
        else:
            assert False

        super().__init__(name_id)
        self.setShortDescription(description)
        self.setDescription(description)
        self.setPriority(priority)

        self._stage = OutputStage.ready
        self._name = name
        self._name_id = name_id
        self._device_type = device_type
        self._url = url
        self._duet_password = duet_password
        self._http_user = http_user
        self._http_password = http_password

        Logger.log("d", self._name_id + " | New Nautilus Connected")
        Logger.log("d", self._name_id + " | URL: " + self._url)
        Logger.log("d", self._name_id + " | Nautilus password: "******"set." if self._duet_password else "empty."))
        Logger.log("d", self._name_id + " | HTTP Basic Auth user: "******"set." if self._http_user else "empty."))
        Logger.log("d", self._name_id + " | HTTP Basic Auth password: "******"set." if self._http_password else "empty."))

        self._qnam = QtNetwork.QNetworkAccessManager()

        self._stream = None
        self._cleanupRequest()

        if hasattr(self, '_message'):
            self._message.hide()
        self._message = None


    def _timestamp(self):
        return ("time", datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S'))

    def _send(self, command, query=None, next_stage=None, data=None):
        enc_query = urllib.parse.urlencode(query or dict())
        if enc_query:
            command += '?' + enc_query
        self._request = QtNetwork.QNetworkRequest(QUrl(self._url + "rr_" + command))
        self._request.setRawHeader(b'User-Agent', b'Cura Plugin Nautilus')
        self._request.setRawHeader(b'Accept', b'application/json, text/javascript')
        self._request.setRawHeader(b'Connection', b'keep-alive')

        if self._http_user and self._http_password:
            self._request.setRawHeader(b'Authorization', b'Basic ' + base64.b64encode("{}:{}".format(self._http_user, self._http_password).encode()))

        if data:
            self._request.setRawHeader(b'Content-Type', b'application/octet-stream')
            self._reply = self._qnam.post(self._request, data)
            self._reply.uploadProgress.connect(self._onUploadProgress)
        else:
            self._reply = self._qnam.get(self._request)

        if next_stage:
            self._reply.finished.connect(next_stage)
        self._reply.error.connect(self._onNetworkError)

    def nameMaker(self):
        base = Application.getInstance().getPrintInformation().baseName

        mat = str(Application.getInstance().getPrintInformation().materialNames)[2:-2]

        noz = Application.getInstance().getExtruderManager().getActiveExtruderStacks()[0].variant.getName()
        layerheight = str(int(Application.getInstance().getMachineManager().activeMachine.getProperty("layer_height", "value")*1000)) + 'um'
        fileName = base + " - " +  mat + " - " + noz + " - " + layerheight
        return fileName

    def requestWrite(self, node, fileName=None, *args, **kwargs):
        if self._stage != OutputStage.ready:
            raise OutputDeviceError.DeviceBusyError()

        if fileName:
            fileName = self.nameMaker() + '.gcode'
        else:
            fileName = "%s.gcode" % Application.getInstance().getPrintInformation().jobName
        self._fileName = fileName
        self._baseLength = len(Application.getInstance().getPrintInformation().baseName)
        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'qml', 'UploadFilename.qml')
        self._dialog = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
        self._dialog.textChanged.connect(self.onFilenameChanged)
        self._dialog.accepted.connect(self.onFilenameAccepted)
        self._dialog.show()
        self._dialog.findChild(QObject, "nameField").setProperty('text', self._fileName)
        self._dialog.findChild(QObject, "nameField").select(0, self._baseLength)
        self._dialog.findChild(QObject, "nameField").setProperty('focus', True)

    def onFilenameChanged(self):
        fileName = self._dialog.findChild(QObject, "nameField").property('text')
        self._dialog.setProperty('validName', len(fileName) > 0)

    def onFilenameAccepted(self):
        self._fileName = self._dialog.findChild(QObject, "nameField").property('text')
        if not self._fileName.endswith('.gcode') and '.' not in self._fileName:
            self._fileName += '.gcode'
        Logger.log("d", self._name_id + " | Filename set to: " + self._fileName)

        self._dialog.deleteLater()

        # create the temp file for the gcode
        self._stream = StringIO()
        self._stage = OutputStage.writing
        self.writeStarted.emit(self)

        # show a progress message
        self._message = Message(catalog.i18nc("@info:progress", "Sending to {}").format(self._name), 0, False, -1)
        self._message.show()

        Logger.log("d", self._name_id + " | Loading gcode...")

        # get the g-code through the GCodeWrite plugin
        # this serializes the actual scene and should produce the same output as "Save to File"
        gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
        success = gcode_writer.write(self._stream, None)
        if not success:
            Logger.log("e", "GCodeWrite failed.")
            return

        # start
        Logger.log("d", self._name_id + " | Connecting...")
        self._send('connect', [("password", self._duet_password), self._timestamp()], self.onUploadReady)

    def onUploadReady(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Uploading...")
        self._stream.seek(0)
        self._postData = QByteArray()
        self._postData.append(self._stream.getvalue().encode())
        self._send('upload', [("name", "0:/gcodes/" + self._fileName), self._timestamp()], self.onUploadDone, self._postData)

    def onUploadDone(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Upload done")

        self._stream.close()
        self.stream = None

        if self._device_type == DeviceType.upload:
            self._send('disconnect')
            if self._message:
                self._message.hide()
            text = "Uploaded file {} to {}.".format(os.path.basename(self._fileName), self._name)
            self._message = Message(catalog.i18nc("@info:status", text), 0, False)
            self._message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl."))
            self._message.actionTriggered.connect(self._onMessageActionTriggered)
            self._message.show()

            self.writeSuccess.emit(self)
            self._cleanupRequest()

    def onReadyToPrint(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Ready to print")
        self._send('gcode', [("gcode", 'M32 "0:/gcodes/' + self._fileName + '"')], self.onPrintStarted)

    def onPrintStarted(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Print started")

        self._send('disconnect')
        if self._message:
            self._message.hide()
        text = "Print started on {} with file {}".format(self._name, self._fileName)
        self._message = Message(catalog.i18nc("@info:status", text), 0, False)
        self._message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self.writeSuccess.emit(self)
        self._cleanupRequest()

    def onSimulationPrintStarted(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Simulation print started for file " + self._fileName)

        # give it some to start the simulation
        QTimer.singleShot(15000, self.onCheckStatus)

    def onCheckStatus(self):
        if self._stage != OutputStage.writing:
            return

        Logger.log("d", self._name_id + " | Checking status...")

        self._send('status', [("type", "3")], self.onStatusReceived)

    def onStatusReceived(self):
        if self._stage != OutputStage.writing:
            return

        reply_body = bytes(self._reply.readAll()).decode()
        Logger.log("d", self._name_id + " | Status received | " + reply_body)

        status = json.loads(reply_body)
        if status["status"] in ['P', 'M'] :
            # still simulating
            # RRF 1.21RC2 and earlier used P while simulating
            # RRF 1.21RC3 and later uses M while simulating
            if self._message and "fractionPrinted" in status:
                self._message.setProgress(float(status["fractionPrinted"]))
            QTimer.singleShot(5000, self.onCheckStatus)
        else:
            Logger.log("d", self._name_id + " | Simulation print finished")
            self._send('reply', [], self.onReported)

    def onReported(self):
        if self._stage != OutputStage.writing:
            return

        reply_body = bytes(self._reply.readAll()).decode().strip()
        Logger.log("d", self._name_id + " | Reported | " + reply_body)

        if self._message:
            self._message.hide()

        text = "Simulation finished on {}:\n\n{}".format(self._name, reply_body)
        self._message = Message(catalog.i18nc("@info:status", text), 0, False)
        self._message.addAction("open_browser", catalog.i18nc("@action:button", "Open Browser"), "globe", catalog.i18nc("@info:tooltip", "Open browser to DuetWebControl."))
        self._message.actionTriggered.connect(self._onMessageActionTriggered)
        self._message.show()

        self._send('disconnect')
        self.writeSuccess.emit(self)
        self._cleanupRequest()

    def _onProgress(self, progress):
        if self._message:
            self._message.setProgress(progress)
        self.writeProgress.emit(self, progress)

    def _cleanupRequest(self):
        self._reply = None
        self._request = None
        if self._stream:
            self._stream.close()
        self._stream = None
        self._stage = OutputStage.ready
        self._fileName = None

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._url))
            if self._message:
                self._message.hide()
            self._message = None

    def _onUploadProgress(self, bytesSent, bytesTotal):
        if bytesTotal > 0:
            self._onProgress(int(bytesSent * 100 / bytesTotal))

    def _onNetworkError(self, errorCode):
        Logger.log("e", "_onNetworkError: %s", repr(errorCode))
        if self._message:
            self._message.hide()
        self._message = None

        if self._reply:
            errorString = self._reply.errorString()
        else:
            errorString = ''
        message = Message(catalog.i18nc("@info:status", "There was a network error: {} {}").format(errorCode, errorString), 0, False)
        message.show()

        self.writeError.emit(self)
        self._cleanupRequest()
コード例 #21
0
class OctoPrintOutputDevice(NetworkedPrinterOutputDevice):
    def __init__(self, instance_id: str, address: str, port: int,
                 properties: dict, **kwargs) -> None:
        super().__init__(device_id=instance_id,
                         address=address,
                         properties=properties,
                         **kwargs)

        self._address = address
        self._port = port
        self._path = properties.get(b"path", b"/").decode("utf-8")
        if self._path[-1:] != "/":
            self._path += "/"
        self._id = instance_id
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode_stream = None  # type: Optional[Union[StringIO, BytesIO]]

        self._auto_print = True
        self._forced_queue = False

        # We start with a single extruder, but update this when we get data from octoprint
        self._number_of_extruders_set = False
        self._number_of_extruders = 1

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            plugin_version = "Unknown"
            Logger.logException(
                "w", "Could not get version information for the plugin")

        self._user_agent_header = "User-Agent".encode()
        self._user_agent = (
            "%s/%s %s/%s" %
            (CuraApplication.getInstance().getApplicationName(),
             CuraApplication.getInstance().getVersion(), "OctoPrintPlugin",
             plugin_version)
        )  # NetworkedPrinterOutputDevice defines this as string, so we encode this later

        self._api_prefix = "api/"
        self._api_header = "X-Api-Key".encode()
        self._api_key = b""

        self._protocol = "https" if properties.get(
            b'useHttps') == b"true" else "http"
        self._base_url = "%s://%s:%d%s" % (self._protocol, self._address,
                                           self._port, self._path)
        self._api_url = self._base_url + self._api_prefix

        self._basic_auth_header = "Authorization".encode()
        self._basic_auth_data = None
        basic_auth_username = properties.get(b"userName", b"").decode("utf-8")
        basic_auth_password = properties.get(b"password", b"").decode("utf-8")
        if basic_auth_username and basic_auth_password:
            data = base64.b64encode(
                ("%s:%s" % (basic_auth_username,
                            basic_auth_password)).encode()).decode("utf-8")
            self._basic_auth_data = ("basic %s" % data).encode()

        try:
            major_api_version = CuraApplication.getInstance().getAPIVersion(
            ).getMajor()
        except AttributeError:
            # UM.Application.getAPIVersion was added for API > 6 (Cura 4)
            # Since this plugin version is only compatible with Cura 3.5 and newer, it is safe to assume API 5
            major_api_version = 5

        if major_api_version <= 5:
            # In Cura 3.x, the monitor item only shows the camera stream
            self._monitor_view_qml_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "qml",
                "MonitorItem3x.qml")
        else:
            # In Cura 4.x, the monitor item shows the camera stream as well as the monitor sidebar
            self._monitor_view_qml_path = os.path.join(
                os.path.dirname(os.path.abspath(__file__)), "qml",
                "MonitorItem4x.qml")

        name = self._id
        matches = re.search(r"^\"(.*)\"\._octoprint\._tcp.local$", name)
        if matches:
            name = matches.group(1)

        self.setPriority(
            2
        )  # Make sure the output device gets selected above local file output
        self.setName(name)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connected to OctoPrint on {0}").format(
                                   self._id))

        self._post_reply = None

        self._progress_message = None  # type: Union[None, Message]
        self._error_message = None  # type: Union[None, Message]
        self._connection_message = None  # type: Union[None, Message]

        self._queued_gcode_commands = []  # type: List[str]
        self._queued_gcode_timer = QTimer()
        self._queued_gcode_timer.setInterval(0)
        self._queued_gcode_timer.setSingleShot(True)
        self._queued_gcode_timer.timeout.connect(self._sendQueuedGcode)

        # TODO; Add preference for update intervals
        self._update_fast_interval = 2000
        self._update_slow_interval = 10000
        self._update_timer = QTimer()
        self._update_timer.setInterval(self._update_fast_interval)
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._psucontrol_timer = QTimer()
        self._psucontrol_timer.setInterval(
            20000)  # TODO; Add preference for timer interval
        self._psucontrol_timer.setSingleShot(True)
        self._psucontrol_timer.timeout.connect(self._startPrint)

        self._show_camera = True
        self._camera_mirror = False
        self._camera_rotation = 0
        self._camera_url = ""
        self._camera_shares_proxy = False

        self._sd_supported = False  # supports storing gcode on sd card in printer
        self._ufp_supported = False  # supports .ufp files in addition to raw .gcode files

        self._plugin_data = {}  #type: Dict[str, Any]

        self._output_controller = GenericOutputController(self)

    def getProperties(self) -> Dict[bytes, bytes]:
        return self._properties

    @pyqtSlot(str, result=str)
    def getProperty(self, key: str) -> str:
        key_b = key.encode("utf-8")
        if key_b in self._properties:
            return self._properties.get(key_b, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result=str)
    def getId(self) -> str:
        return self._id

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key: str) -> None:
        self._api_key = api_key.encode()

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def name(self) -> str:
        return self._name

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self) -> str:
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self) -> str:
        return self._address

    ## IPadress of this instance
    #  Overridden from NetworkedPrinterOutputDevice because OctoPrint does not
    #  send the ip address with zeroconf
    @pyqtProperty(str, constant=True)
    def address(self) -> str:
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self) -> int:
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self) -> str:
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self) -> str:
        return self._base_url

    cameraOrientationChanged = pyqtSignal()

    @pyqtProperty("QVariantMap", notify=cameraOrientationChanged)
    def cameraOrientation(self) -> Dict[str, Any]:
        return {
            "mirror": self._camera_mirror,
            "rotation": self._camera_rotation,
        }

    cameraUrlChanged = pyqtSignal()

    @pyqtProperty("QUrl", notify=cameraUrlChanged)
    def cameraUrl(self) -> QUrl:
        return QUrl(self._camera_url)

    def setShowCamera(self, show_camera: bool) -> None:
        if show_camera != self._show_camera:
            self._show_camera = show_camera
            self.showCameraChanged.emit()

    showCameraChanged = pyqtSignal()

    @pyqtProperty(bool, notify=showCameraChanged)
    def showCamera(self) -> bool:
        return self._show_camera

    def _update(self) -> None:
        ## Request 'general' printer data
        self.get("printer", self._onRequestFinished)

        ## Request print_job data
        self.get("job", self._onRequestFinished)

    def _createEmptyRequest(self,
                            target: str,
                            content_type: Optional[str] = "application/json"
                            ) -> QNetworkRequest:
        request = QNetworkRequest(QUrl(self._api_url + target))
        request.setRawHeader(self._user_agent_header,
                             self._user_agent.encode())
        request.setRawHeader(self._api_header, self._api_key)
        if content_type is not None:
            request.setHeader(QNetworkRequest.ContentTypeHeader,
                              "application/json")
        if self._basic_auth_data:
            request.setRawHeader(self._basic_auth_header,
                                 self._basic_auth_data)
        return request

    def close(self) -> None:
        self.setConnectionState(
            cast(ConnectionState, UnifiedConnectionState.Closed))
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()

    ##  Start requesting data from the instance
    def connect(self) -> None:
        self._createNetworkManager()

        self.setConnectionState(
            cast(ConnectionState, UnifiedConnectionState.Connecting))
        self._update(
        )  # Manually trigger the first update, as we don't want to wait a few secs before it starts.

        Logger.log("d", "Connection with instance %s with url %s started",
                   self._id, self._base_url)
        self._update_timer.start()

        self._last_response_time = None
        self._setAcceptsCommands(False)
        self.setConnectionText(
            i18n_catalog.i18nc("@info:status",
                               "Connecting to OctoPrint on {0}").format(
                                   self._id))

        ## Request 'settings' dump
        self.get("settings", self._onRequestFinished)

    ##  Stop requesting data from the instance
    def disconnect(self) -> None:
        Logger.log("d", "Connection with instance %s with url %s stopped",
                   self._id, self._base_url)
        self.close()

    def pausePrint(self) -> None:
        self._sendJobCommand("pause")

    def resumePrint(self) -> None:
        if not self._printers[0].activePrintJob:
            return

        if self._printers[0].activePrintJob.state == "paused":
            self._sendJobCommand("pause")
        else:
            self._sendJobCommand("start")

    def cancelPrint(self) -> None:
        self._sendJobCommand("cancel")

    def requestWrite(self,
                     nodes: List["SceneNode"],
                     file_name: Optional[str] = None,
                     limit_mimetypes: bool = False,
                     file_handler: Optional["FileHandler"] = None,
                     **kwargs: str) -> None:
        global_container_stack = CuraApplication.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        # Make sure post-processing plugin are run on the gcode
        self.writeStarted.emit(self)

        # Get the g-code through the GCodeWriter plugin
        # This produces the same output as "Save to File", adding the print settings to the bottom of the file
        if not self._ufp_supported:
            gcode_writer = cast(
                MeshWriter,
                PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
            self._gcode_stream = StringIO()
        else:
            gcode_writer = cast(
                MeshWriter,
                PluginRegistry.getInstance().getPluginObject("UFPWriter"))
            self._gcode_stream = BytesIO()

        if not gcode_writer.write(self._gcode_stream, None):
            Logger.log("e",
                       "GCodeWrite failed: %s" % gcode_writer.getInformation())
            return

        if self._error_message:
            self._error_message.hide()
            self._error_message = None

        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self._auto_print = parseBool(
            global_container_stack.getMetaDataEntry("octoprint_auto_print",
                                                    True))
        self._forced_queue = False

        use_psu_control = parseBool(
            global_container_stack.getMetaDataEntry("octoprint_psu_control",
                                                    False))

        if self.activePrinter.state == "offline" and "psucontrol" in self._plugin_data and use_psu_control:
            self._sendCommandToApi("plugin/psucontrol", "turnPSUOn")
            Logger.log("d", "PSU control 'on' command sent")
            self._psucontrol_timer.start()
            return

        elif self.activePrinter.state not in ["idle", ""]:
            Logger.log(
                "d", "Tried starting a print, but current state is %s" %
                self.activePrinter.state)
            if not self._auto_print:
                # Allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled
                self._error_message = None
            elif self.activePrinter.state == "offline":
                self._error_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "The printer is offline. Unable to start a new job."))
            else:
                self._error_message = Message(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "OctoPrint is busy. Unable to start a new job."))

            if self._error_message:
                self._error_message.addAction(
                    "Queue", i18n_catalog.i18nc("@action:button", "Queue job"),
                    "",
                    i18n_catalog.i18nc(
                        "@action:tooltip",
                        "Queue this print job so it can be printed later"))
                self._error_message.actionTriggered.connect(self._queuePrint)
                self._error_message.show()
                return

        self._startPrint()

    def _queuePrint(self,
                    message_id: Optional[str] = None,
                    action_id: Optional[str] = None) -> None:
        if self._error_message:
            self._error_message.hide()
        self._forced_queue = True
        self._startPrint()

    def _startPrint(self) -> None:
        global_container_stack = CuraApplication.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        if self._auto_print and not self._forced_queue:
            CuraApplication.getInstance().getController().setActiveStage(
                "MonitorStage")

            # cancel any ongoing preheat timer before starting a print
            try:
                self._printers[0].stopPreheatTimers()
            except AttributeError:
                # stopPreheatTimers was added after Cura 3.3 beta
                pass

        self._progress_message = Message(
            i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0,
            False, -1)
        self._progress_message.addAction(
            "Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), "", "")
        self._progress_message.actionTriggered.connect(self._cancelSendGcode)
        self._progress_message.show()

        job_name = CuraApplication.getInstance().getPrintInformation(
        ).jobName.strip()
        if job_name is "":
            job_name = "untitled_print"
        extension = "gcode" if not self._ufp_supported else "ufp"
        file_name = "%s.%s" % (job_name, extension)

        ##  Create multi_part request
        post_parts = []  # type: List[QHttpPart]

        ##  Create parts (to be placed inside multipart)
        post_part = QHttpPart()
        post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                            "form-data; name=\"select\"")
        post_part.setBody(b"true")
        post_parts.append(post_part)

        if self._auto_print and not self._forced_queue:
            post_part = QHttpPart()
            post_part.setHeader(QNetworkRequest.ContentDispositionHeader,
                                "form-data; name=\"print\"")
            post_part.setBody(b"true")
            post_parts.append(post_part)

        gcode_body = self._gcode_stream.getvalue()
        try:
            # encode StringIO result to bytes
            gcode_body = gcode_body.encode()
        except AttributeError:
            # encode BytesIO is already byte-encoded
            pass

        post_part = QHttpPart()
        post_part.setHeader(
            QNetworkRequest.ContentDispositionHeader,
            "form-data; name=\"file\"; filename=\"%s\"" % file_name)
        post_part.setBody(gcode_body)
        post_parts.append(post_part)

        destination = "local"
        if self._sd_supported and parseBool(
                global_container_stack.getMetaDataEntry(
                    "octoprint_store_sd", False)):
            destination = "sdcard"

        try:
            ##  Post request + data
            post_request = self._createEmptyRequest("files/" + destination)
            self._post_reply = self.postFormWithParts(
                "files/" + destination,
                post_parts,
                on_finished=self._onRequestFinished,
                on_progress=self._onUploadProgress)

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Unable to send data to OctoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log(
                "e",
                "An exception occurred in network connection: %s" % str(e))

        self._gcode_stream = None  # type: Optional[Union[StringIO, BytesIO]]

    def _cancelSendGcode(self,
                         message_id: Optional[str] = None,
                         action_id: Optional[str] = None) -> None:
        if self._post_reply:
            Logger.log("d", "Stopping upload because the user pressed cancel.")
            try:
                self._post_reply.uploadProgress.disconnect(
                    self._onUploadProgress)
            except TypeError:
                pass  # The disconnection can fail on mac in some cases. Ignore that.

            self._post_reply.abort()
            self._post_reply = None
        if self._progress_message:
            self._progress_message.hide()

    def sendCommand(self, command: str) -> None:
        self._queued_gcode_commands.append(command)
        self._queued_gcode_timer.start()

    # Send gcode commands that are queued in quick succession as a single batch
    def _sendQueuedGcode(self) -> None:
        if self._queued_gcode_commands:
            self._sendCommandToApi("printer/command",
                                   self._queued_gcode_commands)
            Logger.log("d", "Sent gcode command to OctoPrint instance: %s",
                       self._queued_gcode_commands)
            self._queued_gcode_commands = []  # type: List[str]

    def _sendJobCommand(self, command: str) -> None:
        self._sendCommandToApi("job", command)
        Logger.log("d", "Sent job command to OctoPrint instance: %s", command)

    def _sendCommandToApi(self, end_point: str,
                          commands: Union[str, List[str]]) -> None:
        if isinstance(commands, list):
            data = json.dumps({"commands": commands})
        else:
            data = json.dumps({"command": commands})
        self.post(end_point, data, self._onRequestFinished)

    ## Overloaded from NetworkedPrinterOutputDevice.post() to backport https://github.com/Ultimaker/Cura/pull/4678
    def post(self,
             url: str,
             data: Union[str, bytes],
             on_finished: Optional[Callable[[QNetworkReply], None]],
             on_progress: Optional[Callable[[int, int], None]] = None) -> None:
        self._validateManager()

        request = self._createEmptyRequest(url)
        self._last_request_time = time()

        if not self._manager:
            Logger.log("e", "Could not find manager.")
            return

        body = data if isinstance(data,
                                  bytes) else data.encode()  # type: bytes
        reply = self._manager.post(request, body)
        if on_progress is not None:
            reply.uploadProgress.connect(on_progress)
        self._registerOnFinishedCallback(reply, on_finished)

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(
                cast(ConnectionState, UnifiedConnectionState.Error))
            return

        if self._connection_state_before_timeout and reply.error(
        ) == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log(
                    "d",
                    "We got a response from the instance after %s of silence",
                    time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        error_handled = False

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if self._api_prefix + "printer" in reply.url().toString(
            ):  # Status update from /printer.
                if not self._printers:
                    self._createPrinterList()

                # An OctoPrint instance has a single printer.
                printer = self._printers[0]
                update_pace = self._update_slow_interval

                if http_status_code == 200:
                    update_pace = self._update_fast_interval

                    if not self.acceptsCommands:
                        self._setAcceptsCommands(True)
                        self.setConnectionText(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Connected to OctoPrint on {0}").format(
                                    self._id))

                    if self._connection_state == UnifiedConnectionState.Connecting:
                        self.setConnectionState(
                            cast(ConnectionState,
                                 UnifiedConnectionState.Connected))
                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "temperature" in json_data:
                        if not self._number_of_extruders_set:
                            self._number_of_extruders = 0
                            while "tool%d" % self._number_of_extruders in json_data[
                                    "temperature"]:
                                self._number_of_extruders += 1

                            if self._number_of_extruders > 1:
                                # Recreate list of printers to match the new _number_of_extruders
                                self._createPrinterList()
                                printer = self._printers[0]

                            if self._number_of_extruders > 0:
                                self._number_of_extruders_set = True

                        # Check for hotend temperatures
                        for index in range(0, self._number_of_extruders):
                            extruder = printer.extruders[index]
                            if ("tool%d" % index) in json_data["temperature"]:
                                hotend_temperatures = json_data["temperature"][
                                    "tool%d" % index]
                                target_temperature = hotend_temperatures[
                                    "target"] if hotend_temperatures[
                                        "target"] is not None else -1
                                actual_temperature = hotend_temperatures[
                                    "actual"] if hotend_temperatures[
                                        "actual"] is not None else -1
                                extruder.updateTargetHotendTemperature(
                                    target_temperature)
                                extruder.updateHotendTemperature(
                                    actual_temperature)
                            else:
                                extruder.updateTargetHotendTemperature(0)
                                extruder.updateHotendTemperature(0)

                        if "bed" in json_data["temperature"]:
                            bed_temperatures = json_data["temperature"]["bed"]
                            actual_temperature = bed_temperatures[
                                "actual"] if bed_temperatures[
                                    "actual"] is not None else -1
                            printer.updateBedTemperature(actual_temperature)
                            target_temperature = bed_temperatures[
                                "target"] if bed_temperatures[
                                    "target"] is not None else -1
                            printer.updateTargetBedTemperature(
                                target_temperature)
                        else:
                            printer.updateBedTemperature(-1)
                            printer.updateTargetBedTemperature(0)

                    printer_state = "offline"
                    if "state" in json_data:
                        flags = json_data["state"]["flags"]
                        if flags["error"] or flags["closedOrError"]:
                            printer_state = "error"
                        elif flags["paused"] or flags["pausing"]:
                            printer_state = "paused"
                        elif flags["printing"]:
                            printer_state = "printing"
                        elif flags["cancelling"]:
                            printer_state = "aborted"
                        elif flags["ready"] or flags["operational"]:
                            printer_state = "idle"
                    printer.updateState(printer_state)

                elif http_status_code == 401:
                    printer.updateState("offline")
                    if printer.activePrintJob:
                        printer.activePrintJob.updateState("offline")
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "OctoPrint on {0} does not allow access to print").
                        format(self._id))
                    error_handled = True

                elif http_status_code == 409:
                    if self._connection_state == UnifiedConnectionState.Connecting:
                        self.setConnectionState(
                            cast(ConnectionState,
                                 UnifiedConnectionState.Connected))

                    printer.updateState("offline")
                    if printer.activePrintJob:
                        printer.activePrintJob.updateState("offline")
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "The printer connected to OctoPrint on {0} is not operational"
                        ).format(self._id))
                    error_handled = True

                elif http_status_code == 502 or http_status_code == 503:
                    printer.updateState("offline")
                    if printer.activePrintJob:
                        printer.activePrintJob.updateState("offline")
                    self.setConnectionText(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "OctoPrint on {0} is not running").format(
                                self._id))
                    error_handled = True

                else:
                    printer.updateState("offline")
                    if printer.activePrintJob:
                        printer.activePrintJob.updateState("offline")
                    Logger.log("w", "Received an unexpected returncode: %d",
                               http_status_code)

                if update_pace != self._update_timer.interval():
                    self._update_timer.setInterval(update_pace)

            elif self._api_prefix + "job" in reply.url().toString(
            ):  # Status update from /job:
                if not self._printers:
                    return  # Ignore the data for now, we don't have info about a printer yet.
                printer = self._printers[0]

                if http_status_code == 200:
                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if printer.activePrintJob is None:
                        print_job = PrintJobOutputModel(
                            output_controller=self._output_controller)
                        printer.updateActivePrintJob(print_job)
                    else:
                        print_job = printer.activePrintJob

                    print_job_state = "offline"
                    if "state" in json_data:
                        if json_data["state"] == "Error":
                            print_job_state = "error"
                        elif json_data["state"] == "Pausing":
                            print_job_state = "pausing"
                        elif json_data["state"] == "Paused":
                            print_job_state = "paused"
                        elif json_data["state"] == "Printing":
                            print_job_state = "printing"
                        elif json_data["state"] == "Cancelling":
                            print_job_state = "abort"
                        elif json_data["state"] == "Operational":
                            print_job_state = "ready"
                            printer.updateState("idle")
                    print_job.updateState(print_job_state)

                    print_time = json_data["progress"]["printTime"]
                    if print_time:
                        print_job.updateTimeElapsed(print_time)
                        if json_data["progress"][
                                "completion"]:  # not 0 or None or ""
                            print_job.updateTimeTotal(
                                print_time /
                                (json_data["progress"]["completion"] / 100))
                        else:
                            print_job.updateTimeTotal(0)
                    else:
                        print_job.updateTimeElapsed(0)
                        print_job.updateTimeTotal(0)

                    print_job.updateName(json_data["job"]["file"]["name"])
                else:
                    pass  # See generic error handler below

            elif self._api_prefix + "settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                if http_status_code == 200:
                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data[
                            "feature"]:
                        self._sd_supported = json_data["feature"]["sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data[
                            "webcam"]:
                        self._camera_shares_proxy = False
                        stream_url = json_data["webcam"]["streamUrl"]
                        if not stream_url:  #empty string or None
                            self._camera_url = ""
                        elif stream_url[:4].lower() == "http":  # absolute uri
                            self._camera_url = stream_url
                        elif stream_url[:2] == "//":  # protocol-relative
                            self._camera_url = "%s:%s" % (self._protocol,
                                                          stream_url)
                        elif stream_url[:
                                        1] == ":":  # domain-relative (on another port)
                            self._camera_url = "%s://%s%s" % (
                                self._protocol, self._address, stream_url)
                        elif stream_url[:
                                        1] == "/":  # domain-relative (on same port)
                            self._camera_url = "%s://%s:%d%s" % (
                                self._protocol, self._address, self._port,
                                stream_url)
                            self._camera_shares_proxy = True
                        else:
                            Logger.log("w", "Unusable stream url received: %s",
                                       stream_url)
                            self._camera_url = ""

                        Logger.log("d", "Set OctoPrint camera url to %s",
                                   self._camera_url)
                        self.cameraUrlChanged.emit()

                        if "rotate90" in json_data["webcam"]:
                            self._camera_rotation = -90 if json_data["webcam"][
                                "rotate90"] else 0
                            if json_data["webcam"]["flipH"] and json_data[
                                    "webcam"]["flipV"]:
                                self._camera_mirror = False
                                self._camera_rotation += 180
                            elif json_data["webcam"]["flipH"]:
                                self._camera_mirror = True
                                self._camera_rotation += 180
                            elif json_data["webcam"]["flipV"]:
                                self._camera_mirror = True
                            else:
                                self._camera_mirror = False
                            self.cameraOrientationChanged.emit()

                    if "plugins" in json_data:
                        self._plugin_data = json_data["plugins"]

                        if "UltimakerFormatPackage" in self._plugin_data:
                            try:
                                ufp_writer_plugin = PluginRegistry.getInstance(
                                ).getPluginObject("UFPWriter")
                                self._ufp_supported = True
                                Logger.log("d", "Instance supports UFP files")
                            except PluginNotFoundError:
                                Logger.log(
                                    "w",
                                    "Instance supports UFP files, but UFPWriter is not available"
                                )

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if self._api_prefix + "files" in reply.url().toString(
            ):  # Result from /files command:
                if http_status_code == 201:
                    Logger.log(
                        "d", "Resource created on OctoPrint instance: %s",
                        reply.header(
                            QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # See generic error handler below

                reply.uploadProgress.disconnect(self._onUploadProgress)
                if self._progress_message:
                    self._progress_message.hide()

                if self._forced_queue or not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(
                            reply.header(QNetworkRequest.LocationHeader).
                            toString()).fileName()
                        message = Message(
                            i18n_catalog.i18nc(
                                "@info:status",
                                "Saved to OctoPrint as {0}").format(file_name))
                    else:
                        message = Message(
                            i18n_catalog.i18nc("@info:status",
                                               "Saved to OctoPrint"))
                    message.addAction(
                        "open_browser",
                        i18n_catalog.i18nc("@action:button", "OctoPrint..."),
                        "globe",
                        i18n_catalog.i18nc("@info:tooltip",
                                           "Open the OctoPrint web interface"))
                    message.actionTriggered.connect(self._openOctoPrint)
                    message.show()

            elif self._api_prefix + "job" in reply.url().toString(
            ):  # Result from /job command (eg start/pause):
                if http_status_code == 204:
                    Logger.log("d", "Octoprint job command accepted")
                else:
                    pass  # See generic error handler below

            elif self._api_prefix + "printer/command" in reply.url().toString(
            ):  # Result from /printer/command (gcode statements):
                if http_status_code == 204:
                    Logger.log("d", "Octoprint gcode command(s) accepted")
                else:
                    pass  # See generic error handler below

        else:
            Logger.log("d",
                       "OctoPrintOutputDevice got an unhandled operation %s",
                       reply.operation())

        if not error_handled and http_status_code >= 400:
            # Received an error reply
            error_string = bytes(reply.readAll()).decode("utf-8")
            if not error_string:
                error_string = reply.attribute(
                    QNetworkRequest.HttpReasonPhraseAttribute)
            if self._error_message:
                self._error_message.hide()
            self._error_message = Message(error_string,
                                          title=i18n_catalog.i18nc(
                                              "@label", "OctoPrint error"))
            self._error_message.show()
            return

    def _onUploadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if not self._progress_message:
            return

        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            previous_progress = self._progress_message.getProgress()
            if progress < 100:
                if previous_progress is not None and progress > previous_progress:
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(
                    i18n_catalog.i18nc("@info:status",
                                       "Storing data on OctoPrint"), 0, False,
                    -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _createPrinterList(self) -> None:
        printer = PrinterOutputModel(
            output_controller=self._output_controller,
            number_of_extruders=self._number_of_extruders)
        printer.updateName(self.name)
        self._printers = [printer]
        self.printersChanged.emit()

    def _openOctoPrint(self,
                       message_id: Optional[str] = None,
                       action_id: Optional[str] = None) -> None:
        QDesktopServices.openUrl(QUrl(self._base_url))
コード例 #22
0
class LayerView(View):
    # Must match LayerView.qml
    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
    LAYER_VIEW_TYPE_LINE_TYPE = 1

    def __init__(self):
        super().__init__()

        self._max_layers = 0
        self._current_layer_num = 0
        self._minimum_layer_num = 0
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._top_layers_job = None
        self._activity = False
        self._old_max_layers = 0

        self._busy = False

        self._ghost_shader = None
        self._layer_pass = None
        self._composite_pass = None
        self._old_layer_bindings = None
        self._layerview_composite_shader = None
        self._old_composite_shader = None

        self._global_container_stack = None
        self._proxy = LayerViewProxy.LayerViewProxy()
        self._controller.getScene().getRoot().childrenChanged.connect(
            self._onSceneChanged)

        self._resetSettings()
        self._legend_items = None

        Preferences.getInstance().addPreference("view/top_layer_count", 5)
        Preferences.getInstance().addPreference("view/only_show_top_layers",
                                                False)

        Preferences.getInstance().preferenceChanged.connect(
            self._onPreferencesChanged)

        self._solid_layers = int(
            Preferences.getInstance().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(
            Preferences.getInstance().getValue("view/only_show_top_layers"))
        self._compatibility_mode = True  # for safety

        self._wireprint_warning_message = Message(
            catalog.i18nc(
                "@info:status",
                "Cura does not accurately display layers when Wire Printing is enabled"
            ))

    def _resetSettings(self):
        self._layer_view_type = 0  # 0 is material color, 1 is color by linetype, 2 is speed
        self._extruder_count = 0
        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
        self._show_travel_moves = 0
        self._show_support = 1
        self._show_adhesion = 1
        self._show_skin = 1
        self._show_infill = 1

    def getActivity(self):
        return self._activity

    def getLayerPass(self):
        if not self._layer_pass:
            # Currently the RenderPass constructor requires a size > 0
            # This should be fixed in RenderPass's constructor.
            self._layer_pass = LayerPass.LayerPass(1, 1)
            self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
                Preferences.getInstance().getValue(
                    "view/force_layer_view_compatibility_mode"))
            self._layer_pass.setLayerView(self)
            self.getRenderer().addRenderPass(self._layer_pass)
        return self._layer_pass

    def getCurrentLayer(self):
        return self._current_layer_num

    def getMinimumLayer(self):
        return self._minimum_layer_num

    def _onSceneChanged(self, node):
        self.calculateMaxLayers()

    def getMaxLayers(self):
        return self._max_layers

    busyChanged = Signal()

    def isBusy(self):
        return self._busy

    def setBusy(self, busy):
        if busy != self._busy:
            self._busy = busy
            self.busyChanged.emit()

    def resetLayerData(self):
        self._current_layer_mesh = None
        self._current_layer_jumps = None

    def beginRendering(self):
        scene = self.getController().getScene()
        renderer = self.getRenderer()

        if not self._ghost_shader:
            self._ghost_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "color.shader"))
            self._ghost_shader.setUniformValue(
                "u_color",
                Color(*Application.getInstance().getTheme().getColor(
                    "layerview_ghost").getRgb()))

        for node in DepthFirstIterator(scene.getRoot()):
            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
            # However, it is somewhat relevant when the node is selected, so do render it then.
            if type(node) is ConvexHullNode and not Selection.isSelected(
                    node.getWatchedNode()):
                continue

            if not node.render(renderer):
                if (node.getMeshData()) and node.isVisible():
                    renderer.queueNode(node,
                                       transparent=True,
                                       shader=self._ghost_shader)

    def setLayer(self, value):
        if self._current_layer_num != value:
            self._current_layer_num = value
            if self._current_layer_num < 0:
                self._current_layer_num = 0
            if self._current_layer_num > self._max_layers:
                self._current_layer_num = self._max_layers

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setMinimumLayer(self, value):
        if self._minimum_layer_num != value:
            self._minimum_layer_num = value
            if self._minimum_layer_num < 0:
                self._minimum_layer_num = 0

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    ##  Set the layer view type
    #
    #   \param layer_view_type integer as in LayerView.qml and this class
    def setLayerViewType(self, layer_view_type):
        self._layer_view_type = layer_view_type
        self.currentLayerNumChanged.emit()

    ##  Return the layer view type, integer as in LayerView.qml and this class
    def getLayerViewType(self):
        return self._layer_view_type

    ##  Set the extruder opacity
    #
    #   \param extruder_nr 0..3
    #   \param opacity 0.0 .. 1.0
    def setExtruderOpacity(self, extruder_nr, opacity):
        self._extruder_opacity[extruder_nr] = opacity
        self.currentLayerNumChanged.emit()

    def getExtruderOpacities(self):
        return self._extruder_opacity

    def setShowTravelMoves(self, show):
        self._show_travel_moves = show
        self.currentLayerNumChanged.emit()

    def getShowTravelMoves(self):
        return self._show_travel_moves

    def setShowSupport(self, show):
        self._show_support = show
        self.currentLayerNumChanged.emit()

    def getShowSupport(self):
        return self._show_support

    def setShowAdhesion(self, show):
        self._show_adhesion = show
        self.currentLayerNumChanged.emit()

    def getShowAdhesion(self):
        return self._show_adhesion

    def setShowSkin(self, show):
        self._show_skin = show
        self.currentLayerNumChanged.emit()

    def getShowSkin(self):
        return self._show_skin

    def setShowInfill(self, show):
        self._show_infill = show
        self.currentLayerNumChanged.emit()

    def getShowInfill(self):
        return self._show_infill

    def getCompatibilityMode(self):
        return self._compatibility_mode

    def getExtruderCount(self):
        return self._extruder_count

    def calculateMaxLayers(self):
        scene = self.getController().getScene()
        self._activity = True

        self._old_max_layers = self._max_layers
        ## Recalculate num max layers
        new_max_layers = 0
        for node in DepthFirstIterator(scene.getRoot()):
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            if new_max_layers < len(layer_data.getLayers()):
                new_max_layers = len(layer_data.getLayers()) - 1

        if new_max_layers > 0 and new_max_layers != self._old_max_layers:
            self._max_layers = new_max_layers

            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
            # if it's the largest value. If we don't do this, we can have a slider block outside of the
            # slider.
            if new_max_layers > self._current_layer_num:
                self.maxLayersChanged.emit()
                self.setLayer(int(self._max_layers))
            else:
                self.setLayer(int(self._max_layers))
                self.maxLayersChanged.emit()
        self._startUpdateTopLayers()

    maxLayersChanged = Signal()
    currentLayerNumChanged = Signal()
    globalStackChanged = Signal()
    preferencesChanged = Signal()

    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
    #   as this caused some issues.
    def getProxy(self, engine, script_engine):
        return self._proxy

    def endRendering(self):
        pass

    def enableLegend(self):
        Application.getInstance().setViewLegendItems(self._getLegendItems())

    def disableLegend(self):
        Application.getInstance().setViewLegendItems([])

    def event(self, event):
        modifiers = QApplication.keyboardModifiers()
        ctrl_is_active = modifiers == Qt.ControlModifier
        if event.type == Event.KeyPressEvent and ctrl_is_active:
            if event.key == KeyEvent.UpKey:
                self.setLayer(self._current_layer_num + 1)
                return True
            if event.key == KeyEvent.DownKey:
                self.setLayer(self._current_layer_num - 1)
                return True

        if event.type == Event.ViewActivateEvent:
            # Make sure the LayerPass is created
            self.getLayerPass()

            Application.getInstance().globalContainerStackChanged.connect(
                self._onGlobalStackChanged)
            self._onGlobalStackChanged()

            if not self._layerview_composite_shader:
                self._layerview_composite_shader = OpenGL.getInstance(
                ).createShaderProgram(
                    os.path.join(
                        PluginRegistry.getInstance().getPluginPath(
                            "LayerView"), "layerview_composite.shader"))
                theme = Application.getInstance().getTheme()
                self._layerview_composite_shader.setUniformValue(
                    "u_background_color",
                    Color(*theme.getColor("viewport_background").getRgb()))
                self._layerview_composite_shader.setUniformValue(
                    "u_outline_color",
                    Color(*theme.getColor("model_selection_outline").getRgb()))

            if not self._composite_pass:
                self._composite_pass = self.getRenderer().getRenderPass(
                    "composite")

            self._old_layer_bindings = self._composite_pass.getLayerBindings(
            )[:]  # make a copy so we can restore to it later
            self._composite_pass.getLayerBindings().append("layerview")
            self._old_composite_shader = self._composite_pass.getCompositeShader(
            )
            self._composite_pass.setCompositeShader(
                self._layerview_composite_shader)

            if self.getLayerViewType() == self.LAYER_VIEW_TYPE_LINE_TYPE:
                self.enableLegend()

        elif event.type == Event.ViewDeactivateEvent:
            self._wireprint_warning_message.hide()
            Application.getInstance().globalContainerStackChanged.disconnect(
                self._onGlobalStackChanged)
            if self._global_container_stack:
                self._global_container_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)

            self._composite_pass.setLayerBindings(self._old_layer_bindings)
            self._composite_pass.setCompositeShader(self._old_composite_shader)

            self.disableLegend()

    def _onGlobalStackChanged(self):
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(
                self._onPropertyChanged)
        self._global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(
                self._onPropertyChanged)
            self._extruder_count = self._global_container_stack.getProperty(
                "machine_extruder_count", "value")
            self._onPropertyChanged("wireframe_enabled", "value")
            self.globalStackChanged.emit()
        else:
            self._wireprint_warning_message.hide()

    def _onPropertyChanged(self, key, property_name):
        if key == "wireframe_enabled" and property_name == "value":
            if self._global_container_stack.getProperty(
                    "wireframe_enabled", "value"):
                self._wireprint_warning_message.show()
            else:
                self._wireprint_warning_message.hide()

    def _startUpdateTopLayers(self):
        if not self._compatibility_mode:
            return

        if self._top_layers_job:
            self._top_layers_job.finished.disconnect(
                self._updateCurrentLayerMesh)
            self._top_layers_job.cancel()

        self.setBusy(True)

        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(),
                                                   self._current_layer_num,
                                                   self._solid_layers)
        self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
        self._top_layers_job.start()

    def _updateCurrentLayerMesh(self, job):
        self.setBusy(False)

        if not job.getResult():
            return
        self.resetLayerData(
        )  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
        self._current_layer_mesh = job.getResult().get("layers")
        self._current_layer_jumps = job.getResult().get("jumps")
        self._controller.getScene().sceneChanged.emit(
            self._controller.getScene().getRoot())

        self._top_layers_job = None

    def _onPreferencesChanged(self, preference):
        if preference not in {
                "view/top_layer_count", "view/only_show_top_layers",
                "view/force_layer_view_compatibility_mode"
        }:
            return

        self._solid_layers = int(
            Preferences.getInstance().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(
            Preferences.getInstance().getValue("view/only_show_top_layers"))
        self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
            Preferences.getInstance().getValue(
                "view/force_layer_view_compatibility_mode"))

        self._startUpdateTopLayers()
        self.preferencesChanged.emit()

    def _getLegendItems(self):
        if self._legend_items is None:
            theme = Application.getInstance().getTheme()
            self._legend_items = [
                {
                    "color":
                    theme.getColor("layerview_inset_0").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Outer Wall")
                },  # Inset0Type
                {
                    "color":
                    theme.getColor("layerview_inset_x").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Inner Wall")
                },  # InsetXType
                {
                    "color":
                    theme.getColor("layerview_skin").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Top / Bottom")
                },  # SkinType
                {
                    "color":
                    theme.getColor("layerview_infill").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type", "Infill")
                },  # InfillType
                {
                    "color":
                    theme.getColor("layerview_support").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Support Skin")
                },  # SupportType
                {
                    "color":
                    theme.getColor("layerview_support_infill").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Support Infill")
                },  # SupportInfillType
                {
                    "color":
                    theme.getColor("layerview_support_interface").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Support Interface")
                },  # SupportInterfaceType
                {
                    "color":
                    theme.getColor("layerview_skirt").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Build Plate Adhesion")
                },  # SkirtType
                {
                    "color":
                    theme.getColor("layerview_move_combing").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Travel Move")
                },  # MoveCombingType
                {
                    "color":
                    theme.getColor("layerview_move_retraction").name(),
                    "title":
                    catalog.i18nc("@label:layerview polygon type",
                                  "Retraction Move")
                },  # MoveRetractionType
                #{"color": theme.getColor("layerview_none").name(), "title": catalog.i18nc("@label:layerview polygon type", "Unknown")} # NoneType
            ]
        return self._legend_items
コード例 #23
0
class ProcessSlicedObjectListJob(Job):
    def __init__(self, message):
        super().__init__()
        self._message = message
        self._scene = Application.getInstance().getController().getScene()
        self._progress = None

    def run(self):
        if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
            self._progress = Message(catalog.i18nc("Layers View mode", "Layers"), 0, False, 0)
            self._progress.show()

        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)

        objectIdMap = {}
        new_node = SceneNode()
        ## Put all nodes in a dict identified by ID
        for node in DepthFirstIterator(self._scene.getRoot()):
            if type(node) is SceneNode and node.getMeshData():
                if node.callDecoration("getLayerData"):
                #if hasattr(node.getMeshData(), "layerData"):
                    self._scene.getRoot().removeChild(node)
                else:
                    objectIdMap[id(node)] = node

        settings = Application.getInstance().getActiveMachine()
        layerHeight = settings.getSettingValueByKey("layer_height")

        center = None
        if not settings.getSettingValueByKey("machine_center_is_zero"):
            center = numpy.array([settings.getSettingValueByKey("machine_width") / 2, 0.0, -settings.getSettingValueByKey("machine_depth") / 2])
        else:
            center = numpy.array([0.0, 0.0, 0.0])

        if self._progress:
            self._progress.setProgress(2)

        mesh = MeshData()
        layer_data = LayerData.LayerData()
        for object in self._message.objects:
            try:
                node = objectIdMap[object.id]
            except KeyError:
                continue

            for layer in object.layers:
                layer_data.addLayer(layer.id)
                layer_data.setLayerHeight(layer.id, layer.height)
                layer_data.setLayerThickness(layer.id, layer.thickness)
                for polygon in layer.polygons:
                    points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array
                    points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
                    points = numpy.asarray(points, dtype=numpy.float32)
                    points /= 1000
                    points = numpy.insert(points, 1, (layer.height / 1000), axis = 1)

                    points[:,2] *= -1

                    points -= numpy.array(center)

                    layer_data.addPolygon(layer.id, polygon.type, points, polygon.line_width)

        if self._progress:
            self._progress.setProgress(50)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data
        layer_data.build()

        if self._progress:
            self._progress.setProgress(100)
        
        #Add layerdata decorator to scene node to indicate that the node has layerdata
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_data)
        new_node.addDecorator(decorator)
        
        new_node.setMeshData(mesh)
        new_node.setParent(self._scene.getRoot())
        
        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "LayerView":
            view.resetLayerData()

        if self._progress:
            self._progress.hide()

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
                if not self._progress:
                    self._progress = Message(catalog.i18nc("Layers View mode", "Layers"), 0, False, 0)
                    self._progress.show()
            else:
                if self._progress:
                    self._progress.hide()
コード例 #24
0
ファイル: Scene.py プロジェクト: sojhalb/Uranium
class Scene:
    def __init__(self) -> None:
        super().__init__()  # Call super to make multiple inheritance work.

        from UM.Scene.SceneNode import SceneNode
        self._root = SceneNode(name= "Root")
        self._root.setCalculateBoundingBox(False)
        self._connectSignalsRoot()
        self._active_camera = None  # type: Optional[Camera]
        self._ignore_scene_changes = False
        self._lock = threading.Lock()

        # Watching file for changes.
        self._file_watcher = QFileSystemWatcher()
        self._file_watcher.fileChanged.connect(self._onFileChanged)

    def _connectSignalsRoot(self) -> None:
        self._root.transformationChanged.connect(self.sceneChanged)
        self._root.childrenChanged.connect(self.sceneChanged)
        self._root.meshDataChanged.connect(self.sceneChanged)

    def _disconnectSignalsRoot(self) -> None:
        self._root.transformationChanged.disconnect(self.sceneChanged)
        self._root.childrenChanged.disconnect(self.sceneChanged)
        self._root.meshDataChanged.disconnect(self.sceneChanged)

    def setIgnoreSceneChanges(self, ignore_scene_changes: bool) -> None:
        if self._ignore_scene_changes != ignore_scene_changes:
            self._ignore_scene_changes = ignore_scene_changes
            if self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            else:
                self._connectSignalsRoot()

    ##  Acquire the global scene lock.
    #
    #   This will prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    #   Deprecated, use getSceneLock() instead.
    @deprecated("Please use the getSceneLock instead", "3.3")
    def acquireLock(self) -> None:
        self._lock.acquire()

    ##  Release the global scene lock.
    #   Deprecated, use getSceneLock() instead.
    @deprecated("Please use the getSceneLock instead", "3.3")
    def releaseLock(self) -> None:
        self._lock.release()

    ##  Gets the global scene lock.
    #
    #   Use this lock to prevent any read or write actions on the scene from other threads,
    #   assuming those threads also properly acquire the lock. Most notably, this
    #   prevents the rendering thread from rendering the scene while it is changing.
    def getSceneLock(self) -> threading.Lock:
        return self._lock

    ##  Get the root node of the scene.
    def getRoot(self) -> "SceneNode":
        return self._root

    ##  Change the root node of the scene
    def setRoot(self, node: "SceneNode") -> None:
        if self._root != node:
            if not self._ignore_scene_changes:
                self._disconnectSignalsRoot()
            self._root = node
            if not self._ignore_scene_changes:
                self._connectSignalsRoot()
            self.rootChanged.emit()

    rootChanged = Signal()

    ##  Get the camera that should be used for rendering.
    def getActiveCamera(self) -> Optional[Camera]:
        return self._active_camera

    def getAllCameras(self) -> List[Camera]:
        cameras = []
        for node in BreadthFirstIterator(self._root): #type: ignore
            if isinstance(node, Camera):
                cameras.append(node)
        return cameras

    ##  Set the camera that should be used for rendering.
    #   \param name The name of the camera to use.
    def setActiveCamera(self, name: str) -> None:
        camera = self.findCamera(name)
        if camera:
            self._active_camera = camera
        else:
            Logger.log("w", "Couldn't find camera with name [%s] to activate!" % name)

    ##  Signal. Emitted whenever something in the scene changes.
    #   \param object The object that triggered the change.
    sceneChanged = Signal()

    ##  Find an object by id.
    #
    #   \param object_id The id of the object to search for, as returned by the python id() method.
    #
    #   \return The object if found, or None if not.
    def findObject(self, object_id: int) -> Optional["SceneNode"]:
        for node in BreadthFirstIterator(self._root): #type: ignore
            if id(node) == object_id:
                return node
        return None

    def findCamera(self, name: str) -> Optional[Camera]:
        for node in BreadthFirstIterator(self._root): #type: ignore
            if isinstance(node, Camera) and node.getName() == name:
                return node
        return None

    ##  Add a file to be watched for changes.
    #   \param file_path The path to the file that must be watched.
    def addWatchedFile(self, file_path: str) -> None:
        self._file_watcher.addPath(file_path)

    ##  Remove a file so that it will no longer be watched for changes.
    #   \param file_path The path to the file that must no longer be watched.
    def removeWatchedFile(self, file_path: str) -> None:
        self._file_watcher.removePath(file_path)

    ##  Triggered whenever a file is changed that we currently have loaded.
    def _onFileChanged(self, file_path: str) -> None:
        if not os.path.isfile(file_path) or os.path.getsize(file_path) == 0: #File doesn't exist any more, or it is empty
            return

        #Multiple nodes may be loaded from the same file at different stages. Reload them all.
        from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator #To find which nodes to reload when files have changed.
        modified_nodes = [node for node in DepthFirstIterator(self.getRoot()) if node.getMeshData() and node.getMeshData().getFileName() == file_path] #type: ignore

        if modified_nodes:
            self._reload_message = Message(i18n_catalog.i18nc("@info", "Would you like to reload {filename}?").format(filename = os.path.basename(file_path)),
                              title = i18n_catalog.i18nc("@info:title", "File has been modified"))
            self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = None, description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk."))
            self._reload_callback = functools.partial(self._reloadNodes, modified_nodes)
            self._reload_message.actionTriggered.connect(self._reload_callback)
            self._reload_message.show()

    ##  Reloads a list of nodes after the user pressed the "Reload" button.
    #   \param nodes The list of nodes that needs to be reloaded.
    #   \param message The message that triggered the action to reload them.
    #   \param action The button that triggered the action to reload them.
    def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None:
        if action != "reload":
            return
        self._reload_message.hide()
        for node in nodes:
            meshdata = node.getMeshData()
            if meshdata:
                filename = meshdata.getFileName()
                if not filename or not os.path.isfile(filename): #File doesn't exist any more.
                    continue
                job = ReadMeshJob(filename)
                self._reload_finished_callback = functools.partial(self._reloadJobFinished, node)
                job.finished.connect(self._reload_finished_callback)
                job.start()

    ##  Triggered when reloading has finished.
    #
    #   This then puts the resulting mesh data in the node.
    def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None:
        for node in job.getResult():
            mesh_data = node.getMeshData()
            if mesh_data:
                replaced_node.setMeshData(mesh_data)
            else:
                Logger.log("w", "Could not find a mesh in reloaded node.")
コード例 #25
0
ファイル: CuraEngineBackend.py プロジェクト: hmflash/Cura
class CuraEngineBackend(Backend):
    def __init__(self):
        super().__init__()

        # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
        default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", "CuraEngine")
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
        if sys.platform == "win32":
            default_engine_location += ".exe"
        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location", default_engine_location)

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = None


        self._profile = None
        Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
        self._onActiveProfileChanged()

        self._change_timer = QTimer()
        self._change_timer.setInterval(500)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self.slice)

        self._message_handlers[Cura_pb2.SlicedObjectList] = self._onSlicedObjectListMessage
        self._message_handlers[Cura_pb2.Progress] = self._onProgressMessage
        self._message_handlers[Cura_pb2.GCodeLayer] = self._onGCodeLayerMessage
        self._message_handlers[Cura_pb2.GCodePrefix] = self._onGCodePrefixMessage
        self._message_handlers[Cura_pb2.ObjectPrintTime] = self._onObjectPrintTimeMessage

        self._slicing = False
        self._restart = False

        self._save_gcode = True
        self._save_polygons = True
        self._report_progress = True

        self._enabled = True

        self._message = None

        self.backendConnected.connect(self._onBackendConnected)

    ##  Get the command that is used to call the engine.
    #   This is usefull for debugging and used to actually start the engine
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port),  "-j", Resources.getPath(Resources.MachineDefinitions, "fdmprinter.json"), "-vv"]

    ##  Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted whne the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    ##  Perform a slice of the scene with the given set of settings.
    #
    #   \param kwargs Keyword arguments.
    #                 Valid values are:
    #                 - settings: The settings to use for the slice. The default is the active machine.
    #                 - save_gcode: True if the generated gcode should be saved, False if not. True by default.
    #                 - save_polygons: True if the generated polygon data should be saved, False if not. True by default.
    #                 - force_restart: True if the slicing process should be forcefully restarted if it is already slicing.
    #                                  If False, this method will do nothing when already slicing. True by default.
    #                 - report_progress: True if the slicing progress should be reported, False if not. Default is True.
    def slice(self, **kwargs):
        if not self._enabled:
            return

        if self._slicing:
            if not kwargs.get("force_restart", True):
                return

            self._slicing = False
            self._restart = True
            if self._process is not None:
                Logger.log("d", "Killing engine process")
                try:
                    self._process.terminate()
                except: # terminating a process that is already terminating causes an exception, silently ignore this.
                    pass
            self.slicingCancelled.emit()
            return
        Logger.log("d", "Preparing to send slice data to engine.")
        object_groups = []
        if self._profile.getSettingValue("print_sequence") == "one_at_a_time":
            for node in OneAtATimeIterator(self._scene.getRoot()):
                temp_list = []
                children = node.getAllChildren()
                children.append(node)
                for child_node in children:
                    if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
                        temp_list.append(child_node)
                object_groups.append(temp_list)
        else:
            temp_list = []
            for node in DepthFirstIterator(self._scene.getRoot()):
                if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
                    if not getattr(node, "_outside_buildarea", False):
                        temp_list.append(node)
            if len(temp_list) == 0:
                self.processingProgress.emit(0.0)
                return
            object_groups.append(temp_list)
        #for node in DepthFirstIterator(self._scene.getRoot()):
        #    if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
        #        if not getattr(node, "_outside_buildarea", False):
        #            objects.append(node)

        if len(object_groups) == 0:
            if self._message:
                self._message.hide()
                self._message = None
            return #No point in slicing an empty build plate

        if kwargs.get("profile", self._profile).hasErrorValue():
            Logger.log('w', "Profile has error values. Aborting slicing")
            if self._message:
                self._message.hide()
                self._message = None
            self._message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
            self._message.show()
            return #No slicing if we have error values since those are by definition illegal values.
        # Remove existing layer data (if any)
        for node in DepthFirstIterator(self._scene.getRoot()):
            if type(node) is SceneNode and node.getMeshData():
                if node.callDecoration("getLayerData"):
                    Application.getInstance().getController().getScene().getRoot().removeChild(node)
                    break
        Application.getInstance().getController().getScene().gcode_list = None
        self._slicing = True
        self.slicingStarted.emit()

        self._report_progress = kwargs.get("report_progress", True)
        if self._report_progress:
            self.processingProgress.emit(0.0)
            if not self._message:
                self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
                self._message.show()
            else:
                self._message.setProgress(-1)

        self._sendSettings(kwargs.get("profile", self._profile))

        self._scene.acquireLock()

        # Set the gcode as an empty list. This will be filled with strings by GCodeLayer messages.
        # This is done so the gcode can be fragmented in memory and does not need a continues memory space.
        # (AKA. This prevents MemoryErrors)
        self._save_gcode = kwargs.get("save_gcode", True)
        if self._save_gcode:
            setattr(self._scene, "gcode_list", [])

        self._save_polygons = kwargs.get("save_polygons", True)

        slice_message = Cura_pb2.Slice()

        for group in object_groups:
            group_message = slice_message.object_lists.add()
            for object in group:
                mesh_data = object.getMeshData().getTransformed(object.getWorldTransformation())

                obj = group_message.objects.add()
                obj.id = id(object)
                
                verts = numpy.array(mesh_data.getVertices())
                verts[:,[1,2]] = verts[:,[2,1]]
                verts[:,1] *= -1
                obj.vertices = verts.tostring()

                self._handlePerObjectSettings(object, obj)

            # Hack to add per-object settings also to the "MeshGroup" in CuraEngine
            # We really should come up with a better solution for this.
            self._handlePerObjectSettings(group[0], group_message)

        self._scene.releaseLock()
        Logger.log("d", "Sending data to engine for slicing.")
        self._socket.sendMessage(slice_message)

    def _onSceneChanged(self, source):
        if (type(source) is not SceneNode) or (source is self._scene.getRoot()) or (source.getMeshData() is None):
            return

        if(source.getMeshData().getVertices() is None):
            return

        self._onChanged()

    def _onActiveProfileChanged(self):
        if self._profile:
            self._profile.settingValueChanged.disconnect(self._onSettingChanged)

        self._profile = Application.getInstance().getMachineManager().getActiveProfile()
        if self._profile:
            self._profile.settingValueChanged.connect(self._onSettingChanged)
            self._onChanged()

    def _onSettingChanged(self, setting):
        self._onChanged()

    def _onSlicedObjectListMessage(self, message):
        if self._save_polygons:
            if self._layer_view_active:
                job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(message)
                job.start()
            else :
                self._stored_layer_data = message

    def _onProgressMessage(self, message):
        if message.amount >= 0.99:
            self._slicing = False

            if self._message:
                self._message.setProgress(100)
                self._message.hide()
                self._message = None

        if self._message:
            self._message.setProgress(round(message.amount * 100))

        if self._report_progress:
            self.processingProgress.emit(message.amount)

    def _onGCodeLayerMessage(self, message):
        if self._save_gcode:
            job = ProcessGCodeJob.ProcessGCodeLayerJob(message)
            job.start()

    def _onGCodePrefixMessage(self, message):
        if self._save_gcode:
            self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))

    def _onObjectPrintTimeMessage(self, message):
        self.printDurationMessage.emit(message.time, message.material_amount)
        self.processingProgress.emit(1.0)

    def _createSocket(self):
        super()._createSocket()
        
        self._socket.registerMessageType(1, Cura_pb2.Slice)
        self._socket.registerMessageType(2, Cura_pb2.SlicedObjectList)
        self._socket.registerMessageType(3, Cura_pb2.Progress)
        self._socket.registerMessageType(4, Cura_pb2.GCodeLayer)
        self._socket.registerMessageType(5, Cura_pb2.ObjectPrintTime)
        self._socket.registerMessageType(6, Cura_pb2.SettingList)
        self._socket.registerMessageType(7, Cura_pb2.GCodePrefix)

    ##  Manually triggers a reslice
    def forceSlice(self):
        self._change_timer.start()

    def _onChanged(self):
        if not self._profile:
            return

        self._change_timer.start()

    def _sendSettings(self, profile):
        msg = Cura_pb2.SettingList()
        for key, value in profile.getAllSettingValues(include_machine = True).items():
            s = msg.settings.add()
            s.name = key
            s.value = str(value).encode("utf-8")

        self._socket.sendMessage(msg)

    def _onBackendConnected(self):
        if self._restart:
            self._onChanged()
            self._restart = False

    def _onToolOperationStarted(self, tool):
        self._enabled = False # Do not reslice when a tool is doing it's 'thing'

    def _onToolOperationStopped(self, tool):
        self._enabled = True # Tool stop, start listening for changes again.
        self._onChanged()

    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "LayerView":
                self._layer_view_active = True
                if self._stored_layer_data:
                    job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(self._stored_layer_data)
                    job.start()
            else:
                self._layer_view_active = False

    def _handlePerObjectSettings(self, node, message):
        profile = node.callDecoration("getProfile")
        if profile:
            for key, value in profile.getChangedSettingValues().items():
                setting = message.settings.add()
                setting.name = key
                setting.value = str(value).encode()

        object_settings = node.callDecoration("getAllSettingValues")
        if not object_settings:
            return

        for key, value in object_settings.items():
            setting = message.settings.add()
            setting.name = key
            setting.value = str(value).encode()
コード例 #26
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
                                 lifetime = 0,
                                 dismissable=False,
                                 progress = 0,
                                 title = i18n_catalog.i18nc("@info:title", "Finding Location"))
        status_message.show()


        # Collect nodes to be placed
        nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
        for node in self._nodes:
            offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
            nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))

        # Sort the nodes with the biggest area first.
        nodes_arr.sort(key=lambda item: item[0])
        nodes_arr.reverse()

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        machine_width = global_container_stack.getProperty("machine_width", "value")
        machine_depth = global_container_stack.getProperty("machine_depth", "value")

        x, y = machine_width, machine_depth

        arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
        arrange_array.add()

        # Place nodes one at a time
        start_priority = 0
        grouped_operation = GroupedOperation()
        found_solution_for_all = True
        left_over_nodes = []  # nodes that do not fit on an empty build plate

        for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
            # For performance reasons, we assume that when a location does not fit,
            # it will also not fit for the next object (while what can be untrue).

            try_placement = True

            current_build_plate_number = 0  # always start with the first one

            while try_placement:
                # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
                while current_build_plate_number >= arrange_array.count():
                    arrange_array.add()
                arranger = arrange_array.get(current_build_plate_number)

                best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority)
                x, y = best_spot.x, best_spot.y
                node.removeDecorator(ZOffsetDecorator)
                if node.getBoundingBox():
                    center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
                else:
                    center_y = 0
                if x is not None:  # We could find a place
                    arranger.place(x, y, offset_shape_arr)  # place the object in the arranger

                    node.callDecoration("setBuildPlateNumber", current_build_plate_number)
                    grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
                    try_placement = False
                else:
                    # very naive, because we skip to the next build plate if one model doesn't fit.
                    if arranger.isEmpty:
                        # apparently we can never place this object
                        left_over_nodes.append(node)
                        try_placement = False
                    else:
                        # try next build plate
                        current_build_plate_number += 1
                        try_placement = True

            status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
            Job.yieldThread()

        for node in left_over_nodes:
            node.callDecoration("setBuildPlateNumber", -1)  # these are not on any build plate
            found_solution_for_all = False

        grouped_operation.push()

        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
                                               title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
            no_full_solution_message.show()
コード例 #27
0
ファイル: RotateTool.py プロジェクト: dakshsingh/Uranium
class RotateTool(Tool):
    def __init__(self):
        super().__init__()
        self._handle = RotateToolHandle.RotateToolHandle()

        self._snap_rotation = True
        self._snap_angle = math.radians(15)

        self._angle = None
        self._angle_update_time = None

        self._progress_message = None
        self._iterations = 0
        self._total_iterations = 0

        self.setExposedProperties("Rotation", "RotationSnap", "RotationSnapAngle")

    def event(self, event):
        super().event(event)

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.MousePressEvent:
            if MouseEvent.LeftButton not in event.buttons:
                return False

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

            if ToolHandle.isAxis(id):
                self.setLockedAxis(id)
                handle_position = self._handle.getWorldPosition()

                if id == ToolHandle.XAxis:
                    self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x))
                elif id == ToolHandle.YAxis:
                    self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))
                elif self._locked_axis == ToolHandle.ZAxis:
                    self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z))

                self.setDragStart(event.x, event.y)
                self._angle = 0
                self.operationStarted.emit(self)

        if event.type == Event.MouseMoveEvent:
            if not self.getDragPlane():
                return False

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

            handle_position = self._handle.getWorldPosition()

            drag_start = (self.getDragStart() - handle_position).normalize()
            drag_position = self.getDragPosition(event.x, event.y)
            if not drag_position:
                return
            drag_end = (drag_position - handle_position).normalize()

            try:
                angle = math.acos(drag_start.dot(drag_end))
            except ValueError:
                angle = 0

            if self._snap_rotation:
                angle = int(angle / self._snap_angle) * self._snap_angle
                if angle == 0:
                    return

            rotation = None
            if self.getLockedAxis() == ToolHandle.XAxis:
                direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X)
            elif self.getLockedAxis() == ToolHandle.YAxis:
                direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y)
            elif self.getLockedAxis() == ToolHandle.ZAxis:
                direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z)

            self._angle += direction * angle

            # 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._angle_update_time or new_time - self._angle_update_time > 0.01:
                self.propertyChanged.emit()
                self._angle_update_time = new_time

            Selection.applyOperation(RotateOperation, rotation)

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

        if event.type == Event.MouseReleaseEvent:
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(None)
                self._angle = None
                self.propertyChanged.emit()
                self.operationStopped.emit(self)
                return True

    def getRotation(self):
        return round(math.degrees(self._angle)) if self._angle else None

    def getRotationSnap(self):
        return self._snap_rotation

    def setRotationSnap(self, snap):
        if snap != self._snap_rotation:
            self._snap_rotation = snap
            self.propertyChanged.emit()

    def getRotationSnapAngle(self):
        return self._snap_angle

    def setRotationSnapAngle(self, angle):
        if angle != self._snap_angle:
            self._snap_angle = angle
            self.propertyChanged.emit()

    def resetRotation(self):
        Selection.applyOperation(SetTransformOperation, None, Quaternion(), None)

    def layFlat(self):
        self.operationStarted.emit(self)
        self._progress_message = Message("Laying object flat on buildplate...", lifetime = 0, dismissable = False)
        self._progress_message.setProgress(0)

        self._iterations = 0
        self._total_iterations = 0
        for selected_object in Selection.getAllSelectedObjects():
            self._total_iterations += len(selected_object.getMeshDataTransformed().getVertices()) * 2

        self._progress_message.show()

        operations = Selection.applyOperation(LayFlatOperation)
        for op in operations:
            op.progress.connect(self._layFlatProgress)

        job = LayFlatJob(operations)
        job.finished.connect(self._layFlatFinished)
        job.start()

    def _layFlatProgress(self, iterations):
        self._iterations += iterations
        self._progress_message.setProgress(100 * self._iterations / self._total_iterations)

    def _layFlatFinished(self, job):
        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self.operationStopped.emit(self)
コード例 #28
0
ファイル: ProcessSlicedLayersJob.py プロジェクト: alonf/Cura
class ProcessSlicedLayersJob(Job):
    def __init__(self, layers):
        super().__init__()
        self._layers = layers
        self._scene = Application.getInstance().getController().getScene()
        self._progress = None
        self._abort_requested = False

    ##  Aborts the processing of layers.
    #
    #   This abort is made on a best-effort basis, meaning that the actual
    #   job thread will check once in a while to see whether an abort is
    #   requested and then stop processing by itself. There is no guarantee
    #   that the abort will stop the job any time soon or even at all.
    def abort(self):
        self._abort_requested = True

    def run(self):
        if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
            self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
            self._progress.show()
            Job.yieldThread()
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)

        new_node = SceneNode()

        ## Remove old layer data (if any)
        for node in DepthFirstIterator(self._scene.getRoot()):
            if type(node) is SceneNode and node.getMeshData():
                if node.callDecoration("getLayerData"):
                    self._scene.getRoot().removeChild(node)
            Job.yieldThread()
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        settings = Application.getInstance().getMachineManager().getWorkingProfile()

        mesh = MeshData()
        layer_data = LayerData.LayerData()
        layer_count = len(self._layers)

        # Find the minimum layer number
        # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
        # instead simply offset all other layers so the lowest layer is always 0.
        min_layer_number = 0
        for layer in self._layers:
            if(layer.id < min_layer_number):
                min_layer_number = layer.id

        current_layer = 0

        for layer in self._layers:
            abs_layer_number = layer.id + abs(min_layer_number)

            layer_data.addLayer(abs_layer_number)
            layer_data.setLayerHeight(abs_layer_number, layer.height)
            layer_data.setLayerThickness(abs_layer_number, layer.thickness)

            for p in range(layer.repeatedMessageCount("polygons")):
                polygon = layer.getRepeatedMessage("polygons", p)

                points = numpy.fromstring(polygon.points, dtype="i8")  # Convert bytearray to numpy array
                points = points.reshape((-1,2))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                # Create a new 3D-array, copy the 2D points over and insert the right height.
                # This uses manual array creation + copy rather than numpy.insert since this is
                # faster.
                new_points = numpy.empty((len(points), 3), numpy.float32)
                new_points[:,0] = points[:,0]
                new_points[:,1] = layer.height
                new_points[:,2] = -points[:,1]

                new_points /= 1000

                layer_data.addPolygon(abs_layer_number, polygon.type, new_points, polygon.line_width)
                Job.yieldThread()
            Job.yieldThread()
            current_layer += 1
            progress = (current_layer / layer_count) * 100
            # TODO: Rebuild the layer data mesh once the layer has been processed.
            # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.

            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return
            if self._progress:
                self._progress.setProgress(progress)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data
        layer_data.build()

        if self._abort_requested:
            if self._progress:
                self._progress.hide()
            return

        # Add LayerDataDecorator to scene node to indicate that the node has layer data
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_data)
        new_node.addDecorator(decorator)

        new_node.setMeshData(mesh)
        new_node.setParent(self._scene.getRoot())  # Note: After this we can no longer abort!

        if not settings.getSettingValue("machine_center_is_zero"):
            new_node.setPosition(Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2))

        if self._progress:
            self._progress.setProgress(100)

        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "LayerView":
            view.resetLayerData()

        if self._progress:
            self._progress.hide()

        # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
        self._layers = None

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
                if not self._progress:
                    self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
                if self._progress.getProgress() != 100:
                    self._progress.show()
            else:
                if self._progress:
                    self._progress.hide()
コード例 #29
0
ファイル: LayerView.py プロジェクト: daid/Cura
class LayerView(View):
    # Must match LayerView.qml
    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
    LAYER_VIEW_TYPE_LINE_TYPE = 1

    def __init__(self):
        super().__init__()

        self._max_layers = 0
        self._current_layer_num = 0
        self._minimum_layer_num = 0
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._top_layers_job = None
        self._activity = False
        self._old_max_layers = 0

        self._busy = False

        self._ghost_shader = None
        self._layer_pass = None
        self._composite_pass = None
        self._old_layer_bindings = None
        self._layerview_composite_shader = None
        self._old_composite_shader = None

        self._global_container_stack = None
        self._proxy = LayerViewProxy.LayerViewProxy()
        self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)

        self._resetSettings()
        self._legend_items = None
        self._show_travel_moves = False

        Preferences.getInstance().addPreference("view/top_layer_count", 5)
        Preferences.getInstance().addPreference("view/only_show_top_layers", False)
        Preferences.getInstance().addPreference("view/force_layer_view_compatibility_mode", False)

        Preferences.getInstance().addPreference("layerview/layer_view_type", 0)
        Preferences.getInstance().addPreference("layerview/extruder_opacities", "")

        Preferences.getInstance().addPreference("layerview/show_travel_moves", False)
        Preferences.getInstance().addPreference("layerview/show_helpers", True)
        Preferences.getInstance().addPreference("layerview/show_skin", True)
        Preferences.getInstance().addPreference("layerview/show_infill", True)

        Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
        self._updateWithPreferences()

        self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
        self._compatibility_mode = True  # for safety

        self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"))

    def _resetSettings(self):
        self._layer_view_type = 0  # 0 is material color, 1 is color by linetype, 2 is speed
        self._extruder_count = 0
        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
        self._show_travel_moves = 0
        self._show_helpers = 1
        self._show_skin = 1
        self._show_infill = 1

    def getActivity(self):
        return self._activity

    def getLayerPass(self):
        if not self._layer_pass:
            # Currently the RenderPass constructor requires a size > 0
            # This should be fixed in RenderPass's constructor.
            self._layer_pass = LayerPass.LayerPass(1, 1)
            self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
            self._layer_pass.setLayerView(self)
            self.getRenderer().addRenderPass(self._layer_pass)
        return self._layer_pass

    def getCurrentLayer(self):
        return self._current_layer_num

    def getMinimumLayer(self):
        return self._minimum_layer_num

    def _onSceneChanged(self, node):
        self.calculateMaxLayers()

    def getMaxLayers(self):
        return self._max_layers

    busyChanged = Signal()

    def isBusy(self):
        return self._busy

    def setBusy(self, busy):
        if busy != self._busy:
            self._busy = busy
            self.busyChanged.emit()

    def resetLayerData(self):
        self._current_layer_mesh = None
        self._current_layer_jumps = None

    def beginRendering(self):
        scene = self.getController().getScene()
        renderer = self.getRenderer()

        if not self._ghost_shader:
            self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
            self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb()))

        for node in DepthFirstIterator(scene.getRoot()):
            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
            # However, it is somewhat relevant when the node is selected, so do render it then.
            if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
                continue

            if not node.render(renderer):
                if (node.getMeshData()) and node.isVisible():
                    renderer.queueNode(node, transparent = True, shader = self._ghost_shader)

    def setLayer(self, value):
        if self._current_layer_num != value:
            self._current_layer_num = value
            if self._current_layer_num < 0:
                self._current_layer_num = 0
            if self._current_layer_num > self._max_layers:
                self._current_layer_num = self._max_layers
            if self._current_layer_num < self._minimum_layer_num:
                self._minimum_layer_num = self._current_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setMinimumLayer(self, value):
        if self._minimum_layer_num != value:
            self._minimum_layer_num = value
            if self._minimum_layer_num < 0:
                self._minimum_layer_num = 0
            if self._minimum_layer_num > self._max_layers:
                self._minimum_layer_num = self._max_layers
            if self._minimum_layer_num > self._current_layer_num:
                self._current_layer_num = self._minimum_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    ##  Set the layer view type
    #
    #   \param layer_view_type integer as in LayerView.qml and this class
    def setLayerViewType(self, layer_view_type):
        self._layer_view_type = layer_view_type
        self.currentLayerNumChanged.emit()

    ##  Return the layer view type, integer as in LayerView.qml and this class
    def getLayerViewType(self):
        return self._layer_view_type

    ##  Set the extruder opacity
    #
    #   \param extruder_nr 0..3
    #   \param opacity 0.0 .. 1.0
    def setExtruderOpacity(self, extruder_nr, opacity):
        if 0 <= extruder_nr <= 3:
            self._extruder_opacity[extruder_nr] = opacity
            self.currentLayerNumChanged.emit()

    def getExtruderOpacities(self):
        return self._extruder_opacity

    def setShowTravelMoves(self, show):
        self._show_travel_moves = show
        self.currentLayerNumChanged.emit()

    def getShowTravelMoves(self):
        return self._show_travel_moves

    def setShowHelpers(self, show):
        self._show_helpers = show
        self.currentLayerNumChanged.emit()

    def getShowHelpers(self):
        return self._show_helpers

    def setShowSkin(self, show):
        self._show_skin = show
        self.currentLayerNumChanged.emit()

    def getShowSkin(self):
        return self._show_skin

    def setShowInfill(self, show):
        self._show_infill = show
        self.currentLayerNumChanged.emit()

    def getShowInfill(self):
        return self._show_infill

    def getCompatibilityMode(self):
        return self._compatibility_mode

    def getExtruderCount(self):
        return self._extruder_count

    def calculateMaxLayers(self):
        scene = self.getController().getScene()
        self._activity = True

        self._old_max_layers = self._max_layers
        ## Recalculate num max layers
        new_max_layers = 0
        for node in DepthFirstIterator(scene.getRoot()):
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            min_layer_number = sys.maxsize
            max_layer_number = -sys.maxsize
            for layer_id in layer_data.getLayers():
                if max_layer_number < layer_id:
                    max_layer_number = layer_id
                if min_layer_number > layer_id:
                    min_layer_number = layer_id
            layer_count = max_layer_number - min_layer_number

            if new_max_layers < layer_count:
                new_max_layers = layer_count

        if new_max_layers > 0 and new_max_layers != self._old_max_layers:
            self._max_layers = new_max_layers

            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
            # if it's the largest value. If we don't do this, we can have a slider block outside of the
            # slider.
            if new_max_layers > self._current_layer_num:
                self.maxLayersChanged.emit()
                self.setLayer(int(self._max_layers))
            else:
                self.setLayer(int(self._max_layers))
                self.maxLayersChanged.emit()
        self._startUpdateTopLayers()

    maxLayersChanged = Signal()
    currentLayerNumChanged = Signal()
    globalStackChanged = Signal()
    preferencesChanged = Signal()

    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
    #   as this caused some issues.
    def getProxy(self, engine, script_engine):
        return self._proxy

    def endRendering(self):
        pass

    def event(self, event):
        modifiers = QApplication.keyboardModifiers()
        ctrl_is_active = modifiers & Qt.ControlModifier
        shift_is_active = modifiers & Qt.ShiftModifier
        if event.type == Event.KeyPressEvent and ctrl_is_active:
            amount = 10 if shift_is_active else 1
            if event.key == KeyEvent.UpKey:
                self.setLayer(self._current_layer_num + amount)
                return True
            if event.key == KeyEvent.DownKey:
                self.setLayer(self._current_layer_num - amount)
                return True

        if event.type == Event.ViewActivateEvent:
            # Make sure the LayerPass is created
            self.getLayerPass()

            Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
            self._onGlobalStackChanged()

            if not self._layerview_composite_shader:
                self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader"))
                theme = Application.getInstance().getTheme()
                self._layerview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
                self._layerview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))

            if not self._composite_pass:
                self._composite_pass = self.getRenderer().getRenderPass("composite")

            self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
            self._composite_pass.getLayerBindings().append("layerview")
            self._old_composite_shader = self._composite_pass.getCompositeShader()
            self._composite_pass.setCompositeShader(self._layerview_composite_shader)

        elif event.type == Event.ViewDeactivateEvent:
            self._wireprint_warning_message.hide()
            Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
            if self._global_container_stack:
                self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)

            self._composite_pass.setLayerBindings(self._old_layer_bindings)
            self._composite_pass.setCompositeShader(self._old_composite_shader)

    def getCurrentLayerMesh(self):
        return self._current_layer_mesh

    def getCurrentLayerJumps(self):
        return self._current_layer_jumps

    def _onGlobalStackChanged(self):
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
            self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
            self._onPropertyChanged("wireframe_enabled", "value")
            self.globalStackChanged.emit()
        else:
            self._wireprint_warning_message.hide()

    def _onPropertyChanged(self, key, property_name):
        if key == "wireframe_enabled" and property_name == "value":
            if self._global_container_stack.getProperty("wireframe_enabled", "value"):
                self._wireprint_warning_message.show()
            else:
                self._wireprint_warning_message.hide()

    def _startUpdateTopLayers(self):
        if not self._compatibility_mode:
            return

        if self._top_layers_job:
            self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
            self._top_layers_job.cancel()

        self.setBusy(True)

        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
        self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
        self._top_layers_job.start()

    def _updateCurrentLayerMesh(self, job):
        self.setBusy(False)

        if not job.getResult():
            return
        self.resetLayerData()  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
        self._current_layer_mesh = job.getResult().get("layers")
        if self._show_travel_moves:
            self._current_layer_jumps = job.getResult().get("jumps")
        self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())

        self._top_layers_job = None

    def _updateWithPreferences(self):
        self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
        self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
            Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))

        self.setLayerViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));

        for extruder_nr, extruder_opacity in enumerate(Preferences.getInstance().getValue("layerview/extruder_opacities").split("|")):
            try:
                opacity = float(extruder_opacity)
            except ValueError:
                opacity = 1.0
            self.setExtruderOpacity(extruder_nr, opacity)

        self.setShowTravelMoves(bool(Preferences.getInstance().getValue("layerview/show_travel_moves")))
        self.setShowHelpers(bool(Preferences.getInstance().getValue("layerview/show_helpers")))
        self.setShowSkin(bool(Preferences.getInstance().getValue("layerview/show_skin")))
        self.setShowInfill(bool(Preferences.getInstance().getValue("layerview/show_infill")))

        self._startUpdateTopLayers()
        self.preferencesChanged.emit()

    def _onPreferencesChanged(self, preference):
        if preference not in {
            "view/top_layer_count",
            "view/only_show_top_layers",
            "view/force_layer_view_compatibility_mode",
            "layerview/layer_view_type",
            "layerview/extruder_opacities",
            "layerview/show_travel_moves",
            "layerview/show_helpers",
            "layerview/show_skin",
            "layerview/show_infill",
            }:
            return

        self._updateWithPreferences()
コード例 #30
0
class SolidView(View):
    """Standard view for mesh models."""

    _show_xray_warning_preference = "view/show_xray_warning"

    def __init__(self):
        super().__init__()
        application = Application.getInstance()
        application.getPreferences().addPreference("view/show_overhang", True)
        application.globalContainerStackChanged.connect(
            self._onGlobalContainerChanged)
        self._enabled_shader = None
        self._disabled_shader = None
        self._non_printing_shader = None
        self._support_mesh_shader = None

        self._xray_shader = None
        self._xray_pass = None
        self._xray_composite_shader = None
        self._composite_pass = None

        self._extruders_model = None
        self._theme = None
        self._support_angle = 90

        self._global_stack = None

        self._old_composite_shader = None
        self._old_layer_bindings = None

        self._next_xray_checking_time = time.time()
        self._xray_checking_update_time = 30.0  # seconds
        self._xray_warning_cooldown = 60 * 10  # reshow Model error message every 10 minutes
        self._xray_warning_message = Message(
            catalog.i18nc(
                "@info:status",
                "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."
            ),
            lifetime=60 * 5,  # leave message for 5 minutes
            title=catalog.i18nc("@info:title", "Model errors"),
            option_text=catalog.i18nc("@info:option_text",
                                      "Do not show this message again"),
            option_state=False)
        self._xray_warning_message.optionToggled.connect(
            self._onDontAskMeAgain)
        application.getPreferences().addPreference(
            self._show_xray_warning_preference, True)

        application.engineCreatedSignal.connect(self._onGlobalContainerChanged)

    def _onDontAskMeAgain(self, checked: bool) -> None:
        Application.getInstance().getPreferences().setValue(
            self._show_xray_warning_preference, not checked)

    def _onGlobalContainerChanged(self) -> None:
        if self._global_stack:
            try:
                self._global_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)
            except TypeError:
                pass
            for extruder_stack in ExtruderManager.getInstance(
            ).getActiveExtruderStacks():
                extruder_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)

        self._global_stack = Application.getInstance().getGlobalContainerStack(
        )
        if self._global_stack:
            self._global_stack.propertyChanged.connect(self._onPropertyChanged)
            for extruder_stack in ExtruderManager.getInstance(
            ).getActiveExtruderStacks():
                extruder_stack.propertyChanged.connect(self._onPropertyChanged)
            self._onPropertyChanged("support_angle",
                                    "value")  # Force an re-evaluation

    def _onPropertyChanged(self, key: str, property_name: str) -> None:
        if key != "support_angle" or property_name != "value":
            return
        # As the rendering is called a *lot* we really, dont want to re-evaluate the property every time. So we store em!
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            support_extruder_nr = int(
                global_container_stack.getExtruderPositionValueWithDefault(
                    "support_extruder_nr"))
            try:
                support_angle_stack = global_container_stack.extruderList[
                    support_extruder_nr]
            except IndexError:
                pass
            else:
                angle = support_angle_stack.getProperty(
                    "support_angle", "value")
                if angle is not None:
                    self._support_angle = angle

    def _checkSetup(self):
        if not self._extruders_model:
            self._extruders_model = Application.getInstance(
            ).getExtrudersModel()

        if not self._theme:
            self._theme = Application.getInstance().getTheme()

        if not self._enabled_shader:
            self._enabled_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "overhang.shader"))
            self._enabled_shader.setUniformValue(
                "u_overhangColor",
                Color(*self._theme.getColor("model_overhang").getRgb()))
            self._enabled_shader.setUniformValue("u_renderError", 0.0)

        if not self._disabled_shader:
            self._disabled_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "striped.shader"))
            self._disabled_shader.setUniformValue(
                "u_diffuseColor1",
                Color(*self._theme.getColor("model_unslicable").getRgb()))
            self._disabled_shader.setUniformValue(
                "u_diffuseColor2",
                Color(*self._theme.getColor("model_unslicable_alt").getRgb()))
            self._disabled_shader.setUniformValue("u_width", 50.0)

        if not self._non_printing_shader:
            self._non_printing_shader = OpenGL.getInstance(
            ).createShaderProgram(
                Resources.getPath(Resources.Shaders,
                                  "transparent_object.shader"))
            self._non_printing_shader.setUniformValue(
                "u_diffuseColor",
                Color(*self._theme.getColor("model_non_printing").getRgb()))
            self._non_printing_shader.setUniformValue("u_opacity", 0.1)

        if not self._support_mesh_shader:
            self._support_mesh_shader = OpenGL.getInstance(
            ).createShaderProgram(
                Resources.getPath(Resources.Shaders, "striped.shader"))
            self._support_mesh_shader.setUniformValue("u_vertical_stripes",
                                                      True)
            self._support_mesh_shader.setUniformValue("u_width", 5.0)

        if not Application.getInstance().getPreferences().getValue(
                self._show_xray_warning_preference):
            self._xray_shader = None
            self._xray_composite_shader = None
            if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(
            ):
                self._composite_pass.setLayerBindings(self._old_layer_bindings)
                self._composite_pass.setCompositeShader(
                    self._old_composite_shader)
                self._old_layer_bindings = None
                self._old_composite_shader = None
                self._enabled_shader.setUniformValue(
                    "u_renderError", 0.0)  # We don't want any error markers!.
                self._xray_warning_message.hide()
        else:
            if not self._xray_shader:
                self._xray_shader = OpenGL.getInstance().createShaderProgram(
                    Resources.getPath(Resources.Shaders, "xray.shader"))

            if not self._xray_composite_shader:
                self._xray_composite_shader = OpenGL.getInstance(
                ).createShaderProgram(
                    Resources.getPath(Resources.Shaders,
                                      "xray_composite.shader"))
                theme = Application.getInstance().getTheme()
                self._xray_composite_shader.setUniformValue(
                    "u_background_color",
                    Color(*theme.getColor("viewport_background").getRgb()))
                self._xray_composite_shader.setUniformValue(
                    "u_outline_color",
                    Color(*theme.getColor("model_selection_outline").getRgb()))
                self._xray_composite_shader.setUniformValue(
                    "u_flat_error_color_mix",
                    0.)  # Don't show flat error color in solid-view.

            renderer = self.getRenderer()
            if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings(
            ):
                # Currently the RenderPass constructor requires a size > 0
                # This should be fixed in RenderPass's constructor.
                self._xray_pass = XRayPass.XRayPass(1, 1)
                self._enabled_shader.setUniformValue(
                    "u_renderError", 1.0)  # We don't want any error markers!.
                renderer.addRenderPass(self._xray_pass)

                if not self._composite_pass:
                    self._composite_pass = self.getRenderer().getRenderPass(
                        "composite")

                self._old_layer_bindings = self._composite_pass.getLayerBindings(
                )
                self._composite_pass.setLayerBindings(
                    ["default", "selection", "xray"])
                self._old_composite_shader = self._composite_pass.getCompositeShader(
                )
                self._composite_pass.setCompositeShader(
                    self._xray_composite_shader)

    def beginRendering(self):
        scene = self.getController().getScene()
        renderer = self.getRenderer()

        self._checkSetup()

        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if Application.getInstance().getPreferences().getValue(
                    "view/show_overhang"):
                # Make sure the overhang angle is valid before passing it to the shader
                if self._support_angle >= 0 and self._support_angle <= 90:
                    self._enabled_shader.setUniformValue(
                        "u_overhangAngle",
                        math.cos(math.radians(90 - self._support_angle)))
                else:
                    self._enabled_shader.setUniformValue(
                        "u_overhangAngle", math.cos(math.radians(0))
                    )  #Overhang angle of 0 causes no area at all to be marked as overhang.
            else:
                self._enabled_shader.setUniformValue("u_overhangAngle",
                                                     math.cos(math.radians(0)))
        disabled_batch = renderer.createRenderBatch(
            shader=self._disabled_shader)
        normal_object_batch = renderer.createRenderBatch(
            shader=self._enabled_shader)
        renderer.addRenderBatch(disabled_batch)
        renderer.addRenderBatch(normal_object_batch)
        for node in DepthFirstIterator(scene.getRoot()):
            if node.render(renderer):
                continue

            if node.getMeshData() and node.isVisible():
                uniforms = {}
                shade_factor = 1.0

                per_mesh_stack = node.callDecoration("getStack")

                extruder_index = node.callDecoration(
                    "getActiveExtruderPosition")
                if extruder_index is None:
                    extruder_index = "0"
                extruder_index = int(extruder_index)

                try:
                    material_color = self._extruders_model.getItem(
                        extruder_index)["color"]
                except KeyError:
                    material_color = self._extruders_model.defaultColors[0]

                if extruder_index != ExtruderManager.getInstance(
                ).activeExtruderIndex:
                    # Shade objects that are printed with the non-active extruder 25% darker
                    shade_factor = 0.6

                try:
                    # Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
                    # an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
                    uniforms["diffuse_color"] = [
                        shade_factor * int(material_color[1:3], 16) / 255,
                        shade_factor * int(material_color[3:5], 16) / 255,
                        shade_factor * int(material_color[5:7], 16) / 255, 1.0
                    ]

                    # Color the currently selected face-id. (Disable for now.)
                    #face = Selection.getHoverFace()
                    uniforms[
                        "hover_face"] = -1  #if not face or node != face[0] else face[1]
                except ValueError:
                    pass

                if node.callDecoration("isNonPrintingMesh"):
                    if per_mesh_stack and (
                            node.callDecoration("isInfillMesh")
                            or node.callDecoration("isCuttingMesh")):
                        renderer.queueNode(node,
                                           shader=self._non_printing_shader,
                                           uniforms=uniforms,
                                           transparent=True)
                    else:
                        renderer.queueNode(node,
                                           shader=self._non_printing_shader,
                                           transparent=True)
                elif getattr(node, "_outside_buildarea", False):
                    disabled_batch.addItem(
                        node.getWorldTransformation(copy=False),
                        node.getMeshData(),
                        normal_transformation=node.getCachedNormalMatrix())
                elif per_mesh_stack and node.callDecoration("isSupportMesh"):
                    # Render support meshes with a vertical stripe that is darker
                    shade_factor = 0.6
                    uniforms["diffuse_color_2"] = [
                        uniforms["diffuse_color"][0] * shade_factor,
                        uniforms["diffuse_color"][1] * shade_factor,
                        uniforms["diffuse_color"][2] * shade_factor, 1.0
                    ]
                    renderer.queueNode(node,
                                       shader=self._support_mesh_shader,
                                       uniforms=uniforms)
                else:
                    normal_object_batch.addItem(
                        node.getWorldTransformation(copy=False),
                        node.getMeshData(),
                        uniforms=uniforms,
                        normal_transformation=node.getCachedNormalMatrix())
            if node.callDecoration("isGroup") and Selection.isSelected(node):
                renderer.queueNode(scene.getRoot(),
                                   mesh=node.getBoundingBoxMesh(),
                                   mode=RenderBatch.RenderMode.LineLoop)

    def endRendering(self):
        # check whether the xray overlay is showing badness
        if time.time() > self._next_xray_checking_time\
                and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
            self._next_xray_checking_time = time.time(
            ) + self._xray_checking_update_time

            xray_img = self._xray_pass.getOutput()
            xray_img = xray_img.convertToFormat(QImage.Format_RGB888)

            # We can't just read the image since the pixels are aligned to internal memory positions.
            # xray_img.byteCount() != xray_img.width() * xray_img.height() * 3
            # The byte count is a little higher sometimes. We need to check the data per line, but fast using Numpy.
            # See https://stackoverflow.com/questions/5810970/get-raw-data-from-qimage for a description of the problem.
            # We can't use that solution though, since it doesn't perform well in Python.
            class QImageArrayView:
                """
                Class that ducktypes to be a Numpy ndarray.
                """
                def __init__(self, qimage):
                    bits_pointer = qimage.bits()
                    if bits_pointer is None:  # If this happens before there is a window.
                        self.__array_interface__ = {
                            "shape": (0, 0),
                            "typestr": "|u4",
                            "data": (0, False),
                            "strides": (1, 3),
                            "version": 3
                        }
                    else:
                        self.__array_interface__ = {
                            "shape": (qimage.height(), qimage.width()),
                            "typestr":
                            "|u4",  # Use 4 bytes per pixel rather than 3, since Numpy doesn't support 3.
                            "data": (int(bits_pointer), False),
                            "strides": (
                                qimage.bytesPerLine(), 3
                            ),  # This does the magic: For each line, skip the correct number of bytes. Bytes per pixel is always 3 due to QImage.Format.Format_RGB888.
                            "version": 3
                        }

            array = np.asarray(QImageArrayView(xray_img)).view(
                np.dtype({
                    "r": (np.uint8, 0, "red"),
                    "g": (np.uint8, 1, "green"),
                    "b": (np.uint8, 2, "blue"),
                    "a":
                    (np.uint8, 3, "alpha"
                     )  # Never filled since QImage was reformatted to RGB888.
                }),
                np.recarray)
            if np.any(np.mod(array.r, 2)):
                self._next_xray_checking_time = time.time(
                ) + self._xray_warning_cooldown
                self._xray_warning_message.show()
                Logger.log("i", "X-Ray overlay found non-manifold pixels.")

    def event(self, event):
        if event.type == Event.ViewDeactivateEvent:
            if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings(
            ):
                self.getRenderer().removeRenderPass(self._xray_pass)
                self._composite_pass.setLayerBindings(self._old_layer_bindings)
                self._composite_pass.setCompositeShader(
                    self._old_composite_shader)
                self._xray_warning_message.hide()
コード例 #31
0
    def run(self):
        status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
                                 lifetime = 0,
                                 dismissable=False,
                                 progress = 0,
                                 title = i18n_catalog.i18nc("@info:title", "Finding Location"))
        status_message.show()


        # Collect nodes to be placed
        nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
        for node in self._nodes:
            offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
            nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))

        # Sort the nodes with the biggest area first.
        nodes_arr.sort(key=lambda item: item[0])
        nodes_arr.reverse()

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        machine_width = global_container_stack.getProperty("machine_width", "value")
        machine_depth = global_container_stack.getProperty("machine_depth", "value")

        x, y = machine_width, machine_depth

        arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
        arrange_array.add()

        # Place nodes one at a time
        start_priority = 0
        grouped_operation = GroupedOperation()
        found_solution_for_all = True
        left_over_nodes = []  # nodes that do not fit on an empty build plate

        for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
            # For performance reasons, we assume that when a location does not fit,
            # it will also not fit for the next object (while what can be untrue).

            try_placement = True

            current_build_plate_number = 0  # always start with the first one

            while try_placement:
                # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
                while current_build_plate_number >= arrange_array.count():
                    arrange_array.add()
                arranger = arrange_array.get(current_build_plate_number)

                best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority)
                x, y = best_spot.x, best_spot.y
                node.removeDecorator(ZOffsetDecorator)
                if node.getBoundingBox():
                    center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
                else:
                    center_y = 0
                if x is not None:  # We could find a place
                    arranger.place(x, y, offset_shape_arr)  # place the object in the arranger

                    node.callDecoration("setBuildPlateNumber", current_build_plate_number)
                    grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
                    try_placement = False
                else:
                    # very naive, because we skip to the next build plate if one model doesn't fit.
                    if arranger.isEmpty:
                        # apparently we can never place this object
                        left_over_nodes.append(node)
                        try_placement = False
                    else:
                        # try next build plate
                        current_build_plate_number += 1
                        try_placement = True

            status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
            Job.yieldThread()

        for node in left_over_nodes:
            node.callDecoration("setBuildPlateNumber", -1)  # these are not on any build plate
            found_solution_for_all = False

        grouped_operation.push()

        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
                                               title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
            no_full_solution_message.show()
コード例 #32
0
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
    def __init__(self, device_id, address: str, properties, parent = None) -> None:
        super().__init__(device_id = device_id, address = address, properties = properties, connection_type =  ConnectionType.NetworkConnection, parent = parent)
        self._api_prefix = "/api/v1/"
        self._number_of_extruders = 2

        self._authentication_id = None
        self._authentication_key = None

        self._authentication_counter = 0
        self._max_authentication_counter = 5 * 60  # Number of attempts before authentication timed out (5 min)

        self._authentication_timer = QTimer()
        self._authentication_timer.setInterval(1000)  # TODO; Add preference for update interval
        self._authentication_timer.setSingleShot(False)

        self._authentication_timer.timeout.connect(self._onAuthenticationTimer)

        # The messages are created when connect is called the first time.
        # This ensures that the messages are only created for devices that actually want to connect.
        self._authentication_requested_message = None
        self._authentication_failed_message = None
        self._authentication_succeeded_message = None
        self._not_authenticated_message = None

        self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setIconName("print")

        self._output_controller = LegacyUM3PrinterOutputController(self)

    def _createMonitorViewFromQML(self) -> None:
        if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None:
            self._monitor_view_qml_path = os.path.join(
                PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
                "resources", "qml", "MonitorStage.qml"
            )
        super()._createMonitorViewFromQML()

    def _onAuthenticationStateChanged(self):
        # We only accept commands if we are authenticated.
        self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)

        if self._authentication_state == AuthState.Authenticated:
            self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
        elif self._authentication_state == AuthState.AuthenticationRequested:
            self.setConnectionText(i18n_catalog.i18nc("@info:status",
                                                      "Connected over the network. Please approve the access request on the printer."))
        elif self._authentication_state == AuthState.AuthenticationDenied:
            self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))


    def _setupMessages(self):
        self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
                                                                            "Access to the printer requested. Please approve the request on the printer"),
                                                         lifetime=0, dismissable=False, progress=0,
                                                         title=i18n_catalog.i18nc("@info:title",
                                                                                  "Authentication status"))

        self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
        self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
                                                      i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
        self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
        self._authentication_succeeded_message = Message(
            i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))

        self._not_authenticated_message = Message(
            i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
        self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
                                                  None, i18n_catalog.i18nc("@info:tooltip",
                                                                           "Send access request to the printer"))
        self._not_authenticated_message.actionTriggered.connect(self._messageCallback)

    def _messageCallback(self, message_id=None, action_id="Retry"):
        if action_id == "Request" or action_id == "Retry":
            if self._authentication_failed_message:
                self._authentication_failed_message.hide()
            if self._not_authenticated_message:
                self._not_authenticated_message.hide()

            self._requestAuthentication()

    def connect(self):
        super().connect()
        self._setupMessages()
        global_container = CuraApplication.getInstance().getGlobalContainerStack()
        if global_container:
            self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
            self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)

    def close(self):
        super().close()
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        if self._authentication_failed_message:
            self._authentication_failed_message.hide()
        if self._authentication_succeeded_message:
            self._authentication_succeeded_message.hide()
        self._sending_gcode = False
        self._compressing_gcode = False
        self._authentication_timer.stop()

    ##  Send all material profiles to the printer.
    def _sendMaterialProfiles(self):
        Logger.log("i", "Sending material profiles to printer")

        # TODO: Might want to move this to a job...
        for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
            try:
                xml_data = container.serialize()
                if xml_data == "" or xml_data is None:
                    continue

                names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
                if names:
                    # There are other materials that share this GUID.
                    if not container.isReadOnly():
                        continue  # If it's not readonly, it's created by user, so skip it.

                file_name = "none.xml"

                self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None)

            except NotImplementedError:
                # If the material container is not the most "generic" one it can't be serialized an will raise a
                # NotImplementedError. We can simply ignore these.
                pass

    def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
        if not self.activePrinter:
            # No active printer. Unable to write
            return

        if self.activePrinter.state not in ["idle", ""]:
            # Printer is not able to accept commands.
            return

        if self._authentication_state != AuthState.Authenticated:
            # Not authenticated, so unable to send job.
            return

        self.writeStarted.emit(self)

        gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", [])
        active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
        gcode_list = gcode_dict[active_build_plate_id]

        if not gcode_list:
            # Unable to find g-code. Nothing to send
            return

        self._gcode = gcode_list

        errors = self._checkForErrors()
        if errors:
            text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
            informative_text = i18n_catalog.i18nc("@label",
                                                  "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
                                                  "Please resolve this issues before continuing.")
            detailed_text = ""
            for error in errors:
                detailed_text += error + "\n"

            CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
                                                 text,
                                                 informative_text,
                                                 detailed_text,
                                                 buttons=QMessageBox.Ok,
                                                 icon=QMessageBox.Critical,
                                                callback = self._messageBoxCallback
                                                 )
            return  # Don't continue; Errors must block sending the job to the printer.

        # There might be multiple things wrong with the configuration. Check these before starting.
        warnings = self._checkForWarnings()

        if warnings:
            text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
            informative_text = i18n_catalog.i18nc("@label",
                                                  "There is a mismatch between the configuration or calibration of the printer and Cura. "
                                                  "For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
            detailed_text = ""
            for warning in warnings:
                detailed_text += warning + "\n"

            CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
                                                 text,
                                                 informative_text,
                                                 detailed_text,
                                                 buttons=QMessageBox.Yes + QMessageBox.No,
                                                 icon=QMessageBox.Question,
                                                 callback=self._messageBoxCallback
                                                 )
            return

        # No warnings or errors, so we're good to go.
        self._startPrint()

        # Notify the UI that a switch to the print monitor should happen
        CuraApplication.getInstance().getController().setActiveStage("MonitorStage")

    def _startPrint(self):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            return

        self._sending_gcode = True

        self._send_gcode_start = time()
        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
                                         i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()
        
        compressed_gcode = self._compressGCode()
        if compressed_gcode is None:
            # Abort was called.
            return

        file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName
        self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
                      on_finished=self._onPostPrintJobFinished)

        return

    def _progressMessageActionTriggered(self, message_id=None, action_id=None):
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            CuraApplication.getInstance().getController().setActiveStage("PrepareStage")

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)
        else:
            self._progress_message.setProgress(0)

            self._progress_message.hide()

    def _messageBoxCallback(self, button):
        def delayedCallback():
            if button == QMessageBox.Yes:
                self._startPrint()
            else:
                CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
                # For some unknown reason Cura on OSX will hang if we do the call back code
                # immediately without first returning and leaving QML's event system.

        QTimer.singleShot(100, delayedCallback)

    def _checkForErrors(self):
        errors = []
        print_information = CuraApplication.getInstance().getPrintInformation()
        if not print_information.materialLengths:
            Logger.log("w", "There is no material length information. Unable to check for errors.")
            return errors

        for index, extruder in enumerate(self.activePrinter.extruders):
            # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
            if extruder.hotendID == "":
                # No Printcore loaded.
                errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))

            if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.
                if extruder.activeMaterial is None:
                    # No active material
                    errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
        return errors

    def _checkForWarnings(self):
        warnings = []
        print_information = CuraApplication.getInstance().getPrintInformation()

        if not print_information.materialLengths:
            Logger.log("w", "There is no material length information. Unable to check for warnings.")
            return warnings

        extruder_manager = ExtruderManager.getInstance()

        for index, extruder in enumerate(self.activePrinter.extruders):
            if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.

                # TODO: material length check

                # Check if the right Printcore is active.
                variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
                if variant:
                    if variant.getName() != extruder.hotendID:
                        warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
                else:
                    Logger.log("w", "Unable to find variant.")

                # Check if the right material is loaded.
                local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
                if local_material:
                    if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
                        Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
                        warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
                else:
                    Logger.log("w", "Unable to find material.")

        return warnings

    def _update(self):
        if not super()._update():
            return
        if self._authentication_state == AuthState.NotAuthenticated:
            if self._authentication_id is None and self._authentication_key is None:
                # This machine doesn't have any authentication, so request it.
                self._requestAuthentication()
            elif self._authentication_id is not None and self._authentication_key is not None:
                # We have authentication info, but we haven't checked it out yet. Do so now.
                self._verifyAuthentication()
        elif self._authentication_state == AuthState.AuthenticationReceived:
            # We have an authentication, but it's not confirmed yet.
            self._checkAuthentication()

        # We don't need authentication for requesting info, so we can go right ahead with requesting this.
        self.get("printer", on_finished=self._onGetPrinterDataFinished)
        self.get("print_job", on_finished=self._onGetPrintJobFinished)

    def _resetAuthenticationRequestedMessage(self):
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        self._authentication_timer.stop()
        self._authentication_counter = 0

    def _onAuthenticationTimer(self):
        self._authentication_counter += 1
        self._authentication_requested_message.setProgress(
            self._authentication_counter / self._max_authentication_counter * 100)
        if self._authentication_counter > self._max_authentication_counter:
            self._authentication_timer.stop()
            Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._resetAuthenticationRequestedMessage()
            self._authentication_failed_message.show()

    def _verifyAuthentication(self):
        Logger.log("d", "Attempting to verify authentication")
        # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
        self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted)

    def _onVerifyAuthenticationCompleted(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 401:
            # Something went wrong; We somehow tried to verify authentication without having one.
            Logger.log("d", "Attempted to verify auth without having one.")
            self._authentication_id = None
            self._authentication_key = None
            self.setAuthenticationState(AuthState.NotAuthenticated)
        elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
            # If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
            Logger.log("d",
                       "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
                       self._authentication_state)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()
        elif status_code == 200:
            self.setAuthenticationState(AuthState.Authenticated)

    def _checkAuthentication(self):
        Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
        self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished)

    def _onCheckAuthenticationFinished(self, reply):
        if str(self._authentication_id) not in reply.url().toString():
            Logger.log("w", "Got an old id response.")
            # Got response for old authentication ID.
            return
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
            return

        if data.get("message", "") == "authorized":
            Logger.log("i", "Authentication was approved")
            self.setAuthenticationState(AuthState.Authenticated)
            self._saveAuthentication()

            # Double check that everything went well.
            self._verifyAuthentication()

            # Notify the user.
            self._resetAuthenticationRequestedMessage()
            self._authentication_succeeded_message.show()
        elif data.get("message", "") == "unauthorized":
            Logger.log("i", "Authentication was denied.")
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()

    def _saveAuthentication(self) -> None:
        global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
        if self._authentication_key is None:
            Logger.log("e", "Authentication key is None, nothing to save.")
            return
        if self._authentication_id is None:
            Logger.log("e", "Authentication id is None, nothing to save.")
            return
        if global_container_stack:
            global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)

            global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)

            # Force save so we are sure the data is not lost.
            CuraApplication.getInstance().saveStack(global_container_stack)
            Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
                       self._getSafeAuthKey())
        else:
            Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
                       self._getSafeAuthKey())

    def _onRequestAuthenticationFinished(self, reply):
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
            self.setAuthenticationState(AuthState.NotAuthenticated)
            return

        self.setAuthenticationState(AuthState.AuthenticationReceived)
        self._authentication_id = data["id"]
        self._authentication_key = data["key"]
        Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
                   self._authentication_id, self._getSafeAuthKey())

    def _requestAuthentication(self):
        self._authentication_requested_message.show()
        self._authentication_timer.start()

        # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
        # give issues.
        self._authentication_key = None
        self._authentication_id = None

        self.post("auth/request",
                  json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(),
                              "user": self._getUserName()}),
                  on_finished=self._onRequestAuthenticationFinished)

        self.setAuthenticationState(AuthState.AuthenticationRequested)

    def _onAuthenticationRequired(self, reply, authenticator):
        if self._authentication_id is not None and self._authentication_key is not None:
            Logger.log("d",
                       "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
                       self._id, self._authentication_id, self._getSafeAuthKey())
            authenticator.setUser(self._authentication_id)
            authenticator.setPassword(self._authentication_key)
        else:
            Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)

    def _onGetPrintJobFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if not self._printers:
            return  # Ignore the data for now, we don't have info about a printer yet.
        printer = self._printers[0]

        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
                return
            if printer.activePrintJob is None:
                print_job = PrintJobOutputModel(output_controller=self._output_controller)
                printer.updateActivePrintJob(print_job)
            else:
                print_job = printer.activePrintJob
            print_job.updateState(result["state"])
            print_job.updateTimeElapsed(result["time_elapsed"])
            print_job.updateTimeTotal(result["time_total"])
            print_job.updateName(result["name"])
        elif status_code == 404:
            # No job found, so delete the active print job (if any!)
            printer.updateActivePrintJob(None)
        else:
            Logger.log("w",
                       "Got status code {status_code} while trying to get printer data".format(status_code=status_code))

    def materialHotendChangedMessage(self, callback):
        CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
                                             i18n_catalog.i18nc("@label",
                                                                "Would you like to use your current printer configuration in Cura?"),
                                             i18n_catalog.i18nc("@label",
                                                                "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
                                             buttons=QMessageBox.Yes + QMessageBox.No,
                                             icon=QMessageBox.Question,
                                             callback=callback
                                             )

    def _onGetPrinterDataFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
                return

            if not self._printers:
                # Quickest way to get the firmware version is to grab it from the zeroconf.
                firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
                self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
                self._printers[0].setCameraUrl(QUrl("http://" + self._address + ":8080/?action=stream"))
                for extruder in self._printers[0].extruders:
                    extruder.activeMaterialChanged.connect(self.materialIdChanged)
                    extruder.hotendIDChanged.connect(self.hotendIdChanged)
                self.printersChanged.emit()

            # LegacyUM3 always has a single printer.
            printer = self._printers[0]
            printer.updateBedTemperature(result["bed"]["temperature"]["current"])
            printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
            printer.updateState(result["status"])

            try:
                # If we're still handling the request, we should ignore remote for a bit.
                if not printer.getController().isPreheatRequestInProgress():
                    printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
            except KeyError:
                # Older firmwares don't support preheating, so we need to fake it.
                pass

            head_position = result["heads"][0]["position"]
            printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])

            for index in range(0, self._number_of_extruders):
                temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
                extruder = printer.extruders[index]
                extruder.updateTargetHotendTemperature(temperatures["target"])
                extruder.updateHotendTemperature(temperatures["current"])

                material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]

                if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
                    # Find matching material (as we need to set brand, type & color)
                    containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
                                                                                        GUID=material_guid)
                    if containers:
                        color = containers[0].getMetaDataEntry("color_code")
                        brand = containers[0].getMetaDataEntry("brand")
                        material_type = containers[0].getMetaDataEntry("material")
                        name = containers[0].getName()
                    else:
                        # Unknown material.
                        color = "#00000000"
                        brand = "Unknown"
                        material_type = "Unknown"
                        name = "Unknown"
                    material = MaterialOutputModel(guid=material_guid, type=material_type,
                                                   brand=brand, color=color, name = name)
                    extruder.updateActiveMaterial(material)

                try:
                    hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
                except KeyError:
                    hotend_id = ""
                printer.extruders[index].updateHotendID(hotend_id)

        else:
            Logger.log("w",
                       "Got status code {status_code} while trying to get printer data".format(status_code = status_code))

    ##  Convenience function to "blur" out all but the last 5 characters of the auth key.
    #   This can be used to debug print the key, without it compromising the security.
    def _getSafeAuthKey(self):
        if self._authentication_key is not None:
            result = self._authentication_key[-5:]
            result = "********" + result
            return result

        return self._authentication_key
コード例 #33
0
ファイル: MultiplyObjectsJob.py プロジェクト: maodch/Cura
    def run(self):
        status_message = Message(i18n_catalog.i18nc(
            "@info:status", "Multiplying and placing objects"),
                                 lifetime=0,
                                 dismissable=False,
                                 progress=0)
        status_message.show()
        scene = Application.getInstance().getController().getScene()

        total_progress = len(self._objects) * self._count
        current_progress = 0

        root = scene.getRoot()
        arranger = Arrange.create(scene_root=root)
        nodes = []
        for node in self._objects:
            # If object is part of a group, multiply group
            current_node = node
            while current_node.getParent() and current_node.getParent(
            ).callDecoration("isGroup"):
                current_node = current_node.getParent()

            node_too_big = False
            if node.getBoundingBox().width < 300 or node.getBoundingBox(
            ).depth < 300:
                offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(
                    current_node, min_offset=self._min_offset)
            else:
                node_too_big = True

            found_solution_for_all = True
            for i in range(self._count):
                # We do place the nodes one by one, as we want to yield in between.
                if not node_too_big:
                    node, solution_found = arranger.findNodePlacement(
                        current_node, offset_shape_arr, hull_shape_arr)
                if node_too_big or not solution_found:
                    found_solution_for_all = False
                    new_location = node.getPosition()
                    new_location = new_location.set(z=100 - i * 20)
                    node.setPosition(new_location)

                nodes.append(node)
                current_progress += 1
                status_message.setProgress(
                    (current_progress / total_progress) * 100)
                Job.yieldThread()

            Job.yieldThread()

        if nodes:
            op = GroupedOperation()
            for new_node in nodes:
                print_mode_enabled = Application.getInstance(
                ).getGlobalContainerStack().getProperty(
                    "print_mode", "enabled")
                if print_mode_enabled:
                    node_dup = DuplicatedNode(new_node)
                    op.addOperation(
                        AddNodesOperation(node_dup, current_node.getParent()))
                else:
                    op.addOperation(
                        AddSceneNodeOperation(new_node,
                                              current_node.getParent()))
            op.push()
        status_message.hide()

        if not found_solution_for_all:
            no_full_solution_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Unable to find a location within the build volume for all objects"
                ))
            no_full_solution_message.show()
コード例 #34
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()
    activeCameraUrlChanged = pyqtSignal()
    receivedPrintJobsChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent = None) -> None:
        super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

        self._dummy_lambdas = ("", {}, io.BytesIO()) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]]

        self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
        self._received_print_jobs = False # type: bool

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorStage.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True  # type: bool

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None  # type: Optional[Message]
        self._write_job_progress_message = None  # type: Optional[Message]
        self._progress_message = None  # type: Optional[Message]

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None  # type: QObject

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}  # type: Dict[str, str]

        self._finished_jobs = []  # type: List[UM3PrintJobOutputModel]

        self._cluster_size = int(properties.get(b"cluster_size", 0))  # type: int

        self._latest_reply_handler = None  # type: Optional[QNetworkReply]
        self._sending_job = None

        self._active_camera_url = QUrl()  # type: QUrl

    def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
        self.writeStarted.emit(self)

        self.sendMaterialProfiles()

        # Formats supported by this application (file types that we can actually write).
        if file_handler:
            file_formats = file_handler.getSupportedFileTypesWrite()
        else:
            file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()

        global_stack = CuraApplication.getInstance().getGlobalContainerStack()
        # Create a list from the supported file formats string.
        if not global_stack:
            Logger.log("e", "Missing global stack!")
            return

        machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
        machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
        # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
        if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"):
            machine_file_formats = ["application/x-ufp"] + machine_file_formats

        # Take the intersection between file_formats and machine_file_formats.
        format_by_mimetype = {format["mime_type"]: format for format in file_formats}
        file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.

        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
        preferred_format = file_formats[0]

        # Just take the first file format available.
        if file_handler is not None:
            writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
        else:
            writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"]))

        if not writer:
            Logger.log("e", "Unexpected error when trying to get the FileWriter")
            return

        # This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        if not writer:
            Logger.log("e", "Missing file or mesh writer!")
            return
        self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
        if self._sending_job is not None:
            self._sending_job.send(None)  # Start the generator.

            if len(self._printers) > 1:  # We need to ask the user.
                self._spawnPrinterSelectionDialog()
                is_job_sent = True
            else:  # Just immediately continue.
                self._sending_job.send("")  # No specifically selected printer.
                is_job_sent = self._sending_job.send(None)

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml")
            self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self) -> int:
        return self._cluster_size

    ##  Allows the user to choose a printer to print with from the printer
    #   selection dialogue.
    #   \param target_printer The name of the printer to target.
    @pyqtSlot(str)
    def selectPrinter(self, target_printer: str = "") -> None:
        if self._sending_job is not None:
            self._sending_job.send(target_printer)

    @pyqtSlot()
    def cancelPrintSelection(self) -> None:
        self._sending_gcode = False

    ##  Greenlet to send a job to the printer over the network.
    #
    #   This greenlet gets called asynchronously in requestWrite. It is a
    #   greenlet in order to optionally wait for selectPrinter() to select a
    #   printer.
    #   The greenlet yields exactly three times: First time None,
    #   \param writer The file writer to use to create the data.
    #   \param preferred_format A dictionary containing some information about
    #   what format to write to. This is necessary to create the correct buffer
    #   types and file extension and such.
    def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            yield #Wait on the user to select a target printer.
            yield #Wait for the write job to be finished.
            yield False #Return whether this was a success or not.
            yield #Prevent StopIteration.

        self._sending_gcode = True

        target_printer = yield #Potentially wait on the user to select a target printer.

        # Using buffering greatly reduces the write time for many lines of gcode

        stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode.
        if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
            stream = io.StringIO()

        job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])

        self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                                   title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
        self._write_job_progress_message.show()

        self._dummy_lambdas = (target_printer, preferred_format, stream)
        job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)

        job.start()

        yield True  # Return that we had success!
        yield  # To prevent having to catch the StopIteration exception.

    def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
        if self._write_job_progress_message:
            self._write_job_progress_message.hide()

        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                         title = i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()
        parts = []

        target_printer, preferred_format, stream = self._dummy_lambdas

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
            parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))

        # Add user name to the print_job
        parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))

        file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]

        output = stream.getvalue()  # Either str or bytes depending on the output mode.
        if isinstance(stream, io.StringIO):
            output = cast(str, output).encode("utf-8")
        output = cast(bytes, output)

        parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))

        self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify = activePrinterChanged)
    def activePrinter(self) -> Optional[PrinterOutputModel]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
        if self._active_printer != printer:
            self._active_printer = printer
            self.activePrinterChanged.emit()

    @pyqtProperty(QUrl, notify = activeCameraUrlChanged)
    def activeCameraUrl(self) -> "QUrl":
        return self._active_camera_url

    @pyqtSlot(QUrl)
    def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
        if self._active_camera_url != camera_url:
            self._active_camera_url = camera_url
            self.activeCameraUrlChanged.emit()

    def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
        if self._progress_message:
            self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if self._progress_message and new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)

            # If successfully sent:
            if bytes_sent == bytes_total:
                # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
                # the monitor tab.
                self._success_message = Message(
                    i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
                    lifetime=5, dismissable=True,
                    title=i18n_catalog.i18nc("@info:title", "Data Sent"))
                self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None,
                                                description="")
                self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
                self._success_message.show()
        else:
            if self._progress_message is not None:
                self._progress_message.setProgress(0)
                self._progress_message.hide()

    def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            if self._progress_message is not None:
                self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            CuraApplication.getInstance().getController().setActiveStage("PrepareStage")

            # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
            # the "reply" should be disconnected
            if self._latest_reply_handler:
                self._latest_reply_handler.disconnect()
                self._latest_reply_handler = None

    def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
        if action_id == "View":
            CuraApplication.getInstance().getController().setActiveStage("MonitorStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self) -> None:
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self) -> None:
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def printJobs(self)-> List[UM3PrintJobOutputModel]:
        return self._print_jobs

    @pyqtProperty(bool, notify = receivedPrintJobsChanged)
    def receivedPrintJobs(self) -> bool:
        return self._received_print_jobs

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"]

    @pyqtProperty("QVariantList", notify = printJobsChanged)
    def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]

    @pyqtProperty("QVariantList", notify = clusterPrintersChanged)
    def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
        printer_count = {} # type: Dict[str, int]
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
        return result

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def printers(self):
        return self._printers

    @pyqtSlot(int, result = str)
    def formatDuration(self, seconds: int) -> str:
        return Duration(seconds).getDisplayString(DurationFormat.Format.Short)

    @pyqtSlot(int, result = str)
    def getTimeCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result = str)
    def getDateCompleted(self, time_remaining: int) -> str:
        current_time = time()
        completed = datetime.fromtimestamp(current_time + time_remaining)
        today = datetime.fromtimestamp(current_time)

        # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
        if completed.toordinal() > today.toordinal() + 7:
            return completed.strftime("%a %b ") + "{day}".format(day=completed.day)
        
        # If finishing date is within the next week, use "Monday at HH:MM" format
        elif completed.toordinal() > today.toordinal() + 1:
            return completed.strftime("%a")
        
        # If finishing tomorrow, use "tomorrow at HH:MM" format
        elif completed.toordinal() > today.toordinal():
            return "tomorrow"

        # If finishing today, use "today at HH:MM" format
        else:
            return "today"

    @pyqtSlot(str)
    def sendJobToTop(self, print_job_uuid: str) -> None:
        # This function is part of the output device (and not of the printjob output model) as this type of operation
        # is a modification of the cluster queue and not of the actual job.
        data = "{\"to_position\": 0}"
        self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)

    @pyqtSlot(str)
    def deleteJobFromQueue(self, print_job_uuid: str) -> None:
        # This function is part of the output device (and not of the printjob output model) as this type of operation
        # is a modification of the cluster queue and not of the actual job.
        self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)

    @pyqtSlot(str)
    def forceSendJob(self, print_job_uuid: str) -> None:
        data = "{\"force\": true}"
        self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None)

    def _printJobStateChanged(self) -> None:
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]

        newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
            else:
                job_completed_text =  i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
            job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    ##  Called when the connection to the cluster changes.
    def connect(self) -> None:
        super().connect()
        self.sendMaterialProfiles()

    def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
        reply_url = reply.url().toString()

        uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]

        print_job = findByKey(self._print_jobs, uuid)
        if print_job:
            image = QImage()
            image.loadFromData(reply.readAll())
            print_job.updatePreviewImage(image)

    def _update(self) -> None:
        super()._update()
        self.get("printers/", on_finished = self._onGetPrintersDataFinished)
        self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)

        for print_job in self._print_jobs:
            if print_job.getPreviewImage() is None:
                self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
        self._received_print_jobs = True
        self.receivedPrintJobsChanged.emit()

        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for idx, print_job_data in enumerate(result):
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])
            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True
            elif not job_list_changed:
                # Check if the order of the jobs has changed since the last check
                if self._print_jobs.index(print_job) != idx:
                    job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued" and print_job.state != "error":  # Print job should be assigned to a printer.
                if print_job.state in ["failed", "finished", "aborted", "none"]:
                    # Print job was already completed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]

        for removed_job in removed_jobs:
            job_list_changed = job_list_changed or self._removeJob(removed_job)

        if job_list_changed:
            # Override the old list with the new list (either because jobs were removed / added or order changed)
            self._print_jobs = print_jobs_seen
            self.printJobsChanged.emit()  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [printer for printer in self._printers if printer not in printers_seen]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel:
        printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self),
                                     number_of_extruders = self._number_of_extruders)
        printer.setCameraUrl(QUrl("http://" + data["ip_address"] + ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel:
        print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                        key=data["uuid"], name= data["name"])

        configuration = ConfigurationModel()
        extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
        for index in range(0, self._number_of_extruders):
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                continue
            extruder = extruders[int(data["configuration"][index]["extruder_index"])]
            extruder.setHotendID(extruder_data.get("print_core_id", ""))
            extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))

        configuration.setExtruderConfigurations(extruders)
        print_job.updateConfiguration(configuration)
        print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
        print_job.stateChanged.connect(self._printJobStateChanged)
        return print_job

    def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None:
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        impediments_to_printing = data.get("impediments_to_printing", [])
        print_job.updateOwner(data["owner"])

        status_set_by_impediment = False
        for impediment in impediments_to_printing:
            if impediment["severity"] == "UNFIXABLE":
                status_set_by_impediment = True
                print_job.updateState("error")
                break

        if not status_set_by_impediment:
            print_job.updateState(data["status"])

        print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"]))

    def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]:
        result = []
        for change in data:
            result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"],
                                                   index=change["index"],
                                                   target_name=change["target_name"],
                                                   origin_name=change["origin_name"]))
        return result

    def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
        material_manager = CuraApplication.getInstance().getMaterialManager()
        material_group_list = None

        # Avoid crashing if there is no "guid" field in the metadata
        material_guid = material_data.get("guid")
        if material_guid:
            material_group_list = material_manager.getMaterialGroupListByGUID(material_guid)

        # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the		
        # material is unknown to Cura, so we should return an "empty" or "unknown" material model.		
        if material_group_list is None:
            material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \
                        else i18n_catalog.i18nc("@label:material", "Unknown")
            return MaterialOutputModel(guid = material_data.get("guid", ""),
                                        type = material_data.get("type", ""),
                                        color = material_data.get("color", ""),
                                        brand = material_data.get("brand", ""),
                                        name = material_data.get("name", material_name)
                                        )

        # Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
        read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
        non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
        material_group = None
        if read_only_material_group_list:
            read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
            material_group = read_only_material_group_list[0]
        elif non_read_only_material_group_list:
            non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
            material_group = non_read_only_material_group_list[0]

        if material_group:
            container = material_group.root_material_node.getContainer()
            color = container.getMetaDataEntry("color_code")
            brand = container.getMetaDataEntry("brand")
            material_type = container.getMetaDataEntry("material")
            name = container.getName()
        else:
            Logger.log("w",
                       "Unable to find material with guid {guid}. Using data as provided by cluster".format(
                           guid=material_data["guid"]))
            color = material_data["color"]
            brand = material_data["brand"]
            material_type = material_data["material"]
            name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \
                else i18n_catalog.i18nc("@label:material", "Unknown")
        return MaterialOutputModel(guid = material_data["guid"], type = material_type,
                                   brand = brand, color = color, name = name)

    def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]

        definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
        if not definitions:
            Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
            return

        machine_definition = definitions[0]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])

        # Do not store the build plate information that comes from connect if the current printer has not build plate information
        if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
            printer.updateBuildplateName(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

        for index in range(0, self._number_of_extruders):
            extruder = printer.extruders[index]
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                break

            extruder.updateHotendID(extruder_data.get("print_core_id", ""))

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
                material = self._createMaterialOutputModel(material_data)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job: UM3PrintJobOutputModel) -> bool:
        if job not in self._print_jobs:
            return False

        if job.assignedPrinter:
            job.assignedPrinter.updateActivePrintJob(None)
            job.stateChanged.disconnect(self._printJobStateChanged)
        self._print_jobs.remove(job)

        return True

    def _removePrinter(self, printer: PrinterOutputModel) -> None:
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()

    ##  Sync the material profiles in Cura with the printer.
    #
    #   This gets called when connecting to a printer as well as when sending a
    #   print.
    def sendMaterialProfiles(self) -> None:
        job = SendMaterialJob(device = self)
        job.run()
コード例 #35
0
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
    def __init__(self,
                 device_id,
                 address: str,
                 properties,
                 parent=None) -> None:
        super().__init__(device_id=device_id,
                         address=address,
                         properties=properties,
                         parent=parent)
        self._api_prefix = "/api/v1/"
        self._number_of_extruders = 2

        self._authentication_id = None
        self._authentication_key = None

        self._authentication_counter = 0
        self._max_authentication_counter = 5 * 60  # Number of attempts before authentication timed out (5 min)

        self._authentication_timer = QTimer()
        self._authentication_timer.setInterval(
            1000)  # TODO; Add preference for update interval
        self._authentication_timer.setSingleShot(False)

        self._authentication_timer.timeout.connect(self._onAuthenticationTimer)

        # The messages are created when connect is called the first time.
        # This ensures that the messages are only created for devices that actually want to connect.
        self._authentication_requested_message = None
        self._authentication_failed_message = None
        self._authentication_succeeded_message = None
        self._not_authenticated_message = None

        self.authenticationStateChanged.connect(
            self._onAuthenticationStateChanged)

        self.setPriority(
            3
        )  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.",
                               "Print over network"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setIconName("print")

        self._monitor_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "../resources/qml/MonitorItem.qml")

        self._output_controller = LegacyUM3PrinterOutputController(self)

    def _onAuthenticationStateChanged(self):
        # We only accept commands if we are authenticated.
        self._setAcceptsCommands(
            self._authentication_state == AuthState.Authenticated)

        if self._authentication_state == AuthState.Authenticated:
            self.setConnectionText(
                i18n_catalog.i18nc("@info:status",
                                   "Connected over the network."))
        elif self._authentication_state == AuthState.AuthenticationRequested:
            self.setConnectionText(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Connected over the network. Please approve the access request on the printer."
                ))
        elif self._authentication_state == AuthState.AuthenticationDenied:
            self.setConnectionText(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Connected over the network. No access to control the printer."
                ))

    def _setupMessages(self):
        self._authentication_requested_message = Message(
            i18n_catalog.i18nc(
                "@info:status",
                "Access to the printer requested. Please approve the request on the printer"
            ),
            lifetime=0,
            dismissable=False,
            progress=0,
            title=i18n_catalog.i18nc("@info:title", "Authentication status"))

        self._authentication_failed_message = Message(
            "",
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
        self._authentication_failed_message.addAction(
            "Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
            i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
        self._authentication_failed_message.actionTriggered.connect(
            self._messageCallback)
        self._authentication_succeeded_message = Message(
            i18n_catalog.i18nc("@info:status",
                               "Access to the printer accepted"),
            title=i18n_catalog.i18nc("@info:title", "Authentication Status"))

        self._not_authenticated_message = Message(i18n_catalog.i18nc(
            "@info:status",
            "No access to print with this printer. Unable to send print job."),
                                                  title=i18n_catalog.i18nc(
                                                      "@info:title",
                                                      "Authentication Status"))
        self._not_authenticated_message.addAction(
            "Request", i18n_catalog.i18nc("@action:button", "Request Access"),
            None,
            i18n_catalog.i18nc("@info:tooltip",
                               "Send access request to the printer"))
        self._not_authenticated_message.actionTriggered.connect(
            self._messageCallback)

    def _messageCallback(self, message_id=None, action_id="Retry"):
        if action_id == "Request" or action_id == "Retry":
            if self._authentication_failed_message:
                self._authentication_failed_message.hide()
            if self._not_authenticated_message:
                self._not_authenticated_message.hide()

            self._requestAuthentication()

    def connect(self):
        super().connect()
        self._setupMessages()
        global_container = SteSlicerApplication.getInstance(
        ).getGlobalContainerStack()
        if global_container:
            self._authentication_id = global_container.getMetaDataEntry(
                "network_authentication_id", None)
            self._authentication_key = global_container.getMetaDataEntry(
                "network_authentication_key", None)

    def close(self):
        super().close()
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        if self._authentication_failed_message:
            self._authentication_failed_message.hide()
        if self._authentication_succeeded_message:
            self._authentication_succeeded_message.hide()
        self._sending_gcode = False
        self._compressing_gcode = False
        self._authentication_timer.stop()

    ##  Send all material profiles to the printer.
    def _sendMaterialProfiles(self):
        Logger.log("i", "Sending material profiles to printer")

        # TODO: Might want to move this to a job...
        for container in ContainerRegistry.getInstance(
        ).findInstanceContainers(type="material"):
            try:
                xml_data = container.serialize()
                if xml_data == "" or xml_data is None:
                    continue

                names = ContainerManager.getInstance().getLinkedMaterials(
                    container.getId())
                if names:
                    # There are other materials that share this GUID.
                    if not container.isReadOnly():
                        continue  # If it's not readonly, it's created by user, so skip it.

                file_name = "none.xml"

                self.postForm("materials",
                              "form-data; name=\"file\";filename=\"%s\"" %
                              file_name,
                              xml_data.encode(),
                              on_finished=None)

            except NotImplementedError:
                # If the material container is not the most "generic" one it can't be serialized an will raise a
                # NotImplementedError. We can simply ignore these.
                pass

    def requestWrite(self,
                     nodes: List[SceneNode],
                     file_name: Optional[str] = None,
                     limit_mimetypes: bool = False,
                     file_handler: Optional[FileHandler] = None,
                     **kwargs: str) -> None:
        if not self.activePrinter:
            # No active printer. Unable to write
            return

        if self.activePrinter.state not in ["idle", ""]:
            # Printer is not able to accept commands.
            return

        if self._authentication_state != AuthState.Authenticated:
            # Not authenticated, so unable to send job.
            return

        self.writeStarted.emit(self)

        gcode_dict = getattr(
            SteSlicerApplication.getInstance().getController().getScene(),
            "gcode_dict", [])
        active_build_plate_id = SteSlicerApplication.getInstance(
        ).getMultiBuildPlateModel().activeBuildPlate
        gcode_list = gcode_dict[active_build_plate_id]

        if not gcode_list:
            # Unable to find g-code. Nothing to send
            return

        self._gcode = gcode_list

        errors = self._checkForErrors()
        if errors:
            text = i18n_catalog.i18nc("@label",
                                      "Unable to start a new print job.")
            informative_text = i18n_catalog.i18nc(
                "@label",
                "There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
                "Please resolve this issues before continuing.")
            detailed_text = ""
            for error in errors:
                detailed_text += error + "\n"

            SteSlicerApplication.getInstance().messageBox(
                i18n_catalog.i18nc("@window:title",
                                   "Mismatched configuration"),
                text,
                informative_text,
                detailed_text,
                buttons=QMessageBox.Ok,
                icon=QMessageBox.Critical,
                callback=self._messageBoxCallback)
            return  # Don't continue; Errors must block sending the job to the printer.

        # There might be multiple things wrong with the configuration. Check these before starting.
        warnings = self._checkForWarnings()

        if warnings:
            text = i18n_catalog.i18nc(
                "@label",
                "Are you sure you wish to print with the selected configuration?"
            )
            informative_text = i18n_catalog.i18nc(
                "@label",
                "There is a mismatch between the configuration or calibration of the printer and Cura. "
                "For the best result, always slice for the PrintCores and materials that are inserted in your printer."
            )
            detailed_text = ""
            for warning in warnings:
                detailed_text += warning + "\n"

            SteSlicerApplication.getInstance().messageBox(
                i18n_catalog.i18nc("@window:title",
                                   "Mismatched configuration"),
                text,
                informative_text,
                detailed_text,
                buttons=QMessageBox.Yes + QMessageBox.No,
                icon=QMessageBox.Question,
                callback=self._messageBoxCallback)
            return

        # No warnings or errors, so we're good to go.
        self._startPrint()

        # Notify the UI that a switch to the print monitor should happen
        SteSlicerApplication.getInstance().getController().setActiveStage(
            "MonitorStage")

    def _startPrint(self):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Sending new jobs (temporarily) blocked, still sending the previous print job."
                ))
            self._error_message.show()
            return

        self._sending_gcode = True

        self._send_gcode_start = time()
        self._progress_message = Message(
            i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0,
            False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction(
            "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
        self._progress_message.actionTriggered.connect(
            self._progressMessageActionTriggered)
        self._progress_message.show()

        compressed_gcode = self._compressGCode()
        if compressed_gcode is None:
            # Abort was called.
            return

        file_name = "%s.gcode.gz" % SteSlicerApplication.getInstance(
        ).getPrintInformation().jobName
        self.postForm("print_job",
                      "form-data; name=\"file\";filename=\"%s\"" % file_name,
                      compressed_gcode,
                      on_finished=self._onPostPrintJobFinished)

        return

    def _progressMessageActionTriggered(self, message_id=None, action_id=None):
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            SteSlicerApplication.getInstance().getController().setActiveStage(
                "PrepareStage")

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show(
                )  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total *
                                                   100)
        else:
            self._progress_message.setProgress(0)

            self._progress_message.hide()

    def _messageBoxCallback(self, button):
        def delayedCallback():
            if button == QMessageBox.Yes:
                self._startPrint()
            else:
                SteSlicerApplication.getInstance().getController(
                ).setActiveStage("PrepareStage")
                # For some unknown reason Cura on OSX will hang if we do the call back code
                # immediately without first returning and leaving QML's event system.

        QTimer.singleShot(100, delayedCallback)

    def _checkForErrors(self):
        errors = []
        print_information = SteSlicerApplication.getInstance(
        ).getPrintInformation()
        if not print_information.materialLengths:
            Logger.log(
                "w",
                "There is no material length information. Unable to check for errors."
            )
            return errors

        for index, extruder in enumerate(self.activePrinter.extruders):
            # Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
            if extruder.hotendID == "":
                # No Printcore loaded.
                errors.append(
                    i18n_catalog.i18nc(
                        "@info:status",
                        "No Printcore loaded in slot {slot_number}".format(
                            slot_number=index + 1)))

            if index < len(print_information.materialLengths
                           ) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.
                if extruder.activeMaterial is None:
                    # No active material
                    errors.append(
                        i18n_catalog.i18nc(
                            "@info:status",
                            "No material loaded in slot {slot_number}".format(
                                slot_number=index + 1)))
        return errors

    def _checkForWarnings(self):
        warnings = []
        print_information = SteSlicerApplication.getInstance(
        ).getPrintInformation()

        if not print_information.materialLengths:
            Logger.log(
                "w",
                "There is no material length information. Unable to check for warnings."
            )
            return warnings

        extruder_manager = ExtruderManager.getInstance()

        for index, extruder in enumerate(self.activePrinter.extruders):
            if index < len(print_information.materialLengths
                           ) and print_information.materialLengths[index] != 0:
                # The extruder is by this print.

                # TODO: material length check

                # Check if the right Printcore is active.
                variant = extruder_manager.getExtruderStack(
                    index).findContainer({"type": "variant"})
                if variant:
                    if variant.getName() != extruder.hotendID:
                        warnings.append(
                            i18n_catalog.i18nc(
                                "@label",
                                "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}"
                                .format(
                                    cura_printcore_name=variant.getName(),
                                    remote_printcore_name=extruder.hotendID,
                                    extruder_id=index + 1)))
                else:
                    Logger.log("w", "Unable to find variant.")

                # Check if the right material is loaded.
                local_material = extruder_manager.getExtruderStack(
                    index).findContainer({"type": "material"})
                if local_material:
                    if extruder.activeMaterial.guid != local_material.getMetaDataEntry(
                            "GUID"):
                        Logger.log(
                            "w",
                            "Extruder %s has a different material (%s) as Cura (%s)",
                            index + 1, extruder.activeMaterial.guid,
                            local_material.getMetaDataEntry("GUID"))
                        warnings.append(
                            i18n_catalog.i18nc(
                                "@label",
                                "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}"
                            ).format(local_material.getName(),
                                     extruder.activeMaterial.name, index + 1))
                else:
                    Logger.log("w", "Unable to find material.")

        return warnings

    def _update(self):
        if not super()._update():
            return
        if self._authentication_state == AuthState.NotAuthenticated:
            if self._authentication_id is None and self._authentication_key is None:
                # This machine doesn't have any authentication, so request it.
                self._requestAuthentication()
            elif self._authentication_id is not None and self._authentication_key is not None:
                # We have authentication info, but we haven't checked it out yet. Do so now.
                self._verifyAuthentication()
        elif self._authentication_state == AuthState.AuthenticationReceived:
            # We have an authentication, but it's not confirmed yet.
            self._checkAuthentication()

        # We don't need authentication for requesting info, so we can go right ahead with requesting this.
        self.get("printer", on_finished=self._onGetPrinterDataFinished)
        self.get("print_job", on_finished=self._onGetPrintJobFinished)

    def _resetAuthenticationRequestedMessage(self):
        if self._authentication_requested_message:
            self._authentication_requested_message.hide()
        self._authentication_timer.stop()
        self._authentication_counter = 0

    def _onAuthenticationTimer(self):
        self._authentication_counter += 1
        self._authentication_requested_message.setProgress(
            self._authentication_counter / self._max_authentication_counter *
            100)
        if self._authentication_counter > self._max_authentication_counter:
            self._authentication_timer.stop()
            Logger.log(
                "i",
                "Authentication timer ended. Setting authentication to denied for printer: %s"
                % self._id)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._resetAuthenticationRequestedMessage()
            self._authentication_failed_message.show()

    def _verifyAuthentication(self):
        Logger.log("d", "Attempting to verify authentication")
        # This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
        self.get("auth/verify",
                 on_finished=self._onVerifyAuthenticationCompleted)

    def _onVerifyAuthenticationCompleted(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 401:
            # Something went wrong; We somehow tried to verify authentication without having one.
            Logger.log("d", "Attempted to verify auth without having one.")
            self._authentication_id = None
            self._authentication_key = None
            self.setAuthenticationState(AuthState.NotAuthenticated)
        elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
            # If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
            Logger.log(
                "d",
                "While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
                self._authentication_state)
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()
        elif status_code == 200:
            self.setAuthenticationState(AuthState.Authenticated)

    def _checkAuthentication(self):
        Logger.log(
            "d", "Checking if authentication is correct for id %s and key %s",
            self._authentication_id, self._getSafeAuthKey())
        self.get("auth/check/" + str(self._authentication_id),
                 on_finished=self._onCheckAuthenticationFinished)

    def _onCheckAuthenticationFinished(self, reply):
        if str(self._authentication_id) not in reply.url().toString():
            Logger.log("w", "Got an old id response.")
            # Got response for old authentication ID.
            return
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log(
                "w",
                "Received an invalid authentication check from printer: Not valid JSON."
            )
            return

        if data.get("message", "") == "authorized":
            Logger.log("i", "Authentication was approved")
            self.setAuthenticationState(AuthState.Authenticated)
            self._saveAuthentication()

            # Double check that everything went well.
            self._verifyAuthentication()

            # Notify the user.
            self._resetAuthenticationRequestedMessage()
            self._authentication_succeeded_message.show()
        elif data.get("message", "") == "unauthorized":
            Logger.log("i", "Authentication was denied.")
            self.setAuthenticationState(AuthState.AuthenticationDenied)
            self._authentication_failed_message.show()

    def _saveAuthentication(self) -> None:
        global_container_stack = SteSlicerApplication.getInstance(
        ).getGlobalContainerStack()
        if self._authentication_key is None:
            Logger.log("e", "Authentication key is None, nothing to save.")
            return
        if self._authentication_id is None:
            Logger.log("e", "Authentication id is None, nothing to save.")
            return
        if global_container_stack:
            global_container_stack.setMetaDataEntry(
                "network_authentication_key", self._authentication_key)

            global_container_stack.setMetaDataEntry(
                "network_authentication_id", self._authentication_id)

            # Force save so we are sure the data is not lost.
            SteSlicerApplication.getInstance().saveStack(
                global_container_stack)
            Logger.log("i", "Authentication succeeded for id %s and key %s",
                       self._authentication_id, self._getSafeAuthKey())
        else:
            Logger.log("e",
                       "Unable to save authentication for id %s and key %s",
                       self._authentication_id, self._getSafeAuthKey())

    def _onRequestAuthenticationFinished(self, reply):
        try:
            data = json.loads(bytes(reply.readAll()).decode("utf-8"))
        except json.decoder.JSONDecodeError:
            Logger.log(
                "w",
                "Received an invalid authentication request reply from printer: Not valid JSON."
            )
            self.setAuthenticationState(AuthState.NotAuthenticated)
            return

        self.setAuthenticationState(AuthState.AuthenticationReceived)
        self._authentication_id = data["id"]
        self._authentication_key = data["key"]
        Logger.log(
            "i",
            "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
            self._authentication_id, self._getSafeAuthKey())

    def _requestAuthentication(self):
        self._authentication_requested_message.show()
        self._authentication_timer.start()

        # Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
        # give issues.
        self._authentication_key = None
        self._authentication_id = None

        self.post("auth/request",
                  json.dumps({
                      "application":
                      "Cura-" +
                      SteSlicerApplication.getInstance().getVersion(),
                      "user":
                      self._getUserName()
                  }),
                  on_finished=self._onRequestAuthenticationFinished)

        self.setAuthenticationState(AuthState.AuthenticationRequested)

    def _onAuthenticationRequired(self, reply, authenticator):
        if self._authentication_id is not None and self._authentication_key is not None:
            Logger.log(
                "d",
                "Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
                self._id, self._authentication_id, self._getSafeAuthKey())
            authenticator.setUser(self._authentication_id)
            authenticator.setPassword(self._authentication_key)
        else:
            Logger.log(
                "d",
                "No authentication is available to use for %s, but we did got a request for it.",
                self._id)

    def _onGetPrintJobFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if not self._printers:
            return  # Ignore the data for now, we don't have info about a printer yet.
        printer = self._printers[0]

        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log(
                    "w",
                    "Received an invalid print job state message: Not valid JSON."
                )
                return
            if printer.activePrintJob is None:
                print_job = PrintJobOutputModel(
                    output_controller=self._output_controller)
                printer.updateActivePrintJob(print_job)
            else:
                print_job = printer.activePrintJob
            print_job.updateState(result["state"])
            print_job.updateTimeElapsed(result["time_elapsed"])
            print_job.updateTimeTotal(result["time_total"])
            print_job.updateName(result["name"])
        elif status_code == 404:
            # No job found, so delete the active print job (if any!)
            printer.updateActivePrintJob(None)
        else:
            Logger.log(
                "w",
                "Got status code {status_code} while trying to get printer data"
                .format(status_code=status_code))

    def materialHotendChangedMessage(self, callback):
        SteSlicerApplication.getInstance().messageBox(
            i18n_catalog.i18nc("@window:title", "Sync with your printer"),
            i18n_catalog.i18nc(
                "@label",
                "Would you like to use your current printer configuration in Cura?"
            ),
            i18n_catalog.i18nc(
                "@label",
                "The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."
            ),
            buttons=QMessageBox.Yes + QMessageBox.No,
            icon=QMessageBox.Question,
            callback=callback)

    def _onGetPrinterDataFinished(self, reply):
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if status_code == 200:
            try:
                result = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except json.decoder.JSONDecodeError:
                Logger.log(
                    "w",
                    "Received an invalid printer state message: Not valid JSON."
                )
                return

            if not self._printers:
                # Quickest way to get the firmware version is to grab it from the zeroconf.
                firmware_version = self._properties.get(
                    b"firmware_version", b"").decode("utf-8")
                self._printers = [
                    PrinterOutputModel(
                        output_controller=self._output_controller,
                        number_of_extruders=self._number_of_extruders,
                        firmware_version=firmware_version)
                ]
                self._printers[0].setCameraUrl(
                    QUrl("http://" + self._address + ":8080/?action=stream"))
                for extruder in self._printers[0].extruders:
                    extruder.activeMaterialChanged.connect(
                        self.materialIdChanged)
                    extruder.hotendIDChanged.connect(self.hotendIdChanged)
                self.printersChanged.emit()

            # LegacyUM3 always has a single printer.
            printer = self._printers[0]
            printer.updateBedTemperature(
                result["bed"]["temperature"]["current"])
            printer.updateTargetBedTemperature(
                result["bed"]["temperature"]["target"])
            printer.updateState(result["status"])

            try:
                # If we're still handling the request, we should ignore remote for a bit.
                if not printer.getController().isPreheatRequestInProgress():
                    printer.updateIsPreheating(
                        result["bed"]["pre_heat"]["active"])
            except KeyError:
                # Older firmwares don't support preheating, so we need to fake it.
                pass

            head_position = result["heads"][0]["position"]
            printer.updateHeadPosition(head_position["x"], head_position["y"],
                                       head_position["z"])

            for index in range(0, self._number_of_extruders):
                temperatures = result["heads"][0]["extruders"][index][
                    "hotend"]["temperature"]
                extruder = printer.extruders[index]
                extruder.updateTargetHotendTemperature(temperatures["target"])
                extruder.updateHotendTemperature(temperatures["current"])

                material_guid = result["heads"][0]["extruders"][index][
                    "active_material"]["guid"]

                if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
                    # Find matching material (as we need to set brand, type & color)
                    containers = ContainerRegistry.getInstance(
                    ).findInstanceContainers(type="material",
                                             GUID=material_guid)
                    if containers:
                        color = containers[0].getMetaDataEntry("color_code")
                        brand = containers[0].getMetaDataEntry("brand")
                        material_type = containers[0].getMetaDataEntry(
                            "material")
                        name = containers[0].getName()
                    else:
                        # Unknown material.
                        color = "#00000000"
                        brand = "Unknown"
                        material_type = "Unknown"
                        name = "Unknown"
                    material = MaterialOutputModel(guid=material_guid,
                                                   type=material_type,
                                                   brand=brand,
                                                   color=color,
                                                   name=name)
                    extruder.updateActiveMaterial(material)

                try:
                    hotend_id = result["heads"][0]["extruders"][index][
                        "hotend"]["id"]
                except KeyError:
                    hotend_id = ""
                printer.extruders[index].updateHotendID(hotend_id)

        else:
            Logger.log(
                "w",
                "Got status code {status_code} while trying to get printer data"
                .format(status_code=status_code))

    ##  Convenience function to "blur" out all but the last 5 characters of the auth key.
    #   This can be used to debug print the key, without it compromising the security.
    def _getSafeAuthKey(self):
        if self._authentication_key is not None:
            result = self._authentication_key[-5:]
            result = "********" + result
            return result

        return self._authentication_key
コード例 #36
0
ファイル: CuraEngineBackend.py プロジェクト: sonnylowe/Cura
class CuraEngineBackend(Backend):
    def __init__(self):
        super().__init__()

        # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
        default_engine_location = os.path.join(Application.getInstallPrefix(),
                                               "bin", "CuraEngine")
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(
                os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
        if sys.platform == "win32":
            default_engine_location += ".exe"
        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location",
                                                default_engine_location)

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(
            self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = None

        self._profile = None
        Application.getInstance().getMachineManager(
        ).activeProfileChanged.connect(self._onActiveProfileChanged)
        self._onActiveProfileChanged()

        self._change_timer = QTimer()
        self._change_timer.setInterval(500)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self.slice)

        self._message_handlers[
            Cura_pb2.SlicedObjectList] = self._onSlicedObjectListMessage
        self._message_handlers[Cura_pb2.Progress] = self._onProgressMessage
        self._message_handlers[Cura_pb2.GCodeLayer] = self._onGCodeLayerMessage
        self._message_handlers[
            Cura_pb2.GCodePrefix] = self._onGCodePrefixMessage
        self._message_handlers[
            Cura_pb2.ObjectPrintTime] = self._onObjectPrintTimeMessage

        self._slicing = False
        self._restart = False

        self._save_gcode = True
        self._save_polygons = True
        self._report_progress = True

        self._enabled = True

        self._message = None

        self.backendConnected.connect(self._onBackendConnected)
        Application.getInstance().getController().toolOperationStarted.connect(
            self._onToolOperationStarted)
        Application.getInstance().getController().toolOperationStopped.connect(
            self._onToolOperationStopped)

        Application.getInstance().getMachineManager(
        ).activeMachineInstanceChanged.connect(self._onInstanceChanged)

    ##  Get the command that is used to call the engine.
    #   This is usefull for debugging and used to actually start the engine
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        active_machine = Application.getInstance().getMachineManager(
        ).getActiveMachineInstance()
        if not active_machine:
            return None

        return [
            Preferences.getInstance().getValue("backend/location"), "connect",
            "127.0.0.1:{0}".format(self._port), "-j",
            active_machine.getMachineDefinition().getPath(), "-vv"
        ]

    ##  Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted whne the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    ##  Perform a slice of the scene with the given set of settings.
    #
    #   \param kwargs Keyword arguments.
    #                 Valid values are:
    #                 - settings: The settings to use for the slice. The default is the active machine.
    #                 - save_gcode: True if the generated gcode should be saved, False if not. True by default.
    #                 - save_polygons: True if the generated polygon data should be saved, False if not. True by default.
    #                 - force_restart: True if the slicing process should be forcefully restarted if it is already slicing.
    #                                  If False, this method will do nothing when already slicing. True by default.
    #                 - report_progress: True if the slicing progress should be reported, False if not. Default is True.
    def slice(self, **kwargs):
        if not self._enabled:
            return

        if self._slicing:
            if not kwargs.get("force_restart", True):
                return

            self._slicing = False
            self._restart = True
            if self._process is not None:
                Logger.log("d", "Killing engine process")
                try:
                    self._process.terminate()
                except:  # terminating a process that is already terminating causes an exception, silently ignore this.
                    pass
            self.slicingCancelled.emit()
            return

        Logger.log("d", "Preparing to send slice data to engine.")
        object_groups = []
        if self._profile.getSettingValue("print_sequence") == "one_at_a_time":
            for node in OneAtATimeIterator(self._scene.getRoot()):
                temp_list = []
                children = node.getAllChildren()
                children.append(node)
                for child_node in children:
                    if type(child_node) is SceneNode and child_node.getMeshData(
                    ) and child_node.getMeshData().getVertices() is not None:
                        temp_list.append(child_node)
                object_groups.append(temp_list)
        else:
            temp_list = []
            for node in DepthFirstIterator(self._scene.getRoot()):
                if type(node) is SceneNode and node.getMeshData(
                ) and node.getMeshData().getVertices() is not None:
                    if not getattr(node, "_outside_buildarea", False):
                        temp_list.append(node)
            if len(temp_list) == 0:
                self.processingProgress.emit(0.0)
                return
            object_groups.append(temp_list)
        #for node in DepthFirstIterator(self._scene.getRoot()):
        #    if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
        #        if not getattr(node, "_outside_buildarea", False):
        #            objects.append(node)

        if len(object_groups) == 0:
            if self._message:
                self._message.hide()
                self._message = None
            return  #No point in slicing an empty build plate

        if kwargs.get("profile", self._profile).hasErrorValue():
            Logger.log('w', "Profile has error values. Aborting slicing")
            if self._message:
                self._message.hide()
                self._message = None
            self._message = Message(
                catalog.i18nc(
                    "@info:status",
                    "Unable to slice. Please check your setting values for errors."
                ))
            self._message.show()
            return  #No slicing if we have error values since those are by definition illegal values.
        # Remove existing layer data (if any)
        for node in DepthFirstIterator(self._scene.getRoot()):
            if type(node) is SceneNode and node.getMeshData():
                if node.callDecoration("getLayerData"):
                    Application.getInstance().getController().getScene(
                    ).getRoot().removeChild(node)
                    break
        Application.getInstance().getController().getScene().gcode_list = None
        self._slicing = True
        self.slicingStarted.emit()

        self._report_progress = kwargs.get("report_progress", True)
        if self._report_progress:
            self.processingProgress.emit(0.0)
            if not self._message:
                self._message = Message(
                    catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
                self._message.show()
            else:
                self._message.setProgress(-1)

        self._sendSettings(kwargs.get("profile", self._profile))

        self._scene.acquireLock()

        # Set the gcode as an empty list. This will be filled with strings by GCodeLayer messages.
        # This is done so the gcode can be fragmented in memory and does not need a continues memory space.
        # (AKA. This prevents MemoryErrors)
        self._save_gcode = kwargs.get("save_gcode", True)
        if self._save_gcode:
            setattr(self._scene, "gcode_list", [])

        self._save_polygons = kwargs.get("save_polygons", True)

        slice_message = Cura_pb2.Slice()

        for group in object_groups:
            group_message = slice_message.object_lists.add()
            for object in group:
                mesh_data = object.getMeshData().getTransformed(
                    object.getWorldTransformation())

                obj = group_message.objects.add()
                obj.id = id(object)

                verts = numpy.array(mesh_data.getVertices())
                verts[:, [1, 2]] = verts[:, [2, 1]]
                verts[:, 1] *= -1
                obj.vertices = verts.tostring()

                self._handlePerObjectSettings(object, obj)

            # Hack to add per-object settings also to the "MeshGroup" in CuraEngine
            # We really should come up with a better solution for this.
            self._handlePerObjectSettings(group[0], group_message)

        self._scene.releaseLock()
        Logger.log("d", "Sending data to engine for slicing.")
        self._socket.sendMessage(slice_message)

    def _onSceneChanged(self, source):
        if type(source) is not SceneNode:
            return

        if source is self._scene.getRoot():
            return

        if source.getMeshData() is None:
            return

        if source.getMeshData().getVertices() is None:
            return

        self._onChanged()

    def _onActiveProfileChanged(self):
        if self._profile:
            self._profile.settingValueChanged.disconnect(
                self._onSettingChanged)

        self._profile = Application.getInstance().getMachineManager(
        ).getActiveProfile()
        if self._profile:
            self._profile.settingValueChanged.connect(self._onSettingChanged)
            self._onChanged()

    def _onSettingChanged(self, setting):
        self._onChanged()

    def _onSlicedObjectListMessage(self, message):
        if self._save_polygons:
            if self._layer_view_active:
                job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(
                    message)
                job.start()
            else:
                self._stored_layer_data = message

    def _onProgressMessage(self, message):
        if message.amount >= 0.99:
            self._slicing = False

            if self._message:
                self._message.setProgress(100)
                self._message.hide()
                self._message = None

        if self._message:
            self._message.setProgress(round(message.amount * 100))

        if self._report_progress:
            self.processingProgress.emit(message.amount)

    def _onGCodeLayerMessage(self, message):
        if self._save_gcode:
            job = ProcessGCodeJob.ProcessGCodeLayerJob(message)
            job.start()

    def _onGCodePrefixMessage(self, message):
        if self._save_gcode:
            self._scene.gcode_list.insert(
                0, message.data.decode("utf-8", "replace"))

    def _onObjectPrintTimeMessage(self, message):
        self.printDurationMessage.emit(message.time, message.material_amount)
        self.processingProgress.emit(1.0)

    def _createSocket(self):
        super()._createSocket()

        self._socket.registerMessageType(1, Cura_pb2.Slice)
        self._socket.registerMessageType(2, Cura_pb2.SlicedObjectList)
        self._socket.registerMessageType(3, Cura_pb2.Progress)
        self._socket.registerMessageType(4, Cura_pb2.GCodeLayer)
        self._socket.registerMessageType(5, Cura_pb2.ObjectPrintTime)
        self._socket.registerMessageType(6, Cura_pb2.SettingList)
        self._socket.registerMessageType(7, Cura_pb2.GCodePrefix)

    ##  Manually triggers a reslice
    def forceSlice(self):
        self._change_timer.start()

    def _onChanged(self):
        if not self._profile:
            return

        self._change_timer.start()

    def _sendSettings(self, profile):
        msg = Cura_pb2.SettingList()
        for key, value in profile.getAllSettingValues(
                include_machine=True).items():
            s = msg.settings.add()
            s.name = key
            s.value = str(value).encode("utf-8")

        self._socket.sendMessage(msg)

    def _onBackendConnected(self):
        if self._restart:
            self._onChanged()
            self._restart = False

    def _onToolOperationStarted(self, tool):
        self._enabled = False  # Do not reslice when a tool is doing it's 'thing'

    def _onToolOperationStopped(self, tool):
        self._enabled = True  # Tool stop, start listening for changes again.
        self._onChanged()

    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "LayerView":
                self._layer_view_active = True
                if self._stored_layer_data:
                    job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(
                        self._stored_layer_data)
                    job.start()
                    self._stored_layer_data = None
            else:
                self._layer_view_active = False

    def _handlePerObjectSettings(self, node, message):
        profile = node.callDecoration("getProfile")
        if profile:
            for key, value in profile.getChangedSettingValues().items():
                setting = message.settings.add()
                setting.name = key
                setting.value = str(value).encode()

        object_settings = node.callDecoration("getAllSettingValues")
        if not object_settings:
            return

        for key, value in object_settings.items():
            setting = message.settings.add()
            setting.name = key
            setting.value = str(value).encode()

    def _onInstanceChanged(self):
        self._slicing = False
        self._restart = True
        if self._process is not None:
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()
            except:  # terminating a process that is already terminating causes an exception, silently ignore this.
                pass
        self.slicingCancelled.emit()
コード例 #37
0
ファイル: ReadFileJob.py プロジェクト: lulzbackup/uranium
class ReadFileJob(Job):
    def __init__(self, filename, handler=None):
        super().__init__()
        self._filename = filename
        self._handler = handler
        self._loading_message = None

    def getFileName(self):
        return self._filename

    def run(self):
        if self._handler is None:
            Logger.log("e", "FileHandler was not set.")
            return None
        reader = self._handler.getReaderForFile(self._filename)
        if not reader:
            result_message = Message(i18n_catalog.i18nc(
                "@info:status Don't translate the XML tag <filename>!",
                "Cannot open files of the type of <filename>{0}</filename>",
                self._filename),
                                     lifetime=0,
                                     title=i18n_catalog.i18nc(
                                         "@info:title", "Invalid File"))
            result_message.show()
            return

        # Give the plugin a chance to display a dialog before showing the loading UI
        try:
            pre_read_result = reader.preRead(self._filename)
        except:
            Logger.logException("e", "Failed to pre-read the file %s",
                                self._filename)
            pre_read_result = MeshReader.PreReadResult.failed

        if pre_read_result != MeshReader.PreReadResult.accepted:
            if pre_read_result == MeshReader.PreReadResult.failed:
                result_message = Message(i18n_catalog.i18nc(
                    "@info:status Don't translate the XML tag <filename>!",
                    "Failed to load <filename>{0}</filename>", self._filename),
                                         lifetime=0,
                                         title=i18n_catalog.i18nc(
                                             "@info:title", "Invalid File"))
                result_message.show()
            return

        self._loading_message = Message(self._filename,
                                        lifetime=0,
                                        dismissable=False,
                                        title=i18n_catalog.i18nc(
                                            "@info:title", "Loading"))
        self._loading_message.setProgress(-1)
        self._loading_message.show()

        Job.yieldThread(
        )  # Yield to any other thread that might want to do something else.

        try:
            begin_time = time.time()
            self.setResult(self._handler.readerRead(reader, self._filename))
            end_time = time.time()
            Logger.log("d", "Loading file took %0.1f seconds",
                       end_time - begin_time)
        except:
            Logger.logException("e",
                                "Exception occurred while loading file %s",
                                self._filename)
        finally:
            if self._result is None:
                self._loading_message.hide()
                result_message = Message(i18n_catalog.i18nc(
                    "@info:status Don't translate the XML tag <filename>!",
                    "Failed to load <filename>{0}</filename>", self._filename),
                                         lifetime=0,
                                         title=i18n_catalog.i18nc(
                                             "@info:title", "Invalid File"))
                result_message.show()
                return
            self._loading_message.hide()
コード例 #38
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent=None):
        super().__init__(device_id=device_id,
                         address=address,
                         properties=properties,
                         parent=parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

        self._dummy_lambdas = set()

        self._print_jobs = []

        self._monitor_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "ClusterMonitorItem.qml")
        self._control_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "ClusterControlItem.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None
        self._write_job_progress_message = None
        self._progress_message = None

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None

        self.setPriority(
            3
        )  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.",
                               "Print over network"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(
            i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}

        self._finished_jobs = []

        self._cluster_size = int(properties.get(b"cluster_size", 0))

        self._latest_reply_handler = None

    def requestWrite(self,
                     nodes: List[SceneNode],
                     file_name=None,
                     filter_by_machine=False,
                     file_handler=None,
                     **kwargs):
        self.writeStarted.emit(self)

        #Formats supported by this application (file types that we can actually write).
        if file_handler:
            file_formats = file_handler.getSupportedFileTypesWrite()
        else:
            file_formats = Application.getInstance().getMeshFileHandler(
            ).getSupportedFileTypesWrite()

        #Create a list from the supported file formats string.
        machine_file_formats = Application.getInstance(
        ).getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
        machine_file_formats = [
            file_type.strip() for file_type in machine_file_formats
        ]
        #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
        if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(
                self.firmwareVersion) >= Version("4.4"):
            machine_file_formats = ["application/x-ufp"] + machine_file_formats

        # Take the intersection between file_formats and machine_file_formats.
        format_by_mimetype = {
            format["mime_type"]: format
            for format in file_formats
        }
        file_formats = [
            format_by_mimetype[mimetype] for mimetype in machine_file_formats
        ]  #Keep them ordered according to the preference in machine_file_formats.

        if len(file_formats) == 0:
            Logger.log("e",
                       "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(
                i18n_catalog.i18nc(
                    "@info:status",
                    "There are no file formats available to write with!"))
        preferred_format = file_formats[0]

        #Just take the first file format available.
        if file_handler is not None:
            writer = file_handler.getWriterByMimeType(
                preferred_format["mime_type"])
        else:
            writer = Application.getInstance().getMeshFileHandler(
            ).getWriterByMimeType(preferred_format["mime_type"])

        #This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
        self._sending_job.send(None)  #Start the generator.

        if len(self._printers) > 1:  #We need to ask the user.
            self._spawnPrinterSelectionDialog()
            is_job_sent = True
        else:  #Just immediately continue.
            self._sending_job.send("")  #No specifically selected printer.
            is_job_sent = self._sending_job.send(None)

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                "PrintWindow.qml")
            self._printer_selection_dialog = Application.getInstance(
            ).createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self):
        return self._cluster_size

    ##  Allows the user to choose a printer to print with from the printer
    #   selection dialogue.
    #   \param target_printer The name of the printer to target.
    @pyqtSlot(str)
    def selectPrinter(self, target_printer: str = "") -> None:
        self._sending_job.send(target_printer)

    ##  Greenlet to send a job to the printer over the network.
    #
    #   This greenlet gets called asynchronously in requestWrite. It is a
    #   greenlet in order to optionally wait for selectPrinter() to select a
    #   printer.
    #   The greenlet yields exactly three times: First time None,
    #   \param writer The file writer to use to create the data.
    #   \param preferred_format A dictionary containing some information about
    #   what format to write to. This is necessary to create the correct buffer
    #   types and file extension and such.
    def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict,
                      nodes: List[SceneNode]):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Sending new jobs (temporarily) blocked, still sending the previous print job."
                ))
            self._error_message.show()
            yield  #Wait on the user to select a target printer.
            yield  #Wait for the write job to be finished.
            yield False  #Return whether this was a success or not.
            yield  #Prevent StopIteration.

        self._sending_gcode = True

        target_printer = yield  #Potentially wait on the user to select a target printer.

        # Using buffering greatly reduces the write time for many lines of gcode
        if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
            stream = io.StringIO()
        else:  #Binary mode.
            stream = io.BytesIO()

        job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])

        self._write_job_progress_message = Message(
            i18n_catalog.i18nc("@info:status", "Sending data to printer"),
            lifetime=0,
            dismissable=False,
            progress=-1,
            title=i18n_catalog.i18nc("@info:title", "Sending Data"),
            use_inactivity_timer=False)
        self._write_job_progress_message.show()

        self._dummy_lambdas = (target_printer, preferred_format, stream)
        job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)

        job.start()

        yield True  #Return that we had success!
        yield  #To prevent having to catch the StopIteration exception.

    from cura.Utils.Threading import call_on_qt_thread

    def _sendPrintJobWaitOnWriteJobFinished(self, job):
        self._write_job_progress_message.hide()

        self._progress_message = Message(
            i18n_catalog.i18nc("@info:status", "Sending data to printer"),
            lifetime=0,
            dismissable=False,
            progress=-1,
            title=i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort",
                                         i18n_catalog.i18nc(
                                             "@action:button", "Cancel"),
                                         icon=None,
                                         description="")
        self._progress_message.actionTriggered.connect(
            self._progressMessageActionTriggered)
        self._progress_message.show()

        parts = []

        target_printer, preferred_format, stream = self._dummy_lambdas

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[
                target_printer]
            parts.append(
                self._createFormPart("name=require_printer_name",
                                     bytes(target_printer, "utf-8"),
                                     "text/plain"))

        # Add user name to the print_job
        parts.append(
            self._createFormPart("name=owner",
                                 bytes(self._getUserName(), "utf-8"),
                                 "text/plain"))

        file_name = Application.getInstance().getPrintInformation(
        ).jobName + "." + preferred_format["extension"]

        output = stream.getvalue(
        )  #Either str or bytes depending on the output mode.
        if isinstance(stream, io.StringIO):
            output = output.encode("utf-8")

        parts.append(
            self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name,
                                 output))

        self._latest_reply_handler = self.postFormWithParts(
            "print_jobs/",
            parts,
            onFinished=self._onPostPrintJobFinished,
            onProgress=self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify=activePrinterChanged)
    def activePrinter(self) -> Optional[PrinterOutputModel]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
        if self._active_printer != printer:
            if self._active_printer and self._active_printer.camera:
                self._active_printer.camera.stop()
            self._active_printer = printer
            self.activePrinterChanged.emit()

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show(
                )  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total *
                                                   100)

            # If successfully sent:
            if bytes_sent == bytes_total:
                # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
                # monitor tab.
                self._success_message = Message(i18n_catalog.i18nc(
                    "@info:status",
                    "Print job was successfully sent to the printer."),
                                                lifetime=5,
                                                dismissable=True,
                                                title=i18n_catalog.i18nc(
                                                    "@info:title",
                                                    "Data Sent"))
                self._success_message.addAction("View",
                                                i18n_catalog.i18nc(
                                                    "@action:button",
                                                    "View in Monitor"),
                                                icon=None,
                                                description="")
                self._success_message.actionTriggered.connect(
                    self._successMessageActionTriggered)
                self._success_message.show()
        else:
            self._progress_message.setProgress(0)
            self._progress_message.hide()

    def _progressMessageActionTriggered(
            self,
            message_id: Optional[str] = None,
            action_id: Optional[str] = None) -> None:
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            Application.getInstance().getController().setActiveStage(
                "PrepareStage")

            # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
            # the "reply" should be disconnected
            if self._latest_reply_handler:
                self._latest_reply_handler.disconnect()
                self._latest_reply_handler = None

    def _successMessageActionTriggered(
            self,
            message_id: Optional[str] = None,
            action_id: Optional[str] = None) -> None:
        if action_id == "View":
            Application.getInstance().getController().setActiveStage(
                "MonitorStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self) -> None:
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(
            QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self) -> None:
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def printJobs(self) -> List[PrintJobOutputModel]:
        return self._print_jobs

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
        return [
            print_job for print_job in self._print_jobs
            if print_job.state == "queued"
        ]

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def activePrintJobs(self) -> List[PrintJobOutputModel]:
        return [
            print_job for print_job in self._print_jobs
            if print_job.assignedPrinter is not None
            and print_job.state != "queued"
        ]

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
        printer_count = {}
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({
                "machine_type": machine_type,
                "count": printer_count[machine_type]
            })
        return result

    @pyqtSlot(int, result=str)
    def formatDuration(self, seconds: int) -> str:
        return Duration(seconds).getDisplayString(DurationFormat.Format.Short)

    @pyqtSlot(int, result=str)
    def getTimeCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time +
                                                    time_remaining)
        return "{hour:02d}:{minute:02d}".format(
            hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result=str)
    def getDateCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time +
                                                    time_remaining)
        return (datetime_completed.strftime("%a %b ") +
                "{day}".format(day=datetime_completed.day)).upper()

    def _printJobStateChanged(self) -> None:
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [
            job for job in self._print_jobs if job.state == "wait_cleanup"
        ]

        newly_finished_jobs = [
            job for job in finished_jobs
            if job not in self._finished_jobs and job.owner == username
        ]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc(
                    "@info:status",
                    "Printer '{printer_name}' has finished printing '{job_name}'."
                    .format(printer_name=job.assignedPrinter.name,
                            job_name=job.name))
            else:
                job_completed_text = i18n_catalog.i18nc(
                    "@info:status",
                    "The print job '{job_name}' was finished.".format(
                        job_name=job.name))
            job_completed_message = Message(text=job_completed_text,
                                            title=i18n_catalog.i18nc(
                                                "@info:status",
                                                "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    def _update(self) -> None:
        if not super()._update():
            return
        self.get("printers/", onFinished=self._onGetPrintersDataFinished)
        self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for print_job_data in result:
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])

            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued":  # Print job should be assigned to a printer.
                if print_job.state in ["failed", "finished", "aborted"]:
                    # Print job was already completed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(
                        print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [
            print_job for print_job in self._print_jobs
            if print_job not in print_jobs_seen
        ]

        for removed_job in removed_jobs:
            job_list_changed |= self._removeJob(removed_job)

        if job_list_changed:
            self.printJobsChanged.emit(
            )  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [
            printer for printer in self._printers
            if printer not in printers_seen
        ]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
        printer = PrinterOutputModel(
            output_controller=ClusterUM3PrinterOutputController(self),
            number_of_extruders=self._number_of_extruders)
        printer.setCamera(
            NetworkCamera("http://" + data["ip_address"] +
                          ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel:
        print_job = PrintJobOutputModel(
            output_controller=ClusterUM3PrinterOutputController(self),
            key=data["uuid"],
            name=data["name"])
        print_job.stateChanged.connect(self._printJobStateChanged)
        self._print_jobs.append(print_job)
        return print_job

    def _updatePrintJob(self, print_job: PrintJobOutputModel,
                        data: Dict) -> None:
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        print_job.updateState(data["status"])
        print_job.updateOwner(data["owner"])

    def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None:
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[
            data["uuid"]] = data["unique_name"]

        definitions = ContainerRegistry.getInstance().findDefinitionContainers(
            name=data["machine_variant"])
        if not definitions:
            Logger.log("w", "Unable to find definition for machine variant %s",
                       data["machine_variant"])
            return

        machine_definition = definitions[0]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])

        # Do not store the buildplate information that comes from connect if the current printer has not buildplate information
        if "build_plate" in data and machine_definition.getMetaDataEntry(
                "has_variant_buildplates", False):
            printer.updateBuildplateName(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

        for index in range(0, self._number_of_extruders):
            extruder = printer.extruders[index]
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                break

            extruder.updateHotendID(extruder_data.get("print_core_id", ""))

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data[
                    "guid"]:
                containers = ContainerRegistry.getInstance(
                ).findInstanceContainers(type="material",
                                         GUID=material_data["guid"])
                if containers:
                    color = containers[0].getMetaDataEntry("color_code")
                    brand = containers[0].getMetaDataEntry("brand")
                    material_type = containers[0].getMetaDataEntry("material")
                    name = containers[0].getName()
                else:
                    Logger.log(
                        "w",
                        "Unable to find material with guid {guid}. Using data as provided by cluster"
                        .format(guid=material_data["guid"]))
                    color = material_data["color"]
                    brand = material_data["brand"]
                    material_type = material_data["material"]
                    name = "Empty" if material_data[
                        "material"] == "empty" else "Unknown"

                material = MaterialOutputModel(guid=material_data["guid"],
                                               type=material_type,
                                               brand=brand,
                                               color=color,
                                               name=name)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job: PrintJobOutputModel):
        if job not in self._print_jobs:
            return False

        if job.assignedPrinter:
            job.assignedPrinter.updateActivePrintJob(None)
            job.stateChanged.disconnect(self._printJobStateChanged)
        self._print_jobs.remove(job)

        return True

    def _removePrinter(self, printer: PrinterOutputModel):
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()
コード例 #39
0
ファイル: SliceInfo.py プロジェクト: hroncok/Cura
class SliceInfo(Extension):
    info_url = "https://stats.ultimaker.com/api/cura"

    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info", False)

        if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."), lifetime = 0, dismissable = False)
            self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
            self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
            self.send_slice_info_message.show()

    def messageActionTriggered(self, message_id, action_id):
        self.send_slice_info_message.hide()
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)

    def _onWriteStarted(self, output_device):
        try:
            if not Preferences.getInstance().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return  # Do nothing, user does not want to send data

            global_container_stack = Application.getInstance().getGlobalContainerStack()
            print_information = Application.getInstance().getPrintInformation()

            data = dict()  # The data that we're going to submit.
            data["time_stamp"] = time.time()
            data["schema_version"] = 0
            data["cura_version"] = Application.getInstance().getVersion()

            active_mode = Preferences.getInstance().getValue("cura/active_mode")
            if active_mode == 0:
                data["active_mode"] = "recommended"
            else:
                data["active_mode"] = "custom"

            definition_changes = global_container_stack.definitionChanges
            machine_settings_changed_by_user = False
            if definition_changes.getId() != "empty":
                # Now a definition_changes container will always be created for a stack,
                # so we also need to check if there is any instance in the definition_changes container
                if definition_changes.getAllKeys():
                    machine_settings_changed_by_user = True

            data["machine_settings_changed_by_user"] = machine_settings_changed_by_user
            data["language"] = Preferences.getInstance().getValue("general/language")
            data["os"] = {"type": platform.system(), "version": platform.version()}

            data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")}

            data["extruders"] = []
            extruder_count = len(global_container_stack.extruders)
            extruders = []
            if extruder_count > 1:
                extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
                extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))

            if not extruders:
                extruders = [global_container_stack]

            for extruder in extruders:
                extruder_dict = dict()
                extruder_dict["active"] = ExtruderManager.getInstance().getActiveExtruderStack() == extruder
                extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""),
                                             "type": extruder.material.getMetaData().get("material", ""),
                                             "brand": extruder.material.getMetaData().get("brand", "")
                                             }
                extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))]
                extruder_dict["variant"] = extruder.variant.getName()
                extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

                extruder_settings = dict()
                extruder_settings["wall_line_count"] = extruder.getProperty("wall_line_count", "value")
                extruder_settings["retraction_enable"] = extruder.getProperty("retraction_enable", "value")
                extruder_settings["infill_sparse_density"] = extruder.getProperty("infill_sparse_density", "value")
                extruder_settings["infill_pattern"] = extruder.getProperty("infill_pattern", "value")
                extruder_settings["gradual_infill_steps"] = extruder.getProperty("gradual_infill_steps", "value")
                extruder_settings["default_material_print_temperature"] = extruder.getProperty("default_material_print_temperature", "value")
                extruder_settings["material_print_temperature"] = extruder.getProperty("material_print_temperature", "value")
                extruder_dict["extruder_settings"] = extruder_settings
                data["extruders"].append(extruder_dict)

            data["quality_profile"] = global_container_stack.quality.getMetaData().get("quality_type")

            data["models"] = []
            # Listing all files placed on the build plate
            for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()):
                if node.callDecoration("isSliceable"):
                    model = dict()
                    model["hash"] = node.getMeshData().getHash()
                    bounding_box = node.getBoundingBox()
                    model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
                                                         "y": bounding_box.minimum.y,
                                                         "z": bounding_box.minimum.z},
                                             "maximum": {"x": bounding_box.maximum.x,
                                                         "y": bounding_box.maximum.y,
                                                         "z": bounding_box.maximum.z}}
                    model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")}
                    extruder_position = node.callDecoration("getActiveExtruderPosition")
                    model["extruder"] = 0 if extruder_position is None else int(extruder_position)

                    model_settings = dict()
                    model_stack = node.callDecoration("getStack")
                    if model_stack:
                        model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
                        model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))

                        # Mesh modifiers;
                        model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
                        model_settings["cutting_mesh"] = model_stack.getProperty("cutting_mesh", "value")
                        model_settings["support_mesh"] = model_stack.getProperty("support_mesh", "value")
                        model_settings["anti_overhang_mesh"] = model_stack.getProperty("anti_overhang_mesh", "value")

                        model_settings["wall_line_count"] = model_stack.getProperty("wall_line_count", "value")
                        model_settings["retraction_enable"] = model_stack.getProperty("retraction_enable", "value")

                        # Infill settings
                        model_settings["infill_sparse_density"] = model_stack.getProperty("infill_sparse_density", "value")
                        model_settings["infill_pattern"] = model_stack.getProperty("infill_pattern", "value")
                        model_settings["gradual_infill_steps"] = model_stack.getProperty("gradual_infill_steps", "value")

                    model["model_settings"] = model_settings

                    data["models"].append(model)

            print_times = print_information.printTimesPerFeature
            data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "total": int(print_information.currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))}

            print_settings = dict()
            print_settings["layer_height"] = global_container_stack.getProperty("layer_height", "value")

            # Support settings
            print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
            print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))

            # Platform adhesion settings
            print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")

            # Shell settings
            print_settings["wall_line_count"] = global_container_stack.getProperty("wall_line_count", "value")
            print_settings["retraction_enable"] = global_container_stack.getProperty("retraction_enable", "value")

            # Prime tower settings
            print_settings["prime_tower_enable"] = global_container_stack.getProperty("prime_tower_enable", "value")

            # Infill settings
            print_settings["infill_sparse_density"] = global_container_stack.getProperty("infill_sparse_density", "value")
            print_settings["infill_pattern"] = global_container_stack.getProperty("infill_pattern", "value")
            print_settings["gradual_infill_steps"] = global_container_stack.getProperty("gradual_infill_steps", "value")

            print_settings["print_sequence"] = global_container_stack.getProperty("print_sequence", "value")

            data["print_settings"] = print_settings

            # Send the name of the output device type that is used.
            data["output_to"] = type(output_device).__name__

            # Convert data to bytes
            binary_data = json.dumps(data).encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.
コード例 #40
0
class RotateTool(Tool):
    def __init__(self):
        super().__init__()
        self._handle = RotateToolHandle.RotateToolHandle()

        self._snap_rotation = True
        self._snap_angle = math.radians(15)

        self._angle = None
        self._angle_update_time = None

        self._shortcut_key = Qt.Key_Z

        self._progress_message = None
        self._iterations = 0
        self._total_iterations = 0
        self._rotating = False
        self.setExposedProperties("ToolHint", "RotationSnap",
                                  "RotationSnapAngle")
        self._saved_node_positions = []

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

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            # Snap is toggled when pressing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            # Snap is "toggled back" when releasing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(
        ):
            # Start a rotate 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 self._handle.isAxis(id):
                self.setLockedAxis(id)
            else:
                # Not clicked on an axis: do nothing.
                return False

            handle_position = self._handle.getWorldPosition()

            # Save the current positions of the node, as we want to rotate around their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getPosition()))

            if id == ToolHandle.XAxis:
                self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x))
            elif id == ToolHandle.YAxis:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))
            elif self._locked_axis == ToolHandle.ZAxis:
                self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z))
            else:
                self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))

            self.setDragStart(event.x, event.y)
            self._rotating = False
            self._angle = 0

        if event.type == Event.MouseMoveEvent:
            # Perform a rotate operation
            if not self.getDragPlane():
                return False

            if not self.getDragStart():
                self.setDragStart(event.x, event.y)
                if not self.getDragStart():  #May have set it to None.
                    return False

            if not self._rotating:
                self._rotating = True
                self.operationStarted.emit(self)

            handle_position = self._handle.getWorldPosition()

            drag_start = (self.getDragStart() - handle_position).normalized()
            drag_position = self.getDragPosition(event.x, event.y)
            if not drag_position:
                return
            drag_end = (drag_position - handle_position).normalized()

            try:
                angle = math.acos(drag_start.dot(drag_end))
            except ValueError:
                angle = 0

            if self._snap_rotation:
                angle = int(angle / self._snap_angle) * self._snap_angle
                if angle == 0:
                    return

            rotation = None
            if self.getLockedAxis() == ToolHandle.XAxis:
                direction = 1 if Vector.Unit_X.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_X)
            elif self.getLockedAxis() == ToolHandle.YAxis:
                direction = 1 if Vector.Unit_Y.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Y)
            elif self.getLockedAxis() == ToolHandle.ZAxis:
                direction = 1 if Vector.Unit_Z.dot(
                    drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle,
                                                    Vector.Unit_Z)
            else:
                direction = -1

            # 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._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time
                self._angle += direction * angle
                self.propertyChanged.emit()

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(
                        RotateOperation(node,
                                        rotation,
                                        rotate_around_point=position))
                op.push()

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

        if event.type == Event.MouseReleaseEvent:
            # Finish a rotate operation
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(None)
                self._angle = None
                self.propertyChanged.emit()
                if self._rotating:
                    self.operationStopped.emit(self)
                return True

    ##  Return a formatted angle of the current rotate operation
    #
    #   \return type(String) fully formatted string showing the angle by which the mesh(es) are rotated
    def getToolHint(self):
        return "%d°" % round(math.degrees(
            self._angle)) if self._angle else None

    ##  Get the state of the "snap rotation to N-degree increments" option
    #
    #   \return type(Boolean)
    def getRotationSnap(self):
        return self._snap_rotation

    ##  Set the state of the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Boolean)
    def setRotationSnap(self, snap):
        if snap != self._snap_rotation:
            self._snap_rotation = snap
            self.propertyChanged.emit()

    ##  Get the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \return type(Number)
    def getRotationSnapAngle(self):
        return self._snap_angle

    ##  Set the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Number)
    def setRotationSnapAngle(self, angle):
        if angle != self._snap_angle:
            self._snap_angle = angle
            self.propertyChanged.emit()

    ##  Reset the orientation of the mesh(es) to their original orientation(s)
    def resetRotation(self):
        Selection.applyOperation(SetTransformOperation, None, Quaternion(),
                                 None)

    ##  Initialise and start a LayFlatOperation
    #
    #   Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project
    def layFlat(self):
        self.operationStarted.emit(self)
        self._progress_message = Message("Laying object flat on buildplate...",
                                         lifetime=0,
                                         dismissable=False,
                                         title="Object Rotation")
        self._progress_message.setProgress(0)

        self._iterations = 0
        self._total_iterations = 0
        for selected_object in Selection.getAllSelectedObjects():
            self._layObjectFlat(selected_object)

        self._progress_message.show()

        operations = Selection.applyOperation(LayFlatOperation)
        for op in operations:
            op.progress.connect(self._layFlatProgress)

        job = LayFlatJob(operations)
        job.finished.connect(self._layFlatFinished)
        job.start()

    ##  Lays the given object flat. The given object can be a group or not.
    def _layObjectFlat(self, selected_object):
        if not selected_object.callDecoration("isGroup"):
            self._total_iterations += len(
                selected_object.getMeshDataTransformed().getVertices()) * 2
        else:
            for child in selected_object.getChildren():
                self._layObjectFlat(child)

    ##  Called while performing the LayFlatOperation so progress can be shown
    #
    #   Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications,
    #   \param iterations type(int) number of iterations performed since the last callback
    def _layFlatProgress(self, iterations):
        self._iterations += iterations
        self._progress_message.setProgress(100 * self._iterations /
                                           self._total_iterations)

    ##  Called when the LayFlatJob is done running all of its LayFlatOperations
    #
    #   \param job type(LayFlatJob)
    def _layFlatFinished(self, job):
        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self.operationStopped.emit(self)
コード例 #41
0
ファイル: SliceInfo.py プロジェクト: TinkerGnome/Cura
class SliceInfo(QObject, Extension):
    info_url = "https://stats.ultimaker.com/api/cura"

    def __init__(self, parent = None):
        QObject.__init__(self, parent)
        Extension.__init__(self)

        self._application = Application.getInstance()

        self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
        self._application.getPreferences().addPreference("info/send_slice_info", True)
        self._application.getPreferences().addPreference("info/asked_send_slice_info", False)

        self._more_info_dialog = None
        self._example_data_content = None

        self._application.initializationFinished.connect(self._onAppInitialized)

    def _onAppInitialized(self):
        # DO NOT read any preferences values in the constructor because at the time plugins are created, no version
        # upgrade has been performed yet because version upgrades are plugins too!
        if not self._application.getPreferences().getValue("info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
                                                   lifetime = 0,
                                                   dismissable = False,
                                                   title = catalog.i18nc("@info:title", "Collecting Data"))

            self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None,
                                                   description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK)

            self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
                                                   description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
            self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
            self.send_slice_info_message.show()

        if self._more_info_dialog is None:
            self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")

    ##  Perform action based on user input.
    #   Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
    def messageActionTriggered(self, message_id, action_id):
        Application.getInstance().getPreferences().setValue("info/asked_send_slice_info", True)
        if action_id == "MoreInfo":
            self.showMoreInfoDialog()
        self.send_slice_info_message.hide()

    def showMoreInfoDialog(self):
        if self._more_info_dialog is None:
            self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
        self._more_info_dialog.open()

    def _createDialog(self, qml_name):
        Logger.log("d", "Creating dialog [%s]", qml_name)
        file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
        dialog = Application.getInstance().createQmlComponent(file_path, {"manager": self})
        return dialog

    @pyqtSlot(result = str)
    def getExampleData(self) -> Optional[str]:
        if self._example_data_content is None:
            plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
            if not plugin_path:
                Logger.log("e", "Could not get plugin path!", self.getPluginId())
                return None
            file_path = os.path.join(plugin_path, "example_data.json")
            if file_path:
                with open(file_path, "r", encoding = "utf-8") as f:
                    self._example_data_content = f.read()
        return self._example_data_content

    @pyqtSlot(bool)
    def setSendSliceInfo(self, enabled: bool):
        Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)

    def _getUserModifiedSettingKeys(self) -> list:
        from cura.CuraApplication import CuraApplication
        application = cast(CuraApplication, Application.getInstance())
        machine_manager = application.getMachineManager()
        global_stack = machine_manager.activeMachine

        user_modified_setting_keys = set()  # type: Set[str]

        for stack in [global_stack] + list(global_stack.extruders.values()):
            # Get all settings in user_changes and quality_changes
            all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys()
            user_modified_setting_keys |= all_keys

        return list(sorted(user_modified_setting_keys))

    def _onWriteStarted(self, output_device):
        try:
            if not Application.getInstance().getPreferences().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return  # Do nothing, user does not want to send data

            from cura.CuraApplication import CuraApplication
            application = cast(CuraApplication, Application.getInstance())
            machine_manager = application.getMachineManager()
            print_information = application.getPrintInformation()

            global_stack = machine_manager.activeMachine

            data = dict()  # The data that we're going to submit.
            data["time_stamp"] = time.time()
            data["schema_version"] = 0
            data["cura_version"] = application.getVersion()

            active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode")
            if active_mode == 0:
                data["active_mode"] = "recommended"
            else:
                data["active_mode"] = "custom"

            definition_changes = global_stack.definitionChanges
            machine_settings_changed_by_user = False
            if definition_changes.getId() != "empty":
                # Now a definition_changes container will always be created for a stack,
                # so we also need to check if there is any instance in the definition_changes container
                if definition_changes.getAllKeys():
                    machine_settings_changed_by_user = True

            data["machine_settings_changed_by_user"] = machine_settings_changed_by_user
            data["language"] = Application.getInstance().getPreferences().getValue("general/language")
            data["os"] = {"type": platform.system(), "version": platform.version()}

            data["active_machine"] = {"definition_id": global_stack.definition.getId(),
                                      "manufacturer": global_stack.definition.getMetaDataEntry("manufacturer", "")}

            # add extruder specific data to slice info
            data["extruders"] = []
            extruders = list(global_stack.extruders.values())
            extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))

            for extruder in extruders:
                extruder_dict = dict()
                extruder_dict["active"] = machine_manager.activeStack == extruder
                extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""),
                                             "type": extruder.material.getMetaData().get("material", ""),
                                             "brand": extruder.material.getMetaData().get("brand", "")
                                             }
                extruder_position = int(extruder.getMetaDataEntry("position", "0"))
                if len(print_information.materialLengths) > extruder_position:
                    extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
                extruder_dict["variant"] = extruder.variant.getName()
                extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

                extruder_settings = dict()
                extruder_settings["wall_line_count"] = extruder.getProperty("wall_line_count", "value")
                extruder_settings["retraction_enable"] = extruder.getProperty("retraction_enable", "value")
                extruder_settings["infill_sparse_density"] = extruder.getProperty("infill_sparse_density", "value")
                extruder_settings["infill_pattern"] = extruder.getProperty("infill_pattern", "value")
                extruder_settings["gradual_infill_steps"] = extruder.getProperty("gradual_infill_steps", "value")
                extruder_settings["default_material_print_temperature"] = extruder.getProperty("default_material_print_temperature", "value")
                extruder_settings["material_print_temperature"] = extruder.getProperty("material_print_temperature", "value")
                extruder_dict["extruder_settings"] = extruder_settings
                data["extruders"].append(extruder_dict)

            data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type")

            data["user_modified_setting_keys"] = self._getUserModifiedSettingKeys()

            data["models"] = []
            # Listing all files placed on the build plate
            for node in DepthFirstIterator(application.getController().getScene().getRoot()):
                if node.callDecoration("isSliceable"):
                    model = dict()
                    model["hash"] = node.getMeshData().getHash()
                    bounding_box = node.getBoundingBox()
                    model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
                                                         "y": bounding_box.minimum.y,
                                                         "z": bounding_box.minimum.z},
                                             "maximum": {"x": bounding_box.maximum.x,
                                                         "y": bounding_box.maximum.y,
                                                         "z": bounding_box.maximum.z}}
                    model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")}
                    extruder_position = node.callDecoration("getActiveExtruderPosition")
                    model["extruder"] = 0 if extruder_position is None else int(extruder_position)

                    model_settings = dict()
                    model_stack = node.callDecoration("getStack")
                    if model_stack:
                        model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
                        model_settings["support_extruder_nr"] = int(model_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))

                        # Mesh modifiers;
                        model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
                        model_settings["cutting_mesh"] = model_stack.getProperty("cutting_mesh", "value")
                        model_settings["support_mesh"] = model_stack.getProperty("support_mesh", "value")
                        model_settings["anti_overhang_mesh"] = model_stack.getProperty("anti_overhang_mesh", "value")

                        model_settings["wall_line_count"] = model_stack.getProperty("wall_line_count", "value")
                        model_settings["retraction_enable"] = model_stack.getProperty("retraction_enable", "value")

                        # Infill settings
                        model_settings["infill_sparse_density"] = model_stack.getProperty("infill_sparse_density", "value")
                        model_settings["infill_pattern"] = model_stack.getProperty("infill_pattern", "value")
                        model_settings["gradual_infill_steps"] = model_stack.getProperty("gradual_infill_steps", "value")

                    model["model_settings"] = model_settings

                    data["models"].append(model)

            print_times = print_information.printTimes()
            data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),
                                   "total": int(print_information.currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))}

            print_settings = dict()
            print_settings["layer_height"] = global_stack.getProperty("layer_height", "value")

            # Support settings
            print_settings["support_enabled"] = global_stack.getProperty("support_enable", "value")
            print_settings["support_extruder_nr"] = int(global_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))

            # Platform adhesion settings
            print_settings["adhesion_type"] = global_stack.getProperty("adhesion_type", "value")

            # Shell settings
            print_settings["wall_line_count"] = global_stack.getProperty("wall_line_count", "value")
            print_settings["retraction_enable"] = global_stack.getProperty("retraction_enable", "value")

            # Prime tower settings
            print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")

            # Infill settings
            print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")
            print_settings["infill_pattern"] = global_stack.getProperty("infill_pattern", "value")
            print_settings["gradual_infill_steps"] = global_stack.getProperty("gradual_infill_steps", "value")

            print_settings["print_sequence"] = global_stack.getProperty("print_sequence", "value")

            data["print_settings"] = print_settings

            # Send the name of the output device type that is used.
            data["output_to"] = type(output_device).__name__

            # Convert data to bytes
            binary_data = json.dumps(data).encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.
コード例 #42
0
 def hideMessage(self, message: Message) -> None:
     with self._message_lock:
         if message in self._visible_messages:
             message.hide(send_signal = False)  # we're in handling hideMessageSignal so we don't want to resend it
             self._visible_messages.remove(message)
             self.visibleMessageRemoved.emit(message)
コード例 #43
0
class ProcessSlicedObjectListJob(Job):
    def __init__(self, message):
        super().__init__()
        self._message = message
        self._scene = Application.getInstance().getController().getScene()
        self._progress = None
        self._abort_requested = False

    ##  Aborts the processing of layers.
    #
    #   This abort is made on a best-effort basis, meaning that the actual
    #   job thread will check once in a while to see whether an abort is
    #   requested and then stop processing by itself. There is no guarantee
    #   that the abort will stop the job any time soon or even at all.
    def abort(self):
        self._abort_requested = True

    def run(self):
        if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
            self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
            self._progress.show()
            Job.yieldThread()
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)

        object_id_map = {}
        new_node = SceneNode()
        ## Put all nodes in a dictionary identified by ID
        for node in DepthFirstIterator(self._scene.getRoot()):
            if type(node) is SceneNode and node.getMeshData():
                if node.callDecoration("getLayerData"):
                    self._scene.getRoot().removeChild(node)
                else:
                    object_id_map[id(node)] = node
            Job.yieldThread()
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        settings = Application.getInstance().getMachineManager().getWorkingProfile()

        mesh = MeshData()
        layer_data = LayerData.LayerData()

        layer_count = 0
        for i in range(self._message.repeatedMessageCount("objects")):
            layer_count += self._message.getRepeatedMessage("objects", i).repeatedMessageCount("layers")

        current_layer = 0
        for object_position in range(self._message.repeatedMessageCount("objects")):
            current_object = self._message.getRepeatedMessage("objects", object_position)
            try:
                node = object_id_map[current_object.id]
            except KeyError:
                continue

            for l in range(current_object.repeatedMessageCount("layers")):
                layer = current_object.getRepeatedMessage("layers", l)

                layer_data.addLayer(layer.id)
                layer_data.setLayerHeight(layer.id, layer.height)
                layer_data.setLayerThickness(layer.id, layer.thickness)

                for p in range(layer.repeatedMessageCount("polygons")):
                    polygon = layer.getRepeatedMessage("polygons", p)

                    points = numpy.fromstring(polygon.points, dtype="i8") # Convert bytearray to numpy array
                    points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                    # Create a new 3D-array, copy the 2D points over and insert the right height.
                    # This uses manual array creation + copy rather than numpy.insert since this is
                    # faster.
                    new_points = numpy.empty((len(points), 3), numpy.float32)
                    new_points[:,0] = points[:,0]
                    new_points[:,1] = layer.height
                    new_points[:,2] = -points[:,1]

                    new_points /= 1000

                    layer_data.addPolygon(layer.id, polygon.type, new_points, polygon.line_width)
                    Job.yieldThread()
                Job.yieldThread()
                current_layer += 1
                progress = (current_layer / layer_count) * 100
                # TODO: Rebuild the layer data mesh once the layer has been processed.
                # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.

                if self._abort_requested:
                    if self._progress:
                        self._progress.hide()
                    return
                if self._progress:
                    self._progress.setProgress(progress)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data
        layer_data.build()

        if self._abort_requested:
            if self._progress:
                self._progress.hide()
            return

        #Add layerdata decorator to scene node to indicate that the node has layerdata
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_data)
        new_node.addDecorator(decorator)

        new_node.setMeshData(mesh)
        new_node.setParent(self._scene.getRoot()) #Note: After this we can no longer abort!

        if not settings.getSettingValue("machine_center_is_zero"):
            new_node.setPosition(Vector(-settings.getSettingValue("machine_width") / 2, 0.0, settings.getSettingValue("machine_depth") / 2))

        if self._progress:
            self._progress.setProgress(100)

        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "LayerView":
            view.resetLayerData()

        if self._progress:
            self._progress.hide()

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
                if not self._progress:
                    self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
                if self._progress.getProgress() != 100:
                    self._progress.show()
            else:
                if self._progress:
                    self._progress.hide()
コード例 #44
0
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent=None):
        super().__init__(device_id=device_id,
                         address=address,
                         properties=properties,
                         parent=parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

        self._print_jobs = []

        self._monitor_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "ClusterMonitorItem.qml")
        self._control_view_qml_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            "ClusterControlItem.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None
        self._progress_message = None

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None

        self.setPriority(
            3
        )  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(
            i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.",
                               "Print over network"))
        self.setDescription(
            i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(
            i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}

        self._finished_jobs = []

        self._cluster_size = int(properties.get(b"cluster_size", 0))

    def requestWrite(self,
                     nodes,
                     file_name=None,
                     filter_by_machine=False,
                     file_handler=None,
                     **kwargs):
        self.writeStarted.emit(self)

        gcode_dict = getattr(
            Application.getInstance().getController().getScene(), "gcode_dict",
            [])
        active_build_plate_id = Application.getInstance().getBuildPlateModel(
        ).activeBuildPlate
        gcode_list = gcode_dict[active_build_plate_id]

        if not gcode_list:
            # Unable to find g-code. Nothing to send
            return

        self._gcode = gcode_list

        if len(self._printers) > 1:
            self._spawnPrinterSelectionDialog()
        else:
            self.sendPrintJob()

        # Notify the UI that a switch to the print monitor should happen
        Application.getInstance().getController().setActiveStage(
            "MonitorStage")

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                "PrintWindow.qml")
            self._printer_selection_dialog = Application.getInstance(
            ).createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self):
        return self._cluster_size

    @pyqtSlot()
    @pyqtSlot(str)
    def sendPrintJob(self, target_printer=""):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc(
                    "@info:status",
                    "Sending new jobs (temporarily) blocked, still sending the previous print job."
                ))
            self._error_message.show()
            return

        self._sending_gcode = True

        self._progress_message = Message(
            i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0,
            False, -1, i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction(
            "Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
        self._progress_message.actionTriggered.connect(
            self._progressMessageActionTriggered)
        self._progress_message.show()

        compressed_gcode = self._compressGCode()
        if compressed_gcode is None:
            # Abort was called.
            return

        parts = []

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[
                target_printer]
            parts.append(
                self._createFormPart("name=require_printer_name",
                                     bytes(target_printer, "utf-8"),
                                     "text/plain"))

        # Add user name to the print_job
        parts.append(
            self._createFormPart("name=owner",
                                 bytes(self._getUserName(), "utf-8"),
                                 "text/plain"))

        file_name = "%s.gcode.gz" % Application.getInstance(
        ).getPrintInformation().jobName

        parts.append(
            self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name,
                                 compressed_gcode))

        self.postFormWithParts("print_jobs/",
                               parts,
                               onFinished=self._onPostPrintJobFinished,
                               onProgress=self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify=activePrinterChanged)
    def activePrinter(self) -> Optional["PrinterOutputModel"]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer):
        if self._active_printer != printer:
            if self._active_printer and self._active_printer.camera:
                self._active_printer.camera.stop()
            self._active_printer = printer
            self.activePrinterChanged.emit()

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show(
                )  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total *
                                                   100)
        else:
            self._progress_message.setProgress(0)
            self._progress_message.hide()

    def _progressMessageActionTriggered(self, message_id=None, action_id=None):
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            Application.getInstance().getController().setActiveStage(
                "PrepareStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self):
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(
            QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self):
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def printJobs(self):
        return self._print_jobs

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def queuedPrintJobs(self):
        return [
            print_job for print_job in self._print_jobs
            if print_job.assignedPrinter is None
        ]

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def activePrintJobs(self):
        return [
            print_job for print_job in self._print_jobs
            if print_job.assignedPrinter is not None
        ]

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def connectedPrintersTypeCount(self):
        printer_count = {}
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({
                "machine_type": machine_type,
                "count": printer_count[machine_type]
            })
        return result

    @pyqtSlot(int, result=str)
    def formatDuration(self, seconds):
        return Duration(seconds).getDisplayString(DurationFormat.Format.Short)

    @pyqtSlot(int, result=str)
    def getTimeCompleted(self, time_remaining):
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time +
                                                    time_remaining)
        return "{hour:02d}:{minute:02d}".format(
            hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result=str)
    def getDateCompleted(self, time_remaining):
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time +
                                                    time_remaining)
        return (datetime_completed.strftime("%a %b ") +
                "{day}".format(day=datetime_completed.day)).upper()

    def _printJobStateChanged(self):
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [
            job for job in self._print_jobs if job.state == "wait_cleanup"
        ]

        newly_finished_jobs = [
            job for job in finished_jobs
            if job not in self._finished_jobs and job.owner == username
        ]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc(
                    "@info:status",
                    "Printer '{printer_name}' has finished printing '{job_name}'."
                    .format(printer_name=job.assignedPrinter.name,
                            job_name=job.name))
            else:
                job_completed_text = i18n_catalog.i18nc(
                    "@info:status",
                    "The print job '{job_name}' was finished.".format(
                        job_name=job.name))
            job_completed_message = Message(text=job_completed_text,
                                            title=i18n_catalog.i18nc(
                                                "@info:status",
                                                "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    def _update(self):
        if not super()._update():
            return
        self.get("printers/", onFinished=self._onGetPrintersDataFinished)
        self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply):
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for print_job_data in result:
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])

            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued":  # Print job should be assigned to a printer.
                if print_job.state == "failed":
                    # Print job was failed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(
                        print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [
            print_job for print_job in self._print_jobs
            if print_job not in print_jobs_seen
        ]

        for removed_job in removed_jobs:
            job_list_changed |= self._removeJob(removed_job)

        if job_list_changed:
            self.printJobsChanged.emit(
            )  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply):
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [
            printer for printer in self._printers
            if printer not in printers_seen
        ]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data):
        printer = PrinterOutputModel(
            output_controller=ClusterUM3PrinterOutputController(self),
            number_of_extruders=self._number_of_extruders)
        printer.setCamera(
            NetworkCamera("http://" + data["ip_address"] +
                          ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data):
        print_job = PrintJobOutputModel(
            output_controller=ClusterUM3PrinterOutputController(self),
            key=data["uuid"],
            name=data["name"])
        print_job.stateChanged.connect(self._printJobStateChanged)
        self._print_jobs.append(print_job)
        return print_job

    def _updatePrintJob(self, print_job, data):
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        print_job.updateState(data["status"])
        print_job.updateOwner(data["owner"])

    def _updatePrinter(self, printer, data):
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[
            data["uuid"]] = data["unique_name"]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

        for index in range(0, self._number_of_extruders):
            extruder = printer.extruders[index]
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                break

            extruder.updateHotendID(extruder_data.get("print_core_id", ""))

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data[
                    "guid"]:
                containers = ContainerRegistry.getInstance(
                ).findInstanceContainers(type="material",
                                         GUID=material_data["guid"])
                if containers:
                    color = containers[0].getMetaDataEntry("color_code")
                    brand = containers[0].getMetaDataEntry("brand")
                    material_type = containers[0].getMetaDataEntry("material")
                    name = containers[0].getName()
                else:
                    Logger.log(
                        "w",
                        "Unable to find material with guid {guid}. Using data as provided by cluster"
                        .format(guid=material_data["guid"]))
                    color = material_data["color"]
                    brand = material_data["brand"]
                    material_type = material_data["material"]
                    name = "Unknown"

                material = MaterialOutputModel(guid=material_data["guid"],
                                               type=material_type,
                                               brand=brand,
                                               color=color,
                                               name=name)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job):
        if job not in self._print_jobs:
            return False

        if job.assignedPrinter:
            job.assignedPrinter.updateActivePrintJob(None)
            job.stateChanged.disconnect(self._printJobStateChanged)
        self._print_jobs.remove(job)

        return True

    def _removePrinter(self, printer):
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()
コード例 #45
0
ファイル: BuildVolume.py プロジェクト: mifga/Cura
class BuildVolume(SceneNode):
    VolumeOutlineColor = Color(12, 169, 227, 255)
    XAxisColor = Color(255, 0, 0, 255)
    YAxisColor = Color(0, 0, 255, 255)
    ZAxisColor = Color(0, 255, 0, 255)

    raftThicknessChanged = Signal()

    def __init__(self, parent = None):
        super().__init__(parent)

        self._width = 0
        self._height = 0
        self._depth = 0
        self._shape = ""

        self._shader = None

        self._origin_mesh = None
        self._origin_line_length = 20
        self._origin_line_width = 0.5

        self._grid_mesh = None
        self._grid_shader = None

        self._disallowed_areas = []
        self._disallowed_area_mesh = None

        self._error_areas = []
        self._error_mesh = None

        self.setCalculateBoundingBox(False)
        self._volume_aabb = None

        self._raft_thickness = 0.0
        self._adhesion_type = None
        self._platform = Platform(self)

        self._global_container_stack = None
        Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
        self._onStackChanged()

        self._has_errors = False
        Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)

        #Objects loaded at the moment. We are connected to the property changed events of these objects.
        self._scene_objects = set()

        self._change_timer = QTimer()
        self._change_timer.setInterval(100)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self._onChangeTimerFinished)

        self._build_volume_message = Message(catalog.i18nc("@info:status",
            "The build volume height has been reduced due to the value of the"
            " \"Print Sequence\" setting to prevent the gantry from colliding"
            " with printed models."))

        # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
        # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
        # Therefore this works.
        Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
        # This should also ways work, and it is semantically more correct,
        # but it does not update the disallowed areas after material change
        Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)

    def _onSceneChanged(self, source):
        if self._global_container_stack:
            self._change_timer.start()

    def _onChangeTimerFinished(self):
        root = Application.getInstance().getController().getScene().getRoot()
        new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode)
        if new_scene_objects != self._scene_objects:
            for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
                node.decoratorsChanged.connect(self._onNodeDecoratorChanged)
            for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
                per_mesh_stack = node.callDecoration("getStack")
                if per_mesh_stack:
                    per_mesh_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
                active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
                if active_extruder_changed is not None:
                    node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
                node.decoratorsChanged.disconnect(self._onNodeDecoratorChanged)

            self._scene_objects = new_scene_objects
            self._onSettingPropertyChanged("print_sequence", "value")  # Create fake event, so right settings are triggered.

    ##  Updates the listeners that listen for changes in per-mesh stacks.
    #
    #   \param node The node for which the decorators changed.
    def _onNodeDecoratorChanged(self, node):
        per_mesh_stack = node.callDecoration("getStack")
        if per_mesh_stack:
            per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
        active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
        if active_extruder_changed is not None:
            active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
            self._updateDisallowedAreasAndRebuild()

    def setWidth(self, width):
        if width: self._width = width

    def setHeight(self, height):
        if height: self._height = height

    def setDepth(self, depth):
        if depth: self._depth = depth

    def setShape(self, shape):
        if shape: self._shape = shape

    def getDisallowedAreas(self):
        return self._disallowed_areas

    def setDisallowedAreas(self, areas):
        self._disallowed_areas = areas

    def render(self, renderer):
        if not self.getMeshData():
            return True

        if not self._shader:
            self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
            self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))

        renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
        renderer.queueNode(self, mesh = self._origin_mesh)
        renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
        if self._disallowed_area_mesh:
            renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)

        if self._error_mesh:
            renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True,
                               backface_cull=True, sort=-8)

        return True

    ##  Recalculates the build volume & disallowed areas.
    def rebuild(self):
        if not self._width or not self._height or not self._depth:
            return

        min_w = -self._width / 2
        max_w = self._width / 2
        min_h = 0.0
        max_h = self._height
        min_d = -self._depth / 2
        max_d = self._depth / 2

        z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting

        if self._shape != "elliptic":
            # Outline 'cube' of the build volume
            mb = MeshBuilder()
            mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)

            mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)

            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
            mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)

            self.setMeshData(mb.build())

            # Build plate grid mesh
            mb = MeshBuilder()
            mb.addQuad(
                Vector(min_w, min_h - z_fight_distance, min_d),
                Vector(max_w, min_h - z_fight_distance, min_d),
                Vector(max_w, min_h - z_fight_distance, max_d),
                Vector(min_w, min_h - z_fight_distance, max_d)
            )

            for n in range(0, 6):
                v = mb.getVertex(n)
                mb.setVertexUVCoordinates(n, v[0], v[2])
            self._grid_mesh = mb.build()

        else:
            # Bottom and top 'ellipse' of the build volume
            aspect = 1.0
            scale_matrix = Matrix()
            if self._width != 0:
                # Scale circular meshes by aspect ratio if width != height
                aspect = self._height / self._width
                scale_matrix.compose(scale = Vector(1, 1, aspect))
            mb = MeshBuilder()
            mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self.VolumeOutlineColor)
            mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0),  color = self.VolumeOutlineColor)
            self.setMeshData(mb.build().getTransformed(scale_matrix))

            # Build plate grid mesh
            mb = MeshBuilder()
            mb.addVertex(0, min_h - z_fight_distance, 0)
            mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0))
            sections = mb.getVertexCount() - 1 # Center point is not an arc section
            indices = []
            for n in range(0, sections - 1):
                indices.append([0, n + 2, n + 1])
            mb.addIndices(numpy.asarray(indices, dtype = numpy.int32))
            mb.calculateNormals()

            for n in range(0, mb.getVertexCount()):
                v = mb.getVertex(n)
                mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
            self._grid_mesh = mb.build().getTransformed(scale_matrix)

        # Indication of the machine origin
        if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
            origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
        else:
            origin = Vector(min_w, min_h, max_d)

        mb = MeshBuilder()
        mb.addCube(
            width = self._origin_line_length,
            height = self._origin_line_width,
            depth = self._origin_line_width,
            center = origin + Vector(self._origin_line_length / 2, 0, 0),
            color = self.XAxisColor
        )
        mb.addCube(
            width = self._origin_line_width,
            height = self._origin_line_length,
            depth = self._origin_line_width,
            center = origin + Vector(0, self._origin_line_length / 2, 0),
            color = self.YAxisColor
        )
        mb.addCube(
            width = self._origin_line_width,
            height = self._origin_line_width,
            depth = self._origin_line_length,
            center = origin - Vector(0, 0, self._origin_line_length / 2),
            color = self.ZAxisColor
        )
        self._origin_mesh = mb.build()

        disallowed_area_height = 0.1
        disallowed_area_size = 0
        if self._disallowed_areas:
            mb = MeshBuilder()
            color = Color(0.0, 0.0, 0.0, 0.15)
            for polygon in self._disallowed_areas:
                points = polygon.getPoints()
                first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
                previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
                for point in points:
                    new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d))
                    mb.addFace(first, previous_point, new_point, color = color)
                    previous_point = new_point

                # Find the largest disallowed area to exclude it from the maximum scale bounds.
                # This is a very nasty hack. This pretty much only works for UM machines.
                # This disallowed area_size needs a -lot- of rework at some point in the future: TODO
                if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
                    size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
                else:
                    size = 0
                disallowed_area_size = max(size, disallowed_area_size)

            self._disallowed_area_mesh = mb.build()
        else:
            self._disallowed_area_mesh = None

        if self._error_areas:
            mb = MeshBuilder()
            for error_area in self._error_areas:
                color = Color(1.0, 0.0, 0.0, 0.5)
                points = error_area.getPoints()
                first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
                               self._clamp(points[0][1], min_d, max_d))
                previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
                                        self._clamp(points[0][1], min_d, max_d))
                for point in points:
                    new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height,
                                       self._clamp(point[1], min_d, max_d))
                    mb.addFace(first, previous_point, new_point, color=color)
                    previous_point = new_point
            self._error_mesh = mb.build()
        else:
            self._error_mesh = None

        self._volume_aabb = AxisAlignedBox(
            minimum = Vector(min_w, min_h - 1.0, min_d),
            maximum = Vector(max_w, max_h - self._raft_thickness, max_d))

        bed_adhesion_size = self._getEdgeDisallowedSize()

        # As this works better for UM machines, we only add the disallowed_area_size for the z direction.
        # This is probably wrong in all other cases. TODO!
        # The +1 and -1 is added as there is always a bit of extra room required to work properly.
        scale_to_max_bounds = AxisAlignedBox(
            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1),
            maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)
        )

        Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds

    def getBoundingBox(self):
        return self._volume_aabb

    def getRaftThickness(self):
        return self._raft_thickness

    def _updateRaftThickness(self):
        old_raft_thickness = self._raft_thickness
        self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
        self._raft_thickness = 0.0
        if self._adhesion_type == "raft":
            self._raft_thickness = (
                self._global_container_stack.getProperty("raft_base_thickness", "value") +
                self._global_container_stack.getProperty("raft_interface_thickness", "value") +
                self._global_container_stack.getProperty("raft_surface_layers", "value") *
                    self._global_container_stack.getProperty("raft_surface_thickness", "value") +
                self._global_container_stack.getProperty("raft_airgap", "value"))

        # Rounding errors do not matter, we check if raft_thickness has changed at all
        if old_raft_thickness != self._raft_thickness:
            self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
            self.raftThicknessChanged.emit()

    ##  Update the build volume visualization
    def _onStackChanged(self):
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
            extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
            for extruder in extruders:
                extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)

        self._global_container_stack = Application.getInstance().getGlobalContainerStack()

        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
            extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
            for extruder in extruders:
                extruder.propertyChanged.connect(self._onSettingPropertyChanged)

            self._width = self._global_container_stack.getProperty("machine_width", "value")
            machine_height = self._global_container_stack.getProperty("machine_height", "value")
            if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
                if self._height < machine_height:
                    self._build_volume_message.show()
                else:
                    self._build_volume_message.hide()
            else:
                self._height = self._global_container_stack.getProperty("machine_height", "value")
                self._build_volume_message.hide()
            self._depth = self._global_container_stack.getProperty("machine_depth", "value")
            self._shape = self._global_container_stack.getProperty("machine_shape", "value")

            self._updateDisallowedAreas()
            self._updateRaftThickness()

            self.rebuild()

    def _onSettingPropertyChanged(self, setting_key, property_name):
        if property_name != "value":
            return

        rebuild_me = False
        if setting_key == "print_sequence":
            machine_height = self._global_container_stack.getProperty("machine_height", "value")
            if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
                if self._height < machine_height:
                    self._build_volume_message.show()
                else:
                    self._build_volume_message.hide()
            else:
                self._height = self._global_container_stack.getProperty("machine_height", "value")
                self._build_volume_message.hide()
            rebuild_me = True

        if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings:
            self._updateDisallowedAreas()
            rebuild_me = True

        if setting_key in self._raft_settings:
            self._updateRaftThickness()
            rebuild_me = True

        if rebuild_me:
            self.rebuild()

    def hasErrors(self):
        return self._has_errors

    ##  Calls _updateDisallowedAreas and makes sure the changes appear in the
    #   scene.
    #
    #   This is required for a signal to trigger the update in one go. The
    #   ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
    #   since there may be other changes before it needs to be rebuilt, which
    #   would hit performance.
    def _updateDisallowedAreasAndRebuild(self):
        self._updateDisallowedAreas()
        self.rebuild()

    def _updateDisallowedAreas(self):
        if not self._global_container_stack:
            return

        self._error_areas = []

        extruder_manager = ExtruderManager.getInstance()
        used_extruders = extruder_manager.getUsedExtruderStacks()
        disallowed_border_size = self._getEdgeDisallowedSize()

        if not used_extruders:
            # If no extruder is used, assume that the active extruder is used (else nothing is drawn)
            if extruder_manager.getActiveExtruderStack():
                used_extruders = [extruder_manager.getActiveExtruderStack()]
            else:
                used_extruders = [self._global_container_stack]

        result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
        prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
        prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.

        #Check if prime positions intersect with disallowed areas.
        for extruder in used_extruders:
            extruder_id = extruder.getId()

            collision = False
            for prime_polygon in prime_areas[extruder_id]:
                for disallowed_polygon in prime_disallowed_areas[extruder_id]:
                    if prime_polygon.intersectsPolygon(disallowed_polygon) is not None:
                        collision = True
                        break
                if collision:
                    break

                #Also check other prime positions (without additional offset).
                for other_extruder_id in prime_areas:
                    if extruder_id == other_extruder_id: #It is allowed to collide with itself.
                        continue
                    for other_prime_polygon in prime_areas[other_extruder_id]:
                        if prime_polygon.intersectsPolygon(other_prime_polygon):
                            collision = True
                            break
                    if collision:
                        break
                if collision:
                    break

            result_areas[extruder_id].extend(prime_areas[extruder_id])

            nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
            for area in nozzle_disallowed_areas:
                polygon = Polygon(numpy.array(area, numpy.float32))
                polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
                result_areas[extruder_id].append(polygon) #Don't perform the offset on these.

        # Add prime tower location as disallowed area.
        prime_tower_collision = False
        prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
        for extruder_id in prime_tower_areas:
            for prime_tower_area in prime_tower_areas[extruder_id]:
                for area in result_areas[extruder_id]:
                    if prime_tower_area.intersectsPolygon(area) is not None:
                        prime_tower_collision = True
                        break
                if prime_tower_collision: #Already found a collision.
                    break
            if not prime_tower_collision:
                result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
            else:
                self._error_areas.extend(prime_tower_areas[extruder_id])

        self._has_errors = len(self._error_areas) > 0

        self._disallowed_areas = []
        for extruder_id in result_areas:
            self._disallowed_areas.extend(result_areas[extruder_id])

    ##  Computes the disallowed areas for objects that are printed with print
    #   features.
    #
    #   This means that the brim, travel avoidance and such will be applied to
    #   these features.
    #
    #   \return A dictionary with for each used extruder ID the disallowed areas
    #   where that extruder may not print.
    def _computeDisallowedAreasPrinted(self, used_extruders):
        result = {}
        for extruder in used_extruders:
            result[extruder.getId()] = []

        #Currently, the only normally printed object is the prime tower.
        if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable") == True:
            prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
            machine_width = self._global_container_stack.getProperty("machine_width", "value")
            machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
            prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
            prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2

            prime_tower_area = Polygon([
                [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
                [prime_tower_x, prime_tower_y - prime_tower_size],
                [prime_tower_x, prime_tower_y],
                [prime_tower_x - prime_tower_size, prime_tower_y],
            ])
            prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
            for extruder in used_extruders:
                result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.

        return result

    ##  Computes the disallowed areas for the prime locations.
    #
    #   These are special because they are not subject to things like brim or
    #   travel avoidance. They do get a dilute with the border size though
    #   because they may not intersect with brims and such of other objects.
    #
    #   \param border_size The size with which to offset the disallowed areas
    #   due to skirt, brim, travel avoid distance, etc.
    #   \param used_extruders The extruder stacks to generate disallowed areas
    #   for.
    #   \return A dictionary with for each used extruder ID the prime areas.
    def _computeDisallowedAreasPrime(self, border_size, used_extruders):
        result = {}

        machine_width = self._global_container_stack.getProperty("machine_width", "value")
        machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
        for extruder in used_extruders:
            prime_x = extruder.getProperty("extruder_prime_pos_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
            prime_y = machine_depth / 2 - extruder.getProperty("extruder_prime_pos_y", "value")

            prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
            prime_polygon = prime_polygon.translate(prime_x, prime_y)
            prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
            result[extruder.getId()] = [prime_polygon]

        return result

    ##  Computes the disallowed areas that are statically placed in the machine.
    #
    #   It computes different disallowed areas depending on the offset of the
    #   extruder. The resulting dictionary will therefore have an entry for each
    #   extruder that is used.
    #
    #   \param border_size The size with which to offset the disallowed areas
    #   due to skirt, brim, travel avoid distance, etc.
    #   \param used_extruders The extruder stacks to generate disallowed areas
    #   for.
    #   \return A dictionary with for each used extruder ID the disallowed areas
    #   where that extruder may not print.
    def _computeDisallowedAreasStatic(self, border_size, used_extruders):
        #Convert disallowed areas to polygons and dilate them.
        machine_disallowed_polygons = []
        for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
            polygon = Polygon(numpy.array(area, numpy.float32))
            polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
            machine_disallowed_polygons.append(polygon)

        result = {}
        for extruder in used_extruders:
            extruder_id = extruder.getId()
            offset_x = extruder.getProperty("machine_nozzle_offset_x", "value")
            if offset_x is None:
                offset_x = 0
            offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
            if offset_y is None:
                offset_y = 0
            result[extruder_id] = []

            for polygon in machine_disallowed_polygons:
                result[extruder_id].append(polygon.translate(offset_x, offset_y)) #Compensate for the nozzle offset of this extruder.

            #Add the border around the edge of the build volume.
            left_unreachable_border = 0
            right_unreachable_border = 0
            top_unreachable_border = 0
            bottom_unreachable_border = 0
            #The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
            for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
                other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
                other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
                left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
                right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
                top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
                bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
            half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
            half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2

            if self._shape != "elliptic":
                if border_size - left_unreachable_border > 0:
                    result[extruder_id].append(Polygon(numpy.array([
                        [-half_machine_width, -half_machine_depth],
                        [-half_machine_width, half_machine_depth],
                        [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
                        [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
                    ], numpy.float32)))
                if border_size + right_unreachable_border > 0:
                    result[extruder_id].append(Polygon(numpy.array([
                        [half_machine_width, half_machine_depth],
                        [half_machine_width, -half_machine_depth],
                        [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
                        [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
                    ], numpy.float32)))
                if border_size + bottom_unreachable_border > 0:
                    result[extruder_id].append(Polygon(numpy.array([
                        [-half_machine_width, half_machine_depth],
                        [half_machine_width, half_machine_depth],
                        [half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
                        [-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
                    ], numpy.float32)))
                if border_size - top_unreachable_border > 0:
                    result[extruder_id].append(Polygon(numpy.array([
                        [half_machine_width, -half_machine_depth],
                        [-half_machine_width, -half_machine_depth],
                        [-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
                        [half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
                    ], numpy.float32)))
            else:
                sections = 32
                arc_vertex = [0, half_machine_depth - border_size]
                for i in range(0, sections):
                    quadrant = math.floor(4 * i / sections)
                    vertices = []
                    if quadrant == 0:
                        vertices.append([-half_machine_width, half_machine_depth])
                    elif quadrant == 1:
                        vertices.append([-half_machine_width, -half_machine_depth])
                    elif quadrant == 2:
                        vertices.append([half_machine_width, -half_machine_depth])
                    elif quadrant == 3:
                        vertices.append([half_machine_width, half_machine_depth])
                    vertices.append(arc_vertex)

                    angle = 2 * math.pi * (i + 1) / sections
                    arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)]
                    vertices.append(arc_vertex)

                    result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32)))

                if border_size > 0:
                    result[extruder_id].append(Polygon(numpy.array([
                        [-half_machine_width, -half_machine_depth],
                        [-half_machine_width, half_machine_depth],
                        [-half_machine_width + border_size, 0]
                    ], numpy.float32)))
                    result[extruder_id].append(Polygon(numpy.array([
                        [-half_machine_width, half_machine_depth],
                        [ half_machine_width, half_machine_depth],
                        [ 0, half_machine_depth - border_size]
                    ], numpy.float32)))
                    result[extruder_id].append(Polygon(numpy.array([
                        [ half_machine_width, half_machine_depth],
                        [ half_machine_width, -half_machine_depth],
                        [ half_machine_width - border_size, 0]
                    ], numpy.float32)))
                    result[extruder_id].append(Polygon(numpy.array([
                        [ half_machine_width,-half_machine_depth],
                        [-half_machine_width,-half_machine_depth],
                        [ 0, -half_machine_depth + border_size]
                    ], numpy.float32)))

        return result

    ##  Private convenience function to get a setting from the adhesion
    #   extruder.
    #
    #   \param setting_key The key of the setting to get.
    #   \param property The property to get from the setting.
    #   \return The property of the specified setting in the adhesion extruder.
    def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"):
        return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property)

    ##  Private convenience function to get a setting from every extruder.
    #
    #   For single extrusion machines, this gets the setting from the global
    #   stack.
    #
    #   \return A sequence of setting values, one for each extruder.
    def _getSettingFromAllExtruders(self, setting_key, property = "value"):
        return ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)

    ##  Private convenience function to get a setting from the support infill
    #   extruder.
    #
    #   \param setting_key The key of the setting to get.
    #   \param property The property to get from the setting.
    #   \return The property of the specified setting in the support infill
    #   extruder.
    def _getSettingFromSupportInfillExtruder(self, setting_key, property = "value"):
        return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property)

    ##  Helper function to get a setting from an extruder specified in another
    #   setting.
    #
    #   \param setting_key The key of the setting to get.
    #   \param extruder_setting_key The key of the setting that specifies from
    #   which extruder to get the setting, if there are multiple extruders.
    #   \param property The property to get from the setting.
    #   \return The property of the specified setting in the specified extruder.
    def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"):
        multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1

        if not multi_extrusion:
            return self._global_container_stack.getProperty(setting_key, property)

        extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value")

        if extruder_index == "-1":  # If extruder index is -1 use global instead
            return self._global_container_stack.getProperty(setting_key, property)

        extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
        stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
        return stack.getProperty(setting_key, property)

    ##  Convenience function to calculate the disallowed radius around the edge.
    #
    #   This disallowed radius is to allow for space around the models that is
    #   not part of the collision radius, such as bed adhesion (skirt/brim/raft)
    #   and travel avoid distance.
    def _getEdgeDisallowedSize(self):
        if not self._global_container_stack:
            return 0
        container_stack = self._global_container_stack

        # If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
        if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
            return 0.1  # Return a very small value, so we do draw disallowed area's near the edges.

        adhesion_type = container_stack.getProperty("adhesion_type", "value")
        if adhesion_type == "skirt":
            skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap")
            skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count")
            bed_adhesion_size = skirt_distance + (skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width"))
            if len(ExtruderManager.getInstance().getUsedExtruderStacks()) > 1:
                adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
                extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
                del extruder_values[adhesion_extruder_nr]  # Remove the value of the adhesion extruder nr.
                for value in extruder_values:
                    bed_adhesion_size += value
        elif adhesion_type == "brim":
            bed_adhesion_size = self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("skirt_brim_line_width")
            if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
                adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
                extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
                del extruder_values[adhesion_extruder_nr]  # Remove the value of the adhesion extruder nr.
                for value in extruder_values:
                    bed_adhesion_size += value
        elif adhesion_type == "raft":
            bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin")
        elif adhesion_type == "none":
            bed_adhesion_size = 0
        else:
            raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")

        support_expansion = 0
        if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"):
            support_expansion += self._getSettingFromSupportInfillExtruder("support_offset")

        farthest_shield_distance = 0
        if container_stack.getProperty("draft_shield_enabled", "value"):
            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
        if container_stack.getProperty("ooze_shield_enabled", "value"):
            farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value"))

        move_from_wall_radius = 0  # Moves that start from outer wall.
        move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
        used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
        avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders]
        travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
        for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global).
            if avoid_other_parts_enabled:
                move_from_wall_radius = max(move_from_wall_radius, avoid_distance)

        # Now combine our different pieces of data to get the final border size.
        # Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
        # Support expansion is added to farthest shield distance, since the shields go around support.
        border_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
        return border_size

    def _clamp(self, value, min_value, max_value):
        return max(min(value, max_value), min_value)

    _skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist"]
    _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
    _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
    _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
    _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
    _extruder_settings = ["support_enable", "support_interface_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_interface_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
コード例 #46
0
ファイル: SimulationView.py プロジェクト: szymongamza/Cura
class SimulationView(CuraView):
    # Must match SimulationViewMenuComponent.qml
    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
    LAYER_VIEW_TYPE_LINE_TYPE = 1
    LAYER_VIEW_TYPE_FEEDRATE = 2
    LAYER_VIEW_TYPE_THICKNESS = 3

    def __init__(self, parent=None) -> None:
        super().__init__(parent)

        self._max_layers = 0
        self._current_layer_num = 0
        self._minimum_layer_num = 0
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._top_layers_job = None  # type: Optional["_CreateTopLayersJob"]
        self._activity = False
        self._old_max_layers = 0

        self._max_paths = 0
        self._current_path_num = 0
        self._minimum_path_num = 0
        self.start_elements_index = 0
        self.end_elements_index = 0
        self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)

        self._busy = False
        self._simulation_running = False

        self._ghost_shader = None  # type: Optional["ShaderProgram"]
        self._layer_pass = None  # type: Optional[SimulationPass]
        self._composite_pass = None  # type: Optional[CompositePass]
        self._old_layer_bindings = None  # type: Optional[List[str]]
        self._simulationview_composite_shader = None  # type: Optional["ShaderProgram"]
        self._old_composite_shader = None  # type: Optional["ShaderProgram"]

        self._max_feedrate = sys.float_info.min
        self._min_feedrate = sys.float_info.max
        self._max_thickness = sys.float_info.min
        self._min_thickness = sys.float_info.max

        self._global_container_stack = None  # type: Optional[ContainerStack]
        self._proxy = None

        self._resetSettings()
        self._legend_items = None
        self._show_travel_moves = False
        self._nozzle_node = None  # type: Optional[NozzleNode]

        Application.getInstance().getPreferences().addPreference(
            "view/top_layer_count", 5)
        Application.getInstance().getPreferences().addPreference(
            "view/only_show_top_layers", False)
        Application.getInstance().getPreferences().addPreference(
            "view/force_layer_view_compatibility_mode", False)

        Application.getInstance().getPreferences().addPreference(
            "layerview/layer_view_type", 0)
        Application.getInstance().getPreferences().addPreference(
            "layerview/extruder_opacities", "")

        Application.getInstance().getPreferences().addPreference(
            "layerview/show_travel_moves", False)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_helpers", True)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_skin", True)
        Application.getInstance().getPreferences().addPreference(
            "layerview/show_infill", True)

        self._updateWithPreferences()

        self._solid_layers = int(
            Application.getInstance().getPreferences().getValue(
                "view/top_layer_count"))
        self._only_show_top_layers = bool(
            Application.getInstance().getPreferences().getValue(
                "view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self._wireprint_warning_message = Message(catalog.i18nc(
            "@info:status",
            "Cura does not accurately display layers when Wire Printing is enabled"
        ),
                                                  title=catalog.i18nc(
                                                      "@info:title",
                                                      "Simulation View"))

        QtApplication.getInstance().engineCreatedSignal.connect(
            self._onEngineCreated)

    def _onEngineCreated(self) -> None:
        plugin_path = PluginRegistry.getInstance().getPluginPath(
            self.getPluginId())
        if plugin_path:
            self.addDisplayComponent(
                "main",
                os.path.join(plugin_path, "SimulationViewMainComponent.qml"))
            self.addDisplayComponent(
                "menu",
                os.path.join(plugin_path, "SimulationViewMenuComponent.qml"))
        else:
            Logger.log("e", "Unable to find the path for %s",
                       self.getPluginId())

    def _evaluateCompatibilityMode(self) -> bool:
        return OpenGLContext.isLegacyOpenGL() or bool(
            Application.getInstance().getPreferences().getValue(
                "view/force_layer_view_compatibility_mode"))

    def _resetSettings(self) -> None:
        self._layer_view_type = 0  # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
        self._extruder_count = 0
        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
        self._show_travel_moves = False
        self._show_helpers = True
        self._show_skin = True
        self._show_infill = True
        self.resetLayerData()

    def getActivity(self) -> bool:
        return self._activity

    def setActivity(self, activity: bool) -> None:
        if self._activity == activity:
            return
        self._activity = activity
        self.activityChanged.emit()

    def getSimulationPass(self) -> SimulationPass:
        if not self._layer_pass:
            # Currently the RenderPass constructor requires a size > 0
            # This should be fixed in RenderPass's constructor.
            self._layer_pass = SimulationPass(1, 1)
            self._compatibility_mode = self._evaluateCompatibilityMode()
            self._layer_pass.setSimulationView(self)
        return self._layer_pass

    def getCurrentLayer(self) -> int:
        return self._current_layer_num

    def getMinimumLayer(self) -> int:
        return self._minimum_layer_num

    def getMaxLayers(self) -> int:
        return self._max_layers

    def getCurrentPath(self) -> int:
        return self._current_path_num

    def getMinimumPath(self) -> int:
        return self._minimum_path_num

    def getMaxPaths(self) -> int:
        return self._max_paths

    def getNozzleNode(self) -> NozzleNode:
        if not self._nozzle_node:
            self._nozzle_node = NozzleNode()
        return self._nozzle_node

    def _onSceneChanged(self, node: "SceneNode") -> None:
        if node.getMeshData() is None:
            return
        self.setActivity(False)
        self.calculateMaxLayers()
        self.calculateMaxPathsOnLayer(self._current_layer_num)

    def isBusy(self) -> bool:
        return self._busy

    def setBusy(self, busy: bool) -> None:
        if busy != self._busy:
            self._busy = busy
            self.busyChanged.emit()

    def isSimulationRunning(self) -> bool:
        return self._simulation_running

    def setSimulationRunning(self, running: bool) -> None:
        self._simulation_running = running

    def resetLayerData(self) -> None:
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._max_feedrate = sys.float_info.min
        self._min_feedrate = sys.float_info.max
        self._max_thickness = sys.float_info.min
        self._min_thickness = sys.float_info.max

    def beginRendering(self) -> None:
        scene = self.getController().getScene()
        renderer = self.getRenderer()
        if renderer is None:
            return

        if not self._ghost_shader:
            self._ghost_shader = OpenGL.getInstance().createShaderProgram(
                Resources.getPath(Resources.Shaders, "color.shader"))
            theme = CuraApplication.getInstance().getTheme()
            if theme is not None:
                self._ghost_shader.setUniformValue(
                    "u_color",
                    Color(*theme.getColor("layerview_ghost").getRgb()))

        for node in DepthFirstIterator(scene.getRoot()):
            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
            # However, it is somewhat relevant when the node is selected, so do render it then.
            if type(node) is ConvexHullNode and not Selection.isSelected(
                    cast(ConvexHullNode, node).getWatchedNode()):
                continue

            if not node.render(renderer):
                if (node.getMeshData()) and node.isVisible():
                    renderer.queueNode(node,
                                       transparent=True,
                                       shader=self._ghost_shader)

    def setLayer(self, value: int) -> None:
        if self._current_layer_num != value:
            self._current_layer_num = value
            if self._current_layer_num < 0:
                self._current_layer_num = 0
            if self._current_layer_num > self._max_layers:
                self._current_layer_num = self._max_layers
            if self._current_layer_num < self._minimum_layer_num:
                self._minimum_layer_num = self._current_layer_num

            self._startUpdateTopLayers()
            self.recalculateStartEndElements()

            self.currentLayerNumChanged.emit()

    def setMinimumLayer(self, value: int) -> None:
        if self._minimum_layer_num != value:
            self._minimum_layer_num = value
            if self._minimum_layer_num < 0:
                self._minimum_layer_num = 0
            if self._minimum_layer_num > self._max_layers:
                self._minimum_layer_num = self._max_layers
            if self._minimum_layer_num > self._current_layer_num:
                self._current_layer_num = self._minimum_layer_num

            self._startUpdateTopLayers()
            self.recalculateStartEndElements()
            self.currentLayerNumChanged.emit()

    def setPath(self, value: int) -> None:
        if self._current_path_num != value:
            self._current_path_num = value
            if self._current_path_num < 0:
                self._current_path_num = 0
            if self._current_path_num > self._max_paths:
                self._current_path_num = self._max_paths
            if self._current_path_num < self._minimum_path_num:
                self._minimum_path_num = self._current_path_num

            self._startUpdateTopLayers()
            self.recalculateStartEndElements()
            self.currentPathNumChanged.emit()

    def setMinimumPath(self, value: int) -> None:
        if self._minimum_path_num != value:
            self._minimum_path_num = value
            if self._minimum_path_num < 0:
                self._minimum_path_num = 0
            if self._minimum_path_num > self._max_layers:
                self._minimum_path_num = self._max_layers
            if self._minimum_path_num > self._current_path_num:
                self._current_path_num = self._minimum_path_num

            self._startUpdateTopLayers()

            self.currentPathNumChanged.emit()

    ##  Set the layer view type
    #
    #   \param layer_view_type integer as in SimulationView.qml and this class
    def setSimulationViewType(self, layer_view_type: int) -> None:
        if layer_view_type != self._layer_view_type:
            self._layer_view_type = layer_view_type
            self.currentLayerNumChanged.emit()

    ##  Return the layer view type, integer as in SimulationView.qml and this class
    def getSimulationViewType(self) -> int:
        return self._layer_view_type

    ##  Set the extruder opacity
    #
    #   \param extruder_nr 0..3
    #   \param opacity 0.0 .. 1.0
    def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
        if 0 <= extruder_nr <= 3:
            self._extruder_opacity[extruder_nr] = opacity
            self.currentLayerNumChanged.emit()

    def getExtruderOpacities(self) -> List[float]:
        return self._extruder_opacity

    def setShowTravelMoves(self, show):
        self._show_travel_moves = show
        self.currentLayerNumChanged.emit()

    def getShowTravelMoves(self):
        return self._show_travel_moves

    def setShowHelpers(self, show: bool) -> None:
        self._show_helpers = show
        self.currentLayerNumChanged.emit()

    def getShowHelpers(self) -> bool:
        return self._show_helpers

    def setShowSkin(self, show: bool) -> None:
        self._show_skin = show
        self.currentLayerNumChanged.emit()

    def getShowSkin(self) -> bool:
        return self._show_skin

    def setShowInfill(self, show: bool) -> None:
        self._show_infill = show
        self.currentLayerNumChanged.emit()

    def getShowInfill(self) -> bool:
        return self._show_infill

    def getCompatibilityMode(self) -> bool:
        return self._compatibility_mode

    def getExtruderCount(self) -> int:
        return self._extruder_count

    def getMinFeedrate(self) -> float:
        if abs(self._min_feedrate - sys.float_info.max
               ) < 10:  # Some lenience due to floating point rounding.
            return 0.0  # If it's still max-float, there are no measurements. Use 0 then.
        return self._min_feedrate

    def getMaxFeedrate(self) -> float:
        return self._max_feedrate

    def getMinThickness(self) -> float:
        if abs(self._min_thickness - sys.float_info.max
               ) < 10:  # Some lenience due to floating point rounding.
            return 0.0  # If it's still max-float, there are no measurements. Use 0 then.
        return self._min_thickness

    def recalculateStartEndElements(self):
        self.start_elements_index = 0
        self.end_elements_index = 0
        scene = self.getController().getScene()
        for node in DepthFirstIterator(scene.getRoot()):  # type: ignore
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            # Found a the layer data!
            element_counts = layer_data.getElementCounts()
            for layer in sorted(element_counts.keys()):
                if layer == self._current_layer_num:
                    break
                if self._minimum_layer_num > layer:
                    self.start_elements_index += element_counts[layer]
                self.end_elements_index += element_counts[layer]

    def getMaxThickness(self) -> float:
        return self._max_thickness

    def calculateMaxLayers(self) -> None:
        scene = self.getController().getScene()

        self._old_max_layers = self._max_layers
        ## Recalculate num max layers
        new_max_layers = -1
        for node in DepthFirstIterator(scene.getRoot()):  # type: ignore
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            self.setActivity(True)
            min_layer_number = sys.maxsize
            max_layer_number = -sys.maxsize
            for layer_id in layer_data.getLayers():

                # If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
                if len(layer_data.getLayer(layer_id).polygons) < 1:
                    continue

                # Store the max and min feedrates and thicknesses for display purposes
                for p in layer_data.getLayer(layer_id).polygons:
                    self._max_feedrate = max(float(p.lineFeedrates.max()),
                                             self._max_feedrate)
                    self._min_feedrate = min(float(p.lineFeedrates.min()),
                                             self._min_feedrate)
                    self._max_thickness = max(float(p.lineThicknesses.max()),
                                              self._max_thickness)
                    try:
                        self._min_thickness = min(
                            float(p.lineThicknesses[numpy.nonzero(
                                p.lineThicknesses)].min()),
                            self._min_thickness)
                    except ValueError:
                        # Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
                        # the zero) can't be calculated
                        Logger.log(
                            "i",
                            "Min thickness can't be calculated because all the values are zero"
                        )
                if max_layer_number < layer_id:
                    max_layer_number = layer_id
                if min_layer_number > layer_id:
                    min_layer_number = layer_id
            layer_count = max_layer_number - min_layer_number

            if new_max_layers < layer_count:
                new_max_layers = layer_count

        if new_max_layers >= 0 and new_max_layers != self._old_max_layers:
            self._max_layers = new_max_layers

            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
            # if it's the largest value. If we don't do this, we can have a slider block outside of the
            # slider.
            if new_max_layers > self._current_layer_num:
                self.maxLayersChanged.emit()
                self.setLayer(int(self._max_layers))
            else:
                self.setLayer(int(self._max_layers))
                self.maxLayersChanged.emit()
        self._startUpdateTopLayers()

    def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
        # Update the currentPath
        scene = self.getController().getScene()
        for node in DepthFirstIterator(scene.getRoot()):  # type: ignore
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            layer = layer_data.getLayer(layer_num)
            if layer is None:
                return
            new_max_paths = layer.lineMeshElementCount()
            if new_max_paths >= 0 and new_max_paths != self._max_paths:
                self._max_paths = new_max_paths
                self.maxPathsChanged.emit()

            self.setPath(int(new_max_paths))

    maxLayersChanged = Signal()
    maxPathsChanged = Signal()
    currentLayerNumChanged = Signal()
    currentPathNumChanged = Signal()
    globalStackChanged = Signal()
    preferencesChanged = Signal()
    busyChanged = Signal()
    activityChanged = Signal()

    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
    #   as this caused some issues.
    def getProxy(self, engine, script_engine):
        if self._proxy is None:
            self._proxy = SimulationViewProxy(self)
        return self._proxy

    def endRendering(self) -> None:
        pass

    def event(self, event) -> bool:
        modifiers = QApplication.keyboardModifiers()
        ctrl_is_active = modifiers & Qt.ControlModifier
        shift_is_active = modifiers & Qt.ShiftModifier
        if event.type == Event.KeyPressEvent and ctrl_is_active:
            amount = 10 if shift_is_active else 1
            if event.key == KeyEvent.UpKey:
                self.setLayer(self._current_layer_num + amount)
                return True
            if event.key == KeyEvent.DownKey:
                self.setLayer(self._current_layer_num - amount)
                return True

        if event.type == Event.ViewActivateEvent:
            # Start listening to changes.
            Application.getInstance().getPreferences(
            ).preferenceChanged.connect(self._onPreferencesChanged)
            self._controller.getScene().getRoot().childrenChanged.connect(
                self._onSceneChanged)

            self.calculateMaxLayers()
            self.calculateMaxPathsOnLayer(self._current_layer_num)

            # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
            # This can happen when you do the following steps:
            #   1. Start Cura
            #   2. Load a model
            #   3. Switch to Custom mode
            #   4. Select the model and click on the per-object tool icon
            #   5. Switch view to Layer view or X-Ray
            #   6. Cura will very likely crash
            # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why.
            # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL
            # context is None.
            if Platform.isOSX():
                if QOpenGLContext.currentContext() is None:
                    Logger.log(
                        "d",
                        "current context of OpenGL is empty on Mac OS X, will try to create shaders later"
                    )
                    CuraApplication.getInstance().callLater(
                        lambda e=event: self.event(e))
                    return False

            # Make sure the SimulationPass is created
            layer_pass = self.getSimulationPass()
            renderer = self.getRenderer()
            if renderer is None:
                return False

            renderer.addRenderPass(layer_pass)

            # Make sure the NozzleNode is add to the root
            nozzle = self.getNozzleNode()
            nozzle.setParent(self.getController().getScene().getRoot())
            nozzle.setVisible(False)

            Application.getInstance().globalContainerStackChanged.connect(
                self._onGlobalStackChanged)
            self._onGlobalStackChanged()

            if not self._simulationview_composite_shader:
                plugin_path = cast(
                    str,
                    PluginRegistry.getInstance().getPluginPath(
                        "SimulationView"))
                self._simulationview_composite_shader = OpenGL.getInstance(
                ).createShaderProgram(
                    os.path.join(plugin_path,
                                 "simulationview_composite.shader"))
                theme = CuraApplication.getInstance().getTheme()
                if theme is not None:
                    self._simulationview_composite_shader.setUniformValue(
                        "u_background_color",
                        Color(*theme.getColor("viewport_background").getRgb()))
                    self._simulationview_composite_shader.setUniformValue(
                        "u_outline_color",
                        Color(*theme.getColor(
                            "model_selection_outline").getRgb()))

            if not self._composite_pass:
                self._composite_pass = cast(
                    CompositePass, renderer.getRenderPass("composite"))

            self._old_layer_bindings = self._composite_pass.getLayerBindings(
            )[:]  # make a copy so we can restore to it later
            self._composite_pass.getLayerBindings().append("simulationview")
            self._old_composite_shader = self._composite_pass.getCompositeShader(
            )
            self._composite_pass.setCompositeShader(
                self._simulationview_composite_shader)

        elif event.type == Event.ViewDeactivateEvent:
            self._controller.getScene().getRoot().childrenChanged.disconnect(
                self._onSceneChanged)
            Application.getInstance().getPreferences(
            ).preferenceChanged.disconnect(self._onPreferencesChanged)
            self._wireprint_warning_message.hide()
            Application.getInstance().globalContainerStackChanged.disconnect(
                self._onGlobalStackChanged)
            if self._global_container_stack:
                self._global_container_stack.propertyChanged.disconnect(
                    self._onPropertyChanged)
            if self._nozzle_node:
                self._nozzle_node.setParent(None)

            renderer = self.getRenderer()
            if renderer is None:
                return False

            if self._layer_pass is not None:
                renderer.removeRenderPass(self._layer_pass)
            if self._composite_pass:
                self._composite_pass.setLayerBindings(
                    cast(List[str], self._old_layer_bindings))
                self._composite_pass.setCompositeShader(
                    cast(ShaderProgram, self._old_composite_shader))

        return False

    def getCurrentLayerMesh(self):
        return self._current_layer_mesh

    def getCurrentLayerJumps(self):
        return self._current_layer_jumps

    def _onGlobalStackChanged(self) -> None:
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(
                self._onPropertyChanged)
        self._global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(
                self._onPropertyChanged)
            self._extruder_count = self._global_container_stack.getProperty(
                "machine_extruder_count", "value")
            self._onPropertyChanged("wireframe_enabled", "value")
            self.globalStackChanged.emit()
        else:
            self._wireprint_warning_message.hide()

    def _onPropertyChanged(self, key: str, property_name: str) -> None:
        if key == "wireframe_enabled" and property_name == "value":
            if self._global_container_stack and self._global_container_stack.getProperty(
                    "wireframe_enabled", "value"):
                self._wireprint_warning_message.show()
            else:
                self._wireprint_warning_message.hide()

    def _onCurrentLayerNumChanged(self) -> None:
        self.calculateMaxPathsOnLayer(self._current_layer_num)
        scene = Application.getInstance().getController().getScene()
        scene.sceneChanged.emit(scene.getRoot())

    def _startUpdateTopLayers(self) -> None:
        if not self._compatibility_mode:
            return
        self.recalculateStartEndElements()
        if self._top_layers_job:
            self._top_layers_job.finished.disconnect(
                self._updateCurrentLayerMesh)
            self._top_layers_job.cancel()

        self.setBusy(True)

        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(),
                                                   self._current_layer_num,
                                                   self._solid_layers)
        self._top_layers_job.finished.connect(
            self._updateCurrentLayerMesh
        )  # type: ignore  # mypy doesn't understand the whole private class thing that's going on here.
        self._top_layers_job.start()  # type: ignore

    def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None:
        self.setBusy(False)

        if not job.getResult():
            return
        self.resetLayerData(
        )  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
        self._current_layer_mesh = job.getResult().get("layers")
        if self._show_travel_moves:
            self._current_layer_jumps = job.getResult().get("jumps")
        self._controller.getScene().sceneChanged.emit(
            self._controller.getScene().getRoot())

        self._top_layers_job = None

    def _updateWithPreferences(self) -> None:
        self._solid_layers = int(
            Application.getInstance().getPreferences().getValue(
                "view/top_layer_count"))
        self._only_show_top_layers = bool(
            Application.getInstance().getPreferences().getValue(
                "view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self.setSimulationViewType(
            int(
                float(Application.getInstance().getPreferences().getValue(
                    "layerview/layer_view_type"))))

        for extruder_nr, extruder_opacity in enumerate(
                Application.getInstance().getPreferences().getValue(
                    "layerview/extruder_opacities").split("|")):
            try:
                opacity = float(extruder_opacity)
            except ValueError:
                opacity = 1.0
            self.setExtruderOpacity(extruder_nr, opacity)

        self.setShowTravelMoves(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_travel_moves")))
        self.setShowHelpers(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_helpers")))
        self.setShowSkin(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_skin")))
        self.setShowInfill(
            bool(Application.getInstance().getPreferences().getValue(
                "layerview/show_infill")))

        self._startUpdateTopLayers()
        self.preferencesChanged.emit()

    def _onPreferencesChanged(self, preference: str) -> None:
        if preference not in {
                "view/top_layer_count",
                "view/only_show_top_layers",
                "view/force_layer_view_compatibility_mode",
                "layerview/layer_view_type",
                "layerview/extruder_opacities",
                "layerview/show_travel_moves",
                "layerview/show_helpers",
                "layerview/show_skin",
                "layerview/show_infill",
        }:
            return

        self._updateWithPreferences()
コード例 #47
0
ファイル: ClusterUM3OutputDevice.py プロジェクト: CPS-3/Cura
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
    printJobsChanged = pyqtSignal()
    activePrinterChanged = pyqtSignal()

    # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
    # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
    clusterPrintersChanged = pyqtSignal()

    def __init__(self, device_id, address, properties, parent = None):
        super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
        self._api_prefix = "/cluster-api/v1/"

        self._number_of_extruders = 2

        self._dummy_lambdas = set()

        self._print_jobs = []

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
        self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")

        # See comments about this hack with the clusterPrintersChanged signal
        self.printersChanged.connect(self.clusterPrintersChanged)

        self._accepts_commands = True

        # Cluster does not have authentication, so default to authenticated
        self._authentication_state = AuthState.Authenticated

        self._error_message = None
        self._write_job_progress_message = None
        self._progress_message = None

        self._active_printer = None  # type: Optional[PrinterOutputModel]

        self._printer_selection_dialog = None

        self.setPriority(3)  # Make sure the output device gets selected above local file output
        self.setName(self._id)
        self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))

        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))

        self._printer_uuid_to_unique_name_mapping = {}

        self._finished_jobs = []

        self._cluster_size = int(properties.get(b"cluster_size", 0))

        self._latest_reply_handler = None

    def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
        self.writeStarted.emit(self)

        #Formats supported by this application (file types that we can actually write).
        if file_handler:
            file_formats = file_handler.getSupportedFileTypesWrite()
        else:
            file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()

        #Create a list from the supported file formats string.
        machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
        machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
        #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
        if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"):
            machine_file_formats = ["application/x-ufp"] + machine_file_formats

        # Take the intersection between file_formats and machine_file_formats.
        format_by_mimetype = {format["mime_type"]: format for format in file_formats}
        file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.

        if len(file_formats) == 0:
            Logger.log("e", "There are no file formats available to write with!")
            raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
        preferred_format = file_formats[0]

        #Just take the first file format available.
        if file_handler is not None:
            writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
        else:
            writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])

        #This function pauses with the yield, waiting on instructions on which printer it needs to print with.
        self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
        self._sending_job.send(None) #Start the generator.

        if len(self._printers) > 1: #We need to ask the user.
            self._spawnPrinterSelectionDialog()
            is_job_sent = True
        else: #Just immediately continue.
            self._sending_job.send("") #No specifically selected printer.
            is_job_sent = self._sending_job.send(None)

    def _spawnPrinterSelectionDialog(self):
        if self._printer_selection_dialog is None:
            path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
            self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
        if self._printer_selection_dialog is not None:
            self._printer_selection_dialog.show()

    @pyqtProperty(int, constant=True)
    def clusterSize(self):
        return self._cluster_size

    ##  Allows the user to choose a printer to print with from the printer
    #   selection dialogue.
    #   \param target_printer The name of the printer to target.
    @pyqtSlot(str)
    def selectPrinter(self, target_printer: str = "") -> None:
        self._sending_job.send(target_printer)

    ##  Greenlet to send a job to the printer over the network.
    #
    #   This greenlet gets called asynchronously in requestWrite. It is a
    #   greenlet in order to optionally wait for selectPrinter() to select a
    #   printer.
    #   The greenlet yields exactly three times: First time None,
    #   \param writer The file writer to use to create the data.
    #   \param preferred_format A dictionary containing some information about
    #   what format to write to. This is necessary to create the correct buffer
    #   types and file extension and such.
    def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
        Logger.log("i", "Sending print job to printer.")
        if self._sending_gcode:
            self._error_message = Message(
                i18n_catalog.i18nc("@info:status",
                                   "Sending new jobs (temporarily) blocked, still sending the previous print job."))
            self._error_message.show()
            yield #Wait on the user to select a target printer.
            yield #Wait for the write job to be finished.
            yield False #Return whether this was a success or not.
            yield #Prevent StopIteration.

        self._sending_gcode = True

        target_printer = yield #Potentially wait on the user to select a target printer.

        # Using buffering greatly reduces the write time for many lines of gcode
        if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
            stream = io.StringIO()
        else: #Binary mode.
            stream = io.BytesIO()

        job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])

        self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                                   title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
        self._write_job_progress_message.show()

        self._dummy_lambdas = (target_printer, preferred_format, stream)
        job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)

        job.start()

        yield True #Return that we had success!
        yield #To prevent having to catch the StopIteration exception.

    from cura.Utils.Threading import call_on_qt_thread

    def _sendPrintJobWaitOnWriteJobFinished(self, job):
        self._write_job_progress_message.hide()

        self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
                                         title = i18n_catalog.i18nc("@info:title", "Sending Data"))
        self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
        self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
        self._progress_message.show()

        parts = []

        target_printer, preferred_format, stream = self._dummy_lambdas

        # If a specific printer was selected, it should be printed with that machine.
        if target_printer:
            target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
            parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))

        # Add user name to the print_job
        parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))

        file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]

        output = stream.getvalue() #Either str or bytes depending on the output mode.
        if isinstance(stream, io.StringIO):
            output = output.encode("utf-8")

        parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))

        self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)

    @pyqtProperty(QObject, notify=activePrinterChanged)
    def activePrinter(self) -> Optional[PrinterOutputModel]:
        return self._active_printer

    @pyqtSlot(QObject)
    def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
        if self._active_printer != printer:
            if self._active_printer and self._active_printer.camera:
                self._active_printer.camera.stop()
            self._active_printer = printer
            self.activePrinterChanged.emit()

    def _onPostPrintJobFinished(self, reply):
        self._progress_message.hide()
        self._compressing_gcode = False
        self._sending_gcode = False

    def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()
            if new_progress > self._progress_message.getProgress():
                self._progress_message.show()  # Ensure that the message is visible.
                self._progress_message.setProgress(bytes_sent / bytes_total * 100)

            # If successfully sent:
            if bytes_sent == bytes_total:
                # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
                # monitor tab.
                self._success_message = Message(
                    i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
                    lifetime=5, dismissable=True,
                    title=i18n_catalog.i18nc("@info:title", "Data Sent"))
                self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon=None,
                                                description="")
                self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
                self._success_message.show()
        else:
            self._progress_message.setProgress(0)
            self._progress_message.hide()

    def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
        if action_id == "Abort":
            Logger.log("d", "User aborted sending print to remote.")
            self._progress_message.hide()
            self._compressing_gcode = False
            self._sending_gcode = False
            Application.getInstance().getController().setActiveStage("PrepareStage")

            # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
            # the "reply" should be disconnected
            if self._latest_reply_handler:
                self._latest_reply_handler.disconnect()
                self._latest_reply_handler = None

    def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
        if action_id == "View":
            Application.getInstance().getController().setActiveStage("MonitorStage")

    @pyqtSlot()
    def openPrintJobControlPanel(self) -> None:
        Logger.log("d", "Opening print job control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))

    @pyqtSlot()
    def openPrinterControlPanel(self) -> None:
        Logger.log("d", "Opening printer control panel...")
        QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def printJobs(self)-> List[PrintJobOutputModel] :
        return self._print_jobs

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.state == "queued"]

    @pyqtProperty("QVariantList", notify=printJobsChanged)
    def activePrintJobs(self) -> List[PrintJobOutputModel]:
        return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]

    @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
    def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
        printer_count = {}
        for printer in self._printers:
            if printer.type in printer_count:
                printer_count[printer.type] += 1
            else:
                printer_count[printer.type] = 1
        result = []
        for machine_type in printer_count:
            result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
        return result

    @pyqtSlot(int, result=str)
    def formatDuration(self, seconds: int) -> str:
        return Duration(seconds).getDisplayString(DurationFormat.Format.Short)

    @pyqtSlot(int, result=str)
    def getTimeCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)

    @pyqtSlot(int, result=str)
    def getDateCompleted(self, time_remaining: int) -> str:
        current_time = time()
        datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
        return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()

    def _printJobStateChanged(self) -> None:
        username = self._getUserName()

        if username is None:
            return  # We only want to show notifications if username is set.

        finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]

        newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
        for job in newly_finished_jobs:
            if job.assignedPrinter:
                job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
            else:
                job_completed_text =  i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
            job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
            job_completed_message.show()

        # Ensure UI gets updated
        self.printJobsChanged.emit()

        # Keep a list of all completed jobs so we know if something changed next time.
        self._finished_jobs = finished_jobs

    def _update(self) -> None:
        if not super()._update():
            return
        self.get("printers/", onFinished=self._onGetPrintersDataFinished)
        self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)

    def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        print_jobs_seen = []
        job_list_changed = False
        for print_job_data in result:
            print_job = findByKey(self._print_jobs, print_job_data["uuid"])

            if print_job is None:
                print_job = self._createPrintJobModel(print_job_data)
                job_list_changed = True

            self._updatePrintJob(print_job, print_job_data)

            if print_job.state != "queued":  # Print job should be assigned to a printer.
                if print_job.state in ["failed", "finished", "aborted"]:
                    # Print job was already completed, so don't attach it to a printer.
                    printer = None
                else:
                    printer = self._getPrinterByKey(print_job_data["printer_uuid"])
            else:  # The job can "reserve" a printer if some changes are required.
                printer = self._getPrinterByKey(print_job_data["assigned_to"])

            if printer:
                printer.updateActivePrintJob(print_job)

            print_jobs_seen.append(print_job)

        # Check what jobs need to be removed.
        removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]

        for removed_job in removed_jobs:
            job_list_changed |= self._removeJob(removed_job)

        if job_list_changed:
            self.printJobsChanged.emit()  # Do a single emit for all print job changes.

    def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
        if not checkValidGetReply(reply):
            return

        result = loadJsonFromReply(reply)
        if result is None:
            return

        printer_list_changed = False
        printers_seen = []

        for printer_data in result:
            printer = findByKey(self._printers, printer_data["uuid"])

            if printer is None:
                printer = self._createPrinterModel(printer_data)
                printer_list_changed = True

            printers_seen.append(printer)

            self._updatePrinter(printer, printer_data)

        removed_printers = [printer for printer in self._printers if printer not in printers_seen]
        for printer in removed_printers:
            self._removePrinter(printer)

        if removed_printers or printer_list_changed:
            self.printersChanged.emit()

    def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
        printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                     number_of_extruders=self._number_of_extruders)
        printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
        self._printers.append(printer)
        return printer

    def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel:
        print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
                                        key=data["uuid"], name= data["name"])
        print_job.stateChanged.connect(self._printJobStateChanged)
        self._print_jobs.append(print_job)
        return print_job

    def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None:
        print_job.updateTimeTotal(data["time_total"])
        print_job.updateTimeElapsed(data["time_elapsed"])
        print_job.updateState(data["status"])
        print_job.updateOwner(data["owner"])

    def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None:
        # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
        # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
        self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]

        definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
        if not definitions:
            Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
            return

        machine_definition = definitions[0]

        printer.updateName(data["friendly_name"])
        printer.updateKey(data["uuid"])
        printer.updateType(data["machine_variant"])

        # Do not store the buildplate information that comes from connect if the current printer has not buildplate information
        if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
            printer.updateBuildplateName(data["build_plate"]["type"])
        if not data["enabled"]:
            printer.updateState("disabled")
        else:
            printer.updateState(data["status"])

        for index in range(0, self._number_of_extruders):
            extruder = printer.extruders[index]
            try:
                extruder_data = data["configuration"][index]
            except IndexError:
                break

            extruder.updateHotendID(extruder_data.get("print_core_id", ""))

            material_data = extruder_data["material"]
            if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
                containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
                                                                                    GUID=material_data["guid"])
                if containers:
                    color = containers[0].getMetaDataEntry("color_code")
                    brand = containers[0].getMetaDataEntry("brand")
                    material_type = containers[0].getMetaDataEntry("material")
                    name = containers[0].getName()
                else:
                    Logger.log("w",
                               "Unable to find material with guid {guid}. Using data as provided by cluster".format(
                                   guid=material_data["guid"]))
                    color = material_data["color"]
                    brand = material_data["brand"]
                    material_type = material_data["material"]
                    name = "Empty" if material_data["material"] == "empty" else "Unknown"

                material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
                                               brand=brand, color=color, name=name)
                extruder.updateActiveMaterial(material)

    def _removeJob(self, job: PrintJobOutputModel):
        if job not in self._print_jobs:
            return False

        if job.assignedPrinter:
            job.assignedPrinter.updateActivePrintJob(None)
            job.stateChanged.disconnect(self._printJobStateChanged)
        self._print_jobs.remove(job)

        return True

    def _removePrinter(self, printer: PrinterOutputModel):
        self._printers.remove(printer)
        if self._active_printer == printer:
            self._active_printer = None
            self.activePrinterChanged.emit()
コード例 #48
0
ファイル: FlavorParser.py プロジェクト: workflowmate/Cura
class FlavorParser:
    """This parser is intended to interpret the common firmware codes among all the different flavors"""

    def __init__(self) -> None:
        CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
        self._cancelled = False
        self._message = None  # type: Optional[Message]
        self._layer_number = 0
        self._extruder_number = 0
        # All extruder numbers that have been seen
        self._extruders_seen = {0}  # type: Set[int]
        self._clearValues()
        self._scene_node = None
        # X, Y, Z position, F feedrate and E extruder values are stored
        self._position = Position
        self._is_layers_in_file = False  # Does the Gcode have the layers comment?
        self._extruder_offsets = {}  # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._current_layer_thickness = 0.2  # default
        self._filament_diameter = 2.85       # default
        self._previous_extrusion_value = 0.0  # keep track of the filament retractions

        CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)

    def _clearValues(self) -> None:
        self._extruder_number = 0
        self._extrusion_length_offset = [0] # type: List[float]
        self._layer_type = LayerPolygon.Inset0Type
        self._layer_number = 0
        self._previous_z = 0 # type: float
        self._layer_data_builder = LayerDataBuilder()
        self._is_absolute_positioning = True    # It can be absolute (G90) or relative (G91)
        self._is_absolute_extrusion = True  # It can become absolute (M82, default) or relative (M83)

    @staticmethod
    def _getValue(line: str, code: str) -> Optional[Union[str, int, float]]:
        n = line.find(code)
        if n < 0:
            return None
        n += len(code)
        pattern = re.compile("[;\\s]")
        match = pattern.search(line, n)
        m = match.start() if match is not None else -1
        try:
            if m < 0:
                return line[n:]
            return line[n:m]
        except:
            return None

    def _getInt(self, line: str, code: str) -> Optional[int]:
        value = self._getValue(line, code)
        try:
            return int(value)   # type: ignore
        except:
            return None

    def _getFloat(self, line: str, code: str) -> Optional[float]:
        value = self._getValue(line, code)
        try:
            return float(value) # type: ignore
        except:
            return None

    def _onHideMessage(self, message: str) -> None:
        if message == self._message:
            self._cancelled = True

    def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
        countvalid = 0
        for point in path:
            if point[5] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number, path[0][2])
            self._layer_data_builder.setLayerThickness(self._layer_number, layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
            if not this_layer:
                return False
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 3), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:
            points[i, :] = [point[0] + extruder_offsets[0], point[2], -point[1] - extruder_offsets[1]]
            extrusion_values[i] = point[4]
            if i > 0:
                line_feedrates[i - 1] = point[3]
                line_types[i - 1] = point[5]
                if point[5] in [LayerPolygon.MoveCombingType, LayerPolygon.MoveRetractionType]:
                    line_widths[i - 1] = 0.1
                    line_thicknesses[i - 1] = 0.0 # Travels are set as zero thickness lines
                else:
                    line_widths[i - 1] = self._calculateLineWidth(points[i], points[i-1], extrusion_values[i], extrusion_values[i-1], layer_thickness)
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points, line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _createEmptyLayer(self, layer_number: int) -> None:
        self._layer_data_builder.addLayer(layer_number)
        self._layer_data_builder.setLayerHeight(layer_number, 0)
        self._layer_data_builder.setLayerThickness(layer_number, 0)

    def _calculateLineWidth(self, current_point: Position, previous_point: Position, current_extrusion: float, previous_extrusion: float, layer_thickness: float) -> float:
        # Area of the filament
        Af = (self._filament_diameter / 2) ** 2 * numpy.pi
        # Length of the extruded filament
        de = current_extrusion - previous_extrusion
        # Volumne of the extruded filament
        dVe = de * Af
        # Length of the printed line
        dX = numpy.sqrt((current_point[0] - previous_point[0])**2 + (current_point[2] - previous_point[2])**2)
        # When the extruder recovers from a retraction, we get zero distance
        if dX == 0:
            return 0.1
        # Area of the printed line. This area is a rectangle
        Ae = dVe / dX
        # This area is a rectangle with area equal to layer_thickness * layer_width
        line_width = Ae / layer_thickness

        # A threshold is set to avoid weird paths in the GCode
        if line_width > 1.2:
            return 0.35
        # Prevent showing infinitely wide lines
        if line_width < 0.0:
            return 0.0
        return line_width

    def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        x, y, z, f, e = position

        if self._is_absolute_positioning:
            x = params.x if params.x is not None else x
            y = params.y if params.y is not None else y
            z = params.z if params.z is not None else z
        else:
            x += params.x if params.x is not None else 0
            y += params.y if params.y is not None else 0
            z += params.z if params.z is not None else 0

        f = params.f if params.f is not None else f

        if params.e is not None:
            new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
            if new_extrusion_value > e[self._extruder_number]:
                path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type])  # extrusion
                self._previous_extrusion_value = new_extrusion_value
            else:
                path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])  # retraction
            e[self._extruder_number] = new_extrusion_value

            # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
            # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
            if z > self._previous_z and (z - self._previous_z < 1.5):
                self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
                self._previous_z = z
        elif self._previous_extrusion_value > e[self._extruder_number]:
            path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
        else:
            path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
        return self._position(x, y, z, f, e)


    # G0 and G1 should be handled exactly the same.
    _gCode1 = _gCode0

    def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        """Home the head."""

        return self._position(
            params.x if params.x is not None else position.x,
            params.y if params.y is not None else position.y,
            params.z if params.z is not None else position.z,
            position.f,
            position.e)

    def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        """Set the absolute positioning"""

        self._is_absolute_positioning = True
        self._is_absolute_extrusion = True
        return position

    def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        """Set the relative positioning"""

        self._is_absolute_positioning = False
        self._is_absolute_extrusion = False
        return position

    def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
        """Reset the current position to the values specified.

        For example: G92 X10 will set the X to 10 without any physical motion.
        """

        if params.e is not None:
            # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
            self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
            position.e[self._extruder_number] = params.e
            self._previous_extrusion_value = params.e
        else:
            self._previous_extrusion_value = 0.0
        return self._position(
            params.x if params.x is not None else position.x,
            params.y if params.y is not None else position.y,
            params.z if params.z is not None else position.z,
            params.f if params.f is not None else position.f,
            position.e)

    def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        func = getattr(self, "_gCode%s" % G, None)
        line = line.split(";", 1)[0]  # Remove comments (if any)
        if func is not None:
            s = line.upper().split(" ")
            x, y, z, f, e = None, None, None, None, None
            for item in s[1:]:
                if len(item) <= 1:
                    continue
                if item.startswith(";"):
                    continue
                try:
                    if item[0] == "X":
                        x = float(item[1:])
                    elif item[0] == "Y":
                        y = float(item[1:])
                    elif item[0] == "Z":
                        z = float(item[1:])
                    elif item[0] == "F":
                        f = float(item[1:]) / 60
                    elif item[0] == "E":
                        e = float(item[1:])
                except ValueError:  # Improperly formatted g-code: Coordinates are not floats.
                    continue  # Skip the command then.
            params = PositionOptional(x, y, z, f, e)
            return func(position, params, path)
        return position

    def processTCode(self, T: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        self._extruder_number = T
        if self._extruder_number + 1 > len(position.e):
            self._extrusion_length_offset.extend([0] * (self._extruder_number - len(position.e) + 1))
            position.e.extend([0] * (self._extruder_number - len(position.e) + 1))
        return position

    def processMCode(self, M: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
        pass

    _type_keyword = ";TYPE:"
    _layer_keyword = ";LAYER:"

    def _extruderOffsets(self) -> Dict[int, List[float]]:
        """For showing correct x, y offsets for each extruder"""

        result = {}
        for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
            result[int(extruder.getMetaData().get("position", "0"))] = [
                extruder.getProperty("machine_nozzle_offset_x", "value"),
                extruder.getProperty("machine_nozzle_offset_y", "value")]
        return result

    #
    # CURA-6643
    # This function needs the filename so it can be set to the SceneNode. Otherwise, if you load a GCode file and press
    # F5, that gcode SceneNode will be removed because it doesn't have a file to be reloaded from.
    #
    def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
        Logger.log("d", "Preparing to load GCode")
        self._cancelled = False
        # We obtain the filament diameter from the selected extruder to calculate line widths
        global_stack = CuraApplication.getInstance().getGlobalContainerStack()

        if not global_stack:
            return None

        self._filament_diameter = global_stack.extruderList[self._extruder_number].getProperty("material_diameter", "value")

        scene_node = CuraSceneNode()

        gcode_list = []
        self._is_layers_in_file = False

        self._extruder_offsets = self._extruderOffsets()  # dict with index the extruder number. can be empty

        ##############################################################################################
        ##  This part is where the action starts
        ##############################################################################################
        file_lines = 0
        current_line = 0
        for line in stream.split("\n"):
            file_lines += 1
            gcode_list.append(line + "\n")
            if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
                self._is_layers_in_file = True

        file_step = max(math.floor(file_lines / 100), 1)

        self._clearValues()

        self._message = Message(catalog.i18nc("@info:status", "Parsing G-code"),
                                lifetime=0,
                                title = catalog.i18nc("@info:title", "G-code Details"))

        assert(self._message is not None) # use for typing purposes
        self._message.setProgress(0)
        self._message.show()

        Logger.log("d", "Parsing Gcode...")

        current_position = Position(0, 0, 0, 0, [0])
        current_path = [] #type: List[List[float]]
        min_layer_number = 0
        negative_layers = 0
        previous_layer = 0
        self._previous_extrusion_value = 0.0

        for line in stream.split("\n"):
            if self._cancelled:
                Logger.log("d", "Parsing Gcode file cancelled")
                return None
            current_line += 1

            if current_line % file_step == 0:
                self._message.setProgress(math.floor(current_line / file_lines * 100))
                Job.yieldThread()
            if len(line) == 0:
                continue

            if line.find(self._type_keyword) == 0:
                type = line[len(self._type_keyword):].strip()
                if type == "WALL-INNER":
                    self._layer_type = LayerPolygon.InsetXType
                elif type == "WALL-OUTER":
                    self._layer_type = LayerPolygon.Inset0Type
                elif type == "SKIN":
                    self._layer_type = LayerPolygon.SkinType
                elif type == "SKIRT":
                    self._layer_type = LayerPolygon.SkirtType
                elif type == "SUPPORT":
                    self._layer_type = LayerPolygon.SupportType
                elif type == "FILL":
                    self._layer_type = LayerPolygon.InfillType
                elif type == "SUPPORT-INTERFACE":
                    self._layer_type = LayerPolygon.SupportInterfaceType
                elif type == "PRIME-TOWER":
                    self._layer_type = LayerPolygon.PrimeTowerType
                else:
                    Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)

            # When the layer change is reached, the polygon is computed so we have just one layer per extruder
            if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
                try:
                    layer_number = int(line[len(self._layer_keyword):])
                    self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
                    current_path.clear()
                    # Start the new layer at the end position of the last layer
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])

                    # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
                    # as in ProcessSlicedLayersJob
                    if layer_number < min_layer_number:
                        min_layer_number = layer_number
                    if layer_number < 0:
                        layer_number += abs(min_layer_number)
                        negative_layers += 1
                    else:
                        layer_number += negative_layers

                    # In case there is a gap in the layer count, empty layers are created
                    for empty_layer in range(previous_layer + 1, layer_number):
                        self._createEmptyLayer(empty_layer)

                    self._layer_number = layer_number
                    previous_layer = layer_number
                except:
                    pass

            # This line is a comment. Ignore it (except for the layer_keyword)
            if line.startswith(";"):
                continue

            G = self._getInt(line, "G")
            if G is not None:
                # When find a movement, the new posistion is calculated and added to the current_path, but
                # don't need to create a polygon until the end of the layer
                current_position = self.processGCode(G, line, current_position, current_path)
                continue

            # When changing the extruder, the polygon with the stored paths is computed
            if line.startswith("T"):
                T = self._getInt(line, "T")
                if T is not None:
                    self._extruders_seen.add(T)
                    self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
                    current_path.clear()

                    # When changing tool, store the end point of the previous path, then process the code and finally
                    # add another point with the new position of the head.
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])
                    current_position = self.processTCode(T, line, current_position, current_path)
                    current_path.append([current_position.x, current_position.y, current_position.z, current_position.f, current_position.e[self._extruder_number], LayerPolygon.MoveCombingType])

            if line.startswith("M"):
                M = self._getInt(line, "M")
                if M is not None:
                    self.processMCode(M, line, current_position, current_path)

        # "Flush" leftovers. Last layer paths are still stored
        if len(current_path) > 1:
            if self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])):
                self._layer_number += 1
                current_path.clear()

        material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
        material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
        material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
        material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
        material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
        material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGcodeFileName(filename)
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        # gcode_dict stores gcode_lists for a number of build plates.
        active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.

        Logger.log("d", "Finished parsing Gcode")
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w", "File doesn't contain any valid layers")

        if not global_stack.getProperty("machine_center_is_zero", "value"):
            machine_width = global_stack.getProperty("machine_width", "value")
            machine_depth = global_stack.getProperty("machine_depth", "value")
            scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "GCode loading finished")

        if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
                lifetime=0,
                title = catalog.i18nc("@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = CuraApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
コード例 #49
0
ファイル: SimulationView.py プロジェクト: Twosilly/Cura
class SimulationView(View):
    # Must match SimulationView.qml
    LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
    LAYER_VIEW_TYPE_LINE_TYPE = 1
    LAYER_VIEW_TYPE_FEEDRATE = 2
    LAYER_VIEW_TYPE_THICKNESS = 3

    def __init__(self):
        super().__init__()

        self._max_layers = 0
        self._current_layer_num = 0
        self._minimum_layer_num = 0
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._top_layers_job = None
        self._activity = False
        self._old_max_layers = 0

        self._max_paths = 0
        self._current_path_num = 0
        self._minimum_path_num = 0
        self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)

        self._busy = False
        self._simulation_running = False

        self._ghost_shader = None
        self._layer_pass = None
        self._composite_pass = None
        self._old_layer_bindings = None
        self._simulationview_composite_shader = None
        self._old_composite_shader = None

        self._global_container_stack = None
        self._proxy = SimulationViewProxy()
        self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)

        self._resetSettings()
        self._legend_items = None
        self._show_travel_moves = False
        self._nozzle_node = None

        Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
        Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
        Application.getInstance().getPreferences().addPreference("view/force_layer_view_compatibility_mode", False)

        Application.getInstance().getPreferences().addPreference("layerview/layer_view_type", 0)
        Application.getInstance().getPreferences().addPreference("layerview/extruder_opacities", "")

        Application.getInstance().getPreferences().addPreference("layerview/show_travel_moves", False)
        Application.getInstance().getPreferences().addPreference("layerview/show_helpers", True)
        Application.getInstance().getPreferences().addPreference("layerview/show_skin", True)
        Application.getInstance().getPreferences().addPreference("layerview/show_infill", True)

        Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
        self._updateWithPreferences()

        self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
                                                  title = catalog.i18nc("@info:title", "Simulation View"))

    def _evaluateCompatibilityMode(self):
        return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode"))

    def _resetSettings(self):
        self._layer_view_type = 0  # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
        self._extruder_count = 0
        self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
        self._show_travel_moves = 0
        self._show_helpers = 1
        self._show_skin = 1
        self._show_infill = 1
        self.resetLayerData()

    def getActivity(self):
        return self._activity

    def setActivity(self, activity):
        if self._activity == activity:
            return
        self._activity = activity
        self.activityChanged.emit()

    def getSimulationPass(self):
        if not self._layer_pass:
            # Currently the RenderPass constructor requires a size > 0
            # This should be fixed in RenderPass's constructor.
            self._layer_pass = SimulationPass(1, 1)
            self._compatibility_mode = self._evaluateCompatibilityMode()
            self._layer_pass.setSimulationView(self)
        return self._layer_pass

    def getCurrentLayer(self):
        return self._current_layer_num

    def getMinimumLayer(self):
        return self._minimum_layer_num

    def getMaxLayers(self):
        return self._max_layers

    def getCurrentPath(self):
        return self._current_path_num

    def getMinimumPath(self):
        return self._minimum_path_num

    def getMaxPaths(self):
        return self._max_paths

    def getNozzleNode(self):
        if not self._nozzle_node:
            self._nozzle_node = NozzleNode()
        return self._nozzle_node

    def _onSceneChanged(self, node):
        if node.getMeshData() is None:
            self.resetLayerData()

        self.setActivity(False)
        self.calculateMaxLayers()
        self.calculateMaxPathsOnLayer(self._current_layer_num)

    def isBusy(self):
        return self._busy

    def setBusy(self, busy):
        if busy != self._busy:
            self._busy = busy
            self.busyChanged.emit()

    def isSimulationRunning(self):
        return self._simulation_running

    def setSimulationRunning(self, running):
        self._simulation_running = running

    def resetLayerData(self):
        self._current_layer_mesh = None
        self._current_layer_jumps = None
        self._max_feedrate = sys.float_info.min
        self._min_feedrate = sys.float_info.max
        self._max_thickness = sys.float_info.min
        self._min_thickness = sys.float_info.max

    def beginRendering(self):
        scene = self.getController().getScene()
        renderer = self.getRenderer()

        if not self._ghost_shader:
            self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
            self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb()))

        for node in DepthFirstIterator(scene.getRoot()):
            # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
            # However, it is somewhat relevant when the node is selected, so do render it then.
            if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
                continue

            if not node.render(renderer):
                if (node.getMeshData()) and node.isVisible():
                    renderer.queueNode(node, transparent = True, shader = self._ghost_shader)

    def setLayer(self, value):
        if self._current_layer_num != value:
            self._current_layer_num = value
            if self._current_layer_num < 0:
                self._current_layer_num = 0
            if self._current_layer_num > self._max_layers:
                self._current_layer_num = self._max_layers
            if self._current_layer_num < self._minimum_layer_num:
                self._minimum_layer_num = self._current_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setMinimumLayer(self, value):
        if self._minimum_layer_num != value:
            self._minimum_layer_num = value
            if self._minimum_layer_num < 0:
                self._minimum_layer_num = 0
            if self._minimum_layer_num > self._max_layers:
                self._minimum_layer_num = self._max_layers
            if self._minimum_layer_num > self._current_layer_num:
                self._current_layer_num = self._minimum_layer_num

            self._startUpdateTopLayers()

            self.currentLayerNumChanged.emit()

    def setPath(self, value):
        if self._current_path_num != value:
            self._current_path_num = value
            if self._current_path_num < 0:
                self._current_path_num = 0
            if self._current_path_num > self._max_paths:
                self._current_path_num = self._max_paths
            if self._current_path_num < self._minimum_path_num:
                self._minimum_path_num = self._current_path_num

            self._startUpdateTopLayers()

            self.currentPathNumChanged.emit()

    def setMinimumPath(self, value):
        if self._minimum_path_num != value:
            self._minimum_path_num = value
            if self._minimum_path_num < 0:
                self._minimum_path_num = 0
            if self._minimum_path_num > self._max_layers:
                self._minimum_path_num = self._max_layers
            if self._minimum_path_num > self._current_path_num:
                self._current_path_num = self._minimum_path_num

            self._startUpdateTopLayers()

            self.currentPathNumChanged.emit()

    ##  Set the layer view type
    #
    #   \param layer_view_type integer as in SimulationView.qml and this class
    def setSimulationViewType(self, layer_view_type):
        self._layer_view_type = layer_view_type
        self.currentLayerNumChanged.emit()

    ##  Return the layer view type, integer as in SimulationView.qml and this class
    def getSimulationViewType(self):
        return self._layer_view_type

    ##  Set the extruder opacity
    #
    #   \param extruder_nr 0..3
    #   \param opacity 0.0 .. 1.0
    def setExtruderOpacity(self, extruder_nr, opacity):
        if 0 <= extruder_nr <= 3:
            self._extruder_opacity[extruder_nr] = opacity
            self.currentLayerNumChanged.emit()

    def getExtruderOpacities(self):
        return self._extruder_opacity

    def setShowTravelMoves(self, show):
        self._show_travel_moves = show
        self.currentLayerNumChanged.emit()

    def getShowTravelMoves(self):
        return self._show_travel_moves

    def setShowHelpers(self, show):
        self._show_helpers = show
        self.currentLayerNumChanged.emit()

    def getShowHelpers(self):
        return self._show_helpers

    def setShowSkin(self, show):
        self._show_skin = show
        self.currentLayerNumChanged.emit()

    def getShowSkin(self):
        return self._show_skin

    def setShowInfill(self, show):
        self._show_infill = show
        self.currentLayerNumChanged.emit()

    def getShowInfill(self):
        return self._show_infill

    def getCompatibilityMode(self):
        return self._compatibility_mode

    def getExtruderCount(self):
        return self._extruder_count

    def getMinFeedrate(self):
        return self._min_feedrate

    def getMaxFeedrate(self):
        return self._max_feedrate

    def getMinThickness(self):
        return self._min_thickness

    def getMaxThickness(self):
        return self._max_thickness

    def calculateMaxLayers(self):
        scene = self.getController().getScene()

        self._old_max_layers = self._max_layers
        ## Recalculate num max layers
        new_max_layers = 0
        for node in DepthFirstIterator(scene.getRoot()):
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            self.setActivity(True)
            min_layer_number = sys.maxsize
            max_layer_number = -sys.maxsize
            for layer_id in layer_data.getLayers():

                # If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
                if len(layer_data.getLayer(layer_id).polygons) < 1:
                    continue

                # Store the max and min feedrates and thicknesses for display purposes
                for p in layer_data.getLayer(layer_id).polygons:
                    self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
                    self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
                    self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
                    try:
                        self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
                    except:
                        # Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
                        # the zero) can't be calculated
                        Logger.log("i", "Min thickness can't be calculated because all the values are zero")
                if max_layer_number < layer_id:
                    max_layer_number = layer_id
                if min_layer_number > layer_id:
                    min_layer_number = layer_id
            layer_count = max_layer_number - min_layer_number

            if new_max_layers < layer_count:
                new_max_layers = layer_count

        if new_max_layers > 0 and new_max_layers != self._old_max_layers:
            self._max_layers = new_max_layers

            # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
            # if it's the largest value. If we don't do this, we can have a slider block outside of the
            # slider.
            if new_max_layers > self._current_layer_num:
                self.maxLayersChanged.emit()
                self.setLayer(int(self._max_layers))
            else:
                self.setLayer(int(self._max_layers))
                self.maxLayersChanged.emit()
        self._startUpdateTopLayers()

    def calculateMaxPathsOnLayer(self, layer_num):
        # Update the currentPath
        scene = self.getController().getScene()
        for node in DepthFirstIterator(scene.getRoot()):
            layer_data = node.callDecoration("getLayerData")
            if not layer_data:
                continue

            layer = layer_data.getLayer(layer_num)
            if layer is None:
                return
            new_max_paths = layer.lineMeshElementCount()
            if new_max_paths >= 0 and new_max_paths != self._max_paths:
                self._max_paths = new_max_paths
                self.maxPathsChanged.emit()

            self.setPath(int(new_max_paths))

    maxLayersChanged = Signal()
    maxPathsChanged = Signal()
    currentLayerNumChanged = Signal()
    currentPathNumChanged = Signal()
    globalStackChanged = Signal()
    preferencesChanged = Signal()
    busyChanged = Signal()
    activityChanged = Signal()

    ##  Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
    #   as this caused some issues.
    def getProxy(self, engine, script_engine):
        return self._proxy

    def endRendering(self):
        pass

    def event(self, event):
        modifiers = QApplication.keyboardModifiers()
        ctrl_is_active = modifiers & Qt.ControlModifier
        shift_is_active = modifiers & Qt.ShiftModifier
        if event.type == Event.KeyPressEvent and ctrl_is_active:
            amount = 10 if shift_is_active else 1
            if event.key == KeyEvent.UpKey:
                self.setLayer(self._current_layer_num + amount)
                return True
            if event.key == KeyEvent.DownKey:
                self.setLayer(self._current_layer_num - amount)
                return True

        if event.type == Event.ViewActivateEvent:
            # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
            # This can happen when you do the following steps:
            #   1. Start Cura
            #   2. Load a model
            #   3. Switch to Custom mode
            #   4. Select the model and click on the per-object tool icon
            #   5. Switch view to Layer view or X-Ray
            #   6. Cura will very likely crash
            # It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why.
            # This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL
            # context is None.
            if Platform.isOSX():
                if QOpenGLContext.currentContext() is None:
                    Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
                    CuraApplication.getInstance().callLater(lambda e=event: self.event(e))
                    return

            # Make sure the SimulationPass is created
            layer_pass = self.getSimulationPass()
            self.getRenderer().addRenderPass(layer_pass)

            # Make sure the NozzleNode is add to the root
            nozzle = self.getNozzleNode()
            nozzle.setParent(self.getController().getScene().getRoot())
            nozzle.setVisible(False)

            Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
            self._onGlobalStackChanged()

            if not self._simulationview_composite_shader:
                self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
                theme = Application.getInstance().getTheme()
                self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
                self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))

            if not self._composite_pass:
                self._composite_pass = self.getRenderer().getRenderPass("composite")

            self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
            self._composite_pass.getLayerBindings().append("simulationview")
            self._old_composite_shader = self._composite_pass.getCompositeShader()
            self._composite_pass.setCompositeShader(self._simulationview_composite_shader)

        elif event.type == Event.ViewDeactivateEvent:
            self._wireprint_warning_message.hide()
            Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
            if self._global_container_stack:
                self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)

            self._nozzle_node.setParent(None)
            self.getRenderer().removeRenderPass(self._layer_pass)
            self._composite_pass.setLayerBindings(self._old_layer_bindings)
            self._composite_pass.setCompositeShader(self._old_composite_shader)

    def getCurrentLayerMesh(self):
        return self._current_layer_mesh

    def getCurrentLayerJumps(self):
        return self._current_layer_jumps

    def _onGlobalStackChanged(self):
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
        self._global_container_stack = Application.getInstance().getGlobalContainerStack()
        if self._global_container_stack:
            self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
            self._extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
            self._onPropertyChanged("wireframe_enabled", "value")
            self.globalStackChanged.emit()
        else:
            self._wireprint_warning_message.hide()

    def _onPropertyChanged(self, key, property_name):
        if key == "wireframe_enabled" and property_name == "value":
            if self._global_container_stack.getProperty("wireframe_enabled", "value"):
                self._wireprint_warning_message.show()
            else:
                self._wireprint_warning_message.hide()

    def _onCurrentLayerNumChanged(self):
        self.calculateMaxPathsOnLayer(self._current_layer_num)

    def _startUpdateTopLayers(self):
        if not self._compatibility_mode:
            return

        if self._top_layers_job:
            self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
            self._top_layers_job.cancel()

        self.setBusy(True)

        self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
        self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
        self._top_layers_job.start()

    def _updateCurrentLayerMesh(self, job):
        self.setBusy(False)

        if not job.getResult():
            return
        self.resetLayerData()  # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
        self._current_layer_mesh = job.getResult().get("layers")
        if self._show_travel_moves:
            self._current_layer_jumps = job.getResult().get("jumps")
        self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())

        self._top_layers_job = None

    def _updateWithPreferences(self):
        self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
        self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
        self._compatibility_mode = self._evaluateCompatibilityMode()

        self.setSimulationViewType(int(float(Application.getInstance().getPreferences().getValue("layerview/layer_view_type"))));

        for extruder_nr, extruder_opacity in enumerate(Application.getInstance().getPreferences().getValue("layerview/extruder_opacities").split("|")):
            try:
                opacity = float(extruder_opacity)
            except ValueError:
                opacity = 1.0
            self.setExtruderOpacity(extruder_nr, opacity)

        self.setShowTravelMoves(bool(Application.getInstance().getPreferences().getValue("layerview/show_travel_moves")))
        self.setShowHelpers(bool(Application.getInstance().getPreferences().getValue("layerview/show_helpers")))
        self.setShowSkin(bool(Application.getInstance().getPreferences().getValue("layerview/show_skin")))
        self.setShowInfill(bool(Application.getInstance().getPreferences().getValue("layerview/show_infill")))

        self._startUpdateTopLayers()
        self.preferencesChanged.emit()

    def _onPreferencesChanged(self, preference):
        if preference not in {
            "view/top_layer_count",
            "view/only_show_top_layers",
            "view/force_layer_view_compatibility_mode",
            "layerview/layer_view_type",
            "layerview/extruder_opacities",
            "layerview/show_travel_moves",
            "layerview/show_helpers",
            "layerview/show_skin",
            "layerview/show_infill",
            }:
            return

        self._updateWithPreferences()
コード例 #50
0
 def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
     sync_message.hide()
     self.discrepancies.emit(self._model)
コード例 #51
0
ファイル: CuraEngineBackend.py プロジェクト: rphauck/Cura
class CuraEngineBackend(Backend):
    def __init__(self):
        super().__init__()

        # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
        default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", "CuraEngine")
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
        if sys.platform == "win32":
            default_engine_location += ".exe"
        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location", default_engine_location)

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = None

        # When there are current settings and machine instance is changed, there is no profile changed event. We should
        # pretend there is though.
        Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveProfileChanged)

        self._profile = None
        Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
        self._onActiveProfileChanged()

        self._change_timer = QTimer()
        self._change_timer.setInterval(500)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self.slice)

        self._message_handlers["cura.proto.SlicedObjectList"] = self._onSlicedObjectListMessage
        self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
        self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
        self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
        self._message_handlers["cura.proto.ObjectPrintTime"] = self._onObjectPrintTimeMessage
        self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage

        self._slicing = False
        self._restart = False
        self._enabled = True
        self._always_restart = True
        self._process_layers_job = None #The currently active job to process layers, or None if it is not processing layers.

        self._message = None

        self.backendQuit.connect(self._onBackendQuit)

        self.backendConnected.connect(self._onBackendConnected)
        Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
        Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)

        Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onInstanceChanged)

    ##  Get the command that is used to call the engine.
    #   This is usefull for debugging and used to actually start the engine
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        active_machine = Application.getInstance().getMachineManager().getActiveMachineInstance()
        if not active_machine:
            return None

        return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", active_machine.getMachineDefinition().getPath(), "-vv"]

    ##  Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted whne the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    ##  Perform a slice of the scene.
    def slice(self):

        if not self._enabled:
            return

        if self._slicing:
            self._terminate()

            if self._message:
                self._message.hide()
                self._message = None

            return

        if self._process_layers_job:
            self._process_layers_job.abort()
            self._process_layers_job = None

        if self._profile.hasErrorValue():
            Logger.log("w", "Profile has error values. Aborting slicing")
            if self._message:
                self._message.hide()
                self._message = None
            self._message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
            self._message.show()
            return #No slicing if we have error values since those are by definition illegal values.

        self.processingProgress.emit(0.0)
        self.backendStateChange.emit(BackendState.NOT_STARTED)
        if self._message:
            self._message.setProgress(-1)
        #else:
        #    self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
        #    self._message.show()

        self._scene.gcode_list = []
        self._slicing = True
        self.slicingStarted.emit()

        job = StartSliceJob.StartSliceJob(self._profile, self._socket)
        job.start()
        job.finished.connect(self._onStartSliceCompleted)

    def _terminate(self):
        self._slicing = False
        self._restart = True
        self.slicingCancelled.emit()
        self.processingProgress.emit(0)
        Logger.log("d", "Attempting to kill the engine process")
        if self._process is not None:
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()
                Logger.log("d", "Engine process is killed. Recieved return code %s", self._process.wait())
                self._process = None
                #self._createSocket() # Re create the socket
            except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
                Logger.log("d", "Exception occured while trying to kill the engine %s", str(e))


    def _onStartSliceCompleted(self, job):
        if job.getError() or job.getResult() != True:
            if self._message:
                self._message.hide()
                self._message = None
            return

    def _onSceneChanged(self, source):
        if type(source) is not SceneNode:
            return

        if source is self._scene.getRoot():
            return

        if source.getMeshData() is None:
            return

        if source.getMeshData().getVertices() is None:
            return

        self._onChanged()

    def _onSocketError(self, error):
        super()._onSocketError(error)

        self._terminate()

        if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
            Logger.log("e", "A socket error caused the connection to be reset")

    def _onActiveProfileChanged(self):
        if self._profile:
            self._profile.settingValueChanged.disconnect(self._onSettingChanged)

        self._profile = Application.getInstance().getMachineManager().getWorkingProfile()
        if self._profile:
            self._profile.settingValueChanged.connect(self._onSettingChanged)
            self._onChanged()

    def _onSettingChanged(self, setting):
        self._onChanged()

    def _onSlicedObjectListMessage(self, message):
        if self._layer_view_active:
            self._process_layers_job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(message)
            self._process_layers_job.start()
        else :
            self._stored_layer_data = message

    def _onProgressMessage(self, message):
        if self._message:
            self._message.setProgress(round(message.amount * 100))

        self.processingProgress.emit(message.amount)
        self.backendStateChange.emit(BackendState.PROCESSING)

    def _onSlicingFinishedMessage(self, message):
        self.backendStateChange.emit(BackendState.DONE)
        self.processingProgress.emit(1.0)

        self._slicing = False

        if self._message:
            self._message.setProgress(100)
            self._message.hide()
            self._message = None

    def _onGCodeLayerMessage(self, message):
        self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))

    def _onGCodePrefixMessage(self, message):
        self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))

    def _onObjectPrintTimeMessage(self, message):
        self.printDurationMessage.emit(message.time, message.material_amount)

    def _createSocket(self):
        super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))

    ##  Manually triggers a reslice
    def forceSlice(self):
        self._change_timer.start()

    def _onChanged(self):
        if not self._profile:
            return

        self._change_timer.start()

    def _onBackendConnected(self):
        if self._restart:
            self._onChanged()
            self._restart = False

    def _onToolOperationStarted(self, tool):
        self._terminate() # Do not continue slicing once a tool has started
        self._enabled = False # Do not reslice when a tool is doing it's 'thing'

    def _onToolOperationStopped(self, tool):
        self._enabled = True # Tool stop, start listening for changes again.

    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "LayerView":
                self._layer_view_active = True
                # There is data and we're not slicing at the moment
                # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
                if self._stored_layer_data and not self._slicing:
                    self._process_layers_job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(self._stored_layer_data)
                    self._process_layers_job.start()
                    self._stored_layer_data = None
            else:
                self._layer_view_active = False

    def _onInstanceChanged(self):
        self._terminate()

    def _onBackendQuit(self):
        if not self._restart and self._process:
            Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
            self._process = None
            self._createSocket()
コード例 #52
0
class FlavorParser:
    def __init__(self):
        Application.getInstance().hideMessageSignal.connect(
            self._onHideMessage)
        self._cancelled = False
        self._message = None
        self._layer_number = 0
        self._extruder_number = 0
        self._clearValues()
        self._scene_node = None
        # X, Y, Z position, F feedrate and E extruder values are stored
        self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
        self._is_layers_in_file = False  # Does the Gcode have the layers comment?
        self._extruder_offsets = {
        }  # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
        self._current_layer_thickness = 0.2  # default

        self._total_move_length = 0
        self._extrusion_retraction_length = 0
        self._extrusion_max_amounts = [0]
        self._extrusion_saved_value = [0]

        Preferences.getInstance().addPreference("gcodereader/show_caution",
                                                True)

    def _clearValues(self):
        self._filament_diameter = 2.85
        self._extruder_number = 0
        self._extrusion_length_offset = [0]
        self._layer_type = LayerPolygon.Inset0Type
        self._layer_number = 0
        self._previous_z = 0
        self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
        self._center_is_zero = False
        self._is_absolute_positioning = True  # It can be absolute (G90) or relative (G91)
        self._is_absolute_extrusion = True  # It can become absolute (M82, default) or relative (M83)
        self._total_move_length = 0
        self._extrusion_retraction_length = 0
        self._extrusion_max_amounts = [0]
        self._extrusion_saved_value = [0]

    @staticmethod
    def _getValue(line, code):
        n = line.find(code)
        if n < 0:
            return None
        n += len(code)
        pattern = re.compile("[;\s]")
        match = pattern.search(line, n)
        m = match.start() if match is not None else -1
        try:
            if m < 0:
                return line[n:]
            return line[n:m]
        except:
            return None

    def _getInt(self, line, code):
        value = self._getValue(line, code)
        try:
            return int(value)
        except:
            return None

    def _getFloat(self, line, code):
        value = self._getValue(line, code)
        try:
            return float(value)
        except:
            return None

    def _onHideMessage(self, message):
        if message == self._message:
            self._cancelled = True

    @staticmethod
    def _getNullBoundingBox():
        return AxisAlignedBox(minimum=Vector(0, 0, 0),
                              maximum=Vector(10, 10, 10))

    def _createPolygon(self, layer_thickness, path, extruder_offsets):
        countvalid = 0
        for point in path:
            if point[5] > 0:
                countvalid += 1
                if countvalid >= 2:
                    # we know what to do now, no need to count further
                    continue
        if countvalid < 2:
            return False
        try:
            self._layer_data_builder.addLayer(self._layer_number)
            self._layer_data_builder.setLayerHeight(self._layer_number,
                                                    path[0][2])
            self._layer_data_builder.setLayerThickness(self._layer_number,
                                                       layer_thickness)
            this_layer = self._layer_data_builder.getLayer(self._layer_number)
        except ValueError:
            return False
        count = len(path)
        line_types = numpy.empty((count - 1, 1), numpy.int32)
        line_widths = numpy.empty((count - 1, 1), numpy.float32)
        line_thicknesses = numpy.empty((count - 1, 1), numpy.float32)
        line_feedrates = numpy.empty((count - 1, 1), numpy.float32)
        line_widths[:, 0] = 0.35  # Just a guess
        line_thicknesses[:, 0] = layer_thickness
        points = numpy.empty((count, 3), numpy.float32)
        extrusion_values = numpy.empty((count, 1), numpy.float32)
        i = 0
        for point in path:
            points[i, :] = [
                point[0] + extruder_offsets[0], point[2],
                -point[1] - extruder_offsets[1]
            ]
            extrusion_values[i] = point[4]
            if i > 0:
                line_feedrates[i - 1] = point[3]
                line_types[i - 1] = point[5]
                if point[5] in [
                        LayerPolygon.MoveCombingType,
                        LayerPolygon.MoveRetractionType
                ]:
                    line_widths[i - 1] = 0.1
                    line_thicknesses[
                        i - 1] = 0.0  # Travels are set as zero thickness lines
                else:
                    line_widths[i - 1] = self._calculateLineWidth(
                        points[i], points[i - 1], extrusion_values[i],
                        extrusion_values[i - 1], layer_thickness)
            i += 1

        this_poly = LayerPolygon(self._extruder_number, line_types, points,
                                 line_widths, line_thicknesses, line_feedrates)
        this_poly.buildCache()

        this_layer.polygons.append(this_poly)
        return True

    def _createEmptyLayer(self, layer_number):
        self._layer_data_builder.addLayer(layer_number)
        self._layer_data_builder.setLayerHeight(layer_number, 0)
        self._layer_data_builder.setLayerThickness(layer_number, 0)

    def _calculateLineWidth(self, current_point, previous_point,
                            current_extrusion, previous_extrusion,
                            layer_thickness):
        # Area of the filament
        Af = (self._filament_diameter / 2)**2 * numpy.pi
        # Length of the extruded filament
        de = current_extrusion - previous_extrusion
        # Volumne of the extruded filament
        dVe = de * Af
        # Length of the printed line
        dX = numpy.sqrt((current_point[0] - previous_point[0])**2 +
                        (current_point[2] - previous_point[2])**2)
        # When the extruder recovers from a retraction, we get zero distance
        if dX == 0:
            return 0.1
        # Area of the printed line. This area is a rectangle
        Ae = dVe / dX
        # This area is a rectangle with area equal to layer_thickness * layer_width
        line_width = Ae / layer_thickness

        # A threshold is set to avoid weird paths in the GCode
        if line_width > 1.2:
            return 0.35
        return line_width

    def _gCode0(self, position, params, path):
        x, y, z, f, e = position

        if self._is_absolute_positioning:
            x = params.x if params.x is not None else x
            y = params.y if params.y is not None else y
            z = params.z if params.z is not None else z
        else:
            x += params.x if params.x is not None else 0
            y += params.y if params.y is not None else 0
            z += params.z if params.z is not None else 0

        f = params.f if params.f is not None else f
        self._total_move_length += math.sqrt(
            math.pow(x - position.x, 2) + math.pow(y - position.y, 2) +
            math.pow(z - position.z, 2))

        if params.e is not None:
            self._extrusion_retraction_length += math.sqrt(
                math.pow(x - position.x, 2) + math.pow(y - position.y, 2) +
                math.pow(z - position.z, 2))

            new_extrusion_value = params.e if self._is_absolute_extrusion else e[
                self._extruder_number] + params.e
            if new_extrusion_value > e[self._extruder_number]:
                path.append([
                    x, y, z, f, new_extrusion_value +
                    self._extrusion_length_offset[self._extruder_number],
                    self._layer_type
                ])  # extrusion
            else:
                path.append([
                    x, y, z, f, new_extrusion_value +
                    self._extrusion_length_offset[self._extruder_number],
                    LayerPolygon.MoveRetractionType
                ])  # retraction
            e[self._extruder_number] = new_extrusion_value

            # Only when extruding we can determine the latest known "layer height" which is the difference in height between extrusions
            # Also, 1.5 is a heuristic for any priming or whatsoever, we skip those.
            if z > self._previous_z and (z - self._previous_z < 1.5):
                self._current_layer_thickness = z - self._previous_z  # allow a tiny overlap
                self._previous_z = z
        else:
            path.append([
                x, y, z, f, e[self._extruder_number] +
                self._extrusion_length_offset[self._extruder_number],
                LayerPolygon.MoveCombingType
            ])
        return self._position(x, y, z, f, e)

    # G0 and G1 should be handled exactly the same.
    _gCode1 = _gCode0

    ##  Home the head.
    def _gCode28(self, position, params, path):
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              params.z if params.z is not None else position.z,
                              position.f, position.e)

    ##  Set the absolute positioning
    def _gCode90(self, position, params, path):
        self._is_absolute_positioning = True
        self._is_absolute_extrusion = True
        return position

    ##  Set the relative positioning
    def _gCode91(self, position, params, path):
        self._is_absolute_positioning = False
        self._is_absolute_extrusion = False
        return position

    ##  Reset the current position to the values specified.
    #   For example: G92 X10 will set the X to 10 without any physical motion.
    def _gCode92(self, position, params, path):
        if params.e is not None:
            # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
            self._extrusion_length_offset[self._extruder_number] += position.e[
                self._extruder_number] - params.e
            position.e[self._extruder_number] = params.e
        return self._position(params.x if params.x is not None else position.x,
                              params.y if params.y is not None else position.y,
                              params.z if params.z is not None else position.z,
                              params.f if params.f is not None else position.f,
                              position.e)

    def processGCode(self, G, line, position, path):
        func = getattr(self, "_gCode%s" % G, None)
        line = line.split(";", 1)[0]  # Remove comments (if any)
        if func is not None:
            s = line.upper().split(" ")
            x, y, z, f, e = None, None, None, None, None
            for item in s[1:]:
                if len(item) <= 1:
                    continue
                if item.startswith(";"):
                    continue
                if item[0] == "X":
                    x = float(item[1:])
                if item[0] == "Y":
                    y = float(item[1:])
                if item[0] == "Z":
                    z = float(item[1:])
                if item[0] == "F":
                    f = float(item[1:]) / 60
                if item[0] == "E":
                    e = float(item[1:])
            if self._is_absolute_positioning and ((x is not None and x < 0) or
                                                  (y is not None and y < 0)):
                self._center_is_zero = True
            params = self._position(x, y, z, f, e)
            return func(position, params, path)
        return position

    def processTCode(self, T, line, position, path):
        self._extruder_number = T
        if self._extruder_number + 1 > len(position.e):
            self._extrusion_length_offset.extend(
                [0] * (self._extruder_number - len(position.e) + 1))
            position.e.extend([0] *
                              (self._extruder_number - len(position.e) + 1))
            self._extrusion_max_amounts.extend(
                [0] * (self._extruder_number - len(position.e) + 1))
            self._extrusion_saved_value.extend(
                [0] * (self._extruder_number - len(position.e) + 1))
        return position

    def processMCode(self, M, line, position, path):
        pass

    _type_keyword = ";TYPE:"
    _layer_keyword = ";LAYER:"

    ##  For showing correct x, y offsets for each extruder
    def _extruderOffsets(self):
        result = {}
        for extruder in ExtruderManager.getInstance().getExtruderStacks():
            result[int(extruder.getMetaData().get("position", "0"))] = [
                extruder.getProperty("machine_nozzle_offset_x", "value"),
                extruder.getProperty("machine_nozzle_offset_y", "value")
            ]
        return result

    def processGCodeFile(self, file_name):
        Logger.log("d", "Preparing to load %s" % file_name)
        self._cancelled = False
        # We obtain the filament diameter from the selected printer to calculate line widths
        self._filament_diameter = Application.getInstance(
        ).getGlobalContainerStack().getProperty("material_diameter", "value")

        scene_node = CuraSceneNode()
        # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
        # real data to calculate it from.
        scene_node.getBoundingBox = self._getNullBoundingBox

        gcode_list = []
        self._is_layers_in_file = False

        Logger.log("d", "Opening file %s" % file_name)

        self._extruder_offsets = self._extruderOffsets(
        )  # dict with index the extruder number. can be empty

        with open(file_name, "r") as file:
            file_lines = 0
            current_line = 0
            for line in file:
                file_lines += 1
                gcode_list.append(line)
                if not self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    self._is_layers_in_file = True
            file.seek(0)

            file_step = max(math.floor(file_lines / 100), 1)

            self._clearValues()

            self._message = Message(catalog.i18nc("@info:status",
                                                  "Parsing G-code"),
                                    lifetime=0,
                                    title=catalog.i18nc(
                                        "@info:title", "G-code Details"))

            self._message.setProgress(0)
            self._message.show()

            Logger.log("d", "Parsing %s..." % file_name)

            current_position = self._position(0, 0, 0, 0, [0])
            current_path = []
            min_layer_number = 0
            negative_layers = 0
            previous_layer = 0

            for line in file:
                if self._cancelled:
                    Logger.log("d", "Parsing %s cancelled" % file_name)
                    return None
                current_line += 1

                if current_line % file_step == 0:
                    self._message.setProgress(
                        math.floor(current_line / file_lines * 100))
                    Job.yieldThread()
                if len(line) == 0:
                    continue

                if line.find(self._type_keyword) == 0:
                    type = line[len(self._type_keyword):].strip()
                    if type == "WALL-INNER":
                        self._layer_type = LayerPolygon.InsetXType
                    elif type == "WALL-OUTER":
                        self._layer_type = LayerPolygon.Inset0Type
                    elif type == "SKIN":
                        self._layer_type = LayerPolygon.SkinType
                    elif type == "SKIRT":
                        self._layer_type = LayerPolygon.SkirtType
                    elif type == "SUPPORT":
                        self._layer_type = LayerPolygon.SupportType
                    elif type == "FILL":
                        self._layer_type = LayerPolygon.InfillType
                    else:
                        Logger.log(
                            "w",
                            "Encountered a unknown type (%s) while parsing g-code.",
                            type)

                # When the layer change is reached, the polygon is computed so we have just one layer per layer per extruder
                if self._is_layers_in_file and line[:len(
                        self._layer_keyword)] == self._layer_keyword:
                    try:
                        layer_number = int(line[len(self._layer_keyword):])
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()

                        # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior
                        # as in ProcessSlicedLayersJob
                        if layer_number < min_layer_number:
                            min_layer_number = layer_number
                        if layer_number < 0:
                            layer_number += abs(min_layer_number)
                            negative_layers += 1
                        else:
                            layer_number += negative_layers

                        # In case there is a gap in the layer count, empty layers are created
                        for empty_layer in range(previous_layer + 1,
                                                 layer_number):
                            self._createEmptyLayer(empty_layer)

                        self._layer_number = layer_number
                        previous_layer = layer_number
                    except:
                        pass

                # This line is a comment. Ignore it (except for the layer_keyword)
                if line.startswith(";"):
                    continue

                G = self._getInt(line, "G")
                if G is not None:
                    # When find a movement, the new posistion is calculated and added to the current_path, but
                    # don't need to create a polygon until the end of the layer
                    current_position = self.processGCode(
                        G, line, current_position, current_path)
                    continue

                # When changing the extruder, the polygon with the stored paths is computed
                if line.startswith("T"):
                    T = self._getInt(line, "T")
                    if T is not None:
                        self._createPolygon(
                            self._current_layer_thickness, current_path,
                            self._extruder_offsets.get(self._extruder_number,
                                                       [0, 0]))
                        current_path.clear()

                        current_position = self.processTCode(
                            T, line, current_position, current_path)

                if line.startswith("M"):
                    M = self._getInt(line, "M")
                    self.processMCode(M, line, current_position, current_path)

            # "Flush" leftovers. Last layer paths are still stored
            if len(current_path) > 1:
                if self._createPolygon(
                        self._current_layer_thickness, current_path,
                        self._extruder_offsets.get(self._extruder_number,
                                                   [0, 0])):
                    self._layer_number += 1
                    current_path.clear()

        material_color_map = numpy.zeros((8, 4), dtype=numpy.float32)
        material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
        material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
        material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
        material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
        material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
        material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
        material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
        material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        scene_node.addDecorator(decorator)

        gcode_list_decorator = GCodeListDecorator()
        gcode_list_decorator.setGCodeList(gcode_list)
        scene_node.addDecorator(gcode_list_decorator)

        # gcode_dict stores gcode_lists for a number of build plates.
        active_build_plate_id = Application.getInstance().getBuildPlateModel(
        ).activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        Application.getInstance().getController().getScene(
        ).gcode_dict = gcode_dict

        Logger.log("d", "Finished parsing %s" % file_name)
        self._message.hide()

        if self._layer_number == 0:
            Logger.log("w",
                       "File %s doesn't contain any valid layers" % file_name)

        settings = Application.getInstance().getGlobalContainerStack()
        machine_width = settings.getProperty("machine_width", "value")
        machine_depth = settings.getProperty("machine_depth", "value")
        print_speed = settings.getProperty("speed_wall_0", "value")
        travel_speed = settings.getProperty("speed_travel", "value")
        time = (
            self._total_move_length - self._extrusion_retraction_length
        ) / travel_speed + self._extrusion_retraction_length / print_speed
        radius = Application.getInstance().getGlobalContainerStack(
        ).getProperty("material_diameter", "value") / 2

        for i in range(len(self._extrusion_max_amounts)):
            self._extrusion_max_amounts[i] *= (math.pi * (radius**2))
        total_extrusion = self._extrusion_max_amounts

        print_statistics_decorator = PrintStatisticsDecorator()
        print_statistics_decorator.setPrintTime(time)
        print_statistics_decorator.setMaterialAmounts(total_extrusion)
        scene_node.addDecorator(print_statistics_decorator)

        if not self._center_is_zero:
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

        Logger.log("d", "Loaded %s" % file_name)

        if Preferences.getInstance().getValue("gcodereader/show_caution"):
            caution_message = Message(catalog.i18nc(
                "@info:generic",
                "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."
            ),
                                      lifetime=0,
                                      title=catalog.i18nc(
                                          "@info:title", "G-code Details"))
            caution_message.show()

        # The "save/print" button's state is bound to the backend state.
        backend = Application.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
コード例 #53
0
ファイル: RotateTool.py プロジェクト: fujitsai/Uranium
class RotateTool(Tool):
    def __init__(self):
        super().__init__()
        self._handle = RotateToolHandle.RotateToolHandle()

        self._snap_rotation = True
        self._snap_angle = math.radians(15)

        self._angle = None
        self._angle_update_time = None

        self._progress_message = None
        self._iterations = 0
        self._total_iterations = 0

        self.setExposedProperties("ToolHint", "RotationSnap", "RotationSnapAngle")

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

        if event.type == Event.KeyPressEvent and event.key == KeyEvent.ShiftKey:
            # Snap is toggled when pressing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent and event.key == KeyEvent.ShiftKey:
            # Snap is "toggled back" when releasing the shift button
            self._snap_rotation = (not self._snap_rotation)
            self.propertyChanged.emit()

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

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

            if ToolHandle.isAxis(id):
                self.setLockedAxis(id)
                handle_position = self._handle.getWorldPosition()

                # Save the current positions of the node, as we want to rotate around their current centres
                self._saved_node_positions = []
                for node in Selection.getAllSelectedObjects():
                    self._saved_node_positions.append((node, node.getWorldPosition()))

                if id == ToolHandle.XAxis:
                    self.setDragPlane(Plane(Vector(1, 0, 0), handle_position.x))
                elif id == ToolHandle.YAxis:
                    self.setDragPlane(Plane(Vector(0, 1, 0), handle_position.y))
                elif self._locked_axis == ToolHandle.ZAxis:
                    self.setDragPlane(Plane(Vector(0, 0, 1), handle_position.z))

                self.setDragStart(event.x, event.y)
                self._angle = 0
                self.operationStarted.emit(self)

        if event.type == Event.MouseMoveEvent:
            # Perform a rotate operation
            if not self.getDragPlane():
                return False

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

            handle_position = self._handle.getWorldPosition()

            drag_start = (self.getDragStart() - handle_position).normalized()
            drag_position = self.getDragPosition(event.x, event.y)
            if not drag_position:
                return
            drag_end = (drag_position - handle_position).normalized()

            try:
                angle = math.acos(drag_start.dot(drag_end))
            except ValueError:
                angle = 0

            if self._snap_rotation:
                angle = int(angle / self._snap_angle) * self._snap_angle
                if angle == 0:
                    return

            rotation = None
            if self.getLockedAxis() == ToolHandle.XAxis:
                direction = 1 if Vector.Unit_X.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_X)
            elif self.getLockedAxis() == ToolHandle.YAxis:
                direction = 1 if Vector.Unit_Y.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Y)
            elif self.getLockedAxis() == ToolHandle.ZAxis:
                direction = 1 if Vector.Unit_Z.dot(drag_start.cross(drag_end)) > 0 else -1
                rotation = Quaternion.fromAngleAxis(direction * angle, Vector.Unit_Z)

            # 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._angle_update_time or new_time - self._angle_update_time > 0.1:
                self._angle_update_time = new_time
                self._angle += direction * angle
                self.propertyChanged.emit()

                # Rotate around the saved centeres of all selected nodes
                op = GroupedOperation()
                for node, position in self._saved_node_positions:
                    op.addOperation(RotateOperation(node, rotation, rotate_around_point = position))
                op.push()

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

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

    ##  Return a formatted angle of the current rotate operation
    #
    #   \return type(String) fully formatted string showing the angle by which the mesh(es) are rotated
    def getToolHint(self):
        return "%d°" % round(math.degrees(self._angle)) if self._angle else None

    ##  Get the state of the "snap rotation to N-degree increments" option
    #
    #   \return type(Boolean)
    def getRotationSnap(self):
        return self._snap_rotation

    ##  Set the state of the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Boolean)
    def setRotationSnap(self, snap):
        if snap != self._snap_rotation:
            self._snap_rotation = snap
            self.propertyChanged.emit()

    ##  Get the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \return type(Number)
    def getRotationSnapAngle(self):
        return self._snap_angle

    ##  Set the number of degrees used in the "snap rotation to N-degree increments" option
    #
    #   \param snap type(Number)
    def setRotationSnapAngle(self, angle):
        if angle != self._snap_angle:
            self._snap_angle = angle
            self.propertyChanged.emit()

    ##  Reset the orientation of the mesh(es) to their original orientation(s)
    def resetRotation(self):
        Selection.applyOperation(SetTransformOperation, None, Quaternion(), None)

    ##  Initialise and start a LayFlatOperation
    #
    #   Note: The LayFlat functionality is mostly used for 3d printing and should probably be moved into the Cura project
    def layFlat(self):
        self.operationStarted.emit(self)
        self._progress_message = Message("Laying object flat on buildplate...", lifetime = 0, dismissable = False)
        self._progress_message.setProgress(0)

        self._iterations = 0
        self._total_iterations = 0
        for selected_object in Selection.getAllSelectedObjects():
            if not selected_object.callDecoration("isGroup"):
                self._total_iterations += len(selected_object.getMeshDataTransformed().getVertices()) * 2
            else:
                for child in selected_object.getChildren():
                    self._total_iterations += len(child.getMeshDataTransformed().getVertices()) * 2

        self._progress_message.show()

        operations = Selection.applyOperation(LayFlatOperation)
        for op in operations:
            op.progress.connect(self._layFlatProgress)

        job = LayFlatJob(operations)
        job.finished.connect(self._layFlatFinished)
        job.start()

    ##  Called while performing the LayFlatOperation so progress can be shown
    #
    #   Note that the LayFlatOperation rate-limits these callbacks to prevent the UI from being flooded with property change notifications,
    #   \param iterations type(int) number of iterations performed since the last callback
    def _layFlatProgress(self, iterations):
        self._iterations += iterations
        self._progress_message.setProgress(100 * self._iterations / self._total_iterations)

    ##  Called when the LayFlatJob is done running all of its LayFlatOperations
    #
    #   \param job type(LayFlatJob)
    def _layFlatFinished(self, job):
        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self.operationStopped.emit(self)
コード例 #54
0
class ProcessSlicedLayersJob(Job):
    def __init__(self, layers):
        super().__init__()
        self._layers = layers
        self._scene = Application.getInstance().getController().getScene()
        self._progress = None
        self._abort_requested = False

    ##  Aborts the processing of layers.
    #
    #   This abort is made on a best-effort basis, meaning that the actual
    #   job thread will check once in a while to see whether an abort is
    #   requested and then stop processing by itself. There is no guarantee
    #   that the abort will stop the job any time soon or even at all.
    def abort(self):
        self._abort_requested = True

    def run(self):
        start_time = time()
        if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
            self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
            self._progress.show()
            Job.yieldThread()
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)

        new_node = SceneNode()

        ## Remove old layer data (if any)
        for node in DepthFirstIterator(self._scene.getRoot()):
            if node.callDecoration("getLayerData"):
                node.getParent().removeChild(node)
                break
            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return

        # Force garbage collection.
        # For some reason, Python has a tendency to keep the layer data
        # in memory longer than needed. Forcing the GC to run here makes
        # sure any old layer data is really cleaned up before adding new.
        gc.collect()

        mesh = MeshData()
        layer_data = LayerDataBuilder.LayerDataBuilder()
        layer_count = len(self._layers)

        # Find the minimum layer number
        # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
        # instead simply offset all other layers so the lowest layer is always 0.
        min_layer_number = 0
        for layer in self._layers:
            if layer.id < min_layer_number:
                min_layer_number = layer.id

        current_layer = 0

        for layer in self._layers:
            abs_layer_number = layer.id + abs(min_layer_number)

            layer_data.addLayer(abs_layer_number)
            this_layer = layer_data.getLayer(abs_layer_number)
            layer_data.setLayerHeight(abs_layer_number, layer.height)

            for p in range(layer.repeatedMessageCount("path_segment")):
                polygon = layer.getRepeatedMessage("path_segment", p)

                extruder = polygon.extruder

                line_types = numpy.fromstring(polygon.line_type, dtype="u1")  # Convert bytearray to numpy array
                line_types = line_types.reshape((-1,1))

                points = numpy.fromstring(polygon.points, dtype="f4")  # Convert bytearray to numpy array
                if polygon.point_type == 0: # Point2D
                    points = points.reshape((-1,2))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
                else:  # Point3D
                    points = points.reshape((-1,3))

                line_widths = numpy.fromstring(polygon.line_width, dtype="f4")  # Convert bytearray to numpy array
                line_widths = line_widths.reshape((-1,1))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                # In the future, line_thicknesses should be given by CuraEngine as well.
                # Currently the infill layer thickness also translates to line width
                line_thicknesses = numpy.zeros(line_widths.shape, dtype="f4")
                line_thicknesses[:] = layer.thickness / 1000  # from micrometer to millimeter

                # Create a new 3D-array, copy the 2D points over and insert the right height.
                # This uses manual array creation + copy rather than numpy.insert since this is
                # faster.
                new_points = numpy.empty((len(points), 3), numpy.float32)
                if polygon.point_type == 0:  # Point2D
                    new_points[:, 0] = points[:, 0]
                    new_points[:, 1] = layer.height / 1000  # layer height value is in backend representation
                    new_points[:, 2] = -points[:, 1]
                else: # Point3D
                    new_points[:, 0] = points[:, 0]
                    new_points[:, 1] = points[:, 2]
                    new_points[:, 2] = -points[:, 1]

                this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses)
                this_poly.buildCache()

                this_layer.polygons.append(this_poly)

                Job.yieldThread()
            Job.yieldThread()
            current_layer += 1
            progress = (current_layer / layer_count) * 99
            # TODO: Rebuild the layer data mesh once the layer has been processed.
            # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.

            if self._abort_requested:
                if self._progress:
                    self._progress.hide()
                return
            if self._progress:
                self._progress.setProgress(progress)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data

        # Find out colors per extruder
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        manager = ExtruderManager.getInstance()
        extruders = list(manager.getMachineExtruders(global_container_stack.getId()))
        if extruders:
            material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
            for extruder in extruders:
                material = extruder.findContainer({"type": "material"})
                position = int(extruder.getMetaDataEntry("position", default="0"))  # Get the position
                color_code = material.getMetaDataEntry("color_code")
                color = colorCodeToRGBA(color_code)
                material_color_map[position, :] = color
        else:
            # Single extruder via global stack.
            material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
            material = global_container_stack.findContainer({"type": "material"})
            color_code = material.getMetaDataEntry("color_code")
            if color_code is None:  # not all stacks have a material color
                color_code = "#e0e000"
            color = colorCodeToRGBA(color_code)
            material_color_map[0, :] = color

        # We have to scale the colors for compatibility mode
        if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")):
            line_type_brightness = 0.5  # for compatibility mode
        else:
            line_type_brightness = 1.0
        layer_mesh = layer_data.build(material_color_map, line_type_brightness)

        if self._abort_requested:
            if self._progress:
                self._progress.hide()
            return

        # Add LayerDataDecorator to scene node to indicate that the node has layer data
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        new_node.addDecorator(decorator)

        new_node.setMeshData(mesh)
        # Set build volume as parent, the build volume can move as a result of raft settings.
        # It makes sense to set the build volume as parent: the print is actually printed on it.
        new_node_parent = Application.getInstance().getBuildVolume()
        new_node.setParent(new_node_parent)  # Note: After this we can no longer abort!

        settings = Application.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2))

        if self._progress:
            self._progress.setProgress(100)

        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "LayerView":
            view.resetLayerData()

        if self._progress:
            self._progress.hide()

        # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
        self._layers = None

        Logger.log("d", "Processing layers took %s seconds", time() - start_time)

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
                if not self._progress:
                    self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
                if self._progress.getProgress() != 100:
                    self._progress.show()
            else:
                if self._progress:
                    self._progress.hide()
コード例 #55
0
ファイル: SliceInfo.py プロジェクト: Cycloneee/Cura
class SliceInfo(Extension):
    info_url = "https://stats.youmagine.com/curastats/slice"

    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info", False)

        if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False)
            self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
            self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
            self.send_slice_info_message.show()

    def messageActionTriggered(self, message_id, action_id):
        self.send_slice_info_message.hide()
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)

    def _onWriteStarted(self, output_device):
        try:
            if not Preferences.getInstance().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return # Do nothing, user does not want to send data

            global_container_stack = Application.getInstance().getGlobalContainerStack()

            # Get total material used (in mm^3)
            print_information = Application.getInstance().getPrintInformation()
            material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")

            # TODO: Send material per extruder instead of mashing it on a pile
            material_used = math.pi * material_radius * material_radius * sum(print_information.materialLengths) #Volume of all materials used

            # Get model information (bounding boxes, hashes and transformation matrix)
            models_info = []
            for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
                if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
                    if not getattr(node, "_outside_buildarea", False):
                        model_info = {}
                        model_info["hash"] = node.getMeshData().getHash()
                        model_info["bounding_box"] = {}
                        model_info["bounding_box"]["minimum"] = {}
                        model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
                        model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
                        model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z

                        model_info["bounding_box"]["maximum"] = {}
                        model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
                        model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
                        model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
                        model_info["transformation"] = str(node.getWorldTransformation().getData())

                        models_info.append(model_info)

            # Bundle the collected data
            submitted_data = {
                "processor": platform.processor(),
                "machine": platform.machine(),
                "platform": platform.platform(),
                "settings": global_container_stack.serialize(), # global_container with references on used containers
                "version": Application.getInstance().getVersion(),
                "modelhash": "None",
                "printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
                "filament": material_used,
                "language": Preferences.getInstance().getValue("general/language"),
            }
            for container in global_container_stack.getContainers():
                container_id = container.getId()
                try:
                    container_serialized = container.serialize()
                except NotImplementedError:
                    Logger.log("w", "Container %s could not be serialized!", container_id)
                    continue

                if container_serialized:
                    submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
                else:
                    Logger.log("i", "No data found in %s to be serialized!", container_id)

            # Convert data to bytes
            submitted_data = urllib.parse.urlencode(submitted_data)
            binary_data = submitted_data.encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception as e:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.
コード例 #56
0
class OctoPrintOutputDevice(PrinterOutputDevice):
    def __init__(self, key, address, port, properties):
        super().__init__(key)

        self._address = address
        self._port = port
        self._path = properties.get(b"path", b"/").decode("utf-8")
        if self._path[-1:] != "/":
            self._path += "/"
        self._key = key
        self._properties = properties  # Properties dict as provided by zero conf

        self._gcode = None
        self._auto_print = True
        self._forced_queue = False

        # We start with a single extruder, but update this when we get data from octoprint
        self._num_extruders_set = False
        self._num_extruders = 1

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            plugin_version = "Unknown"
            Logger.logException("w", "Could not get version information for the plugin")

        self._user_agent_header = "User-Agent".encode()
        self._user_agent = ("%s/%s %s/%s" % (
            Application.getInstance().getApplicationName(),
            Application.getInstance().getVersion(),
            "OctoPrintPlugin",
            Application.getInstance().getVersion()
        )).encode()

        self._api_prefix = "api/"
        self._api_header = "X-Api-Key".encode()
        self._api_key = None

        self._protocol = "https" if properties.get(b'useHttps') == b"true" else "http"
        self._base_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, self._path)
        self._api_url = self._base_url + self._api_prefix

        self._basic_auth_header = "Authorization".encode()
        self._basic_auth_data = None
        basic_auth_username = properties.get(b"userName", b"").decode("utf-8")
        basic_auth_password = properties.get(b"password", b"").decode("utf-8")
        if basic_auth_username and basic_auth_password:
            data = base64.b64encode(("%s:%s" % (basic_auth_username, basic_auth_password)).encode()).decode("utf-8")
            self._basic_auth_data = ("basic %s" % data).encode()

        self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")

        self.setPriority(2) # Make sure the output device gets selected above local file output
        self.setName(key)
        self.setShortDescription(i18n_catalog.i18nc("@action:button", "Print with OctoPrint"))
        self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print with OctoPrint"))
        self.setIconName("print")
        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        ##  Ensure that the qt networking stuff isn't garbage collected (unless we want it to)
        self._settings_reply = None
        self._printer_reply = None
        self._job_reply = None
        self._command_reply = None

        self._image_reply = None
        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._post_reply = None
        self._post_multi_part = None
        self._post_part = None

        self._progress_message = None
        self._error_message = None
        self._connection_message = None

        self._update_timer = QTimer()
        self._update_timer.setInterval(2000)  # TODO; Add preference for update interval
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)

        self._camera_image_id = 0
        self._camera_image = QImage()
        self._camera_mirror = ""
        self._camera_rotation = 0
        self._camera_url = ""
        self._camera_shares_proxy = False

        self._sd_supported = False

        self._connection_state_before_timeout = None

        self._last_response_time = None
        self._last_request_time = None
        self._response_timeout_time = 5
        self._recreate_network_manager_time = 30 # If we have no connection, re-create network manager every 30 sec.
        self._recreate_network_manager_count = 1

        self._preheat_timer = QTimer()
        self._preheat_timer.setSingleShot(True)
        self._preheat_timer.timeout.connect(self.cancelPreheatBed)

    def getProperties(self):
        return self._properties

    @pyqtSlot(str, result = str)
    def getProperty(self, key):
        key = key.encode("utf-8")
        if key in self._properties:
            return self._properties.get(key, b"").decode("utf-8")
        else:
            return ""

    ##  Get the unique key of this machine
    #   \return key String containing the key of the machine.
    @pyqtSlot(result = str)
    def getKey(self):
        return self._key

    ##  Set the API key of this OctoPrint instance
    def setApiKey(self, api_key):
        self._api_key = api_key.encode()

    ##  Name of the instance (as returned from the zeroConf properties)
    @pyqtProperty(str, constant = True)
    def name(self):
        return self._key

    ##  Version (as returned from the zeroConf properties)
    @pyqtProperty(str, constant=True)
    def octoprintVersion(self):
        return self._properties.get(b"version", b"").decode("utf-8")

    ## IPadress of this instance
    @pyqtProperty(str, constant=True)
    def ipAddress(self):
        return self._address

    ## port of this instance
    @pyqtProperty(int, constant=True)
    def port(self):
        return self._port

    ## path of this instance
    @pyqtProperty(str, constant=True)
    def path(self):
        return self._path

    ## absolute url of this instance
    @pyqtProperty(str, constant=True)
    def baseURL(self):
        return self._base_url

    cameraOrientationChanged = pyqtSignal()

    @pyqtProperty("QVariantMap", notify = cameraOrientationChanged)
    def cameraOrientation(self):
        return {
            "mirror": self._camera_mirror,
            "rotation": self._camera_rotation,
        }

    def _startCamera(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if not global_container_stack or not parseBool(global_container_stack.getMetaDataEntry("octoprint_show_camera", False)) or self._camera_url == "":
            return

        # Start streaming mjpg stream
        url = QUrl(self._camera_url)
        image_request = QNetworkRequest(url)
        image_request.setRawHeader(self._user_agent_header, self._user_agent)
        if self._camera_shares_proxy and self._basic_auth_data:
            image_request.setRawHeader(self._basic_auth_header, self._basic_auth_data)
        self._image_reply = self._manager.get(image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)

    def _stopCamera(self):
        if self._image_reply:
            self._image_reply.abort()
            self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
            self._image_reply = None
        image_request = None

        self._stream_buffer = b""
        self._stream_buffer_start_index = -1

        self._camera_image = QImage()
        self.newImage.emit()

    def _update(self):
        if self._last_response_time:
            time_since_last_response = time() - self._last_response_time
        else:
            time_since_last_response = 0
        if self._last_request_time:
            time_since_last_request = time() - self._last_request_time
        else:
            time_since_last_request = float("inf") # An irrelevantly large number of seconds

        # Connection is in timeout, check if we need to re-start the connection.
        # Sometimes the qNetwork manager incorrectly reports the network status on Mac & Windows.
        # Re-creating the QNetworkManager seems to fix this issue.
        if self._last_response_time and self._connection_state_before_timeout:
            if time_since_last_response > self._recreate_network_manager_time * self._recreate_network_manager_count:
                self._recreate_network_manager_count += 1
                # It can happen that we had a very long timeout (multiple times the recreate time).
                # In that case we should jump through the point that the next update won't be right away.
                while time_since_last_response - self._recreate_network_manager_time * self._recreate_network_manager_count > self._recreate_network_manager_time:
                    self._recreate_network_manager_count += 1
                Logger.log("d", "Timeout lasted over 30 seconds (%.1fs), re-checking connection.", time_since_last_response)
                self._createNetworkManager()
                return

        # Check if we have an connection in the first place.
        if not self._manager.networkAccessible():
            if not self._connection_state_before_timeout:
                Logger.log("d", "The network connection seems to be disabled. Going into timeout mode")
                self._connection_state_before_timeout = self._connection_state
                self.setConnectionState(ConnectionState.error)
                self._connection_message = Message(i18n_catalog.i18nc("@info:status",
                                                                      "The connection with the network was lost."))
                self._connection_message.show()
                # Check if we were uploading something. Abort if this is the case.
                # Some operating systems handle this themselves, others give weird issues.
                try:
                    if self._post_reply:
                        Logger.log("d", "Stopping post upload because the connection was lost.")
                        try:
                            self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
                        except TypeError:
                            pass  # The disconnection can fail on mac in some cases. Ignore that.

                        self._post_reply.abort()
                        self._progress_message.hide()
                except RuntimeError:
                    self._post_reply = None  # It can happen that the wrapped c++ object is already deleted.
            return
        else:
            if not self._connection_state_before_timeout:
                self._recreate_network_manager_count = 1

        # Check that we aren't in a timeout state
        if self._last_response_time and self._last_request_time and not self._connection_state_before_timeout:
            if time_since_last_response > self._response_timeout_time and time_since_last_request <= self._response_timeout_time:
                # Go into timeout state.
                Logger.log("d", "We did not receive a response for %s seconds, so it seems OctoPrint is no longer accesible.", time() - self._last_response_time)
                self._connection_state_before_timeout = self._connection_state
                self._connection_message = Message(i18n_catalog.i18nc("@info:status", "The connection with OctoPrint was lost. Check your network-connections."))
                self._connection_message.show()
                self.setConnectionState(ConnectionState.error)

        ## Request 'general' printer data
        self._printer_reply = self._manager.get(self._createApiRequest("printer"))

        ## Request print_job data
        self._job_reply = self._manager.get(self._createApiRequest("job"))

    def _createNetworkManager(self):
        if self._manager:
            self._manager.finished.disconnect(self._onRequestFinished)

        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

    def _createApiRequest(self, end_point):
        request = QNetworkRequest(QUrl(self._api_url + end_point))
        request.setRawHeader(self._user_agent_header, self._user_agent)
        request.setRawHeader(self._api_header, self._api_key)
        if self._basic_auth_data:
            request.setRawHeader(self._basic_auth_header, self._basic_auth_data)
        return request

    def close(self):
        self._updateJobState("")
        self.setConnectionState(ConnectionState.closed)
        if self._progress_message:
            self._progress_message.hide()
        if self._error_message:
            self._error_message.hide()
        self._update_timer.stop()

        self._stopCamera()

    def requestWrite(self, node, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
        self.writeStarted.emit(self)

        active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
        scene = Application.getInstance().getController().getScene()
        gcode_dict = getattr(scene, "gcode_dict", None)
        if not gcode_dict:
            return
        self._gcode = gcode_dict.get(active_build_plate, None)

        self.startPrint()

    def isConnected(self):
        return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error

    ##  Start requesting data from the instance
    def connect(self):
        self._createNetworkManager()

        self.setConnectionState(ConnectionState.connecting)
        self._update()  # Manually trigger the first update, as we don't want to wait a few secs before it starts.
        Logger.log("d", "Connection with instance %s with url %s started", self._key, self._base_url)
        self._update_timer.start()

        self._last_response_time = None
        self.setAcceptsCommands(False)
        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connecting to OctoPrint on {0}").format(self._key))

        ## Request 'settings' dump
        self._settings_reply = self._manager.get(self._createApiRequest("settings"))

    ##  Stop requesting data from the instance
    def disconnect(self):
        Logger.log("d", "Connection with instance %s with url %s stopped", self._key, self._base_url)
        self.close()

    newImage = pyqtSignal()

    @pyqtProperty(QUrl, notify = newImage)
    def cameraImage(self):
        self._camera_image_id += 1
        # There is an image provider that is called "camera". In order to ensure that the image qml object, that
        # requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
        # as new (instead of relying on cached version and thus forces an update.
        temp = "image://camera/" + str(self._camera_image_id)
        return QUrl(temp, QUrl.TolerantMode)

    def getCameraImage(self):
        return self._camera_image

    def _setJobState(self, job_state):
        if job_state == "abort":
            command = "cancel"
        elif job_state == "print":
            if self.jobState == "paused":
                command = "pause"
            else:
                command = "start"
        elif job_state == "pause":
            command = "pause"

        if command:
            self._sendJobCommand(command)

    def startPrint(self):
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if not global_container_stack:
            return

        if self._error_message:
            self._error_message.hide()
            self._error_message = None

        if self._progress_message:
            self._progress_message.hide()
            self._progress_message = None

        self._auto_print = parseBool(global_container_stack.getMetaDataEntry("octoprint_auto_print", True))
        self._forced_queue = False

        if self.jobState not in ["ready", ""]:
            if self.jobState == "offline":
                self._error_message = Message(i18n_catalog.i18nc("@info:status", "The printer is offline. Unable to start a new job."))
            elif self._auto_print:
                self._error_message = Message(i18n_catalog.i18nc("@info:status", "OctoPrint is busy. Unable to start a new job."))
            else:
                # allow queueing the job even if OctoPrint is currently busy if autoprinting is disabled
                self._error_message = None

            if self._error_message:
                self._error_message.addAction("Queue", i18n_catalog.i18nc("@action:button", "Queue job"), None, i18n_catalog.i18nc("@action:tooltip", "Queue this print job so it can be printed later"))
                self._error_message.actionTriggered.connect(self._queuePrint)
                self._error_message.show()
                return

        self._startPrint()

    def _queuePrint(self, message_id, action_id):
        if self._error_message:
            self._error_message.hide()
        self._forced_queue = True
        self._startPrint()

    def _startPrint(self):
        self._preheat_timer.stop()

        if self._auto_print and not self._forced_queue:
            Application.getInstance().showPrintMonitor.emit(True)

        try:
            self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to OctoPrint"), 0, False, -1)
            self._progress_message.addAction("Cancel", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
            self._progress_message.actionTriggered.connect(self._cancelSendGcode)
            self._progress_message.show()

            ## Mash the data into single string
            single_string_file_data = ""
            last_process_events = time()
            for line in self._gcode:
                single_string_file_data += line
                if time() > last_process_events + 0.05:
                    # Ensure that the GUI keeps updated at least 20 times per second.
                    QCoreApplication.processEvents()
                    last_process_events = time()

            job_name = Application.getInstance().getPrintInformation().jobName.strip()
            if job_name is "":
                job_name = "untitled_print"
            file_name = "%s.gcode" % job_name

            ##  Create multi_part request
            self._post_multi_part = QHttpMultiPart(QHttpMultiPart.FormDataType)

            ##  Create parts (to be placed inside multipart)
            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"select\"")
            self._post_part.setBody(b"true")
            self._post_multi_part.append(self._post_part)

            if self._auto_print and not self._forced_queue:
                self._post_part = QHttpPart()
                self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"print\"")
                self._post_part.setBody(b"true")
                self._post_multi_part.append(self._post_part)

            self._post_part = QHttpPart()
            self._post_part.setHeader(QNetworkRequest.ContentDispositionHeader, "form-data; name=\"file\"; filename=\"%s\"" % file_name)
            self._post_part.setBody(single_string_file_data.encode())
            self._post_multi_part.append(self._post_part)

            destination = "local"
            if self._sd_supported and parseBool(Application.getInstance().getGlobalContainerStack().getMetaDataEntry("octoprint_store_sd", False)):
                destination = "sdcard"

            ##  Post request + data
            post_request = self._createApiRequest("files/" + destination)
            self._post_reply = self._manager.post(post_request, self._post_multi_part)
            self._post_reply.uploadProgress.connect(self._onUploadProgress)

            self._gcode = None

        except IOError:
            self._progress_message.hide()
            self._error_message = Message(i18n_catalog.i18nc("@info:status", "Unable to send data to OctoPrint."))
            self._error_message.show()
        except Exception as e:
            self._progress_message.hide()
            Logger.log("e", "An exception occurred in network connection: %s" % str(e))

    def _cancelSendGcode(self, message_id, action_id):
        if self._post_reply:
            Logger.log("d", "Stopping upload because the user pressed cancel.")
            try:
                self._post_reply.uploadProgress.disconnect(self._onUploadProgress)
            except TypeError:
                pass  # The disconnection can fail on mac in some cases. Ignore that.

            self._post_reply.abort()
            self._post_reply = None
        if self._progress_message:
            self._progress_message.hide()

    def _sendCommand(self, command):
        self._sendCommandToApi("printer/command", command)
        Logger.log("d", "Sent gcode command to OctoPrint instance: %s", command)

    def _sendJobCommand(self, command):
        self._sendCommandToApi("job", command)
        Logger.log("d", "Sent job command to OctoPrint instance: %s", command)

    def _sendCommandToApi(self, end_point, commands):
        command_request = self._createApiRequest(end_point)
        command_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")

        if isinstance(commands, list):
            data = json.dumps({"commands": commands})
        else:
            data = json.dumps({"command": commands})
        self._command_reply = self._manager.post(command_request, data.encode())

    ##  Pre-heats the heated bed of the printer.
    #
    #   \param temperature The temperature to heat the bed to, in degrees
    #   Celsius.
    #   \param duration How long the bed should stay warm, in seconds.
    @pyqtSlot(float, float)
    def preheatBed(self, temperature, duration):
        self._setTargetBedTemperature(temperature)
        if duration > 0:
            self._preheat_timer.setInterval(duration * 1000)
            self._preheat_timer.start()
        else:
            self._preheat_timer.stop()

    ##  Cancels pre-heating the heated bed of the printer.
    #
    #   If the bed is not pre-heated, nothing happens.
    @pyqtSlot()
    def cancelPreheatBed(self):
        self._setTargetBedTemperature(0)
        self._preheat_timer.stop()

    ##  Changes the target bed temperature on the OctoPrint instance.
    #
    #   /param temperature The new target temperature of the bed.
    def _setTargetBedTemperature(self, temperature):
        if not self._updateTargetBedTemperature(temperature):
            Logger.log("d", "Target bed temperature is already set to %s", temperature)
            return

        Logger.log("d", "Setting bed temperature to %s", temperature)
        self._sendCommand("M140 S%s" % temperature)

    ##  Updates the target bed temperature from the printer, and emit a signal if it was changed.
    #
    #   /param temperature The new target temperature of the bed.
    #   /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
    def _updateTargetBedTemperature(self, temperature):
        if self._target_bed_temperature == temperature:
            return False
        self._target_bed_temperature = temperature
        self.targetBedTemperatureChanged.emit()
        return True

    ##  Changes the target bed temperature on the OctoPrint instance.
    #
    #   /param index The index of the hotend.
    #   /param temperature The new target temperature of the bed.
    def _setTargetHotendTemperature(self, index, temperature):
        if not self._updateTargetHotendTemperature(index, temperature):
            Logger.log("d", "Target hotend %s temperature is already set to %s", index, temperature)
            return

        Logger.log("d", "Setting hotend %s temperature to %s", index, temperature)
        self._sendCommand("M104 T%s S%s" % (index, temperature))

    ##  Updates the target hotend temperature from the printer, and emit a signal if it was changed.
    #
    #   /param index The index of the hotend.
    #   /param temperature The new target temperature of the hotend.
    #   /return boolean, True if the temperature was changed, false if the new temperature has the same value as the already stored temperature
    def _updateTargetHotendTemperature(self, index, temperature):
        if self._target_hotend_temperatures[index] == temperature:
            return False
        self._target_hotend_temperatures[index] = temperature
        self.targetHotendTemperaturesChanged.emit()
        return True

    def _setHeadPosition(self, x, y , z, speed):
        self._sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))

    def _setHeadX(self, x, speed):
        self._sendCommand("G0 X%s F%s" % (x, speed))

    def _setHeadY(self, y, speed):
        self._sendCommand("G0 Y%s F%s" % (y, speed))

    def _setHeadZ(self, z, speed):
        self._sendCommand("G0 Z%s F%s" % (z, speed))

    def _homeHead(self):
        self._sendCommand("G28 X Y")

    def _homeBed(self):
        self._sendCommand("G28 Z")

    def _moveHead(self, x, y, z, speed):
        self._sendCommand(["G91", "G0 X%s Y%s Z%s F%s" % (x, y, z, speed), "G90"])

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Received a timeout on a request to the instance")
            self._connection_state_before_timeout = self._connection_state
            self.setConnectionState(ConnectionState.error)
            return

        if self._connection_state_before_timeout and reply.error() == QNetworkReply.NoError:  #  There was a timeout, but we got a correct answer again.
            if self._last_response_time:
                Logger.log("d", "We got a response from the instance after %s of silence", time() - self._last_response_time)
            self.setConnectionState(self._connection_state_before_timeout)
            self._connection_state_before_timeout = None

        if reply.error() == QNetworkReply.NoError:
            self._last_response_time = time()

        http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if self._api_prefix + "printer" in reply.url().toString():  # Status update from /printer.
                if http_status_code == 200:
                    if not self.acceptsCommands:
                        self.setAcceptsCommands(True)
                        self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected to OctoPrint on {0}").format(self._key))

                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)
                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "temperature" in json_data:
                        if not self._num_extruders_set:
                            self._num_extruders = 0
                            while "tool%d" % self._num_extruders in json_data["temperature"]:
                                self._num_extruders = self._num_extruders + 1

                            # Reinitialise from PrinterOutputDevice to match the new _num_extruders
                            self._hotend_temperatures = [0] * self._num_extruders
                            self._target_hotend_temperatures = [0] * self._num_extruders

                            self._num_extruders_set = True

                        # Check for hotend temperatures
                        for index in range(0, self._num_extruders):
                            if ("tool%d" % index) in json_data["temperature"]:
                                hotend_temperatures = json_data["temperature"]["tool%d" % index]
                                self._setHotendTemperature(index, hotend_temperatures["actual"])
                                self._updateTargetHotendTemperature(index, hotend_temperatures["target"])
                            else:
                                self._setHotendTemperature(index, 0)
                                self._updateTargetHotendTemperature(index, 0)

                        if "bed" in json_data["temperature"]:
                            bed_temperatures = json_data["temperature"]["bed"]
                            self._setBedTemperature(bed_temperatures["actual"])
                            self._updateTargetBedTemperature(bed_temperatures["target"])
                        else:
                            self._setBedTemperature(0)
                            self._updateTargetBedTemperature(0)

                    job_state = "offline"
                    if "state" in json_data:
                        if json_data["state"]["flags"]["error"]:
                            job_state = "error"
                        elif json_data["state"]["flags"]["paused"]:
                            job_state = "paused"
                        elif json_data["state"]["flags"]["printing"]:
                            job_state = "printing"
                        elif json_data["state"]["flags"]["ready"]:
                            job_state = "ready"
                    self._updateJobState(job_state)

                elif http_status_code == 401:
                    self._updateJobState("offline")
                    self.setConnectionText(i18n_catalog.i18nc("@info:status", "OctoPrint on {0} does not allow access to print").format(self._key))
                elif http_status_code == 409:
                    if self._connection_state == ConnectionState.connecting:
                        self.setConnectionState(ConnectionState.connected)

                    self._updateJobState("offline")
                    self.setConnectionText(i18n_catalog.i18nc("@info:status", "The printer connected to OctoPrint on {0} is not operational").format(self._key))
                else:
                    self._updateJobState("offline")
                    Logger.log("w", "Received an unexpected returncode: %d", http_status_code)

            elif self._api_prefix + "job" in reply.url().toString():  # Status update from /job:
                if http_status_code == 200:
                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    progress = json_data["progress"]["completion"]
                    if progress:
                        self.setProgress(progress)

                    if json_data["progress"]["printTime"]:
                        self.setTimeElapsed(json_data["progress"]["printTime"])
                        if json_data["progress"]["printTimeLeft"]:
                            self.setTimeTotal(json_data["progress"]["printTime"] + json_data["progress"]["printTimeLeft"])
                        elif json_data["job"]["estimatedPrintTime"]:
                            self.setTimeTotal(max(json_data["job"]["estimatedPrintTime"], json_data["progress"]["printTime"]))
                        elif progress > 0:
                            self.setTimeTotal(json_data["progress"]["printTime"] / (progress / 100))
                        else:
                            self.setTimeTotal(0)
                    else:
                        self.setTimeElapsed(0)
                        self.setTimeTotal(0)
                    self.setJobName(json_data["job"]["file"]["name"])
                else:
                    pass  # TODO: Handle errors

            elif self._api_prefix + "settings" in reply.url().toString():  # OctoPrint settings dump from /settings:
                if http_status_code == 200:
                    try:
                        json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log("w", "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data["feature"]:
                        self._sd_supported = json_data["feature"]["sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data["webcam"]:
                        self._camera_shares_proxy = False
                        stream_url = json_data["webcam"]["streamUrl"]
                        if not stream_url: #empty string or None
                            self._camera_url = ""
                        elif stream_url[:4].lower() == "http": # absolute uri
                            self._camera_url = stream_url
                        elif stream_url[:2] == "//": # protocol-relative
                            self._camera_url = "%s:%s" % (self._protocol, stream_url)
                        elif stream_url[:1] == ":": # domain-relative (on another port)
                            self._camera_url = "%s://%s%s" % (self._protocol, self._address, stream_url)
                        elif stream_url[:1] == "/": # domain-relative (on same port)
                            self._camera_url = "%s://%s:%d%s" % (self._protocol, self._address, self._port, stream_url)
                            self._camera_shares_proxy = True
                        else:
                            Logger.log("w", "Unusable stream url received: %s", stream_url)
                            self._camera_url = ""

                        Logger.log("d", "Set OctoPrint camera url to %s", self._camera_url)

                        if "rotate90" in json_data["webcam"]:
                            self._camera_rotation = -90 if json_data["webcam"]["rotate90"] else 0
                            if json_data["webcam"]["flipH"] and json_data["webcam"]["flipV"]:
                                self._camera_mirror = False
                                self._camera_rotation += 180
                            elif json_data["webcam"]["flipH"]:
                                self._camera_mirror = True
                            elif json_data["webcam"]["flipV"]:
                                self._camera_mirror = True
                                self._camera_rotation += 180
                            else:
                                self._camera_mirror = False
                            self.cameraOrientationChanged.emit()

        elif reply.operation() == QNetworkAccessManager.PostOperation:
            if self._api_prefix + "files" in reply.url().toString():  # Result from /files command:
                if http_status_code == 201:
                    Logger.log("d", "Resource created on OctoPrint instance: %s", reply.header(QNetworkRequest.LocationHeader).toString())
                else:
                    pass  # TODO: Handle errors

                reply.uploadProgress.disconnect(self._onUploadProgress)
                self._progress_message.hide()

                if self._forced_queue or not self._auto_print:
                    location = reply.header(QNetworkRequest.LocationHeader)
                    if location:
                        file_name = QUrl(reply.header(QNetworkRequest.LocationHeader).toString()).fileName()
                        message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint as {0}").format(file_name))
                    else:
                        message = Message(i18n_catalog.i18nc("@info:status", "Saved to OctoPrint"))
                    message.addAction("open_browser", i18n_catalog.i18nc("@action:button", "OctoPrint..."), "globe",
                                        i18n_catalog.i18nc("@info:tooltip", "Open the OctoPrint web interface"))
                    message.actionTriggered.connect(self._onMessageActionTriggered)
                    message.show()

            elif self._api_prefix + "job" in reply.url().toString():  # Result from /job command:
                if http_status_code == 204:
                    Logger.log("d", "Octoprint command accepted")
                else:
                    pass  # TODO: Handle errors

        else:
            Logger.log("d", "OctoPrintOutputDevice got an unhandled operation %s", reply.operation())

    def _onStreamDownloadProgress(self, bytes_received, bytes_total):
        self._stream_buffer += self._image_reply.readAll()

        if self._stream_buffer_start_index == -1:
            self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
        stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')

        if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
            jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
            self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
            self._stream_buffer_start_index = -1

            self._camera_image.loadFromData(jpg_data)
            self.newImage.emit()

    def _onUploadProgress(self, bytes_sent, bytes_total):
        if bytes_total > 0:
            # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
            # timeout responses if this happens.
            self._last_response_time = time()

            progress = bytes_sent / bytes_total * 100
            if progress < 100:
                if progress > self._progress_message.getProgress():
                    self._progress_message.setProgress(progress)
            else:
                self._progress_message.hide()
                self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Storing data on OctoPrint"), 0, False, -1)
                self._progress_message.show()
        else:
            self._progress_message.setProgress(0)

    def _onMessageActionTriggered(self, message, action):
        if action == "open_browser":
            QDesktopServices.openUrl(QUrl(self._base_url))
コード例 #57
0
class ProcessSlicedLayersJob(Job):
    def __init__(self, layers):
        super().__init__()
        self._layers = layers
        self._scene = Application.getInstance().getController().getScene()
        self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
        self._abort_requested = False
        self._build_plate_number = None

    ##  Aborts the processing of layers.
    #
    #   This abort is made on a best-effort basis, meaning that the actual
    #   job thread will check once in a while to see whether an abort is
    #   requested and then stop processing by itself. There is no guarantee
    #   that the abort will stop the job any time soon or even at all.
    def abort(self):
        self._abort_requested = True

    def setBuildPlate(self, new_value):
        self._build_plate_number = new_value

    def getBuildPlate(self):
        return self._build_plate_number

    def run(self):
        Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number)
        start_time = time()
        view = Application.getInstance().getController().getActiveView()
        if view.getPluginId() == "SimulationView":
            view.resetLayerData()
            self._progress_message.show()
            Job.yieldThread()
            if self._abort_requested:
                if self._progress_message:
                    self._progress_message.hide()
                return

        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)

        # The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice
        new_node = CuraSceneNode(no_setting_override = True)
        new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))

        # Force garbage collection.
        # For some reason, Python has a tendency to keep the layer data
        # in memory longer than needed. Forcing the GC to run here makes
        # sure any old layer data is really cleaned up before adding new.
        gc.collect()

        mesh = MeshData()
        layer_data = LayerDataBuilder.LayerDataBuilder()
        layer_count = len(self._layers)

        # Find the minimum layer number
        # When disabling the remove empty first layers setting, the minimum layer number will be a positive
        # value. In that case the first empty layers will be discarded and start processing layers from the
        # first layer with data.
        # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
        # simply offset all other layers so the lowest layer is always 0. It could happens that the first
        # raft layer has value -8 but there are just 4 raft (negative) layers.
        min_layer_number = sys.maxsize
        negative_layers = 0
        for layer in self._layers:
            if layer.repeatedMessageCount("path_segment") > 0:
                if layer.id < min_layer_number:
                    min_layer_number = layer.id
                if layer.id < 0:
                    negative_layers += 1

        current_layer = 0

        for layer in self._layers:
            # If the layer is below the minimum, it means that there is no data, so that we don't create a layer
            # data. However, if there are empty layers in between, we compute them.
            if layer.id < min_layer_number:
                continue

            # Layers are offset by the minimum layer number. In case the raft (negative layers) is being used,
            # then the absolute layer number is adjusted by removing the empty layers that can be in between raft
            # and the model
            abs_layer_number = layer.id - min_layer_number
            if layer.id >= 0 and negative_layers != 0:
                abs_layer_number += (min_layer_number + negative_layers)

            layer_data.addLayer(abs_layer_number)
            this_layer = layer_data.getLayer(abs_layer_number)
            layer_data.setLayerHeight(abs_layer_number, layer.height)
            layer_data.setLayerThickness(abs_layer_number, layer.thickness)

            for p in range(layer.repeatedMessageCount("path_segment")):
                polygon = layer.getRepeatedMessage("path_segment", p)

                extruder = polygon.extruder

                line_types = numpy.fromstring(polygon.line_type, dtype="u1")  # Convert bytearray to numpy array

                line_types = line_types.reshape((-1,1))

                points = numpy.fromstring(polygon.points, dtype="f4")  # Convert bytearray to numpy array
                if polygon.point_type == 0: # Point2D
                    points = points.reshape((-1,2))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
                else:  # Point3D
                    points = points.reshape((-1,3))

                line_widths = numpy.fromstring(polygon.line_width, dtype="f4")  # Convert bytearray to numpy array
                line_widths = line_widths.reshape((-1,1))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4")  # Convert bytearray to numpy array
                line_thicknesses = line_thicknesses.reshape((-1,1))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4")  # Convert bytearray to numpy array
                line_feedrates = line_feedrates.reshape((-1,1))  # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.

                # Create a new 3D-array, copy the 2D points over and insert the right height.
                # This uses manual array creation + copy rather than numpy.insert since this is
                # faster.
                new_points = numpy.empty((len(points), 3), numpy.float32)
                if polygon.point_type == 0:  # Point2D
                    new_points[:, 0] = points[:, 0]
                    new_points[:, 1] = layer.height / 1000  # layer height value is in backend representation
                    new_points[:, 2] = -points[:, 1]
                else: # Point3D
                    new_points[:, 0] = points[:, 0]
                    new_points[:, 1] = points[:, 2]
                    new_points[:, 2] = -points[:, 1]

                this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates)
                this_poly.buildCache()

                this_layer.polygons.append(this_poly)

                Job.yieldThread()
            Job.yieldThread()
            current_layer += 1
            progress = (current_layer / layer_count) * 99
            # TODO: Rebuild the layer data mesh once the layer has been processed.
            # This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.

            if self._abort_requested:
                if self._progress_message:
                    self._progress_message.hide()
                return
            if self._progress_message:
                self._progress_message.setProgress(progress)

        # We are done processing all the layers we got from the engine, now create a mesh out of the data

        # Find out colors per extruder
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        manager = ExtruderManager.getInstance()
        extruders = manager.getActiveExtruderStacks()
        if extruders:
            material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
            for extruder in extruders:
                position = int(extruder.getMetaDataEntry("position", default = "0"))
                try:
                    default_color = ExtrudersModel.defaultColors[position]
                except IndexError:
                    default_color = "#e0e000"
                color_code = extruder.material.getMetaDataEntry("color_code", default=default_color)
                color = colorCodeToRGBA(color_code)
                material_color_map[position, :] = color
        else:
            # Single extruder via global stack.
            material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
            color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000")
            color = colorCodeToRGBA(color_code)
            material_color_map[0, :] = color

        # We have to scale the colors for compatibility mode
        if OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")):
            line_type_brightness = 0.5  # for compatibility mode
        else:
            line_type_brightness = 1.0
        layer_mesh = layer_data.build(material_color_map, line_type_brightness)

        if self._abort_requested:
            if self._progress_message:
                self._progress_message.hide()
            return

        # Add LayerDataDecorator to scene node to indicate that the node has layer data
        decorator = LayerDataDecorator.LayerDataDecorator()
        decorator.setLayerData(layer_mesh)
        new_node.addDecorator(decorator)

        new_node.setMeshData(mesh)
        # Set build volume as parent, the build volume can move as a result of raft settings.
        # It makes sense to set the build volume as parent: the print is actually printed on it.
        new_node_parent = Application.getInstance().getBuildVolume()
        new_node.setParent(new_node_parent)  # Note: After this we can no longer abort!

        settings = Application.getInstance().getGlobalContainerStack()
        if not settings.getProperty("machine_center_is_zero", "value"):
            new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2))

        if self._progress_message:
            self._progress_message.setProgress(100)

        if self._progress_message:
            self._progress_message.hide()

        # Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
        self._layers = None

        Logger.log("d", "Processing layers took %s seconds", time() - start_time)

    def _onActiveViewChanged(self):
        if self.isRunning():
            if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
                if not self._progress_message:
                    self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information"))
                if self._progress_message.getProgress() != 100:
                    self._progress_message.show()
            else:
                if self._progress_message:
                    self._progress_message.hide()
コード例 #58
0
class SliceInfo(Extension):
    info_url = "https://stats.ultimaker.com/api/cura"

    def __init__(self):
        super().__init__()
        Application.getInstance().getOutputDeviceManager(
        ).writeStarted.connect(self._onWriteStarted)
        Preferences.getInstance().addPreference("info/send_slice_info", True)
        Preferences.getInstance().addPreference("info/asked_send_slice_info",
                                                False)

        if not Preferences.getInstance().getValue(
                "info/asked_send_slice_info"):
            self.send_slice_info_message = Message(catalog.i18nc(
                "@info",
                "Cura collects anonymised slicing statistics. You can disable this in preferences"
            ),
                                                   lifetime=0,
                                                   dismissable=False)
            self.send_slice_info_message.addAction(
                "Dismiss", catalog.i18nc("@action:button", "Dismiss"), None,
                "")
            self.send_slice_info_message.actionTriggered.connect(
                self.messageActionTriggered)
            self.send_slice_info_message.show()

    def messageActionTriggered(self, message_id, action_id):
        self.send_slice_info_message.hide()
        Preferences.getInstance().setValue("info/asked_send_slice_info", True)

    def _onWriteStarted(self, output_device):
        try:
            if not Preferences.getInstance().getValue("info/send_slice_info"):
                Logger.log("d", "'info/send_slice_info' is turned off.")
                return  # Do nothing, user does not want to send data

            global_container_stack = Application.getInstance(
            ).getGlobalContainerStack()
            print_information = Application.getInstance().getPrintInformation()

            data = dict()  # The data that we're going to submit.
            data["time_stamp"] = time.time()
            data["schema_version"] = 0
            data["cura_version"] = Application.getInstance().getVersion()

            active_mode = Preferences.getInstance().getValue(
                "cura/active_mode")
            if active_mode == 0:
                data["active_mode"] = "recommended"
            else:
                data["active_mode"] = "custom"

            data[
                "machine_settings_changed_by_user"] = global_container_stack.definitionChanges.getId(
                ) != "empty"
            data["language"] = Preferences.getInstance().getValue(
                "general/language")
            data["os"] = {
                "type": platform.system(),
                "version": platform.version()
            }

            data["active_machine"] = {
                "definition_id":
                global_container_stack.definition.getId(),
                "manufacturer":
                global_container_stack.definition.getMetaData().get(
                    "manufacturer", "")
            }

            data["extruders"] = []
            extruders = list(ExtruderManager.getInstance().getMachineExtruders(
                global_container_stack.getId()))
            extruders = sorted(
                extruders,
                key=lambda extruder: extruder.getMetaDataEntry("position"))

            if not extruders:
                extruders = [global_container_stack]

            for extruder in extruders:
                extruder_dict = dict()
                extruder_dict["active"] = ExtruderManager.getInstance(
                ).getActiveExtruderStack() == extruder
                extruder_dict["material"] = {
                    "GUID": extruder.material.getMetaData().get("GUID", ""),
                    "type":
                    extruder.material.getMetaData().get("material", ""),
                    "brand": extruder.material.getMetaData().get("brand", "")
                }
                extruder_dict[
                    "material_used"] = print_information.materialLengths[int(
                        extruder.getMetaDataEntry("position", "0"))]
                extruder_dict["variant"] = extruder.variant.getName()
                extruder_dict["nozzle_size"] = extruder.getProperty(
                    "machine_nozzle_size", "value")

                extruder_settings = dict()
                extruder_settings["wall_line_count"] = extruder.getProperty(
                    "wall_line_count", "value")
                extruder_settings["retraction_enable"] = extruder.getProperty(
                    "retraction_enable", "value")
                extruder_settings[
                    "infill_sparse_density"] = extruder.getProperty(
                        "infill_sparse_density", "value")
                extruder_settings["infill_pattern"] = extruder.getProperty(
                    "infill_pattern", "value")
                extruder_settings[
                    "gradual_infill_steps"] = extruder.getProperty(
                        "gradual_infill_steps", "value")
                extruder_settings[
                    "default_material_print_temperature"] = extruder.getProperty(
                        "default_material_print_temperature", "value")
                extruder_settings[
                    "material_print_temperature"] = extruder.getProperty(
                        "material_print_temperature", "value")
                extruder_dict["extruder_settings"] = extruder_settings
                data["extruders"].append(extruder_dict)

            data[
                "quality_profile"] = global_container_stack.quality.getMetaData(
                ).get("quality_type")

            data["models"] = []
            # Listing all files placed on the build plate
            for node in DepthFirstIterator(CuraApplication.getInstance(
            ).getController().getScene().getRoot()):
                if node.callDecoration("isSliceable"):
                    model = dict()
                    model["hash"] = node.getMeshData().getHash()
                    bounding_box = node.getBoundingBox()
                    model["bounding_box"] = {
                        "minimum": {
                            "x": bounding_box.minimum.x,
                            "y": bounding_box.minimum.y,
                            "z": bounding_box.minimum.z
                        },
                        "maximum": {
                            "x": bounding_box.maximum.x,
                            "y": bounding_box.maximum.y,
                            "z": bounding_box.maximum.z
                        }
                    }
                    model["transformation"] = {
                        "data":
                        str(node.getWorldTransformation().getData()).replace(
                            "\n", "")
                    }
                    extruder_position = node.callDecoration(
                        "getActiveExtruderPosition")
                    model[
                        "extruder"] = 0 if extruder_position is None else int(
                            extruder_position)

                    model_settings = dict()
                    model_stack = node.callDecoration("getStack")
                    if model_stack:
                        model_settings[
                            "support_enabled"] = model_stack.getProperty(
                                "support_enable", "value")
                        model_settings["support_extruder_nr"] = int(
                            model_stack.getProperty("support_extruder_nr",
                                                    "value"))

                        # Mesh modifiers;
                        model_settings[
                            "infill_mesh"] = model_stack.getProperty(
                                "infill_mesh", "value")
                        model_settings[
                            "cutting_mesh"] = model_stack.getProperty(
                                "cutting_mesh", "value")
                        model_settings[
                            "support_mesh"] = model_stack.getProperty(
                                "support_mesh", "value")
                        model_settings[
                            "anti_overhang_mesh"] = model_stack.getProperty(
                                "anti_overhang_mesh", "value")

                        model_settings[
                            "wall_line_count"] = model_stack.getProperty(
                                "wall_line_count", "value")
                        model_settings[
                            "retraction_enable"] = model_stack.getProperty(
                                "retraction_enable", "value")

                        # Infill settings
                        model_settings[
                            "infill_sparse_density"] = model_stack.getProperty(
                                "infill_sparse_density", "value")
                        model_settings[
                            "infill_pattern"] = model_stack.getProperty(
                                "infill_pattern", "value")
                        model_settings[
                            "gradual_infill_steps"] = model_stack.getProperty(
                                "gradual_infill_steps", "value")

                    model["model_settings"] = model_settings

                    data["models"].append(model)

            print_times = print_information.printTimesPerFeature
            data["print_times"] = {
                "travel":
                int(print_times["travel"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "support":
                int(print_times["support"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "infill":
                int(print_times["infill"].getDisplayString(
                    DurationFormat.Format.Seconds)),
                "total":
                int(
                    print_information.currentPrintTime.getDisplayString(
                        DurationFormat.Format.Seconds))
            }

            print_settings = dict()
            print_settings[
                "layer_height"] = global_container_stack.getProperty(
                    "layer_height", "value")

            # Support settings
            print_settings[
                "support_enabled"] = global_container_stack.getProperty(
                    "support_enable", "value")
            print_settings["support_extruder_nr"] = int(
                global_container_stack.getProperty("support_extruder_nr",
                                                   "value"))

            # Platform adhesion settings
            print_settings[
                "adhesion_type"] = global_container_stack.getProperty(
                    "adhesion_type", "value")

            # Shell settings
            print_settings[
                "wall_line_count"] = global_container_stack.getProperty(
                    "wall_line_count", "value")
            print_settings[
                "retraction_enable"] = global_container_stack.getProperty(
                    "retraction_enable", "value")

            # Prime tower settings
            print_settings[
                "prime_tower_enable"] = global_container_stack.getProperty(
                    "prime_tower_enable", "value")

            # Infill settings
            print_settings[
                "infill_sparse_density"] = global_container_stack.getProperty(
                    "infill_sparse_density", "value")
            print_settings[
                "infill_pattern"] = global_container_stack.getProperty(
                    "infill_pattern", "value")
            print_settings[
                "gradual_infill_steps"] = global_container_stack.getProperty(
                    "gradual_infill_steps", "value")

            print_settings[
                "print_sequence"] = global_container_stack.getProperty(
                    "print_sequence", "value")

            data["print_settings"] = print_settings

            # Convert data to bytes
            binary_data = json.dumps(data).encode("utf-8")

            # Sending slice info non-blocking
            reportJob = SliceInfoJob(self.info_url, binary_data)
            reportJob.start()
        except Exception:
            # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
            # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
            Logger.logException(
                "e", "Exception raised while sending slice info."
            )  # But we should be notified about these problems of course.
コード例 #59
0
ファイル: CuraEngineBackend.py プロジェクト: cmn-l10n/Cura
class CuraEngineBackend(Backend):
    def __init__(self):
        super().__init__()

        # Find out where the engine is located, and how it is called. This depends on how Cura is packaged and which OS we are running on.
        default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", "CuraEngine")
        if hasattr(sys, "frozen"):
            default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "CuraEngine")
        if sys.platform == "win32":
            default_engine_location += ".exe"
        default_engine_location = os.path.abspath(default_engine_location)
        Preferences.getInstance().addPreference("backend/location", default_engine_location)

        self._scene = Application.getInstance().getController().getScene()
        self._scene.sceneChanged.connect(self._onSceneChanged)

        # Workaround to disable layer view processing if layer view is not active.
        self._layer_view_active = False
        Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
        self._onActiveViewChanged()
        self._stored_layer_data = None

        Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onChanged)

        self._profile = None
        Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
        self._onActiveProfileChanged()

        self._change_timer = QTimer()
        self._change_timer.setInterval(500)
        self._change_timer.setSingleShot(True)
        self._change_timer.timeout.connect(self.slice)

        self._message_handlers[Cura_pb2.SlicedObjectList] = self._onSlicedObjectListMessage
        self._message_handlers[Cura_pb2.Progress] = self._onProgressMessage
        self._message_handlers[Cura_pb2.GCodeLayer] = self._onGCodeLayerMessage
        self._message_handlers[Cura_pb2.GCodePrefix] = self._onGCodePrefixMessage
        self._message_handlers[Cura_pb2.ObjectPrintTime] = self._onObjectPrintTimeMessage

        self._slicing = False
        self._restart = False
        self._enabled = True
        self._always_restart = True

        self._message = None

        self.backendConnected.connect(self._onBackendConnected)
        Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
        Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)

        Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onInstanceChanged)

    ##  Get the command that is used to call the engine.
    #   This is usefull for debugging and used to actually start the engine
    #   \return list of commands and args / parameters.
    def getEngineCommand(self):
        active_machine = Application.getInstance().getMachineManager().getActiveMachineInstance()
        if not active_machine:
            return None

        return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", active_machine.getMachineDefinition().getPath(), "-vv"]

    ##  Emitted when we get a message containing print duration and material amount. This also implies the slicing has finished.
    #   \param time The amount of time the print will take.
    #   \param material_amount The amount of material the print will use.
    printDurationMessage = Signal()

    ##  Emitted when the slicing process starts.
    slicingStarted = Signal()

    ##  Emitted whne the slicing process is aborted forcefully.
    slicingCancelled = Signal()

    ##  Perform a slice of the scene.
    def slice(self):
        if not self._enabled:
            return

        if self._slicing:
            self._slicing = False
            self._restart = True
            if self._process is not None:
                Logger.log("d", "Killing engine process")
                try:
                    self._process.terminate()
                except: # terminating a process that is already terminating causes an exception, silently ignore this.
                    pass


            if self._message:
                self._message.hide()
                self._message = None

            self.slicingCancelled.emit()
            return

        if self._profile.hasErrorValue():
            Logger.log("w", "Profile has error values. Aborting slicing")
            if self._message:
                self._message.hide()
                self._message = None
            self._message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
            self._message.show()
            return #No slicing if we have error values since those are by definition illegal values.

        self.processingProgress.emit(0.0)
        if self._message:
            self._message.setProgress(-1)
        #else:
        #    self._message = Message(catalog.i18nc("@info:status", "Slicing..."), 0, False, -1)
        #    self._message.show()

        self._scene.gcode_list = []
        self._slicing = True

        job = StartSliceJob.StartSliceJob(self._profile, self._socket)
        job.start()
        job.finished.connect(self._onStartSliceCompleted)

    def _onStartSliceCompleted(self, job):
        if job.getError() or job.getResult() != True:
            if self._message:
                self._message.hide()
                self._message = None
            return

    def _onSceneChanged(self, source):
        if type(source) is not SceneNode:
            return

        if source is self._scene.getRoot():
            return

        if source.getMeshData() is None:
            return

        if source.getMeshData().getVertices() is None:
            return

        self._onChanged()

    def _onActiveProfileChanged(self):
        if self._profile:
            self._profile.settingValueChanged.disconnect(self._onSettingChanged)

        self._profile = Application.getInstance().getMachineManager().getActiveProfile()
        if self._profile:
            self._profile.settingValueChanged.connect(self._onSettingChanged)
            self._onChanged()

    def _onSettingChanged(self, setting):
        self._onChanged()

    def _onSlicedObjectListMessage(self, message):
        if self._layer_view_active:
            job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(message)
            job.start()
        else :
            self._stored_layer_data = message

    def _onProgressMessage(self, message):
        if self._message:
            self._message.setProgress(round(message.amount * 100))

        self.processingProgress.emit(message.amount)

    def _onGCodeLayerMessage(self, message):
        self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))

    def _onGCodePrefixMessage(self, message):
        self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))

    def _onObjectPrintTimeMessage(self, message):
        self.printDurationMessage.emit(message.time, message.material_amount)
        self.processingProgress.emit(1.0)

        self._slicing = False

        if self._message:
            self._message.setProgress(100)
            self._message.hide()
            self._message = None

        if self._always_restart:
            try:
                self._process.terminate()
                self._createSocket()
            except: # terminating a process that is already terminating causes an exception, silently ignore this.
                pass

    def _createSocket(self):
        super()._createSocket()
        
        self._socket.registerMessageType(1, Cura_pb2.Slice)
        self._socket.registerMessageType(2, Cura_pb2.SlicedObjectList)
        self._socket.registerMessageType(3, Cura_pb2.Progress)
        self._socket.registerMessageType(4, Cura_pb2.GCodeLayer)
        self._socket.registerMessageType(5, Cura_pb2.ObjectPrintTime)
        self._socket.registerMessageType(6, Cura_pb2.SettingList)
        self._socket.registerMessageType(7, Cura_pb2.GCodePrefix)

    ##  Manually triggers a reslice
    def forceSlice(self):
        self._change_timer.start()

    def _onChanged(self):
        if not self._profile:
            return

        self._change_timer.start()

    def _onBackendConnected(self):
        if self._restart:
            self._onChanged()
            self._restart = False

    def _onToolOperationStarted(self, tool):
        self._enabled = False # Do not reslice when a tool is doing it's 'thing'

    def _onToolOperationStopped(self, tool):
        self._enabled = True # Tool stop, start listening for changes again.
        self._onChanged()

    def _onActiveViewChanged(self):
        if Application.getInstance().getController().getActiveView():
            view = Application.getInstance().getController().getActiveView()
            if view.getPluginId() == "LayerView":
                self._layer_view_active = True
                if self._stored_layer_data:
                    job = ProcessSlicedObjectListJob.ProcessSlicedObjectListJob(self._stored_layer_data)
                    job.start()
                    self._stored_layer_data = None
            else:
                self._layer_view_active = False


    def _onInstanceChanged(self):
        self._slicing = False
        self._restart = True
        if self._process is not None:
            Logger.log("d", "Killing engine process")
            try:
                self._process.terminate()
            except: # terminating a process that is already terminating causes an exception, silently ignore this.
                pass
        self.slicingCancelled.emit()
コード例 #60
0
class OrientationPlugin(Extension):
    def __init__(self):
        super().__init__()
        self.addMenuItem(i18n_catalog.i18n("Calculate fast optimal printing orientation"), self.doFastAutoOrientation)
        self.addMenuItem(i18n_catalog.i18n("Calculate extended optimal printing orientation"), self.doExtendedAutoOrientiation)
        self.addMenuItem(i18n_catalog.i18n("Modify Settings"), self.showPopup)
        self._message = None

        self._currently_loading_files = []  # type: List[str]
        self._check_node_queue = []  # type: List[SceneNode]
        CuraApplication.getInstance().getPreferences().addPreference("OrientationPlugin/do_auto_orientation", False)
        self._do_auto_orientation = CuraApplication.getInstance().getPreferences().getValue("OrientationPlugin/do_auto_orientation")
        # Should the volume beneath the overhangs be penalized?
        CuraApplication.getInstance().getPreferences().addPreference("OrientationPlugin/min_volume", True)

        self._popup = None

        CuraApplication.getInstance().fileLoaded.connect(self._onFileLoaded)
        CuraApplication.getInstance().fileCompleted.connect(self._onFileCompleted)
        CuraApplication.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
        CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)

    def _onPreferencesChanged(self, name: str) -> None:
        if name != "OrientationPlugin/do_auto_orientation":
            return
        self._do_auto_orientation = CuraApplication.getInstance().getPreferences().getValue("OrientationPlugin/do_auto_orientation")

    def _createPopup(self) -> None:
        # Create the plugin dialog component
        path = os.path.join(PluginRegistry.getInstance().getPluginPath("OrientationPlugin"), "SettingsPopup.qml")
        self._popup = CuraApplication.getInstance().createQmlComponent(path)
        if self._popup is None:
            return

    def showPopup(self) -> None:
        if self._popup is None:
            self._createPopup()
            if self._popup is None:
                return
        self._popup.show()

    def _onFileLoaded(self, file_name):
        self._currently_loading_files.append(file_name)

    def _onFileCompleted(self, file_name):
        if file_name in self._currently_loading_files:
            self._currently_loading_files.remove(file_name)

    def _onSceneChanged(self, node):
        if not self._do_auto_orientation:
            return  # Nothing to do!

        if not node or not node.getMeshData():
            return

        # only check meshes that have just been loaded
        if node.getMeshData().getFileName() not in self._currently_loading_files:
            return

        # the scene may change multiple times while loading a mesh,
        # but we want to check the mesh only once
        if node not in self._check_node_queue:
            self._check_node_queue.append(node)
            CuraApplication.getInstance().callLater(self.checkQueuedNodes)

    def checkQueuedNodes(self):
        for node in self._check_node_queue:
            if self._message:
                self._message.hide()
            auto_orient_message = Message(i18n_catalog.i18nc("@info:status", "Auto-Calculating the optimal orientation because auto orientation is enabled"), 0,
                                    False, -1, title=i18n_catalog.i18nc("@title", "Auto-Orientation"))
            auto_orient_message.show()
            job = CalculateOrientationJob([node], extended_mode=True, message=auto_orient_message)
            job.finished.connect(self._onFinished)
            job.start()

        self._check_node_queue = []

    def doFastAutoOrientation(self):
        self.doAutoOrientation(False)

    def doExtendedAutoOrientiation(self):
        self.doAutoOrientation(True)

    def doAutoOrientation(self, extended_mode):
        # If we still had a message open from last time, hide it.
        if self._message:
            self._message.hide()

        selected_nodes = Selection.getAllSelectedObjects()
        if len(selected_nodes) == 0:
            self._message = Message(i18n_catalog.i18nc("@info:status", "No objects selected to orient. Please select one or more objects and try again."), title = i18n_catalog.i18nc("@title", "Auto-Orientation"))
            self._message.show()
            return

        message = Message(i18n_catalog.i18nc("@info:status", "Calculating the optimal orientation..."), 0, False, -1, title = i18n_catalog.i18nc("@title", "Auto-Orientation"))
        message.show()

        job = CalculateOrientationJob(selected_nodes, extended_mode = extended_mode, message = message)
        job.finished.connect(self._onFinished)
        job.start()

    def _onFinished(self, job):
        if self._message:
            self._message.hide()

        if job.getMessage() is not None:
            job.getMessage().hide()
            self._message = Message(i18n_catalog.i18nc("@info:status", "All selected objects have been oriented."),
                                    title=i18n_catalog.i18nc("@title", "Auto-Orientation"))
            self._message.show()