Example #1
0
    def __init__(self, *args, **kwargs):
        super().__init__()

        self._min = Vector()
        self._max = Vector()

        if len(args) == 3:
            self.setLeft(-args[0] / 2)
            self.setRight(args[0] / 2)

            self.setTop(args[1] / 2)
            self.setBottom(-args[1] / 2)

            self.setBack(-args[2] / 2)
            self.setFront(args[2] / 2)

        if "minimum" in kwargs:
            self._min = kwargs["minimum"]

        if "maximum" in kwargs:
            self._max = kwargs["maximum"]
            
        ## Sometimes the min and max are not correctly set. 
        if not self._max:
            self._max = Vector()
            
        if not self._min:
            self._min = Vector()    

        self._ensureMinMax()
Example #2
0
 def test_add(self):
     vector = Vector(0, 1, 0)
     vector2 = Vector(1, 0, 1)
     assert vector + vector2 == Vector(1, 1, 1)
     assert vector + vector2.getData() == Vector(1, 1, 1)
     vector += vector2
     assert vector == Vector(1, 1, 1)
Example #3
0
 def findFirstAngleNormal():
     for i in range(1, ns - 1):
         spt = spine[i]
         z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
         if z.length() > EPSILON:
             return z
     # All the spines are collinear. Fallback to the rotated source
     # XZ plane.
     # TODO: handle the situation where the first two spine points match
     if len(spine) < 2:
         return Vector(0, 0, 1)
     v = spine[1] - spine[0]
     orig_y = Vector(0, 1, 0)
     orig_z = Vector(0, 0, 1)
     if v.cross(orig_y).length() > EPSILON:
         # Spine at angle with global y - rotate the z accordingly
         a = v.cross(orig_y) # Axis of rotation to get to the Z
         (x, y, z) = a.normalized().getData()  
         s = a.length()/v.length()
         c = sqrt(1-s*s)
         t = 1-c
         m = numpy.array((
             (x * x * t + c,  x * y * t + z*s, x * z * t - y * s),
             (x * y * t - z*s, y * y * t + c, y * z * t + x * s),
             (x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
         orig_z = Vector(*m.dot(orig_z.getData()))
     return orig_z
Example #4
0
 def compose(self, scale: Vector = None, shear: Vector = None, angles: Vector = None, translate: Vector = None, perspective: Vector = None, mirror: Vector = None) -> None:
     M = numpy.identity(4)
     if perspective is not None:
         P = numpy.identity(4)
         P[3, :] = perspective.getData()[:4]
         M = numpy.dot(M, P)
     if translate is not None:
         T = numpy.identity(4)
         T[:3, 3] = translate.getData()[:3]
         M = numpy.dot(M, T)
     if angles is not None:
         R = Matrix()
         R.setByEuler(angles.x, angles.y, angles.z, "sxyz")
         M = numpy.dot(M, R.getData())
     if shear is not None:
         Z = numpy.identity(4)
         Z[1, 2] = shear.x
         Z[0, 2] = shear.y
         Z[0, 1] = shear.z
         M = numpy.dot(M, Z)
     if scale is not None:
         S = numpy.identity(4)
         S[0, 0] = scale.x
         S[1, 1] = scale.y
         S[2, 2] = scale.z
         M = numpy.dot(M, S)
     if mirror is not None:
         mir = numpy.identity(4)
         mir[0, 0] *= mirror.x
         mir[1, 1] *= mirror.y
         mir[2, 2] *= mirror.z
         M = numpy.dot(M, mir)
     M /= M[3, 3]
     self._data = M
Example #5
0
    def _rotateCamera(self, x, y):
        camera = self._scene.getActiveCamera()
        if not camera or not camera.isEnabled():
            return

        self._scene.acquireLock()

        dx = math.radians(x * 180.0)
        dy = math.radians(y * 180.0)

        diff = camera.getPosition() - self._origin

        diff_flat = Vector(diff.x, 0.0, diff.z).getNormalized()
        try:
            new_angle = math.acos(diff_flat.dot(diff.getNormalized())) + dy
        except ValueError:
            return

        m = Matrix()
        m.setByRotationAxis(dx, Vector.Unit_Y)
        if new_angle < (math.pi / 2 - 0.01):
            m.rotateByAxis(dy, Vector.Unit_Y.cross(diff).normalize())

        n = diff.multiply(m)
        n += self._origin

        camera.setPosition(n)
        camera.lookAt(self._origin)

        self._scene.releaseLock()
Example #6
0
 def test_multiply(self):
     vector = Vector(2, 2, 2)
     vector2 = Vector(2, 2, 2)
     assert vector * vector2 == Vector(4, 4, 4)
     assert vector * 2 == Vector(4, 4, 4)
     assert vector.scale(vector2) == Vector(4, 4, 4)
     vector *= vector2
     assert vector == Vector(4, 4, 4)
Example #7
0
    def project(self, position: Vector) -> Tuple[float, float]:
        projection = self._projection_matrix
        view = self.getWorldTransformation()
        view.invert()

        position = position.preMultiply(view)
        position = position.preMultiply(projection)
        return position.x / position.z / 2.0, position.y / position.z / 2.0
Example #8
0
    def test_setValues(self):
        x = 10
        y = 10
        z = 10 
        temp_vector = Vector(x,y,z)
        numpy.testing.assert_array_almost_equal(temp_vector.getData(), numpy.array([x,y,z]))

        temp_vector2 = temp_vector.set(1, 2, 3)
        numpy.testing.assert_array_almost_equal(temp_vector2.getData(), numpy.array([1, 2, 3]))
Example #9
0
 def test_subtract(self):
     vector = Vector(2, 2, 2)
     vector2 = Vector(1, 1, 1)
     assert vector - vector2 == Vector(1, 1, 1)
     assert vector - vector2.getData() == Vector(1, 1, 1)
     assert vector2 - vector == Vector(-1, -1, -1)
     assert vector2 - vector.getData() == Vector(-1, -1, -1)
     vector -= vector2
     assert vector == Vector(1, 1, 1)
Example #10
0
 def __init__(self, minimum: Vector = Vector.Null, maximum: Vector = Vector.Null) -> None:
     if minimum.x > maximum.x or minimum.y > maximum.y or minimum.z > maximum.z:
         swapped_minimum = Vector(min(minimum.x, maximum.x), min(minimum.y, maximum.y), min(minimum.z, maximum.z))
         swapped_maximum = Vector(max(minimum.x, maximum.x), max(minimum.y, maximum.y), max(minimum.z, maximum.z))
         minimum = swapped_minimum
         maximum = swapped_maximum
     minimum.setRoundDigits(3)
     maximum.setRoundDigits(3)
     self._min = minimum #type: Vector
     self._max = maximum #type: Vector
Example #11
0
    def __init__(self, *args, **kwargs):
        self._position = Vector()
        self._normal = None

        if len(args) == 3:
            self._position = Vector(args[0], args[1], args[2])

        if "position" in kwargs:
            self._position = kwargs["position"]

        if "normal" in kwargs:
            self._normal = kwargs["normal"]
Example #12
0
class Vertex(object):
    ##  Construct a vertex.
    #
    #   Possible keyword parameters:
    #   - position: Vector with the vertex' position.
    #   - normal: Vector with the vertex' normal
    #
    #   Unnamed arguments:
    #   - x, y, z passed as numbers.
    def __init__(self, *args, **kwargs):
        self._position = Vector()
        self._normal = None

        if len(args) == 3:
            self._position = Vector(args[0], args[1], args[2])

        if "position" in kwargs:
            self._position = kwargs["position"]

        if "normal" in kwargs:
            self._normal = kwargs["normal"]

    ##  Get the position the vertex
    #   \returns position Vector
    @property
    def position(self):
        return self._position

    ##  Set the position the vertex
    #   \param position Vector
    def setPosition(self, position):
        self._position = position
    
    ##  Get the normal the vertex
    #   \returns normal Vector
    @property
    def normal(self):
        return self._normal
    
    ##  Set the normal the vertex
    #   \param normal Vector
    def setNormal(self, normal):
        self._normal = normal

    def hasNormal(self):
        return self._normal != None
    
    ##  Convert the vertex into a string, which is required for parsing over sockets / streams
    #   It's kinda hackish to do it this way, but it would take to much effort to implement myself.
    def toString(self):
        return numpy.array([self._position.x(),self._position.y(),self._position.z(),self._normal.x(),self._normal.y(),self._normal.z()]).tostring()
Example #13
0
    def __init__(self, vertices=None, normals=None, indices=None, colors=None, uvs=None, file_name=None,
                 center_position=None, zero_position=None, type = MeshType.faces, attributes=None):
        self._application = None  # Initialize this later otherwise unit tests break

        self._vertices = NumPyUtil.immutableNDArray(vertices)
        self._normals = NumPyUtil.immutableNDArray(normals)
        self._indices = NumPyUtil.immutableNDArray(indices)
        self._colors = NumPyUtil.immutableNDArray(colors)
        self._uvs = NumPyUtil.immutableNDArray(uvs)
        self._vertex_count = len(self._vertices) if self._vertices is not None else 0
        self._face_count = len(self._indices) if self._indices is not None else 0
        self._type = type
        self._file_name = file_name  # type: Optional[str]
        # original center position
        self._center_position = center_position
        # original zero position, is changed after transformation
        if zero_position is not None:
            self._zero_position = zero_position
        else:
            self._zero_position = Vector(0, 0, 0) # type: Vector
        self._convex_hull = None    # type: Optional[scipy.spatial.ConvexHull]
        self._convex_hull_vertices = None  # type: Optional[numpy.ndarray]
        self._convex_hull_lock = threading.Lock()

        self._attributes = {}
        if attributes is not None:
            for key, attribute in attributes.items():
                new_value = {}
                for attribute_key, attribute_value in attribute.items():
                    if attribute_key == "value":
                        new_value["value"] = NumPyUtil.immutableNDArray(attribute_value)
                    else:
                        new_value[attribute_key] = attribute_value
                self._attributes[key] = new_value
Example #14
0
    def __init__(self, parent=None):
        super().__init__()  # Call super to make multiple inheritence work.

        self._children = []
        self._mesh_data = None

        self._position = Vector()
        self._scale = Vector(1.0, 1.0, 1.0)
        self._orientation = Quaternion()

        self._transformation = None
        self._world_transformation = None

        self._derived_position = None
        self._derived_orientation = None
        self._derived_scale = None

        self._inherit_orientation = True
        self._inherit_scale = True

        self._parent = parent
        self._enabled = True
        self._selectable = False
        self._calculate_aabb = True
        self._aabb = None
        self._aabb_job = None
        self._visible = True
        self._name = ""
        self._decorators = []
        self._bounding_box_mesh = None
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)
Example #15
0
    def __init__(self, parent = None):
        super().__init__() # Call super to make multiple inheritence work.

        self._children = []
        self._mesh_data = None

        self._position = Vector()
        self._scale = Vector(1.0, 1.0, 1.0)
        self._orientation = Quaternion()

        self._transformation = None
        self._world_transformation = None

        self._derived_position = None
        self._derived_orientation = None
        self._derived_scale = None

        self._inherit_orientation = True
        self._inherit_scale = True

        self._parent = parent
        self._enabled = True
        self._selectable = False
        self._calculate_aabb = True
        self._aabb = None
        self._aabb_job = None
        self._visible = True
        self._name = ""

        if parent:
            parent.addChild(self)
Example #16
0
    def read(self, file_name, **kwargs):
        try:
            for id, reader in self._mesh_readers.items():
                result = reader.read(file_name)
                if result is not None:
                    if kwargs.get("center", True):
                        # If the result has a mesh and no children it needs to be centered
                        if result.getMeshData() and len(result.getChildren()) == 0:
                            extents = result.getMeshData().getExtents()
                            move_vector = Vector()
                            move_vector.setX(extents.center.x)
                            move_vector.setY(extents.center.y) # Ensure that bottom is on 0 (above plate)
                            move_vector.setZ(extents.center.z)
                            result.setCenterPosition(move_vector)

                            if result.getMeshData().getExtents().bottom != 0:
                               result.translate(Vector(0,-result.getMeshData().getExtents().bottom ,0))

                        # Move all the meshes of children so that toolhandles are shown in the correct place.
                        for node in result.getChildren():
                            if node.getMeshData():
                                extents = node.getMeshData().getExtents()
                                m = Matrix()
                                m.translate(-extents.center)
                                node.setMeshData(node.getMeshData().getTransformed(m))
                                node.translate(extents.center)
                    return result

        except OSError as e:
            Logger.log("e", str(e))

        Logger.log("w", "Unable to read file %s", file_name)
        return None #unable to read
Example #17
0
    def __imul__(self, other):
        if type(other) is Quaternion:
            v1 = Vector(other.x, other.y, other.z)
            v2 = Vector(self.x, self.y, self.z)

            w = other.w * self.w - v1.dot(v2)
            v = v2 * other.w + v1 * self.w + v2.cross(v1)

            self._data[0] = v.x
            self._data[1] = v.y
            self._data[2] = v.z
            self._data[3] = w
        elif type(other) is float or type(other) is int:
            self._data *= other
        else:
            raise NotImplementedError()

        return self
Example #18
0
    def _onChangeTimerFinished(self):
        root = self._controller.getScene().getRoot()
        for node in BreadthFirstIterator(root):
            if node is root or type(node) is not SceneNode:
                continue

            bbox = node.getBoundingBox()
            if not bbox or not bbox.isValid():
                continue

            # Mark the node as outside the build volume if the bounding box test fails.
            if self._build_volume.getBoundingBox().intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
                node._outside_buildarea = True
            else:
                node._outside_buildarea = False

            # Move the node upwards if the bottom is below the build platform.
            move_vector = Vector()
            if not Float.fuzzyCompare(bbox.bottom, 0.0):
                move_vector.setY(-bbox.bottom)

            # If there is no convex hull for the node, start calculating it and continue.
            if not hasattr(node, "_convex_hull"):
                if not hasattr(node, "_convex_hull_job"):
                    job = ConvexHullJob.ConvexHullJob(node)
                    job.start()
                    node._convex_hull_job = job
            elif Selection.isSelected(node):
                pass
            else:
                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or type(other_node) is not SceneNode or other_node is node:
                        continue

                    # Ignore nodes that do not have the right properties set.
                    if not hasattr(other_node, "_convex_hull") or not other_node.getBoundingBox():
                        continue

                    # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects.
                    if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection:
                        continue

                    # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                    overlap = node._convex_hull.intersectsPolygon(other_node._convex_hull)
                    if overlap is None:
                        continue

                    move_vector.setX(overlap[0] * 1.1)
                    move_vector.setZ(overlap[1] * 1.1)

            if move_vector != Vector():
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()

            if node.getBoundingBox().intersectsBox(self._build_volume.getBoundingBox()) == AxisAlignedBox.IntersectionResult.FullIntersection:
                op = ScaleToBoundsOperation(node, self._build_volume.getBoundingBox())
                op.push()
Example #19
0
    def _zoomCamera(self, zoom_range: float, event: Optional[Event] = None) -> None:
        camera = self._scene.getActiveCamera()
        if not camera or not camera.isEnabled():
            return

        self.clipToZoom()

        self._scene.getSceneLock().acquire()

        r = (camera.getWorldPosition() - self._origin).length()
        delta = r * (zoom_range / 128 / 10.0)
        r -= delta

        if self._invert_zoom:
            delta *= -1

        move_vector = Vector(0.0, 0.0, 1.0)

        if event is not None and self._zoom_to_mouse:
            viewport_center_x = QtApplication.getInstance().getRenderer().getViewportWidth() / 2
            viewport_center_y = QtApplication.getInstance().getRenderer().getViewportHeight() / 2
            main_window = cast(MainWindow, QtApplication.getInstance().getMainWindow())
            mouse_diff_center_x = viewport_center_x - main_window.mouseX
            mouse_diff_center_y = viewport_center_y - main_window.mouseY

            x_component = mouse_diff_center_x / QtApplication.getInstance().getRenderer().getViewportWidth()
            y_component = mouse_diff_center_y / QtApplication.getInstance().getRenderer().getViewportHeight()

            move_vector = Vector(x_component, -y_component, 1)
            move_vector = move_vector.normalized()

        move_vector = -delta * move_vector
        if delta != 0:
            if self._min_zoom < r < self._max_zoom:
                camera.translate(move_vector)
                if self._zoom_to_mouse:
                    # Set the origin of the camera to the new distance, right in front of the new camera position.
                    self._origin = (r * Vector(0.0, 0.0, -1.0)).preMultiply(camera.getWorldTransformation())
        self._scene.getSceneLock().release()
Example #20
0
class TestVector(unittest.TestCase):
    def setUp(self):
        # Called before the first testfunction is executed
        self._vector = Vector(1, 0, 0)
        pass

    def tearDown(self):
        # Called after the last testfunction was executed
        pass

    def test_getData(self):
        numpy.testing.assert_array_almost_equal(self._vector.getData(), numpy.array([1, 0, 0]))
        pass

    def test_angleBetweenVectors(self):
        second_vector = Vector(1, 0, 0)
        third_vector = Vector(0, 1, 0)
        fourth_vector = Vector(0, 0, 1)
        # Check if angle with itself is 0
        self.assertEqual(self._vector.angleToVector(second_vector), 0)
        # Check if angle between the two vectors that are rotated in equal angle but different direction are the same
        self.assertEqual(self._vector.angleToVector(third_vector), self._vector.angleToVector(fourth_vector))

    def test_normalize(self):
        pass

    def test_setValues(self):
        x = 10
        y = 10
        z = 10
        temp_vector = Vector(x, y, z)
        numpy.testing.assert_array_almost_equal(temp_vector.getData(), numpy.array([x, y, z]))

    def test_negPos(self):
        v = Vector(0, 1, 0)

        self.assertEqual(Vector(0, -1, 0), -v)
        self.assertEqual(Vector(0, 1, 0), v)  # - should have no side effects
Example #21
0
def findOuterNormal(face):
    n = len(face)
    for i in range(n):
        for j in range(i+1, n):
            edge = face[j] - face[i]
            if edge.length() > EPSILON:
                edge = edge.normalized()
                prev_rejection = Vector()
                is_outer = True
                for k in range(n):
                    if k != i and k != j:
                        pt = face[k] - face[i]
                        pte = pt.dot(edge)
                        rejection = pt - edge*pte
                        if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
                            is_outer = False
                            break
                        elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability 
                            prev_rejection = rejection
                        
                if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
                    return edge.cross(prev_rejection)

    return False
Example #22
0
 def setByRotationAxis(self, angle: float, direction: Vector, point: Optional[List[float]] = None) -> None:
     sina = math.sin(angle)
     cosa = math.cos(angle)
     direction_data = self._unitVector(direction.getData())
     # rotation matrix around unit vector
     R = numpy.diag([cosa, cosa, cosa])
     R += numpy.outer(direction_data, direction_data) * (1.0 - cosa)
     direction_data *= sina
     R += numpy.array([[ 0.0, -direction_data[2], direction_data[1]],
                     [ direction_data[2], 0.0, -direction_data[0]],
                     [-direction_data[1], direction_data[0], 0.0]], dtype = numpy.float64)
     M = numpy.identity(4)
     M[:3, :3] = R
     if point is not None:
         # rotation not around origin
         point = numpy.array(point[:3], dtype = numpy.float64, copy=False)
         M[:3, 3] = point - numpy.dot(R, point)
     self._data = M
Example #23
0
    def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalized()
        up = up.normalized()
        s = f.cross(up).normalized()
        u = s.cross(f).normalized()

        m = Matrix([
            [ s.x,  u.x,  -f.x, 0.0],
            [ s.y,  u.y,  -f.y, 0.0],
            [ s.z,  u.z,  -f.z, 0.0],
            [ 0.0,  0.0,  0.0,  1.0]
        ])

        self.setOrientation(Quaternion.fromMatrix(m))
    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.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
                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
Example #25
0
    def snapshot(width=300, height=300):
        scene = Application.getInstance().getController().getScene()
        active_camera = scene.getActiveCamera()
        render_width, render_height = active_camera.getWindowSize()
        render_width = int(render_width)
        render_height = int(render_height)
        preview_pass = PreviewPass(render_width, render_height)

        root = scene.getRoot()
        camera = Camera("snapshot", root)

        # determine zoom and look at
        bbox = None
        for node in DepthFirstIterator(root):
            if not getattr(node, "_outside_buildarea", False):
                if node.callDecoration("isSliceable") and node.getMeshData(
                ) and node.isVisible(
                ) and not node.callDecoration("isNonThumbnailVisibleMesh"):
                    if bbox is None:
                        bbox = node.getBoundingBox()
                    else:
                        bbox = bbox + node.getBoundingBox()
        # If there is no bounding box, it means that there is no model in the buildplate
        if bbox is None:
            return None

        look_at = bbox.center
        # guessed size so the objects are hopefully big
        size = max(bbox.width, bbox.height, bbox.depth * 0.5)

        # Looking from this direction (x, y, z) in OGL coordinates
        looking_from_offset = Vector(-1, 1, 2)
        if size > 0:
            # determine the watch distance depending on the size
            looking_from_offset = looking_from_offset * size * 1.3
        camera.setPosition(look_at + looking_from_offset)
        camera.lookAt(look_at)

        satisfied = False
        size = None
        fovy = 30

        while not satisfied:
            if size is not None:
                satisfied = True  # always be satisfied after second try
            projection_matrix = Matrix()
            # Somehow the aspect ratio is also influenced in reverse by the screen width/height
            # So you have to set it to render_width/render_height to get 1
            projection_matrix.setPerspective(fovy,
                                             render_width / render_height, 1,
                                             500)
            camera.setProjectionMatrix(projection_matrix)
            preview_pass.setCamera(camera)
            preview_pass.render()
            pixel_output = preview_pass.getOutput()

            min_x, max_x, min_y, max_y = Snapshot.getImageBoundaries(
                pixel_output)

            size = max((max_x - min_x) / render_width,
                       (max_y - min_y) / render_height)
            if size > 0.5 or satisfied:
                satisfied = True
            else:
                # make it big and allow for some empty space around
                fovy *= 0.5  # strangely enough this messes up the aspect ratio: fovy *= size * 1.1

        # make it a square
        if max_x - min_x >= max_y - min_y:
            # make y bigger
            min_y, max_y = int((max_y + min_y) / 2 -
                               (max_x - min_x) / 2), int((max_y + min_y) / 2 +
                                                         (max_x - min_x) / 2)
        else:
            # make x bigger
            min_x, max_x = int((max_x + min_x) / 2 -
                               (max_y - min_y) / 2), int((max_x + min_x) / 2 +
                                                         (max_y - min_y) / 2)
        cropped_image = pixel_output.copy(min_x, min_y, max_x - min_x,
                                          max_y - min_y)

        # Scale it to the correct size
        scaled_image = cropped_image.scaled(
            width,
            height,
            aspectRatioMode=QtCore.Qt.IgnoreAspectRatio,
            transformMode=QtCore.Qt.SmoothTransformation)

        return scaled_image
Example #26
0
    def _onChangeTimerFinished(self):
        if not self._enabled:
            return

        root = self._controller.getScene().getRoot()

        # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
        # same direction.
        transformed_nodes = []

        group_nodes = []
        # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
        # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
        nodes = list(BreadthFirstIterator(root))
        random.shuffle(nodes)
        for node in nodes:
            if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
                continue

            bbox = node.getBoundingBox()

            # Ignore intersections with the bottom
            build_volume_bounding_box = self._build_volume.getBoundingBox()
            if build_volume_bounding_box:
                # It's over 9000!
                build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
            else:
                # No bounding box. This is triggered when running Cura from command line with a model for the first time
                # In that situation there is a model, but no machine (and therefore no build volume.
                return
            node._outside_buildarea = False

            # Mark the node as outside the build volume if the bounding box test fails.
            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
                node._outside_buildarea = True

            if node.callDecoration("isGroup"):
                group_nodes.append(node)  # Keep list of affected group_nodes

            # Move it downwards if bottom is above platform
            move_vector = Vector()
            if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
                z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
                move_vector = move_vector.set(y=-bbox.bottom + z_offset)

            # If there is no convex hull for the node, start calculating it and continue.
            if not node.getDecorator(ConvexHullDecorator):
                node.addDecorator(ConvexHullDecorator())

            if Preferences.getInstance().getValue("physics/automatic_push_free"):
                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or type(other_node) is not SceneNode or other_node is node:
                        continue
                    
                    # Ignore collisions of a group with it's own children
                    if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                        continue
                    
                    # Ignore collisions within a group
                    if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
                        continue
                    
                    # Ignore nodes that do not have the right properties set.
                    if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                        continue

                    if other_node in transformed_nodes:
                        continue  # Other node is already moving, wait for next pass.

                    overlap = (0, 0)  # Start loop with no overlap
                    current_overlap_checks = 0
                    # Continue to check the overlap until we no longer find one.
                    while overlap and current_overlap_checks < self._max_overlap_checks:
                        current_overlap_checks += 1
                        head_hull = node.callDecoration("getConvexHullHead")
                        if head_hull:  # One at a time intersection.
                            overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
                            if not overlap:
                                other_head_hull = other_node.callDecoration("getConvexHullHead")
                                if other_head_hull:
                                    overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
                                    if overlap:
                                        # Moving ensured that overlap was still there. Try anew!
                                        move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
                                                                      z=move_vector.z + overlap[1] * self._move_factor)
                            else:
                                # Moving ensured that overlap was still there. Try anew!
                                move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
                                                              z=move_vector.z + overlap[1] * self._move_factor)
                        else:
                            own_convex_hull = node.callDecoration("getConvexHull")
                            other_convex_hull = other_node.callDecoration("getConvexHull")
                            if own_convex_hull and other_convex_hull:
                                overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
                                if overlap:  # Moving ensured that overlap was still there. Try anew!
                                    move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
                                                                  z=move_vector.z + overlap[1] * self._move_factor)
                            else:
                                # This can happen in some cases if the object is not yet done with being loaded.
                                #  Simply waiting for the next tick seems to resolve this correctly.
                                overlap = None

            convex_hull = node.callDecoration("getConvexHull")
            if convex_hull:
                if not convex_hull.isValid():
                    return
                # Check for collisions between disallowed areas and the object
                for area in self._build_volume.getDisallowedAreas():
                    overlap = convex_hull.intersectsPolygon(area)
                    if overlap is None:
                        continue
                    node._outside_buildarea = True

            if not Vector.Null.equals(move_vector, epsilon=1e-5):
                transformed_nodes.append(node)
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()

        # Group nodes should override the _outside_buildarea property of their children.
        for group_node in group_nodes:
            for child_node in group_node.getAllChildren():
                child_node._outside_buildarea = group_node._outside_buildarea
Example #27
0
    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._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
Example #28
0
 def getTranslation(self) -> Vector:
     return Vector(data=self._data[:3, 3])
Example #29
0
    def test_translateWorld(self):
        node1 = SceneNode()

        node2 = SceneNode(node1)

        self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 0))

        node1.translate(Vector(0, 0, 10))

        self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10))
        self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 10))

        node2.translate(Vector(0, 0, 10))

        self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10))
        self.assertEqual(node2.getWorldPosition(), Vector(0, 0, 20))

        node1.rotate(Quaternion.fromAngleAxis(math.pi / 2, Vector.Unit_Y))

        self.assertEqual(node1.getWorldPosition(), Vector(0, 0, 10))
        self.assertEqual(node2.getWorldPosition(), Vector(10, 0, 10))

        node2.translate(Vector(0, 0, 10))

        # Local translation on Z with a parent rotated 90 degrees results in movement on X axis
        pos = node2.getWorldPosition()
        #Using fuzzyCompare due to accumulation of floating point error
        self.assertTrue(
            Float.fuzzyCompare(pos.x, 20, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 10)))
        self.assertTrue(
            Float.fuzzyCompare(pos.y, 0, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 10)))
        self.assertTrue(
            Float.fuzzyCompare(pos.z, 10, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 10)))

        node2.translate(Vector(0, 0, 10), SceneNode.TransformSpace.World)

        # World translation on Z with a parent rotated 90 degrees results in movement on Z axis
        pos = node2.getWorldPosition()
        self.assertTrue(
            Float.fuzzyCompare(pos.x, 20, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 20)))
        self.assertTrue(
            Float.fuzzyCompare(pos.y, 0, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 20)))
        self.assertTrue(
            Float.fuzzyCompare(pos.z, 20, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(20, 0, 20)))

        node1.translate(Vector(0, 0, 10))

        self.assertEqual(node1.getWorldPosition(), Vector(10, 0, 10))

        pos = node2.getWorldPosition()
        self.assertTrue(
            Float.fuzzyCompare(pos.x, 30, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(30, 0, 20)))
        self.assertTrue(
            Float.fuzzyCompare(pos.y, 0, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(30, 0, 20)))
        self.assertTrue(
            Float.fuzzyCompare(pos.z, 20, 1e-5),
            "{0} does not equal {1}".format(pos, Vector(30, 0, 20)))

        node1.scale(Vector(2, 2, 2))

        pos = node2.getWorldPosition()
        self.assertTrue(
            Float.fuzzyCompare(pos.x, 50, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(50, 0, 30)))
        self.assertTrue(
            Float.fuzzyCompare(pos.y, 0, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(50, 0, 30)))
        self.assertTrue(
            Float.fuzzyCompare(pos.z, 30, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(50, 0, 30)))

        node2.translate(Vector(0, 0, 10))

        pos = node2.getWorldPosition()
        self.assertTrue(
            Float.fuzzyCompare(pos.x, 70, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(70, 0, 30)))
        self.assertTrue(
            Float.fuzzyCompare(pos.y, 0, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(70, 0, 30)))
        self.assertTrue(
            Float.fuzzyCompare(pos.z, 30, 1e-4),
            "{0} does not equal {1}".format(pos, Vector(70, 0, 30)))

        # World space set position
        node1 = SceneNode()
        node2 = SceneNode(node1)
        node1.setPosition(Vector(15, 15, 15))
        node2.setPosition(Vector(10, 10, 10))
        self.assertEqual(node2.getWorldPosition(), Vector(25, 25, 25))
        #node2.setPosition(Vector(15,15,15), SceneNode.TransformSpace.World)
        #self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 15))
        #self.assertEqual(node2.getPosition(), Vector(0,0,0))

        node1.setPosition(Vector(15, 15, 15))
        node2.setPosition(Vector(0, 0, 0))
        node2.rotate(Quaternion.fromAngleAxis(-math.pi / 2, Vector.Unit_Y))
        node2.translate(Vector(10, 0, 0))
        self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25))

        node2.setPosition(Vector(15, 15, 25), SceneNode.TransformSpace.World)
        self.assertEqual(node2.getWorldPosition(), Vector(15, 15, 25))
        self.assertEqual(node2.getPosition(), Vector(0, 0, 10))
Example #30
0
    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)
Example #31
0
 def updateCurrentValue(self, value):
     self._camera_tool.setOrigin(Vector(value.x(), value.y(), value.z()))
Example #32
0
    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:
                position = int(
                    extruder.getMetaDataEntry("position",
                                              default="0"))  # Get the position
                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(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)
Example #33
0
    def processCliStream(self, stream: str) -> Optional[SteSlicerSceneNode]:
        Logger.log("d", "Preparing to load CLI")
        self._cancelled = False
        self._setPrintSettings()
        self._is_layers_in_file = False

        scene_node = SteSlicerSceneNode()

        gcode_list = []
        self._writeStartCode(gcode_list)
        gcode_list.append(";LAYER_COUNT\n")

        # Reading starts here
        file_lines = 0
        current_line = 0
        for line in stream.split("\n"):
            file_lines += 1
            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 CLI"),
                                lifetime=0,
                                title=catalog.i18nc("@info:title", "CLI Details"))

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

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

        self._position = Position(0, 0, 0, 0, 0, 1, 0, [0])
        self._gcode_position = Position(0, 0, 0, 0, 0, 0, 0, [0])
        current_path = []  # type: List[List[float]]
        geometry_start = False
        for line in stream.split("\n"):
            if self._cancelled:
                Logger.log("d", "Parsing CLI 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 == "$$GEOMETRYSTART":
                geometry_start = True
                continue
            if not geometry_start:
                continue

            if self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
                try:
                    layer_height = float(line[len(self._layer_keyword):])
                    self._current_layer_thickness = layer_height - self._current_layer_height
                    if self._current_layer_thickness > 0.4:
                        self._current_layer_thickness = 0.2
                    self._current_layer_height = layer_height
                    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([self._position.x, self._position.y, self._position.z, self._position.a, self._position.b,
                                         self._position.c, self._position.f, self._position.e[self._extruder_number], LayerPolygon.MoveCombingType])
                    self._layer_number += 1
                    gcode_list.append(";LAYER:%s\n" % self._layer_number)
                except:
                    pass
                
            if line.find(self._body_type_keyword) == 0:
                self._layer_type = LayerPolygon.Inset0Type
            if line.find(self._support_type_keyword) == 0:
                self._layer_type = LayerPolygon.SupportType
            if line.find(self._perimeter_type_keyword) == 0:
                self._layer_type = LayerPolygon.Inset0Type
            if line.find(self._skin_type_keyword) == 0:
                self._layer_type = LayerPolygon.SkinType
            if line.find(self._infill_type_keyword) == 0:
                self._layer_type = LayerPolygon.InfillType

            # Comment line
            if line.startswith("//"):
                continue

            # Polyline processing
            self.processPolyline(line, current_path, gcode_list)

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

        layer_count_idx = gcode_list.index(";LAYER_COUNT\n")
        if layer_count_idx > 0:
            gcode_list[layer_count_idx] = ";LAYER_COUNT:%s\n" % self._layer_number

        end_gcode = self._global_stack.getProperty(
            "machine_end_gcode", "value")
        gcode_list.append(end_gcode + "\n")
        
        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 = SteSlicerApplication.getInstance(
        ).getMultiBuildPlateModel().activeBuildPlate
        gcode_dict = {active_build_plate_id: gcode_list}
        # type: ignore #Because gcode_dict is generated dynamically.
        SteSlicerApplication.getInstance().getController().getScene().gcode_dict = gcode_dict

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

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

        if not self._global_stack.getProperty("machine_center_is_zero", "value"):
            machine_width = self._global_stack.getProperty(
                "machine_width", "value")
            machine_depth = self._global_stack.getProperty(
                "machine_depth", "value")
            scene_node.setPosition(
                Vector(-machine_width / 2, 0, machine_depth / 2))

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

        if SteSlicerApplication.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()

        backend = SteSlicerApplication.getInstance().getBackend()
        backend.backendStateChange.emit(Backend.BackendState.Disabled)

        return scene_node
Example #34
0
    def _onChangeTimerFinished(self):
        if not self._enabled:
            return

        root = self._controller.getScene().getRoot()
        for node in BreadthFirstIterator(root):
            if node is root or type(node) is not SceneNode:
                continue

            bbox = node.getBoundingBox()
            if not bbox or not bbox.isValid():
                self._change_timer.start()
                continue

            build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox())
            build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom
            node._outside_buildarea = False

            # Mark the node as outside the build volume if the bounding box test fails.
            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
                node._outside_buildarea = True
            else:
                # When printing one at a time too high objects are not printable.
                if Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("print_sequence") == "one_at_a_time":
                    if node.getBoundingBox().height > Application.getInstance().getMachineManager().getWorkingProfile().getSettingValue("gantry_height"):
                        node._outside_buildarea = True

            # Move it downwards if bottom is above platform
            move_vector = Vector()
            if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
                z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
                if bbox.bottom > 0:
                    move_vector.setY(-bbox.bottom + z_offset)
                elif bbox.bottom < z_offset:
                    move_vector.setY((-bbox.bottom) - z_offset)

            #if not Float.fuzzyCompare(bbox.bottom, 0.0):
            #   pass#move_vector.setY(-bbox.bottom)

            # If there is no convex hull for the node, start calculating it and continue.
            if not node.getDecorator(ConvexHullDecorator):
                node.addDecorator(ConvexHullDecorator())
            
            if not node.callDecoration("getConvexHull"):
                if not node.callDecoration("getConvexHullJob"):
                    job = ConvexHullJob.ConvexHullJob(node)
                    job.start()
                    node.callDecoration("setConvexHullJob", job)
                    
            elif Preferences.getInstance().getValue("physics/automatic_push_free"):
                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or type(other_node) is not SceneNode or other_node is node:
                        continue
                    
                    # Ignore colissions of a group with it's own children
                    if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                        continue
                    
                    # Ignore colissions within a group
                    if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
                        continue
                        #if node.getParent().callDecoration("isGroup") is other_node.getParent().callDecoration("isGroup"):
                        #    continue
                    
                    # Ignore nodes that do not have the right properties set.
                    if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                        continue

                    # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects.
                    #if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection:
                    #    continue

                    # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                    try:
                        head_hull = node.callDecoration("getConvexHullHead")
                        if head_hull:
                            overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull"))
                            if not overlap:
                                other_head_hull = other_node.callDecoration("getConvexHullHead")
                                if other_head_hull:
                                    overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull)
                        else:
                            overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_node.callDecoration("getConvexHull"))
                    except:
                        overlap = None #It can sometimes occur that the calculated convex hull has no size, in which case there is no overlap.

                    if overlap is None:
                        continue
                    move_vector.setX(overlap[0] * 1.1)
                    move_vector.setZ(overlap[1] * 1.1)
            convex_hull = node.callDecoration("getConvexHull")
            if convex_hull:
                if not convex_hull.isValid():
                    return
                # Check for collisions between disallowed areas and the object
                for area in self._build_volume.getDisallowedAreas():
                    overlap = convex_hull.intersectsPolygon(area)
                    if overlap is None:
                        continue

                    node._outside_buildarea = True

            if move_vector != Vector():
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()
Example #35
0
    def addPyramid(self,
                   width,
                   height,
                   depth,
                   angle=0,
                   axis=Vector.Unit_Y,
                   center=Vector(0, 0, 0),
                   color=None):
        """Adds a pyramid to the mesh of this mesh builder.

        :param width: The width of the base of the pyramid.
        :param height: The height of the pyramid (from base to notch).
        :param depth: The depth of the base of the pyramid.
        :param angle: (Optional) An angle of rotation to rotate the pyramid by,
        in degrees.
        :param axis: (Optional) An axis of rotation to rotate the pyramid around.
        If no axis is provided and the angle of rotation is nonzero, the pyramid
        will be rotated around the Y-axis.
        :param center: (Optional) The position of the centre of the base of the
        pyramid. If not provided, the pyramid will be placed on the coordinate
        origin.
        :param color: (Optional) The colour of the pyramid. If no colour is
        provided, a colour will be determined by the shader.
        """

        angle = math.radians(angle)

        minW = -width / 2
        maxW = width / 2
        minD = -depth / 2
        maxD = depth / 2

        start = self.getVertexCount()  #Starting index.

        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        verts = numpy.asarray(
            [  #All 5 vertices of the pyramid.
                [minW, 0, maxD], [maxW, 0, maxD], [minW, 0, minD],
                [maxW, 0, minD], [0, height, 0]
            ],
            dtype=numpy.float32)
        verts = verts.dot(
            matrix.getData()[0:3, 0:3])  #Rotate the pyramid around the axis.
        verts[:] += center.getData()
        self.addVertices(verts)

        indices = numpy.asarray(
            [  #Connect the vertices to each other (6 triangles).
                [start, start + 1, start + 4],  #The four sides of the pyramid.
                [start + 1, start + 3, start + 4],
                [start + 3, start + 2, start + 4],
                [start + 2, start, start + 4],
                [start, start + 3, start + 1],  #The base of the pyramid.
                [start, start + 2, start + 3]
            ],
            dtype=numpy.int32)
        self.addIndices(indices)

        if color:  #If we have a colour, add the colour to each of the vertices.
            vertex_count = self.getVertexCount()
            for i in range(1, 6):
                self.setVertexColor(vertex_count - i, color)
Example #36
0
    def addDonut(self,
                 inner_radius,
                 outer_radius,
                 width,
                 center=Vector(0, 0, 0),
                 sections=32,
                 color=None,
                 angle=0,
                 axis=Vector.Unit_Y):
        """Adds a torus to the mesh of this mesh builder.

        The torus is the shape of a doughnut. This doughnut is delicious and
        moist, but not very healthy.

        :param inner_radius: The radius of the hole inside the torus. Must be
        smaller than outer_radius.
        :param outer_radius: The radius of the outside of the torus. Must be
        larger than inner_radius.
        :param width: The radius of the torus in perpendicular direction to its
        perimeter. This is the "thickness".
        :param center: (Optional) The position of the centre of the torus. If no
        position is provided, the torus will be centred around the coordinate
        origin.
        :param sections: (Optional) The resolution of the torus in the
        circumference. The resolution of the intersection of the torus cannot be
        changed.
        :param color: (Optional) The colour of the torus. If no colour is
        provided, a colour will be determined by the shader.
        :param angle: (Optional) An angle of rotation to rotate the torus by, in
        radians.
        :param axis: (Optional) An axis of rotation to rotate the torus around.
        If no axis is provided and the angle of rotation is nonzero, the torus
        will be rotated around the Y-axis.
        """

        vertices = []
        indices = []
        colors = []

        start = self.getVertexCount()  #Starting index.

        for i in range(sections):
            v1 = start + i * 3  #Indices for each of the vertices we'll add for this section.
            v2 = v1 + 1
            v3 = v1 + 2
            v4 = v1 + 3
            v5 = v1 + 4
            v6 = v1 + 5

            if i + 1 >= sections:  # connect the end to the start
                v4 = start
                v5 = start + 1
                v6 = start + 2

            theta = i * math.pi / (
                sections / 2)  #Angle of this piece around torus perimeter.
            c = math.cos(theta)  #X-coordinate around torus perimeter.
            s = math.sin(theta)  #Y-coordinate around torus perimeter.

            #One vertex on the inside perimeter, two on the outside perimiter (up and down).
            vertices.append([inner_radius * c, inner_radius * s, 0])
            vertices.append([outer_radius * c, outer_radius * s, width])
            vertices.append([outer_radius * c, outer_radius * s, -width])

            #Connect the vertices to the next segment.
            indices.append([v1, v4, v5])
            indices.append([v2, v1, v5])

            indices.append([v2, v5, v6])
            indices.append([v3, v2, v6])

            indices.append([v3, v6, v4])
            indices.append([v1, v3, v4])

            if color:  #If we have a colour, add it to the vertices.
                colors.append([color.r, color.g, color.b, color.a])
                colors.append([color.r, color.g, color.b, color.a])
                colors.append([color.r, color.g, color.b, color.a])

        #Rotate the resulting torus around the specified axis.
        matrix = Matrix()
        matrix.setByRotationAxis(angle, axis)
        vertices = numpy.asarray(vertices, dtype=numpy.float32)
        vertices = vertices.dot(matrix.getData()[0:3, 0:3])
        vertices[:] += center.getData(
        )  #And translate to the desired position.

        self.addVertices(vertices)
        self.addIndices(numpy.asarray(indices, dtype=numpy.int32))
        self.addColors(numpy.asarray(colors, dtype=numpy.float32))
Example #37
0
    def addCube(self,
                width,
                height,
                depth,
                center=Vector(0, 0, 0),
                color=None):
        """Add a rectangular cuboid to the mesh of this mesh builder.

        A rectangular cuboid is a square block with arbitrary width, height and
        depth.

        :param width: The size of the rectangular cuboid in the X dimension.
        :param height: The size of the rectangular cuboid in the Y dimension.
        :param depth: The size of the rectangular cuboid in the Z dimension.
        :param center: (Optional) The position of the centre of the rectangular
        cuboid in space. If not provided, the cuboid is placed at the coordinate
        origin.
        :param color: (Optional) The colour for the rectangular cuboid. If no
        colour is provided, the colour is determined by the shader.
        """

        #Compute the actual positions of the planes.
        minW = -width / 2 + center.x
        maxW = width / 2 + center.x
        minH = -height / 2 + center.y
        maxH = height / 2 + center.y
        minD = -depth / 2 + center.z
        maxD = depth / 2 + center.z

        start = self.getVertexCount()

        verts = numpy.asarray(
            [  #All 8 corners.
                [minW, minH, maxD],
                [minW, maxH, maxD],
                [maxW, maxH, maxD],
                [maxW, minH, maxD],
                [minW, minH, minD],
                [minW, maxH, minD],
                [maxW, maxH, minD],
                [maxW, minH, minD],
            ],
            dtype=numpy.float32)
        self.addVertices(verts)

        indices = numpy.asarray(
            [  #All 6 quads (12 triangles).
                [start, start + 2, start + 1], [start, start + 3, start + 2],
                [start + 3, start + 7, start + 6],
                [start + 3, start + 6, start + 2],
                [start + 7, start + 5, start + 6],
                [start + 7, start + 4, start + 5],
                [start + 4, start + 1, start + 5],
                [start + 4, start + 0, start + 1],
                [start + 1, start + 6, start + 5],
                [start + 1, start + 2, start + 6],
                [start + 0, start + 7, start + 3],
                [start + 0, start + 4, start + 7]
            ],
            dtype=numpy.int32)
        self.addIndices(indices)

        if color:  #If we have a colour, add a colour to all of the vertices.
            vertex_count = self.getVertexCount()
            for i in range(1, 9):
                self.setVertexColor(vertex_count - i, color)
Example #38
0
    def _generateSceneNode(self, file_name, xz_size, height_from_base,
                           base_height, blur_iterations, max_size,
                           lighter_is_higher, use_transparency_model,
                           transmittance_1mm):
        scene_node = SceneNode()

        mesh = MeshBuilder()

        img = QImage(file_name)

        if img.isNull():
            Logger.log("e", "Image is corrupt.")
            return None

        width = max(img.width(), 2)
        height = max(img.height(), 2)
        aspect = height / width

        if img.width() < 2 or img.height() < 2:
            img = img.scaled(width, height, Qt.IgnoreAspectRatio)

        height_from_base = max(height_from_base, 0)
        base_height = max(base_height, 0)
        peak_height = base_height + height_from_base

        xz_size = max(xz_size, 1)
        scale_vector = Vector(xz_size, peak_height, xz_size)

        if width > height:
            scale_vector = scale_vector.set(z=scale_vector.z * aspect)
        elif height > width:
            scale_vector = scale_vector.set(x=scale_vector.x / aspect)

        if width > max_size or height > max_size:
            scale_factor = max_size / width
            if height > width:
                scale_factor = max_size / height

            width = int(max(round(width * scale_factor), 2))
            height = int(max(round(height * scale_factor), 2))
            img = img.scaled(width, height, Qt.IgnoreAspectRatio)

        width_minus_one = width - 1
        height_minus_one = height - 1

        Job.yieldThread()

        texel_width = 1.0 / (width_minus_one) * scale_vector.x
        texel_height = 1.0 / (height_minus_one) * scale_vector.z

        height_data = numpy.zeros((height, width), dtype=numpy.float32)

        for x in range(0, width):
            for y in range(0, height):
                qrgb = img.pixel(x, y)
                if use_transparency_model:
                    height_data[y, x] = (
                        0.299 * math.pow(qRed(qrgb) / 255.0, 2.2) +
                        0.587 * math.pow(qGreen(qrgb) / 255.0, 2.2) +
                        0.114 * math.pow(qBlue(qrgb) / 255.0, 2.2))
                else:
                    height_data[y, x] = (
                        0.212655 * qRed(qrgb) + 0.715158 * qGreen(qrgb) +
                        0.072187 * qBlue(qrgb)
                    ) / 255  # fast computation ignoring gamma and degamma

        Job.yieldThread()

        if lighter_is_higher == use_transparency_model:
            height_data = 1 - height_data

        for _ in range(0, blur_iterations):
            copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode="edge")

            height_data += copy[1:-1, 2:]
            height_data += copy[1:-1, :-2]
            height_data += copy[2:, 1:-1]
            height_data += copy[:-2, 1:-1]

            height_data += copy[2:, 2:]
            height_data += copy[:-2, 2:]
            height_data += copy[2:, :-2]
            height_data += copy[:-2, :-2]

            height_data /= 9

            Job.yieldThread()

        if use_transparency_model:
            divisor = 1.0 / math.log(
                transmittance_1mm / 100.0
            )  # log-base doesn't matter here. Precompute this value for faster computation of each pixel.
            min_luminance = (transmittance_1mm / 100.0)**(peak_height -
                                                          base_height)
            for (y, x) in numpy.ndindex(height_data.shape):
                mapped_luminance = min_luminance + (
                    1.0 - min_luminance) * height_data[y, x]
                height_data[y, x] = base_height + divisor * math.log(
                    mapped_luminance
                )  # use same base as a couple lines above this
        else:
            height_data *= scale_vector.y
            height_data += base_height

        if img.hasAlphaChannel():
            for x in range(0, width):
                for y in range(0, height):
                    height_data[y, x] *= qAlpha(img.pixel(x, y)) / 255.0

        heightmap_face_count = 2 * height_minus_one * width_minus_one
        total_face_count = heightmap_face_count + (width_minus_one * 2) * (
            height_minus_one * 2) + 2

        mesh.reserveFaceCount(total_face_count)

        # initialize to texel space vertex offsets.
        # 6 is for 6 vertices for each texel quad.
        heightmap_vertices = numpy.zeros(
            (width_minus_one * height_minus_one, 6, 3), dtype=numpy.float32)
        heightmap_vertices = heightmap_vertices + numpy.array(
            [[[0, base_height, 0], [0, base_height, texel_height],
              [texel_width, base_height, texel_height],
              [texel_width, base_height, texel_height],
              [texel_width, base_height, 0], [0, base_height, 0]]],
            dtype=numpy.float32)

        offsetsz, offsetsx = numpy.mgrid[0:height_minus_one, 0:width - 1]
        offsetsx = numpy.array(offsetsx, numpy.float32).reshape(
            -1, 1) * texel_width
        offsetsz = numpy.array(offsetsz, numpy.float32).reshape(
            -1, 1) * texel_height

        # offsets for each texel quad
        heightmap_vertex_offsets = numpy.concatenate([
            offsetsx,
            numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]),
                        dtype=numpy.float32), offsetsz
        ], 1)
        heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(
            -1, 6, 3)

        # apply height data to y values
        heightmap_vertices[:, 0,
                           1] = heightmap_vertices[:, 5,
                                                   1] = height_data[:-1, :
                                                                    -1].reshape(
                                                                        -1)
        heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1)
        heightmap_vertices[:, 2,
                           1] = heightmap_vertices[:, 3, 1] = height_data[
                               1:, 1:].reshape(-1)
        heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)

        heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count *
                                                    3],
                                        dtype=numpy.int32).reshape(-1, 3)

        mesh._vertices[0:(heightmap_vertices.size //
                          3), :] = heightmap_vertices.reshape(-1, 3)
        mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices

        mesh._vertex_count = heightmap_vertices.size // 3
        mesh._face_count = heightmap_indices.size // 3

        geo_width = width_minus_one * texel_width
        geo_height = height_minus_one * texel_height

        # bottom
        mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0,
                             geo_height)
        mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0,
                             0)

        # north and south walls
        for n in range(0, width_minus_one):
            x = n * texel_width
            nx = (n + 1) * texel_width

            hn0 = height_data[0, n]
            hn1 = height_data[0, n + 1]

            hs0 = height_data[height_minus_one, n]
            hs1 = height_data[height_minus_one, n + 1]

            mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0)
            mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0)

            mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1,
                                 geo_height)
            mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0,
                                 geo_height)

        # west and east walls
        for n in range(0, height_minus_one):
            y = n * texel_height
            ny = (n + 1) * texel_height

            hw0 = height_data[n, 0]
            hw1 = height_data[n + 1, 0]

            he0 = height_data[n, width_minus_one]
            he1 = height_data[n + 1, width_minus_one]

            mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny)
            mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y)

            mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width,
                                 he1, ny)
            mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y,
                                 geo_width, 0, y)

        mesh.calculateNormals(fast=True)

        scene_node.setMeshData(mesh.build())

        return scene_node
Example #39
0
    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 = SceneNode()
        # 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((10, 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]
        layer_mesh = self._layer_data_builder.build(material_color_map)
        decorator = LayerDataDecorator.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)

        Application.getInstance().getController().getScene(
        ).gcode_list = gcode_list

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

        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
Example #40
0
 def test_getSetMirror(self):
     node = SceneNode()
     node.setMirror(Vector(-1, 1, 1))
     assert node.getMirror() == Vector(-1, 1, 1)
Example #41
0
    def read(self, file_name):
        result = SceneNode()
        # The base object of 3mf is a zipped archive.
        archive = zipfile.ZipFile(file_name, "r")
        try:
            root = ET.parse(archive.open("3D/3dmodel.model"))

            # There can be multiple objects, try to load all of them.
            objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
            if len(objects) == 0:
                Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
                return None

            for entry in objects:
                mesh = MeshData()
                node = SceneNode()
                vertex_list = []
                #for vertex in entry.mesh.vertices.vertex:
                for vertex in entry.findall(".//3mf:vertex", self._namespaces):
                    vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
                    Job.yieldThread()

                triangles = entry.findall(".//3mf:triangle", self._namespaces)

                mesh.reserveFaceCount(len(triangles))

                #for triangle in object.mesh.triangles.triangle:
                for triangle in triangles:
                    v1 = int(triangle.get("v1"))
                    v2 = int(triangle.get("v2"))
                    v3 = int(triangle.get("v3"))
                    mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2])
                    Job.yieldThread()

                # Rotate the model; We use a different coordinate frame.
                rotation = Matrix()
                rotation.setByRotationAxis(-0.5 * math.pi, Vector(1,0,0))
                mesh = mesh.getTransformed(rotation)

                #TODO: We currently do not check for normals and simply recalculate them.
                mesh.calculateNormals()
                node.setMeshData(mesh)
                node.setSelectable(True)

                transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces)
                if transformation:
                    transformation = transformation[0]
                try:
                    if transformation.get("transform"):
                        splitted_transformation = transformation.get("transform").split()
                        ## Transformation is saved as:
                        ## M00 M01 M02 0.0
                        ## M10 M11 M12 0.0
                        ## M20 M21 M22 0.0
                        ## M30 M31 M32 1.0
                        ## We switch the row & cols as that is how everyone else uses matrices!
                        temp_mat = Matrix()
                        # Rotation & Scale
                        temp_mat._data[0,0] = splitted_transformation[0]
                        temp_mat._data[1,0] = splitted_transformation[1]
                        temp_mat._data[2,0] = splitted_transformation[2]
                        temp_mat._data[0,1] = splitted_transformation[3]
                        temp_mat._data[1,1] = splitted_transformation[4]
                        temp_mat._data[2,1] = splitted_transformation[5]
                        temp_mat._data[0,2] = splitted_transformation[6]
                        temp_mat._data[1,2] = splitted_transformation[7]
                        temp_mat._data[2,2] = splitted_transformation[8]

                        # Translation
                        temp_mat._data[0,3] = splitted_transformation[9]
                        temp_mat._data[1,3] = splitted_transformation[10]
                        temp_mat._data[2,3] = splitted_transformation[11]

                        node.setTransformation(temp_mat)
                except AttributeError:
                    pass # Empty list was found. Getting transformation is not possible

                result.addChild(node)

                Job.yieldThread()

            #If there is more then one object, group them.
            try:
                if len(objects) > 1:
                    group_decorator = GroupDecorator()
                    result.addDecorator(group_decorator)
            except:
                pass
        except Exception as e:
            Logger.log("e" ,"exception occured in 3mf reader: %s" , e)

        return result  
Example #42
0
 def _getNullBoundingBox():
     return AxisAlignedBox(minimum=Vector(0, 0, 0),
                           maximum=Vector(10, 10, 10))
Example #43
0
    def _createCustom(self, size, maxs, pos1 , pos2, dep, ztop):
        mesh = MeshBuilder()
        # Init point
        Pt1 = Vector(pos1.x,pos1.z,pos1.y)
        Pt2 = Vector(pos2.x,pos2.z,pos2.y)

        V_Dir = Pt2 - Pt1

        # Calcul vecteur
        s = size / 2
        sm = maxs / 2
        l_a = pos1.y 
        s_infa=math.tan(math.radians(dep))*l_a+s
        l_b = pos2.y 
        s_infb=math.tan(math.radians(dep))*l_b+s
 
        if sm>s and dep!=0:
            l_max_a=(sm-s) / math.tan(math.radians(dep))
            l_max_b=(sm-s) / math.tan(math.radians(dep))
        else :
            l_max_a=l_a
            l_max_b=l_b
 
        Vtop = Vector(0,0,ztop)
        VZ = Vector(0,0,s)
        VZa = Vector(0,0,-l_a)
        VZb = Vector(0,0,-l_b)
        
        Norm=Vector.cross(V_Dir,VZ).normalized()
        Dec = Vector(Norm.x*s,Norm.y*s,Norm.z*s)
            
        if l_max_a<l_a and l_max_b<l_b and l_max_a>0 and l_max_b>0: 
            nbv=40
            
            Deca = Vector(Norm.x*sm,Norm.y*sm,Norm.z*sm)
            Decb = Vector(Norm.x*sm,Norm.y*sm,Norm.z*sm)

            VZam = Vector(0,0,-l_max_a)
            VZbm = Vector(0,0,-l_max_b)
        
            # X Z Y
            P_1t = Vtop+Dec
            P_2t = Vtop-Dec
            P_3t = V_Dir+Vtop+Dec
            P_4t = V_Dir+Vtop-Dec
 
            P_1m = VZam+Deca
            P_2m = VZam-Deca
            P_3m = VZbm+V_Dir+Decb
            P_4m = VZbm+V_Dir-Decb
            
            P_1i = VZa+Deca
            P_2i = VZa-Deca
            P_3i = VZb+V_Dir+Decb
            P_4i = VZb+V_Dir-Decb
             
            """
            1) Top
            2) Front
            3) Left
            4) Right
            5) Back 
            6) Front inf
            7) Left inf
            8) Right inf
            9) Back inf
            10) Bottom
            """
            verts = [ # 10 faces with 4 corners each
                [P_1t.x, P_1t.z, P_1t.y], [P_2t.x, P_2t.z, P_2t.y], [P_4t.x, P_4t.z, P_4t.y], [P_3t.x, P_3t.z, P_3t.y],              
                [P_1t.x, P_1t.z, P_1t.y], [P_3t.x, P_3t.z, P_3t.y], [P_3m.x, P_3m.z, P_3m.y], [P_1m.x, P_1m.z, P_1m.y],
                [P_2t.x, P_2t.z, P_2t.y], [P_1t.x, P_1t.z, P_1t.y], [P_1m.x, P_1m.z, P_1m.y], [P_2m.x, P_2m.z, P_2m.y],
                [P_3t.x, P_3t.z, P_3t.y], [P_4t.x, P_4t.z, P_4t.y], [P_4m.x, P_4m.z, P_4m.y], [P_3m.x, P_3m.z, P_3m.y],
                [P_4t.x, P_4t.z, P_4t.y], [P_2t.x, P_2t.z, P_2t.y], [P_2m.x, P_2m.z, P_2m.y], [P_4m.x, P_4m.z, P_4m.y],
                [P_1m.x, P_1m.z, P_1m.y], [P_3m.x, P_3m.z, P_3m.y], [P_3i.x, P_3i.z, P_3i.y], [P_1i.x, P_1i.z, P_1i.y],
                [P_2m.x, P_2m.z, P_2m.y], [P_1m.x, P_1m.z, P_1m.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y],
                [P_3m.x, P_3m.z, P_3m.y], [P_4m.x, P_4m.z, P_4m.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y],
                [P_4m.x, P_4m.z, P_4m.y], [P_2m.x, P_2m.z, P_2m.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y],
                [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y]
            ]
            
        else:
            nbv=24

            Deca = Vector(Norm.x*s_infa,Norm.y*s_infa,Norm.z*s_infa)
            Decb = Vector(Norm.x*s_infb,Norm.y*s_infb,Norm.z*s_infb)

            # X Z Y
            P_1t = Vtop+Dec
            P_2t = Vtop-Dec
            P_3t = V_Dir+Vtop+Dec
            P_4t = V_Dir+Vtop-Dec
     
            P_1i = VZa+Deca
            P_2i = VZa-Deca
            P_3i = VZb+V_Dir+Decb
            P_4i = VZb+V_Dir-Decb
             
            """
            1) Top
            2) Front
            3) Left
            4) Right
            5) Back 
            6) Bottom
            """
            verts = [ # 6 faces with 4 corners each
                [P_1t.x, P_1t.z, P_1t.y], [P_2t.x, P_2t.z, P_2t.y], [P_4t.x, P_4t.z, P_4t.y], [P_3t.x, P_3t.z, P_3t.y],
                [P_1t.x, P_1t.z, P_1t.y], [P_3t.x, P_3t.z, P_3t.y], [P_3i.x, P_3i.z, P_3i.y], [P_1i.x, P_1i.z, P_1i.y],
                [P_2t.x, P_2t.z, P_2t.y], [P_1t.x, P_1t.z, P_1t.y], [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y],
                [P_3t.x, P_3t.z, P_3t.y], [P_4t.x, P_4t.z, P_4t.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y],
                [P_4t.x, P_4t.z, P_4t.y], [P_2t.x, P_2t.z, P_2t.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y],
                [P_1i.x, P_1i.z, P_1i.y], [P_2i.x, P_2i.z, P_2i.y], [P_4i.x, P_4i.z, P_4i.y], [P_3i.x, P_3i.z, P_3i.y]
            ]
        
        mesh.setVertices(numpy.asarray(verts, dtype=numpy.float32))

        indices = []
        for i in range(0, nbv, 4): # All 6 quads (12 triangles)
            indices.append([i, i+2, i+1])
            indices.append([i, i+3, i+2])
        mesh.setIndices(numpy.asarray(indices, dtype=numpy.int32))

        mesh.calculateNormals()
        return mesh
Example #44
0
class Selection:
    @classmethod
    def add(cls, object: SceneNode) -> None:
        if object not in cls.__selection:
            cls.__selection.append(object)
            object.transformationChanged.connect(cls._onTransformationChanged)
            cls._onTransformationChanged(object)
            cls.selectionChanged.emit()

    @classmethod
    def remove(cls, object: SceneNode) -> None:
        if object in cls.__selection:
            cls.__selection.remove(object)
            object.transformationChanged.disconnect(
                cls._onTransformationChanged)
            cls._onTransformationChanged(object)
            cls.selectionChanged.emit()

    @classmethod
    ##  Get number of selected objects
    def getCount(cls) -> int:
        return len(cls.__selection)

    @classmethod
    def getAllSelectedObjects(cls) -> List[SceneNode]:
        return cls.__selection

    @classmethod
    def getBoundingBox(cls) -> AxisAlignedBox:
        bounding_box = None  # don't start with an empty bounding box, because that includes (0,0,0)
        for node in cls.__selection:
            if not isinstance(node, SceneNode):
                continue

            if not bounding_box:
                bounding_box = node.getBoundingBox()
            else:
                bounding_box = bounding_box + node.getBoundingBox()

        if not bounding_box:
            bounding_box = AxisAlignedBox.Null

        return bounding_box

    @classmethod
    ##  Get selected object by index
    #   \param index index of the object to return
    #   \returns selected object or None if index was incorrect / not found
    def getSelectedObject(cls, index: int) -> Optional[SceneNode]:
        try:
            return cls.__selection[index]
        except IndexError:
            return None

    @classmethod
    def isSelected(cls, object: SceneNode) -> bool:
        return object in cls.__selection

    @classmethod
    def clear(cls):
        cls.__selection.clear()
        cls.selectionChanged.emit()

    @classmethod
    ##  Check if anything is selected at all.
    def hasSelection(cls) -> bool:
        return bool(cls.__selection)

    selectionChanged = Signal()

    selectionCenterChanged = Signal()

    @classmethod
    def getSelectionCenter(cls) -> Vector:
        if not cls.__selection:
            cls.__selection_center = Vector.Null

        return cls.__selection_center

    ##  Apply an operation to the entire selection
    #
    #   This will create and push an operation onto the operation stack. Dependent
    #   on whether there is one item selected or multiple it will be just the
    #   operation or a grouped operation containing the operation for each selected
    #   node.
    #
    #   \param operation \type{Class} The operation to create and push. It should take a SceneNode as first positional parameter.
    #   \param args The additional positional arguments passed along to the operation constructor.
    #   \param kwargs The additional keyword arguments that will be passed along to the operation constructor.
    #
    #   \return list of instantiated operations
    @classmethod
    def applyOperation(cls, operation, *args, **kwargs):
        if not cls.__selection:
            return

        operations = []

        if len(cls.__selection) == 1:
            node = cls.__selection[0]
            op = operation(node, *args, **kwargs)
            operations.append(op)
        else:
            op = GroupedOperation()

            for node in Selection.getAllSelectedObjects():
                sub_op = operation(node, *args, **kwargs)
                op.addOperation(sub_op)
                operations.append(sub_op)

        op.push()
        return operations

    @classmethod
    def _onTransformationChanged(cls, node):
        cls.__selection_center = cls.getBoundingBox().center
        cls.selectionCenterChanged.emit()

    __selection = []  # type: List[SceneNode]
    __selection_center = Vector(0, 0, 0)
Example #45
0
    def event(self, event):
        super().event(event)

        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:
            if event.key == KeyEvent.ShiftKey:
                self._lock_steps = False
                self.propertyChanged.emit()
            elif event.key == KeyEvent.ControlKey:
                self._non_uniform_scale = True
                self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent:
            if event.key == KeyEvent.ShiftKey:
                self._lock_steps = True
                self.propertyChanged.emit()
            elif event.key == KeyEvent.ControlKey:
                self._non_uniform_scale = False
                self.propertyChanged.emit()

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

            id = self._renderer.getIdAtCoordinate(event.x, event.y)
            if not id:
                return False

            if ToolHandle.isAxis(id):
                self.setLockedAxis(id)

            handle_position = self._handle.getWorldPosition()

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

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

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

            handle_position = self._handle.getWorldPosition()
            drag_position = self.getDragPosition(event.x, event.y)
            if drag_position:
                drag_length = (drag_position - handle_position).length()
                if self._drag_length > 0:
                    drag_change = (drag_length - self._drag_length) / 100

                    if self._snap_scale and abs(drag_change) < self._snap_amount:
                        return False

                    scale = Vector(1.0, 1.0, 1.0)
                    if self._non_uniform_scale:
                        if self.getLockedAxis() == ToolHandle.XAxis:
                            scale.setX(1.0 + drag_change)
                        elif self.getLockedAxis() == ToolHandle.YAxis:
                            scale.setY(1.0 + drag_change)
                        elif self.getLockedAxis() == ToolHandle.ZAxis:
                            scale.setZ(1.0 + drag_change)

                    if scale == Vector(1.0, 1.0, 1.0):
                        scale.setX(1.0 + drag_change)
                        scale.setY(1.0 + drag_change)
                        scale.setZ(1.0 + drag_change)

                    Selection.applyOperation(ScaleOperation, scale)

                self._drag_length = (handle_position - drag_position).length()
                return True

        if event.type == Event.MouseReleaseEvent:
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(None)
                self._drag_length = 0
                self.operationStopped.emit(self)
                return True
Example #46
0
 def homeCamera(self) -> None:
     scene = Application.getInstance().getController().getScene()
     camera = scene.getActiveCamera()
     camera.setPosition(Vector(-80, 250, 700))
     camera.setPerspective(True)
     camera.lookAt(Vector(0, 0, 0))
Example #47
0
    def render(self):
        if not self._layer_shader:
            if self._compatibility_mode:
                shader_filename = "layers.shader"
                shadow_shader_filename = "layers_shadow.shader"
            else:
                shader_filename = "layers3d.shader"
                shadow_shader_filename = "layers3d_shadow.shader"
            self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
            self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
            self._current_shader = self._layer_shader
        # Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
        self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
        if self._layer_view:
            self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
            self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
            self._layer_shader.setUniformValue("u_max_thickness", self._layer_view.getMaxThickness())
            self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
            self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
            self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
            self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
            self._layer_shader.setUniformValue("u_show_helpers", self._layer_view.getShowHelpers())
            self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
            self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
        else:
            #defaults
            self._layer_shader.setUniformValue("u_max_feedrate", 1)
            self._layer_shader.setUniformValue("u_min_feedrate", 0)
            self._layer_shader.setUniformValue("u_max_thickness", 1)
            self._layer_shader.setUniformValue("u_min_thickness", 0)
            self._layer_shader.setUniformValue("u_layer_view_type", 1)
            self._layer_shader.setUniformValue("u_extruder_opacity", [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
            self._layer_shader.setUniformValue("u_show_travel_moves", 0)
            self._layer_shader.setUniformValue("u_show_helpers", 1)
            self._layer_shader.setUniformValue("u_show_skin", 1)
            self._layer_shader.setUniformValue("u_show_infill", 1)

        if not self._tool_handle_shader:
            self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))

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

        if not self._disabled_shader:
            self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
            self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*Application.getInstance().getTheme().getColor("model_unslicable").getRgb()))
            self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*Application.getInstance().getTheme().getColor("model_unslicable_alt").getRgb()))
            self._disabled_shader.setUniformValue("u_width", 50.0)
            self._disabled_shader.setUniformValue("u_opacity", 0.6)

        self.bind()

        tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
        disabled_batch = RenderBatch(self._disabled_shader)
        head_position = None  # Indicates the current position of the print head
        nozzle_node = None

        for node in DepthFirstIterator(self._scene.getRoot()):

            if isinstance(node, ToolHandle):
                tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())

            elif isinstance(node, NozzleNode):
                nozzle_node = node
                nozzle_node.setVisible(False)  # Don't set to true, we render it separately!

            elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"):
                disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData())

            elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
                layer_data = node.callDecoration("getLayerData")
                if not layer_data:
                    continue

                # Render all layers below a certain number as line mesh instead of vertices.
                if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
                    start = 0
                    end = 0
                    element_counts = layer_data.getElementCounts()
                    for layer in sorted(element_counts.keys()):
                        # In the current layer, we show just the indicated paths
                        if layer == self._layer_view._current_layer_num:
                            # We look for the position of the head, searching the point of the current path
                            index = self._layer_view._current_path_num
                            offset = 0
                            for polygon in layer_data.getLayer(layer).polygons:
                                # The size indicates all values in the two-dimension array, and the second dimension is
                                # always size 3 because we have 3D points.
                                if index >= polygon.data.size // 3 - offset:
                                    index -= polygon.data.size // 3 - offset
                                    offset = 1  # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
                                    continue
                                # The head position is calculated and translated
                                head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
                                break
                            break
                        if self._layer_view._minimum_layer_num > layer:
                            start += element_counts[layer]
                        end += element_counts[layer]

                    # Calculate the range of paths in the last layer
                    current_layer_start = end
                    current_layer_end = end + self._layer_view._current_path_num * 2 # Because each point is used twice

                    # This uses glDrawRangeElements internally to only draw a certain range of lines.
                    # All the layers but the current selected layer are rendered first
                    if self._old_current_path != self._layer_view._current_path_num:
                        self._current_shader = self._layer_shadow_shader
                        self._switching_layers = False
                    if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num:
                        self._current_shader = self._layer_shader
                        self._switching_layers = True

                    layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end), backface_cull = True)
                    layers_batch.addItem(node.getWorldTransformation(), layer_data)
                    layers_batch.render(self._scene.getActiveCamera())

                    # Current selected layer is rendered
                    current_layer_batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (current_layer_start, current_layer_end))
                    current_layer_batch.addItem(node.getWorldTransformation(), layer_data)
                    current_layer_batch.render(self._scene.getActiveCamera())

                    self._old_current_layer = self._layer_view._current_layer_num
                    self._old_current_path = self._layer_view._current_path_num

                # Create a new batch that is not range-limited
                batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)

                if self._layer_view.getCurrentLayerMesh():
                    batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerMesh())

                if self._layer_view.getCurrentLayerJumps():
                    batch.addItem(node.getWorldTransformation(), self._layer_view.getCurrentLayerJumps())

                if len(batch.items) > 0:
                    batch.render(self._scene.getActiveCamera())

        # The nozzle is drawn when once we know the correct position of the head,
        # but the user is not using the layer slider, and the compatibility mode is not enabled
        if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
            if head_position is not None:
                nozzle_node.setPosition(head_position)
                nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
                nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
                nozzle_batch.render(self._scene.getActiveCamera())

        if len(disabled_batch.items) > 0:
            disabled_batch.render(self._scene.getActiveCamera())

        # Render toolhandles on top of the layerview
        if len(tool_handle_batch.items) > 0:
            tool_handle_batch.render(self._scene.getActiveCamera())

        self.release()
Example #48
0
    def run(self):
        self.showSplashMessage(
            self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))

        controller = self.getController()

        controller.setActiveView("SolidView")
        controller.setCameraTool("CameraTool")
        controller.setSelectionTool("SelectionTool")

        t = controller.getTool("TranslateTool")
        if t:
            t.setEnabledAxis(
                [ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])

        Selection.selectionChanged.connect(self.onSelectionChanged)

        root = controller.getScene().getRoot()

        # The platform is a child of BuildVolume
        self._volume = BuildVolume.BuildVolume(root)

        self.getRenderer().setBackgroundColor(QColor(245, 245, 245))

        self._physics = PlatformPhysics.PlatformPhysics(
            controller, self._volume)

        camera = Camera("3d", root)
        camera.setPosition(Vector(-80, 250, 700))
        camera.setPerspective(True)
        camera.lookAt(Vector(0, 0, 0))
        controller.getScene().setActiveCamera("3d")

        self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))

        self._camera_animation = CameraAnimation.CameraAnimation()
        self._camera_animation.setCameraTool(
            self.getController().getTool("CameraTool"))

        self.showSplashMessage(
            self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))

        # Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
        cura.Settings.ExtruderManager.getInstance()
        qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0,
                                 "MachineManager", self.getMachineManager)

        qmlRegisterSingletonType(MachineActionManager.MachineActionManager,
                                 "Cura", 1, 0, "MachineActionManager",
                                 self.getMachineActionManager)
        self.setMainQml(
            Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
        self._qml_import_paths.append(
            Resources.getPath(self.ResourceTypes.QmlFiles))
        self.initializeEngine()

        if self._engine.rootObjects:
            self.closeSplash()

            for file in self.getCommandLineOption("file", []):
                self._openFile(file)
            for file_name in self._open_file_queue:  #Open all the files that were queued up while plug-ins were loading.
                self._openFile(file_name)

            self._started = True

            self.exec_()
Example #49
0
    def _onChangeTimerFinished(self):
        if not self._enabled:
            return

        root = self._controller.getScene().getRoot()

        # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
        # same direction.
        transformed_nodes = []

        # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
        # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
        nodes = list(BreadthFirstIterator(root))

        # Only check nodes inside build area.
        nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]

        random.shuffle(nodes)
        for node in nodes:
            if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
                continue

            bbox = node.getBoundingBox()

            # Move it downwards if bottom is above platform
            move_vector = Vector()

            if Application.getInstance().getPreferences().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
                z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
                move_vector = move_vector.set(y = -bbox.bottom + z_offset)

            # If there is no convex hull for the node, start calculating it and continue.
            if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh"):
                node.addDecorator(ConvexHullDecorator())

            # only push away objects if this node is a printing mesh
            if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"):
                # Do not move locked nodes
                if node.getSetting(SceneNodeSettings.LockPosition):
                    continue

                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"):
                        continue
                    
                    # Ignore collisions of a group with it's own children
                    if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                        continue
                    
                    # Ignore collisions within a group
                    if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
                        continue
                    
                    # Ignore nodes that do not have the right properties set.
                    if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                        continue

                    if other_node in transformed_nodes:
                        continue  # Other node is already moving, wait for next pass.

                    if other_node.callDecoration("isNonPrintingMesh"):
                        continue

                    overlap = (0, 0)  # Start loop with no overlap
                    current_overlap_checks = 0
                    # Continue to check the overlap until we no longer find one.
                    while overlap and current_overlap_checks < self._max_overlap_checks:
                        current_overlap_checks += 1
                        head_hull = node.callDecoration("getConvexHullHead")
                        if head_hull:  # One at a time intersection.
                            overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
                            if not overlap:
                                other_head_hull = other_node.callDecoration("getConvexHullHead")
                                if other_head_hull:
                                    overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
                                    if overlap:
                                        # Moving ensured that overlap was still there. Try anew!
                                        move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
                                                                      z = move_vector.z + overlap[1] * self._move_factor)
                            else:
                                # Moving ensured that overlap was still there. Try anew!
                                move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
                                                              z = move_vector.z + overlap[1] * self._move_factor)
                        else:
                            own_convex_hull = node.callDecoration("getConvexHull")
                            other_convex_hull = other_node.callDecoration("getConvexHull")
                            if own_convex_hull and other_convex_hull:
                                overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
                                if overlap:  # Moving ensured that overlap was still there. Try anew!
                                    temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
                                                                       z = move_vector.z + overlap[1] * self._move_factor)

                                    # if the distance between two models less than 2mm then try to find a new factor
                                    if abs(temp_move_vector.x - overlap[0]) < self._minimum_gap and abs(temp_move_vector.y - overlap[1]) < self._minimum_gap:
                                        temp_x_factor = (abs(overlap[0]) + self._minimum_gap) / overlap[0] if overlap[0] != 0 else 0 # find x move_factor, like (3.4 + 2) / 3.4 = 1.58
                                        temp_y_factor = (abs(overlap[1]) + self._minimum_gap) / overlap[1] if overlap[1] != 0 else 0 # find y move_factor

                                        temp_scale_factor = temp_x_factor if abs(temp_x_factor) > abs(temp_y_factor) else temp_y_factor

                                        move_vector = move_vector.set(x = move_vector.x + overlap[0] * temp_scale_factor,
                                                                      z = move_vector.z + overlap[1] * temp_scale_factor)
                                    else:
                                        move_vector = temp_move_vector
                            else:
                                # This can happen in some cases if the object is not yet done with being loaded.
                                # Simply waiting for the next tick seems to resolve this correctly.
                                overlap = None

            if not Vector.Null.equals(move_vector, epsilon = 1e-5):
                transformed_nodes.append(node)
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()

        # After moving, we have to evaluate the boundary checks for nodes
        build_volume = Application.getInstance().getBuildVolume()
        build_volume.updateNodeBoundaryCheck()
Example #50
0
    def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert):
        scene_node = SceneNode()

        mesh = MeshBuilder()

        img = QImage(file_name)

        if img.isNull():
            Logger.log("e", "Image is corrupt.")
            return None

        width = max(img.width(), 2)
        height = max(img.height(), 2)
        aspect = height / width

        if img.width() < 2 or img.height() < 2:
            img = img.scaled(width, height, Qt.IgnoreAspectRatio)

        base_height = max(base_height, 0)
        peak_height = max(peak_height, -base_height)

        xz_size = max(xz_size, 1)
        scale_vector = Vector(xz_size, peak_height, xz_size)

        if width > height:
            scale_vector = scale_vector.set(z=scale_vector.z * aspect)
        elif height > width:
            scale_vector = scale_vector.set(x=scale_vector.x / aspect)

        if width > max_size or height > max_size:
            scale_factor = max_size / width
            if height > width:
                scale_factor = max_size / height

            width = int(max(round(width * scale_factor), 2))
            height = int(max(round(height * scale_factor), 2))
            img = img.scaled(width, height, Qt.IgnoreAspectRatio)

        width_minus_one = width - 1
        height_minus_one = height - 1

        Job.yieldThread()

        texel_width = 1.0 / (width_minus_one) * scale_vector.x
        texel_height = 1.0 / (height_minus_one) * scale_vector.z

        height_data = numpy.zeros((height, width), dtype=numpy.float32)

        for x in range(0, width):
            for y in range(0, height):
                qrgb = img.pixel(x, y)
                avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255)
                height_data[y, x] = avg

        Job.yieldThread()

        if image_color_invert:
            height_data = 1 - height_data

        for _ in range(0, blur_iterations):
            copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge")

            height_data += copy[1:-1, 2:]
            height_data += copy[1:-1, :-2]
            height_data += copy[2:, 1:-1]
            height_data += copy[:-2, 1:-1]

            height_data += copy[2:, 2:]
            height_data += copy[:-2, 2:]
            height_data += copy[2:, :-2]
            height_data += copy[:-2, :-2]

            height_data /= 9

            Job.yieldThread()

        height_data *= scale_vector.y
        height_data += base_height

        heightmap_face_count = 2 * height_minus_one * width_minus_one
        total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2

        mesh.reserveFaceCount(total_face_count)

        # initialize to texel space vertex offsets.
        # 6 is for 6 vertices for each texel quad.
        heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype = numpy.float32)
        heightmap_vertices = heightmap_vertices + numpy.array([[
            [0, base_height, 0],
            [0, base_height, texel_height],
            [texel_width, base_height, texel_height],
            [texel_width, base_height, texel_height],
            [texel_width, base_height, 0],
            [0, base_height, 0]
        ]], dtype = numpy.float32)

        offsetsz, offsetsx = numpy.mgrid[0: height_minus_one, 0: width - 1]
        offsetsx = numpy.array(offsetsx, numpy.float32).reshape(-1, 1) * texel_width
        offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height

        # offsets for each texel quad
        heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1)
        heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)

        # apply height data to y values
        heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, :-1].reshape(-1)
        heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1)
        heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
        heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)

        heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3)

        mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
        mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices

        mesh._vertex_count = heightmap_vertices.size // 3
        mesh._face_count = heightmap_indices.size // 3

        geo_width = width_minus_one * texel_width
        geo_height = height_minus_one * texel_height

        # bottom
        mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height)
        mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0)

        # north and south walls
        for n in range(0, width_minus_one):
            x = n * texel_width
            nx = (n + 1) * texel_width

            hn0 = height_data[0, n]
            hn1 = height_data[0, n + 1]

            hs0 = height_data[height_minus_one, n]
            hs1 = height_data[height_minus_one, n + 1]

            mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0)
            mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0)

            mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height)
            mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height)

        # west and east walls
        for n in range(0, height_minus_one):
            y = n * texel_height
            ny = (n + 1) * texel_height

            hw0 = height_data[n, 0]
            hw1 = height_data[n + 1, 0]

            he0 = height_data[n, width_minus_one]
            he1 = height_data[n + 1, width_minus_one]

            mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny)
            mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y)

            mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
            mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)

        mesh.calculateNormals(fast=True)

        scene_node.setMeshData(mesh.build())

        return scene_node
Example #51
0
    def event(self, event):
        super().event(event)

        if event.type == Event.ToolActivateEvent:
            self._old_scale = Selection.getSelectedObject(0).getScale()
            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)

        # Handle modifier keys: Shift toggles snap, Control toggles uniform scaling
        if event.type == Event.KeyPressEvent:
            if event.key == KeyEvent.ShiftKey:
                self._snap_scale = False
                self.propertyChanged.emit()
            elif event.key == KeyEvent.ControlKey:
                self._non_uniform_scale = True
                self.propertyChanged.emit()

        if event.type == Event.KeyReleaseEvent:
            if event.key == KeyEvent.ShiftKey:
                self._snap_scale = True
                self.propertyChanged.emit()
            elif event.key == KeyEvent.ControlKey:
                self._non_uniform_scale = False
                self.propertyChanged.emit()

        if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
            # Initialise a scale 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 ToolHandle.isAxis(id):
                self.setLockedAxis(id)

            # Save the current positions of the node, as we want to scale arround their current centres
            self._saved_node_positions = []
            for node in Selection.getAllSelectedObjects():
                self._saved_node_positions.append((node, node.getWorldPosition()))

            self._saved_handle_position = self._handle.getWorldPosition()

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

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

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

            drag_position = self.getDragPosition(event.x, event.y)
            if drag_position:
                drag_length = (drag_position - self._saved_handle_position).length()
                if self._drag_length > 0:
                    drag_change = (drag_length - self._drag_length) / 100 * self._scale_speed
                    if self._snap_scale:
                        scale_factor = round(drag_change, 1)
                    else:
                        scale_factor = drag_change
                    if scale_factor:
                        scale_change = Vector(0.0, 0.0, 0.0)
                        if self._non_uniform_scale:
                            if self.getLockedAxis() == ToolHandle.XAxis:
                                scale_change = scale_change.set(x=scale_factor)
                            elif self.getLockedAxis() == ToolHandle.YAxis:
                                scale_change = scale_change.set(y=scale_factor)
                            elif self.getLockedAxis() == ToolHandle.ZAxis:
                                scale_change = scale_change.set(z=scale_factor)
                        else:
                            scale_change = Vector(x=scale_factor, y=scale_factor, z=scale_factor)

                        # Scale around the saved centeres of all selected nodes
                        op = GroupedOperation()
                        for node, position in self._saved_node_positions:
                            op.addOperation(ScaleOperation(node, scale_change, relative_scale = True, scale_around_point = position))
                        op.push()
                        self._drag_length = (self._saved_handle_position - drag_position).length()
                else:
                    self._drag_length = (self._saved_handle_position - drag_position).length() #First move, do nothing but set right length.
                return True

        if event.type == Event.MouseReleaseEvent:
            # Finish a scale operation
            if self.getDragPlane():
                self.setDragPlane(None)
                self.setLockedAxis(None)
                self._drag_length = 0
                self.operationStopped.emit(self)
                return True
Example #52
0
    def _onChangeTimerFinished(self):
        if not self._enabled:
            return

        root = self._controller.getScene().getRoot()
        for node in BreadthFirstIterator(root):
            if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
                continue

            bbox = node.getBoundingBox()

            # Ignore intersections with the bottom
            build_volume_bounding_box = self._build_volume.getBoundingBox().set(bottom=-9001)
            node._outside_buildarea = False

            # Mark the node as outside the build volume if the bounding box test fails.
            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
                node._outside_buildarea = True

            # Move it downwards if bottom is above platform
            move_vector = Vector()
            if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
                z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
                if bbox.bottom > 0:
                    move_vector = move_vector.set(y=-bbox.bottom + z_offset)
                elif bbox.bottom < z_offset:
                    move_vector = move_vector.set(y=(-bbox.bottom) - z_offset)

            # If there is no convex hull for the node, start calculating it and continue.
            if not node.getDecorator(ConvexHullDecorator):
                node.addDecorator(ConvexHullDecorator())
            node.callDecoration("recomputeConvexHull")

            if Preferences.getInstance().getValue("physics/automatic_push_free"):
                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or type(other_node) is not SceneNode or other_node is node:
                        continue
                    
                    # Ignore collisions of a group with it's own children
                    if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                        continue
                    
                    # Ignore collisions within a group
                    if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
                        continue
                    
                    # Ignore nodes that do not have the right properties set.
                    if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                        continue

                    # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                    head_hull = node.callDecoration("getConvexHullHead")
                    if head_hull:
                        overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull"))
                        if not overlap:
                            other_head_hull = other_node.callDecoration("getConvexHullHead")
                            if other_head_hull:
                                overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull)
                    else:
                        own_convex_hull = node.callDecoration("getConvexHull")
                        other_convex_hull = other_node.callDecoration("getConvexHull")
                        if own_convex_hull and other_convex_hull:
                            overlap = own_convex_hull.intersectsPolygon(other_convex_hull)
                        else:
                            # This can happen in some cases if the object is not yet done with being loaded.
                            #  Simply waiting for the next tick seems to resolve this correctly.
                            overlap = None

                    if overlap is None:
                        continue
                    move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1)
            convex_hull = node.callDecoration("getConvexHull")
            if convex_hull:
                if not convex_hull.isValid():
                    return
                # Check for collisions between disallowed areas and the object
                for area in self._build_volume.getDisallowedAreas():
                    overlap = convex_hull.intersectsPolygon(area)
                    if overlap is None:
                        continue

                    node._outside_buildarea = True

            if not Vector.Null.equals(move_vector, epsilon=1e-5):
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()
Example #53
0
    def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
        self._archive = None # Reset archive
        archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
        try:
            model_file = zipfile.ZipInfo("3D/3dmodel.model")
            # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
            model_file.compress_type = zipfile.ZIP_DEFLATED

            # Create content types file
            content_types_file = zipfile.ZipInfo("[Content_Types].xml")
            content_types_file.compress_type = zipfile.ZIP_DEFLATED
            content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
            rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
            model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")

            # Create _rels/.rels file
            relations_file = zipfile.ZipInfo("_rels/.rels")
            relations_file.compress_type = zipfile.ZIP_DEFLATED
            relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
            model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")

            # Attempt to add a thumbnail
            snapshot = self._createSnapshot()
            if snapshot:
                thumbnail_buffer = QBuffer()
                thumbnail_buffer.open(QBuffer.ReadWrite)
                snapshot.save(thumbnail_buffer, "PNG")

                thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
                # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
                archive.writestr(thumbnail_file, thumbnail_buffer.data())

                # Add PNG to content types file
                thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
                # Add thumbnail relation to _rels/.rels file
                thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")

            savitar_scene = Savitar.Scene()

            metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()

            for key, value in metadata_to_store.items():
                savitar_scene.setMetaDataEntry(key, value)

            current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            if "Application" not in metadata_to_store:
                # This might sound a bit strange, but this field should store the original application that created
                # the 3mf. So if it was already set, leave it to whatever it was.
                savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
            if "CreationDate" not in metadata_to_store:
                savitar_scene.setMetaDataEntry("CreationDate", current_time_string)

            savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)

            transformation_matrix = Matrix()
            transformation_matrix._data[1, 1] = 0
            transformation_matrix._data[1, 2] = -1
            transformation_matrix._data[2, 1] = 1
            transformation_matrix._data[2, 2] = 0

            global_container_stack = Application.getInstance().getGlobalContainerStack()
            # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
            # build volume.
            if global_container_stack:
                translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
                                            y=global_container_stack.getProperty("machine_depth", "value") / 2,
                                            z=0)
                translation_matrix = Matrix()
                translation_matrix.setByTranslation(translation_vector)
                transformation_matrix.preMultiply(translation_matrix)

            root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
            for node in nodes:
                if node == root_node:
                    for root_child in node.getChildren():
                        savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
                        if savitar_node:
                            savitar_scene.addSceneNode(savitar_node)
                else:
                    savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
                    if savitar_node:
                        savitar_scene.addSceneNode(savitar_node)

            parser = Savitar.ThreeMFParser()
            scene_string = parser.sceneToString(savitar_scene)

            archive.writestr(model_file, scene_string)
            archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
            archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
        except Exception as e:
            Logger.logException("e", "Error writing zip file")
            self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file."))
            return False
        finally:
            if not self._store_archive:
                archive.close()
            else:
                self._archive = archive

        return True
Example #54
0
 def setByTranslation(self, direction: Vector):
     M = numpy.identity(4, dtype=numpy.float64)
     M[:3, 3] = direction.getData()[:3]
     self._data = M
Example #55
0
class SceneNode(SignalEmitter):
    class TransformSpace:
        Local = 1
        Parent = 2
        World = 3

    def __init__(self, parent = None, name = ""):
        super().__init__() # Call super to make multiple inheritence work.

        self._children = []
        self._mesh_data = None

        self._position = Vector()
        self._scale = Vector(1.0, 1.0, 1.0)
        self._mirror = Vector(1.0, 1.0, 1.0)
        self._orientation = Quaternion()

        self._transformation = None
        self._world_transformation = None

        self._derived_position = None
        self._derived_orientation = None
        self._derived_scale = None

        self._inherit_orientation = True
        self._inherit_scale = True

        self._parent = parent
        self._enabled = True
        self._selectable = False
        self._calculate_aabb = True
        self._aabb = None
        self._aabb_job = None
        self._visible = True
        self._name = name
        self._decorators = []
        self._bounding_box_mesh = None
        self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh)
        self.parentChanged.connect(self._onParentChanged)

        if parent:
            parent.addChild(self)

    def __deepcopy__(self, memo):
        copy = SceneNode()
        copy.translate(self.getPosition())
        copy.setOrientation(self.getOrientation())
        copy.setScale(self.getScale())
        copy.setMeshData(deepcopy(self._mesh_data, memo))
        copy.setVisible(deepcopy(self._visible, memo))
        copy._selectable = deepcopy(self._selectable, memo)
        for decorator in self._decorators:
            copy.addDecorator(deepcopy(decorator, memo))

        for child in self._children:
            copy.addChild(deepcopy(child, memo))
        self.calculateBoundingBoxMesh()
        return copy


    def setCenterPosition(self, center):
        if self._mesh_data:
            m = Matrix()
            m.setByTranslation(-center)
            self._mesh_data = self._mesh_data.getTransformed(m)
            self._mesh_data.setCenterPosition(center)
        for child in self._children:
            child.setCenterPosition(center)

    ##  \brief Get the parent of this node. If the node has no parent, it is the root node.
    #   \returns SceneNode if it has a parent and None if it's the root node.
    def getParent(self):
        return self._parent

    def getBoundingBoxMesh(self):
        return self._bounding_box_mesh

    def calculateBoundingBoxMesh(self):
        if self._aabb:
            self._bounding_box_mesh = MeshData()
            rtf = self._aabb.maximum
            lbb = self._aabb.minimum

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back

            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front
            self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front
            self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back

            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front
            self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back

            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front
            self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back
        else:
            self._resetAABB()

    def _onParentChanged(self, node):
        for child in self.getChildren():
            child.parentChanged.emit(self)

    decoratorsChanged = Signal()
    def addDecorator(self, decorator):
        decorator.setNode(self)
        self._decorators.append(decorator)
        self.decoratorsChanged.emit(self)

    def getDecorators(self):
        return self._decorators

    def getDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                return decorator

    def removeDecorators(self):
        self._decorators = []
        self.decoratorsChanged.emit(self)

    def removeDecorator(self, dec_type):
        for decorator in self._decorators:
            if type(decorator) == dec_type:
                self._decorators.remove(decorator)
                self.decoratorsChanged.emit(self)
                break

    def callDecoration(self, function, *args, **kwargs):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                try:
                    return getattr(decorator, function)(*args, **kwargs)
                except Exception as e:
                    Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e))
                    return None

    def hasDecoration(self, function):
        for decorator in self._decorators:
            if hasattr(decorator, function):
                return True
        return False

    def getName(self):
        return self._name

    def setName(self, name):
        self._name = name

    ##  How many nodes is this node removed from the root
    def getDepth(self):
        if self._parent is None:
            return 0
        return self._parent.getDepth() + 1

    ##  \brief Set the parent of this object
    #   \param scene_node SceneNode that is the parent of this object.
    def setParent(self, scene_node):
        if self._parent:
            self._parent.removeChild(self)
        #self._parent = scene_node

        if scene_node:
            scene_node.addChild(self)

    ##  Emitted whenever the parent changes.
    parentChanged = Signal()

    ##  \brief Get the visibility of this node. The parents visibility overrides the visibility.
    #   TODO: Let renderer actually use the visibility to decide wether to render or not.
    def isVisible(self):
        if self._parent != None and self._visible:
            return self._parent.isVisible()
        else:
            return self._visible

    def setVisible(self, visible):
        self._visible = visible

    ##  \brief Get the (original) mesh data from the scene node/object.
    #   \returns MeshData
    def getMeshData(self):
        return self._mesh_data

    ##  \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root.
    #   \returns MeshData
    def getMeshDataTransformed(self):
        #transformed_mesh = deepcopy(self._mesh_data)
        #transformed_mesh.transform(self.getWorldTransformation())
        return self._mesh_data.getTransformed(self.getWorldTransformation())

    ##  \brief Set the mesh of this node/object
    #   \param mesh_data MeshData object
    def setMeshData(self, mesh_data):
        if self._mesh_data:
            self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged)
        self._mesh_data = mesh_data
        if self._mesh_data is not None:
            self._mesh_data.dataChanged.connect(self._onMeshDataChanged)
        self._resetAABB()
        self.meshDataChanged.emit(self)

    ##  Emitted whenever the attached mesh data object changes.
    meshDataChanged = Signal()


    def _onMeshDataChanged(self):
        self.meshDataChanged.emit(self)

    ##  \brief Add a child to this node and set it's parent as this node.
    #   \params scene_node SceneNode to add.
    def addChild(self, scene_node):
        if scene_node not in self._children:
            scene_node.transformationChanged.connect(self.transformationChanged)
            scene_node.childrenChanged.connect(self.childrenChanged)
            scene_node.meshDataChanged.connect(self.meshDataChanged)

            self._children.append(scene_node)
            self._resetAABB()
            self.childrenChanged.emit(self)

            if not scene_node._parent is self:
                scene_node._parent = self
                scene_node._transformChanged()
                scene_node.parentChanged.emit(self)

    ##  \brief remove a single child
    #   \param child Scene node that needs to be removed.
    def removeChild(self, child):
        if child not in self._children:
            return

        child.transformationChanged.disconnect(self.transformationChanged)
        child.childrenChanged.disconnect(self.childrenChanged)
        child.meshDataChanged.disconnect(self.meshDataChanged)

        self._children.remove(child)
        child._parent = None
        child._transformChanged()
        child.parentChanged.emit(self)

        self.childrenChanged.emit(self)

    ##  \brief Removes all children and its children's children.
    def removeAllChildren(self):
        for child in self._children:
            child.removeAllChildren()
            self.removeChild(child)

        self.childrenChanged.emit(self)

    ##  \brief Get the list of direct children
    #   \returns List of children
    def getChildren(self):
        return self._children

    def hasChildren(self):
        return True if self._children else False

    ##  \brief Get list of all children (including it's children children children etc.)
    #   \returns list ALl children in this 'tree'
    def getAllChildren(self):
        children = []
        children.extend(self._children)
        for child in self._children:
            children.extend(child.getAllChildren())
        return children

    ##  \brief Emitted whenever the list of children of this object or any child object changes.
    #   \param object The object that triggered the change.
    childrenChanged = Signal()

    ##  \brief Computes and returns the transformation from world to local space.
    #   \returns 4x4 transformation matrix
    def getWorldTransformation(self):
        if self._world_transformation is None:
            self._updateTransformation()

        return deepcopy(self._world_transformation)

    ##  \brief Returns the local transformation with respect to its parent. (from parent to local)
    #   \retuns transformation 4x4 (homogenous) matrix
    def getLocalTransformation(self):
        if self._transformation is None:
            self._updateTransformation()

        return deepcopy(self._transformation)

    ##  Get the local orientation value.
    def getOrientation(self):
        return deepcopy(self._orientation)


    ##  \brief Rotate the scene object (and thus its children) by given amount
    #
    #   \param rotation \type{Quaternion} A quaternion indicating the amount of rotation.
    #   \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace.
    def rotate(self, rotation, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        if transform_space == SceneNode.TransformSpace.Local:
            self._orientation = self._orientation * rotation
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._orientation = rotation * self._orientation
        elif transform_space == SceneNode.TransformSpace.World:
            self._orientation = self._orientation * self._getDerivedOrientation().getInverse() * rotation * self._getDerivedOrientation()
        else:
            raise ValueError("Unknown transform space {0}".format(transform_space))

        self._orientation.normalize()
        self._transformChanged()

    ##  Set the local orientation of this scene node.
    #
    #   \param orientation \type{Quaternion} The new orientation of this scene node.
    def setOrientation(self, orientation):
        if not self._enabled or orientation == self._orientation:
            return

        self._orientation = orientation
        self._orientation.normalize()
        self._transformChanged()

    ##  Get the local scaling value.
    def getScale(self):
        return deepcopy(self._scale)

    ##  Scale the scene object (and thus its children) by given amount
    #
    #   \param scale \type{Vector} A Vector with three scale values
    #   \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace.
    def scale(self, scale, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        if transform_space == SceneNode.TransformSpace.Local:
            self._scale = self._scale.scale(scale)
        elif transform_space == SceneNode.TransformSpace.Parent:
            raise NotImplementedError()
        elif transform_space == SceneNode.TransformSpace.World:
            if self._parent:
                scale_change = Vector(1,1,1) - scale

                if scale_change.x < 0 or scale_change.y < 0 or scale_change.z < 0:
                    direction = -1
                else:
                    direction = 1
                # Hackish way to do this, but this seems to correctly scale the object.
                change_vector = self._scale.scale(self._getDerivedOrientation().getInverse().rotate(scale_change))

                if change_vector.x < 0 and direction == 1:
                    change_vector.setX(-change_vector.x)
                if change_vector.x > 0 and direction == -1:
                    change_vector.setX(-change_vector.x)

                if change_vector.y < 0 and direction == 1:
                    change_vector.setY(-change_vector.y)
                if change_vector.y > 0 and direction == -1:
                    change_vector.setY(-change_vector.y)

                if change_vector.z < 0 and direction == 1:
                    change_vector.setZ(-change_vector.z)
                if change_vector.z > 0 and direction == -1:
                    change_vector.setZ(-change_vector.z)

                self._scale -= self._scale.scale(change_vector)
        else:
            raise ValueError("Unknown transform space {0}".format(transform_space))

        self._transformChanged()

    ##  Set the local scale value.
    #
    #   \param scale \type{Vector} The new scale value of the scene node.
    def setScale(self, scale):
        if not self._enabled or scale == self._scale:
            return

        self._scale = scale
        self._transformChanged()

    ##  Get the local mirror values.
    #
    #   \return The mirror values as vector of scaling values.
    def getMirror(self):
        return deepcopy(self._mirror)

    ##  Mirror the scene object (and thus its children) in the given directions.
    #
    #   \param mirror \type{Vector} A vector of three scale values that is used
    #   to mirror the node.
    #   \param transform_space The space relative to which to scale. Can be any
    #   one of the constants in SceneNode::TransformSpace.
    def mirror(self, mirror, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        if transform_space == SceneNode.TransformSpace.Local:
            self._mirror *= mirror
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._mirror *= mirror
        elif transform_space == SceneNode.TransformSpace.World:
            self._mirror *= mirror
        else:
            raise ValueError("Unknown transform space {0}".format(transform_space))

        self._transformChanged()

    ##  Set the local mirror values.
    #
    #   \param mirror \type{Vector} The new mirror values as scale multipliers.
    def setMirror(self, mirror):
        if not self._enabled or mirror == self._mirror:
            return

        self._mirror = mirror
        self._transformChanged()

    ##  Get the local position.
    def getPosition(self):
        return deepcopy(self._position)

    ##  Get the position of this scene node relative to the world.
    def getWorldPosition(self):
        if not self._derived_position:
            self._updateTransformation()

        return deepcopy(self._derived_position)

    ##  Translate the scene object (and thus its children) by given amount.
    #
    #   \param translation \type{Vector} The amount to translate by.
    #   \param transform_space The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace.
    def translate(self, translation, transform_space = TransformSpace.Local):
        if not self._enabled:
            return

        if transform_space == SceneNode.TransformSpace.Local:
            self._position += self._orientation.rotate(translation)
        elif transform_space == SceneNode.TransformSpace.Parent:
            self._position += translation
        elif transform_space == SceneNode.TransformSpace.World:
            if self._parent:
                self._position += (1.0 / self._parent._getDerivedScale()).scale(self._parent._getDerivedOrientation().getInverse().rotate(translation))
            else:
                self._position += translation

        self._transformChanged()

    ##  Set the local position value.
    #
    #   \param position The new position value of the SceneNode.
    def setPosition(self, position):
        if not self._enabled or position == self._position:
            return

        self._position = position
        self._transformChanged()

    ##  Signal. Emitted whenever the transformation of this object or any child object changes.
    #   \param object The object that caused the change.
    transformationChanged = Signal()

    ##  Rotate this scene node in such a way that it is looking at target.
    #
    #   \param target \type{Vector} The target to look at.
    #   \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0).
    def lookAt(self, target, up = Vector.Unit_Y):
        if not self._enabled:
            return

        eye = self.getWorldPosition()
        f = (target - eye).normalize()
        up.normalize()
        s = f.cross(up).normalize()
        u = s.cross(f).normalize()

        m = Matrix([
            [ s.x,  u.x,  -f.x, 0.0],
            [ s.y,  u.y,  -f.y, 0.0],
            [ s.z,  u.z,  -f.z, 0.0],
            [ 0.0,  0.0,  0.0,  1.0]
        ])

        if self._parent:
            self._orientation = self._parent._getDerivedOrientation() * Quaternion.fromMatrix(m)
        else:
            self._orientation = Quaternion.fromMatrix(m)
        self._transformChanged()

    ##  Can be overridden by child nodes if they need to perform special rendering.
    #   If you need to handle rendering in a special way, for example for tool handles,
    #   you can override this method and render the node. Return True to prevent the
    #   view from rendering any attached mesh data.
    #
    #   \param renderer The renderer object to use for rendering.
    #
    #   \return False if the view should render this node, True if we handle our own rendering.
    def render(self, renderer):
        return False

    ##  Get whether this SceneNode is enabled, that is, it can be modified in any way.
    def isEnabled(self):
        if self._parent != None and self._enabled:
            return self._parent.isEnabled()
        else:
            return self._enabled

    ##  Set whether this SceneNode is enabled.
    #   \param enable True if this object should be enabled, False if not.
    #   \sa isEnabled
    def setEnabled(self, enable):
        self._enabled = enable

    ##  Get whether this SceneNode can be selected.
    #
    #   \note This will return false if isEnabled() returns false.
    def isSelectable(self):
        return self._enabled and self._selectable

    ##  Set whether this SceneNode can be selected.
    #
    #   \param select True if this SceneNode should be selectable, False if not.
    def setSelectable(self, select):
        self._selectable = select

    ##  Get the bounding box of this node and its children.
    #
    #   Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB
    #   while the calculation happens.
    def getBoundingBox(self):
        if self._aabb:
            return self._aabb

        if not self._aabb_job:
            self._resetAABB()

        return AxisAlignedBox()

    ##  Set whether or not to calculate the bounding box for this node.
    #
    #   \param calculate True if the bounding box should be calculated, False if not.
    def setCalculateBoundingBox(self, calculate):
        self._calculate_aabb = calculate

    boundingBoxChanged = Signal()

    ##  private:
    def _getDerivedPosition(self):
        if not self._derived_position:
            self._updateTransformation()

        return self._derived_position

    def _getDerivedOrientation(self):
        if not self._derived_orientation:
            self._updateTransformation()

        # Sometimes the derived orientation can be None.
        # I've not been able to locate the cause of this, but this prevents it being an issue.
        if not self._derived_orientation:
            self._derived_orientation = Quaternion()
        return self._derived_orientation

    def _getDerivedScale(self):
        if not self._derived_scale:
            self._updateTransformation()

        return self._derived_scale

    def _transformChanged(self):
        self._resetAABB()
        self._transformation = None
        self._world_transformation = None
        self._derived_position = None
        self._derived_orientation = None
        self._derived_scale = None

        self.transformationChanged.emit(self)

        for child in self._children:
            child._transformChanged()

    def _updateTransformation(self):
        scale_and_mirror = self._scale * self._mirror
        self._transformation = Matrix.fromPositionOrientationScale(self._position, self._orientation, scale_and_mirror)

        if self._parent:
            parent_orientation = self._parent._getDerivedOrientation()
            if self._inherit_orientation:
                self._derived_orientation = parent_orientation * self._orientation
            else:
                self._derived_orientation = self._orientation

            # Sometimes the derived orientation can be None.
            # I've not been able to locate the cause of this, but this prevents it being an issue.
            if not self._derived_orientation:
                self._derived_orientation = Quaternion()

            parent_scale = self._parent._getDerivedScale()
            if self._inherit_scale:
                self._derived_scale = parent_scale.scale(scale_and_mirror)
            else:
                self._derived_scale = scale_and_mirror

            self._derived_position = parent_orientation.rotate(parent_scale.scale(self._position))
            self._derived_position += self._parent._getDerivedPosition()

            self._world_transformation = Matrix.fromPositionOrientationScale(self._derived_position, self._derived_orientation, self._derived_scale)
        else:
            self._derived_position = self._position
            self._derived_orientation = self._orientation
            self._derived_scale = scale_and_mirror
            self._world_transformation = self._transformation

    def _resetAABB(self):
        if not self._calculate_aabb:
            return

        self._aabb = None

        if self._aabb_job:
            self._aabb_job.cancel()

        self._aabb_job = _CalculateAABBJob(self)
        self._aabb_job.start()
Example #56
0
    def getScale(self) -> Vector:
        x = numpy.linalg.norm(self._data[0, 0:3])
        y = numpy.linalg.norm(self._data[1, 0:3])
        z = numpy.linalg.norm(self._data[2, 0:3])

        return Vector(x, y, z)
Example #57
0
    def write(self, stream, nodes, mode=MeshWriter.OutputMode.BinaryMode):
        self._archive = None  # Reset archive
        archive = zipfile.ZipFile(stream,
                                  "w",
                                  compression=zipfile.ZIP_DEFLATED)
        try:
            model_file = zipfile.ZipInfo("3D/3dmodel.model")
            # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
            model_file.compress_type = zipfile.ZIP_DEFLATED

            # Create content types file
            content_types_file = zipfile.ZipInfo("[Content_Types].xml")
            content_types_file.compress_type = zipfile.ZIP_DEFLATED
            content_types = ET.Element("Types",
                                       xmlns=self._namespaces["content-types"])
            rels_type = ET.SubElement(
                content_types,
                "Default",
                Extension="rels",
                ContentType=
                "application/vnd.openxmlformats-package.relationships+xml")
            model_type = ET.SubElement(
                content_types,
                "Default",
                Extension="model",
                ContentType=
                "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")

            # Create _rels/.rels file
            relations_file = zipfile.ZipInfo("_rels/.rels")
            relations_file.compress_type = zipfile.ZIP_DEFLATED
            relations_element = ET.Element(
                "Relationships", xmlns=self._namespaces["relationships"])
            model_relation_element = ET.SubElement(
                relations_element,
                "Relationship",
                Target="/3D/3dmodel.model",
                Id="rel0",
                Type=
                "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")

            savitar_scene = Savitar.Scene()
            transformation_matrix = Matrix()
            transformation_matrix._data[1, 1] = 0
            transformation_matrix._data[1, 2] = -1
            transformation_matrix._data[2, 1] = 1
            transformation_matrix._data[2, 2] = 0

            global_container_stack = Application.getInstance(
            ).getGlobalContainerStack()
            # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
            # build volume.
            if global_container_stack:
                translation_vector = Vector(
                    x=global_container_stack.getProperty(
                        "machine_width", "value") / 2,
                    y=global_container_stack.getProperty(
                        "machine_depth", "value") / 2,
                    z=0)
                translation_matrix = Matrix()
                translation_matrix.setByTranslation(translation_vector)
                transformation_matrix.preMultiply(translation_matrix)

            root_node = UM.Application.Application.getInstance().getController(
            ).getScene().getRoot()
            for node in nodes:
                if node == root_node:
                    for root_child in node.getChildren():
                        savitar_node = self._convertUMNodeToSavitarNode(
                            root_child, transformation_matrix)
                        if savitar_node:
                            savitar_scene.addSceneNode(savitar_node)
                else:
                    savitar_node = self._convertUMNodeToSavitarNode(
                        node, transformation_matrix)
                    if savitar_node:
                        savitar_scene.addSceneNode(savitar_node)

            parser = Savitar.ThreeMFParser()
            scene_string = parser.sceneToString(savitar_scene)

            archive.writestr(model_file, scene_string)
            archive.writestr(
                content_types_file,
                b'<?xml version="1.0" encoding="UTF-8"?> \n' +
                ET.tostring(content_types))
            archive.writestr(
                relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' +
                ET.tostring(relations_element))
        except Exception as e:
            Logger.logException("e", "Error writing zip file")
            self.setInformation(
                catalog.i18nc("@error:zip", "Error writing 3mf file."))
            return False
        finally:
            if not self._store_archive:
                archive.close()
            else:
                self._archive = archive

        return True
Example #58
0
    def _onChangeTimerFinished(self):
        if not self._enabled:
            return

        root = self._controller.getScene().getRoot()
        for node in BreadthFirstIterator(root):
            if node is root or type(node) is not SceneNode:
                continue

            bbox = node.getBoundingBox()
            if not bbox or not bbox.isValid():
                self._change_timer.start()
                continue

            build_volume_bounding_box = copy.deepcopy(self._build_volume.getBoundingBox())
            build_volume_bounding_box.setBottom(-9001) # Ignore intersections with the bottom

            # Mark the node as outside the build volume if the bounding box test fails.
            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
                node._outside_buildarea = True
            else:
                node._outside_buildarea = False

            # Move it downwards if bottom is above platform
            move_vector = Vector()
            if bbox.bottom > 0:
                move_vector.setY(-bbox.bottom)
            #if not Float.fuzzyCompare(bbox.bottom, 0.0):
            #   pass#move_vector.setY(-bbox.bottom)

            # If there is no convex hull for the node, start calculating it and continue.
            if not node.getDecorator(ConvexHullDecorator):
                node.addDecorator(ConvexHullDecorator())
            
            if not node.callDecoration("getConvexHull"):
                if not node.callDecoration("getConvexHullJob"):
                    job = ConvexHullJob.ConvexHullJob(node)
                    job.start()
                    node.callDecoration("setConvexHullJob", job)

            elif Selection.isSelected(node):
                pass
            elif Preferences.getInstance().getValue("physics/automatic_push_free"):
                # Check for collisions between convex hulls
                for other_node in BreadthFirstIterator(root):
                    # Ignore root, ourselves and anything that is not a normal SceneNode.
                    if other_node is root or type(other_node) is not SceneNode or other_node is node:
                        continue
                    
                    # Ignore colissions of a group with it's own children
                    if other_node in node.getAllChildren() or node in other_node.getAllChildren():
                        continue
                    
                    # Ignore colissions within a group
                    if other_node.getParent().callDecoration("isGroup") is not None:
                        if node.getParent().callDecoration("isGroup") is other_node.getParent().callDecoration("isGroup"):
                            continue
                    
                    # Ignore nodes that do not have the right properties set.
                    if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
                        continue

                    # Check to see if the bounding boxes intersect. If not, we can ignore the node as there is no way the hull intersects.
                    if node.getBoundingBox().intersectsBox(other_node.getBoundingBox()) == AxisAlignedBox.IntersectionResult.NoIntersection:
                        continue

                    # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
                    overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_node.callDecoration("getConvexHull"))
                    if overlap is None:
                        continue
                    
                    move_vector.setX(overlap[0] * 1.1)
                    move_vector.setZ(overlap[1] * 1.1)
            convex_hull = node.callDecoration("getConvexHull")
            if convex_hull:
                if not convex_hull.isValid():
                    return
                # Check for collisions between disallowed areas and the object
                for area in self._build_volume.getDisallowedAreas():
                    overlap = convex_hull.intersectsPolygon(area)
                    if overlap is None:
                        continue

                    node._outside_buildarea = True

            if move_vector != Vector():
                op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                op.push()
    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()

        x, y = 200, 200

        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).
            # We also skip possibilities by slicing through the possibilities (step = 10)

            try_placement = True

            current_build_plate_number = 0  # always start with the first one

            # # Only for first build plate
            # if last_size == size and last_build_plate_number == current_build_plate_number:
            #     # This optimization works if many of the objects have the same size
            #     # Continue with same build plate number
            #     start_priority = last_priority
            # else:
            #     start_priority = 0

            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(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
                    arranger.place(
                        x, y,
                        hull_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()
Example #60
0
    def read(self, file_name):
        result = None
        extension = os.path.splitext(file_name)[1]
        if extension.lower() == self._supported_extension:
            result = SceneNode()
            # The base object of 3mf is a zipped archive.
            archive = zipfile.ZipFile(file_name, 'r')
            try:
                root = ET.parse(archive.open("3D/3dmodel.model"))

                # There can be multiple objects, try to load all of them.
                objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
                if len(objects) == 0:
                    Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
                    return None

                for object in objects:
                    mesh = MeshData()
                    node = SceneNode()
                    vertex_list = []
                    #for vertex in object.mesh.vertices.vertex:
                    for vertex in object.findall(".//3mf:vertex", self._namespaces):
                        vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
                        Job.yieldThread()

                    triangles = object.findall(".//3mf:triangle", self._namespaces)

                    mesh.reserveFaceCount(len(triangles))

                    #for triangle in object.mesh.triangles.triangle:
                    for triangle in triangles:
                        v1 = int(triangle.get("v1"))
                        v2 = int(triangle.get("v2"))
                        v3 = int(triangle.get("v3"))
                        mesh.addFace(vertex_list[v1][0],vertex_list[v1][1],vertex_list[v1][2],vertex_list[v2][0],vertex_list[v2][1],vertex_list[v2][2],vertex_list[v3][0],vertex_list[v3][1],vertex_list[v3][2])
                        Job.yieldThread()

                    #TODO: We currently do not check for normals and simply recalculate them.
                    mesh.calculateNormals()
                    node.setMeshData(mesh)
                    node.setSelectable(True)

                    transformation = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(object.get("id")), self._namespaces)
                    if transformation:
                        transformation = transformation[0]

                    if transformation.get("transform"):
                        splitted_transformation = transformation.get("transform").split()
                        ## Transformation is saved as:
                        ## M00 M01 M02 0.0
                        ## M10 M11 M12 0.0
                        ## M20 M21 M22 0.0
                        ## M30 M31 M32 1.0
                        ## We switch the row & cols as that is how everyone else uses matrices!
                        temp_mat = Matrix()
                        # Rotation & Scale
                        temp_mat._data[0,0] = splitted_transformation[0]
                        temp_mat._data[1,0] = splitted_transformation[1]
                        temp_mat._data[2,0] = splitted_transformation[2]
                        temp_mat._data[0,1] = splitted_transformation[3]
                        temp_mat._data[1,1] = splitted_transformation[4]
                        temp_mat._data[2,1] = splitted_transformation[5]
                        temp_mat._data[0,2] = splitted_transformation[6]
                        temp_mat._data[1,2] = splitted_transformation[7]
                        temp_mat._data[2,2] = splitted_transformation[8]

                        # Translation
                        temp_mat._data[0,3] = splitted_transformation[9]
                        temp_mat._data[1,3] = splitted_transformation[10]
                        temp_mat._data[2,3] = splitted_transformation[11]

                        node.setPosition(Vector(temp_mat.at(0,3), temp_mat.at(1,3), temp_mat.at(2,3)))

                        temp_quaternion = Quaternion()
                        temp_quaternion.setByMatrix(temp_mat)
                        node.setOrientation(temp_quaternion)

                        # Magical scale extraction
                        S2 = temp_mat.getTransposed().multiply(temp_mat)
                        scale_x = math.sqrt(S2.at(0,0))
                        scale_y = math.sqrt(S2.at(1,1))
                        scale_z = math.sqrt(S2.at(2,2))
                        node.setScale(Vector(scale_x,scale_y,scale_z))

                        # We use a different coordinate frame, so rotate.
                        #rotation = Quaternion.fromAngleAxis(-0.5 * math.pi, Vector(1,0,0))
                        #node.rotate(rotation)
                    result.addChild(node)

                    Job.yieldThread()

                #If there is more then one object, group them.
                try:
                    if len(objects) > 1:
                        group_decorator = GroupDecorator()
                        result.addDecorator(group_decorator)
                except:
                    pass
            except Exception as e:
                Logger.log("e" ,"exception occured in 3mf reader: %s" , e)
        return result