def test_addAndMergeOperations():
    group_operation_1 = GroupedOperation()
    group_operation_2 = GroupedOperation()
    group_operation_3 = GroupedOperation()

    scene_node = SceneNode()

    operation_1 = TranslateOperation(scene_node, Vector(10, 10))
    operation_2 = TranslateOperation(scene_node, Vector(10, 20))
    operation_3 = Operation()

    operation_1.mergeWith = MagicMock()

    group_operation_1.addOperation(operation_1)
    assert group_operation_1.getNumChildrenOperations() == 1

    # Length of the grouped operations must be the same.
    assert not group_operation_1.mergeWith(group_operation_2)

    group_operation_3.addOperation(operation_3)

    # The base operation always says it can't be merged (so one child says it can't be merged). This should result
    # in the parent (grouped) operation also failing.
    assert not group_operation_3.mergeWith(group_operation_1)

    group_operation_2.addOperation(operation_2)
    merged_operation = group_operation_1.mergeWith(group_operation_2)

    # The merge of the nested operation should have been called once.
    assert operation_1.mergeWith.call_count == 1

    # Number of operations should still be the same.
    assert merged_operation.getNumChildrenOperations() == 1
    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()
Beispiel #3
0
    def event(self, event: Event) -> bool:
        """Handle mouse and keyboard events.

        :param event: The event to handle.
        :return: Whether this event has been caught by this tool (True) or should
        be passed on (False).
        """
        super().event(event)

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

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

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

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

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

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

            self._moved = False

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

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

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

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

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

            if not self.getDragPlane():
                return False

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

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

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

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

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

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

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

            self.setDragStart(x, y)

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

            return True

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

        return False
    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()
Beispiel #5
0
    def event(self, event):
        super().event(event)

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

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

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

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

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

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

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

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

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

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

            if not self.getDragPlane():
                return False

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

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

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

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

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

                self._distance += drag

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

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

            return True

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

        return False
Beispiel #6
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()
        global_container_stack = Application.getInstance().getGlobalContainerStack()
        machine_width = global_container_stack.getProperty("machine_width", "value")
        machine_depth = global_container_stack.getProperty("machine_depth", "value")

        arranger = Arrange.create(x = machine_width, y = machine_depth, 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)
            if offset_shape_arr is None:
                Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
                continue
            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
        not_fit_count = 0
        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).
            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)
            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, -not_fit_count * 20), set_position = True))
                not_fit_count += 1

            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"),
                                               title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
            no_full_solution_message.show()

        self.finished.emit(self)