Merge branch 'master' into libArachne_rebased

Conflicts:
	cura/CuraApplication.py: Setting version
	fdmprinter and fdmextruder: Setting version
	intents, qualities and variants: Setting version
	Changelog: 4.9.1 was added, should be below Arachne changes
This commit is contained in:
Ghostkeeper 2021-06-16 18:04:54 +02:00
commit 611208368c
No known key found for this signature in database
GPG key ID: D2A8871EE34EC59A
1015 changed files with 103196 additions and 86144 deletions

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from configparser import ConfigParser
@ -412,7 +412,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
quality_container_id = parser["containers"][str(_ContainerIndexes.Quality)]
quality_type = "empty_quality"
if quality_container_id not in ("empty", "empty_quality"):
quality_type = instance_container_info_dict[quality_container_id].parser["metadata"]["quality_type"]
if quality_container_id in instance_container_info_dict:
quality_type = instance_container_info_dict[quality_container_id].parser["metadata"]["quality_type"]
else: # If a version upgrade changed the quality profile in the stack, we'll need to look for it in the built-in profiles instead of the workspace.
quality_matches = ContainerRegistry.getInstance().findContainersMetadata(id = quality_container_id)
if quality_matches: # If there's no profile with this ID, leave it empty_quality.
quality_type = quality_matches[0]["quality_type"]
# Get machine info
serialized = archive.open(global_stack_file).read().decode("utf-8")

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for reading 3MF files.",
"api": "7.5.0",
"api": "7.6.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.5.0",
"api": "7.6.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.5.0"
"api": "7.6.0"
}

View file

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

View file

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

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
"version": "1.0.0",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -65,6 +65,11 @@ Item
model: manager.digitalFactoryFileModel
visible: model.count != 0 && manager.retrievingFileStatus != DF.RetrievalStatus.InProgress
selectionMode: OldControls.SelectionMode.SingleSelection
onDoubleClicked:
{
manager.setSelectedFileIndices([row]);
openFilesButton.clicked();
}
OldControls.TableViewColumn
{

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
import json
import threading
from json import JSONDecodeError
from typing import List, Dict, Any, Callable, Union, Optional
from PyQt5.QtCore import QUrl
@ -43,7 +44,7 @@ class DFFileExportAndUploadManager:
self._library_project_id = library_project_id # type: str
self._library_project_name = library_project_name # type: str
self._file_name = file_name # type: str
self._upload_jobs = [] # type: List[ExportFileJob]
self._formats = formats # type: List[str]
self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
@ -80,6 +81,8 @@ class DFFileExportAndUploadManager:
)
self._generic_success_message.actionTriggered.connect(self._onMessageActionTriggered)
def _onCuraProjectFileExported(self, job: ExportFileJob) -> None:
"""Handler for when the DF Library workspace file (3MF) has been created locally.
@ -271,7 +274,11 @@ class DFFileExportAndUploadManager:
def extractErrorTitle(reply_body: Optional[str]) -> str:
error_title = ""
if reply_body:
reply_dict = json.loads(reply_body)
try:
reply_dict = json.loads(reply_body)
except JSONDecodeError:
Logger.logException("w", "Unable to extract title from reply body")
return error_title
if "errors" in reply_dict and len(reply_dict["errors"]) >= 1 and "title" in reply_dict["errors"][0]:
error_title = reply_dict["errors"][0]["title"]
return error_title
@ -313,8 +320,13 @@ class DFFileExportAndUploadManager:
QDesktopServices.openUrl(QUrl(project_url))
message.hide()
def start(self) -> None:
for job in self._upload_jobs:
job.start()
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
metadata = {}
self._upload_jobs = []
if "3mf" in self._formats and "3mf" in self._file_handlers and self._file_handlers["3mf"]:
filename_3mf = self._file_name + ".3mf"
metadata[filename_3mf] = {
@ -335,7 +347,7 @@ class DFFileExportAndUploadManager:
}
job_3mf = ExportFileJob(self._file_handlers["3mf"], self._nodes, self._file_name, "3mf")
job_3mf.finished.connect(self._onCuraProjectFileExported)
job_3mf.start()
self._upload_jobs.append(job_3mf)
if "ufp" in self._formats and "ufp" in self._file_handlers and self._file_handlers["ufp"]:
filename_ufp = self._file_name + ".ufp"
@ -357,5 +369,5 @@ class DFFileExportAndUploadManager:
}
job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp")
job_ufp.finished.connect(self._onPrintFileExported)
job_ufp.start()
self._upload_jobs.append(job_ufp)
return metadata

View file

@ -385,6 +385,11 @@ class DigitalFactoryController(QObject):
def _applicationInitializationFinished(self) -> None:
self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
# Although Cura supports these, it's super confusing in this context to show them.
for extension in ["jpg", "jpeg", "png", "bmp", "gif"]:
if extension in self._supported_file_types:
del self._supported_file_types[extension]
@pyqtSlot()
def openSelectedFiles(self) -> None:
""" Downloads, then opens all files selected in the Qt frontend open dialog.
@ -541,6 +546,7 @@ class DigitalFactoryController(QObject):
on_upload_success = self.uploadFileSuccess.emit,
on_upload_finished = self.uploadFileFinished.emit,
on_upload_progress = self.uploadFileProgress.emit)
self.file_upload_manager.start()
# Save the project id to make sure it will be preselected the next time the user opens the save dialog
self._current_workspace_information.setEntryToStore("digital_factory", "library_project_id", library_project_id)

View file

@ -40,6 +40,7 @@ class DigitalFactoryFileModel(ListModel):
def setFiles(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None:
if self._files == df_files_in_project:
return
self.clear()
self._files = df_files_in_project
self._update()

View file

@ -21,7 +21,7 @@ class DigitalFactoryProjectModel(ListModel):
dfProjectModelChanged = pyqtSignal()
def __init__(self, parent = None):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self.addRoleName(self.DisplayNameRole, "displayName")
self.addRoleName(self.LibraryProjectIdRole, "libraryProjectId")

View file

@ -0,0 +1,48 @@
from unittest.mock import MagicMock, patch
import pytest
from src.DFFileExportAndUploadManager import DFFileExportAndUploadManager
@pytest.fixture
def upload_manager():
file_handler = MagicMock(name = "file_handler")
file_handler.getSupportedFileTypesWrite = MagicMock(return_value = [{
"id": "test",
"extension": ".3mf",
"description": "nope",
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": "binary",
"hide_in_file_dialog": True,
}])
node = MagicMock(name = "SceneNode")
application = MagicMock(name = "CuraApplication")
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)):
return DFFileExportAndUploadManager(file_handlers = {"3mf": file_handler},
nodes = [node],
library_project_id = "test_library_project_id",
library_project_name = "test_library_project_name",
file_name = "file_name",
formats = ["3mf"],
on_upload_error = MagicMock(),
on_upload_success = MagicMock(),
on_upload_finished = MagicMock(),
on_upload_progress = MagicMock())
@pytest.mark.parametrize("input,expected_result",
[("", ""),
("invalid json! {}", ""),
("{\"errors\": [{}]}", ""),
("{\"errors\": [{\"title\": \"some title\"}]}", "some title")])
def test_extractErrorTitle(upload_manager, input, expected_result):
assert upload_manager.extractErrorTitle(input) == expected_result
def test_exportJobError(upload_manager):
mocked_application = MagicMock()
with patch("UM.Application.Application.getInstance", MagicMock(return_value = mocked_application)):
upload_manager._onJobExportError("file_name.3mf")
# Ensure that message was displayed
mocked_application.showMessageSignal.emit.assert_called_once()

View file

@ -0,0 +1,73 @@
from pathlib import Path
from src.DigitalFactoryFileModel import DigitalFactoryFileModel
from src.DigitalFactoryFileResponse import DigitalFactoryFileResponse
file_1 = DigitalFactoryFileResponse(client_id = "client_id_1",
content_type = "zomg",
file_name = "file_1.3mf",
file_id = "file_id_1",
library_project_id = "project_id_1",
status = "test",
user_id = "user_id_1",
username = "username_1",
uploaded_at = "2021-04-07T10:33:25.000Z")
file_2 = DigitalFactoryFileResponse(client_id ="client_id_2",
content_type = "zomg",
file_name = "file_2.3mf",
file_id = "file_id_2",
library_project_id = "project_id_2",
status = "test",
user_id = "user_id_2",
username = "username_2",
uploaded_at = "2021-02-06T09:33:22.000Z")
file_wtf = DigitalFactoryFileResponse(client_id ="client_id_1",
content_type = "zomg",
file_name = "file_3.wtf",
file_id = "file_id_3",
library_project_id = "project_id_1",
status = "test",
user_id = "user_id_1",
username = "username_1",
uploaded_at = "2021-04-06T12:33:25.000Z")
def test_setFiles():
model = DigitalFactoryFileModel()
assert model.count == 0
model.setFiles([file_1, file_2])
assert model.count == 2
assert model.getItem(0)["fileName"] == "file_1.3mf"
assert model.getItem(1)["fileName"] == "file_2.3mf"
def test_clearProjects():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2])
model.clearFiles()
assert model.count == 0
def test_setProjectMultipleTimes():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2])
model.setFiles([file_2])
assert model.count == 1
assert model.getItem(0)["fileName"] == "file_2.3mf"
def test_setFilter():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2, file_wtf])
model.setFilters({"file_name": lambda x: Path(x).suffix[1:].lower() in ["3mf"]})
assert model.count == 2
model.clearFilters()
assert model.count == 3

View file

@ -0,0 +1,55 @@
from src.DigitalFactoryProjectModel import DigitalFactoryProjectModel
from src.DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
project_1 = DigitalFactoryProjectResponse(library_project_id = "omg",
display_name = "zomg",
username = "nope",
organization_shared = True)
project_2 = DigitalFactoryProjectResponse(library_project_id = "omg2",
display_name = "zomg2",
username = "nope",
organization_shared = False)
def test_setProjects():
model = DigitalFactoryProjectModel()
assert model.count == 0
model.setProjects([project_1, project_2])
assert model.count == 2
assert model.getItem(0)["displayName"] == "zomg"
assert model.getItem(1)["displayName"] == "zomg2"
def test_clearProjects():
model = DigitalFactoryProjectModel()
model.setProjects([project_1, project_2])
model.clearProjects()
assert model.count == 0
def test_setProjectMultipleTimes():
model = DigitalFactoryProjectModel()
model.setProjects([project_1, project_2])
model.setProjects([project_2])
assert model.count == 1
assert model.getItem(0)["displayName"] == "zomg2"
def test_extendProjects():
model = DigitalFactoryProjectModel()
assert model.count == 0
model.setProjects([project_1])
assert model.count == 1
model.extendProjects([project_2])
assert model.count == 2
assert model.getItem(0)["displayName"] == "zomg"
assert model.getItem(1)["displayName"] == "zomg2"

View file

@ -0,0 +1,86 @@
from unittest.mock import MagicMock
import pytest
from cura.CuraApplication import CuraApplication
from src.DigitalFactoryApiClient import DigitalFactoryApiClient
from src.PaginationManager import PaginationManager
@pytest.fixture
def application():
app = MagicMock(spec=CuraApplication, name = "Mocked Cura Application")
return app
@pytest.fixture
def pagination_manager():
manager = MagicMock(name = "Mocked Pagination Manager")
return manager
@pytest.fixture
def api_client(application, pagination_manager):
api_client = DigitalFactoryApiClient(application, MagicMock())
api_client._projects_pagination_mgr = pagination_manager
return api_client
def test_getProjectsFirstPage(api_client):
# setup
http_manager = MagicMock()
api_client._http = http_manager
pagination_manager = api_client._projects_pagination_mgr
pagination_manager.limit = 20
finished_callback = MagicMock()
failed_callback = MagicMock()
# Call
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
# Asserts
pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects
http_manager.get.assert_called_once()
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20"
# Change the limit & try again
http_manager.get.reset_mock()
pagination_manager.limit = 80
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80"
def test_getMoreProjects_noNewProjects(api_client):
api_client.hasMoreProjectsToLoad = MagicMock(return_value = False)
http_manager = MagicMock()
api_client._http = http_manager
finished_callback = MagicMock()
failed_callback = MagicMock()
api_client.getMoreProjects(finished_callback, failed_callback)
http_manager.get.assert_not_called()
def test_getMoreProjects_hasNewProjects(api_client):
api_client.hasMoreProjectsToLoad = MagicMock(return_value = True)
http_manager = MagicMock()
api_client._http = http_manager
finished_callback = MagicMock()
failed_callback = MagicMock()
api_client.getMoreProjects(finished_callback, failed_callback)
http_manager.get.assert_called_once()
def test_clear(api_client):
api_client.clear()
api_client._projects_pagination_mgr.reset.assert_called_once()

View file

@ -0,0 +1,5 @@
# Ensure that the importing for all tests work
import sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))

View file

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

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.",
"api": "7.5.0",
"api": "7.6.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.5.0",
"api": "7.6.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.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -2,7 +2,7 @@
"name": "Model Checker",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": "7.5.0",
"api": "7.6.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.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -73,38 +73,40 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Add all instances that are not added, but are in visibility list
for item in visible:
if settings.getInstance(item) is None: # Setting was not added already.
definition = self._stack.getSettingDefinition(item)
if definition:
new_instance = SettingInstance(definition, settings)
if settings.getInstance(item) is not None: # Setting was added already.
continue
definition = self._stack.getSettingDefinition(item)
if not definition:
Logger.log("w", f"Unable to add instance ({item}) to per-object visibility because we couldn't find the matching definition.")
continue
new_instance = SettingInstance(definition, settings)
stack_nr = -1
stack = None
# Check from what stack we should copy the raw property of the setting from.
if self._stack.getProperty("machine_extruder_count", "value") > 1:
if definition.limit_to_extruder != "-1":
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
# Check if the found stack_number is in the extruder list of extruders.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
stack_nr = -1
stack = None
# Check from what stack we should copy the raw property of the setting from.
if self._stack.getProperty("machine_extruder_count", "value") > 1:
if definition.limit_to_extruder != "-1":
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
# Check if the found stack_number is in the extruder list of extruders.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
stack_nr = -1
# Use the found stack number to get the right stack to copy the value from.
if stack_nr in ExtruderManager.getInstance().extruderIds:
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:
stack = self._stack
# Use the found stack number to get the right stack to copy the value from.
if stack_nr in ExtruderManager.getInstance().extruderIds:
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:
stack = self._stack
# Use the raw property to set the value (so the inheritance doesn't break)
if stack is not None:
new_instance.setProperty("value", stack.getRawProperty(item, "value"))
else:
new_instance.setProperty("value", None)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
visibility_changed = True
else:
Logger.log("w", "Unable to add instance (%s) to per-object visibility because we couldn't find the matching definition", item)
# Use the raw property to set the value (so the inheritance doesn't break)
if stack is not None:
new_instance.setProperty("value", stack.getRawProperty(item, "value"))
else:
new_instance.setProperty("value", None)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
visibility_changed = True
if visibility_changed:
self.visibilityChanged.emit()

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
@ -136,10 +136,12 @@ Item
}
ComboBox
Cura.ComboBox
{
id: infillOnlyComboBox
width: parent.width / 2 - UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("setting_control").height
textRole: "text"
model: ListModel
{

View file

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

View file

@ -1,14 +1,6 @@
# Copyright (c) 2020 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
import pynest2d # @UnusedImport
from . import PostProcessingPlugin

View file

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

View file

@ -1,6 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# Modification 06.09.2020
# add checkbox, now you can choose and use configuration from the firmware itself.
from typing import List
from ..Script import Script
@ -13,7 +16,7 @@ class FilamentChange(Script):
def getSettingDataString(self):
return """{
"name":"Filament Change",
"name": "Filament Change",
"key": "FilamentChange",
"metadata": {},
"version": 2,
@ -27,14 +30,21 @@ class FilamentChange(Script):
"type": "str",
"default_value": "1"
},
"firmware_config":
{
"label": "Use Firmware Configuration",
"description": "Use the settings in your firmware, or customise the parameters of the filament change here.",
"type": "bool",
"default_value": false
},
"initial_retract":
{
"label": "Initial Retraction",
"description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.",
"unit": "mm",
"type": "float",
"default_value": 30.0
"default_value": 30.0,
"enabled": "not firmware_config"
},
"later_retract":
{
@ -42,7 +52,8 @@ class FilamentChange(Script):
"description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.",
"unit": "mm",
"type": "float",
"default_value": 300.0
"default_value": 300.0,
"enabled": "not firmware_config"
},
"x_position":
{
@ -50,7 +61,8 @@ class FilamentChange(Script):
"description": "Extruder X position. The print head will move here for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0
"default_value": 0,
"enabled": "not firmware_config"
},
"y_position":
{
@ -58,7 +70,17 @@ class FilamentChange(Script):
"description": "Extruder Y position. The print head will move here for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0
"default_value": 0,
"enabled": "not firmware_config"
},
"z_position":
{
"label": "Z Position (relative)",
"description": "Extruder relative Z position. Move the print head up for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0,
"minimum_value": 0
}
}
}"""
@ -74,20 +96,26 @@ class FilamentChange(Script):
later_retract = self.getSettingValueByKey("later_retract")
x_pos = self.getSettingValueByKey("x_position")
y_pos = self.getSettingValueByKey("y_position")
z_pos = self.getSettingValueByKey("z_position")
firmware_config = self.getSettingValueByKey("firmware_config")
color_change = "M600"
if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract)
if not firmware_config:
if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract)
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
if x_pos is not None:
color_change = color_change + (" X%.2f" % x_pos)
if y_pos is not None:
color_change = color_change + (" Y%.2f" % y_pos)
if x_pos is not None:
color_change = color_change + (" X%.2f" % x_pos)
if y_pos is not None:
color_change = color_change + (" Y%.2f" % y_pos)
if z_pos is not None and z_pos > 0.:
color_change = color_change + (" Z%.2f" % z_pos)
color_change = color_change + " ; Generated by FilamentChange plugin\n"
@ -101,4 +129,4 @@ class FilamentChange(Script):
if 0 < layer_num < len(data):
data[layer_num] = color_change + data[layer_num]
return data
return data

View file

@ -387,7 +387,7 @@ class PauseAtHeight(Script):
#Retraction
prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n"
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n"
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = 6000) + "\n"
#Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
@ -507,7 +507,15 @@ class PauseAtHeight(Script):
else:
Logger.log("w", "No previous feedrate found in gcode, feedrate for next layer(s) might be incorrect")
prepend_gcode += self.putValue(M = 82) + " ; switch back to absolute E values\n"
extrusion_mode_string = "absolute"
extrusion_mode_numeric = 82
relative_extrusion = Application.getInstance().getGlobalContainerStack().getProperty("relative_extrusion", "value")
if relative_extrusion:
extrusion_mode_string = "relative"
extrusion_mode_numeric = 83
prepend_gcode += self.putValue(M = extrusion_mode_numeric) + " ; switch back to " + extrusion_mode_string + " E values\n"
# reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"

View file

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

View file

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

View file

@ -65,7 +65,7 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if not self._compatibility_mode:
self._layer_shader.setUniformValue("u_starts_color", Color(*Application.getInstance().getTheme().getColor("layerview_starts").getRgb()))
if self._layer_view:
self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
@ -73,6 +73,8 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
self._layer_shader.setUniformValue("u_max_line_width", self._layer_view.getMaxLineWidth())
self._layer_shader.setUniformValue("u_min_line_width", self._layer_view.getMinLineWidth())
self._layer_shader.setUniformValue("u_max_flow_rate", self._layer_view.getMaxFlowRate())
self._layer_shader.setUniformValue("u_min_flow_rate", self._layer_view.getMinFlowRate())
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
@ -86,6 +88,8 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_min_feedrate", 0)
self._layer_shader.setUniformValue("u_max_thickness", 1)
self._layer_shader.setUniformValue("u_min_thickness", 0)
self._layer_shader.setUniformValue("u_max_flow_rate", 1)
self._layer_shader.setUniformValue("u_min_flow_rate", 0)
self._layer_shader.setUniformValue("u_max_line_width", 1)
self._layer_shader.setUniformValue("u_min_line_width", 0)
self._layer_shader.setUniformValue("u_layer_view_type", 1)
@ -174,9 +178,9 @@ class SimulationPass(RenderPass):
self._switching_layers = True
# The first line does not have a previous line: add a MoveCombingType in front for start detection
# this way the first start of the layer can also be drawn
# this way the first start of the layer can also be drawn
prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
# Remove the last element
# Remove the last element
prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size]
layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'}

View file

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import sys
@ -30,6 +30,7 @@ from UM.View.GL.ShaderProgram import ShaderProgram
from UM.i18n import i18nCatalog
from cura.CuraView import CuraView
from cura.LayerPolygon import LayerPolygon # To distinguish line types.
from cura.Scene.ConvexHullNode import ConvexHullNode
from cura.CuraApplication import CuraApplication
@ -93,6 +94,8 @@ class SimulationView(CuraView):
self._min_thickness = sys.float_info.max
self._max_line_width = sys.float_info.min
self._min_line_width = sys.float_info.max
self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min
self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = None
@ -115,6 +118,7 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().addPreference("layerview/show_infill", True)
Application.getInstance().getPreferences().addPreference("layerview/show_starts", True)
self.visibleStructuresChanged.connect(self.calculateColorSchemeLimits)
self._updateWithPreferences()
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
@ -198,6 +202,7 @@ class SimulationView(CuraView):
if node.getMeshData() is None:
return
self.setActivity(False)
self.calculateColorSchemeLimits()
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
@ -218,12 +223,6 @@ class SimulationView(CuraView):
def resetLayerData(self) -> None:
self._current_layer_mesh = None
self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
self._max_line_width = sys.float_info.min
self._min_line_width = sys.float_info.max
def beginRendering(self) -> None:
scene = self.getController().getScene()
@ -248,58 +247,59 @@ class SimulationView(CuraView):
renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
def setLayer(self, value: int) -> None:
"""
Set the upper end of the range of visible layers.
If setting it below the lower end of the range, the lower end is lowered so that 1 layer stays visible.
:param value: The new layer number to show, 0-indexed.
"""
if self._current_layer_num != value:
self._current_layer_num = value
if self._current_layer_num < 0:
self._current_layer_num = 0
if self._current_layer_num > self._max_layers:
self._current_layer_num = self._max_layers
if self._current_layer_num < self._minimum_layer_num:
self._minimum_layer_num = self._current_layer_num
self._current_layer_num = min(max(value, 0), self._max_layers)
self._minimum_layer_num = min(self._current_layer_num, self._minimum_layer_num)
self._startUpdateTopLayers()
self.currentLayerNumChanged.emit()
def setMinimumLayer(self, value: int) -> None:
"""
Set the lower end of the range of visible layers.
If setting it above the upper end of the range, the upper end is increased so that 1 layer stays visible.
:param value: The new lower end of the range of visible layers, 0-indexed.
"""
if self._minimum_layer_num != value:
self._minimum_layer_num = value
if self._minimum_layer_num < 0:
self._minimum_layer_num = 0
if self._minimum_layer_num > self._max_layers:
self._minimum_layer_num = self._max_layers
if self._minimum_layer_num > self._current_layer_num:
self._current_layer_num = self._minimum_layer_num
self._minimum_layer_num = min(max(value, 0), self._max_layers)
self._current_layer_num = max(self._current_layer_num, self._minimum_layer_num)
self._startUpdateTopLayers()
self.currentLayerNumChanged.emit()
def setPath(self, value: int) -> None:
"""
Set the upper end of the range of visible paths on the current layer.
If setting it below the lower end of the range, the lower end is lowered so that 1 path stays visible.
:param value: The new path index to show, 0-indexed.
"""
if self._current_path_num != value:
self._current_path_num = value
if self._current_path_num < 0:
self._current_path_num = 0
if self._current_path_num > self._max_paths:
self._current_path_num = self._max_paths
if self._current_path_num < self._minimum_path_num:
self._minimum_path_num = self._current_path_num
self._current_path_num = min(max(value, 0), self._max_paths)
self._minimum_path_num = min(self._minimum_path_num, self._current_path_num)
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
def setMinimumPath(self, value: int) -> None:
"""
Set the lower end of the range of visible paths on the current layer.
If setting it above the upper end of the range, the upper end is increased so that 1 path stays visible.
:param value: The new lower end of the range of visible paths, 0-indexed.
"""
if self._minimum_path_num != value:
self._minimum_path_num = value
if self._minimum_path_num < 0:
self._minimum_path_num = 0
if self._minimum_path_num > self._max_layers:
self._minimum_path_num = self._max_layers
if self._minimum_path_num > self._current_path_num:
self._current_path_num = self._minimum_path_num
self._minimum_path_num = min(max(value, 0), self._max_paths)
self._current_path_num = max(self._current_path_num, self._minimum_path_num)
self._startUpdateTopLayers()
self.currentPathNumChanged.emit()
def setSimulationViewType(self, layer_view_type: int) -> None:
@ -333,37 +333,52 @@ class SimulationView(CuraView):
# If more than 16 extruders are called for, this should be converted to a sampler1d.
return Matrix(self._extruder_opacity)
def setShowTravelMoves(self, show):
def setShowTravelMoves(self, show: bool) -> None:
if show == self._show_travel_moves:
return
self._show_travel_moves = show
self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowTravelMoves(self):
def getShowTravelMoves(self) -> bool:
return self._show_travel_moves
def setShowHelpers(self, show: bool) -> None:
if show == self._show_helpers:
return
self._show_helpers = show
self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowHelpers(self) -> bool:
return self._show_helpers
def setShowSkin(self, show: bool) -> None:
if show == self._show_skin:
return
self._show_skin = show
self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowSkin(self) -> bool:
return self._show_skin
def setShowInfill(self, show: bool) -> None:
if show == self._show_infill:
return
self._show_infill = show
self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowInfill(self) -> bool:
return self._show_infill
def setShowStarts(self, show: bool) -> None:
if show == self._show_starts:
return
self._show_starts = show
self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowStarts(self) -> bool:
return self._show_starts
@ -398,12 +413,23 @@ class SimulationView(CuraView):
return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_line_width
def getMaxFlowRate(self) -> float:
return self._max_flow_rate
def getMinFlowRate(self) -> float:
if abs(self._min_flow_rate - sys.float_info.max) < 10: # Some lenience due to floating point rounding.
return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_flow_rate
def calculateMaxLayers(self) -> None:
"""
Calculates number of layers, triggers signals if the number of layers changed and makes sure the top layers are
recalculated for legacy layer view.
"""
scene = self.getController().getScene()
self._old_max_layers = self._max_layers
new_max_layers = -1
"""Recalculate num max layers"""
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData")
if not layer_data:
@ -418,19 +444,6 @@ class SimulationView(CuraView):
if len(layer_data.getLayer(layer_id).polygons) < 1:
continue
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_line_width = max(float(p.lineWidths.max()), self._max_line_width)
self._min_line_width = min(float(p.lineWidths.min()), self._min_line_width)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
try:
self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
# the zero) can't be calculated
Logger.log("i", "Min thickness can't be calculated because all the values are zero")
if max_layer_number < layer_id:
max_layer_number = layer_id
if min_layer_number > layer_id:
@ -454,6 +467,87 @@ class SimulationView(CuraView):
self.maxLayersChanged.emit()
self._startUpdateTopLayers()
def calculateColorSchemeLimits(self) -> None:
"""
Calculates the limits of the colour schemes, depending on the layer view data that is visible to the user.
"""
# Before we start, save the old values so that we can tell if any of the spectrums need to change.
old_min_feedrate = self._min_feedrate
old_max_feedrate = self._max_feedrate
old_min_linewidth = self._min_line_width
old_max_linewidth = self._max_line_width
old_min_thickness = self._min_thickness
old_max_thickness = self._max_thickness
old_min_flow_rate = self._min_flow_rate
old_max_flow_rate = self._max_flow_rate
self._min_feedrate = sys.float_info.max
self._max_feedrate = sys.float_info.min
self._min_line_width = sys.float_info.max
self._max_line_width = sys.float_info.min
self._min_thickness = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min
# The colour scheme is only influenced by the visible lines, so filter the lines by if they should be visible.
visible_line_types = []
if self.getShowSkin(): # Actually "shell".
visible_line_types.append(LayerPolygon.SkinType)
visible_line_types.append(LayerPolygon.Inset0Type)
visible_line_types.append(LayerPolygon.InsetXType)
if self.getShowInfill():
visible_line_types.append(LayerPolygon.InfillType)
if self.getShowHelpers():
visible_line_types.append(LayerPolygon.PrimeTowerType)
visible_line_types.append(LayerPolygon.SkirtType)
visible_line_types.append(LayerPolygon.SupportType)
visible_line_types.append(LayerPolygon.SupportInfillType)
visible_line_types.append(LayerPolygon.SupportInterfaceType)
visible_line_types_with_extrusion = visible_line_types.copy() # Copy before travel moves are added
if self.getShowTravelMoves():
visible_line_types.append(LayerPolygon.MoveCombingType)
visible_line_types.append(LayerPolygon.MoveRetractionType)
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
for layer_index in layer_data.getLayers():
for polyline in layer_data.getLayer(layer_index).polygons:
is_visible = numpy.isin(polyline.types, visible_line_types)
visible_indices = numpy.where(is_visible)[0]
visible_indicies_with_extrusion = numpy.where(numpy.isin(polyline.types, visible_line_types_with_extrusion))[0]
if visible_indices.size == 0: # No items to take maximum or minimum of.
continue
visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices)
visible_feedrates_with_extrusion = numpy.take(polyline.lineFeedrates, visible_indicies_with_extrusion)
visible_linewidths = numpy.take(polyline.lineWidths, visible_indices)
visible_linewidths_with_extrusion = numpy.take(polyline.lineWidths, visible_indicies_with_extrusion)
visible_thicknesses = numpy.take(polyline.lineThicknesses, visible_indices)
visible_thicknesses_with_extrusion = numpy.take(polyline.lineThicknesses, visible_indicies_with_extrusion)
self._max_feedrate = max(float(visible_feedrates.max()), self._max_feedrate)
if visible_feedrates_with_extrusion.size != 0:
flow_rates = visible_feedrates_with_extrusion * visible_linewidths_with_extrusion * visible_thicknesses_with_extrusion
self._min_flow_rate = min(float(flow_rates.min()), self._min_flow_rate)
self._max_flow_rate = max(float(flow_rates.max()), self._max_flow_rate)
self._min_feedrate = min(float(visible_feedrates.min()), self._min_feedrate)
self._max_line_width = max(float(visible_linewidths.max()), self._max_line_width)
self._min_line_width = min(float(visible_linewidths.min()), self._min_line_width)
self._max_thickness = max(float(visible_thicknesses.max()), self._max_thickness)
try:
self._min_thickness = min(float(visible_thicknesses[numpy.nonzero(visible_thicknesses)].min()), self._min_thickness)
except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding the zero) can't be calculated.
Logger.log("w", "Min thickness can't be calculated because all the values are zero")
if old_min_feedrate != self._min_feedrate or old_max_feedrate != self._max_feedrate \
or old_min_linewidth != self._min_line_width or old_max_linewidth != self._max_line_width \
or old_min_thickness != self._min_thickness or old_max_thickness != self._max_thickness \
or old_min_flow_rate != self._min_flow_rate or old_max_flow_rate != self._max_flow_rate:
self.colorSchemeLimitsChanged.emit()
def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
# Update the currentPath
scene = self.getController().getScene()
@ -480,6 +574,8 @@ class SimulationView(CuraView):
preferencesChanged = Signal()
busyChanged = Signal()
activityChanged = Signal()
visibleStructuresChanged = Signal()
colorSchemeLimitsChanged = Signal()
def getProxy(self, engine, script_engine):
"""Hackish way to ensure the proxy is already created
@ -511,6 +607,7 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self.calculateColorSchemeLimits()
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)

View file

@ -90,6 +90,7 @@ Cura.ExpandableComponent
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool show_line_width_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 4
property bool show_flow_rate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 5
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
@ -125,6 +126,10 @@ Cura.ExpandableComponent
text: catalog.i18nc("@label:listbox", "Line Width"),
type_id: 4
})
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Flow"),
type_id: 5
})
}
ComboBox
@ -150,10 +155,13 @@ Cura.ExpandableComponent
{
// Update the visibility of the legends.
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3 || type_id == 4);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode &&
(type_id == 2 || type_id == 3 || type_id == 4 || type_id == 5) ;
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
viewSettings.show_line_width_gradient = viewSettings.show_gradient && (type_id == 4);
viewSettings.show_flow_rate_gradient = viewSettings.show_gradient && (type_id == 5);
}
}
@ -389,18 +397,24 @@ Cura.ExpandableComponent
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
return parseFloat(UM.SimulationView.minFeedrate).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
return parseFloat(UM.SimulationView.minThickness).toFixed(2)
}
//Line width selected
// Line width selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 4)
{
return parseFloat(UM.SimulationView.getMinLineWidth()).toFixed(2);
return parseFloat(UM.SimulationView.minLineWidth).toFixed(2);
}
// Flow Rate selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return parseFloat(UM.SimulationView.minFlowRate).toFixed(2);
}
}
return catalog.i18nc("@label","min")
}
@ -431,6 +445,11 @@ Cura.ExpandableComponent
{
return "mm"
}
// Flow Rate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return "mm³/s"
}
}
return ""
}
@ -448,17 +467,22 @@ Cura.ExpandableComponent
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
return parseFloat(UM.SimulationView.maxFeedrate).toFixed(2)
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
return parseFloat(UM.SimulationView.maxThickness).toFixed(2)
}
//Line width selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 4)
{
return parseFloat(UM.SimulationView.getMaxLineWidth()).toFixed(2);
return parseFloat(UM.SimulationView.maxLineWidth).toFixed(2);
}
// Flow rate selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return parseFloat(UM.SimulationView.maxFlowRate).toFixed(2);
}
}
return catalog.i18nc("@label","max")
@ -474,7 +498,10 @@ Cura.ExpandableComponent
Rectangle
{
id: feedrateGradient
visible: viewSettings.show_feedrate_gradient || viewSettings.show_line_width_gradient
visible: (
viewSettings.show_feedrate_gradient ||
viewSettings.show_line_width_gradient
)
anchors.left: parent.left
anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
@ -526,7 +553,9 @@ Cura.ExpandableComponent
Rectangle
{
id: thicknessGradient
visible: viewSettings.show_thickness_gradient
visible: (
viewSettings.show_thickness_gradient
)
anchors.left: parent.left
anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
@ -578,6 +607,85 @@ Cura.ExpandableComponent
}
}
}
// Gradient colors for flow (similar to jet colormap)
Rectangle
{
id: jetGradient
visible: (
viewSettings.show_flow_rate_gradient
)
anchors.left: parent.left
anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
LinearGradient
{
anchors
{
left: parent.left
leftMargin: UM.Theme.getSize("default_lining").width
right: parent.right
rightMargin: UM.Theme.getSize("default_lining").width
top: parent.top
topMargin: UM.Theme.getSize("default_lining").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_lining").width
}
start: Qt.point(0, 0)
end: Qt.point(parent.width, 0)
gradient: Gradient
{
GradientStop
{
position: 0.0
color: Qt.rgba(0, 0, 0.5, 1)
}
GradientStop
{
position: 0.125
color: Qt.rgba(0, 0.0, 1.0, 1)
}
GradientStop
{
position: 0.25
color: Qt.rgba(0, 0.5, 1.0, 1)
}
GradientStop
{
position: 0.375
color: Qt.rgba(0.0, 1.0, 1.0, 1)
}
GradientStop
{
position: 0.5
color: Qt.rgba(0.5, 1.0, 0.5, 1)
}
GradientStop
{
position: 0.625
color: Qt.rgba(1.0, 1.0, 0.0, 1)
}
GradientStop
{
position: 0.75
color: Qt.rgba(1.0, 0.5, 0, 1)
}
GradientStop
{
position: 0.875
color: Qt.rgba(1.0, 0.0, 0, 1)
}
GradientStop
{
position: 1.0
color: Qt.rgba(0.5, 0, 0, 1)
}
}
}
}
}
FontMetrics

View file

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
@ -28,6 +28,7 @@ class SimulationViewProxy(QObject):
globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal()
colorSchemeLimitsChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged)
def layerActivity(self):
@ -101,30 +102,38 @@ class SimulationViewProxy(QObject):
def getSimulationRunning(self):
return self._simulation_view.isSimulationRunning()
@pyqtSlot(result=float)
def getMinFeedrate(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def minFeedrate(self):
return self._simulation_view.getMinFeedrate()
@pyqtSlot(result=float)
def getMaxFeedrate(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def maxFeedrate(self):
return self._simulation_view.getMaxFeedrate()
@pyqtSlot(result=float)
def getMinThickness(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def minThickness(self):
return self._simulation_view.getMinThickness()
@pyqtSlot(result=float)
def getMaxThickness(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def maxThickness(self):
return self._simulation_view.getMaxThickness()
@pyqtSlot(result=float)
def getMaxLineWidth(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def maxLineWidth(self):
return self._simulation_view.getMaxLineWidth()
@pyqtSlot(result=float)
def getMinLineWidth(self):
@pyqtProperty(float, notify = colorSchemeLimitsChanged)
def minLineWidth(self):
return self._simulation_view.getMinLineWidth()
@pyqtProperty(float, notify=colorSchemeLimitsChanged)
def maxFlowRate(self):
return self._simulation_view.getMaxFlowRate()
@pyqtProperty(float, notify=colorSchemeLimitsChanged)
def minFlowRate(self):
return self._simulation_view.getMinFlowRate()
# Opacity 0..1
@pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity):
@ -153,6 +162,9 @@ class SimulationViewProxy(QObject):
self.currentLayerChanged.emit()
self._layerActivityChanged()
def _onColorSchemeLimitsChanged(self):
self.colorSchemeLimitsChanged.emit()
def _onPathChanged(self):
self.currentPathChanged.emit()
self._layerActivityChanged()
@ -182,6 +194,7 @@ class SimulationViewProxy(QObject):
active_view = self._controller.getActiveView()
if active_view == self._simulation_view:
self._simulation_view.currentLayerNumChanged.connect(self._onLayerChanged)
self._simulation_view.colorSchemeLimitsChanged.connect(self._onColorSchemeLimitsChanged)
self._simulation_view.currentPathNumChanged.connect(self._onPathChanged)
self._simulation_view.maxLayersChanged.connect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.connect(self._onMaxPathsChanged)
@ -194,6 +207,7 @@ class SimulationViewProxy(QObject):
# Disconnect all of em again.
self.is_simulationView_selected = False
self._simulation_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
self._simulation_view.colorSchemeLimitsChanged.connect(self._onColorSchemeLimitsChanged)
self._simulation_view.currentPathNumChanged.disconnect(self._onPathChanged)
self._simulation_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)

View file

@ -12,6 +12,8 @@ vertex41core =
uniform lowp float u_min_thickness;
uniform lowp float u_max_line_width;
uniform lowp float u_min_line_width;
uniform lowp float u_max_flow_rate;
uniform lowp float u_min_flow_rate;
uniform lowp int u_layer_view_type;
uniform lowp mat4 u_extruder_opacity; // currently only for max 16 extruders, others always visible
@ -44,7 +46,15 @@ vertex41core =
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = value;
float green = 1-abs(1-4*value);
if (value > 0.375)
@ -57,7 +67,15 @@ vertex41core =
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = min(max(4*value-2, 0), 1);
float green = min(1.5*value, 0.75);
if (value > 0.75)
@ -70,7 +88,15 @@ vertex41core =
vec4 lineWidthGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value) / (max_value - min_value);
float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = value;
float green = 1 - abs(1 - 4 * value);
if(value > 0.375)
@ -81,6 +107,30 @@ vertex41core =
return vec4(red, green, blue, 1.0);
}
float clamp(float v)
{
float t = v < 0 ? 0 : v;
return t > 1.0 ? 1.0 : t;
}
// Inspired by https://stackoverflow.com/a/46628410
vec4 flowRateGradientColor(float abs_value, float min_value, float max_value)
{
float t;
if(abs(min_value - max_value) < 0.0001)
{
t = 0;
}
else
{
t = 2.0 * ((abs_value - min_value) / (max_value - min_value)) - 1;
}
float red = clamp(1.5 - abs(2.0 * t - 1.0));
float green = clamp(1.5 - abs(2.0 * t));
float blue = clamp(1.5 - abs(2.0 * t + 1.0));
return vec4(red, green, blue, 1.0);
}
void main()
{
vec4 v1_vertex = a_vertex;
@ -106,6 +156,10 @@ vertex41core =
case 4: // "Line width"
v_color = lineWidthGradientColor(a_line_dim.x, u_min_line_width, u_max_line_width);
break;
case 5: // "Flow"
float flow_rate = a_line_dim.x * a_line_dim.y * a_feedrate;
v_color = flowRateGradientColor(flow_rate, u_min_flow_rate, u_max_flow_rate);
break;
}
v_vertex = world_space_vert.xyz;
@ -294,7 +348,6 @@ geometry41core =
EndPrimitive();
}
if ((u_show_starts == 1) && (v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) {
float w = size_x;
float h = size_y;
@ -313,7 +366,7 @@ geometry41core =
myEmitVertex(v_vertex[0] + vec3(-w, -h, -w), u_starts_color, normalize(vec3(-1.0, -1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, -h, -w, 0.0))); // Back-bottom-right
myEmitVertex(v_vertex[0] + vec3( w, h, -w), u_starts_color, normalize(vec3( 1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4( w, h, -w, 0.0))); // Back-top-left
myEmitVertex(v_vertex[0] + vec3(-w, h, -w), u_starts_color, normalize(vec3(-1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, h, -w, 0.0))); // Back-top-right
EndPrimitive();
}
}

View file

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

View file

@ -229,6 +229,11 @@ class SliceInfo(QObject, Extension):
model["model_settings"] = model_settings
if node.source_mime_type is None:
model["mime_type"] = ""
else:
model["mime_type"] = node.source_mime_type.name
data["models"].append(model)
print_times = print_information.printTimes()

View file

@ -54,6 +54,7 @@
<li><b>Bounding Box:</b> [minimum x, y, z; maximum x, y, z]</li>
<li><b>Is Helper Mesh:</b> no</li>
<li><b>Helper Mesh Type:</b> support mesh</li>
<li><b>File type:</b> STL</li>
</ul>
</li>
</ul>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ from typing import cast, List, Dict
from Charon.VirtualFile import VirtualFile # To open UFP files.
from Charon.OpenMode import OpenMode # To indicate that we want to write to UFP files.
from Charon.filetypes.OpenPackagingConvention import OPCError
from io import StringIO # For converting g-code to bytes.
from PyQt5.QtCore import QBuffer
@ -47,35 +48,53 @@ class UFPWriter(MeshWriter):
archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
self._writeObjectList(archive)
try:
self._writeObjectList(archive)
# Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
# Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
gcode_textio = StringIO() # We have to convert the g-code into bytes.
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
success = gcode_writer.write(gcode_textio, None)
if not success: # Writing the g-code failed. Then I can also not write the gzipped g-code.
self.setInformation(gcode_writer.getInformation())
return False
gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
try:
gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
# Attempt to store the thumbnail, if any:
backend = CuraApplication.getInstance().getBackend()
snapshot = None if getattr(backend, "getLatestSnapshot", None) is None else backend.getLatestSnapshot()
if snapshot:
archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png")
try:
archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png")
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG")
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG")
thumbnail.write(thumbnail_buffer.data())
archive.addRelation(virtual_path = "/Metadata/thumbnail.png",
relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
origin = "/3D/model.gcode")
thumbnail.write(thumbnail_buffer.data())
archive.addRelation(virtual_path = "/Metadata/thumbnail.png",
relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
origin = "/3D/model.gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
else:
Logger.log("w", "Thumbnail not created, cannot save it")
@ -90,7 +109,7 @@ class UFPWriter(MeshWriter):
try:
archive.addContentType(extension = material_extension, mime_type = material_mime_type)
except:
except OPCError:
Logger.log("w", "The material extension: %s was already added", material_extension)
added_materials = []
@ -120,17 +139,23 @@ class UFPWriter(MeshWriter):
Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
return False
material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode")
try:
material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
added_materials.append(material_file_name)
try:
archive.close()
except OSError as e:
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)

View file

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

View file

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

View file

@ -7,26 +7,34 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from ..Models.Http.ClusterMaterial import ClusterMaterial
from ..Models.LocalMaterial import LocalMaterial
from ..Messages.MaterialSyncMessage import MaterialSyncMessage
import time
import threading
if TYPE_CHECKING:
from .LocalClusterOutputDevice import LocalClusterOutputDevice
class SendMaterialJob(Job):
"""Asynchronous job to send material profiles to the printer.
This way it won't freeze up the interface while sending those materials.
"""
def __init__(self, device: "LocalClusterOutputDevice") -> None:
super().__init__()
self.device = device # type: LocalClusterOutputDevice
self._send_material_thread = threading.Thread(target = self._sendMissingMaterials)
self._send_material_thread.setDaemon(True)
self._remote_materials = {} # type: Dict[str, ClusterMaterial]
def run(self) -> None:
"""Send the request to the printer and register a callback"""
@ -36,9 +44,15 @@ class SendMaterialJob(Job):
"""Callback for when the remote materials were returned."""
remote_materials_by_guid = {material.guid: material for material in materials}
self._sendMissingMaterials(remote_materials_by_guid)
self._remote_materials = remote_materials_by_guid
# It's not the nicest way to do it, but if we don't handle this in a thread
# we are blocking the main interface (even though the original call was done in a job)
# This should really be refactored so that calculating the list of materials that need to be sent
# to the printer is done outside of the job (and running the job actually sends the materials)
# TODO: Fix this hack that was introduced for 4.9.1
self._send_material_thread.start()
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
def _sendMissingMaterials(self) -> None:
"""Determine which materials should be updated and send them to the printer.
:param remote_materials_by_guid: The remote materials by GUID.
@ -47,7 +61,7 @@ class SendMaterialJob(Job):
if len(local_materials_by_guid) == 0:
Logger.log("d", "There are no local materials to synchronize with the printer.")
return
material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, remote_materials_by_guid)
material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, self._remote_materials)
if len(material_ids_to_send) == 0:
Logger.log("d", "There are no remote materials to update.")
return
@ -96,7 +110,11 @@ class SendMaterialJob(Job):
file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id)
time.sleep(1) # Throttle the sending a bit.
# This needs to be called on the QT thread since the onFinished needs to happen
# in the same thread as where the network manager is located (aka; main thread)
@call_on_qt_thread
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
"""Send a single material file to the printer.

View file

@ -1,9 +1,2 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport
import Savitar #@UnusedImport

View file

@ -70,7 +70,10 @@ class AutoDetectBaudJob(Job):
timeout_time = time() + wait_response_timeout
while timeout_time > time():
line = serial.readline()
# If baudrate is wrong, then readline() might never
# return, even with timeouts set. Using read_until
# with size limit seems to fix this.
line = serial.read_until(size = 100)
if b"ok" in line and b"T:" in line:
self.setResult(baud_rate)
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.1 to Cura 4.2.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.2 to Cura 4.3.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.5 to Cura 4.6.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.6.0 to Cura 4.6.2.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.6.2 to Cura 4.7.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.7 to Cura 4.8.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.8 to Cura 4.9.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,178 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
import io
import os.path # To get the file ID.
from typing import Dict, List, Tuple
from UM.VersionUpgrade import VersionUpgrade
class VersionUpgrade49to410(VersionUpgrade):
"""
Upgrades configurations from the state they were in at version 4.9 to the state they should be in at version 4.10.
"""
_renamed_profiles = {
# Definitions.
"twotrees_bluer" : "two_trees_bluer",
# Upgrade for people who had the original TwoTrees Bluer profiles from 4.9 and earlier.
"twotrees_bluer_extruder_0": "two_trees_base_extruder_0",
"twotrees_bluer_extruder_1": "two_trees_base_extruder_0"
}
_quality_changes_to_two_trees_base = {
"twotrees_bluer_extruder_0",
"twotrees_bluer_extruder_1",
"twotrees_bluer"
}
# Default variant to select for legacy TwoTrees printers, now that we have variants.
_default_variants = {
"twotrees_bluer_extruder_0" : "two_trees_bluer_0.4",
"twotrees_bluer_extruder_1" : "two_trees_bluer_0.4"
}
_two_trees_bluer_quality_type_conversion = {
"high" : "ultra",
"normal" : "super",
"fast" : "standard",
"draft" : "standard",
"extra_fast" : "low",
"coarse" : "draft",
"extra_coarse": "draft"
}
_two_trees_quality_per_material = {
# Since legacy TwoTrees printers didn't have different variants, we always pick the 0.4mm variant.
"generic_abs_175" : {
"high" : "two_trees_0.4_ABS_super",
"normal" : "two_trees_0.4_ABS_super",
"fast" : "two_trees_0.4_ABS_super",
"draft" : "two_trees_0.4_ABS_standard",
"extra_fast" : "two_trees_0.4_ABS_low",
"coarse" : "two_trees_0.4_ABS_low",
"extra_coarse": "two_trees_0.4_ABS_low"
},
"generic_petg_175": {
"high" : "two_trees_0.4_PETG_super",
"normal" : "two_trees_0.4_PETG_super",
"fast" : "two_trees_0.4_PETG_super",
"draft" : "two_trees_0.4_PETG_standard",
"extra_fast" : "two_trees_0.4_PETG_low",
"coarse" : "two_trees_0.4_PETG_low",
"extra_coarse": "two_trees_0.4_PETG_low"
},
"generic_pla_175" : {
"high" : "two_trees_0.4_PLA_super",
"normal" : "two_trees_0.4_PLA_super",
"fast" : "two_trees_0.4_PLA_super",
"draft" : "two_trees_0.4_PLA_standard",
"extra_fast" : "two_trees_0.4_PLA_low",
"coarse" : "two_trees_0.4_PLA_low",
"extra_coarse": "two_trees_0.4_PLA_low"
},
"generic_tpu_175" : {
"high" : "two_trees_0.4_TPU_super",
"normal" : "two_trees_0.4_TPU_super",
"fast" : "two_trees_0.4_TPU_super",
"draft" : "two_trees_0.4_TPU_standard",
"extra_fast" : "two_trees_0.4_TPU_standard",
"coarse" : "two_trees_0.4_TPU_standard",
"extra_coarse": "two_trees_0.4_TPU_standard"
},
"empty_material" : { # For the global stack.
"high" : "two_trees_global_super",
"normal" : "two_trees_global_super",
"fast" : "two_trees_global_super",
"draft" : "two_trees_global_standard",
"extra_fast" : "two_trees_global_low",
"coarse" : "two_trees_global_low",
"extra_coarse": "two_trees_global_low"
}
}
_deltacomb_quality_type_conversion = {
"a" : "D005",
"b" : "D010",
"c" : "D015",
"d" : "D020",
"e" : "D030",
"f" : "D045",
"g" : "D060"
}
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""Upgrades instance containers to have the new version number.
This renames the renamed settings in the containers.
"""
parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized)
# Update setting version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = "17"
if "general" not in parser:
parser["general"] = {}
# Certain instance containers (such as definition changes) reference to a certain definition container
# Since a number of those changed name, we also need to update those.
old_definition = parser.get("general", "definition", fallback = "")
if old_definition in self._renamed_profiles:
parser["general"]["definition"] = self._renamed_profiles[old_definition]
# For quality-changes profiles made for TwoTrees Bluer printers, change the definition to the two_trees_base and make sure that the quality is something we have a profile for.
if parser.get("metadata", "type", fallback = "") == "quality_changes":
for possible_printer in self._quality_changes_to_two_trees_base:
if os.path.basename(filename).startswith(possible_printer + "_"):
parser["general"]["definition"] = "two_trees_base"
parser["metadata"]["quality_type"] = self._two_trees_bluer_quality_type_conversion.get(parser.get("metadata", "quality_type", fallback = "fast"), "standard")
break
if os.path.basename(filename).startswith("deltacomb_"):
parser["general"]["definition"] = "deltacomb_base"
parser["metadata"]["quality_type"] = self._deltacomb_quality_type_conversion.get(parser.get("metadata", "quality_type", fallback = "c"), "D015")
break
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
"""Upgrades stacks to have the new version number."""
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update setting version number.
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = "17"
# Change renamed profiles.
if "containers" in parser:
definition_id = parser["containers"]["7"]
if definition_id in self._quality_changes_to_two_trees_base:
material_id = parser["containers"]["4"]
old_quality_id = parser["containers"]["3"]
if parser["metadata"].get("type", "machine") == "extruder_train":
if parser["containers"]["5"] == "empty_variant":
if definition_id in self._default_variants:
parser["containers"]["5"] = self._default_variants[definition_id] # For legacy TwoTrees printers, change the variant to 0.4.
# Also change the quality to go along with it.
if material_id in self._two_trees_quality_per_material and old_quality_id in self._two_trees_quality_per_material[material_id]:
parser["containers"]["3"] = self._two_trees_quality_per_material[material_id][old_quality_id]
stack_copy = {} # type: Dict[str, str] # Make a copy so that we don't modify the dict we're iterating over.
stack_copy.update(parser["containers"])
for position, profile_id in stack_copy.items():
if profile_id in self._renamed_profiles:
parser["containers"][position] = self._renamed_profiles[profile_id]
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,55 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, TYPE_CHECKING
from . import VersionUpgrade49to410
if TYPE_CHECKING:
from UM.Application import Application
upgrade = VersionUpgrade49to410.VersionUpgrade49to410()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("machine_stack", 5000016): ("machine_stack", 5000017, upgrade.upgradeStack),
("extruder_train", 5000016): ("extruder_train", 5000017, upgrade.upgradeStack),
("definition_changes", 4000016): ("definition_changes", 4000017, upgrade.upgradeInstanceContainer),
("quality_changes", 4000016): ("quality_changes", 4000017, upgrade.upgradeInstanceContainer),
("quality", 4000016): ("quality", 4000017, upgrade.upgradeInstanceContainer),
("user", 4000016): ("user", 4000017, upgrade.upgradeInstanceContainer),
},
"sources": {
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app: "Application") -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 4.9 to 4.10",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 4.9 to Cura 4.10.",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Seva Alekseyev, Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for reading X3D files.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides the X-Ray view.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides capabilities to read and write XML-based material profiles.",
"api": "7.5.0",
"api": "7.6.0",
"i18n-catalog": "cura"
}

View file

@ -1,9 +1,5 @@
from unittest.mock import patch, MagicMock
# 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