Merge branch 'master' into libArachne_rebased

This commit is contained in:
Ghostkeeper 2020-10-25 11:50:21 +01:00
commit 0bcbafd0a6
No known key found for this signature in database
GPG Key ID: D2A8871EE34EC59A
442 changed files with 12344 additions and 4948 deletions

View File

@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template.
CuraSDKVersion = "7.3.0"
CuraSDKVersion = "7.4.0"
try:
from cura.CuraVersion import CuraAppName # type: ignore

View File

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Decorators import deprecated
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from UM.Math.Polygon import Polygon
@ -32,6 +33,7 @@ class Arrange:
build_volume = None # type: Optional[BuildVolume]
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
self._scale = scale # convert input coordinates to arrange coordinates
world_x, world_y = int(x * self._scale), int(y * self._scale)
@ -45,6 +47,7 @@ class Arrange:
self._is_empty = True
@classmethod
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange":
"""Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance
@ -101,6 +104,7 @@ class Arrange:
self._last_priority = 0
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
"""Find placement for a node (using offset shape) and place it (using hull shape)

View File

@ -1,24 +1,17 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QCoreApplication
from typing import List
from UM.Application import Application
from UM.Job import Job
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Operations.TranslateOperation import TranslateOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Logger import Logger
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from cura.Arranging.Nest2DArrange import arrange
i18n_catalog = i18nCatalog("cura")
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray
from typing import List
class ArrangeObjectsJob(Job):
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
@ -30,80 +23,22 @@ class ArrangeObjectsJob(Job):
def run(self):
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
lifetime = 0,
dismissable=False,
dismissable = False,
progress = 0,
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()
global_container_stack = Application.getInstance().getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
# Build set to exclude children (those get arranged together with the parents).
included_as_child = set()
for node in self._nodes:
included_as_child.update(node.getAllChildren())
# Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in self._nodes:
if node in included_as_child:
continue
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True)
if offset_shape_arr is None:
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
continue
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
# Sort the nodes with the biggest area first.
nodes_arr.sort(key=lambda item: item[0])
nodes_arr.reverse()
# Place nodes one at a time
start_priority = 0
last_priority = start_priority
last_size = None
grouped_operation = GroupedOperation()
found_solution_for_all = True
not_fit_count = 0
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
# For performance reasons, we assume that when a location does not fit,
# it will also not fit for the next object (while what can be untrue).
if last_size == size: # This optimization works if many of the objects have the same size
start_priority = last_priority
else:
start_priority = 0
best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority)
x, y = best_spot.x, best_spot.y
node.removeDecorator(ZOffsetDecorator)
if node.getBoundingBox():
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = 0
if x is not None: # We could find a place
last_size = size
last_priority = best_spot.priority
arranger.place(x, y, offset_shape_arr) # take place before the next one
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
else:
Logger.log("d", "Arrange all: could not find spot!")
found_solution_for_all = False
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
not_fit_count += 1
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
Job.yieldThread()
QCoreApplication.processEvents()
grouped_operation.push()
found_solution_for_all = None
try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
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"))
if found_solution_for_all is not None and not found_solution_for_all:
no_full_solution_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
no_full_solution_message.show()
self.finished.emit(self)

View File

@ -0,0 +1,144 @@
import numpy
from pynest2d import Point, Box, Item, NfpConfig, nest
from typing import List, TYPE_CHECKING, Optional, Tuple
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Polygon import Polygon
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from cura.BuildVolume import BuildVolume
def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]:
"""
Find placement for a set of scene nodes, but don't actually move them just yet.
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accurate we want it to be.
:return: tuple (found_solution_for_all, node_items)
WHERE
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
"""
machine_width = build_volume.getWidth()
machine_depth = build_volume.getDepth()
build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor)
if fixed_nodes is None:
fixed_nodes = []
# Add all the items we want to arrange
node_items = []
for node in nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None:
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue
converted_points = []
for point in hull_polygon.getPoints():
converted_points.append(Point(point[0] * factor, point[1] * factor))
item = Item(converted_points)
node_items.append(item)
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
half_machine_width = 0.5 * machine_width - 1
half_machine_depth = 0.5 * machine_depth - 1
build_plate_polygon = Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth]
], numpy.float32))
disallowed_areas = build_volume.getDisallowedAreas()
num_disallowed_areas_added = 0
for area in disallowed_areas:
converted_points = []
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
if clipped_area.getPoints() is not None: # numpy array has to be explicitly checked against None
for point in clipped_area.getPoints():
converted_points.append(Point(point[0] * factor, point[1] * factor))
disallowed_area = Item(converted_points)
disallowed_area.markAsDisallowedAreaInBin(0)
node_items.append(disallowed_area)
num_disallowed_areas_added += 1
for node in fixed_nodes:
converted_points = []
hull_polygon = node.callDecoration("getConvexHull")
if hull_polygon is not None and hull_polygon.getPoints() is not None: # numpy array has to be explicitly checked against None
for point in hull_polygon.getPoints():
converted_points.append(Point(point[0] * factor, point[1] * factor))
item = Item(converted_points)
item.markAsFixedInBin(0)
node_items.append(item)
num_disallowed_areas_added += 1
config = NfpConfig()
config.accuracy = 1.0
num_bins = nest(node_items, build_plate_bounding_box, 10000, config)
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
node_items = list(filter(lambda item: not item.isFixed(), node_items))
found_solution_for_all = num_bins == 1
return found_solution_for_all, node_items
def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> bool:
"""
Find placement for a set of scene nodes, and move them by using a single grouped operation.
:param nodes_to_arrange: The list of nodes that need to be moved.
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed.
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
"""
scene_root = Application.getInstance().getController().getScene().getRoot()
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)
not_fit_count = 0
grouped_operation = GroupedOperation()
for node, node_item in zip(nodes_to_arrange, node_items):
if add_new_nodes_in_scene:
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
if node_item.binId() == 0:
# We found a spot for it
rotation_matrix = Matrix()
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0,
node_item.translation().y() / factor)))
else:
# We didn't find a spot
grouped_operation.addOperation(
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
not_fit_count += 1
grouped_operation.push()
return found_solution_for_all

View File

@ -180,12 +180,21 @@ class BuildVolume(SceneNode):
def setWidth(self, width: float) -> None:
self._width = width
def getWidth(self) -> float:
return self._width
def setHeight(self, height: float) -> None:
self._height = height
def getHeight(self) -> float:
return self._height
def setDepth(self, depth: float) -> None:
self._depth = depth
def getDepth(self) -> float:
return self._depth
def setShape(self, shape: str) -> None:
if shape:
self._shape = shape
@ -782,7 +791,9 @@ class BuildVolume(SceneNode):
break
if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
brim_size = self._calculateBedAdhesionSize(used_extruders, "brim")
prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(brim_size))
# Use 2x the brim size, since we need 1x brim size distance due to the object brim and another
# times the brim due to the brim of the prime tower
prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(2 * brim_size, num_segments = 24))
if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
@ -834,7 +845,7 @@ class BuildVolume(SceneNode):
prime_tower_y += brim_size
radius = prime_tower_size / 2
prime_tower_area = Polygon.approximatedCircle(radius)
prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 24)
prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))

View File

@ -36,6 +36,7 @@ from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -52,7 +53,7 @@ from cura.API.Account import Account
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ShapeArray import ShapeArray
from cura.Arranging.Nest2DArrange import arrange
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
@ -801,6 +802,8 @@ class CuraApplication(QtApplication):
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume..."))
root = self.getController().getScene().getRoot()
self._volume = BuildVolume.BuildVolume(self, root)
# Ensure that the old style arranger still works.
Arrange.build_volume = self._volume
# initialize info objects
@ -1379,6 +1382,7 @@ class CuraApplication(QtApplication):
def arrangeAll(self) -> None:
nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
locked_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not isinstance(node, SceneNode):
continue
@ -1400,8 +1404,12 @@ class CuraApplication(QtApplication):
# Skip nodes that are too big
bounding_box = node.getBoundingBox()
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, fixed_nodes = [])
# Arrange only the unlocked nodes and keep the locked ones in place
if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)):
locked_nodes.append(node)
else:
nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, locked_nodes)
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
"""Arrange a set of nodes given a set of fixed nodes
@ -1815,17 +1823,21 @@ class CuraApplication(QtApplication):
for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_)
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
min_offset = 8
default_extruder_position = self.getMachineManager().defaultExtruderPosition
default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
for original_node in nodes:
nodes_to_arrange = [] # type: List[CuraSceneNode]
fixed_nodes = []
for node_ in DepthFirstIterator(self.getController().getScene().getRoot()):
# Only count sliceable objects
if node_.callDecoration("isSliceable"):
fixed_nodes.append(node_)
for original_node in nodes:
# Create a CuraSceneNode just if the original node is not that type
if isinstance(original_node, CuraSceneNode):
node = original_node
@ -1833,8 +1845,8 @@ class CuraApplication(QtApplication):
node = CuraSceneNode()
node.setMeshData(original_node.getMeshData())
#Setting meshdata does not apply scaling.
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
# Setting meshdata does not apply scaling.
if original_node.getScale() != Vector(1.0, 1.0, 1.0):
node.scale(original_node.getScale())
node.setSelectable(True)
@ -1865,19 +1877,15 @@ class CuraApplication(QtApplication):
if file_extension != "3mf":
if node.callDecoration("isSliceable"):
# Only check position if it's not already blatantly obvious that it won't fit.
if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
# Find node location
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
# Ensure that the bottom of the bounding box is on the build plate
if node.getBoundingBox():
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else:
center_y = 0
# If a model is to small then it will not contain any points
if offset_shape_arr is None and hull_shape_arr is None:
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
title = self._i18n_catalog.i18nc("@info:title", "Warning")).show()
return
node.translate(Vector(0, center_y, 0))
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
nodes_to_arrange.append(node)
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
@ -1897,6 +1905,8 @@ class CuraApplication(QtApplication):
if select_models_on_load:
Selection.add(node)
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
self.fileCompleted.emit(file_name)
def addNonSliceableExtension(self, extension):

View File

@ -4,21 +4,16 @@
import copy
from typing import List
from PyQt5.QtCore import QCoreApplication
from UM.Application import Application
from UM.Job import Job
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Message import Message
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from cura.Arranging.Nest2DArrange import arrange
i18n_catalog = i18nCatalog("cura")
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ShapeArray import ShapeArray
from UM.Application import Application
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
class MultiplyObjectsJob(Job):
def __init__(self, objects, count, min_offset = 8):
@ -28,28 +23,27 @@ class MultiplyObjectsJob(Job):
self._min_offset = min_offset
def run(self) -> None:
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
dismissable = False, progress = 0,
title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
status_message.show()
scene = Application.getInstance().getController().getScene()
total_progress = len(self._objects) * self._count
current_progress = 0
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return # We can't do anything in this case.
machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value")
root = scene.getRoot()
scale = 0.5
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
processed_nodes = [] # type: List[SceneNode]
nodes = []
not_fit_count = 0
found_solution_for_all = False
fixed_nodes = []
for node_ in DepthFirstIterator(root):
# Only count sliceable objects
if node_.callDecoration("isSliceable"):
fixed_nodes.append(node_)
for node in self._objects:
# If object is part of a group, multiply group
current_node = node
@ -60,31 +54,8 @@ class MultiplyObjectsJob(Job):
continue
processed_nodes.append(current_node)
node_too_big = False
if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
else:
node_too_big = True
found_solution_for_all = True
arranger.resetLastPriority()
for _ in range(self._count):
# We do place the nodes one by one, as we want to yield in between.
new_node = copy.deepcopy(node)
solution_found = False
if not node_too_big:
if offset_shape_arr is not None and hull_shape_arr is not None:
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
else:
# The node has no shape, so no need to arrange it. The solution is simple: Do nothing.
solution_found = True
if node_too_big or not solution_found:
found_solution_for_all = False
new_location = new_node.getPosition()
new_location = new_location.set(z = - not_fit_count * 20)
new_node.setPosition(new_location)
not_fit_count += 1
# Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
@ -93,20 +64,15 @@ class MultiplyObjectsJob(Job):
child.callDecoration("setBuildPlateNumber", build_plate_number)
nodes.append(new_node)
current_progress += 1
status_message.setProgress((current_progress / total_progress) * 100)
QCoreApplication.processEvents()
Job.yieldThread()
QCoreApplication.processEvents()
Job.yieldThread()
found_solution_for_all = True
if nodes:
operation = GroupedOperation()
for new_node in nodes:
operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
operation.push()
found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes,
factor = 10000, add_new_nodes_in_scene = True)
status_message.hide()
if not found_solution_for_all:
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object"))
no_full_solution_message = Message(
i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
title = i18n_catalog.i18nc("@info:title", "Placing Object"))
no_full_solution_message.show()

View File

@ -1,11 +1,11 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime
import json
import random
from hashlib import sha512
from base64 import b64encode
from typing import Optional
from typing import Optional, Any, Dict, Tuple
import requests
@ -16,6 +16,7 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin
catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
class AuthorizationHelpers:
"""Class containing several helpers to deal with the authorization flow."""
@ -121,10 +122,13 @@ class AuthorizationHelpers:
if not user_data or not isinstance(user_data, dict):
Logger.log("w", "Could not parse user data from token: %s", user_data)
return None
return UserProfile(
user_id = user_data["user_id"],
username = user_data["username"],
profile_image_url = user_data.get("profile_image_url", "")
profile_image_url = user_data.get("profile_image_url", ""),
organization_id = user_data.get("organization", {}).get("organization_id", ""),
subscriptions = user_data.get("subscriptions", [])
)
@staticmethod

View File

@ -1,6 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, List
class BaseModel:
@ -27,6 +27,8 @@ class UserProfile(BaseModel):
user_id = None # type: Optional[str]
username = None # type: Optional[str]
profile_image_url = None # type: Optional[str]
organization_id = None # type: Optional[str]
subscriptions = None # type: Optional[List[Dict[str, Any]]]
class AuthenticationResponse(BaseModel):

View File

@ -27,10 +27,13 @@ class OneAtATimeIterator(Iterator.Iterator):
if not issubclass(type(node), SceneNode):
continue
# Node can't be printed, so don't bother sending it.
if getattr(node, "_outside_buildarea", False):
continue
if node.callDecoration("getConvexHull"):
node_list.append(node)
if len(node_list) < 2:
self._node_stack = node_list[:]
return
@ -38,8 +41,8 @@ class OneAtATimeIterator(Iterator.Iterator):
# Copy the list
self._original_node_list = node_list[:]
## Initialise the hit map (pre-compute all hits between all objects)
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
# Initialise the hit map (pre-compute all hits between all objects)
self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list]
# Check if we have to files that block each other. If this is the case, there is no solution!
for a in range(0, len(node_list)):

View File

@ -269,7 +269,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
if mesh is None:
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
world_transform = self._node.getWorldTransformation(copy = False)
world_transform = self._node.getWorldTransformation(copy = True)
# Check the cache
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:

View File

@ -5,7 +5,7 @@ import os
import re
import configparser
from typing import Any, cast, Dict, Optional, List, Union
from typing import Any, cast, Dict, Optional, List, Union, Tuple
from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override
@ -179,7 +179,7 @@ class CuraContainerRegistry(ContainerRegistry):
"""Imports a profile from a file
:param file_name: The full path and filename of the profile to import.
:return: Dict with a 'status' key containing the string 'ok' or 'error',
:return: Dict with a 'status' key containing the string 'ok', 'warning' or 'error',
and a 'message' key containing a message for the user.
"""
@ -305,6 +305,7 @@ class CuraContainerRegistry(ContainerRegistry):
# Import all profiles
profile_ids_added = [] # type: List[str]
additional_message = None
for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0:
# This is assumed to be the global profile
@ -323,18 +324,26 @@ class CuraContainerRegistry(ContainerRegistry):
else: # More extruders in the imported file than in the machine.
continue # Delete the additional profiles.
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
if result is not None:
# Remove any profiles that did got added.
for profile_id in profile_ids_added:
configuration_successful, message = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
if configuration_successful:
additional_message = message
else:
# Remove any profiles that were added.
for profile_id in profile_ids_added + [profile.getId()]:
self.removeContainer(profile_id)
if not message:
message = ""
return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tag <filename>!",
"Failed to import profile from <filename>{0}</filename>:",
file_name) + " " + result}
"@info:status Don't translate the XML tag <filename>!",
"Failed to import profile from <filename>{0}</filename>:",
file_name) + " " + message}
profile_ids_added.append(profile.getId())
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
result_status = "ok"
success_message = catalog.i18nc("@info:status", "Successfully imported profile {0}.", profile_or_list[0].getName())
if additional_message:
result_status = "warning"
success_message += additional_message
return {"status": result_status, "message": success_message}
# This message is throw when the profile reader doesn't find any profile in the file
return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
@ -395,14 +404,18 @@ class CuraContainerRegistry(ContainerRegistry):
return False
return True
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Tuple[bool, Optional[str]]:
"""Update an imported profile to match the current machine configuration.
:param profile: The profile to configure.
:param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
:param new_name: The new name for the profile.
:return: None if configuring was successful or an error message if an error occurred.
:returns: tuple (configuration_successful, message)
WHERE
bool configuration_successful: Whether the process of configuring the profile was successful
optional str message: A message indicating the outcome of configuring the profile. If the configuration
is successful, this message can be None or contain a warning
"""
profile.setDirty(True) # Ensure the profiles are correctly saved
@ -423,26 +436,39 @@ class CuraContainerRegistry(ContainerRegistry):
quality_type = profile.getMetaDataEntry("quality_type")
if not quality_type:
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
return False, catalog.i18nc("@info:status", "Profile is missing a quality type.")
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return None
if not global_stack:
return False, catalog.i18nc("@info:status", "Global stack is missing.")
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
profile.setDefinition(definition_id)
if not self.addContainer(profile):
return False, catalog.i18nc("@info:status", "Unable to add the profile.")
# "not_supported" profiles can be imported.
if quality_type == empty_quality_container.getMetaDataEntry("quality_type"):
return True, None
# Check to make sure the imported profile actually makes sense in context of the current configuration.
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
# successfully imported but then fail to show up.
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
# "not_supported" profiles can be imported.
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
available_quality_groups_dict = {name: quality_group for name, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items() if quality_group.is_available}
all_quality_groups_dict = ContainerTree.getInstance().getCurrentQualityGroups()
if not self.addContainer(profile):
return catalog.i18nc("@info:status", "Unable to add the profile.")
# If the quality type doesn't exist at all in the quality_groups of this machine, reject the profile
if quality_type not in all_quality_groups_dict:
return False, catalog.i18nc("@info:status", "Quality type '{0}' is not compatible with the current active machine definition '{1}'.", quality_type, definition_id)
return None
# If the quality_type exists in the quality_groups of this printer but it is not available with the current
# machine configuration (e.g. not available for the selected nozzles), accept it with a warning
if quality_type not in available_quality_groups_dict:
return True, "\n\n" + catalog.i18nc("@info:status", "Warning: The profile is not visible because its quality type '{0}' is not available for the current configuration. "
"Switch to a material/nozzle combination that can use this quality type.", quality_type)
return True, None
@override(ContainerRegistry)
def saveDirtyContainers(self) -> None:

View File

@ -369,7 +369,8 @@ class MachineManager(QObject):
material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
if material_node is None:
Logger.log("w", "An extruder has an unknown material, switching it to the preferred material")
self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material)
if not self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material):
Logger.log("w", "Failed to switch to %s keeping old material instead", machine_node.preferred_material)
@staticmethod
@ -997,6 +998,11 @@ class MachineManager(QObject):
self.activeMaterialChanged.emit()
self.activeIntentChanged.emit()
# Force an update of resolve values
property_names = ["resolve", "validationState"]
for setting_key in self._global_container_stack.getAllKeys():
self._global_container_stack.propertiesChanged.emit(setting_key, property_names)
def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit()
@ -1152,6 +1158,7 @@ class MachineManager(QObject):
extruder.qualityChanges = quality_changes_container
self.setIntentByCategory(quality_changes_group.intent_category)
self._reCalculateNumUserSettings()
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
@ -1452,17 +1459,21 @@ class MachineManager(QObject):
self.updateMaterialWithVariant(None) # Update all materials
self._updateQualityWithMaterial()
@pyqtSlot(str, str)
def setMaterialById(self, position: str, root_material_id: str) -> None:
@pyqtSlot(str, str, result = bool)
def setMaterialById(self, position: str, root_material_id: str) -> bool:
if self._global_container_stack is None:
return
return False
machine_definition_id = self._global_container_stack.definition.id
position = str(position)
extruder_stack = self._global_container_stack.extruderList[int(position)]
nozzle_name = extruder_stack.variant.getName()
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
self.setMaterial(position, material_node)
materials = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials
if root_material_id in materials:
self.setMaterial(position, materials[root_material_id])
return True
return False
@pyqtSlot(str, "QVariant")
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:

View File

@ -7,10 +7,9 @@ from cura.CuraApplication import CuraApplication
class UltimakerCloudScope(DefaultUserAgentScope):
"""Add an Authorization header to the request for Ultimaker Cloud Api requests.
When the user is not logged in or a token is not available, a warning will be logged
Also add the user agent headers (see DefaultUserAgentScope)
"""
Add an Authorization header to the request for Ultimaker Cloud Api requests, if available.
Also add the user agent headers (see DefaultUserAgentScope).
"""
def __init__(self, application: CuraApplication):
@ -22,7 +21,7 @@ class UltimakerCloudScope(DefaultUserAgentScope):
super().requestHook(request)
token = self._account.accessToken
if not self._account.isLoggedIn or token is None:
Logger.warning("Cannot add authorization to Cloud Api request")
Logger.debug("User is not logged in for Cloud API request to {url}".format(url = request.url().toDisplayString()))
return
header_dict = {

View File

@ -22,6 +22,7 @@ import os
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
import pynest2d # @UnusedImport
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket

View File

@ -19,6 +19,7 @@ from UM.Scene.SceneNode import SceneNode # For typing.
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
@ -108,6 +109,10 @@ class ThreeMFReader(MeshReader):
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
try:
um_node.addDecorator(ConvexHullDecorator())
except:
pass
um_node.setName(node_name)
um_node.setId(node_id)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for reading 3MF files.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for writing 3MF files.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,5 +3,5 @@
"author": "fieldOfView",
"version": "1.0.0",
"description": "Provides support for reading AMF files.",
"api": "7.3.0"
"api": "7.4.0"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.",
"version": "1.2.0",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -82,7 +82,7 @@ class CuraEngineBackend(QObject, Backend):
default_engine_location = execpath
break
self._application = CuraApplication.getInstance() #type: CuraApplication
application = CuraApplication.getInstance() #type: CuraApplication
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
@ -92,7 +92,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
default_engine_location = os.path.abspath(default_engine_location)
self._application.getPreferences().addPreference("backend/location", default_engine_location)
application.getPreferences().addPreference("backend/location", default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False #type: bool
@ -101,7 +101,7 @@ class CuraEngineBackend(QObject, Backend):
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = self._application.getController().getScene() #type: Scene
self._scene = application.getController().getScene() #type: Scene
self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
@ -141,7 +141,7 @@ class CuraEngineBackend(QObject, Backend):
self._slice_start_time = None #type: Optional[float]
self._is_disabled = False #type: bool
self._application.getPreferences().addPreference("general/auto_slice", False)
application.getPreferences().addPreference("general/auto_slice", False)
self._use_timer = False #type: bool
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
@ -151,19 +151,20 @@ class CuraEngineBackend(QObject, Backend):
self._change_timer.setSingleShot(True)
self._change_timer.setInterval(500)
self.determineAutoSlicing()
self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._application.initializationFinished.connect(self.initialize)
application.initializationFinished.connect(self.initialize)
def initialize(self) -> None:
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
application = CuraApplication.getInstance()
self._multi_build_plate_model = application.getMultiBuildPlateModel()
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
application.getController().activeViewChanged.connect(self._onActiveViewChanged)
if self._multi_build_plate_model:
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
@ -173,10 +174,10 @@ class CuraEngineBackend(QObject, Backend):
self.backendConnected.connect(self._onBackendConnected)
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
self._machine_error_checker = self._application.getMachineErrorChecker()
self._machine_error_checker = application.getMachineErrorChecker()
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
def close(self) -> None:
@ -195,7 +196,7 @@ class CuraEngineBackend(QObject, Backend):
This is useful for debugging and used to actually start the engine.
:return: list of commands and args / parameters.
"""
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
@ -259,7 +260,8 @@ class CuraEngineBackend(QObject, Backend):
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
# see if we really have to slice
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
application = CuraApplication.getInstance()
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjectsPerBuildPlate()
@ -274,8 +276,8 @@ class CuraEngineBackend(QObject, Backend):
self.slice()
return
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._process is None: # type: ignore
self._createSocket()
@ -314,7 +316,7 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0)
Logger.log("d", "Attempting to kill the engine process")
if self._application.getUseExternalBackend():
if CuraApplication.getInstance().getUseExternalBackend():
return
if self._process is not None: # type: ignore
@ -350,8 +352,9 @@ class CuraEngineBackend(QObject, Backend):
self.backendError.emit(job)
return
application = CuraApplication.getInstance()
if job.getResult() == StartJobResult.MaterialIncompatible:
if self._application.platformActivity:
if application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status",
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -362,7 +365,7 @@ class CuraEngineBackend(QObject, Backend):
return
if job.getResult() == StartJobResult.SettingError:
if self._application.platformActivity:
if application.platformActivity:
if not self._global_container_stack:
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return
@ -394,7 +397,7 @@ class CuraEngineBackend(QObject, Backend):
elif job.getResult() == StartJobResult.ObjectSettingError:
errors = {}
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()):
for node in DepthFirstIterator(application.getController().getScene().getRoot()):
stack = node.callDecoration("getStack")
if not stack:
continue
@ -415,7 +418,7 @@ class CuraEngineBackend(QObject, Backend):
return
if job.getResult() == StartJobResult.BuildPlateError:
if self._application.platformActivity:
if application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -433,7 +436,7 @@ class CuraEngineBackend(QObject, Backend):
return
if job.getResult() == StartJobResult.NothingToSlice:
if self._application.platformActivity:
if application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
"\n- Fit within the build volume"
"\n- Are assigned to an enabled extruder"
@ -466,7 +469,7 @@ class CuraEngineBackend(QObject, Backend):
enable_timer = True
self._is_disabled = False
if not self._application.getPreferences().getValue("general/auto_slice"):
if not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice"):
enable_timer = False
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isBlockSlicing"):
@ -560,7 +563,7 @@ class CuraEngineBackend(QObject, Backend):
:param error: The exception that occurred.
"""
if self._application.isShuttingDown():
if CuraApplication.getInstance().isShuttingDown():
return
super()._onSocketError(error)
@ -600,7 +603,7 @@ class CuraEngineBackend(QObject, Backend):
cast(SceneNode, node.getParent()).removeChild(node)
def markSliceAll(self) -> None:
for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
for build_plate_number in range(CuraApplication.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
@ -696,12 +699,13 @@ class CuraEngineBackend(QObject, Backend):
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
gcode_list = []
application = CuraApplication.getInstance()
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths))
replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights))
replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts))
replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName))
replaced = line.replace("{print_time}", str(application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(application.getPrintInformation().materialLengths))
replaced = replaced.replace("{filament_weight}", str(application.getPrintInformation().materialWeights))
replaced = replaced.replace("{filament_cost}", str(application.getPrintInformation().materialCosts))
replaced = replaced.replace("{jobname}", str(application.getPrintInformation().jobName))
gcode_list[index] = replaced
@ -711,7 +715,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
# See if we need to process the sliced layers job.
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
if (
self._layer_view_active and
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
@ -870,9 +874,9 @@ class CuraEngineBackend(QObject, Backend):
def _onActiveViewChanged(self) -> None:
"""Called when the user changes the active view mode."""
view = self._application.getController().getActiveView()
view = CuraApplication.getInstance().getController().getActiveView()
if view:
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
@ -909,7 +913,7 @@ class CuraEngineBackend(QObject, Backend):
extruder.propertyChanged.disconnect(self._onSettingChanged)
extruder.containersChanged.disconnect(self._onChanged)
self._global_container_stack = self._application.getMachineManager().activeMachine
self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.

View File

@ -205,10 +205,6 @@ class StartSliceJob(Job):
for node in OneAtATimeIterator(self._scene.getRoot()):
temp_list = []
# Node can't be printed, so don't bother sending it.
if getattr(node, "_outside_buildarea", False):
continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:

View File

@ -2,7 +2,7 @@
"name": "CuraEngine Backend",
"author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.",
"api": "7.3.0",
"api": "7.4.0",
"version": "1.0.1",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing Cura profiles.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for exporting Cura profiles.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog":"cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Checks for firmware updates.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a machine actions for updating firmware.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Reads g-code from a compressed archive.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Writes g-code to a compressed archive.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing profiles from g-code files.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Victor Larchenko, Ultimaker B.V.",
"version": "1.0.1",
"description": "Allows loading and displaying G-code files.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Writes g-code to a file.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing profiles from legacy Cura versions.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "fieldOfView, Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -2,7 +2,7 @@
"name": "Model Checker",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": "7.3.0",
"api": "7.4.0",
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura"
}

View File

@ -99,7 +99,7 @@ Rectangle
visible: isNetworkConfigured && !isConnected
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
lineHeightMode: Text.FixedHeight
@ -116,7 +116,7 @@ Rectangle
visible: !isNetworkConfigured && isNetworkConfigurable
text: catalog.i18nc("@info", "Please connect your printer to the network.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap
width: contentWidth
lineHeight: UM.Theme.getSize("monitor_text_line_large").height

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a monitor stage in Cura.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides the Per Model Settings.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -261,7 +261,11 @@ class PostProcessingPlugin(QObject, Extension):
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
script_parser = configparser.ConfigParser(interpolation=None)
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_parser.read_string(script_str)
try:
script_parser.read_string(script_str)
except configparser.Error as e:
Logger.error("Stored post-processing scripts have syntax errors: {err}".format(err = str(e)))
continue
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
continue

View File

@ -7,6 +7,7 @@
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
import pynest2d # @UnusedImport
from . import PostProcessingPlugin

View File

@ -2,7 +2,7 @@
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2.1",
"api": "7.3.0",
"api": "7.4.0",
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}

View File

@ -889,7 +889,7 @@ class ChangeAtZProcessor:
# set feedrate percentage
if "speed" in values:
codes.append("M220 S" + str(values["speed"]) + " T1")
codes.append("M220 S" + str(values["speed"]) + "")
# set print rate percentage
if "printspeed" in values:
@ -1305,7 +1305,7 @@ class ChangeAtZProcessor:
self.targetLayer = None
self.targetZ = None
self.layerHeight = None
self.lastValues = {}
self.lastValues = {"speed": 100}
self.linearRetraction = True
self.insideTargetLayer = False
self.targetValuesInjected = False

View File

@ -1,7 +1,7 @@
# Cura PostProcessingPlugin
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen
# Date: July 31, 2019
# Modified: May 22, 2020
# Modified: Okt 22, 2020
# Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage.
@ -26,10 +26,31 @@ class DisplayProgressOnLCD(Script):
"time_remaining":
{
"label": "Time Remaining",
"description": "When enabled, write Time Left: HHMMSS on the display using M117. This is updated every layer.",
"description": "Select to write remaining time to the display.Select to write remaining time on the display using M117 status line message (almost all printers) or using M73 command (Prusa and Marlin 2 if enabled).",
"type": "bool",
"default_value": false
},
"time_remaining_method":
{
"label": "Time Reporting Method",
"description": "How should remaining time be shown on the display? It could use a generic message command (M117, almost all printers), or a specialised time remaining command (M73, Prusa and Marlin 2).",
"type": "enum",
"options": {
"m117":"M117 - All printers",
"m73":"M73 - Prusa, Marlin 2"
},
"enabled": "time_remaining",
"default_value": "m117"
},
"update_frequency":
{
"label": "Update frequency",
"description": "Update remaining time for every layer or periodically every minute or faster.",
"type": "enum",
"options": {"0":"Every layer","15":"Every 15 seconds","30":"Every 30 seconds","60":"Every minute"},
"default_value": "0",
"enabled": "time_remaining"
},
"percentage":
{
"label": "Percentage",
@ -46,34 +67,44 @@ class DisplayProgressOnLCD(Script):
list_split = re.split(":", line) # Split at ":" so we can get the numerical value
return float(list_split[1]) # Convert the numerical portion to a float
def outputTime(self, lines, line_index, time_left):
def outputTime(self, lines, line_index, time_left, mode):
# Do some math to get the time left in seconds into the right format. (HH,MM,SS)
time_left = max(time_left, 0)
m, s = divmod(time_left, 60)
h, m = divmod(m, 60)
# Create the string
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
# And now insert that into the GCODE
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
if mode == "m117":
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
# And now insert that into the GCODE
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
else: # Must be m73.
mins = int(60 * h + m + s / 30)
lines.insert(line_index, "M73 R{}".format(mins))
def execute(self, data):
output_time = self.getSettingValueByKey("time_remaining")
output_time_method = self.getSettingValueByKey("time_remaining_method")
output_frequency = int(self.getSettingValueByKey("update_frequency"))
output_percentage = self.getSettingValueByKey("percentage")
line_set = {}
if output_percentage or output_time:
total_time = -1
previous_layer_end_percentage = 0
previous_layer_end_time = 0
for layer in data:
layer_index = data.index(layer)
lines = layer.split("\n")
for line in lines:
if line.startswith(";TIME:") and total_time == -1:
if (line.startswith(";TIME:") or line.startswith(";PRINT.TIME:")) and total_time == -1:
# This line represents the total time required to print the gcode
total_time = self.getTimeValue(line)
line_index = lines.index(line)
# In the beginning we may have 2 M73 lines, but it makes logic less complicated
if output_time:
self.outputTime(lines, line_index, total_time)
self.outputTime(lines, line_index, total_time, output_time_method)
if output_percentage:
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
lines.insert(line_index, "M73 P0")
@ -96,8 +127,34 @@ class DisplayProgressOnLCD(Script):
line_index = lines.index(line)
if output_time:
# Here we calculate remaining time
self.outputTime(lines, line_index, total_time - current_time)
if output_frequency == 0:
# Here we calculate remaining time
self.outputTime(lines, line_index, total_time - current_time, output_time_method)
else:
# Here we calculate remaining time and how many outputs are expected for the layer
layer_time_delta = int(current_time - previous_layer_end_time)
layer_step_delta = int((current_time - previous_layer_end_time) / output_frequency)
# If this layer represents less than 1 step then we don't need to emit anything, continue to the next layer
if layer_step_delta != 0:
# Grab the index of the current line and figure out how many lines represent one second
step = line_index / layer_time_delta
# Move new lines further as we add new lines above it
lines_added = 1
# Run through layer in seconds
for seconds in range(1, layer_time_delta + 1):
# Time from start to decide when to update
line_time = int(previous_layer_end_time + seconds)
# Output every X seconds and after last layer
if line_time % output_frequency == 0 or line_time == total_time:
# Line to add the output
time_line_index = int((seconds * step) + lines_added)
# Insert remaining time into the GCODE
self.outputTime(lines, time_line_index, total_time - line_time, output_time_method)
# Next line will be again lower
lines_added = lines_added + 1
previous_layer_end_time = int(current_time)
if output_percentage:
# Calculate percentage value this layer ends at

View File

@ -13,7 +13,7 @@ from ..PostProcessingPlugin import PostProcessingPlugin
# not sure if needed
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
""" In this file, commnunity refers to regular Cura for makers."""
""" In this file, community refers to regular Cura for makers."""
mock_plugin_registry = MagicMock()
mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path")

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a prepare stage in Cura.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a preview stage in Cura.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -112,7 +112,7 @@ class SimulationPass(RenderPass):
elif isinstance(node, NozzleNode):
nozzle_node = node
nozzle_node.setVisible(False)
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():
disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData())
@ -189,7 +189,6 @@ class SimulationPass(RenderPass):
# 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.setVisible(True)
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())

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides the Simulation view.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
@ -116,6 +116,7 @@ class SliceInfo(QObject, Extension):
machine_manager = self._application.getMachineManager()
print_information = self._application.getPrintInformation()
user_profile = self._application.getCuraAPI().account.userProfile
global_stack = machine_manager.activeMachine
@ -124,6 +125,8 @@ class SliceInfo(QObject, Extension):
data["schema_version"] = 0
data["cura_version"] = self._application.getVersion()
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
data["organization_id"] = user_profile.get("organization_id", None) if user_profile else None
data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else []
active_mode = self._application.getPreferences().getValue("cura/active_mode")
if active_mode == 0:

View File

@ -1,12 +1,17 @@
<html>
<body>
<b>Cura Version:</b> 4.0<br/>
<b>Cura Version:</b> 4.8<br/>
<b>Operating System:</b> Windows 10<br/>
<b>Language:</b> en_US<br/>
<b>Machine Type:</b> Ultimaker S5<br/>
<b>Intent Profile:</b> Default<br/>
<b>Quality Profile:</b> Fast<br/>
<b>Using Custom Settings:</b> No
<b>Using Custom Settings:</b> No<br/>
<b>Organization ID (if any):</b> ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=<br/>
<b>Subscriptions (if any):</b>
<ul>
<li><b>Level:</b> 10, <b>Type:</b> Enterprise, <b>Plan:</b> Basic</li>
</ul>
<h3>Extruder 1:</h3>
<ul>

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a normal solid mesh view.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Creates an eraser mesh to block the printing of support in certain places",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -2,6 +2,6 @@
"name": "Toolbox",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": "7.3.0",
"api": "7.4.0",
"description": "Find, manage and install new Cura packages."
}

View File

@ -42,8 +42,7 @@ UM.Dialog
Row {
id: packageRow
anchors.left: parent.left
anchors.right: parent.right
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width

View File

@ -140,8 +140,7 @@ class CloudPackageChecker(QObject):
sync_message = Message(self._i18n_catalog.i18nc(
"@info:generic",
"Do you want to sync material and software packages with your account?"),
title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ),
lifetime = 0)
title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync",
name = self._i18n_catalog.i18nc("@action:button", "Sync"),
icon = "",

View File

@ -3,5 +3,5 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading model files.",
"api": "7.3.0"
"api": "7.4.0"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["7.3.0"],
"supported_sdk_versions": ["7.4.0"],
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for writing Ultimaker Format Packages.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker networked printers.",
"version": "2.0.0",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 KiB

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -60,7 +60,7 @@ Item
Label
{
id: buildplateLabel
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: buildplate ? buildplate : ""

View File

@ -97,7 +97,7 @@ Item
height: width // TODO: Theme!
sourceSize.width: width // TODO: Theme!
sourceSize.height: width // TODO: Theme!
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
source: UM.Theme.getIcon("arrow_left")
}
}
@ -176,7 +176,7 @@ Item
height: width // TODO: Theme!
sourceSize.width: width // TODO: Theme!
sourceSize.height: width // TODO: Theme!
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
source: UM.Theme.getIcon("arrow_right")
}
}

View File

@ -18,7 +18,7 @@ Button
width: base.width
}
contentItem: Label {
color: enabled ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
color: enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled")
font.pixelSize: 32 * screenScaleFactor
horizontalAlignment: Text.AlignHCenter
text: base.text

View File

@ -57,7 +57,7 @@ Item
{
id: materialLabel
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: ""
@ -87,7 +87,7 @@ Item
{
id: printCoreLabel
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold") // 12pt, bold
text: ""

View File

@ -39,7 +39,7 @@ Item
{
id: positionLabel
font: UM.Theme.getFont("small")
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
height: Math.round(size / 2)
horizontalAlignment: Text.AlignHCenter
text: position + 1

View File

@ -58,7 +58,7 @@ Item
Label
{
text: printJob && printJob.name ? printJob.name : ""
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
visible: printJob
@ -89,7 +89,7 @@ Item
Label
{
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
visible: printJob
@ -120,7 +120,7 @@ Item
{
id: printerAssignmentLabel
anchors.verticalCenter: parent.verticalCenter
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
text: {
@ -188,7 +188,7 @@ Item
Label {
text: printJob && printJob.owner ? printJob.owner : ""
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.top: printerConfiguration.top

View File

@ -44,7 +44,7 @@ Item
verticalCenter: parent.verticalCenter
}
text: printJob ? Math.round(printJob.progress * 100) + "%" : "0%"
color: printJob && printJob.isActive ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
color: printJob && printJob.isActive ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled")
width: contentWidth
font: UM.Theme.getFont("default") // 12pt, regular
@ -62,7 +62,7 @@ Item
leftMargin: UM.Theme.getSize("monitor_margin").width
verticalCenter: parent.verticalCenter
}
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text:
{

View File

@ -103,7 +103,7 @@ Item
Label
{
text: printer && printer.name ? printer.name : ""
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
font: UM.Theme.getFont("large") // 16pt, bold
width: parent.width
@ -130,7 +130,7 @@ Item
anchors
{
top: printerNameLabel.bottom
topMargin: 6 * screenScaleFactor // TODO: Theme!
topMargin: UM.Theme.getSize("narrow_margin").height
left: printerNameLabel.left
}
text: printer ? printer.type : ""
@ -140,7 +140,7 @@ Item
id: managePrinterLink
anchors {
top: printerFamilyPill.bottom
topMargin: 6 * screenScaleFactor
topMargin: UM.Theme.getSize("narrow_margin").height
}
height: 18 * screenScaleFactor // TODO: Theme!
width: childrenRect.width
@ -160,7 +160,7 @@ Item
anchors
{
left: managePrinterText.right
leftMargin: 6 * screenScaleFactor
leftMargin: UM.Theme.getSize("narrow_margin").width
verticalCenter: managePrinterText.verticalCenter
}
color: UM.Theme.getColor("text_link")
@ -194,8 +194,7 @@ Item
var configs = []
if (printer)
{
configs.push(printer.printerConfiguration.extruderConfigurations[0])
configs.push(printer.printerConfiguration.extruderConfigurations[1])
configs = configs.concat(printer.printerConfiguration.extruderConfigurations)
}
else
{
@ -272,7 +271,8 @@ Item
}
// For cloud printing, add this mouse area over the disabled cameraButton to indicate that it's not available
MouseArea
//Warning message is commented out because it's factually incorrect. Fix CURA-7637 to allow camera connections via cloud.
/* MouseArea
{
id: cameraDisabledButtonArea
anchors.fill: cameraButton
@ -282,7 +282,7 @@ Item
enabled: !cameraButton.enabled
}
/* //Warning message is commented out because it's factually incorrect. Fix CURA-7637 to allow camera connections via cloud.
MonitorInfoBlurb
{
id: cameraDisabledInfo
@ -341,24 +341,33 @@ Item
{
verticalCenter: parent.verticalCenter
}
color: printer ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
color: printer ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled")
font: UM.Theme.getFont("large_bold") // 16pt, bold
text: {
if (!printer) {
return catalog.i18nc("@label:status", "Loading...")
}
if (printer && printer.state == "disabled")
if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Unavailable")
}
if (printer && printer.state == "unreachable")
if (printer.state == "unreachable")
{
return catalog.i18nc("@label:status", "Unreachable")
}
if (printer && !printer.activePrintJob && printer.state == "idle")
if (!printer.activePrintJob && printer.state == "idle")
{
return catalog.i18nc("@label:status", "Idle")
}
if (!printer.activePrintJob && printer.state == "pre_print")
{
return catalog.i18nc("@label:status", "Preparing...")
}
if (!printer.activePrintJob && printer.state == "printing")
{
// The print job isn't quite updated yet.
return catalog.i18nc("@label:status", "Printing")
}
return ""
}
visible: text !== ""
@ -395,7 +404,7 @@ Item
Label
{
id: printerJobNameLabel
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled")
elide: Text.ElideRight
font: UM.Theme.getFont("large") // 16pt, bold
text: printer && printer.activePrintJob ? printer.activePrintJob.name : catalog.i18nc("@label", "Untitled")
@ -413,10 +422,10 @@ Item
anchors
{
top: printerJobNameLabel.bottom
topMargin: 6 * screenScaleFactor // TODO: Theme!
topMargin: UM.Theme.getSize("narrow_margin").height
left: printerJobNameLabel.left
}
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled")
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: printer && printer.activePrintJob ? printer.activePrintJob.owner : catalog.i18nc("@label", "Anonymous")
@ -448,7 +457,7 @@ Item
font: UM.Theme.getFont("default")
text: catalog.i18nc("@label:status", "Requires configuration changes")
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
@ -484,7 +493,7 @@ Item
anchors.bottomMargin: 2 * screenScaleFactor // TODO: Theme!
color: UM.Theme.getColor("monitor_secondary_button_text")
font: UM.Theme.getFont("medium") // 14pt, regular
text: catalog.i18nc("@action:button","Details");
text: catalog.i18nc("@action:button", "Details");
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
height: 18 * screenScaleFactor // TODO: Theme!

View File

@ -27,7 +27,7 @@ Item
Label {
id: printerNameLabel
anchors.centerIn: parent
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
text: monitorPrinterPill.text
font.pointSize: 10 // TODO: Theme!
visible: monitorPrinterPill.text !== ""

View File

@ -26,7 +26,7 @@ Item
left: queuedPrintJobs.left
top: parent.top
}
color: UM.Theme.getColor("monitor_text_primary")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
text: catalog.i18nc("@label", "Queued")
renderType: Text.NativeRendering
@ -58,7 +58,7 @@ Item
anchors
{
left: externalLinkIcon.right
leftMargin: 6 * screenScaleFactor // TODO: Theme!
leftMargin: UM.Theme.getSize("narrow_margin").width
verticalCenter: externalLinkIcon.verticalCenter
}
color: UM.Theme.getColor("text_link")
@ -88,7 +88,7 @@ Item
anchors
{
left: queuedPrintJobs.left
leftMargin: 6 * screenScaleFactor // TODO: Theme!
leftMargin: UM.Theme.getSize("narrow_margin").width
top: queuedLabel.bottom
topMargin: 24 * screenScaleFactor // TODO: Theme!
}
@ -97,14 +97,10 @@ Item
Label
{
text: catalog.i18nc("@label", "There are no print jobs in the queue. Slice and send a job to add one.")
color: UM.Theme.getColor("monitor_text_primary")
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium")
anchors.verticalCenter: parent.verticalCenter
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
visible: printJobList.count === 0
}
@ -112,15 +108,11 @@ Item
Label
{
text: catalog.i18nc("@label", "Print jobs")
color: UM.Theme.getColor("monitor_text_primary")
elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
width: 284 * screenScaleFactor // TODO: Theme! (Should match column size)
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
visible: printJobList.count > 0
}
@ -128,15 +120,11 @@ Item
Label
{
text: catalog.i18nc("@label", "Total print time")
color: UM.Theme.getColor("monitor_text_primary")
elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
width: UM.Theme.getSize("monitor_column").width
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
visible: printJobList.count > 0
}
@ -144,15 +132,11 @@ Item
Label
{
text: catalog.i18nc("@label", "Waiting for")
color: UM.Theme.getColor("monitor_text_primary")
elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
width: UM.Theme.getSize("monitor_column").width
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
visible: printJobList.count > 0
}

View File

@ -12,7 +12,7 @@ Button {
color: UM.Theme.getColor("monitor_context_menu_hover")
}
contentItem: Label {
color: enabled ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled");
color: enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_text_disabled");
text: parent.text
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignVCenter;

View File

@ -127,12 +127,16 @@ class CloudApiClient:
# \param cluster_id: The ID of the cluster.
# \param job_id: The ID of the print job.
# \param on_finished: The function to be called after the result is parsed.
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
# \param on_error: A function to be called if there was a server-side problem uploading. Generic errors (not
# specific to sending print jobs) such as lost connection, unparsable responses, etc. are not returned here, but
# handled in a generic way by the CloudApiClient.
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
self._http.post(url,
scope = self._scope,
data = b"",
callback = self._parseCallback(on_finished, CloudPrintResponse),
error_callback = on_error,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,

View File

@ -3,10 +3,11 @@
from time import time
import os
from typing import List, Optional, cast
from typing import cast, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest # Parse errors specific to print job uploading.
from UM import i18nCatalog
from UM.Backend.Backend import BackendState
@ -23,6 +24,7 @@ from ..ExportFileJob import ExportFileJob
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
from ..Messages.PrintJobUploadQueueFullMessage import PrintJobUploadQueueFullMessage
from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
@ -194,7 +196,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
# The mesh didn't change, let's not upload it to the cloud again.
# Note that self.writeFinished is called in _onPrintUploadCompleted as well.
if self._uploaded_print_job:
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError)
return
# Export the scene to the correct file type.
@ -240,7 +242,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
if not print_job: # It's possible that another print job is requested in the meanwhile, which then fails to upload with an error, which sets self._uploaded_print_job to `None`.
# TODO: Maybe _onUploadError shouldn't set the _uploaded_print_job to None or we need to prevent such asynchronous cases.
return # Prevent a crash.
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted, self._onPrintUploadSpecificError)
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
"""Shows a message when the upload has succeeded
@ -251,9 +253,23 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
PrintJobUploadSuccessMessage().show()
self.writeFinished.emit()
def _onUploadError(self, message: str = None) -> None:
"""Displays the given message if uploading the mesh has failed
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"):
"""
Displays a message when an error occurs specific to uploading print job (i.e. queue is full).
"""
error_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if error_code == 409:
PrintJobUploadQueueFullMessage().show()
else:
PrintJobUploadErrorMessage(I18N_CATALOG.i18nc("@error:send", "Unknown error code when uploading print job: {0}", error_code)).show()
self._progress.hide()
self._uploaded_print_job = None
self.writeError.emit()
def _onUploadError(self, message: str = None) -> None:
"""
Displays the given message if uploading the mesh has failed due to a generic error (i.e. lost connection).
:param message: The message to display.
"""
self._progress.hide()

View File

@ -108,7 +108,11 @@ class ToolPathUploader:
Logger.log("i", "Finished callback %s %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) # type: int
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) # type: Optional[int]
if not status_code:
Logger.log("e", "Reply contained no status code.")
self._errorCallback(reply, None)
return
# check if we should retry the last chunk
if self._retries < self.MAX_RETRIES and status_code in self.RETRY_HTTP_CODES:

View File

@ -0,0 +1,19 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM import i18nCatalog
from UM.Message import Message
I18N_CATALOG = i18nCatalog("cura")
class PrintJobUploadQueueFullMessage(Message):
"""Message shown when uploading a print job to a cluster and the print queue is full."""
def __init__(self) -> None:
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Print job queue is full. The printer can't accept a new job."),
title = I18N_CATALOG.i18nc("@info:title", "Queue Full"),
lifetime = 10
)

View File

@ -71,12 +71,7 @@ class ClusterPrinterStatus(BaseModel):
:param controller: - The controller of the model.
"""
# FIXME
# Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the
# amount of extruders coming back from the API is actually lower (which it can be if a printer was just added
# to a cluster). This should be fixed in the future, probably also on the cluster API side.
model = PrinterOutputModel(controller, 2, firmware_version = self.firmware_version)
model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version)
self.updateOutputModel(model)
return model

View File

@ -81,6 +81,8 @@ class SendMaterialJob(Job):
container_registry = CuraApplication.getInstance().getContainerRegistry()
all_materials = container_registry.findInstanceContainersMetadata(type = "material")
all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material).
if "empty_material" in all_base_files:
all_base_files.remove("empty_material") # Don't send the empty material.
for root_material_id in all_base_files:
if root_material_id not in materials_to_send:

View File

@ -85,6 +85,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
self._timeout_time = 30
self._num_is_host_check_failed = 0
@pyqtProperty(str, constant=True)
def address(self) -> str:
"""The IP address of the printer."""
@ -293,8 +295,16 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice):
def _checkIfClusterHost(self):
"""Check is this device is a cluster host and takes the needed actions when it is not."""
if len(self._printers) < 1 and self.isConnected():
self._num_is_host_check_failed += 1
else:
self._num_is_host_check_failed = 0
# Since we request the status of the cluster itself way less frequent in the cloud, it can happen that a cloud
# printer reports having 0 printers (since they are offline!) but we haven't asked if the entire cluster is
# offline. (See CURA-7360)
# So by just counting a number of subsequent times that this has happened fixes the incorrect display.
if self._num_is_host_check_failed >= 6:
NotClusterHostMessage(self).show()
self.close()
CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(self.key)

View File

@ -62,7 +62,6 @@ class AutoDetectBaudJob(Job):
except ValueError:
continue
sleep(wait_bootloader) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
successful_responses = 0
serial.write(b"\n") # Ensure we clear out previous responses
serial.write(b"M105\n")
@ -73,13 +72,11 @@ class AutoDetectBaudJob(Job):
while timeout_time > time():
line = serial.readline()
if b"ok" in line and b"T:" in line:
successful_responses += 1
if successful_responses >= 1:
self.setResult(baud_rate)
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(
serial = self._serial_port, baud_rate = baud_rate, retry = retry, time_elapsed = time() - start_timeout_time))
serial.close() # close serial port so it can be opened by the USBPrinterOutputDevice
return
self.setResult(baud_rate)
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(
serial = self._serial_port, baud_rate = baud_rate, retry = retry, time_elapsed = time() - start_timeout_time))
serial.close() # close serial port so it can be opened by the USBPrinterOutputDevice
return
serial.write(b"M105\n")
sleep(15) # Give the printer some time to init and try again.

View File

@ -88,8 +88,12 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._firmware_name_requested = False
self._firmware_updater = AvrFirmwareUpdater(self)
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("USBPrinting"))
self._monitor_view_qml_path = os.path.join(plugin_path, "MonitorItem.qml")
plugin_path = PluginRegistry.getInstance().getPluginPath("USBPrinting")
if plugin_path:
self._monitor_view_qml_path = os.path.join(plugin_path, "MonitorItem.qml")
else:
Logger.log("e", "Cannot create Monitor QML view: cannot find plugin path for plugin [USBPrinting]")
self._monitor_view_qml_path = ""
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)

View File

@ -2,7 +2,7 @@
"name": "USB printing",
"author": "Ultimaker B.V.",
"version": "1.0.2",
"api": "7.3.0",
"api": "7.4.0",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"api": "7.3.0",
"api": "7.4.0",
"i18n-catalog": "cura"
}

Some files were not shown because too many files have changed in this diff Show More