Merge branch 'master' into feature_unify_pause_at_height

# Conflicts:
#	plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
This commit is contained in:
fieldOfView 2020-05-14 08:45:38 +02:00
commit 172e6a0759
2596 changed files with 344277 additions and 557779 deletions

1
.gitignore vendored
View file

@ -53,6 +53,7 @@ plugins/GodMode
plugins/OctoPrintPlugin
plugins/ProfileFlattener
plugins/SettingsGuide
plugins/SettingsGuide2
plugins/SVGToolpathReader
plugins/X3GWriter

View file

@ -1,9 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, TYPE_CHECKING
from datetime import datetime
from typing import Optional, Dict, TYPE_CHECKING, Union
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationService import AuthorizationService
@ -16,6 +18,13 @@ if TYPE_CHECKING:
i18n_catalog = i18nCatalog("cura")
class SyncState:
"""QML: Cura.AccountSyncState"""
SYNCING = 0
SUCCESS = 1
ERROR = 2
## The account API provides a version-proof bridge to use Ultimaker Accounts
#
# Usage:
@ -26,16 +35,31 @@ i18n_catalog = i18nCatalog("cura")
# api.account.userProfile # Who is logged in``
#
class Account(QObject):
# The interval in which sync services are automatically triggered
SYNC_INTERVAL = 30.0 # seconds
Q_ENUMS(SyncState)
# Signal emitted when user logged in or out.
loginStateChanged = pyqtSignal(bool)
accessTokenChanged = pyqtSignal()
syncRequested = pyqtSignal()
"""Sync services may connect to this signal to receive sync triggers.
Services should be resilient to receiving a signal while they are still syncing,
either by ignoring subsequent signals or restarting a sync.
See setSyncState() for providing user feedback on the state of your service.
"""
lastSyncDateTimeChanged = pyqtSignal()
syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum
def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent)
self._application = application
self._new_cloud_printers_detected = False
self._error_message = None # type: Optional[Message]
self._logged_in = False
self._sync_state = SyncState.SUCCESS
self._last_sync_str = "-"
self._callback_port = 32118
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
@ -55,6 +79,16 @@ class Account(QObject):
self._authorization_service = AuthorizationService(self._oauth_settings)
# Create a timer for automatic account sync
self._update_timer = QTimer()
self._update_timer.setInterval(int(self.SYNC_INTERVAL * 1000))
# The timer is restarted explicitly after an update was processed. This prevents 2 concurrent updates
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self.syncRequested)
self._sync_services = {} # type: Dict[str, int]
"""contains entries "service_name" : SyncState"""
def initialize(self) -> None:
self._authorization_service.initialize(self._application.getPreferences())
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
@ -62,6 +96,39 @@ class Account(QObject):
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
self._authorization_service.loadAuthDataFromPreferences()
def setSyncState(self, service_name: str, state: int) -> None:
""" Can be used to register sync services and update account sync states
Contract: A sync service is expected exit syncing state in all cases, within reasonable time
Example: `setSyncState("PluginSyncService", SyncState.SYNCING)`
:param service_name: A unique name for your service, such as `plugins` or `backups`
:param state: One of SyncState
"""
prev_state = self._sync_state
self._sync_services[service_name] = state
if any(val == SyncState.SYNCING for val in self._sync_services.values()):
self._sync_state = SyncState.SYNCING
elif any(val == SyncState.ERROR for val in self._sync_services.values()):
self._sync_state = SyncState.ERROR
else:
self._sync_state = SyncState.SUCCESS
if self._sync_state != prev_state:
self.syncStateChanged.emit(self._sync_state)
if self._sync_state == SyncState.SUCCESS:
self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M")
self.lastSyncDateTimeChanged.emit()
if self._sync_state != SyncState.SYNCING:
# schedule new auto update after syncing completed (for whatever reason)
if not self._update_timer.isActive():
self._update_timer.start()
def _onAccessTokenChanged(self):
self.accessTokenChanged.emit()
@ -82,18 +149,37 @@ class Account(QObject):
self._error_message.show()
self._logged_in = False
self.loginStateChanged.emit(False)
if self._update_timer.isActive():
self._update_timer.stop()
return
if self._logged_in != logged_in:
self._logged_in = logged_in
self.loginStateChanged.emit(logged_in)
if logged_in:
self.sync()
else:
if self._update_timer.isActive():
self._update_timer.stop()
@pyqtSlot()
def login(self) -> None:
@pyqtSlot(bool)
def login(self, force_logout_before_login: bool = False) -> None:
"""
Initializes the login process. If the user is logged in already and force_logout_before_login is true, Cura will
logout from the account before initiating the authorization flow. If the user is logged in and
force_logout_before_login is false, the function will return, as there is nothing to do.
:param force_logout_before_login: Optional boolean parameter
:return: None
"""
if self._logged_in:
if force_logout_before_login:
self.logout()
else:
# Nothing to do, user already logged in.
return
self._authorization_service.startAuthorizationFlow()
self._authorization_service.startAuthorizationFlow(force_logout_before_login)
@pyqtProperty(str, notify=loginStateChanged)
def userName(self):
@ -122,6 +208,25 @@ class Account(QObject):
return None
return user_profile.__dict__
@pyqtProperty(str, notify=lastSyncDateTimeChanged)
def lastSyncDateTime(self) -> str:
return self._last_sync_str
@pyqtSlot()
def sync(self) -> None:
"""Signals all sync services to start syncing
This can be considered a forced sync: even when a
sync is currently running, a sync will be requested.
"""
if self._update_timer.isActive():
self._update_timer.stop()
elif self._sync_state == SyncState.SYNCING:
Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
self.syncRequested.emit()
@pyqtSlot()
def logout(self) -> None:
if not self._logged_in:

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.1.0"
CuraSDKVersion = "7.2.0"
try:
from cura.CuraVersion import CuraAppName # type: ignore

View file

@ -1,6 +1,6 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional
from typing import Optional
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
@ -16,18 +16,17 @@ from collections import namedtuple
import numpy
import copy
## Return object for bestSpot
LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
## The Arrange classed is used together with ShapeArray. Use it to find
# good locations for objects that you try to put on a build place.
# Different priority schemes can be defined so it alters the behavior while using
# the same logic.
#
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
class Arrange:
"""
The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put
on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic.
Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
"""
build_volume = None # type: Optional[BuildVolume]
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
@ -42,14 +41,21 @@ class Arrange:
self._last_priority = 0
self._is_empty = True
## Helper to create an Arranger instance
#
# Either fill in scene_root and create will find all sliceable nodes by itself,
# or use fixed_nodes to provide the nodes yourself.
# \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed
@classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8):
"""
Helper to create an Arranger instance
Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the
nodes yourself.
:param scene_root: Root for finding all scene nodes
:param fixed_nodes: Scene nodes to be placed
:param scale:
:param x:
:param y:
:param min_offset:
:return:
"""
arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
arranger.centerFirst()
@ -88,12 +94,15 @@ class Arrange:
def resetLastPriority(self):
self._last_priority = 0
## Find placement for a node (using offset shape) and place it (using hull shape)
# return the nodes that should be placed
# \param node
# \param offset_shape_arr ShapeArray with offset, for placing the shape
# \param hull_shape_arr ShapeArray without offset, used to find location
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1):
"""
Find placement for a node (using offset shape) and place it (using hull shape)
:param node:
:param offset_shape_arr: hapeArray with offset, for placing the shape
:param hull_shape_arr: ShapeArray without offset, used to find location
:param step:
:return: the nodes that should be placed
"""
best_spot = self.bestSpot(
hull_shape_arr, start_prio = self._last_priority, step = step)
x, y = best_spot.x, best_spot.y
@ -119,29 +128,35 @@ class Arrange:
node.setPosition(Vector(200, center_y, 100))
return found_spot
## Fill priority, center is best. Lower value is better
# This is a strategy for the arranger.
def centerFirst(self):
"""
Fill priority, center is best. Lower value is better.
:return:
"""
# Square distance: creates a more round shape
self._priority = numpy.fromfunction(
lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort()
## Fill priority, back is best. Lower value is better
# This is a strategy for the arranger.
def backFirst(self):
"""
Fill priority, back is best. Lower value is better
:return:
"""
self._priority = numpy.fromfunction(
lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
self._priority_unique_values = numpy.unique(self._priority)
self._priority_unique_values.sort()
## Return the amount of "penalty points" for polygon, which is the sum of priority
# None if occupied
# \param x x-coordinate to check shape
# \param y y-coordinate
# \param shape_arr the ShapeArray object to place
def checkShape(self, x, y, shape_arr):
"""
Return the amount of "penalty points" for polygon, which is the sum of priority
:param x: x-coordinate to check shape
:param y:
:param shape_arr: the ShapeArray object to place
:return: None if occupied
"""
x = int(self._scale * x)
y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x
@ -165,12 +180,14 @@ class Arrange:
offset_x:offset_x + shape_arr.arr.shape[1]]
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
## Find "best" spot for ShapeArray
# Return namedtuple with properties x, y, penalty_points, priority.
# \param shape_arr ShapeArray
# \param start_prio Start with this priority value (and skip the ones before)
# \param step Slicing value, higher = more skips = faster but less accurate
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
"""
Find "best" spot for ShapeArray
:param shape_arr:
:param start_prio: Start with this priority value (and skip the ones before)
:param step: Slicing value, higher = more skips = faster but less accurate
:return: namedtuple with properties x, y, penalty_points, priority.
"""
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
if start_idx_list:
try:
@ -179,6 +196,7 @@ class Arrange:
start_idx = 0
else:
start_idx = 0
priority = 0
for priority in self._priority_unique_values[start_idx::step]:
tryout_idx = numpy.where(self._priority == priority)
for idx in range(len(tryout_idx[0])):
@ -192,13 +210,16 @@ class Arrange:
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
## Place the object.
# Marks the locations in self._occupied and self._priority
# \param x x-coordinate
# \param y y-coordinate
# \param shape_arr ShapeArray object
# \param update_empty updates the _is_empty, used when adding disallowed areas
def place(self, x, y, shape_arr, update_empty = True):
"""
Place the object.
Marks the locations in self._occupied and self._priority
:param x:
:param y:
:param shape_arr:
:param update_empty: updates the _is_empty, used when adding disallowed areas
:return:
"""
x = int(self._scale * x)
y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy
@ -274,7 +274,9 @@ class BuildVolume(SceneNode):
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
node.setOutsideBuildArea(True)
continue
except IndexError:
except IndexError: # Happens when the extruder list is too short. We're not done building the printer in memory yet.
continue
except TypeError: # Happens when extruder_position is None. This object has no extruder decoration.
continue
node.setOutsideBuildArea(False)
@ -1084,14 +1086,19 @@ class BuildVolume(SceneNode):
def _calculateMoveFromWallRadius(self, used_extruders):
move_from_wall_radius = 0 # Moves that start from outer wall.
all_values = [move_from_wall_radius]
all_values.extend(self._getSettingFromAllExtruders("infill_wipe_dist"))
move_from_wall_radius = max(all_values)
avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts", "value") for stack in used_extruders]
travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): # For each extruder (or just global).
if avoid_other_parts_enabled:
move_from_wall_radius = max(move_from_wall_radius, avoid_distance)
for stack in used_extruders:
if stack.getProperty("travel_avoid_other_parts", "value"):
move_from_wall_radius = max(move_from_wall_radius, stack.getProperty("travel_avoid_distance", "value"))
infill_wipe_distance = stack.getProperty("infill_wipe_dist", "value")
num_walls = stack.getProperty("wall_line_count", "value")
if num_walls >= 1: # Infill wipes start from the infill, so subtract the total wall thickness from this.
infill_wipe_distance -= stack.getProperty("wall_line_width_0", "value")
if num_walls >= 2:
infill_wipe_distance -= stack.getProperty("wall_line_width_x", "value") * (num_walls - 1)
move_from_wall_radius = max(move_from_wall_radius, infill_wipe_distance)
return move_from_wall_radius
## Calculate the disallowed radius around the edge.
@ -1128,10 +1135,10 @@ class BuildVolume(SceneNode):
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings

View file

@ -4,7 +4,7 @@
import os
import sys
import time
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
import numpy
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
@ -48,6 +48,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.API import CuraAPI
from cura.API.Account import Account
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
@ -56,6 +57,7 @@ from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
from cura.Machines.Models.DiscoveredCloudPrintersModel import DiscoveredCloudPrintersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
@ -124,7 +126,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings.
SettingVersion = 11
SettingVersion = 15
Created = False
@ -201,6 +203,7 @@ class CuraApplication(QtApplication):
self._quality_management_model = None
self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
self._discovered_cloud_printers_model = DiscoveredCloudPrintersModel(self, parent = self)
self._first_start_machine_actions_model = None
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
@ -451,7 +454,10 @@ class CuraApplication(QtApplication):
super().startSplashWindowPhase()
if not self.getIsHeadLess():
try:
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
except FileNotFoundError:
Logger.log("w", "Unable to find the window icon.")
self.setRequiredPlugins([
# Misc.:
@ -886,6 +892,10 @@ class CuraApplication(QtApplication):
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
return self._discovered_printer_model
@pyqtSlot(result=QObject)
def getDiscoveredCloudPrintersModel(self, *args) -> "DiscoveredCloudPrintersModel":
return self._discovered_cloud_printers_model
@pyqtSlot(result = QObject)
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
if self._first_start_machine_actions_model is None:
@ -1084,6 +1094,7 @@ class CuraApplication(QtApplication):
self.processEvents()
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
qmlRegisterType(DiscoveredCloudPrintersModel, "Cura", 1, 7, "DiscoveredCloudPrintersModel")
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
@ -1106,6 +1117,7 @@ class CuraApplication(QtApplication):
from cura.API import CuraAPI
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
qmlRegisterUncreatableType(Account, "Cura", 1, 0, "AccountSyncState", "Could not create AccountSyncState")
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@ -1382,12 +1394,21 @@ class CuraApplication(QtApplication):
if not nodes:
return
objects_in_filename = {} # type: Dict[str, List[CuraSceneNode]]
for node in nodes:
mesh_data = node.getMeshData()
if mesh_data:
file_name = mesh_data.getFileName()
if file_name:
if file_name not in objects_in_filename:
objects_in_filename[file_name] = []
if file_name in objects_in_filename:
objects_in_filename[file_name].append(node)
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
for file_name, nodes in objects_in_filename.items():
for node in nodes:
job = ReadMeshJob(file_name)
job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished)
@ -1395,8 +1416,6 @@ class CuraApplication(QtApplication):
job.finished.connect(self.updateOriginOfMergedMeshes)
job.start()
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
@pyqtSlot("QStringList")
def setExpandedCategories(self, categories: List[str]) -> None:
@ -1572,13 +1591,30 @@ class CuraApplication(QtApplication):
fileLoaded = pyqtSignal(str)
fileCompleted = pyqtSignal(str)
def _reloadMeshFinished(self, job):
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
job_result = job.getResult()
def _reloadMeshFinished(self, job) -> None:
"""
Function called whenever a ReadMeshJob finishes in the background. It reloads a specific node object in the
scene from its source file. The function gets all the nodes that exist in the file through the job result, and
then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
:param job: The ReadMeshJob running in the background that reads all the meshes in a file
:return: None
"""
job_result = job.getResult() # nodes that exist inside the file read by this job
if len(job_result) == 0:
Logger.log("e", "Reloading the mesh failed.")
return
mesh_data = job_result[0].getMeshData()
object_found = False
mesh_data = None
# Find the node to be refreshed based on its id
for job_result_node in job_result:
if job_result_node.getId() == job._node.getId():
mesh_data = job_result_node.getMeshData()
object_found = True
break
if not object_found:
Logger.warning("The object with id {} no longer exists! Keeping the old version in the scene.".format(job_result_node.getId()))
return
if not mesh_data:
Logger.log("w", "Could not find a mesh in reloaded node.")
return
@ -1701,6 +1737,9 @@ class CuraApplication(QtApplication):
if not global_container_stack:
Logger.log("w", "Can't load meshes before a printer is added.")
return
if not self._volume:
Logger.log("w", "Can't load meshes before the build volume is initialized")
return
nodes = job.getResult()
file_name = job.getFileName()
@ -1774,7 +1813,7 @@ class CuraApplication(QtApplication):
# 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()
title = self._i18n_catalog.i18nc("@info:title", "Warning")).show()
return
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher

View file

@ -171,6 +171,10 @@ class MachineNode(ContainerNode):
if variant_name not in self.variants:
self.variants[variant_name] = VariantNode(variant["id"], machine = self)
self.variants[variant_name].materialsChanged.connect(self.materialsChanged)
else:
# Force reloading the materials if the variant already exists or else materals won't be loaded
# when the G-Code flavor changes --> CURA-7354
self.variants[variant_name]._loadAll()
if not self.variants:
self.variants["empty"] = VariantNode("empty_variant", machine = self)

View file

@ -0,0 +1,71 @@
from typing import Optional, TYPE_CHECKING, List, Dict
from PyQt5.QtCore import QObject, pyqtSlot, Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
class DiscoveredCloudPrintersModel(ListModel):
"""
Model used to inform the application about newly added cloud printers, which are discovered from the user's account
"""
DeviceKeyRole = Qt.UserRole + 1
DeviceNameRole = Qt.UserRole + 2
DeviceTypeRole = Qt.UserRole + 3
DeviceFirmwareVersionRole = Qt.UserRole + 4
cloudPrintersDetectedChanged = pyqtSignal(bool)
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self.addRoleName(self.DeviceKeyRole, "key")
self.addRoleName(self.DeviceNameRole, "name")
self.addRoleName(self.DeviceTypeRole, "machine_type")
self.addRoleName(self.DeviceFirmwareVersionRole, "firmware_version")
self._discovered_cloud_printers_list = [] # type: List[Dict[str, str]]
self._application = application # type: CuraApplication
def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None:
"""
Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel.
:param new_devices: List of dictionaries which contain information about added cloud printers. Example:
{
"key": "YjW8pwGYcaUvaa0YgVyWeFkX3z",
"name": "NG 001",
"machine_type": "Ultimaker S5",
"firmware_version": "5.5.12.202001"
}
:return: None
"""
self._discovered_cloud_printers_list.extend(new_devices)
self._update()
# Inform whether new cloud printers have been detected. If they have, the welcome wizard can close.
self.cloudPrintersDetectedChanged.emit(len(new_devices) > 0)
@pyqtSlot()
def clear(self) -> None:
"""
Clears the contents of the DiscoveredCloudPrintersModel.
:return: None
"""
self._discovered_cloud_printers_list = []
self._update()
self.cloudPrintersDetectedChanged.emit(False)
def _update(self) -> None:
"""
Sorts the newly discovered cloud printers by name and then updates the ListModel.
:return: None
"""
items = self._discovered_cloud_printers_list[:]
items.sort(key = lambda k: k["name"])
self.setItems(items)

View file

@ -72,8 +72,6 @@ class DiscoveredPrinter(QObject):
# Human readable machine type string
@pyqtProperty(str, notify = machineTypeChanged)
def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication
machine_manager = CuraApplication.getInstance().getMachineManager()
# In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
# like "Ultimaker 3". The code below handles this case.

View file

@ -4,13 +4,12 @@
import collections
from PyQt5.QtCore import Qt, QTimer
from typing import TYPE_CHECKING, Optional, Dict
from cura.Machines.Models.IntentTranslations import intent_translations
from cura.Machines.Models.IntentModel import IntentModel
from cura.Settings.IntentManager import IntentManager
from UM.Qt.ListModel import ListModel
from UM.Settings.ContainerRegistry import ContainerRegistry #To update the list if anything changes.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
from PyQt5.QtCore import pyqtSignal
import cura.CuraApplication
if TYPE_CHECKING:
from UM.Settings.ContainerRegistry import ContainerInterface

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 os
@ -7,6 +7,7 @@ from collections import OrderedDict
from PyQt5.QtCore import pyqtSlot, Qt
from UM.Application import Application
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Settings.SettingFunction import SettingFunction
@ -83,14 +84,18 @@ class UserChangesModel(ListModel):
# Find the category of the instance by moving up until we find a category.
category = user_changes.getInstance(setting_key).definition
while category.type != "category":
while category is not None and category.type != "category":
category = category.parent
# Handle translation (and fallback if we weren't able to find any translation files.
if category is not None:
if self._i18n_catalog:
category_label = self._i18n_catalog.i18nc(category.key + " label", category.label)
else:
category_label = category.label
else: # Setting is not in any category. Shouldn't happen, but it do. See https://sentry.io/share/issue/d735884370154166bc846904d9b812ff/
Logger.error("Setting {key} is not in any setting category.".format(key = setting_key))
category_label = ""
if self._i18n_catalog:
label = self._i18n_catalog.i18nc(setting_key + " label", stack.getProperty(setting_key, "label"))

View file

@ -3,8 +3,6 @@
from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.Util import parseBool

View file

@ -3,29 +3,28 @@
import json
from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode
from typing import Optional, TYPE_CHECKING, Dict
from urllib.parse import urlencode, quote_plus
import requests.exceptions
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from cura.OAuth2.Models import AuthenticationResponse
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from cura.OAuth2.Models import UserProfile, OAuth2Settings
from UM.Preferences import Preferences
MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff"
## The authorization service is responsible for handling the login flow,
# storing user credentials and providing account information.
@ -144,7 +143,7 @@ class AuthorizationService:
self.onAuthStateChanged.emit(logged_in = False)
## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self) -> None:
def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None:
Logger.log("d", "Starting new OAuth2 flow...")
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
@ -155,8 +154,8 @@ class AuthorizationService:
state = AuthorizationHelpers.generateVerificationCode()
# Create the query string needed for the OAuth2 flow.
query_string = urlencode({
# Create the query dict needed for the OAuth2 flow.
query_parameters_dict = {
"client_id": self._settings.CLIENT_ID,
"redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES,
@ -164,13 +163,39 @@ class AuthorizationService:
"state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
"code_challenge": challenge_code,
"code_challenge_method": "S512"
})
# Open the authorization page in a new browser window.
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
}
# Start a local web server to receive the callback URL on.
try:
self._server.start(verification_code, state)
except OSError:
Logger.logException("w", "Unable to create authorization request server")
Message(i18n_catalog.i18nc("@info", "Unable to start a new sign in process. Check if another sign in attempt is still active."),
title=i18n_catalog.i18nc("@info:title", "Warning")).show()
return
auth_url = self._generate_auth_url(query_parameters_dict, force_browser_logout)
# Open the authorization page in a new browser window.
QDesktopServices.openUrl(QUrl(auth_url))
def _generate_auth_url(self, query_parameters_dict: Dict[str, Optional[str]], force_browser_logout: bool) -> str:
"""
Generates the authentications url based on the original auth_url and the query_parameters_dict to be included.
If there is a request to force logging out of mycloud in the browser, the link to logoff from mycloud is
prepended in order to force the browser to logoff from mycloud and then redirect to the authentication url to
login again. This case is used to sync the accounts between Cura and the browser.
:param query_parameters_dict: A dictionary with the query parameters to be url encoded and added to the
authentication link
:param force_browser_logout: If True, Cura will prepend the MYCLOUD_LOGOFF_URL link before the authentication
link to force the a browser logout from mycloud.ultimaker.com
:return: The authentication URL, properly formatted and encoded
"""
auth_url = "{}?{}".format(self._auth_url, urlencode(query_parameters_dict))
if force_browser_logout:
# The url after '?next=' should be urlencoded
auth_url = "{}?next={}".format(MYCLOUD_LOGOFF_URL, quote_plus(auth_url))
return auth_url
## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:

View file

@ -5,7 +5,6 @@ from typing import Optional
from UM.Scene.SceneNode import SceneNode
from UM.Operations import Operation
from UM.Math.Vector import Vector
## An operation that parents a scene node to another scene node.

View file

@ -1,14 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING
from UM.Qt.QtApplication import QtApplication
from UM.Logger import Logger
from UM.Math.Vector import Vector
from UM.Resources import Resources
from UM.View.RenderPass import RenderPass
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.ShaderProgram import InvalidShaderProgramError
from UM.View.RenderBatch import RenderBatch
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@ -31,7 +33,11 @@ class PickingPass(RenderPass):
def render(self) -> None:
if not self._shader:
try:
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader"))
except InvalidShaderProgramError:
Logger.error("Unable to compile shader program: camera_distance.shader")
return
width, height = self.getSize()
self._gl.glViewport(0, 0, width, height)

View file

@ -181,7 +181,7 @@ class PlatformPhysics:
if tool.getPluginId() == "TranslateTool":
for node in Selection.getAllSelectedObjects():
if node.getBoundingBox().bottom < 0:
if node.getBoundingBox() and node.getBoundingBox().bottom < 0:
if not node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
node.addDecorator(ZOffsetDecorator.ZOffsetDecorator())

View file

@ -1,10 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING, cast
from UM.Application import Application
from UM.Logger import Logger
from UM.Resources import Resources
from UM.View.RenderPass import RenderPass
@ -61,7 +62,10 @@ class PreviewPass(RenderPass):
self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0])
self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0])
self._shader.setUniformValue("u_shininess", 20.0)
self._shader.setUniformValue("u_renderError", 0.0) # We don't want any error markers!.
self._shader.setUniformValue("u_faceId", -1) # Don't render any selected faces in the preview.
else:
Logger.error("Unable to compile shader program: overhang.shader")
if not self._non_printing_shader:
if self._non_printing_shader:

View file

@ -33,6 +33,10 @@ class FirmwareUpdater(QObject):
else:
self._firmware_file = firmware_file
if self._firmware_file == "":
self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()

View file

@ -111,7 +111,7 @@ class NetworkMJPGImage(QQuickPaintedItem):
if not self._image_reply.isFinished():
self._image_reply.close()
except Exception as e: # RuntimeError
except Exception: # RuntimeError
pass # It can happen that the wrapped c++ object is already deleted.
self._image_reply = None

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
@ -206,8 +206,11 @@ class ContainerManager(QObject):
if contents is None:
return {"status": "error", "message": "Serialization returned None. Unable to write to file"}
try:
with SaveFile(file_url, "w") as f:
f.write(contents)
except OSError:
return {"status": "error", "message": "Unable to write to this location.", "path": file_url}
return {"status": "success", "message": "Successfully exported container", "path": file_url}

View file

@ -9,7 +9,6 @@ from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.ContainerTree import ContainerTree
from cura.Machines.MachineNode import MachineNode
from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack

View file

@ -248,6 +248,8 @@ class ExtruderManager(QObject):
extruder_nr = int(global_stack.getProperty(extruder_nr_feature_name, "value"))
if extruder_nr == -1:
continue
if str(extruder_nr) not in self.extruderIds:
extruder_nr = int(self._application.getMachineManager().defaultExtruderPosition)
used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)])
# Check support extruders

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
from typing import Any, Dict, List, Set, Tuple, TYPE_CHECKING
from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer

View file

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import time
@ -320,9 +320,10 @@ class MachineManager(QObject):
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
extruder_manager.activeExtruderChanged.emit()
# Validate if the machine has the correct variants
# It can happen that a variant is empty, even though the machine has variants. This will ensure that that
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant instead!)
# Validate if the machine has the correct variants and materials.
# It can happen that a variant or material is empty, even though the machine has them. This will ensure that
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant or
# variant instead!)
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
for extruder in self._global_container_stack.extruderList:
variant_name = extruder.variant.getName()
@ -330,8 +331,12 @@ class MachineManager(QObject):
if variant_node is None:
Logger.log("w", "An extruder has an unknown variant, switching it to the preferred variant")
self.setVariantByName(extruder.getMetaDataEntry("position"), machine_node.preferred_variant_name)
self.__emitChangedSignals()
variant_node = machine_node.variants.get(machine_node.preferred_variant_name)
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)
## Given a definition id, return the machine with this id.
# Optional: add a list of keys and values to filter the list of machines with the given definition id
@ -1249,7 +1254,11 @@ class MachineManager(QObject):
return
Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name)
# Get the definition id corresponding to this machine name
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(name=machine_name)
if not definitions:
Logger.log("e", "Unable to switch printer type since it could not be found!")
return
machine_definition_id = definitions[0].getId()
# Try to find a machine with the same network key
metadata_filter = {"group_id": self._global_container_stack.getMetaDataEntry("group_id")}
new_machine = self.getMachine(machine_definition_id, metadata_filter = metadata_filter)
@ -1415,6 +1424,9 @@ class MachineManager(QObject):
machine_definition_id = self._global_container_stack.definition.id
machine_node = ContainerTree.getInstance().machines.get(machine_definition_id)
variant_node = machine_node.variants.get(variant_name)
if variant_node is None:
Logger.error("There is no variant with the name {variant_name}.")
return
self.setVariant(position, variant_node)
@pyqtSlot(str, "QVariant")

View file

@ -94,6 +94,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
#
# \return An extruder's position, or None if no position info is available.
def getActiveExtruderPosition(self):
# for support_meshes, always use the support_extruder
if self.getStack().getProperty("support_mesh", "value"):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return str(global_container_stack.getProperty("support_extruder_nr", "value"))
containers = ContainerRegistry.getInstance().findContainers(id = self.getActiveExtruder())
if containers:
container_stack = containers[0]

View file

@ -1,8 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Set
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
from UM.Application import Application

View file

@ -21,6 +21,11 @@ class AddPrinterPagesModel(WelcomePagesModel):
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
"next_page_id": "machine_actions",
})
self._pages.append({"id": "add_cloud_printers",
"page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"),
"is_final_page": True,
"next_page_button_text": self._catalog.i18nc("@action:button", "Finish"),
})
self._pages.append({"id": "machine_actions",
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
"should_show_function": self.shouldShowMachineActions,

View file

@ -1,8 +1,8 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
import re
from typing import Any, Dict, List, Optional, Union
from typing import Dict, List, Optional, Union
from PyQt5.QtCore import QTimer, Qt
@ -38,6 +38,9 @@ class ObjectsModel(ListModel):
OutsideAreaRole = Qt.UserRole + 3
BuilplateNumberRole = Qt.UserRole + 4
NodeRole = Qt.UserRole + 5
PerObjectSettingsCountRole = Qt.UserRole + 6
MeshTypeRole = Qt.UserRole + 7
ExtruderNumberRole = Qt.UserRole + 8
def __init__(self, parent = None) -> None:
super().__init__(parent)
@ -46,6 +49,9 @@ class ObjectsModel(ListModel):
self.addRoleName(self.SelectedRole, "selected")
self.addRoleName(self.OutsideAreaRole, "outside_build_area")
self.addRoleName(self.BuilplateNumberRole, "buildplate_number")
self.addRoleName(self.ExtruderNumberRole, "extruder_number")
self.addRoleName(self.PerObjectSettingsCountRole, "per_object_settings_count")
self.addRoleName(self.MeshTypeRole, "mesh_type")
self.addRoleName(self.NodeRole, "node")
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
@ -172,11 +178,47 @@ class ObjectsModel(ListModel):
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
node_mesh_type = ""
per_object_settings_count = 0
per_object_stack = node.callDecoration("getStack")
if per_object_stack:
per_object_settings_count = per_object_stack.getTop().getNumInstances()
for mesh_type in ["anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"]:
if per_object_stack.getProperty(mesh_type, "value"):
node_mesh_type = mesh_type
per_object_settings_count -= 1 # do not count this mesh type setting
break
if per_object_settings_count > 0:
if node_mesh_type == "support_mesh":
# support meshes only allow support settings
per_object_settings_count = 0
for key in per_object_stack.getTop().getAllKeys():
if per_object_stack.getTop().getInstance(key).definition.isAncestor("support"):
per_object_settings_count += 1
elif node_mesh_type == "anti_overhang_mesh":
# anti overhang meshes ignore per model settings
per_object_settings_count = 0
extruder_position = node.callDecoration("getActiveExtruderPosition")
if extruder_position is None:
extruder_number = -1
else:
extruder_number = int(extruder_position)
if node_mesh_type == "anti_overhang_mesh" or node.callDecoration("isGroup"):
# for anti overhang meshes and groups the extruder nr is irrelevant
extruder_number = -1
nodes.append({
"name": node.getName(),
"selected": Selection.isSelected(node),
"outside_build_area": is_outside_build_area,
"buildplate_number": node_build_plate_number,
"extruder_number": extruder_number,
"per_object_settings_count": per_object_settings_count,
"mesh_type": node_mesh_type,
"node": node
})

View file

@ -119,8 +119,10 @@ class WelcomePagesModel(ListModel):
return
next_page_index = idx
is_final_page = page_item.get("is_final_page")
# If we have reached the last page, emit allFinished signal and reset.
if next_page_index == len(self._items):
if next_page_index == len(self._items) or is_final_page:
self.atEnd()
return
@ -243,6 +245,10 @@ class WelcomePagesModel(ListModel):
{"id": "data_collections",
"page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
},
{"id": "cloud",
"page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
"should_show_function": self.shouldShowCloudPage,
},
{"id": "add_network_or_local_printer",
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
"next_page_id": "machine_actions",
@ -251,14 +257,15 @@ class WelcomePagesModel(ListModel):
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
"next_page_id": "machine_actions",
},
{"id": "add_cloud_printers",
"page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"),
"is_final_page": True, # If we end up in this page, the next button will close the dialog
"next_page_button_text": self._catalog.i18nc("@action:button", "Finish"),
},
{"id": "machine_actions",
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
"next_page_id": "cloud",
"should_show_function": self.shouldShowMachineActions,
},
{"id": "cloud",
"page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
},
]
pages_to_show = all_pages_list
@ -287,6 +294,17 @@ class WelcomePagesModel(ListModel):
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
return len([action for action in first_start_actions if action.needsUserInteraction()]) > 0
def shouldShowCloudPage(self) -> bool:
"""
The cloud page should be shown only if the user is not logged in
:return: True if the user is not logged in, False if he/she is
"""
# Import CuraApplication locally or else it fails
from cura.CuraApplication import CuraApplication
api = CuraApplication.getInstance().getCuraAPI()
return not api.account.isLoggedIn
def addPage(self) -> None:
pass

View file

@ -3,6 +3,7 @@
import os.path
from UM.Resources import Resources
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
@ -23,7 +24,7 @@ class XRayPass(RenderPass):
def render(self):
if not self._shader:
self._shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("XRayView"), "xray.shader"))
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader"))
batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive)
for node in DepthFirstIterator(self._scene.getRoot()):

View file

@ -53,7 +53,7 @@ if with_sentry_sdk:
if ApplicationMetadata.CuraVersion == "master":
sentry_env = "development" # Master is always a development version.
elif ApplicationMetadata.CuraVersion in ["beta", "BETA"]:
elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
sentry_env = "beta"
try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99":

Binary file not shown.

View file

@ -1 +0,0 @@
<mxfile host="www.draw.io" modified="2019-12-20T12:41:33.716Z" agent="Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" etag="exp7abRcULgdJsv-qAei" version="12.4.3" type="device" pages="1"><diagram id="05EojhSyumsKE0fvOSX8" name="Page-1">7ZtNb9s4EIZ/jY+70LftY+NkuwskQFrvts2pYCRaIkqLBkXXcn79Di3Ssk3ZTR195EDAB82IpIZ8ZgzxBTXyZ8vyI0er7IElmI48JylH/u3I89zA80by5yTbyjOOxpUj5SRRjWrHnLxg5XSUd00SXBw1FIxRQVbHzpjlOY7FkQ9xzjbHzRaMHj91hVJsOOYxoqb3K0lEVnkn3rj2/41Jmuknu9G0urNEurGaSZGhhG0OXP7dyJ9xxkR1tSxnmMrF0+tS9fvrzN19YBzn4jUdvnDvJQzwv+sXOp0/lfHk+3fxh1+N8hPRtZrwXSn4OsF8LlD8Q0Uutno5ig1ZUpSDdbNguZirOw7YcUZoco+2bC3DKWR3bd1kjJMXaI8o3HLBAbe5ULS9SI5GKJ0xyjg4crZ7QN2piqV6DMcFdHvU03ZPXA+oPGp4jwqhA2SUolVBnnchy45LxFOS3zAh2FI1UuuBucDl2YV29/gg7zFbYsG30ER3mCriKuXdsbI3dQK5gfJlB8nj+YFKXJW06X7smitcKLS/gTkwMP9XYG7QhTmLHRzOfuATGg2AECVpDibFC9lNLhqByvmg3IKt5GArFJM8vd+1uQ1qz2c1celi0HdBd9WRkSTBueTHBBLoeZ9fK0ZysVuY8AZ+sH4z589wFELgM7Dd2oafbM7FjOUwF0R23DBkwgbLbHgd5PP1YpJXpCGTXwc66ohzaHD+tAZKQsY4y1Cewl+phd4u9NAbGHpkQP8nF3JGlnS7pMeTgUmPz5e3Rd0qatcJBmY9MVg/IIE5gVcoC7tl2P5rX9C6gj01YH9BgNr+h7fPOhz6HU3vbA9g3+IFyYkgLLfvad2Rnwz9oua6F8lb3u1uxpyhX9c8k/dHyp61nmG1lbdrK4GWEw8g+06v2opWOK248tZ6Psf+vLrSjLqzejbVUiuvdI69QV/pGbupnlqBpRvWDQpLz6wvKKgWdruwmzSWnmmb0qkVWTrD3aCy9Izb1E+tzNIV7QadpWfapoJqdZZe0DcILT2jN/VUK7R0uDFrUFr6Be6ae3B7juWtWosfvrtzLK65A7day1UlXVXM+z3J4pqbbqu1dI598LMsrrn/tlpLN6wHP83imrtvq7V0BHv48yyuufm2WktnuAc/0eKau2+rtXRFe/gzLZc33PaFrTP0gx9q0Z+3WK2ln43Z4KdafBO4gRnnyQf58RVYMUVFQWKpg4illk1gCfj2m1x2WE9lPikKO+O2PLK22iqJOOgG1pMeEa7rTtLQfargcGJ853Wig8AE2JrH+NLUq3YC8RRfo5cdMAsbkGkfxxQJ8vM43iaO6gmPMm9H58/I6BLVQ1TzVL3qbDAG8oLjgaLpyUDVQhgDAXq0PWim6upswF50ErDvXIzLDy62h4sqgjrH9wzekPbmYa7fT/sr0lcaj/CaDPFjflgUV5RPi6Wgc+qd10LknaTK+MpaiMa/KKqWaiE4DbjfWgCz/iK1al5/1+vf/Q8=</diagram></mxfile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

22
docs/index.md Normal file
View file

@ -0,0 +1,22 @@
Cura Documentation
====
Welcome to the Cura documentation pages.
Objective
----
The goal of this documentation is to give an overview of the architecture of Cura's source code. The purpose of this overview is to make programmers familiar with Cura's source code so that they may contribute more easily, write plug-ins more easily or get started within the Cura team more quickly.
There are some caveats though. These are *not* within the scope of this documentation:
* There is no documentation on individual functions or classes of the code here. For that, refer to the Doxygen documentation and Python Docstrings in the source code itself, or generate the documentation locally using Doxygen.
* It's virtually impossible and indeed not worth the effort or money to keep this 100% up to date.
* There are no example plug-ins here. There are a number of example plug-ins in the Ultimaker organisation on Github.com to draw from.
* The slicing process is not documented here. Refer to CuraEngine for that.
This documentation will touch on the inner workings of Uranium as well though, due to the nature of the architecture.
Index
----
The following chapters are available in this documentation:
* [Repositories](repositories.md): An overview of the repositories that together make up the Cura application.
* [Profiles](profiles/profiles.md): About the setting and profile system of Cura.
* [Scene](scene/scene.md): How Cura's 3D scene looks.

View file

@ -0,0 +1,33 @@
Container Stacks
====
When the user selects the profiles and settings to print with, he can swap out a number of profiles. The profiles that are currently in use are stored in several container stacks. These container stacks always have a definition container at the bottom, which defines all available settings and all available properties for each setting. The profiles on top of that definition can then override the `value` property of some of those settings.
When deriving a setting value, a container stack starts looking at the top-most profile to see if it contains an override for that setting. If it does, it returns that override. Otherwise, it looks into the second profile. If that also doesn't have an override for this setting, it looks into the third profile, and so on. The last profile is always a definition container which always contains an value for all settings. This way, the profiles at the top will always win over the profiles at the bottom. There is a clear precedence order for which profile wins over which other profile.
A Machine Instance
----
A machine instance is a printer that the user has added to his configuration. It consists of multiple container stacks: One for global settings and one for each of the available extruders. This way, different extruders can contain different materials and quality profiles, for instance. The global stack contains a different set of profiles than the extruder stacks.
While Uranium defines no specific roles for the entries in a container stack, Cura defines rigid roles for each slot in a container stack. These are the layouts for the container stacks of an example printer with 2 extruders.
![Three container stacks](../resources/machine_instance.svg)
To expand on this a bit further, each extruder stack contains the following profiles:
* A user profile, where extruder-specific setting changes are stored that are not (yet) saved to a custom profile. If the user changes a setting that can be adjusted per extruder (such as infill density) then it gets stored here. If the user adjusts a setting that is global it will immediately be stored in the user profile of the global stack.
* A custom profile. If the user saves his setting changes to a custom profile, it gets moved from the user profile to here. Actually a "custom profile" as the user sees it consists of multiple profiles: one for each extruder and one for the global settings.
* An intent profile. The user can select between several intents for his print, such as precision, strength, visual quality, etc. This may be empty as well, which indicates the "default" intent.
* A quality profile. The user can select between several quality levels.
* A material profile, where the user selects which material is loaded in this extruder.
* A nozzle profile, where the user selects which nozzle is installed in this extruder.
* Definition changes, which stores the changes that the user made for this extruder in the Printer Settings dialogue.
* Extruder. The user is not able to swap this out. This is a definition that lists the extruder number for this extruder and optionally things that are fixed in the printer, such as the nozzle offset.
The global container stack contains the following profiles:
* A user profile, where global setting changes are stored that are not (yet) saved to a custom profile. If the user changes for instance the layer height, the new value for the layer height gets stored here.
* A custom profile. If the user saves his setting changes to a custom profile, the global settings that were in the global user profile get moved here.
* An intent profile. Currently this must ALWAYS be empty. There are no global intent profiles. This is there for historical reasons.
* A quality profile. This contains global settings that match with the quality level that the user selected. This global quality profile cannot be specific to a material or nozzle.
* A material profile. Currently this must ALWAYS be empty. There are no global material profiles. This is there for historical reasons.
* A variant profile. Currently this must ALWAYS be empty. There are no global variant profiles. This is there for historical reasons.
* Definition changes, which stores the changes that the user made to the printer in the Printer Settings dialogue.
* Printer. This specifies the currently used printer model, such as Ultimaker 3, Ultimaker S5, etc.

View file

@ -0,0 +1,66 @@
Getting a Setting Value
====
How Cura gets a setting's value is a complex endeavour that requires some explanation. The `value` property gets special treatment for this because there are a few other properties that influence the value. In this page we explain the algorithm to getting a setting value.
This page explains all possible cases for a setting, but not all of them may apply. For instance, a global setting will not evaluate the per-object settings to get its value. Exceptions to the rules for other types of settings will be written down.
Per Object Settings
----
Per-object settings, which are added to an object using the per-object settings tool, will always prevail over other setting values. They are not evaluated with the rest of the settings system because Cura's front-end doesn't need to send all setting values for all objects to CuraEngine separately. It only sends over the per-object settings that get overridden. CuraEngine then evaluates settings that can be changed per-object using the list of settings for that object but if the object doesn't have the setting attached falls back on the settings in the object's extruder. Refer to the [CuraEngine](#CuraEngine) chapter to see how this works.
Settings where the `settable_per_mesh` property is false will not be shown in Cura's interface in the list of available settings in the per-object settings panel. They cannot be adjusted per object then. CuraEngine will also not evaluate those settings for each object separately. There is (or should always be) a good reason why each of these settings are not evaluated per object: Simply because CuraEngine is not processing one particular mesh at that moment. For instance, when writing the move to change to the next layer, CuraEngine hasn't processed any of the meshes on that layer yet and so the layer change movement speed, or indeed the layer height, can't change for each object.
The per-object settings are stored in a separate container stack that is particular to the object. The container stack is added to the object via a scene decorator. It has just a single container in it, which contains all of the settings that the user changed.
Resolve
----
If the setting is not listed in the per-object settings, it needs to be evaluated from the main settings list. However before evaluating it from a particular extruder, Cura will check if the setting has the `resolve` property. If it does, it returns the output of the `resolve` property and that's everything.
The `resolve` property is intended for settings which are global in nature, but still need to be influenced by extruder-specific settings. A good example is the Build Plate Temperature, which is very dependent on the material(s) used by the printer, but there can only be a single bed temperature at a time.
Cura will simply evaluate the `resolve` setting if present, which is an arbitrary Python expression, and return its result as the setting's value. However typically the `resolve` property is a function that takes the values of this setting for all extruders in use and then computes a result based on those. There is a built-in function for that called `extruderValues()`, which returns a list of setting values, one for each extruder. The function can then for instance take the average of those. In the case of the build plate temperature it will take the highest of those. In the case of the adhesion type it will choose "raft" if any extruder uses a raft, or "brim" as second choice, "skirt" as third choice and "none" only if all extruders use "none". Each setting with a `resolve` property has its own way of resolving the setting. The `extruderValues()` function continues with the algorithm as written below, but repeats it for each extruder.
Limit To Extruder
----
If a setting is evaluated from a particular extruder stack, it normally gets evaluated from the extruder that the object is assigned to. However there are some exceptions. Some groups of settings belong to a particular "extruder setting", like the Infill Extruder setting, or the Support Extruder setting. Which extruder a setting belongs to is stored in the `limit_to_extruder` property. Settings which have their `limit_to_extruder` property set to `adhesion_extruder_nr`, for instance, belong to the build plate adhesion settings.
If the `limit_to_extruder` property evaluates to a positive number, instead of getting the setting from the object's extruder it will be obtained from the extruder written in the `limit_to_extruder` property. So even if an object is set to be printed with extruder 0, if the infill extruder is set to extruder 1 any infill setting will be obtained from extruder 1. If `limit_to_extruder` is negative (in particular -1, which is the default), then the setting will be obtained from the object's own extruder.
This property is communicated to CuraEngine separately. CuraEngine makes sure that the setting is evaluated from the correct extruder. Refer to the [CuraEngine](#CuraEngine) chapter to see how this works.
Evaluating a Stack
----
After the resolve and limit to extruder properties have been checked, the setting value needs to be evaluated from an extruder stack.
This is explained in more detail in the [Container Stacks](container_stacks.md) documentation. In brief, Cura will check the highest container in the extruder stack first to see whether that container overrides the setting. If it does, it returns that as the setting value. Otherwise, it checks the second container on the stack to see if that one overrides it. If it does it returns that value, and otherwise it checks the third container, and so on. If a setting is not overridden by any container in the extruder stack, it continues downward in the global stack. If it is also not overridden there, it eventually arrives at the definition in the bottom of the global stack.
Evaluating a Definition
----
If the evaluation for a setting reaches the last entry of the global stack, its definition, a few more things can happen.
Definition containers have an inheritance structure. For instance, the `ultimaker3` definition container specifies in its metadata that it inherits from `ultimaker`, which in turn inherits from `fdmprinter`. So again here, when evaluating a property from the `ultimaker3` definition it will first look to see if the property is overridden by the `ultimaker3` definition itself, and otherwise refer on to the `ultimaker` definition or otherwise finally to the `fdmprinter` definition. `fdmprinter` is the last line of defence, and it contains *all* properties for *all* settings.
But even in `fdmprinter`, not all settings have a `value` property. It is not a required property. If the setting doesn't have a `value` property, the `default_value` property is returned, which is a required property. The distinction between `value` and `default_value` is made in order to allow CuraEngine to load a definition file as well when running from the command line (a debugging technique for CuraEngine). It then won't have all of the correct setting values but it at least doesn't need to evaluate all of the Python expressions and you'll be able to make some debugging slices.
Evaluating a Value Property
----
The `value` property may contain a formula, which is an arbitrary Python expression that will be executed by Cura to arrive at a setting value. All containers may set the `value` property. Instance containers can only set the `value`, while definitions can set all properties.
While the value could be any sort of formula, some functions of Python are restricted for security reasons. Since Cura 4.6, profiles are no longer a "trusted" resource and are therefore subject to heavy restrictions. It can use Python's built in mathematical functions and list functions as well as a few basic other ones, but things like writing to a file are prohibited.
There are also a few extra things that can be used in these expressions:
* Any setting key can be used as a variable that contains the setting's value.
* As explained before, `extruderValues(key)` is a function that returns a list of setting values for a particular setting for all used extruders.
* The function `extruderValue(extruder, key)` will evaluate a particular setting for a particular extruder.
* The function `resolveOrValue(key)` will perform the full setting evaluation as described in this document for the current context (so if this setting is being evaluated for the second extruder it would perform it as if coming from the second extruder).
* The function `defaultExtruderPosition()` will get the first extruder that is not disabled. For instance, if a printer has three extruders but the first is disabled, this would return `1` to indicate the second extruder (0-indexed).
* The function `valueFromContainer(key, index)` will get a setting value from the global stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.
* The function `valueFromExtruderContainer(key, index)` will get a setting value from the current extruder stack, but skip the first few containers in that stack. It will skip until it reaches a particular index in the container stack.
CuraEngine
----
When starting a slice, Cura will send the scene to CuraEngine and with each model send over the per-object settings that belong to it. It also sends all setting values over, as evaluated from each extruder and from the global stack, and sends the `limit_to_extruder` property along as well. CuraEngine stores this and then starts its slicing process. CuraEngine also has a hierarchical structure for its settings with fallbacks. This is explained in detail in [the documentation of CuraEngine](https://github.com/Ultimaker/CuraEngine/blob/master/docs/settings.md) and shortly again here.
Each model gets a setting container assigned. The per-object settings are stored in those. The fallback for this container is set to be the extruder with which the object is printed. The extruder uses the current *mesh group* as fallback (which is a concept that Cura's front-end doesn't have). Each mesh group uses the global settings container as fallback.
During the slicing process CuraEngine will evaluate the settings from its current context as it goes. For instance, when processing the walls for a particular mesh, it will request the Outer Wall Line Width setting from the settings container of that mesh. When it's not processing a particular mesh but for instance the travel moves between two meshes, it uses the currently applicable extruder. So this business logic defines actually how a setting can be configured per mesh, per extruder or only globally. The `settable_per_extruder`, and related properties of settings are only used in the front-end to determine how the settings are shown to the user.

30
docs/profiles/profiles.md Normal file
View file

@ -0,0 +1,30 @@
Profiles
====
Cura's profile system is very advanced and has gotten pretty complex. This chapter is an attempt to document how it is structured.
Index
----
The following pages describe the profile and setting system of Cura:
* [Container Stacks](container_stacks.md): Which profiles can be swapped out and how they are ordered when evaluating a setting.
* [Setting Properties](setting_properties.md): What properties can each setting have?
* [Getting a Setting Value](getting_a_setting_value.md): How Cura arrives at a value for a certain setting.
Glossary
----
The terminology for these profiles is not always obvious. Here is a glossary of the terms that we'll use in this chapter.
* **Profile:** Either an *instance container* or a *definition container*.
* **Definition container:** Profile that's stored as .def.json file, defining new settings and all of their properties. In Cura these represent printer models and extruder trains.
* **Instance container:** Profile that's stored as .inst.cfg file or .xml.fdm_material file, which override some setting values. In Cura these represent the other profiles.
* **[Container] stack:** A list of profiles, with one definition container at the bottom and instance containers for the rest. All settings are defined in the definition container. The rest of the profiles each specify a set of value overrides. The profiles at the top always override the profiles at the bottom.
* **Machine instance:** An instance of a printer that the user has added. The list of all machine instances is shown in a drop-down in Cura's interface.
* **Material:** A type of filament that's being sold by a vendor as a product.
* **Filament spool:** A single spool of material.
* **Quality profile:** A profile that is one of the options when the user selects which quality level they want to print with.
* **Intent profile:** A profile that is one of the options when the user selects what his intent is.
* **Custom profile:** A user-made profile that is stored when the user selects to "create a profile from the current settings/overrides".
* **Quality-changes profile:** Alternative name for *custom profile*. This name is used in the code more often, but it's a bit misleading so this documentation prefers the term "custom profile".
* **User profile:** A profile containing the settings that the user has changed, but not yet saved to a profile.
* **Variant profile:** A profile containing some overrides that allow the user to select variants of the definition. As of this writing this is only used for the nozzles.
* **Quality level:** A measure of quality where the user can select from, for instance "normal", "fast", "high". When selecting a quality level, Cura will select a matching quality profile for each extruder.
* **Quality type:** Alternative name for *quality level*. This name is used in the code more often, but this documentation prefers the term "quality level".
* **Inheritance function:** A function through which the `value` of a setting is calculated. This may depend on other settings.

View file

@ -0,0 +1,33 @@
Setting Properties
====
Each setting in Cura has a number of properties. It's not just a key and a value. This page lists the properties that a setting can define.
* `key` (string): The identifier by which the setting is referenced. This is not a human-readable name, but just a reference string, such as `layer_height_0`. Typically these are named with the most significant category first, in order to sort them better, such as `material_print_temperature`. This is not actually a real property but just an identifier; it can't be changed.
* `value` (optional): The current value of the setting. This can be a function, an arbitrary Python expression that depends on the values of other settings. If it's not present, the `default_value` is used.
* `default_value`: A default value for the setting if `value` is undefined. This property is required however. It can't be a Python expression, but it can be any JSON type. This is made separate so that CuraEngine can read it out as well for its debugging mode via the command line, without needing a complete Python interpreter.
* `label` (string): The human-readable name for the setting. This label is translated.
* `description` (string): A longer description of what the setting does when you change it. This description is translated as well.
* `type` (string): The type of value that this setting contains. Allowed types are: `bool`, `str`, `float`, `int`, `enum`, `category`, `[int]`, `vec3`, `polygon` and `polygons`.
* `unit` (optional string): A unit that is displayed at the right-hand side of the text field where the user enters the setting value.
* `resolve` (optional string): A Python expression that resolves disagreements for global settings if multiple per-extruder profiles define different values for a setting. Typically this takes the values for the setting from all stacks and computes one final value for it that will be used for the global setting. For instance, the `resolve` function for the build plate temperature is `max(extruderValues('material_bed_temperature')`, meaning that it will use the hottest bed temperature of all materials of the extruders in use.
* `limit_to_extruder` (optional): A Python expression that indicates which extruder a setting will be obtained from. This is used for settings that may be extruder-specific but the extruder is not necessarily the current extruder. For instance, support settings need to be evaluated for the support extruder. Infill settings need to be evaluated for the infill extruder if the infill extruder is changed.
* `enabled` (optional string or boolean): Whether the setting can currently be made visible for the user. This can be a simple true/false, or a Python expression that depends on other settings. Typically used for settings that don't apply when another setting is disabled, such as to hide the support settings if support is disabled.
* `minimum_value` (optional): The lowest acceptable value for this setting. If it's any lower, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a layer height of 0mm. This property only applies to numerical settings.
* `maximum_value` (optional): The highest acceptable value for this setting. If it's any higher, Cura will not allow the user to slice. By convention this is used to prevent setting values that are technically or physically impossible, such as a support overhang angle of more than 90 degrees. This property only applies to numerical settings.
* `minimum_value_warning` (optional): The threshold under which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a low setting value. This property only applies to numerical settings.
* `maximum_value_warning` (optional): The threshold above which a warning is displayed to the user. By convention this is used to indicate that it will probably not print very nicely with such a high setting value. This property only applies to numerical settings.
* `settable_globally` (optional boolean): Whether the setting can be changed globally. For some mesh-type settings such as `support_mesh` this doesn't make sense, so those can't be changed globally. They are not displayed in the main settings list then.
* `settable_per_meshgroup` (optional boolean): Whether a setting can be changed per group of meshes. Currently unused in Cura.
* `settable_per_extruder` (optional boolean): Whether a setting can be changed per extruder. Some settings, like the build plate temperature, can't be adjusted separately for each extruder. An icon is shown in the interface to indicate this. If the user changes these settings they are stored in the global stack.
* `settable_per_mesh` (optional boolean): Whether a setting can be changed per mesh. The settings that can be changed per mesh are shown in the list of available settings in the per-object settings tool.
* `children` (optional list): A list of child settings. These are displayed with an indentation. If all child settings are overridden by the user, the parent setting gets greyed out to indicate that the parent setting has no effect any more. This is not strictly always the case though, because that would depend on the inheritance functions in the `value`.
* `icon` (optional string): A path to an icon to be displayed. Only applies to setting categories.
* `allow_empty` (optional bool): Whether the setting is allowed to be empty. If it's not, this will be treated as a setting error and Cura will not allow the user to slice. Only applies to string-type settings.
* `warning_description` (optional string): A warning message to display when the setting has a warning value. This is currently unused by Cura.
* `error_description` (optional string): An error message to display when the setting has an error value. This is currently unused by Cura.
* `options` (dictionary): A list of values that the user can choose from. The keys of this dictionary are keys that CuraEngine identifies the option with. The values are human-readable strings and will be translated. Only applies to (and only required for) enum-type settings.
* `comments` (optional string): Comments to other programmers about the setting. This is not used by Cura.
* `is_uuid` (optional boolean): Whether or not this setting indicates a UUID-4. If it is, the setting will indicate an error if it's not in the correct format. Only applies to string-type settings.
* `regex_blacklist_pattern` (optional string): A regular expression, where if the setting value matches with this regular expression, it gets an error state. Only applies to string-type settings.
* `error_value` (optional): If the setting value is equal to this value, it will show a setting error. This is used to display errors for non-numerical settings such as checkboxes.
* `warning_value` (optional): If the setting value is equal to this value, it will show a setting warning. This is used to display warnings for non-numerical settings such as checkboxes.

21
docs/repositories.md Normal file
View file

@ -0,0 +1,21 @@
Repositories
====
Cura uses a number of repositories where parts of our source code are separated, in order to get a cleaner architecture. Those repositories are:
* [Cura](https://github.com/Ultimaker/Cura), the main repository for the front-end of Cura. This contains all of the business logic for the front-end, including the specific types of profiles that are available, the concept of 3D printers and materials, specific tools for handling 3D printed models, pretty much all of the GUI, as well as Ultimaker services such as the Marketplace and accounts.
* The Cura repository is built on [Uranium](https://github.com/Ultimaker/Uranium), a framework for desktop applications that handle 3D models and have a separate back-end. This provides Cura with a basic GUI framework ([Qt](https://www.qt.io/)), a 3D scene, a rendering system, a plug-in system and a system for stacked profiles that change settings.
* In order to slice, Cura starts [CuraEngine](https://github.com/Ultimaker/CuraEngine) in the background. This does the actual process that converts 3D models into a toolpath for the printer.
* Communication to CuraEngine goes via [libArcus](https://github.com/Ultimaker/libArcus), a small library that wraps around [Protobuf](https://developers.google.com/protocol-buffers/) in order to make it run over a local socket.
* Cura's build scripts are in [cura-build](https://github.com/Ultimaker/cura-build) and build scripts for building dependencies are in [cura-build-environment](https://github.com/Ultimaker/cura-build-environment).
There are also a number of repositories under our control that are not integral parts of Cura's architecture, but more like separated side-gigs:
* Loading and writing 3MF files is done through [libSavitar](https://github.com/Ultimaker/libSavitar).
* Loading and writing UFP files is done through [libCharon](https://github.com/Ultimaker/libCharon).
* To make the build system a bit simpler, some parts are pre-compiled in [cura-binary-data](https://github.com/Ultimaker/cura-binary-data). This holds things like the machine-readable translation files and the Marlin builds for firmware updates, which would require considerable tooling to build automatically.
* There are automated GUI tests in [Cura-squish-tests](https://github.com/Ultimaker/Cura-squish-tests).
* Material profiles are stored in [fdm_materials](https://github.com/Ultimaker/fdm_materials). This is separated out and combined in our build process, so that the firmware for Ultimaker's printers can use the same set of profiles too.
Interplay
----
At a very high level, Cura's repositories interconnect as follows:
![Overview of interplay between repositories](resources/repositories.svg)

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="1010">
<defs>
<path id="stack-header" d="m0,50 v-30 a20,20 0 0 1 20,-20 h260 a20,20 0 0 1 20,20 v30 z" />
<marker id="arrow" refX="2" refY="1.5" markerWidth="3" markerHeight="3" orient="auto-start-reverse">
<polygon points="0,0 3,1.5 0,3" />
</marker>
</defs>
<g stroke="black" stroke-width="5" fill="silver"> <!-- Stack headers. -->
<use href="#stack-header" x="200" y="555" />
<use href="#stack-header" x="5" y="5" />
<use href="#stack-header" x="395" y="5" />
</g>
<g stroke="black" stroke-width="10" fill="none"> <!-- Stack outlines. -->
<rect x="200" y="555" width="300" height="450" rx="20" /> <!-- Global stack. -->
<rect x="5" y="5" width="300" height="450" rx="20" /> <!-- Left extruder. -->
<rect x="395" y="5" width="300" height="450" rx="20" /> <!-- Right extruder. -->
</g>
<g font-family="sans-serif" font-size="25" dominant-baseline="middle" text-anchor="middle">
<text x="350" y="582.5">Global stack</text> <!-- Slightly lowered since the top line is thicker than the bottom. -->
<text x="350" y="630">User</text>
<text x="350" y="680">Custom</text>
<text x="350" y="730">Intent</text>
<text x="350" y="780">Quality</text>
<text x="350" y="830">Material</text>
<text x="350" y="880">Variant</text>
<text x="350" y="930">Definition changes</text>
<text x="350" y="980">Printer</text>
<text x="155" y="32.5">Left extruder</text> <!-- Slightly lowered again. -->
<text x="155" y="80">User</text>
<text x="155" y="130">Custom</text>
<text x="155" y="180">Intent</text>
<text x="155" y="230">Quality</text>
<text x="155" y="280">Material</text>
<text x="155" y="330">Nozzle</text>
<text x="155" y="380">Definition changes</text>
<text x="155" y="430">Extruder</text>
<text x="545" y="32.5">Right extruder</text> <!-- Slightly lowered again. -->
<text x="545" y="80">User</text>
<text x="545" y="130">Custom</text>
<text x="545" y="180">Intent</text>
<text x="545" y="230">Quality</text>
<text x="545" y="280">Material</text>
<text x="545" y="330">Nozzle</text>
<text x="545" y="380">Definition changes</text>
<text x="545" y="430">Extruder</text>
</g>
<g stroke="black" stroke-width="5" marker-end="url(#arrow)"> <!-- Arrows. -->
<line x1="155" y1="455" x2="345" y2="545" />
<line x1="545" y1="455" x2="355" y2="545" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000">
<defs>
<marker id="arrow" refX="2" refY="1.5" markerWidth="3" markerHeight="3" orient="auto-start-reverse">
<polygon points="0,0 3,1.5 0,3" />
</marker>
</defs>
<g marker-end="url(#arrow)" stroke="black" stroke-width="5"> <!-- Arrows. -->
<!-- Towards CuraEngine and back. -->
<line x1="475" y1="400" x2="475" y2="307.5" />
<line x1="475" y1="250" x2="475" y2="210" />
<line x1="525" y1="200" x2="525" y2="242.5" />
<line x1="525" y1="300" x2="525" y2="390" />
<!-- From libSavitar. -->
<line x1="100" y1="425" x2="142.5" y2="425" />
<line x1="300" y1="425" x2="390" y2="425" />
<!-- From fdm_materials. -->
<line x1="350" y1="575" x2="390" y2="575" />
<!-- To libCharon. -->
<line x1="600" y1="500" x2="692.5" y2="500" />
<line x1="900" y1="500" x2="945" y2="500" />
<!-- To Uranium. -->
<line x1="500" y1="600" x2="500" y2="690" />
</g>
<g stroke="black" fill="none"> <!-- Boxes representing repositories. -->
<g stroke-width="10"> <!-- Major repositories. -->
<rect x="400" y="400" width="200" height="200" rx="20" /> <!-- Cura. -->
<rect x="350" y="700" width="300" height="200" rx="20" /> <!-- Uranium. -->
<rect x="300" y="5" width="400" height="195" rx="20" /> <!-- CuraEngine. -->
</g>
<g stroke-width="5"> <!-- Minor repositories. -->
<rect x="150" y="350" width="150" height="100" rx="20" /> <!-- libSavitar. -->
<rect x="100" y="550" width="250" height="100" rx="20" /> <!-- fdm_materials. -->
<rect x="430" y="250" width="140" height="50" rx="20" /> <!-- libArcus. -->
<rect x="700" y="450" width="200" height="100" rx="20" /> <!-- libCharon. -->
</g>
</g>
<g font-family="sans-serif" text-anchor="middle" dominant-baseline="middle"> <!-- Labels. -->
<g font-size="50"> <!-- Major repositories. -->
<text x="500" y="500">Cura</text>
<text x="500" y="800">Uranium</text>
<text x="500" y="102.5">CuraEngine</text>
</g>
<g font-size="25"> <!-- Minor repositories and arrows. -->
<text x="225" y="400">libSavitar</text>
<text x="225" y="600">fdm_materials</text>
<text x="500" y="275">libArcus</text>
<text x="800" y="500">libCharon</text>
<g text-anchor="start">
<text x="645" y="490" transform="rotate(-90, 645, 490)">G-code</text>
<text x="950" y="500">UFP</text>
<text x="535" y="345">G-code</text>
<text x="345" y="415" transform="rotate(-90, 345, 415)">Model</text>
<text x="510" y="645">Built upon</text>
</g>
<g text-anchor="end">
<text x="465" y="345">Scene</text>
<text x="90" y="425">3MF</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,27 @@
Build Volume
====
The build volume is a scene node. This node gets placed somewhere in the scene. This is a specialised scene node that draws the build volume and all of its bits and pieces.
Volume bounds
----
The build volume draws a cube (for rectangular build plates) that represents the printable build volume. This outline is drawn with a blue line. To render this, the Build Volume scene node generates a cube and instructs OpenGL to draw a wireframe of this cube. This way the wireframe is always a single pixel wide regardless of view distance. This cube is automatically resized when the relevant settings change, like the width, height and depth of the printer, the shape of the build plate, the Print Sequence or the gantry height.
The build volume also draws a grid underneath the build volume. The grid features 1cm lines which allows the user to roughly estimate how big its print is or the distance between prints. It also features a finer 1mm line pattern within that grid. The grid is drawn as a single quad. This quad is then sent to the graphical card with a specialised shader which draws the grid pattern.
For elliptical build plates, the volume bounds are drawn as two circles, one at the top and one at the bottom of the available height. The build plate grid is drawn as a tesselated circle, but with the same shader.
Disallowed areas
----
The build volume also calculates and draws the disallowed areas. These are drawn as a grey shadow. The point of these disallowed areas is to denote the areas where the user is not allowed to place any objects. The reason to forbid placing an object can be a lot of things.
One disallowed area that is always present is the border around the build volume. This border is there to prevent the nozzle from going outside of the bounds of the build volume. For instance, if you were to print an object with a brim of 8mm, you won't be able to place that object closer than 8mm to the edge of the build volume. Doing so would draw part of the brim outside of the build volume. The width of these disallowed areas depends on a bunch of things. Most commonly the build plate adhesion setting or the Avoid Distance setting is the culprit. However this border is also affected by the draft shield, ooze shield and Support Horizontal Expansion, among others.
Another disallowed area stems from the distance between the nozzles for some multi-extrusion printers. The total build volume in Cura is normally the volume that can be reached by either nozzle. However for every extruder that your print uses, the build volume will be shrunk to the intersecting area that all used nozzles can reach. This is done by adding disallowed areas near the border. For instance, if you have two extruders with 18mm X distance between them, and your print uses only the left extruder, there will be an extra border of 18mm on the right hand side of the printer, because the left nozzle can't reach that far to the right. If you then use both extruders, there will be an 18mm border on both sides.
There are also disallowed areas for features that are printed. There are as of this writing two such disallowed areas: The prime tower and the prime blob. You can't print an object on those locations since they would intersect with the printed feature.
Then there are disallowed areas imposed by the current printer. Some printers have things in the way of your print, such as clips that hold the build plate down, or cameras, switching bays or wiping brushes. These are encoded in the `machine_disallowed_areas` and `nozzle_disallowed_areas` settings, as polygons. The difference between these two settings is that one is intended to describe where the print head is not allowed to move. The other is intended to describe where the currently active nozzle is not allowed to move. This distinction is meant to allow inactive nozzles to move over things like build plate clips or stickers, which can slide underneath an inactive nozzle.
Finally, there are disallowed areas imposed by other objects that you want to print. Each object and group has an associated Convex Hull Node, which denotes the volume that the object is going to be taking up while printing. This convex hull is projected down to the build plate and determines there the volume that the object is going to occupy.
Each type of disallowed area is affected by certain settings. The border around the build volume, for instance, is affected by the brim, but the disallowed areas for printed objects are not. This is because the brim could go outside of the build volume but the brim can't hit any other objects. If the brim comes too close to other objects, it merges with the brim of those objects. As such, generating each type of disallowed area requires specialised business logic to determine how the setting affects the disallowed area. It needs to take the highest of two settings sometimes, or it needs to sum them together, multiplying a certain line width by an accompanying line count setting, and so on. All this logic is implemented in the BuildVolume class.

26
docs/scene/scene.md Normal file
View file

@ -0,0 +1,26 @@
Scene
====
The 3D scene in Cura is designed as a [Scene Graph](https://en.wikipedia.org/wiki/Scene_graph), which is common in many 3D graphics applications. The scene graph of Cura is usually very flat, but has the possibility to have nested objects which inherit transformations from each other.
Scene Graph
----
Cura's scene graph is a mere tree data structure. This tree contains all scene nodes, which represent the objects in the 3D scene.
The main idea behind the scene tree is that each scene node has a transformation applied to it. The scene nodes can be nested beneath other scene nodes. The transformation of the parents is then also applied to the children. This way you can have scene nodes grouped together and transform the group as a whole. Since the transformations are all linear, this ensures that the elements of this group stay in the same relative position and orientation. It will look as if the whole group is a single object. This idea is very common for games where objects are often composed of multiple 3D models but need to move together as a whole. For Cura it is used to group objects together and to transform the collision area correctly.
A Typical Scene
----
Cura's scene has a few nodes that are always present, and a few nodes that are repeated for every object that the user loads onto their build plate. To give an idea of how a scene normally looks, this is an overview of a typical scene tree for Cura.
* Root
* Camera
* [Build volume](build_volume.md)
* Platform
* Object 1
* Group 1
* Object 2
* Object 3
* Object 1 convex hull node
* Object 2 convex hull node
* Object 3 convex hull node
* Group 1 convex hull node

View file

@ -1,31 +1,28 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional, Union, TYPE_CHECKING
import os.path
import zipfile
import numpy
from typing import List, Optional, Union, TYPE_CHECKING
import Savitar
import numpy
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.SceneNode import SceneNode # For typing.
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Settings.ExtruderManager import ExtruderManager
try:
if not TYPE_CHECKING:
@ -52,7 +49,6 @@ class ThreeMFReader(MeshReader):
self._root = None
self._base_name = ""
self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
if transformation == "":
@ -87,17 +83,26 @@ class ThreeMFReader(MeshReader):
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
# \returns Scene node.
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
self._object_count += 1
try:
node_name = savitar_node.getName()
node_id = savitar_node.getId()
except AttributeError:
Logger.log("e", "Outdated version of libSavitar detected! Please update to the newest version!")
node_name = ""
node_id = ""
if node_name == "":
node_name = "Object %s" % self._object_count
if file_name != "":
node_name = os.path.basename(file_name)
else:
node_name = "Object {}".format(node_id)
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name)
um_node.setId(node_id)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
@ -169,7 +174,6 @@ class ThreeMFReader(MeshReader):
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
result = []
self._object_count = 0 # Used to name objects as there is no node name yet.
# The base object of 3mf is a zipped archive.
try:
archive = zipfile.ZipFile(file_name, "r")

View file

@ -738,11 +738,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
@staticmethod
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
result = dict() # type: Dict[str, Dict[str, Any]]
try:
archive = zipfile.ZipFile(file_name, "r")
except zipfile.BadZipFile:
Logger.logException("w", "Unable to retrieve metadata from {fname}: 3MF archive is corrupt.".format(fname = file_name))
return result
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
result = dict()
for metadata_file in metadata_files:
try:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for reading 3MF files.",
"api": "7.1",
"api": "7.2.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.1",
"api": "7.2.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.1.0"
"api": "7.2.0"
}

View file

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

View file

@ -489,7 +489,7 @@ class CuraEngineBackend(QObject, Backend):
#
# \param source The scene node that was changed.
def _onSceneChanged(self, source: SceneNode) -> None:
if not source.callDecoration("isSliceable"):
if not source.callDecoration("isSliceable") and source != self._scene.getRoot():
return
# This case checks if the source node is a node that contains GCode. In this case the
@ -720,9 +720,12 @@ class CuraEngineBackend(QObject, Backend):
## Creates a new socket connection.
def _createSocket(self, protocol_file: str = None) -> None:
if not protocol_file:
if not self.getPluginId():
Logger.error("Can't create socket before CuraEngineBackend plug-in is registered.")
return
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if not plugin_path:
Logger.log("e", "Could not get plugin path!", self.getPluginId())
Logger.error("Could not get plugin path!", self.getPluginId())
return
protocol_file = os.path.abspath(os.path.join(plugin_path, "Cura.proto"))
super()._createSocket(protocol_file)

View file

@ -13,6 +13,8 @@ from UM.Job import Job
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerStack import ContainerStack #For typing.
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.SettingRelation import SettingRelation #For typing.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@ -103,20 +105,33 @@ class StartSliceJob(Job):
## Check if a stack has any errors.
## returns true if it has errors, false otherwise.
def _checkStackForErrors(self, stack: ContainerStack) -> bool:
if stack is None:
return False
# if there are no per-object settings we don't need to check the other settings here
stack_top = stack.getTop()
if stack_top is None or not stack_top.getAllKeys():
return False
top_of_stack = cast(InstanceContainer, stack.getTop()) # Cache for efficiency.
changed_setting_keys = top_of_stack.getAllKeys()
for key in stack.getAllKeys():
validation_state = stack.getProperty(key, "validationState")
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state)
# Add all relations to changed settings as well.
for key in top_of_stack.getAllKeys():
instance = top_of_stack.getInstance(key)
if instance is None:
continue
self._addRelations(changed_setting_keys, instance.definition.relations)
Job.yieldThread()
for changed_setting_key in changed_setting_keys:
validation_state = stack.getProperty(changed_setting_key, "validationState")
if validation_state is None:
definition = cast(SettingDefinition, stack.getSettingDefinition(changed_setting_key))
validator_type = SettingDefinition.getValidatorForType(definition.type)
if validator_type:
validator = validator_type(changed_setting_key)
validation_state = validator(stack)
if validation_state in (
ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", changed_setting_key, validation_state)
return True
Job.yieldThread()
return False
## Runs the job that initiates the slicing.
@ -511,4 +526,3 @@ class StartSliceJob(Job):
relations_set.add(relation.target.key)
self._addRelations(relations_set, relation.target.relations)

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend",
"author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.",
"api": "7.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.0",
"i18n-catalog":"cura"
}

View file

@ -9,7 +9,7 @@ from UM.Version import Version
import urllib.request
from urllib.error import URLError
from typing import Dict, Optional
from typing import Dict
import ssl
import certifi

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Checks for firmware updates.",
"api": "7.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

@ -43,7 +43,7 @@ UM.Dialog
TextField {
id: peak_height
objectName: "Peak_Height"
validator: RegExpValidator {regExp: /^-?\d{1,3}([\,|\.]\d*)?$/}
validator: RegExpValidator {regExp: /^\d{1,3}([\,|\.]\d*)?$/}
width: 180 * screenScaleFactor
onTextChanged: { manager.onPeakHeightChanged(text) }
}

View file

@ -50,7 +50,7 @@ class ImageReader(MeshReader):
size = max(self._ui.getWidth(), self._ui.getDepth())
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher, self._ui.use_transparency_model, self._ui.transmittance_1mm)
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm):
def _generateSceneNode(self, file_name, xz_size, height_from_base, base_height, blur_iterations, max_size, lighter_is_higher, use_transparency_model, transmittance_1mm):
scene_node = SceneNode()
mesh = MeshBuilder()
@ -68,8 +68,10 @@ class ImageReader(MeshReader):
if img.width() < 2 or img.height() < 2:
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
height_from_base = max(height_from_base, 0)
base_height = max(base_height, 0)
peak_height = max(peak_height, -base_height)
peak_height = base_height + height_from_base
xz_size = max(xz_size, 1)
scale_vector = Vector(xz_size, peak_height, xz_size)

View file

@ -92,19 +92,30 @@ class ImageReaderUI(QObject):
def onOkButtonClicked(self):
self._cancelled = False
self._ui_view.close()
try:
self._ui_lock.release()
except RuntimeError:
# We don't really care if it was held or not. Just make sure it's not held now
pass
@pyqtSlot()
def onCancelButtonClicked(self):
self._cancelled = True
self._ui_view.close()
try:
self._ui_lock.release()
except RuntimeError:
# We don't really care if it was held or not. Just make sure it's not held now
pass
@pyqtSlot(str)
def onWidthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
try:
self._width = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self._width = 0
else:
self._width = 0
@ -117,7 +128,10 @@ class ImageReaderUI(QObject):
def onDepthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0:
try:
self._depth = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self._depth = 0
else:
self._depth = 0
@ -128,15 +142,23 @@ class ImageReaderUI(QObject):
@pyqtSlot(str)
def onBaseHeightChanged(self, value):
if (len(value) > 0):
if len(value) > 0:
try:
self.base_height = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self.base_height = 0
else:
self.base_height = 0
@pyqtSlot(str)
def onPeakHeightChanged(self, value):
if (len(value) > 0):
if len(value) > 0:
try:
self.peak_height = float(value.replace(",", "."))
if self.peak_height < 0:
self.peak_height = 2.5
except ValueError: # Can happen with incomplete numbers, such as "-".
self.peak_height = 2.5 # restore default
else:
self.peak_height = 0

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.1",
"api": "7.2.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.1",
"api": "7.2.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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

@ -79,14 +79,14 @@ class ModelChecker(QObject, Extension):
# This function can be triggered in the middle of a machine change, so do not proceed if the machine change
# has not done yet.
try:
extruder = global_container_stack.extruderList[int(node_extruder_position)]
global_container_stack.extruderList[int(node_extruder_position)]
except IndexError:
Application.getInstance().callLater(lambda: self.onChanged.emit())
return False
if material_shrinkage[node_extruder_position] > shrinkage_threshold:
bbox = node.getBoundingBox()
if bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z:
if bbox is not None and (bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z):
warning_nodes.append(node)
self._caution_message.setText(catalog.i18nc(

View file

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

View file

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

View file

@ -54,7 +54,7 @@ Item
UM.ActiveTool.setProperty("MeshType", type)
}
UM.I18nCatalog { id: catalog; name: "uranium"}
UM.I18nCatalog { id: catalog; name: "cura"}
Column
{

View file

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

View file

@ -16,7 +16,7 @@ from UM.Extension import Extension
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Trust import Trust
from UM.Trust import Trust, TrustBasics
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.CuraApplication import CuraApplication
@ -156,6 +156,23 @@ class PostProcessingPlugin(QObject, Extension):
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadScripts(self, path: str) -> None:
if ApplicationMetadata.IsEnterpriseVersion:
# Delete all __pycache__ not in installation folder, as it may present a security risk.
# It prevents this very strange scenario (should already be prevented on enterprise because signed-fault):
# - Copy an existing script from the postprocessing-script folder to the appdata scripts folder.
# - Also copy the entire __pycache__ folder from the first to the last location.
# - Leave the __pycache__ as is, but write malicious code just before the class begins.
# - It'll execute, despite that the script has not been signed.
# It's not known if these reproduction steps are minimal, but it does at least happen in this case.
install_prefix = os.path.abspath(CuraApplication.getInstance().getInstallPrefix())
try:
is_in_installation_path = os.path.commonpath([install_prefix, path]).startswith(install_prefix)
except ValueError:
is_in_installation_path = False
if not is_in_installation_path:
TrustBasics.removeCached(path)
## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:

View file

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

File diff suppressed because it is too large Load diff

View file

@ -170,7 +170,7 @@ class ColorMix(Script):
modelNumber = 0
for active_layer in data:
modified_gcode = ""
lineIndex = 0;
lineIndex = 0
lines = active_layer.split("\n")
for line in lines:
#dont leave blanks

View file

@ -132,13 +132,12 @@ class PauseAtHeight(Script):
"default_value": 3.3333,
"enabled": "pause_method not in [\\\"griffin\\\", \\\"repetier\\\"]"
},
"redo_layers":
"redo_layer":
{
"label": "Redo Layers",
"description": "Redo a number of previous layers after a pause to increases adhesion.",
"unit": "layers",
"type": "int",
"default_value": 0
"label": "Redo Layer",
"description": "Redo the last layer before the pause, to get the filament flowing again after having oozed a bit during the pause.",
"type": "bool",
"default_value": false
},
"standby_temperature":
{
@ -226,7 +225,7 @@ class PauseAtHeight(Script):
park_y = self.getSettingValueByKey("head_park_y")
move_z = self.getSettingValueByKey("head_move_z")
layers_started = False
redo_layers = self.getSettingValueByKey("redo_layers")
redo_layer = self.getSettingValueByKey("redo_layer")
standby_temperature = self.getSettingValueByKey("standby_temperature")
firmware_retract = Application.getInstance().getGlobalContainerStack().getProperty("machine_firmware_retract", "value")
control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value")
@ -335,15 +334,14 @@ class PauseAtHeight(Script):
if current_e >= 0:
break
# include a number of previous layers
for i in range(1, redo_layers + 1):
prev_layer = data[index - i]
# Maybe redo the last layer.
if redo_layer:
prev_layer = data[index - 1]
layer = prev_layer + layer
# Get extruder's absolute position at the
# beginning of the first layer redone
# beginning of the redone layer.
# see https://github.com/nallath/PostProcessingPlugin/issues/55
if i == redo_layers:
# Get X and Y from the next layer (better position for
# the nozzle)
x, y = self.getNextXY(layer)

View file

@ -29,24 +29,29 @@ class RetractContinue(Script):
current_e = 0
current_x = 0
current_y = 0
current_z = 0
extra_retraction_speed = self.getSettingValueByKey("extra_retraction_speed")
for layer_number, layer in enumerate(data):
lines = layer.split("\n")
for line_number, line in enumerate(lines):
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
if self.getValue(line, "G") in {0, 1}: # Track X,Y,Z location.
current_x = self.getValue(line, "X", current_x)
current_y = self.getValue(line, "Y", current_y)
current_z = self.getValue(line, "Z", current_z)
if self.getValue(line, "G") == 1:
if self.getValue(line, "E"):
if not self.getValue(line, "E"): # Either None or 0: Not a retraction then.
continue
new_e = self.getValue(line, "E")
if new_e >= current_e: # Not a retraction.
if new_e - current_e >= -0.0001: # Not a retraction. Account for floating point rounding errors.
current_e = new_e
continue
# A retracted travel move may consist of multiple commands, due to combing.
# This continues retracting over all of these moves and only unretracts at the end.
delta_line = 1
dx = current_x # Track the difference in X for this move only to compute the length of the travel.
dy = current_y
dz = current_z
while line_number + delta_line < len(lines) and self.getValue(lines[line_number + delta_line], "G") != 1:
travel_move = lines[line_number + delta_line]
if self.getValue(travel_move, "G") != 0:
@ -54,18 +59,20 @@ class RetractContinue(Script):
continue
travel_x = self.getValue(travel_move, "X", dx)
travel_y = self.getValue(travel_move, "Y", dy)
travel_z = self.getValue(travel_move, "Z", dz)
f = self.getValue(travel_move, "F", "no f")
length = math.sqrt((travel_x - dx) * (travel_x - dx) + (travel_y - dy) * (travel_y - dy)) # Length of the travel move.
length = math.sqrt((travel_x - dx) * (travel_x - dx) + (travel_y - dy) * (travel_y - dy) + (travel_z - dz) * (travel_z - dz)) # Length of the travel move.
new_e -= length * extra_retraction_speed # New retraction is by ratio of this travel move.
if f == "no f":
new_travel_move = "G1 X{travel_x} Y{travel_y} E{new_e}".format(travel_x = travel_x, travel_y = travel_y, new_e = new_e)
new_travel_move = "G1 X{travel_x} Y{travel_y} Z{travel_z} E{new_e}".format(travel_x = travel_x, travel_y = travel_y, travel_z = travel_z, new_e = new_e)
else:
new_travel_move = "G1 F{f} X{travel_x} Y{travel_y} E{new_e}".format(f = f, travel_x = travel_x, travel_y = travel_y, new_e = new_e)
new_travel_move = "G1 F{f} X{travel_x} Y{travel_y} Z{travel_z} E{new_e}".format(f = f, travel_x = travel_x, travel_y = travel_y, travel_z = travel_z, new_e = new_e)
lines[line_number + delta_line] = new_travel_move
delta_line += 1
dx = travel_x
dy = travel_y
dz = travel_z
current_e = new_e

View file

@ -10,10 +10,10 @@ WARNING This script has never been tested with several extruders
from ..Script import Script
import numpy as np
from UM.Logger import Logger
from UM.Application import Application
import re
from cura.Settings.ExtruderManager import ExtruderManager
def _getValue(line, key, default=None):
"""
Convenience function that finds the value in a line of g-code.
@ -30,6 +30,7 @@ def _getValue(line, key, default=None):
return default
return float(number.group(0))
class GCodeStep():
"""
Class to store the current value of each G_Code parameter
@ -85,7 +86,7 @@ class GCodeStep():
# Execution part of the stretch plugin
class Stretcher():
class Stretcher:
"""
Execution part of the stretch algorithm
"""
@ -207,7 +208,6 @@ class Stretcher():
return False
return True # New sequence
def processLayer(self, layer_steps):
"""
Computes the new coordinates of g-code steps
@ -291,7 +291,6 @@ class Stretcher():
else:
self.layergcode = self.layergcode + layer_steps[i].comment + "\n"
def workOnSequence(self, orig_seq, modif_seq):
"""
Computes new coordinates for a sequence

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a prepare stage in Cura.",
"api": "7.1",
"api": "7.2.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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

@ -49,7 +49,7 @@ class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
def performEjectDevice(self, device):
p = subprocess.Popen(["diskutil", "eject", device.getId()], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
output = p.communicate()
p.communicate()
return_code = p.wait()
if return_code != 0:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1",
"api": "7.1",
"api": "7.2.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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

@ -77,7 +77,7 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_max_thickness", 1)
self._layer_shader.setUniformValue("u_min_thickness", 0)
self._layer_shader.setUniformValue("u_layer_view_type", 1)
self._layer_shader.setUniformValue("u_extruder_opacity", [1, 1, 1, 1])
self._layer_shader.setUniformValue("u_extruder_opacity", [[1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1], [1, 1, 1, 1]])
self._layer_shader.setUniformValue("u_show_travel_moves", 0)
self._layer_shader.setUniformValue("u_show_helpers", 1)
self._layer_shader.setUniformValue("u_show_skin", 1)

View file

@ -12,6 +12,7 @@ from UM.Event import Event, KeyEvent
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.Color import Color
from UM.Math.Matrix import Matrix
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Message import Message
from UM.Platform import Platform
@ -139,7 +140,7 @@ class SimulationView(CuraView):
def _resetSettings(self) -> None:
self._layer_view_type = 0 # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._extruder_opacity = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
self._show_travel_moves = False
self._show_helpers = True
self._show_skin = True
@ -308,15 +309,17 @@ class SimulationView(CuraView):
## Set the extruder opacity
#
# \param extruder_nr 0..3
# \param extruder_nr 0..15
# \param opacity 0.0 .. 1.0
def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
if 0 <= extruder_nr <= 3:
self._extruder_opacity[extruder_nr] = opacity
if 0 <= extruder_nr <= 15:
self._extruder_opacity[extruder_nr // 4][extruder_nr % 4] = opacity
self.currentLayerNumChanged.emit()
def getExtruderOpacities(self)-> List[float]:
return self._extruder_opacity
def getExtruderOpacities(self) -> Matrix:
# NOTE: Extruder opacities are stored in a matrix for (minor) performance reasons (w.r.t. OpenGL/shaders).
# If more than 16 extruders are called for, this should be converted to a sampler1d.
return Matrix(self._extruder_opacity)
def setShowTravelMoves(self, show):
self._show_travel_moves = show

View file

@ -152,7 +152,7 @@ fragment41core =
u_active_extruder = 0.0
u_shade_factor = 0.60
u_layer_view_type = 0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_extruder_opacity = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
u_show_travel_moves = 0
u_show_helpers = 1

View file

@ -11,7 +11,7 @@ vertex41core =
uniform lowp float u_max_thickness;
uniform lowp float u_min_thickness;
uniform lowp int u_layer_view_type;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
uniform lowp mat4 u_extruder_opacity; // currently only for max 16 extruders, others always visible
uniform highp mat4 u_normalMatrix;
@ -31,7 +31,7 @@ vertex41core =
out highp vec3 v_normal;
out lowp vec2 v_line_dim;
out highp int v_extruder;
out highp vec4 v_extruder_opacity;
out highp mat4 v_extruder_opacity;
out float v_line_type;
out lowp vec4 f_color;
@ -121,7 +121,7 @@ geometry41core =
in vec3 v_normal[];
in vec2 v_line_dim[];
in int v_extruder[];
in vec4 v_extruder_opacity[];
in mat4 v_extruder_opacity[];
in float v_line_type[];
out vec4 f_color;
@ -152,7 +152,7 @@ geometry41core =
float size_x;
float size_y;
if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
return;
}
// See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType
@ -304,7 +304,7 @@ fragment41core =
[defaults]
u_active_extruder = 0.0
u_layer_view_type = 0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_extruder_opacity = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0]

View file

@ -6,7 +6,7 @@ vertex41core =
uniform highp mat4 u_projectionMatrix;
uniform lowp float u_active_extruder;
uniform lowp vec4 u_extruder_opacity; // currently only for max 4 extruders, others always visible
uniform lowp mat4 u_extruder_opacity; // currently only for max 16 extruders, others always visible
uniform highp mat4 u_normalMatrix;
@ -25,7 +25,7 @@ vertex41core =
out highp vec3 v_normal;
out lowp vec2 v_line_dim;
out highp int v_extruder;
out highp vec4 v_extruder_opacity;
out highp mat4 v_extruder_opacity;
out float v_line_type;
out lowp vec4 f_color;
@ -75,7 +75,7 @@ geometry41core =
in vec3 v_normal[];
in vec2 v_line_dim[];
in int v_extruder[];
in vec4 v_extruder_opacity[];
in mat4 v_extruder_opacity[];
in float v_line_type[];
out vec4 f_color;
@ -106,7 +106,7 @@ geometry41core =
float size_x;
float size_y;
if ((v_extruder_opacity[0][v_extruder[0]] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
if ((v_extruder_opacity[0][int(mod(v_extruder[0], 4))][v_extruder[0] / 4] == 0.0) && (v_line_type[0] != 8) && (v_line_type[0] != 9)) {
return;
}
// See LayerPolygon; 8 is MoveCombingType, 9 is RetractionType
@ -256,7 +256,7 @@ fragment41core =
[defaults]
u_active_extruder = 0.0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_extruder_opacity = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0]

View file

@ -157,7 +157,7 @@ fragment41core =
u_active_extruder = 0.0
u_shade_factor = 0.60
u_layer_view_type = 0
u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_extruder_opacity = [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]]
u_show_travel_moves = 0
u_show_helpers = 1

View file

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

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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

@ -1,22 +1,43 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.View.View import View
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Resources import Resources
from PyQt5.QtGui import QOpenGLContext, QImage
from PyQt5.QtCore import QSize
import numpy as np
import time
from UM.Application import Application
from UM.View.RenderBatch import RenderBatch
from UM.Logger import Logger
from UM.Message import Message
from UM.Math.Color import Color
from UM.PluginRegistry import PluginRegistry
from UM.Platform import Platform
from UM.Event import Event
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from UM.i18n import i18nCatalog
from cura.Settings.ExtruderManager import ExtruderManager
from cura import XRayPass
import math
catalog = i18nCatalog("cura")
## Standard view for mesh models.
class SolidView(View):
_show_xray_warning_preference = "view/show_xray_warning"
def __init__(self):
super().__init__()
application = Application.getInstance()
@ -27,13 +48,31 @@ class SolidView(View):
self._non_printing_shader = None
self._support_mesh_shader = None
self._xray_shader = None
self._xray_pass = None
self._xray_composite_shader = None
self._composite_pass = None
self._extruders_model = None
self._theme = None
self._support_angle = 90
self._global_stack = None
Application.getInstance().engineCreatedSignal.connect(self._onGlobalContainerChanged)
self._old_composite_shader = None
self._old_layer_bindings = None
self._next_xray_checking_time = time.time()
self._xray_checking_update_time = 1.0 # seconds
self._xray_warning_cooldown = 60 * 10 # reshow Model error message every 10 minutes
self._xray_warning_message = Message(
catalog.i18nc("@info:status", "Your model is not manifold. The highlighted areas indicate either missing or extraneous surfaces."),
lifetime = 60 * 5, # leave message for 5 minutes
title = catalog.i18nc("@info:title", "Model errors"),
)
application.getPreferences().addPreference(self._show_xray_warning_preference, True)
application.engineCreatedSignal.connect(self._onGlobalContainerChanged)
def _onGlobalContainerChanged(self) -> None:
if self._global_stack:
@ -92,6 +131,42 @@ class SolidView(View):
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
self._support_mesh_shader.setUniformValue("u_width", 5.0)
if not Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
self._xray_shader = None
self._xray_composite_shader = None
if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings():
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
self._old_layer_bindings = None
self._old_composite_shader = None
self._xray_warning_message.hide()
else:
if not self._xray_shader:
self._xray_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray.shader"))
if not self._xray_composite_shader:
self._xray_composite_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "xray_composite.shader"))
theme = Application.getInstance().getTheme()
self._xray_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._xray_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
self._xray_composite_shader.setUniformValue("u_flat_error_color_mix", 0.) # Don't show flat error color in solid-view.
renderer = self.getRenderer()
if not self._composite_pass or not 'xray' in self._composite_pass.getLayerBindings():
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._xray_pass = XRayPass.XRayPass(1, 1)
renderer.addRenderPass(self._xray_pass)
if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite")
self._old_layer_bindings = self._composite_pass.getLayerBindings()
self._composite_pass.setLayerBindings(["default", "selection", "xray"])
self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._xray_composite_shader)
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
@ -175,4 +250,56 @@ class SolidView(View):
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
def endRendering(self):
pass
# check whether the xray overlay is showing badness
if time.time() > self._next_xray_checking_time\
and Application.getInstance().getPreferences().getValue(self._show_xray_warning_preference):
self._next_xray_checking_time = time.time() + self._xray_checking_update_time
xray_img = self._xray_pass.getOutput()
xray_img = xray_img.convertToFormat(QImage.Format_RGB888)
# We can't just read the image since the pixels are aligned to internal memory positions.
# xray_img.byteCount() != xray_img.width() * xray_img.height() * 3
# The byte count is a little higher sometimes. We need to check the data per line, but fast using Numpy.
# See https://stackoverflow.com/questions/5810970/get-raw-data-from-qimage for a description of the problem.
# We can't use that solution though, since it doesn't perform well in Python.
class QImageArrayView:
"""
Class that ducktypes to be a Numpy ndarray.
"""
def __init__(self, qimage):
bits_pointer = qimage.bits()
if bits_pointer is None: # If this happens before there is a window.
self.__array_interface__ = {
"shape": (0, 0),
"typestr": "|u4",
"data": (0, False),
"strides": (1, 3),
"version": 3
}
else:
self.__array_interface__ = {
"shape": (qimage.height(), qimage.width()),
"typestr": "|u4", # Use 4 bytes per pixel rather than 3, since Numpy doesn't support 3.
"data": (int(bits_pointer), False),
"strides": (qimage.bytesPerLine(), 3), # This does the magic: For each line, skip the correct number of bytes. Bytes per pixel is always 3 due to QImage.Format.Format_RGB888.
"version": 3
}
array = np.asarray(QImageArrayView(xray_img)).view(np.dtype({
"r": (np.uint8, 0, "red"),
"g": (np.uint8, 1, "green"),
"b": (np.uint8, 2, "blue"),
"a": (np.uint8, 3, "alpha") # Never filled since QImage was reformatted to RGB888.
}), np.recarray)
if np.any(np.mod(array.r, 2)):
self._next_xray_checking_time = time.time() + self._xray_warning_cooldown
self._xray_warning_message.show()
Logger.log("i", "X-Ray overlay found non-manifold pixels.")
def event(self, event):
if event.type == Event.ViewDeactivateEvent:
if self._composite_pass and 'xray' in self._composite_pass.getLayerBindings():
self.getRenderer().removeRenderPass(self._xray_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
self._xray_warning_message.hide()

View file

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

View file

@ -0,0 +1,50 @@
[shaders]
vertex =
uniform highp mat4 u_modelViewProjectionMatrix;
attribute highp vec4 a_vertex;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
}
fragment =
uniform lowp vec4 u_xray_error;
void main()
{
gl_FragColor = u_xray_error;
}
vertex41core =
#version 410
uniform highp mat4 u_modelViewProjectionMatrix;
in highp vec4 a_vertex;
void main()
{
gl_Position = u_modelViewProjectionMatrix * a_vertex;
}
fragment41core =
#version 410
uniform lowp vec4 u_xray_error;
out vec4 frag_color;
void main()
{
frag_color = u_xray_error;
}
[defaults]
u_xray_error = [1.0, 1.0, 1.0, 1.0]
[bindings]
u_modelViewProjectionMatrix = model_view_projection_matrix
[attributes]
a_vertex = vertex

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.1",
"api": "7.2.0",
"i18n-catalog": "cura"
}

View file

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

View file

@ -1,161 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="-122.4 196.9 50.3 74.1"
enable-background="new -122.4 196.9 50.3 74.1" xml:space="preserve">
<title>logo</title>
<desc>Created with Sketch.</desc>
<g id="homepage" sketch:type="MSPage">
<g id="Home-menu" transform="translate(-35.000000, -37.000000)" sketch:type="MSArtboardGroup">
<g id="hero-3" transform="translate(-792.000000, -68.000000)" sketch:type="MSLayerGroup">
<g id="Group-2">
</g>
</g>
<g id="Menu" sketch:type="MSLayerGroup">
<g id="logo" transform="translate(35.000000, 37.845203)" sketch:type="MSShapeGroup">
<g id="Robot" transform="translate(51.265823, 0.000000)">
<path id="Fill-23" fill="#000000" d="M-139.9,203.6c-0.3,0-0.6,0-0.9,0.1c-0.3,0.1-0.5,0.2-0.7,0.4c0,0,0,0,0,0c0,0,0,0,0,0
c-0.1,0.1-0.2,0.2-0.3,0.3c0,0,0,0,0,0c-0.1,0.1-0.1,0.2-0.2,0.3c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.1,0.2-0.1,0.4
c0,0,0,0,0,0c0,0.1,0,0.3,0,0.4c0,0.3,0.1,0.6,0.2,0.9c0.1,0.3,0.3,0.5,0.5,0.7c0.2,0.2,0.5,0.4,0.7,0.5
c0.3,0.1,0.6,0.2,0.9,0.2c0.1,0,0.2,0,0.3,0c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.2,0c0,0,0.1,0,0.1,0c0.1,0,0.1,0,0.2-0.1
c0,0,0,0,0.1,0c0,0,0,0,0,0c0,0,0,0,0,0c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.3c0.1-0.1,0.2-0.3,0.2-0.4
c0.1-0.3,0.2-0.5,0.2-0.8s-0.1-0.6-0.2-0.9c-0.1-0.2-0.2-0.3-0.3-0.5c0,0,0,0,0,0c-0.1-0.1-0.1-0.1-0.2-0.2
c-0.1-0.1-0.2-0.2-0.3-0.3c-0.1-0.1-0.3-0.2-0.4-0.2C-139.3,203.7-139.6,203.6-139.9,203.6"/>
<path id="Fill-24" fill="#000000" d="M-138.4,211.3c-0.1-0.1-0.1-0.1-0.2-0.1c-0.1,0-0.2-0.1-0.3-0.1l-11.4-0.3
c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0.1-0.2,0.1c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0.1-0.1,0.1-0.1,0.2c0,0,0,0,0,0c0,0,0,0,0,0
c0,0.1-0.1,0.1-0.1,0.2v0.7c0,0.1,0,0.2,0.1,0.3c0,0.1,0.1,0.1,0.2,0.2c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0,0.2,0.1,0.3,0.1
l11.4,0.2c0.1,0,0.2,0,0.3,0c0.1,0,0.1-0.1,0.2-0.1c0.1-0.1,0.1-0.1,0.1-0.2c0-0.1,0.1-0.2,0.1-0.2v-0.6c0-0.1,0-0.2-0.1-0.2
C-138.3,211.5-138.4,211.4-138.4,211.3"/>
<path id="Fill-25" fill="#000000" d="M-151,207.2c0.2,0.2,0.5,0.4,0.9,0.6c0.3,0.1,0.7,0.2,1,0.2c0.1,0,0.2,0,0.4,0
c0,0,0.1,0,0.1,0c0.1,0,0.2,0,0.3-0.1c0,0,0,0,0,0c0.1,0,0.2,0,0.2-0.1c0,0,0,0,0,0c0.1,0,0.3-0.1,0.4-0.2c0,0,0,0,0,0
c0.1-0.1,0.3-0.2,0.4-0.3c0.2-0.2,0.4-0.5,0.6-0.8c0.1-0.3,0.2-0.6,0.2-0.9c0-0.3-0.1-0.7-0.2-1c-0.1-0.3-0.3-0.6-0.6-0.8
c-0.2-0.2-0.5-0.4-0.8-0.6c-0.3-0.1-0.7-0.2-1-0.2c-0.4,0-0.7,0-1,0.1c-0.3,0.1-0.6,0.3-0.8,0.4c0,0-0.1,0-0.1,0.1
c0,0-0.1,0.1-0.1,0.1c-0.1,0.1-0.1,0.1-0.2,0.2c-0.1,0.1-0.1,0.2-0.2,0.2c0,0.1-0.1,0.1-0.1,0.2c0,0,0,0,0,0c0,0,0,0,0,0
c-0.1,0.1-0.1,0.3-0.1,0.4c0,0,0,0,0,0c0,0.1-0.1,0.3-0.1,0.5c0,0.3,0.1,0.7,0.2,1C-151.4,206.7-151.3,207-151,207.2"/>
<path id="Fill-26" fill="#000000" d="M-139.8,235.5h0.9c0.6,0,2.1,0.3,2.1,0.3s-1.5,0.3-2.1,0.3h-0.9c0,0-0.1,0-0.1,0l-0.1,0.8
l-0.1-0.8c-0.7-0.1-1.7-0.2-1.7-0.2s1-0.2,1.7-0.2l-0.3-1.6l-3.2-0.1l-0.2,1.9l1.9,0.1l-1.9,0.1l-0.2,1.8h0.4
c0.9,0,3.1,0.3,3.1,0.3s-2.2,0.3-3.1,0.3h-0.4l-0.1,0.7l-0.1-0.7h-0.7c-0.9,0-3.1-0.3-3.1-0.3s2.2-0.3,3.1-0.3h0.7l-0.2-1.8
l-4.6,0.3l-0.4,1.9l-0.4-1.9l-3.6-0.4l3.6-0.4l0.4-1.8l-5.1-0.2l9.8-0.4l-0.1-1.6l-3-0.2l-0.3,1.6l-0.3-1.6l-5.9-0.4l5.9-0.4
l0.4-1.7l0.3,1.6l3-0.2l0.2-2.1c-0.9-0.1-1.6-0.2-1.6-0.2s0.8-0.1,1.7-0.2l0.2-1.6h-0.6c-1.1,0-3.7-0.4-3.7-0.4
s2.6-0.4,3.7-0.4h0.7l0.1-1.2l0.1,1.2h0.6c1.1,0,3.8,0.4,3.8,0.4s-2.7,0.4-3.8,0.4h-0.5l0.2,1.5c0.4,0,0.8-0.1,1.1-0.1h1.5
c1,0,3.7,0.4,3.7,0.4c0,0-2.6,0.4-3.7,0.4h-1.5c-0.3,0-0.7,0-1.1-0.1l0.2,2l2.5,0.7l-2.5,0.7l-0.1,1.5l3.1-0.1l0.5-3.4l0.5,3.4
l4.2,0.5l-4.2,0.5L-139.8,235.5L-139.8,235.5L-139.8,235.5z M-150.9,227.5h1c0.7,0,2.4,0.3,2.4,0.3s-1.7,0.3-2.4,0.3h-1
c-0.7,0-2.3-0.3-2.3-0.3S-151.6,227.5-150.9,227.5L-150.9,227.5z M-137.4,230.5h0.1c0.4,0,1.3,0.3,1.3,0.3s-0.9,0.3-1.3,0.3
h-0.6c-0.4,0-1.3-0.3-1.3-0.3s0.9-0.3,1.3-0.3H-137.4L-137.4,230.5z M-131.8,221.6c0-0.1,0-0.1-0.1-0.2c0-0.1-0.1-0.1-0.1-0.2
c-0.1,0-0.1-0.1-0.2-0.1c-0.1,0-0.2,0-0.3,0l-25,0.1c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0.1-0.2,0.1c-0.1,0.1-0.1,0.1-0.2,0.2
c0,0.1-0.1,0.2-0.1,0.2c-0.2,1.8-0.3,3.6-0.3,5.4c-0.1,1.8-0.1,3.6-0.1,5.4c0,1.8,0,3.6,0.1,5.4c0.1,1.8,0.2,3.6,0.3,5.4
c0,0.1,0,0.2,0.1,0.2c0,0.1,0.1,0.1,0.2,0.2c0.1,0.1,0.2,0.1,0.2,0.1c0.2,0.1,6.9,0.4,6.9,0.4c0.5-0.3,1-0.6,1.5-0.8
c0.5-0.2,1-0.4,1.6-0.6c0.3-0.1,0.5-0.2,0.8-0.2c0,0,0,0,0,0c0.2,0,0.3-0.1,0.5-0.1c0.1,0,0.2-0.1,0.3-0.1c0,0,0,0,0,0h0
c0,0,0,0,0.1,0c0.1,0,0.1,0,0.2,0c0,0,0,0,0,0c0,0,0,0,0.1,0c0.1,0,0.2,0,0.3,0c0.1,0,0.1,0,0.2,0c0.3,0,0.6-0.1,0.8-0.1
c0.3,0,0.5,0,0.8,0c0.1,0,0.1,0,0.2,0c0.2,0,0.4,0,0.6,0h0c0,0,0.1,0,0.1,0c0,0,0,0,0.1,0h0c0.1,0,0.3,0,0.4,0
c0.1,0,0.2,0,0.3,0c2,0.2,3.8,1.1,3.8,1.1c0.5-0.1,6-1.3,6.1-1.3c0.1,0,0.2-0.1,0.2-0.1c0.1-0.1,0.1-0.1,0.2-0.2
c0-0.1,0.1-0.1,0.1-0.2c0.1-1.7,0.2-3.3,0.3-5c0.1-1.7,0.1-3.3,0.1-5c0-1.7,0-3.3-0.1-5C-131.6,224.9-131.7,223.2-131.8,221.6
L-131.8,221.6z"/>
<path id="Fill-27" fill="#000000" d="M-149.4,233.6l0.4,1.8l4.5,0.3l-0.2-1.9L-149.4,233.6"/>
<path id="Fill-28" fill="#000000" d="M-142.3,260.2c-0.2,0.3-0.2,2.2-0.1,2.6c0.1,0.5,2.1,0.1,2.1,0.1s-1.1-2.1,0.7-2.7
c0.4-0.1,1.6-0.4,2.9-0.6c1.9-0.4,4.1-0.7,4.1-0.7s-1.1-0.3-2.7-0.1l-0.3,0l-0.3,0h0l-3.7,0.5
C-141.4,259.6-142,259.8-142.3,260.2"/>
<path id="Fill-29" fill="#000000" d="M-128.1,243.8v-1.5c0,0,0,0,0,0c0.1-0.1,0.1-0.1,0.2-0.2c0.1-0.1,0.3-0.2,0.4-0.3
c0.1-0.1,0.1-0.2,0.1-0.3c0-0.1,0-0.2-0.1-0.4c0-0.1-0.1-0.3-0.2-0.4c-0.1-0.1-0.2-0.3-0.3-0.3c0,0-0.1-0.1-0.1-0.1v-3.2
c0,0,0.1,0,0.1,0c0.3-0.1,0.6-0.2,0.8-0.4c0,0.1,0.1,0.1,0.1,0.2c0.2,0.3,0.5,0.6,0.7,1.1c0.2,0.4,0.4,0.9,0.5,1.5
C-125.8,239.5-125.2,243-128.1,243.8L-128.1,243.8z M-128.1,234.4c0.3-0.1,0.8-0.2,1.1-0.3c0.1,0,0.2-0.1,0.3-0.1v0.3
c-0.1,0-0.2,0.1-0.2,0.1c-0.4,0.2-0.9,0.4-1.2,0.5V234.4L-128.1,234.4z M-128.1,232.8c0.3,0,0.7-0.1,1.1-0.1
c0.1,0,0.3,0,0.4-0.1v0.3c-0.1,0-0.2,0.1-0.4,0.1c-0.4,0.1-0.8,0.2-1.1,0.3V232.8L-128.1,232.8z M-128.1,231.1
c0.4,0,1,0,1.5,0.1v0.2c-0.5,0.1-1,0.2-1.5,0.3V231.1L-128.1,231.1z M-128.1,229.4c0.3,0.1,0.7,0.1,1.1,0.2
c0.1,0,0.3,0.1,0.4,0.1v0.3c-0.1,0-0.3,0-0.4,0c-0.4,0-0.7,0-1,0V229.4L-128.1,229.4z M-128.1,227.6c0.4,0.1,0.8,0.2,1.2,0.4
c0.1,0,0.2,0.1,0.3,0.1v0.3c-0.1,0-0.2-0.1-0.4-0.1c-0.4-0.1-0.8-0.1-1.1-0.2V227.6L-128.1,227.6z M-128.1,221.4
C-128.1,221.4-128,221.4-128.1,221.4c0.5,0.3,1,1.6,1.3,2.2c0.3,0.6,0.4,1.3,0.4,2.1c0,0.1,0,0.1,0,0.2c0,0.1-0.1,0.1-0.1,0.1
c0,0-0.1,0.1-0.1,0.1c0,0-0.1,0-0.2,0c0,0-0.1,0-0.2,0s-0.1,0-0.2,0c0,0-0.1,0-0.1,0c-0.3,0-0.6,0-0.9,0L-128.1,221.4
L-128.1,221.4z M-129.9,221.4v22.7c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.3-0.3,0.5-0.5,0.7c-0.2,0.2-0.5,0.4-0.7,0.5
c-0.3,0.1-0.6,0.2-0.9,0.2l-1.5,0.1c0,0,0,0,0,0l-0.1-0.3c-0.1-0.4-0.5-0.7-0.8-0.7l-1.6,0.1l-19,1.2c-0.4,0-0.8,0.3-0.9,0.7
l-0.4,0.9c0,0,0,0,0,0l-1.3,0.1c-0.4,0-0.7,0-1-0.1c-0.3-0.1-0.6-0.2-0.9-0.4c-0.2-0.2-0.4-0.4-0.6-0.7
c-0.1-0.3-0.2-0.6-0.2-0.9v-25c0-0.3,0.1-0.7,0.2-0.9c0.1-0.3,0.3-0.6,0.6-0.8c0.2-0.2,0.5-0.4,0.9-0.5
c0.3-0.1,0.6-0.2,0.9-0.2c0,0,0.1,0,0.1,0l1.2,0c0-0.1,0-0.1,0-0.2l0-0.4c0-0.4,0.3-0.7,0.7-0.7l11.4,0.2l1.1,0l2.7,0l1.7,0
l5.2,0.1c0.3,0,0.6,0.2,0.7,0.6c0,0.1,0,0.1,0,0.2l0,0.2c0,0,0,0.1,0,0.1l0.8,0c0.3,0,0.6,0.1,0.9,0.2c0.3,0.1,0.5,0.3,0.7,0.5
c0.2,0.2,0.4,0.4,0.5,0.7C-130,220.7-129.9,221-129.9,221.4L-129.9,221.4L-129.9,221.4z M-137.6,255.8v-6.4c0,0,0-0.1,0-0.1
s0,0,0-0.1c0.1,0,0.3,0,0.4,0l0,2.1c0.7-0.6,1.4-1.2,2.3-0.9c0.4,0.1,0.7,0.3,1,0.4v4.6L-137.6,255.8L-137.6,255.8z
M-141.1,258.1l5.5-0.6l1.7-0.2c0.3,0,0.5,0,0.7,0.1c0.2,0.1,0.4,0.2,0.6,0.4c0.2,0.2,0.3,0.4,0.4,0.7c0,0,0,0.1,0,0.1
c0.1,0.2,0.1,0.5,0.1,0.8v3c-0.4,0.1-1.1,0.1-1.8,0.3c-2.4,0.3-6.1,0.8-7.4,1c-2,0.3-1.9-0.5-1.9-0.5v-2.4c0-0.3,0-0.5,0.1-0.8
c0-0.1,0-0.1,0.1-0.2c0.1-0.3,0.2-0.6,0.4-0.8c0.2-0.2,0.4-0.4,0.6-0.6C-141.6,258.2-141.4,258.1-141.1,258.1L-141.1,258.1z
M-140.6,251.9c0.6,0.4,1.4,0.8,2.2,0.4l0.2-0.1l-0.4,3.7l-3.2,0.3v-6.4c0,0,0-0.1,0-0.2c0.1,0,0.2,0,0.4,0l0,3
C-141.2,252.2-140.8,251.7-140.6,251.9L-140.6,251.9z M-131.6,262.9c0,0,0.1,0,0.1,0c0,0,0.1,0,0.1,0.1c0,0,0.1,0.1,0.1,0.1
c0,0.1,0,0.1,0,0.2v0.4c0,0.1,0,0.1,0,0.2c0,0.1,0,0.1-0.1,0.2c0,0-0.1,0.1-0.1,0.1c0,0-0.1,0-0.1,0.1l-11.8,1.6
c-0.1,0-0.1,0-0.2,0c0,0-0.1,0-0.1-0.1c0,0-0.1-0.1-0.1-0.1c0-0.1,0-0.1,0-0.2v-0.5c0-0.1,0-0.1,0-0.2c0-0.1,0-0.1,0.1-0.2
c0,0,0.1-0.1,0.1-0.1c0,0,0.1-0.1,0.2-0.1l9.4-1.3l0.1,0l0.2,0l0.8-0.1l0.8-0.1L-131.6,262.9L-131.6,262.9z M-145.2,265.7
c0,0.1,0,0.1,0,0.2c0,0.1-0.1,0.1-0.1,0.2c0,0.1-0.1,0.1-0.1,0.1c0,0-0.1,0.1-0.2,0.1l-12.6,1.7c-0.4,0-0.5-0.5-0.3-1l-7.5-6.2
l8.2,5.7c0,0,0.1,0,0.3,0l11.9-1.6c0.1,0,0.1,0,0.2,0c0,0,0.1,0,0.1,0.1c0,0,0.1,0.1,0.1,0.1c0,0.1,0,0.1,0,0.2V265.7
L-145.2,265.7z M-165.1,245.8c-0.1,0-0.2,0-0.2,0.1c-0.1,0-0.2,0-0.3,0.1c-0.1,0-0.1,0-0.2,0c-0.1,0-0.1,0-0.2,0c0,0,0,0,0,0
c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.2,0h0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2,0-0.3,0c0,0,0,0,0,0
c0,0,0,0,0,0c-0.2,0-0.4-0.1-0.6-0.1c0,0-0.1,0-0.1,0c0,0,0,0,0,0c0,0-0.1,0-0.2,0c0,0-0.1,0-0.1-0.1c0,0-0.1,0-0.1,0
c0,0,0,0,0,0c-0.1,0-0.1,0-0.2-0.1c0,0,0,0,0,0c0,0,0,0,0,0c0,0-0.1,0-0.1,0c0,0,0,0,0,0h0c-0.1,0-0.1-0.1-0.2-0.1c0,0,0,0,0,0
c0,0-0.1-0.1-0.1-0.1c0,0,0,0,0,0l0,0c0,0-0.1,0-0.1-0.1c0,0,0,0-0.1,0c0,0,3.1-0.1,3.1-0.1s-1.3-0.8-1.6-1.1
c-0.5-0.5,0.7-2.9,0.8-3.2l-2.4-1.1l-0.5-0.2l0.6,0.1l2.7,0.3c-0.2-0.3-1.6-0.6-2.3-0.9c-0.3-0.1-0.4-0.2-0.3-0.3c0,0,0,0,0,0
c0.1,0,0.3,0,0.5,0.1c0.4,0,0.9,0,1.6,0c1.4,0,2.3-0.1,2.9-0.2c0,0,0,0.1,0.1,0.1c0.2,0.4,0.4,1,0.6,1.6
c0.2,0.6,0.4,1.2,0.4,1.8c0.1,0.6,0.1,1.2-0.1,1.7h0c-0.1,0-0.2,0-0.3,0c-0.1,0-0.2-0.1-0.4-0.1c-0.1,0-0.2-0.1-0.3-0.1
c0,0,0,0,0,0c0,0,0,0,0,0c-0.1,0-0.2-0.1-0.3-0.1c0,0,0-0.1,0-0.2c0-0.1,0-0.2-0.1-0.2c0-0.1,0-0.2-0.1-0.3
c0-0.1-0.1-0.2-0.1-0.2c0-0.1-0.1-0.2-0.1-0.3c-0.1-0.1-0.1-0.1-0.2-0.2c-0.1-0.1-0.1-0.1-0.2-0.1c-0.1,0-0.1,0-0.2,0
c-0.2,0-0.3,0.1-0.4,0.2c-0.1,0.1-0.3,0.3-0.4,0.4c-0.1,0.2-0.2,0.3-0.2,0.5c-0.1,0.2-0.1,0.3-0.1,0.4c0,0.1,0.1,0.2,0.2,0.3
c0.1,0.1,0.3,0.2,0.4,0.3c0.1,0.1,0.2,0.1,0.3,0.2c0.1,0,0.1,0.1,0.2,0.1c0.2,0.1,0.4,0.2,0.5,0.2c0,0.1,0,0.2,0,0.3
c0,0.1,0,0.2,0,0.4c0,0.1,0,0.2-0.1,0.3c0,0.1-0.1,0.2-0.2,0.2C-165,245.7-165.1,245.8-165.1,245.8L-165.1,245.8z
M-163.9,235.5l0,0.3c-0.1,0-0.1,0.1-0.2,0.1c-0.7,0.4-1.5,0.6-1.6,0.6c-0.1,0-0.7,0.1-1.2,0.1c-0.3,0-0.5,0-0.7,0
c-0.3,0-0.4,0-0.5,0c-0.1,0-0.8-0.1-1.4-0.3l0-0.4c0.6,0,1.3,0,1.4,0c0,0,0.2,0,0.5,0c0.2,0,0.4,0,0.7,0c0.6,0,1.1-0.1,1.2-0.1
c0.2,0,0.9-0.1,1.6-0.3C-164.1,235.5-164,235.5-163.9,235.5L-163.9,235.5z M-164.2,234.4c-0.7,0.2-1.4,0.4-1.5,0.4
c-0.1,0-0.6,0.1-1.1,0.1c-0.3,0-0.6,0-0.8,0c-0.2,0-0.3,0-0.3,0c-0.1,0-0.8-0.1-1.5-0.3c0,0-0.1,0-0.1,0l0-0.3c0,0,0.1,0,0.1,0
c0.7,0,1.4-0.1,1.5-0.1c0,0,0.2,0,0.3,0c0.2,0,0.5,0,0.8,0c0.5,0,1-0.1,1.1-0.1c0.1,0,0.8,0,1.5,0c0.1,0,0.2,0,0.3,0l0,0.3
C-164,234.3-164.1,234.4-164.2,234.4L-164.2,234.4z M-169.7,232.6c0.7-0.1,1.7-0.3,1.9-0.3l0.1,0l2,0c0.2,0,1.2,0.1,1.9,0.2
v0.2c-0.7,0.1-1.7,0.3-1.9,0.3l-2,0l-0.1,0c-0.2,0-1.2-0.1-1.9-0.2V232.6L-169.7,232.6z M-163.8,231v0.3c-0.1,0-0.2,0-0.4,0
c-0.7,0-1.5,0-1.6,0c-0.1,0-0.6,0-1.2,0c-0.3,0-0.5,0-0.7,0c-0.2,0-0.4,0-0.5,0c-0.2,0-0.9-0.1-1.6,0v-0.4
c0.7-0.2,1.4-0.3,1.6-0.4c0,0,0.2,0,0.5,0c0.2,0,0.5,0,0.7,0c0.6,0,1.1,0,1.2,0c0.2,0,0.9,0.1,1.6,0.4
C-164,230.9-163.9,230.9-163.8,231L-163.8,231z M-164.1,229.6c-0.7-0.2-1.6-0.2-1.7-0.2c-0.1,0-0.7-0.1-1.3-0.1
c-0.2,0-0.4,0-0.6,0c-0.4,0-0.6,0-0.7,0l-0.6,0c-0.2,0-0.5,0-0.7,0.1V229c0.2-0.1,0.5-0.1,0.7-0.2c0.3-0.1,0.6-0.1,0.7-0.1
c0.1,0,0.3,0,0.7,0c0.2,0,0.4,0,0.6,0c0.6,0,1.2,0.1,1.3,0.1c0.2,0,1,0.2,1.7,0.5c0.1,0,0.2,0.1,0.3,0.1v0.2
C-163.9,229.6-164,229.6-164.1,229.6L-164.1,229.6z M-170.2,226.5c0-0.1,0-0.4,0-0.4c0.2-1.6,4.1-6,4.1-6s1.7-0.5,2.4-0.5
c0.5,0,1,0.4,0.9,1.6c0,0.2-0.4,5-0.4,5c0,0,0,0.1-0.1,0.1c0,0-0.1,0.1-0.1,0.1c-0.7,0.6-5.9,0.6-6.4,0.5
C-170.1,226.9-170.2,226.7-170.2,226.5L-170.2,226.5z M-151.5,257.2l0-6.5c0,0,0.1,0,0.4,0l0,2.2c0.7-0.6,1.5-1.2,2.4-1
c0.5,0.1,0.9,0.3,1.3,0.6v4.4L-151.5,257.2L-151.5,257.2z M-154.7,253.4c0.6,0.5,1.4,0.9,2.3,0.4l0.4-0.2l-0.4,3.7l-3.5,0.4
v-5.6l0-1c0,0,0.2,0,0.5,0l0,3.1C-155.4,253.6-155,253.2-154.7,253.4L-154.7,253.4z M-158.1,262.2c0,0,0-0.3,0.1-0.4
c0-0.1,0-0.2,0.1-0.3c0.1-0.3,0.3-0.6,0.5-0.8c0.2-0.2,0.4-0.5,0.7-0.6c0.3-0.2,0.5-0.3,0.8-0.3l7.9-0.9c0.3,0,0.6,0,0.8,0.1
c0.2,0.1,0.5,0.2,0.7,0.4c0.2,0.2,0.3,0.4,0.4,0.7c0,0.1,0,0.1,0.1,0.2c0.1,0.2,0.1,0.5,0.1,0.7v3.2l-0.4,0l-10.1,1.3l-0.5,0.1
c0,0-1,0.1-1.2-0.6v-1.7c-0.4-0.3-6.4-5.2-6.4-5.2L-158.1,262.2L-158.1,262.2z M-158.8,206.5l1.2-0.1l0.5,0c0,0,0,0,0.1,0
c0,0,0,0,0.1,0c0.1,0,0.2,0,0.3,0.1c0.6,0.4,1,1.7,1,3.4c0,2-0.6,3.6-1.3,3.6v0h0c0,0,0,0,0,0h0c0,0,0,0,0,0h0l-1.3,0l-1.6,0.1
l-0.3,0c0,0,0,0,0.1,0c0.4-0.1,0.7-0.8,0.9-1.8c0.1-0.5,0.1-1.1,0.1-1.7c0-0.7-0.1-1.3-0.2-1.8c-0.2-1-0.6-1.6-0.9-1.6
L-158.8,206.5L-158.8,206.5z M-153.3,196.9c0.4-0.1,0.9-0.1,0.9,0.3c0,0.4,0,1.3,0,1.8h0c-0.3,0.1-0.6,0.3-0.9,0.4V196.9
L-153.3,196.9z M-151.5,200.9c0.3-0.1,0.7-0.2,1-0.1l8.4,0.5l2.8,0.2l0.6,0c0.3,0,0.7,0.1,1,0.2c0.3,0.1,0.6,0.3,0.8,0.5
c0.2,0.2,0.4,0.5,0.5,0.8c0.1,0.3,0.2,0.6,0.2,0.9v10.7c0,0.3-0.1,0.6-0.2,0.9c-0.1,0.3-0.3,0.5-0.5,0.7l-3.9-0.1l-11.7-0.2
c-0.2-0.2-0.3-0.4-0.4-0.6c-0.1-0.3-0.2-0.6-0.2-1v-11.2c0-0.3,0.1-0.7,0.2-0.9c0.1-0.3,0.3-0.5,0.6-0.7
C-152.2,201.2-151.9,201-151.5,200.9L-151.5,200.9z M-139.3,197c0.4-0.1,0.8-0.1,0.8,0.3c0,0.5,0,1.6,0,2.1c0,0,0,0,0,0l-0.8,0
V197L-139.3,197z M-172.7,226.4c-0.2,1.8,0.9,2.2,0.9,2.2v5.8c0,0.1,0,0.1,0,0.2l0,1.2c0,0,0,0.1,0,0.1v0.7
c0,0-0.1,0.1-0.1,0.1c-0.4,0.4-0.7,0.9-1,1.4c-0.4,0.7-0.6,1.3-0.7,2c-0.1,0.9-0.1,1.7,0.2,2.5c0.1,0.3,0.3,0.7,0.5,1
c0.9,1.5,2,2.5,3,3.2c1.8,1.3,5,1,5,1v6.7c-0.1,0.1-0.1,0.1-0.2,0.2c0,0-0.1,0.1-0.1,0.1c-0.2,0.2-0.3,0.3-0.5,0.5
c0,0-0.1,0.1-0.1,0.1c-0.2,0.2-0.3,0.5-0.4,0.7c0,0.1-0.1,0.1-0.1,0.2c-0.1,0.2-0.2,0.4-0.2,0.6c0,0.1,0,0.2-0.1,0.3
c0,0.1,0,0.1,0,0.2c0,0.2,0,0.4,0,0.6v2.1c0,0,0,0,0,0c0,0-0.1,0.1-0.1,0.1c-0.2,0.2-0.3,0.4-0.4,0.7c0,0,0,0.1,0,0.1
c-0.1,0.3-0.1,0.5-0.1,0.8v0.4c0,0.1,0,0.3,0,0.4c0,0.1,0.1,0.3,0.1,0.4c0.1,0.1,0.1,0.3,0.2,0.4c0.1,0.1,0.2,0.3,0.3,0.4
l6.1,5.5c0,0,0,0,0.1,0.1c0.3,0.2,0.6,0.4,0.9,0.5c0.3,0.1,0.7,0.1,1,0.1c0.1,0,0.2,0,0.3,0l12.9-1.8c0.4,0,0.7-0.2,1-0.3
c0.4,0.1,0.7,0.1,1.1,0l11.8-1.6c0.4-0.1,0.8-0.2,1.2-0.4c0.3-0.2,0.5-0.4,0.7-0.7c0.2-0.2,0.3-0.5,0.4-0.8
c0.1-0.3,0.1-0.5,0.1-0.8v-0.4c0-0.3-0.1-0.6-0.2-0.8c-0.1-0.3-0.3-0.5-0.5-0.8c0,0,0,0-0.1-0.1v-2.4c0-1.4-0.4-3.3-2.2-3.8
v-6.8c0.6-0.1,3.7-0.4,3.7-2.7c0.9-0.1,3.4-0.6,4.4-4.2c0.2-0.8,0.3-1.7,0.2-2.6c-0.1-0.7-0.3-1.4-0.6-2
c-0.2-0.5-0.5-1-0.9-1.4c-0.1-0.1-0.1-0.1-0.2-0.2v-8.3c0.2-0.1,0.8-0.4,0.8-1.7c0-0.8-1.9-5.4-2.5-6.1c-0.7-0.8-2-1.8-2.8-2.1
c-0.2-0.1-0.4-0.2-0.5-0.2c-0.6-0.2-1.2-0.3-1.8-0.3c-0.1-0.3-0.4-0.5-0.7-0.5l-1.1,0v-2.6c1.2-0.5,1.9-1.7,1.9-3.3
c0-0.7-0.1-2.5-1.2-3.4c-0.2-0.2-0.4-0.3-0.6-0.4v-2.5l0-0.2c0-0.6-0.1-1.1-0.3-1.6c-0.2-0.5-0.5-1-0.9-1.4
c-0.4-0.4-0.9-0.8-1.4-1c-0.3-0.2-0.7-0.3-1-0.3V197c0-0.5-0.9-1.3-3.8-0.3c0,0,0,0,0,0c-0.4,0.1-0.6,0.5-0.6,0.8v1.7l-8.2-0.5
c-0.4,0-0.8,0-1.2,0.1v-1.9c0-0.5-0.9-1.3-3.8-0.3c-0.4,0.1-0.7,0.5-0.7,0.8v3.9c0,0.1,0,0.3,0,0.3l-3.8,3.1
c-0.4,0-0.9,0.1-1.5,0.2c-1.9,0.4-2.6,3.1-2.6,5.3c0,2,0.5,3.5,1.3,4.4v2.3l-3.8,1.1C-167.3,218.3-172.5,224.6-172.7,226.4
L-172.7,226.4z"/>
<path id="Fill-30" fill="#000000" d="M-157,265.1c0.5,0,4.2-0.5,4.2-0.5s-0.8-0.2-0.5-1.6c0.2-1,2.1-1.4,3.4-1.7
c1.3-0.3,3.1-0.5,3.1-0.5s-1.1-0.3-2.7-0.1l-0.3,0l-0.3,0h0c0,0-5.7,0.7-7.1,1.3c-0.4,0.2-0.4,1.9-0.4,2.7
C-157.6,265.1-157.3,265.1-157,265.1"/>
<path id="Fill-31" fill="#000000" d="M-156,260.4c-0.6,0.1-1,0.9-1,0.9l5-0.7C-152.1,260.5-153.5,260.1-156,260.4"/>
<path id="Fill-32" fill="#000000" d="M-138.6,258.8c0,0-1.4-0.4-2.4-0.3c-0.9,0.1-1.2,1-1.2,1
C-141.7,259-138.7,258.8-138.6,258.8"/>
<path id="Fill-33" fill="#000000" d="M-165.5,226.1c0,0-1.8-1-1.5-1.6c0.5-0.9,1.9-0.6,2.4-1.2c0.8-1,0.6-2.5,0.6-2.5
s-1.7-0.3-2.2,0.3c-0.8,1-3,4.2-3.2,4.6C-170.2,226.7-165.5,226.1-165.5,226.1"/>
<path id="Fill-34" fill="#000000" d="M-157,212.9c0.2,0,0.5-0.6,0.7-1.7c0,0-0.1,0-0.1,0c-0.1-0.1-0.3-0.1-0.4-0.2
c-0.1,0-0.1-0.1-0.2-0.1c-0.2-0.2-0.2-0.4-0.4-0.6c-0.3-0.2-0.6-0.2-0.9-0.1c0,0-0.3,0.1-0.4-0.2c0,0.2,0,0.3,0,0.5
c0,0.5-0.1,1.1-0.2,1.4c-0.1,0.3-0.1,0.6-0.2,0.8c0,0.1-0.1,0.2-0.1,0.3L-157,212.9C-157.1,212.9-157.1,212.9-157,212.9"/>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 17 KiB

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