This commit is contained in:
Remco Burema 2020-10-20 12:36:41 +02:00
commit a8acdd01e4
No known key found for this signature in database
GPG key ID: 215C49431D43F98C
23 changed files with 147 additions and 57 deletions

View file

@ -4,6 +4,7 @@ from typing import List
from UM.Application import Application
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
@ -27,10 +28,14 @@ class ArrangeObjectsJob(Job):
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
status_message.show()
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
found_solution_for_all = None
try:
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
except: # If the thread crashes, the message should still close
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
status_message.hide()
if not found_solution_for_all:
if found_solution_for_all is not None and not found_solution_for_all:
no_full_solution_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to find a location within the build volume for all objects"),

View file

@ -3,6 +3,7 @@ from pynest2d import Point, Box, Item, NfpConfig, nest
from typing import List, TYPE_CHECKING, Optional, Tuple
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Polygon import Polygon
from UM.Math.Quaternion import Quaternion
@ -44,6 +45,9 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
node_items = []
for node in nodes_to_arrange:
hull_polygon = node.callDecoration("getConvexHull")
if not hull_polygon or hull_polygon.getPoints is None:
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
continue
converted_points = []
for point in hull_polygon.getPoints():
converted_points.append(Point(point[0] * factor, point[1] * factor))

View file

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

View file

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

View file

@ -998,6 +998,11 @@ class MachineManager(QObject):
self.activeMaterialChanged.emit()
self.activeIntentChanged.emit()
# Force an update of resolve values
property_names = ["resolve", "validationState"]
for setting_key in self._global_container_stack.getAllKeys():
self._global_container_stack.propertiesChanged.emit(setting_key, property_names)
def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,014 KiB

After

Width:  |  Height:  |  Size: 899 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 KiB

After

Width:  |  Height:  |  Size: 682 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 473 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

View file

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

View file

@ -1,8 +1,9 @@
from unittest.mock import patch, MagicMock
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import Arcus and Savitar first!
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import custom Sip bindings first!
import Savitar # Dont remove this line
import Arcus # No really. Don't. It needs to be there!
import pynest2d # Really!
from UM.Qt.QtApplication import QtApplication # QtApplication import is required, even though it isn't used.
import pytest

View file

@ -231,7 +231,7 @@ Item
target: enabledCheckbox
property: "checked"
value: Cura.MachineManager.activeStack.isEnabled
when: Cura.MachineManger.activeStack != null
when: Cura.MachineManager.activeStack != null
}
/* Use a MouseArea to process the click on this checkbox.

View file

@ -1,3 +1,57 @@
[4.8.0]
* (NOTE: Draft release notes for Beta, these may change for final.)
* New arrange algorithm!
Shoutout to Prusa, since they made the libnest2d library for this, and allowed a licence change.
* When opening a project file, pick any matching printer in addition to just exact match and new definition.
Previously, when someone sent you a project, you either had to have the exact same printer under the exact same name, or create an entirely new instance. Now, in the open project dialog, you can specify any printer that has a(n exactly) matching printer-type.
* Show warning message on profiles that where successfully imported, but not supported by the currently active configuration.
People where a bit confused when adding profiles, which then didn't show up. With this new version, when you add a profile that isn't supported by the current instance (but otherwise correctly imported), you get a warning-message.
* Show parts of the model below the buildplate in a different color.
When viewing the buildplate from below, there's now shadow visible anymore. As this helped the user determine what part of the model was below the buildplate, we decided to color that part differently instead.
* Show the familiar striped pattern for objects outside of the build-volume in Preview mode as well.
Models outside of the build-volume can of course not be sliced. In the Prepare mode, this was already visible with solid objects indicated in the familiar grey-yellow striped pattern. Now you can also see the objects that are still in the scene just outside if the build-volume in Preview mode.
* Iron the top-most bottom layer when spiralizing a solid model, contributed by smartavionics
Ironing was only used for top-layers, or every layer. But what is the biggest flat surface in a vase? This helpful pull request made it so that, in this case, the top-most bottom layer is used to iron on.
* Allow scrolling through setting-tooltips, useful for some plugins.
Certain plugins, such as the very useful Settings Guide, occasionally have very large tooltips. This update allows you to scroll through those.
* Bug Fixes
- Fix the simplify algorithm. Again.
- Fix percentage text-fields when scaling non-uniformly.
- Fix cloud printer stuck in connect/disconnect loop.
- Fix rare crash when processing stair stepping in support.
- Fix sudden increase in tree support branch diameter.
- Fix cases of tree-support resting against vertical wall.
- Fix conical support missing on printers with 'origin at center' set.
- Fix infill multiplier and connected lines settings not cooperating with each other.
- Fixed an issue with skin-edge support, contributed by smartavionics
- Fix printer renaming didn't always stick after restart.
- Fix move after retraction not changing speed if it's a factor 60 greater.
- Fix Windows file alteration detection (reload file popup message appears again).
- OBJ-file reader now doesn't get confused by legal negative indices.
- Fix off-by-one error that could cause horizontal faces to shift one layer upwards.
- Fix out of bounds array and lost checks for segments ended with mesh vertices, contributed bt skarasov
- Remove redundant 'successful responses' variable, contributed by aerotog
* Printer definitions and profiles
- Artillery Sidewinder X1, Artillery Sidewinder Genius, contributed by cataclism
- AnyCubic Kossel, contributed by FoxExe
- BIQU B1, contributed by looxonline
- BLV mgn Cube 300, contributed by wolfgangmauer
- Cocoon Create, Cocoon Create Touch, contributed by thushan
- Creality CR-6 SE, contributed by MatthieuMH
- Flying Bear Ghost 5, contributed by oducceu
- Fused Form 3D (FF300, FF600, FF600+, FFmini), contributed by FusedForm
- Add Acetate profiles for Strateo3D, contributed by KOUBeMT
[4.7.1]
For an overview of the new features in Cura 4.7, please see this video: <a href="https://www.youtube.com/watch?v=vuKuil0dJqE">Change log overview</a>

View file

@ -6,9 +6,10 @@
from unittest.mock import MagicMock, patch
import pytest
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import Arcus and Savitar first!
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import custom Sip bindings first!
import Savitar # Dont remove this line
import Arcus # No really. Don't. It needs to be there!
import pynest2d # Really!
from UM.Qt.QtApplication import QtApplication # QtApplication import is required, even though it isn't used.
# Even though your IDE says these files are not used, don't believe it. It's lying. They need to be there.