Merge branch 'master' into feature_extruder_warning_icon
# Conflicts: # resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml
2
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
name: Bug report
|
||||
name: Old Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
|
|
49
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||
|
||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application version**
|
||||
(The version of the application this issue occurs with.)
|
||||
|
||||
**Platform**
|
||||
(Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.)
|
||||
|
||||
**Printer**
|
||||
(Which printer was selected in Cura?)
|
||||
|
||||
**Reproduction steps**
|
||||
1. (Something you did.)
|
||||
2. (Something you did next.)
|
||||
|
||||
**Screenshot(s)**
|
||||
(Image showing the problem, perhaps before/after images.)
|
||||
|
||||
**Actual results**
|
||||
(What happens after the above steps have been followed.)
|
||||
|
||||
**Expected results**
|
||||
(What should happen after the above steps have been followed.)
|
||||
|
||||
**Project file**
|
||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save Project. For big files you may need to use WeTransfer or similar file sharing sites. G-code files are not project files!)
|
||||
|
||||
**Log file**
|
||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||
|
||||
**Additional information**
|
||||
(Extra information relevant to the issue.)
|
82
.github/ISSUE_TEMPLATE/bugreport.yaml
vendored
Normal file
|
@ -0,0 +1,82 @@
|
|||
name: Bug Report
|
||||
description: Create a report to help us fix issues.
|
||||
labels: "Type: Bug"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for using Cura and wanting to report a bug.**
|
||||
|
||||
Before filing, please check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue "For example (3.2.1) Cannot connect to 3rd-party printer". Please do not write things like **Request** or **BUG** in the title, this is what labels are for.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Application Version
|
||||
description: The version of Cura this issue occurs with.
|
||||
placeholder: 4.9.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Information about the operating system the issue occurs on. Include at least the operating system and maybe GPU.
|
||||
placeholder: Windows 10
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Printer
|
||||
description: Which printer was selected in Cura?
|
||||
placeholder: Ultimaker S5
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: Tell us what you did!
|
||||
placeholder: |
|
||||
1. Something you did
|
||||
2. Something you did next
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Actual results
|
||||
description: What happens after the above steps have been followed.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected results
|
||||
description: What should happen after the above steps have been followed.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please be sure to add the following files:
|
||||
* For slicing issues, upload a **project file** that clearly shows the bug.
|
||||
To save a project file go to `File -> Save project`. Please make sure to .zip your project file. For big files you may need to use WeTransfer or similar file sharing sites.
|
||||
G-code files are not project files!
|
||||
* **Screenshots** of showing the problem, perhaps before/after images.
|
||||
* A **log file** for crashes and similar issues.
|
||||
You can find your log file here:
|
||||
Windows: `%APPDATA%\cura\<Cura version>\cura.log` or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
||||
MacOS: `$USER/Library/Application Support/cura/<Cura version>/cura.log`
|
||||
Ubuntu/Linus: `$USER/.local/share/cura/<Cura version>/cura.log`
|
||||
|
||||
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist of files to include
|
||||
options:
|
||||
- label: Log file
|
||||
- label: Project file
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information & file uploads
|
||||
description: You can add these files and additional information that is relevant to the issue in the comments below.
|
||||
validations:
|
||||
required: true
|
||||
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Have questions or need support?
|
||||
url: https://community.ultimaker.com/
|
||||
about: Please get in touch on our Ultimaker Community Forum!
|
23
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Type: New Feature'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
(A clear and concise description of what the problem is. Ex. I'm always frustrated when [...])
|
||||
|
||||
**Describe the solution you'd like**
|
||||
(A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.)
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
(A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out.)
|
||||
|
||||
**Affected users and/or printers**
|
||||
(Who do you think will benefit from this? Is everyone going to benefit from these changes? Or specific kinds of users?)
|
||||
|
||||
**Additional context**
|
||||
(Add any other context or screenshots about the feature request here.)
|
44
.github/ISSUE_TEMPLATE/featurerequest.yaml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
name: Feature Request
|
||||
description: Suggest an idea for this project.
|
||||
labels: "Type: New Feature"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Thank you for using Cura and wanting to suggest a new feature.**
|
||||
|
||||
Before filing, please check if the feature request already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Please do not write things like **Request** or **BUG** in the title, this is what labels are for.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: Please describe a clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.
|
||||
placeholder: I believe this will solve...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered. If possible, think about why these alternatives are not working out.
|
||||
placeholder: The alternatives I've considered are...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Affected users and/or printers
|
||||
description: Who do you think will benefit from this? Is everyone going to benefit from these changes? Or specific kinds of users?
|
||||
placeholder: It will affect...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information & file uploads
|
||||
description: You can add pictures or files to visualize your feature request in the comments below.
|
|
@ -28,7 +28,7 @@ set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account
|
|||
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
|
||||
set(CURA_DIGITAL_FACTORY_URL "" CACHE STRING "Alternative Digital Factory location")
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||
configure_file(${CMAKE_SOURCE_DIR}/com.ultimaker.cura.desktop.in ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop @ONLY)
|
||||
|
||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||
|
||||
|
@ -82,11 +82,11 @@ if(NOT APPLE AND NOT WIN32)
|
|||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
|
||||
endif()
|
||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||
install(FILES ${CMAKE_BINARY_DIR}/com.ultimaker.cura.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES ${CMAKE_SOURCE_DIR}/resources/images/cura-icon.png
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
|
||||
install(FILES cura.appdata.xml
|
||||
install(FILES com.ultimaker.cura.appdata.xml
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||
install(FILES cura.sharedmimeinfo
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
|
||||
|
|
14
README.md
|
@ -10,13 +10,13 @@ For crashes and similar issues, please attach the following information:
|
|||
|
||||
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
||||
* The Cura GUI log file, located at
|
||||
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
||||
* `$USER/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
|
||||
* `$USER/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
|
||||
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
|
||||
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
|
||||
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
|
||||
|
||||
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
||||
|
||||
For additional support, you could also ask in the #cura channel on FreeNode IRC. For help with development, there is also the #cura-dev channel.
|
||||
For additional support, you could also ask in the [#cura channel](https://web.libera.chat/#cura) on [libera.chat](https://libera.chat/). For help with development, there is also the [#cura-dev channel](https://web.libera.chat/#cura-dev).
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
@ -26,10 +26,16 @@ Dependencies
|
|||
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
|
||||
|
||||
For a list of required Python packages, with their recommended version, see `requirements.txt`.
|
||||
|
||||
This list is not exhaustive at the moment, please check the links in the next section for more details.
|
||||
|
||||
Build scripts
|
||||
-------------
|
||||
Please check out [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
|
||||
|
||||
If you want to build the entire environment from scratch before building Cura as well, [cura-build-environment](https://github.com/Ultimaker/cura-build) might be a starting point before cura-build. (Again, see cura-build for more details.)
|
||||
|
||||
Running from Source
|
||||
-------------
|
||||
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
|
||||
<component type="desktop">
|
||||
<id>cura.desktop</id>
|
||||
<id>com.ultimaker.cura.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>LGPL-3.0 and CC-BY-SA-4.0</project_license>
|
||||
<name>Cura</name>
|
||||
|
@ -24,7 +24,9 @@
|
|||
</ul>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/Ultimaker/Cura/master/screenshot.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://ultimaker.com/en/products/cura-software?utm_source=cura&utm_medium=software&utm_campaign=resources</url>
|
||||
<translation type="gettext">Cura</translation>
|
|
@ -40,7 +40,7 @@ class Account(QObject):
|
|||
"""
|
||||
|
||||
# The interval in which sync services are automatically triggered
|
||||
SYNC_INTERVAL = 30.0 # seconds
|
||||
SYNC_INTERVAL = 60.0 # seconds
|
||||
Q_ENUMS(SyncState)
|
||||
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
|
@ -58,6 +58,11 @@ class Account(QObject):
|
|||
manualSyncEnabledChanged = pyqtSignal(bool)
|
||||
updatePackagesEnabledChanged = pyqtSignal(bool)
|
||||
|
||||
CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " \
|
||||
"library.project.read library.project.write cura.printjob.read cura.printjob.write " \
|
||||
"cura.mesh.read cura.mesh.write"
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
@ -79,9 +84,7 @@ class Account(QObject):
|
|||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
|
||||
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||
CLIENT_SCOPES=self.CLIENT_SCOPES,
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||
|
@ -106,7 +109,6 @@ class Account(QObject):
|
|||
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
|
||||
@pyqtProperty(int, notify=syncStateChanged)
|
||||
def syncState(self):
|
||||
return self._sync_state
|
||||
|
@ -175,6 +177,7 @@ class Account(QObject):
|
|||
if error_message:
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
Logger.log("w", "Failed to login: %s", error_message)
|
||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||
self._error_message.show()
|
||||
self._logged_in = False
|
||||
|
|
|
@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
|
|||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "7.4.0"
|
||||
CuraSDKVersion = "7.6.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
|
|
|
@ -36,6 +36,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
|
|||
found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
||||
node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate
|
||||
"""
|
||||
spacing = int(1.5 * factor) # 1.5mm spacing.
|
||||
|
||||
machine_width = build_volume.getWidth()
|
||||
machine_depth = build_volume.getDepth()
|
||||
|
@ -75,7 +76,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
|
|||
# Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
|
||||
clipped_area = area.intersectionConvexHulls(build_plate_polygon)
|
||||
|
||||
if clipped_area.getPoints() is not None: # numpy array has to be explicitly checked against None
|
||||
if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
||||
for point in clipped_area.getPoints():
|
||||
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
||||
|
||||
|
@ -88,7 +89,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
|
|||
converted_points = []
|
||||
hull_polygon = node.callDecoration("getConvexHull")
|
||||
|
||||
if hull_polygon is not None and hull_polygon.getPoints() is not None: # numpy array has to be explicitly checked against None
|
||||
if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
|
||||
for point in hull_polygon.getPoints():
|
||||
converted_points.append(Point(point[0] * factor, point[1] * factor))
|
||||
item = Item(converted_points)
|
||||
|
@ -99,7 +100,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
|
|||
config = NfpConfig()
|
||||
config.accuracy = 1.0
|
||||
|
||||
num_bins = nest(node_items, build_plate_bounding_box, 10000, config)
|
||||
num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
|
||||
|
||||
# Strip the fixed items (previously placed) and the disallowed areas from the results again.
|
||||
node_items = list(filter(lambda item: not item.isFixed(), node_items))
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import numpy
|
||||
import copy
|
||||
from typing import Optional, Tuple, TYPE_CHECKING
|
||||
from typing import Optional, Tuple, TYPE_CHECKING, Union
|
||||
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
|
@ -14,14 +14,14 @@ if TYPE_CHECKING:
|
|||
class ShapeArray:
|
||||
"""Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`"""
|
||||
|
||||
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||
def __init__(self, arr: numpy.ndarray, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||
self.arr = arr
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
self.scale = scale
|
||||
|
||||
@classmethod
|
||||
def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
|
||||
def fromPolygon(cls, vertices: numpy.ndarray, scale: float = 1) -> "ShapeArray":
|
||||
"""Instantiate from a bunch of vertices
|
||||
|
||||
:param vertices:
|
||||
|
@ -98,7 +98,7 @@ class ShapeArray:
|
|||
return offset_shape_arr, hull_shape_arr
|
||||
|
||||
@classmethod
|
||||
def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
|
||||
def arrayFromPolygon(cls, shape: Union[Tuple[int, int], numpy.ndarray], vertices: numpy.ndarray) -> numpy.ndarray:
|
||||
"""Create :py:class:`numpy.ndarray` with dimensions defined by shape
|
||||
|
||||
Fills polygon defined by vertices with ones, all other values zero
|
||||
|
@ -110,7 +110,7 @@ class ShapeArray:
|
|||
:return: numpy array with dimensions defined by shape
|
||||
"""
|
||||
|
||||
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||
base_array = numpy.zeros(shape, dtype = numpy.int32) # type: ignore # Initialize your array of zeros
|
||||
|
||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||
|
||||
|
@ -126,7 +126,7 @@ class ShapeArray:
|
|||
return base_array
|
||||
|
||||
@classmethod
|
||||
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
|
||||
def _check(cls, p1: numpy.ndarray, p2: numpy.ndarray, base_array: numpy.ndarray) -> Optional[numpy.ndarray]:
|
||||
"""Return indices that mark one side of the line, used by arrayFromPolygon
|
||||
|
||||
Uses the line defined by p1 and p2 to check array of
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
@ -6,6 +6,8 @@ from typing import Any, TYPE_CHECKING
|
|||
|
||||
from UM.Logger import Logger
|
||||
|
||||
import time
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
@ -56,8 +58,8 @@ class AutoSave:
|
|||
|
||||
def _onTimeout(self) -> None:
|
||||
self._saving = True # To prevent the save process from triggering another autosave.
|
||||
Logger.log("d", "Autosaving preferences, instances and profiles")
|
||||
|
||||
save_start_time = time.time()
|
||||
self._application.saveSettings()
|
||||
|
||||
Logger.log("d", "Autosaving preferences, instances and profiles took %s seconds", time.time() - save_start_time)
|
||||
self._saving = False
|
||||
|
|
|
@ -5,14 +5,16 @@ import io
|
|||
import os
|
||||
import re
|
||||
import shutil
|
||||
from copy import deepcopy
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
from typing import Dict, Optional, TYPE_CHECKING, List
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
from UM.Version import Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -27,6 +29,11 @@ class Backup:
|
|||
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
|
||||
"""These files should be ignored when making a backup."""
|
||||
|
||||
IGNORED_FOLDERS = [] # type: List[str]
|
||||
|
||||
SECRETS_SETTINGS = ["general/ultimaker_auth_data"]
|
||||
"""Secret preferences that need to obfuscated when making a backup of Cura"""
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
"""Re-use translation catalog"""
|
||||
|
||||
|
@ -43,6 +50,9 @@ class Backup:
|
|||
|
||||
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||
|
||||
# obfuscate sensitive secrets
|
||||
secrets = self._obfuscate()
|
||||
|
||||
# Ensure all current settings are saved.
|
||||
self._application.saveSettings()
|
||||
|
||||
|
@ -67,8 +77,9 @@ class Backup:
|
|||
machine_count = max(len([s for s in files if "machine_instances/" in s]) - 1, 0) # If people delete their profiles but not their preferences, it can still make a backup, and report -1 profiles. Server crashes on this.
|
||||
material_count = max(len([s for s in files if "materials/" in s]) - 1, 0)
|
||||
profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0)
|
||||
plugin_count = len([s for s in files if "plugin.json" in s])
|
||||
|
||||
# We don't store plugins anymore, since if you can make backups, you have an account (and the plugins are
|
||||
# on the marketplace anyway)
|
||||
plugin_count = 0
|
||||
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
||||
self.zip_file = buffer.getvalue()
|
||||
self.meta_data = {
|
||||
|
@ -78,6 +89,8 @@ class Backup:
|
|||
"profile_count": str(profile_count),
|
||||
"plugin_count": str(plugin_count)
|
||||
}
|
||||
# Restore the obfuscated settings
|
||||
self._illuminate(**secrets)
|
||||
|
||||
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
|
||||
"""Make a full archive from the given root path with the given name.
|
||||
|
@ -85,8 +98,7 @@ class Backup:
|
|||
:param root_path: The root directory to archive recursively.
|
||||
:return: The archive as bytes.
|
||||
"""
|
||||
|
||||
ignore_string = re.compile("|".join(self.IGNORED_FILES))
|
||||
ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
|
||||
try:
|
||||
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
|
||||
for root, folders, files in os.walk(root_path):
|
||||
|
@ -123,8 +135,8 @@ class Backup:
|
|||
"Tried to restore a Cura backup without having proper data or meta data."))
|
||||
return False
|
||||
|
||||
current_version = self._application.getVersion()
|
||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||
current_version = Version(self._application.getVersion())
|
||||
version_to_restore = Version(self.meta_data.get("cura_release", "master"))
|
||||
|
||||
if current_version < version_to_restore:
|
||||
# Cannot restore version newer than current because settings might have changed.
|
||||
|
@ -134,8 +146,16 @@ class Backup:
|
|||
"Tried to restore a Cura backup that is higher than the current version."))
|
||||
return False
|
||||
|
||||
# Get the current secrets and store since the back-up doesn't contain those
|
||||
secrets = self._obfuscate()
|
||||
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
try:
|
||||
archive = ZipFile(io.BytesIO(self.zip_file), "r")
|
||||
except LookupError as e:
|
||||
Logger.log("d", f"The following error occurred while trying to restore a Cura backup: {str(e)}")
|
||||
self._showMessage(self.catalog.i18nc("@info:backup_failed", "The following error occurred while trying to restore a Cura backup:") + str(e))
|
||||
return False
|
||||
extracted = self._extractArchive(archive, version_data_dir)
|
||||
|
||||
# Under Linux, preferences are stored elsewhere, so we copy the file to there.
|
||||
|
@ -146,6 +166,12 @@ class Backup:
|
|||
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||
shutil.move(backup_preferences_file, preferences_file)
|
||||
|
||||
# Read the preferences from the newly restored configuration (or else the cached Preferences will override the restored ones)
|
||||
self._application.readPreferencesFromConfiguration()
|
||||
|
||||
# Restore the obfuscated settings
|
||||
self._illuminate(**secrets)
|
||||
|
||||
return extracted
|
||||
|
||||
@staticmethod
|
||||
|
@ -167,9 +193,36 @@ class Backup:
|
|||
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||
Resources.factoryReset()
|
||||
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||
name_list = archive.namelist()
|
||||
for archive_filename in name_list:
|
||||
try:
|
||||
archive.extractall(target_path)
|
||||
archive.extract(archive_filename, target_path)
|
||||
except (PermissionError, EnvironmentError):
|
||||
Logger.logException("e", "Unable to extract the backup due to permission or file system errors.")
|
||||
return False
|
||||
Logger.logException("e", f"Unable to extract the file {archive_filename} from the backup due to permission or file system errors.")
|
||||
CuraApplication.getInstance().processEvents()
|
||||
return True
|
||||
|
||||
def _obfuscate(self) -> Dict[str, str]:
|
||||
"""
|
||||
Obfuscate and remove the secret preferences that are specified in SECRETS_SETTINGS
|
||||
|
||||
:return: a dictionary of the removed secrets. Note: the '/' is replaced by '__'
|
||||
"""
|
||||
preferences = self._application.getPreferences()
|
||||
secrets = {}
|
||||
for secret in self.SECRETS_SETTINGS:
|
||||
secrets[secret.replace("/", "__")] = deepcopy(preferences.getValue(secret))
|
||||
preferences.setValue(secret, None)
|
||||
self._application.savePreferences()
|
||||
return secrets
|
||||
|
||||
def _illuminate(self, **kwargs) -> None:
|
||||
"""
|
||||
Restore the obfuscated settings
|
||||
|
||||
:param kwargs: a dict of obscured preferences. Note: the '__' of the keys will be replaced by '/'
|
||||
"""
|
||||
preferences = self._application.getPreferences()
|
||||
for key, value in kwargs.items():
|
||||
preferences.setValue(key.replace("__", "/"), value)
|
||||
self._application.savePreferences()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Version import Version
|
||||
from cura.Backups.Backup import Backup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -52,6 +53,7 @@ class BackupsManager:
|
|||
|
||||
backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
|
||||
restored = backup.restore()
|
||||
|
||||
if restored:
|
||||
# At this point, Cura will need to restart for the changes to take effect.
|
||||
# We don't want to store the data at this point as that would override the just-restored backup.
|
||||
|
|
|
@ -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 numpy
|
||||
|
@ -281,7 +281,7 @@ class BuildVolume(SceneNode):
|
|||
continue
|
||||
# If the entire node is below the build plate, still mark it as outside.
|
||||
node_bounding_box = node.getBoundingBox()
|
||||
if node_bounding_box and node_bounding_box.top < 0:
|
||||
if node_bounding_box and node_bounding_box.top < 0 and not node.getParent().callDecoration("isGroup"):
|
||||
node.setOutsideBuildArea(True)
|
||||
continue
|
||||
# Mark the node as outside build volume if the set extruder is disabled
|
||||
|
@ -344,9 +344,14 @@ class BuildVolume(SceneNode):
|
|||
|
||||
# Mark the node as outside build volume if the set extruder is disabled
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
try:
|
||||
if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
|
||||
node.setOutsideBuildArea(True)
|
||||
return
|
||||
except IndexError:
|
||||
# If the extruder doesn't exist, also mark it as unprintable.
|
||||
node.setOutsideBuildArea(True)
|
||||
return
|
||||
|
||||
node.setOutsideBuildArea(False)
|
||||
|
||||
|
@ -911,6 +916,8 @@ class BuildVolume(SceneNode):
|
|||
return {}
|
||||
|
||||
for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
|
||||
if len(area) == 0:
|
||||
continue # Numpy doesn't deal well with 0-length arrays, since it can't determine the dimensionality of them.
|
||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||
machine_disallowed_polygons.append(polygon)
|
||||
|
@ -1063,7 +1070,14 @@ class BuildVolume(SceneNode):
|
|||
adhesion_type = adhesion_override
|
||||
if adhesion_type is None:
|
||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
||||
skirt_brim_line_width = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
|
||||
|
||||
# Skirt_brim_line_width is a bit of an odd one out. The primary bit of the skirt/brim is printed
|
||||
# with the adhesion extruder, but it also prints one extra line by all other extruders. As such, the
|
||||
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
|
||||
# the value is.
|
||||
adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value")
|
||||
skirt_brim_line_width = self._global_container_stack.extruderList[int(adhesion_extruder)].getProperty("skirt_brim_line_width", "value")
|
||||
|
||||
initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "value")
|
||||
# Use brim width if brim is enabled OR the prime tower has a brim.
|
||||
if adhesion_type == "brim":
|
||||
|
|
|
@ -67,11 +67,15 @@ class CuraActions(QObject):
|
|||
current_node = parent_node
|
||||
parent_node = current_node.getParent()
|
||||
|
||||
# This was formerly done with SetTransformOperation but because of
|
||||
# unpredictable matrix deconstruction it was possible that mirrors
|
||||
# could manifest as rotations. Centering is therefore done by
|
||||
# moving the node to negative whatever its position is:
|
||||
center_operation = TranslateOperation(current_node, -current_node._position)
|
||||
# Find out where the bottom of the object is
|
||||
bbox = current_node.getBoundingBox()
|
||||
if bbox:
|
||||
center_y = current_node.getWorldPosition().y - bbox.bottom
|
||||
else:
|
||||
center_y = 0
|
||||
|
||||
# Move the object so that it's bottom is on to of the buildplate
|
||||
center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
|
||||
operation.addOperation(center_operation)
|
||||
operation.push()
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
|
||||
|
||||
|
@ -30,6 +31,7 @@ from UM.Operations.SetTransformOperation import SetTransformOperation
|
|||
from UM.Platform import Platform
|
||||
from UM.PluginError import PluginNotFoundError
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Qt.Bindings.FileProviderModel import FileProviderModel
|
||||
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.Camera import Camera
|
||||
|
@ -127,7 +129,7 @@ class CuraApplication(QtApplication):
|
|||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 16
|
||||
SettingVersion = 17
|
||||
|
||||
Created = False
|
||||
|
||||
|
@ -255,6 +257,9 @@ class CuraApplication(QtApplication):
|
|||
from cura.CuraPackageManager import CuraPackageManager
|
||||
self._package_manager_class = CuraPackageManager
|
||||
|
||||
from UM.CentralFileStorage import CentralFileStorage
|
||||
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def ultimakerCloudApiRootUrl(self) -> str:
|
||||
return UltimakerCloudConstants.CuraCloudAPIRoot
|
||||
|
@ -465,6 +470,7 @@ class CuraApplication(QtApplication):
|
|||
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
|
||||
("variant", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.VariantInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("setting_visibility", SettingVisibilityPresetsModel.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.SettingVisibilityPreset, "application/x-uranium-preferences"),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -601,6 +607,15 @@ class CuraApplication(QtApplication):
|
|||
@pyqtSlot()
|
||||
def closeApplication(self) -> None:
|
||||
Logger.log("i", "Close application")
|
||||
|
||||
# Workaround: Before closing the window, remove the global stack.
|
||||
# This is necessary because as the main window gets closed, hundreds of QML elements get updated which often
|
||||
# request the global stack. However as the Qt-side of the Machine Manager is being dismantled, the conversion of
|
||||
# the Global Stack to a QObject fails.
|
||||
# If instead we first take down the global stack, PyQt will just convert `None` to `null` which succeeds, and
|
||||
# the QML code then gets `null` as the global stack and can deal with that as it deems fit.
|
||||
self.getMachineManager().setActiveMachine(None)
|
||||
|
||||
main_window = self.getMainWindow()
|
||||
if main_window is not None:
|
||||
main_window.close()
|
||||
|
@ -693,6 +708,8 @@ class CuraApplication(QtApplication):
|
|||
@pyqtSlot(str)
|
||||
def discardOrKeepProfileChangesClosed(self, option: str) -> None:
|
||||
global_stack = self.getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
if option == "discard":
|
||||
for extruder in global_stack.extruderList:
|
||||
extruder.userChanges.clear()
|
||||
|
@ -756,7 +773,7 @@ class CuraApplication(QtApplication):
|
|||
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
|
||||
self._plugin_registry.loadPlugin("ConsoleLogger")
|
||||
self._plugin_registry.preloaded_plugins.append("ConsoleLogger")
|
||||
|
||||
self._plugin_registry.loadPlugins()
|
||||
|
||||
|
@ -822,6 +839,9 @@ class CuraApplication(QtApplication):
|
|||
self._add_printer_pages_model_without_cancel.initialize(cancellable = False)
|
||||
self._whats_new_pages_model.initialize()
|
||||
|
||||
# Initialize the FileProviderModel
|
||||
self._file_provider_model.initialize(self._onFileProviderEnabledChanged)
|
||||
|
||||
# Detect in which mode to run and execute that mode
|
||||
if self._is_headless:
|
||||
self.runWithoutGUI()
|
||||
|
@ -889,14 +909,14 @@ class CuraApplication(QtApplication):
|
|||
diagonal = self.getBuildVolume().getDiagonalSize()
|
||||
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
|
||||
diagonal = 375
|
||||
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
|
||||
camera.setPosition(Vector(-80, 180, 700) * diagonal / 375)
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
|
||||
# Initialize camera tool
|
||||
camera_tool = controller.getTool("CameraTool")
|
||||
if camera_tool:
|
||||
camera_tool.setOrigin(Vector(0, 100, 0))
|
||||
camera_tool.setOrigin(Vector(0, 30, 0))
|
||||
camera_tool.setZoomRange(0.1, 2000)
|
||||
|
||||
# Initialize camera animations
|
||||
|
@ -1051,6 +1071,13 @@ class CuraApplication(QtApplication):
|
|||
self._simple_mode_settings_manager = SimpleModeSettingsManager()
|
||||
return self._simple_mode_settings_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getFileProviderModel(self) -> FileProviderModel:
|
||||
return self._file_provider_model
|
||||
|
||||
def _onFileProviderEnabledChanged(self):
|
||||
self._file_provider_model.update()
|
||||
|
||||
def event(self, event):
|
||||
"""Handle Qt events"""
|
||||
|
||||
|
@ -1256,10 +1283,11 @@ class CuraApplication(QtApplication):
|
|||
if other_bb is not None:
|
||||
scene_bounding_box = scene_bounding_box + node.getBoundingBox()
|
||||
|
||||
|
||||
if print_information:
|
||||
print_information.setPreSliced(is_block_slicing_node)
|
||||
|
||||
self.getWorkspaceFileHandler().setEnabled(not is_block_slicing_node)
|
||||
|
||||
if not scene_bounding_box:
|
||||
scene_bounding_box = AxisAlignedBox.Null
|
||||
|
||||
|
@ -1466,7 +1494,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
for file_name, nodes in objects_in_filename.items():
|
||||
for node in nodes:
|
||||
job = ReadMeshJob(file_name)
|
||||
file_path = os.path.normpath(os.path.dirname(file_name))
|
||||
job = ReadMeshJob(file_name, add_to_recent_files = file_path != tempfile.gettempdir()) # Don't add temp files to the recent files list
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
|
@ -1502,12 +1531,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
# Compute the center of the objects
|
||||
object_centers = []
|
||||
# Forget about the translation that the original objects have
|
||||
zero_translation = Matrix(data=numpy.zeros(3))
|
||||
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||
transformation = node.getLocalTransformation()
|
||||
transformation.setTranslation(zero_translation)
|
||||
transformed_mesh = mesh.getTransformed(transformation)
|
||||
transformed_mesh = mesh.getTransformed(Matrix()) # Forget about the transformations that the original object had.
|
||||
center = transformed_mesh.getCenterPosition()
|
||||
if center is not None:
|
||||
object_centers.append(center)
|
||||
|
@ -1522,7 +1547,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
# Move each node to the same position.
|
||||
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||
node.setTransformation(Matrix())
|
||||
node.setTransformation(Matrix()) # Removes any changes in position and rotation.
|
||||
# Align the object around its zero position
|
||||
# and also apply the offset to center it inside the group.
|
||||
node.setPosition(-mesh.getZeroPosition() - offset)
|
||||
|
@ -1720,15 +1745,17 @@ class CuraApplication(QtApplication):
|
|||
def log(self, msg):
|
||||
Logger.log("d", msg)
|
||||
|
||||
openProjectFile = pyqtSignal(QUrl, arguments = ["project_file"]) # Emitted when a project file is about to open.
|
||||
openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open.
|
||||
|
||||
@pyqtSlot(QUrl, str, bool)
|
||||
@pyqtSlot(QUrl, str)
|
||||
@pyqtSlot(QUrl)
|
||||
def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None):
|
||||
def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None, add_to_recent_files: bool = True):
|
||||
"""Open a local file
|
||||
|
||||
:param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model"
|
||||
or "open_as_project". This parameter is only considered if the file is a project file.
|
||||
:param add_to_recent_files: Whether or not to add the file as an option to the Recent Files list.
|
||||
"""
|
||||
Logger.log("i", "Attempting to read file %s", file.toString())
|
||||
if not file.isValid():
|
||||
|
@ -1749,12 +1776,12 @@ class CuraApplication(QtApplication):
|
|||
if is_project_file and project_mode == "open_as_project":
|
||||
# open as project immediately without presenting a dialog
|
||||
workspace_handler = self.getWorkspaceFileHandler()
|
||||
workspace_handler.readLocalFile(file)
|
||||
workspace_handler.readLocalFile(file, add_to_recent_files_hint = add_to_recent_files)
|
||||
return
|
||||
|
||||
if is_project_file and project_mode == "always_ask":
|
||||
# present a dialog asking to open as project or import models
|
||||
self.callLater(self.openProjectFile.emit, file)
|
||||
self.callLater(self.openProjectFile.emit, file, add_to_recent_files)
|
||||
return
|
||||
|
||||
# Either the file is a model file or we want to load only models from project. Continue to load models.
|
||||
|
@ -1790,7 +1817,7 @@ class CuraApplication(QtApplication):
|
|||
if extension in self._non_sliceable_extensions:
|
||||
self.deleteAll(only_selectable = False)
|
||||
|
||||
job = ReadMeshJob(f)
|
||||
job = ReadMeshJob(f, add_to_recent_files = add_to_recent_files)
|
||||
job.finished.connect(self._readMeshFinished)
|
||||
job.start()
|
||||
|
||||
|
@ -1841,6 +1868,7 @@ class CuraApplication(QtApplication):
|
|||
else:
|
||||
node = CuraSceneNode()
|
||||
node.setMeshData(original_node.getMeshData())
|
||||
node.source_mime_type = original_node.source_mime_type
|
||||
|
||||
# Setting meshdata does not apply scaling.
|
||||
if original_node.getScale() != Vector(1.0, 1.0, 1.0):
|
||||
|
@ -1905,6 +1933,11 @@ class CuraApplication(QtApplication):
|
|||
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
|
||||
except:
|
||||
Logger.logException("e", "Failed to arrange the models")
|
||||
|
||||
# Ensure that we don't have any weird floaty objects (CURA-7855)
|
||||
for node in nodes_to_arrange:
|
||||
node.translate(Vector(0, -node.getBoundingBox().bottom, 0), SceneNode.TransformSpace.World)
|
||||
|
||||
self.fileCompleted.emit(file_name)
|
||||
|
||||
def addNonSliceableExtension(self, extension):
|
||||
|
@ -1921,7 +1954,7 @@ class CuraApplication(QtApplication):
|
|||
try:
|
||||
result = workspace_reader.preRead(file_path, show_dialog=False)
|
||||
return result == WorkspaceReader.PreReadResult.accepted
|
||||
except Exception:
|
||||
except:
|
||||
Logger.logException("e", "Could not check file %s", file_url)
|
||||
return False
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ class LayerPolygon:
|
|||
|
||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||
# Should be generated in better way, not hardcoded.
|
||||
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
|
||||
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||
|
@ -73,18 +73,17 @@ class LayerPolygon:
|
|||
def buildCache(self) -> None:
|
||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
|
||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||
self._index_begin = 0
|
||||
self._index_end = mesh_line_count
|
||||
self._index_end = cast(int, numpy.sum(self._build_cache_line_mesh_mask))
|
||||
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = bool)
|
||||
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points )
|
||||
|
||||
self._vertex_begin = 0
|
||||
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
||||
self._vertex_end = cast(int, numpy.sum(self._build_cache_needed_points))
|
||||
|
||||
def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
|
||||
"""Set all the arrays provided by the function caller, representing the LayerPolygon
|
||||
|
|
|
@ -88,8 +88,10 @@ class MaterialNode(ContainerNode):
|
|||
variant = self.variant.variant_name)
|
||||
else:
|
||||
qualities_any_material = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.variant.machine.quality_definition)
|
||||
for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type):
|
||||
qualities.extend((quality for quality in qualities_any_material if quality.get("material") == material_metadata["base_file"]))
|
||||
|
||||
all_material_base_files = {material_metadata["base_file"] for material_metadata in container_registry.findInstanceContainersMetadata(type = "material", material = my_material_type)}
|
||||
|
||||
qualities.extend((quality for quality in qualities_any_material if quality.get("material") in all_material_base_files))
|
||||
|
||||
if not qualities: # No quality profiles found. Go by GUID then.
|
||||
my_guid = self.guid
|
||||
|
|
|
@ -53,6 +53,9 @@ class ExtrudersModel(ListModel):
|
|||
EnabledRole = Qt.UserRole + 11
|
||||
"""Is the extruder enabled?"""
|
||||
|
||||
MaterialTypeRole = Qt.UserRole + 12
|
||||
"""The type of the material (e.g. PLA, ABS, PETG, etc.)."""
|
||||
|
||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||
"""List of colours to display if there is no material or the material has no known colour. """
|
||||
|
||||
|
@ -75,6 +78,7 @@ class ExtrudersModel(ListModel):
|
|||
self.addRoleName(self.StackRole, "stack")
|
||||
self.addRoleName(self.MaterialBrandRole, "material_brand")
|
||||
self.addRoleName(self.ColorNameRole, "color_name")
|
||||
self.addRoleName(self.MaterialTypeRole, "material_type")
|
||||
self._update_extruder_timer = QTimer()
|
||||
self._update_extruder_timer.setInterval(100)
|
||||
self._update_extruder_timer.setSingleShot(True)
|
||||
|
@ -193,7 +197,8 @@ class ExtrudersModel(ListModel):
|
|||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||
"stack": extruder,
|
||||
"material_brand": material_brand,
|
||||
"color_name": color_name
|
||||
"color_name": color_name,
|
||||
"material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "",
|
||||
}
|
||||
|
||||
items.append(item)
|
||||
|
@ -210,7 +215,7 @@ class ExtrudersModel(ListModel):
|
|||
"id": "",
|
||||
"name": catalog.i18nc("@menuitem", "Not overridden"),
|
||||
"enabled": True,
|
||||
"color": "#ffffff",
|
||||
"color": "transparent",
|
||||
"index": -1,
|
||||
"definition": "",
|
||||
"material": "",
|
||||
|
@ -218,6 +223,7 @@ class ExtrudersModel(ListModel):
|
|||
"stack": None,
|
||||
"material_brand": "",
|
||||
"color_name": "",
|
||||
"material_type": "",
|
||||
}
|
||||
items.append(item)
|
||||
if self._items != items:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 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
|
||||
|
@ -34,4 +34,4 @@ def fetchLayerHeight(quality_group: "QualityGroup") -> float:
|
|||
if isinstance(layer_height, SettingFunction):
|
||||
layer_height = layer_height(global_stack)
|
||||
|
||||
return float(layer_height)
|
||||
return round(float(layer_height), 3)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy # To duplicate materials.
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
import uuid # To generate new GUIDs for new materials.
|
||||
import zipfile # To export all materials in a .zip archive.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
|
@ -20,11 +21,6 @@ if TYPE_CHECKING:
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
class MaterialManagementModel(QObject):
|
||||
"""Proxy class to the materials page in the preferences.
|
||||
|
||||
This class handles the actions in that page, such as creating new materials, renaming them, etc.
|
||||
"""
|
||||
|
||||
favoritesChanged = pyqtSignal(str)
|
||||
"""Triggered when a favorite is added or removed.
|
||||
|
||||
|
@ -79,6 +75,7 @@ class MaterialManagementModel(QObject):
|
|||
|
||||
:param material_node: The material to remove.
|
||||
"""
|
||||
Logger.info(f"Removing material {material_node.container_id}")
|
||||
|
||||
container_registry = CuraContainerRegistry.getInstance()
|
||||
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
|
||||
|
@ -194,6 +191,7 @@ class MaterialManagementModel(QObject):
|
|||
|
||||
:return: The root material ID of the duplicate material.
|
||||
"""
|
||||
Logger.info(f"Duplicating material {material_node.base_file} to {new_base_id}")
|
||||
return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
|
@ -262,3 +260,40 @@ class MaterialManagementModel(QObject):
|
|||
self.favoritesChanged.emit(material_base_file)
|
||||
except ValueError: # Material was not in the favorites list.
|
||||
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
def getPreferredExportAllPath(self) -> QUrl:
|
||||
"""
|
||||
Get the preferred path to export materials to.
|
||||
|
||||
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
|
||||
file path.
|
||||
:return: The preferred path to export all materials to.
|
||||
"""
|
||||
cura_application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
device_manager = cura_application.getOutputDeviceManager()
|
||||
devices = device_manager.getOutputDevices()
|
||||
for device in devices:
|
||||
if device.__class__.__name__ == "RemovableDriveOutputDevice":
|
||||
return QUrl.fromLocalFile(device.getId())
|
||||
else: # No removable drives? Use local path.
|
||||
return cura_application.getDefaultPath("dialog_material_path")
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def exportAll(self, file_path: QUrl) -> None:
|
||||
"""
|
||||
Export all materials to a certain file path.
|
||||
:param file_path: The path to export the materials to.
|
||||
"""
|
||||
registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
|
||||
for metadata in registry.findInstanceContainersMetadata(type = "material"):
|
||||
if metadata["base_file"] != metadata["id"]: # Only process base files.
|
||||
continue
|
||||
if metadata["id"] == "empty_material": # Don't export the empty material.
|
||||
continue
|
||||
material = registry.findContainers(id = metadata["id"])[0]
|
||||
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
|
||||
filename = metadata["id"] + "." + suffix
|
||||
archive.writestr(filename, material.serialize())
|
||||
|
|
|
@ -99,7 +99,7 @@ class QualitySettingsModel(ListModel):
|
|||
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||
quality_node = quality_group.node_for_global
|
||||
else:
|
||||
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position))
|
||||
quality_node = quality_group.nodes_for_extruders.get(self._selected_position)
|
||||
settings_keys = quality_group.getAllKeys()
|
||||
quality_containers = []
|
||||
if quality_node is not None and quality_node.container is not None:
|
||||
|
@ -114,10 +114,13 @@ class QualitySettingsModel(ListModel):
|
|||
global_container = None if len(global_containers) == 0 else global_containers[0]
|
||||
extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder}
|
||||
extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()}
|
||||
quality_changes_metadata = None
|
||||
if self._selected_position == self.GLOBAL_STACK_POSITION and global_container:
|
||||
quality_changes_metadata = global_container.getMetaData()
|
||||
else:
|
||||
quality_changes_metadata = extruders_container.get(str(self._selected_position))
|
||||
extruder = extruders_container.get(self._selected_position)
|
||||
if extruder:
|
||||
quality_changes_metadata = extruder.getMetaData()
|
||||
if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
|
||||
container = container_registry.findContainers(id = quality_changes_metadata["id"])
|
||||
if container:
|
||||
|
|
|
@ -19,6 +19,8 @@ class SettingVisibilityPresetsModel(QObject):
|
|||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
Version = 2
|
||||
|
||||
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 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, Any, Dict, Tuple
|
||||
|
||||
from typing import Optional
|
||||
import requests
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -58,7 +58,7 @@ class AuthorizationHelpers:
|
|||
:return: An AuthenticationResponse object.
|
||||
"""
|
||||
|
||||
Logger.log("d", "Refreshing the access token.")
|
||||
Logger.log("d", "Refreshing the access token for [%s]", self._settings.OAUTH_SERVER_URL)
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||
|
@ -70,6 +70,8 @@ class AuthorizationHelpers:
|
|||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
except requests.exceptions.ConnectionError:
|
||||
return AuthenticationResponse(success = False, err_message = "Unable to connect to remote server")
|
||||
except OSError as e:
|
||||
return AuthenticationResponse(success = False, err_message = "Operating system is unable to set up a secure connection: {err}".format(err = str(e)))
|
||||
|
||||
@staticmethod
|
||||
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
|
||||
|
@ -108,10 +110,12 @@ class AuthorizationHelpers:
|
|||
"""
|
||||
|
||||
try:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
check_token_url = "{}/check-token".format(self._settings.OAUTH_SERVER_URL)
|
||||
Logger.log("d", "Checking the access token for [%s]", check_token_url)
|
||||
token_request = requests.get(check_token_url, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
||||
# Connection was suddenly dropped. Nothing we can do about that.
|
||||
Logger.logException("w", "Something failed while attempting to parse the JWT token")
|
||||
return None
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
|
@ -113,7 +113,9 @@ class AuthorizationService:
|
|||
# The token could not be refreshed using the refresh token. We should login again.
|
||||
return None
|
||||
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
|
||||
# from the server already.
|
||||
# from the server already. Do not store the auth_data if we could not get new auth_data (eg due to a
|
||||
# network error), since this would cause an infinite loop trying to get new auth-data
|
||||
if self._auth_data.success:
|
||||
self._storeAuthData(self._auth_data)
|
||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
|
||||
|
@ -241,13 +243,13 @@ class AuthorizationService:
|
|||
|
||||
self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning"))
|
||||
self._unable_to_get_data_message.show()
|
||||
except ValueError:
|
||||
except (ValueError, TypeError):
|
||||
Logger.logException("w", "Could not load auth data from preferences")
|
||||
|
||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||
"""Store authentication data in preferences."""
|
||||
|
||||
Logger.log("d", "Attempting to store the auth data")
|
||||
Logger.log("d", "Attempting to store the auth data for [%s]", self._settings.OAUTH_SERVER_URL)
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||
return
|
||||
|
@ -255,10 +257,9 @@ class AuthorizationService:
|
|||
self._auth_data = auth_data
|
||||
if auth_data:
|
||||
self._user_profile = self.getUserProfile()
|
||||
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
|
||||
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(auth_data.dump()))
|
||||
else:
|
||||
self._user_profile = None
|
||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||
|
||||
self.accessTokenChanged.emit()
|
||||
|
||||
|
|
83
cura/OAuth2/KeyringAttribute.py
Normal file
|
@ -0,0 +1,83 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Type, TYPE_CHECKING, Optional, List
|
||||
|
||||
import keyring
|
||||
from keyring.backend import KeyringBackend
|
||||
from keyring.errors import NoKeyringError, PasswordSetError, KeyringLocked
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import BaseModel
|
||||
|
||||
# Need to do some extra workarounds on windows:
|
||||
import sys
|
||||
from UM.Platform import Platform
|
||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||
import win32timezone
|
||||
from keyring.backends.Windows import WinVaultKeyring
|
||||
keyring.set_keyring(WinVaultKeyring())
|
||||
if Platform.isOSX() and hasattr(sys, "frozen"):
|
||||
from keyring.backends.macOS import Keyring
|
||||
keyring.set_keyring(Keyring())
|
||||
|
||||
# Even if errors happen, we don't want this stored locally:
|
||||
DONT_EVER_STORE_LOCALLY: List[str] = ["refresh_token"]
|
||||
|
||||
|
||||
class KeyringAttribute:
|
||||
"""
|
||||
Descriptor for attributes that need to be stored in the keyring. With Fallback behaviour to the preference cfg file
|
||||
"""
|
||||
def __get__(self, instance: "BaseModel", owner: type) -> Optional[str]:
|
||||
if self._store_secure: # type: ignore
|
||||
try:
|
||||
value = keyring.get_password("cura", self._keyring_name)
|
||||
return value if value != "" else None
|
||||
except NoKeyringError:
|
||||
self._store_secure = False
|
||||
Logger.logException("w", "No keyring backend present")
|
||||
return getattr(instance, self._name)
|
||||
except KeyringLocked:
|
||||
self._store_secure = False
|
||||
Logger.log("i", "Access to the keyring was denied.")
|
||||
return getattr(instance, self._name)
|
||||
else:
|
||||
return getattr(instance, self._name)
|
||||
|
||||
def __set__(self, instance: "BaseModel", value: Optional[str]):
|
||||
if self._store_secure:
|
||||
setattr(instance, self._name, None)
|
||||
if value is not None:
|
||||
try:
|
||||
keyring.set_password("cura", self._keyring_name, value)
|
||||
except (PasswordSetError, KeyringLocked):
|
||||
self._store_secure = False
|
||||
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||
setattr(instance, self._name, value)
|
||||
Logger.logException("w", "Keyring access denied")
|
||||
except NoKeyringError:
|
||||
self._store_secure = False
|
||||
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||
setattr(instance, self._name, value)
|
||||
Logger.logException("w", "No keyring backend present")
|
||||
except BaseException as e:
|
||||
# A BaseException can occur in Windows when the keyring attempts to write a token longer than 1024
|
||||
# characters in the Windows Credentials Manager.
|
||||
self._store_secure = False
|
||||
if self._name not in DONT_EVER_STORE_LOCALLY:
|
||||
setattr(instance, self._name, value)
|
||||
Logger.log("w", "Keyring failed: {}".format(e))
|
||||
else:
|
||||
setattr(instance, self._name, value)
|
||||
|
||||
def __set_name__(self, owner: type, name: str):
|
||||
self._name = "_{}".format(name)
|
||||
self._keyring_name = name
|
||||
self._store_secure = False
|
||||
try:
|
||||
self._store_secure = KeyringBackend.viable
|
||||
except NoKeyringError:
|
||||
Logger.logException("w", "Could not use keyring")
|
||||
setattr(owner, self._name, None)
|
|
@ -54,6 +54,7 @@ class LocalAuthorizationServer:
|
|||
if self._web_server:
|
||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||
# We still inject the new verification code though.
|
||||
Logger.log("d", "Auth web server was already running. Updating the verification code")
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
return
|
||||
|
||||
|
@ -85,6 +86,7 @@ class LocalAuthorizationServer:
|
|||
except OSError:
|
||||
# OS error can happen if the socket was already closed. We really don't care about that case.
|
||||
pass
|
||||
Logger.log("d", "Local oauth2 web server was shut down")
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
||||
|
||||
|
@ -96,12 +98,13 @@ class LocalAuthorizationServer:
|
|||
|
||||
:return: None
|
||||
"""
|
||||
Logger.log("d", "Local web server for authorization has started")
|
||||
if self._web_server:
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
self._web_server.serve_forever()
|
||||
except OSError as e:
|
||||
Logger.warning(str(e))
|
||||
except OSError:
|
||||
Logger.logException("w", "An exception happened while serving the auth server")
|
||||
else:
|
||||
# Leave the default behavior in non-windows platforms
|
||||
self._web_server.serve_forever()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, Any, List
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
from copy import deepcopy
|
||||
from cura.OAuth2.KeyringAttribute import KeyringAttribute
|
||||
|
||||
|
||||
class BaseModel:
|
||||
|
@ -37,12 +39,29 @@ class AuthenticationResponse(BaseModel):
|
|||
# Data comes from the token response with success flag and error message added.
|
||||
success = True # type: bool
|
||||
token_type = None # type: Optional[str]
|
||||
access_token = None # type: Optional[str]
|
||||
refresh_token = None # type: Optional[str]
|
||||
expires_in = None # type: Optional[str]
|
||||
scope = None # type: Optional[str]
|
||||
err_message = None # type: Optional[str]
|
||||
received_at = None # type: Optional[str]
|
||||
access_token = KeyringAttribute()
|
||||
refresh_token = KeyringAttribute()
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.access_token = kwargs.pop("access_token", None)
|
||||
self.refresh_token = kwargs.pop("refresh_token", None)
|
||||
super(AuthenticationResponse, self).__init__(**kwargs)
|
||||
|
||||
def dump(self) -> Dict[str, Union[bool, Optional[str]]]:
|
||||
"""
|
||||
Dumps the dictionary of Authentication attributes. KeyringAttributes are transformed to public attributes
|
||||
If the keyring was used, these will have a None value, otherwise they will have the secret value
|
||||
|
||||
:return: Dictionary of Authentication attributes
|
||||
"""
|
||||
dumped = deepcopy(vars(self))
|
||||
dumped["access_token"] = dumped.pop("_access_token")
|
||||
dumped["refresh_token"] = dumped.pop("_refresh_token")
|
||||
return dumped
|
||||
|
||||
|
||||
class ResponseStatus(BaseModel):
|
||||
|
|
|
@ -119,21 +119,23 @@ class CuraSceneNode(SceneNode):
|
|||
self._aabb = None
|
||||
if self._mesh_data:
|
||||
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation(copy = False))
|
||||
else: # If there is no mesh_data, use a bounding box that encompasses the local (0,0,0)
|
||||
position = self.getWorldPosition()
|
||||
self._aabb = AxisAlignedBox(minimum = position, maximum = position)
|
||||
|
||||
for child in self.getAllChildren():
|
||||
if child.callDecoration("isNonPrintingMesh"):
|
||||
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
||||
continue
|
||||
if not child.getMeshData():
|
||||
# Nodes without mesh data should not affect bounding boxes of their parents.
|
||||
child_bb = child.getBoundingBox()
|
||||
if child_bb is None or child_bb.minimum == child_bb.maximum:
|
||||
# Child had a degenerate bounding box, such as an empty group. Don't count it along.
|
||||
continue
|
||||
if self._aabb is None:
|
||||
self._aabb = child.getBoundingBox()
|
||||
self._aabb = child_bb
|
||||
else:
|
||||
self._aabb = self._aabb + child.getBoundingBox()
|
||||
self._aabb = self._aabb + child_bb
|
||||
|
||||
if self._aabb is None: # No children that should be included? Just use your own position then, but it's an invalid AABB.
|
||||
position = self.getWorldPosition()
|
||||
self._aabb = AxisAlignedBox(minimum = position, maximum = position)
|
||||
|
||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||
"""Taken from SceneNode, but replaced SceneNode with CuraSceneNode"""
|
||||
|
@ -142,6 +144,7 @@ class CuraSceneNode(SceneNode):
|
|||
copy.setTransformation(self.getLocalTransformation(copy= False))
|
||||
copy.setMeshData(self._mesh_data)
|
||||
copy.setVisible(cast(bool, deepcopy(self._visible, memo)))
|
||||
copy.source_mime_type = cast(str, deepcopy(self.source_mime_type, memo))
|
||||
copy._selectable = cast(bool, deepcopy(self._selectable, memo))
|
||||
copy._name = cast(str, deepcopy(self._name, memo))
|
||||
for decorator in self._decorators:
|
||||
|
|
|
@ -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 os
|
||||
|
@ -221,6 +221,7 @@ class ContainerManager(QObject):
|
|||
except OSError:
|
||||
return {"status": "error", "message": "Unable to write to this location.", "path": file_url}
|
||||
|
||||
Logger.info("Successfully exported container to {path}".format(path = file_url))
|
||||
return {"status": "success", "message": "Successfully exported container", "path": file_url}
|
||||
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
|
@ -240,6 +241,7 @@ class ContainerManager(QObject):
|
|||
file_url = file_url_or_string.toLocalFile()
|
||||
else:
|
||||
file_url = file_url_or_string
|
||||
Logger.info(f"Importing material from {file_url}")
|
||||
|
||||
if not file_url or not os.path.exists(file_url):
|
||||
return {"status": "error", "message": "Invalid path"}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
|
@ -381,8 +381,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if profile_count > 1:
|
||||
continue
|
||||
# Only one profile found, this should not ever be the case, so that profile needs to be removed!
|
||||
Logger.log("d", "Found an invalid quality_changes profile with the name %s. Going to remove that now", profile_name)
|
||||
invalid_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(name=profile_name)
|
||||
if invalid_quality_changes:
|
||||
Logger.log("d", "Found an invalid quality_changes profile with the name %s. Going to remove that now", profile_name)
|
||||
self.removeContainer(invalid_quality_changes[0]["id"])
|
||||
|
||||
@override(ContainerRegistry)
|
||||
|
@ -402,6 +403,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
return False
|
||||
except ValueError: # Not parsable as int.
|
||||
return False
|
||||
except TypeError: # Expecting string input here, not e.g. list or anything.
|
||||
return False
|
||||
return True
|
||||
|
||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Tuple[bool, Optional[str]]:
|
||||
|
|
|
@ -86,6 +86,14 @@ class GlobalStack(CuraContainerStack):
|
|||
def supportsNetworkConnection(self):
|
||||
return self.getMetaDataEntry("supports_network_connection", False)
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def supportsMaterialExport(self):
|
||||
"""
|
||||
Whether the printer supports Cura's export format of material profiles.
|
||||
:return: ``True`` if it supports it, or ``False`` if not.
|
||||
"""
|
||||
return self.getMetaDataEntry("supports_material_export", False)
|
||||
|
||||
@classmethod
|
||||
def getLoadingPriority(cls) -> int:
|
||||
return 2
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QObject, pyqtSignal, QRegExp
|
||||
|
@ -23,7 +23,7 @@ class MachineNameValidator(QObject):
|
|||
#Compute the validation regex for printer names. This is limited by the maximum file name length.
|
||||
try:
|
||||
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||
except (AttributeError, EnvironmentError): # Doesn't support statvfs. Probably because it's not a Unix system. Or perhaps there is no permission or it doesn't exist.
|
||||
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||
machine_name_max_length = filename_max_length - len("_current_settings.") - len(ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix)
|
||||
# Characters that urllib.parse.quote_plus escapes count for 12! So now
|
||||
|
|
|
@ -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.
|
||||
import numpy
|
||||
|
||||
|
@ -25,8 +25,8 @@ class Snapshot:
|
|||
pixels = numpy.frombuffer(pixel_array, dtype=numpy.uint8).reshape([height, width, 4])
|
||||
# Find indices of non zero pixels
|
||||
nonzero_pixels = numpy.nonzero(pixels)
|
||||
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1)
|
||||
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1)
|
||||
min_y, min_x, min_a_ = numpy.amin(nonzero_pixels, axis=1) # type: ignore
|
||||
max_y, max_x, max_a_ = numpy.amax(nonzero_pixels, axis=1) # type: ignore
|
||||
|
||||
return min_x, max_x, min_y, max_y
|
||||
|
||||
|
@ -42,8 +42,8 @@ class Snapshot:
|
|||
"""
|
||||
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
active_camera = scene.getActiveCamera()
|
||||
render_width, render_height = active_camera.getWindowSize()
|
||||
active_camera = scene.getActiveCamera() or scene.findCamera("3d")
|
||||
render_width, render_height = (width, height) if active_camera is None else active_camera.getWindowSize()
|
||||
render_width = int(render_width)
|
||||
render_height = int(render_height)
|
||||
preview_pass = PreviewPass(render_width, render_height)
|
||||
|
|
|
@ -132,9 +132,26 @@ class ObjectsModel(ListModel):
|
|||
|
||||
is_group = bool(node.callDecoration("isGroup"))
|
||||
|
||||
name_handled_as_group = False
|
||||
force_rename = False
|
||||
if not is_group:
|
||||
# Handle names for individual nodes
|
||||
if is_group:
|
||||
# Handle names for grouped nodes
|
||||
original_name = self._group_name_prefix
|
||||
|
||||
current_name = node.getName()
|
||||
if current_name.startswith(self._group_name_prefix):
|
||||
# This group has a standard group name, but we may need to renumber it
|
||||
name_index = int(current_name.split("#")[-1])
|
||||
name_handled_as_group = True
|
||||
elif not current_name:
|
||||
# Force rename this group because this node has not been named as a group yet, probably because
|
||||
# it's a newly created group.
|
||||
name_index = 0
|
||||
force_rename = True
|
||||
name_handled_as_group = True
|
||||
|
||||
if not is_group or not name_handled_as_group:
|
||||
# Handle names for individual nodes or groups that already have a non-group name
|
||||
name = node.getName()
|
||||
|
||||
name_match = self._naming_regex.fullmatch(name)
|
||||
|
@ -144,18 +161,6 @@ class ObjectsModel(ListModel):
|
|||
else:
|
||||
original_name = name_match.groups()[0]
|
||||
name_index = int(name_match.groups()[1])
|
||||
else:
|
||||
# Handle names for grouped nodes
|
||||
original_name = self._group_name_prefix
|
||||
|
||||
current_name = node.getName()
|
||||
if current_name.startswith(self._group_name_prefix):
|
||||
name_index = int(current_name.split("#")[-1])
|
||||
else:
|
||||
# Force rename this group because this node has not been named as a group yet, probably because
|
||||
# it's a newly created group.
|
||||
name_index = 0
|
||||
force_rename = True
|
||||
|
||||
if original_name not in name_to_node_info_dict:
|
||||
# Keep track of 2 things:
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import json
|
||||
import math
|
||||
import os
|
||||
import unicodedata
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot, QTimer
|
||||
|
@ -301,10 +300,11 @@ class PrintInformation(QObject):
|
|||
if self._base_name == "":
|
||||
self._job_name = self.UNTITLED_JOB_NAME
|
||||
self._is_user_specified_job_name = False
|
||||
self._application.getController().getScene().clearMetaData()
|
||||
self.jobNameChanged.emit()
|
||||
return
|
||||
|
||||
base_name = self._stripAccents(self._base_name)
|
||||
base_name = self._base_name
|
||||
self._defineAbbreviatedMachineName()
|
||||
|
||||
# Only update the job name when it's not user-specified.
|
||||
|
@ -400,11 +400,6 @@ class PrintInformation(QObject):
|
|||
|
||||
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||
|
||||
def _stripAccents(self, to_strip: str) -> str:
|
||||
"""Utility method that strips accents from characters (eg: â -> a)"""
|
||||
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||
|
||||
@pyqtSlot(result = "QVariantMap")
|
||||
def getFeaturePrintTimes(self) -> Dict[str, Duration]:
|
||||
result = {}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import collections
|
||||
|
@ -6,9 +6,11 @@ from typing import Optional, Dict, List, cast
|
|||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Resources import Resources
|
||||
from UM.Version import Version
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
#
|
||||
# This manager provides means to load texts to QML.
|
||||
|
@ -30,10 +32,11 @@ class TextManager(QObject):
|
|||
# Load change log texts and organize them with a dict
|
||||
try:
|
||||
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
|
||||
except FileNotFoundError:
|
||||
except FileNotFoundError as e:
|
||||
# I have no idea how / when this happens, but we're getting crash reports about it.
|
||||
return ""
|
||||
return catalog.i18nc("@text:window", "The release notes could not be opened.") + "<br>" + str(e)
|
||||
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
|
||||
try:
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
open_version = None # type: Optional[Version]
|
||||
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||
|
@ -54,6 +57,8 @@ class TextManager(QObject):
|
|||
if open_header not in change_logs_dict[cast(Version, open_version)]:
|
||||
change_logs_dict[cast(Version, open_version)][open_header] = []
|
||||
change_logs_dict[cast(Version, open_version)][open_header].append(line)
|
||||
except EnvironmentError as e:
|
||||
return catalog.i18nc("@text:window", "The release notes could not be opened.") + "<br>" + str(e)
|
||||
|
||||
# Format changelog text
|
||||
content = ""
|
||||
|
|
|
@ -239,9 +239,6 @@ class WelcomePagesModel(ListModel):
|
|||
{"id": "user_agreement",
|
||||
"page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"),
|
||||
},
|
||||
{"id": "whats_new",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||
},
|
||||
{"id": "data_collections",
|
||||
"page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
|
||||
},
|
||||
|
@ -259,13 +256,21 @@ class WelcomePagesModel(ListModel):
|
|||
},
|
||||
{"id": "add_cloud_printers",
|
||||
"page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"),
|
||||
"is_final_page": True, # If we end up in this page, the next button will close the dialog
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Finish"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Next"),
|
||||
"next_page_id": "whats_new",
|
||||
},
|
||||
{"id": "machine_actions",
|
||||
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
|
||||
"should_show_function": self.shouldShowMachineActions,
|
||||
},
|
||||
{"id": "whats_new",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
|
||||
},
|
||||
{"id": "changelog",
|
||||
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Finish"),
|
||||
},
|
||||
]
|
||||
|
||||
pages_to_show = all_pages_list
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .WelcomePagesModel import WelcomePagesModel
|
||||
|
||||
import os
|
||||
from typing import Optional, Dict, List, Tuple
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot
|
||||
from UM.Logger import Logger
|
||||
from UM.Resources import Resources
|
||||
|
||||
#
|
||||
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for showing the
|
||||
|
@ -10,13 +14,84 @@ from .WelcomePagesModel import WelcomePagesModel
|
|||
#
|
||||
class WhatsNewPagesModel(WelcomePagesModel):
|
||||
|
||||
image_formats = [".png", ".jpg", ".jpeg", ".gif", ".svg"]
|
||||
text_formats = [".txt", ".htm", ".html"]
|
||||
image_key = "image"
|
||||
text_key = "text"
|
||||
|
||||
@staticmethod
|
||||
def _collectOrdinalFiles(resource_type: int, include: List[str]) -> Tuple[Dict[int, str], int]:
|
||||
result = {} #type: Dict[int, str]
|
||||
highest = -1
|
||||
try:
|
||||
folder_path = Resources.getPath(resource_type, "whats_new")
|
||||
for _, _, files in os.walk(folder_path):
|
||||
for filename in files:
|
||||
basename = os.path.basename(filename)
|
||||
base, ext = os.path.splitext(basename)
|
||||
if ext.lower() not in include or not base.isdigit():
|
||||
continue
|
||||
page_no = int(base)
|
||||
highest = max(highest, page_no)
|
||||
result[page_no] = os.path.join(folder_path, filename)
|
||||
except FileNotFoundError:
|
||||
Logger.logException("w", "Could not find 'whats_new' folder for resource-type {0}".format(resource_type))
|
||||
return result, highest
|
||||
|
||||
@staticmethod
|
||||
def _loadText(filename: str) -> str:
|
||||
result = ""
|
||||
try:
|
||||
with open(filename, "r", encoding="utf-8") as file:
|
||||
result = file.read()
|
||||
except OSError:
|
||||
Logger.logException("w", "Could not open {0}".format(filename))
|
||||
return result
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._pages = []
|
||||
self._pages.append({"id": "whats_new",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Skip"),
|
||||
"next_page_id": "changelog"
|
||||
})
|
||||
self._pages.append({"id": "changelog",
|
||||
"page_url": self._getBuiltinWelcomePagePath("ChangelogContent.qml"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
|
||||
})
|
||||
self.setItems(self._pages)
|
||||
|
||||
images, max_image = WhatsNewPagesModel._collectOrdinalFiles(Resources.Images, WhatsNewPagesModel.image_formats)
|
||||
texts, max_text = WhatsNewPagesModel._collectOrdinalFiles(Resources.Texts, WhatsNewPagesModel.text_formats)
|
||||
highest = max(max_image, max_text)
|
||||
|
||||
self._subpages = [] #type: List[Dict[str, Optional[str]]]
|
||||
for n in range(0, highest + 1):
|
||||
self._subpages.append({
|
||||
WhatsNewPagesModel.image_key: None if n not in images else images[n],
|
||||
WhatsNewPagesModel.text_key: None if n not in texts else self._loadText(texts[n])
|
||||
})
|
||||
if len(self._subpages) == 0:
|
||||
self._subpages.append({WhatsNewPagesModel.text_key: "~ There Is Nothing New Under The Sun ~"})
|
||||
|
||||
def _getSubpageItem(self, page: int, item: str) -> Optional[str]:
|
||||
if 0 <= page < self.subpageCount and item in self._subpages[page]:
|
||||
return self._subpages[page][item]
|
||||
else:
|
||||
return None
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def subpageCount(self) -> int:
|
||||
return len(self._subpages)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getSubpageImageSource(self, page: int) -> str:
|
||||
result = self._getSubpageItem(page, WhatsNewPagesModel.image_key)
|
||||
return "file:///" + (result if result else Resources.getPath(Resources.Images, "cura-icon.png"))
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getSubpageText(self, page: int) -> str:
|
||||
result = self._getSubpageItem(page, WhatsNewPagesModel.text_key)
|
||||
return result if result else "* * *"
|
||||
|
||||
__all__ = ["WhatsNewPagesModel"]
|
||||
|
|
14
cura_app.py
|
@ -16,14 +16,6 @@ import argparse
|
|||
import faulthandler
|
||||
import os
|
||||
|
||||
# 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 PyQt5.QtNetwork import QSslConfiguration, QSslSocket
|
||||
|
||||
from UM.Platform import Platform
|
||||
|
@ -226,6 +218,12 @@ if Platform.isLinux() and getattr(sys, "frozen", False):
|
|||
import trimesh.exchange.load
|
||||
os.environ["LD_LIBRARY_PATH"] = old_env
|
||||
|
||||
# WORKAROUND: Cura#5488
|
||||
# When using the KDE qqc2-desktop-style, the UI layout is completely broken, and
|
||||
# even worse, it crashes when switching to the "Preview" pane.
|
||||
if Platform.isLinux():
|
||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "default"
|
||||
|
||||
if ApplicationMetadata.CuraDebugMode:
|
||||
ssl_conf = QSslConfiguration.defaultConfiguration()
|
||||
ssl_conf.setPeerVerifyMode(QSslSocket.VerifyNone)
|
||||
|
|
|
@ -7,7 +7,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|||
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
||||
|
||||
# Make sure that environment variables are set properly
|
||||
source /opt/rh/devtoolset-7/enable
|
||||
source /opt/rh/devtoolset-8/enable
|
||||
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
||||
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ Note: The profiler front-end itself is quite "heavy" (ok, not optimised). It run
|
|||
|
||||
What the Profiler Sees
|
||||
----------------------
|
||||
The profiler doesn't capture every function call in Cura. It hooks into a number of important systems which give a good picture of activity without too much run time overhead. The most important system is Uranium's signal mechanism and PyQt5 slots. Functions which are called via the signal mechanism are recorded and thier names appear in the results. PyQt5 slots appear in the results with the prefix `[SLOT]`.
|
||||
The profiler doesn't capture every function call in Cura. It hooks into a number of important systems which give a good picture of activity without too much run time overhead. The most important system is Uranium's signal mechanism and PyQt5 slots. Functions which are called via the signal mechanism are recorded and their names appear in the results. PyQt5 slots appear in the results with the prefix `[SLOT]`.
|
||||
|
||||
Note that not all slots are captured. Only those slots which belong to classes which use the `pyqtSlot` decorator from the `UM.FlameProfiler` module.
|
||||
|
||||
|
|
BIN
docs/scene/images/components_interacting_with_scene.jpg
Normal file
After Width: | Height: | Size: 96 KiB |
BIN
docs/scene/images/components_interacting_with_scene.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
docs/scene/images/layer_data_scene_node.jpg
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
docs/scene/images/mirror_tool.jpg
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
docs/scene/images/per_objectsettings_tool.jpg
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
docs/scene/images/rotate_tool.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
docs/scene/images/scale_tool.jpg
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
docs/scene/images/scene_example.jpg
Normal file
After Width: | Height: | Size: 196 KiB |
BIN
docs/scene/images/scene_example_scene_graph.jpg
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
docs/scene/images/selection_tool.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
docs/scene/images/support_blocker_tool.jpg
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
docs/scene/images/tools_tool-handles_class_diagram.jpg
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
docs/scene/images/translate_tool.jpg
Normal file
After Width: | Height: | Size: 27 KiB |
113
docs/scene/operations.md
Normal file
|
@ -0,0 +1,113 @@
|
|||
# Operations and the OperationStack
|
||||
|
||||
Cura supports an operation stack. The `OperationStack` class maintains a history of the operations performed in Cura, which allows for undo and redo actions. Every operation registers itself in the stack. The OperationStuck supports the following functions:
|
||||
|
||||
* `push(operation)`: Pushes an operation in the stack and applies the operation. This function is called when an operation pushes itself in the stack.
|
||||
* `undo()`: Reverses the actions performed by the last operation and reduces the current index of the stack.
|
||||
* `redo()`: Applies the actions performed by the next operation in the stack and increments the current index of the stack.
|
||||
* `getOperations()`: Returns a list of all the operations that are currently inside the OperationStack
|
||||
* `canUndo()`: Indicates whether the index of the operation stack has reached the bottom of the stack, which means that there are no more operations to be undone.
|
||||
* `canRedo()`: Indicates whether the index of the operation stack has reached the top of the stack, which means that there are no more operations to be redone.
|
||||
|
||||
**Note 1:** When consecutive operations are performed very quickly after each other, they are merged together at the top of the stack. This action ensures that these minor operation will be undone with one Undo keystroke (e.g. when moving the object around and you press and release the left mouse button really fast, it is considered as one move operation).
|
||||
|
||||
**Note 2:** When an operation is pushed in the middle of the stack, all operations above it are removed from the stack. This ensures that there won't be any "history branches" created.
|
||||
|
||||
### Operations
|
||||
|
||||
Every action that happens in the scene and affects one or multiple models is associated with a subclass of the `Operation` class and is it added to the `OperationStack`. The subclassed operations that can be found in Cura (excluding the ones from downloadable plugins) are the following:
|
||||
|
||||
* [GroupedOperation](#groupedoperation)
|
||||
* [AddSceneNodeOperation](#addscenenodeoperation)
|
||||
* [RemoveSceneNodeOperation](#removescenenodeoperation)
|
||||
* [SetParentOperation](#setparentoperation)
|
||||
* [SetTransformOperation](#settransformoperation)
|
||||
* [SetObjectExtruderOperation](#setobjectextruderoperation)
|
||||
* [GravityOperation](#gravityoperation)
|
||||
* [PlatformPhysicsOperation](#platformphysicsoperation)
|
||||
* [TranslateOperation](#translateoperation)
|
||||
* [ScaleOperation](#scaleoperation)
|
||||
* [RotateOperation](#rotateoperation)
|
||||
* [MirrorOperation](#mirroroperation)
|
||||
* [LayFlatOperation](#layflatoperation)
|
||||
* [SetBuildPlateNumberOperation]()
|
||||
|
||||
### GroupedOperation
|
||||
|
||||
The `GroupedOperation` is an operation that groups several other operations together. The intent of this operation is to hide an underlying chain of operations from the user if they correspond to only one interaction with the user, such as an operation applied to multiple scene nodes or a re-arrangement of multiple items in the scene.
|
||||
|
||||
Once a `GroupedOperation` is pushed into the stack, it applies all of its children operations in one go. Similarly, when it is undone, it reverses all its children operations at once.
|
||||
|
||||
|
||||
### AddSceneNodeOperation
|
||||
|
||||
The `AddSceneNodeOperation` is added to the stack whenever a mesh is loaded inside the `Scene`, either by a `FileReader` or by inserting a [Support Blocker](tools.md#supporteraser-tool) in an object.
|
||||
|
||||
### RemoveSceneNodeOperation
|
||||
|
||||
The `RemoveSceneNodeOperation` is added to the stack whenever a mesh is removed from the Scene by the user or when the user requests to clear the build plate (_Ctrl+D_).
|
||||
|
||||
### SetParentOperation
|
||||
|
||||
The `SetParentOperation` changes the parent of a node. It is primarily used when grouping (the group node is set as the nodes' parent) and ungrouping (the group's children's parent is set to the group's parent before the group node is deleted), or when a SupportEraser node is added to the scene (to set the selected object as the Eraser's parent).
|
||||
|
||||
### SetTransformOperation
|
||||
|
||||
The `SetTransformOperation` translates, rotates, and scales a node all at once. This operation accepts a transformation matrix, an orientation matrix, and a scale matrix, and it is used by the _"Reset All Model Positions"_ and _"Reset All Model Transformations"_ options in the right-click (context) menu.
|
||||
|
||||
### SetObjectExtruderOperation
|
||||
|
||||
This operation is used to set the extruder with which a certain object should be printed with. It adds a [SettingOverrideDecorator](scene.md#settingoverridedecorator) to the object (if it doesn't have any) and then sets the extruder number via the decoration function `node.callDecoration("setActiveExtruder", extruder_id)`.
|
||||
|
||||
### GravityOperation
|
||||
|
||||
The `GravityOperation` moves a scene node down to 0 on the y-axis. It is currently used by the _"Lay flat"_ and _"Select face to align to the build plate"_ actions of the `RotationTool` to ensure that the object will end up touching the build plate after the corresponding rotation operations have be done.
|
||||
|
||||
### PlatformPhysicsOperation
|
||||
|
||||
The `PlatformPhysicsOperation` is generated by the `PlatformPhysics` class and it is associated with the preferences _"Ensure models are kept apart"_ and _"Automatically drop models to the build plate"_. If any of these preferences is set to true, the `PlatformPhysics` class periodically checks to make sure that the two conditions are met and if not, it calculates the move vector for each of the nodes that will satisfy the conditions.
|
||||
|
||||
Once the move vectors have been computed, they are applied to the nodes through consecutive `PlatformPhysicsOperations`, whose job is to use the `translate` function on the nodes.
|
||||
|
||||
**Note:** When there are multiple nodes, multiple `PlatformPhysicsOperations` may be generated (all models may be moved to ensure they are kept apart). These operations eventually get merged together by the `OperationStack` due to the fact that the individual operations are applied very fast one after the other.
|
||||
|
||||
### TranslateOperation
|
||||
|
||||
The `TranslateOperation` applies a linear transformation on a node, moving the node in the scene. This operation is primarily linked to the [TranslateTool](tools.md#translatetool) but it is also used in other places around Cura, such as arranging objects on the build plate (Ctrl+R) and centering an object to the build plate (via the right-click context menu's _"Center Selected Model"_ option).
|
||||
|
||||
When an object is moved using the move tool handles, multiple translate operations are generated to make sure that the object is rendered properly while it is moved. These translate operations are merged together once the user releases the tool handle.
|
||||
|
||||
**Note:** Some functionalities may move (translate) nodes without generating a TranslateOperation (such as when a model with is imported from a 3mf into a certain position). This ensures that the moving of the object cannot be accidentally undone by the user.
|
||||
|
||||
### ScaleOperation
|
||||
|
||||
The `ScaleOperation` scales the selected scene node uniformly or non-uniformly. This operation is primarily generated by the [ScaleTool](tools.md#scaletool).
|
||||
|
||||
When an object is scaled using the scale tool handles, multiple scale operations are generated to make sure that the object is rendered properly while it is being resized. These scale operations are merged together once the user releases the tool handle.
|
||||
|
||||
**Note:** When the _"Scale extremely small models"_ or the _"Scale large models"_ preferences are enabled the model is scaled when it is inserted into the build plate but it **DOES NOT** generate a `ScaleOperation`. This ensures that Cura doesn't register the scaling as an action that can be undone and the user doesn't accidentally end up with a very big or very small model.
|
||||
|
||||
|
||||
### RotateOperation
|
||||
|
||||
The `RotateOperation` rotates the selected scene node(s) according to a given rotation quaternion and, optionally, around a given point. This operation is primarily generated by the [RotationTool](tools.md#rotatetool). It is also used by the arrange algorithm, which may rotate some models to fit them in the build plate.
|
||||
|
||||
When an object is rotated using the rotate tool handles, multiple rotate operations are generated to make sure that the object is rendered properly while it is being rotated. These operations are merged together once the user releases the tool handle.
|
||||
|
||||
### MirrorOperation
|
||||
|
||||
The `MirrorOperation` mirrors the selected object. It is primarily associated with the [MirrorTool](tools.md#mirrortool) and allows for mirroring the object in a certain direction, using the `MirrorToolHandles`.
|
||||
|
||||
The `MirrorOperation` accepts a transformation matrix that should only define values on the diagonal of the matrix, and only the values 1 or -1. It allows for mirroring around the center of the object or around the axis origin. The latter isn't used that often.
|
||||
|
||||
### LayFlatOperation
|
||||
|
||||
The `LayFlatOperation` computes some orientation to hopefully lay the object flat on the build plate. It is generated by the `layFlat()` function of the [RotateTool](tools.md#rotatetool). Contrary to the other operations, the `LayFlatOperation` is computed in a separate thread through the `LayFlatJob` since it can be quite computationally expensive.
|
||||
|
||||
|
||||
### SetBuildPlateNumberOperation
|
||||
|
||||
The `SetBuildPlateNumberOperation` is linked to a legacy feature which allowed the user to have multiple build plates open in Cura at the same time. With this operation it was possible to transfer a node to another build plate through the node's [BuildPlateDecorator](scene.md#buildplatedecorator) by calling the decoration `node.callDecoration("setBuildPlateNumber", new_build_plate_nr)`.
|
||||
|
||||
**Note:** Changing the active build plate is a disabled feature in Cura and it is intended to be completely removed (internal ticket: CURA-4975), along with the `SetBuildPlateNumberOperation`.
|
||||
|
|
@ -8,19 +8,209 @@ Cura's scene graph is a mere tree data structure. This tree contains all scene n
|
|||
|
||||
The main idea behind the scene tree is that each scene node has a transformation applied to it. The scene nodes can be nested beneath other scene nodes. The transformation of the parents is then also applied to the children. This way you can have scene nodes grouped together and transform the group as a whole. Since the transformations are all linear, this ensures that the elements of this group stay in the same relative position and orientation. It will look as if the whole group is a single object. This idea is very common for games where objects are often composed of multiple 3D models but need to move together as a whole. For Cura it is used to group objects together and to transform the collision area correctly.
|
||||
|
||||
Class Diagram
|
||||
----
|
||||
|
||||
The following class diagram depicts the classes that interact with the Scene
|
||||
|
||||

|
||||
|
||||
The scene lives in the Controller of the Application, and it is primarily interacting with SceneNode objects, which are the components of the Scene Graph.
|
||||
|
||||
|
||||
A Typical Scene
|
||||
----
|
||||
Cura's scene has a few nodes that are always present, and a few nodes that are repeated for every object that the user loads onto their build plate. To give an idea of how a scene normally looks, this is an overview of a typical scene tree for Cura.
|
||||
Cura's scene has a few nodes that are always present, and a few nodes that are repeated for every object that the user loads onto their build plate. The root of the scene graph is a SceneNode that lives inside the Scene and contains all the other children SceneNodes of the scene. Typically, inside the root you can find the SceneNodes that are always loaded (the Cameras, the [BuildVolume](build_volume.md), and the Platform), the objects that are loaded on the platform, and finally a ConvexHullNode for each object and each group of objects in the Scene.
|
||||
|
||||
* Root
|
||||
* Camera
|
||||
* [Build volume](build_volume.md)
|
||||
* Platform
|
||||
* Object 1
|
||||
* Group 1
|
||||
* Object 2
|
||||
* Object 3
|
||||
* Object 1 convex hull node
|
||||
* Object 2 convex hull node
|
||||
* Object 3 convex hull node
|
||||
* Group 1 convex hull node
|
||||
Let's take the following example Scene:
|
||||
|
||||

|
||||
|
||||
The scene graph in this case is the following:
|
||||
|
||||
|
||||

|
||||
|
||||
**Note 1:** The Platform is actually a child of the BuildVolume.
|
||||
|
||||
**Note 2:** The ConvexHullNodes are not actually named after the object they decorate. Their names are used in the image to convey how the ConvexHullNodes are related to the objects in the scene.
|
||||
|
||||
**Note 3:** The CuraSceneNode that holds the layer data (inside the BuildVolume) is created and destroyed according to the availability of sliced layer data provided by the CuraEngine. See the [LayerDataDecorator](#layerdatadecorator) for more information.
|
||||
|
||||
Accessing SceneNodes in the Scene
|
||||
----
|
||||
|
||||
SceneNodes can be accessed using a `BreadthFirstIterator` or a `DepthFirstIterator`. Each iterator traverses the scene graph and returns a Python iterator, which yields all the SceneNodes and their children.
|
||||
|
||||
``` python
|
||||
for node in BreadthFirstIterator(scene.getRoot()):
|
||||
# do stuff with the node
|
||||
```
|
||||
|
||||
Example result when iterating the above scene graph:
|
||||
|
||||
```python
|
||||
[i for i in BreadthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()]
|
||||
```
|
||||
* 00 = {SceneNode} <SceneNode object: 'Root'>
|
||||
* 01 = {BuildVolume} <BuildVolume object '0x2e35dbce108'>
|
||||
* 02 = {Camera} <Camera object: '3d'>
|
||||
* 03 = {CuraSceneNode} <CuraSceneNode object: 'Torus.stl'>
|
||||
* 04 = {CuraSceneNode} <CuraSceneNode object: 'Group #1'>
|
||||
* 05 = {Camera} <Camera object: 'snapshot'>
|
||||
* 06 = {CuraSceneNode} <CuraSceneNode object: 'Star.stl'>
|
||||
* 07 = {ConvexHullNode} <ConvexHullNode object: '0x2e3000def08'>
|
||||
* 08 = {ConvexHullNode} <ConvexHullNode object: '0x2e36861bd88'>
|
||||
* 09 = {ConvexHullNode} <ConvexHullNode object: '0x2e3000bd4c8'>
|
||||
* 10 = {ConvexHullNode} <ConvexHullNode object: '0x2e35fbb62c8'>
|
||||
* 11 = {ConvexHullNode} <ConvexHullNode object: '0x2e3000a0648'>
|
||||
* 12 = {ConvexHullNode} <ConvexHullNode object: '0x2e30019d0c8'>
|
||||
* 13 = {ConvexHullNode} <ConvexHullNode object: '0x2e3001a2dc8'>
|
||||
* 14 = {Platform} <Platform object '0x2e35a001948'>
|
||||
* 15 = {CuraSceneNode} <CuraSceneNode object: 'Group #2'>
|
||||
* 16 = {CuraSceneNode} <CuraSceneNode object: 'Sphere.stl'>
|
||||
* 17 = {CuraSceneNode} <CuraSceneNode object: 'Cylinder.stl'>
|
||||
* 18 = {CuraSceneNode} <CuraSceneNode object: 'Cube.stl'>
|
||||
|
||||
SceneNodeDecorators
|
||||
----
|
||||
|
||||
SceneNodeDecorators are decorators that can be added to the nodes of the scene to provide them with additional functions.
|
||||
|
||||
Cura provides the following classes derived from the SceneNodeDecorator class:
|
||||
1. [GroupDecorator](#groupdecorator)
|
||||
2. [ConvexHullDecorator](#convexhulldecorator)
|
||||
3. [SettingOverrideDecorator](#settingoverridedecorator)
|
||||
4. [SliceableObjectDecorator](#sliceableobjectdecorator)
|
||||
5. [LayerDataDecorator](#layerdatadecorator)
|
||||
6. [ZOffsetDecorator](#zoffsetdecorator)
|
||||
7. [BlockSlicingDecorator](#blockslicingdecorator)
|
||||
8. [GCodeListDecorator](#gcodelistdecorator)
|
||||
9. [BuildPlateDecorator](#buildplatedecorator)
|
||||
|
||||
GroupDecorator
|
||||
----
|
||||
|
||||
Whenever objects on the build plate are grouped together, a new node is added in the scene as the parent of the grouped objects. Group nodes can be identified when traversing the SceneGraph by running the following:
|
||||
|
||||
```python
|
||||
node.callDecoration("isGroup") == True
|
||||
```
|
||||
|
||||
Group nodes decorated by GroupDecorators are added in the scene either by reading project files which contain grouped objects, or when the user selects multiple objects and groups them together (Ctrl + G).
|
||||
|
||||
Group nodes that are left with only one child are removed from the scene, making their only child a child of the group's parent. In addition, group nodes without any remaining children are removed from the scene.
|
||||
|
||||
ConvexHullDecorator
|
||||
----
|
||||
|
||||
As seen in the scene graph of the scene example, each CuraSceneNode that represents an object on the build plate is linked to a ConvexHullNode which is rendered as the object's shadow on the build plate. The ConvexHullDecorator is the link between these two nodes.
|
||||
|
||||
In essence, the CuraSceneNode has a ConvexHullDecorator which points to the ConvexHullNode of the object. The data of the object's convex hull can be accessed via
|
||||
|
||||
```python
|
||||
convex_hull_polygon = object_node.callDecoration("getConvexHull")
|
||||
```
|
||||
|
||||
The ConvexHullDecorator also provides convex hulls that include the head, the fans, and the adhesion of the object. These are primarily used and rendered when One-at-a-time mode is activated.
|
||||
|
||||
For more information on the functions added to the node by this decorator, visit the [ConvexHullDecorator.py](https://github.com/Ultimaker/Cura/blob/master/cura/Scene/ConvexHullDecorator.py).
|
||||
|
||||
SettingOverrideDecorator
|
||||
----
|
||||
|
||||
SettingOverrideDecorators are primarily used for modifier meshes such as support meshes, cutting meshes, infill meshes, and anti-overhang meshes. When a user converts an object to a modifier mesh, the object's node is decorated by a SettingOverrideDecorator. This decorator adds a PerObjectContainerStack to the CuraSceneNode, which allows the user to modify the settings of the specific model.
|
||||
|
||||
For more information on the functions added to the node by this decorator, visit the [SettingOverrideDecorator.py](https://github.com/Ultimaker/Cura/blob/master/cura/Settings/SettingOverrideDecorator.py).
|
||||
|
||||
|
||||
SliceableObjectDecorator
|
||||
----
|
||||
|
||||
This is a convenience decorator that allows us to easily identify the nodes which can be sliced. All **individual** objects (meshes) added to the build plate receive this decorator, apart from the nodes loaded from GCode files (.gcode, .g, .gz, .ufp).
|
||||
|
||||
The SceneNodes that do not receive this decorator are:
|
||||
|
||||
- Cameras
|
||||
- BuildVolume
|
||||
- Platform
|
||||
- ConvexHullNodes
|
||||
- CuraSceneNodes that serve as group nodes (these have a GroupDecorator instead)
|
||||
- The CuraSceneNode that serves as the layer data node
|
||||
- ToolHandles
|
||||
- NozzleNode
|
||||
- Nodes that contain GCode data. See the [BlockSlicingDecorator](#blockslicingdecorator) for more information on that.
|
||||
|
||||
This decorator provides the following function to the node:
|
||||
|
||||
```python
|
||||
node.callDecoration("isSliceable")
|
||||
```
|
||||
|
||||
LayerDataDecorator
|
||||
----
|
||||
|
||||
Once the Slicing has completed and the CuraEngine has returned the slicing data, Cura creates a CuraSceneNode inside the BuildVolume which is decorated by a LayerDataDecorator. This decorator holds the layer data of the scene.
|
||||
|
||||

|
||||
|
||||
The layer data can be accessed through the function given to the aforementioned CuraSceneNode by the LayerDataDecorator:
|
||||
|
||||
```python
|
||||
node.callDecoration("getLayerData")
|
||||
```
|
||||
|
||||
This CuraSceneNode is created once Cura has completed processing the Layer data (after the user clicks on the Preview tab after slicing). The CuraSceneNode then is destroyed once any action that changes the Scene occurs (e.g. if the user moves/rotates/scales an object or changes a setting value), indicating that the layer data is no longer available. When that happens, the "Slice" button becomes available again.
|
||||
|
||||
ZOffsetDecorator
|
||||
----
|
||||
|
||||
The ZOffsetDecorator is added to an object in the scene when that object is moved below the build plate. It is primarily used when the "Automatically drop models to the build plate" preference is enabled, in order to make sure that the GravityOperation, which drops the mode on the build plate, is not applied when the object is moved under the build plate.
|
||||
|
||||
The amount the object is moved under the build plate can be retrieved by calling the "getZOffset" decoration on the node:
|
||||
|
||||
```python
|
||||
z_offset = node.callDecoration("getZOffset")
|
||||
```
|
||||
|
||||
The ZOffsetDecorator is removed from the node when the node is move above the build plate.
|
||||
|
||||
BlockSlicingDecorator
|
||||
----
|
||||
|
||||
The BlockSlicingDecorator is the opposite of the SliceableObjectDecorator. It is added on objects loaded on the scene which should not be sliced. This decorator is primarily added on objects loaded from ".gcode", ".ufp", ".g", and ".gz" files. Such an object already contains all the slice information and therefore should not allow Cura to slice it.
|
||||
|
||||
If an object with a BlockSlicingDecorator appears in the scene, the backend (CuraEngine) and the print setup (changing print settings) become disabled, considering that G-code files cannot be modified.
|
||||
|
||||
The BlockSlicingDecorator adds the following decoration function to the node:
|
||||
|
||||
```python
|
||||
node.callDecoration("isBlockSlicing")
|
||||
```
|
||||
|
||||
GCodeListDecorator
|
||||
----
|
||||
|
||||
The GCodeListDecorator is also added only when a file containing GCode is loaded in the scene. It's purpose is to hold a list of all the GCode data of the loaded object.
|
||||
The GCode list data is stored in the scene's gcode_dict attribute which then is used in other places in the Cura code, e.g. to provide the GCode to the GCodeWriter or to the PostProcessingPlugin.
|
||||
|
||||
The GCode data becomes available by calling the "getGCodeList" decoration of the node:
|
||||
|
||||
```python
|
||||
gcode_list = node.callDecoration("getGCodeList")
|
||||
```
|
||||
|
||||
The CuraSceneNode with the GCodeListDecorator is destroyed when another object or project file is loaded in the Scene.
|
||||
|
||||
BuildPlateDecorator
|
||||
----
|
||||
|
||||
The BuildPlateDecorator is added to all the CuraSceneNodes. This decorator is linked to a legacy feature which allowed the user to have multiple build plates open in Cura at the same time. With this decorator it was possible to determine which nodes are present on each build plate, and therefore, which objects should be visible in the currently active build plate. It indicates the number of the build plate this scene node belongs to, which currently is always the build plate -1.
|
||||
|
||||
This decorator provides a function to the node that returns the number of the build plate it belongs to:
|
||||
|
||||
```python
|
||||
node.callDecoration("getBuildPlateNumber")
|
||||
```
|
||||
|
||||
**Note:** Changing the active build plate is a disabled feature in Cura and it is intended to be completely removed (internal ticket: CURA-4975).
|
||||
|
|
86
docs/scene/tools.md
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Tools
|
||||
|
||||
Tools are plugin objects which are used to manipulate or interact with the scene and the objects (node) in the scene.
|
||||
|
||||

|
||||
|
||||
Tools live inside the Controller of the Application and may be associated with ToolHandles. Some of them interact with the scene as a whole (such as the Camera), while others interact with the objects (nodes) in the Scene (selection tool, rotate tool, scale tool etc.). The tools that are available in Cura (excluding the ones provided by downloadable plugins) are the following:
|
||||
|
||||
* [CameraTool](#cameratool)
|
||||
* [SelectionTool](#selectiontool)
|
||||
* [TranslateTool](#translatetool)
|
||||
* [ScaleTool](#scaletool)
|
||||
* [RotateTool](#rotatetool)
|
||||
* [MirrorTool](#mirrortool)
|
||||
* [PerObjectSettingsTool](#perobjectsettingstool)
|
||||
* [SupportEraserTool](#supporteraser)
|
||||
|
||||
*****
|
||||
|
||||
### CameraTool
|
||||
|
||||
The CameraTool is the tool that allows the user to manipulate the Camera. It provides the functions of moving, zooming, and rotating the Camera. This tool does not contain a handle.
|
||||
|
||||
### SelectionTool
|
||||
This tool allows the user to select objects and groups of objects in the scene. The selected objects gain a blue outline and become available in the code through the Selection class.
|
||||
|
||||

|
||||
|
||||
This tool does not contain a handle.
|
||||
|
||||
### TranslateTool
|
||||
|
||||
This tool allows the user to move the object around the build plate. The TranslateTool is activated once the user presses the Move icon in the tool sidebar or hits the shortcut (T) while an object is selected.
|
||||
|
||||

|
||||
|
||||
The TranslateTool contains the TranslateToolHandle, which draws the arrow handles on the selected object(s). The TranslateTool generates TranslateOperations whenever the object is moved around the build plate.
|
||||
|
||||
|
||||
### ScaleTool
|
||||
|
||||
This tool allows the user to scale the selected object(s). The ScaleTool is activated once the user presses the Scale icon in the tool sidebar or hits the shortcut (S) while an object is selected.
|
||||
|
||||

|
||||
|
||||
The ScaleTool contains the ScaleToolHandle, which draws the box handles on the selected object(s). The ScaleTool generates ScaleOperations whenever the object is scaled.
|
||||
|
||||
### RotateTool
|
||||
|
||||
This tool allows the user to rotate the selected object(s). The RotateTool is activated once the user presses the Rotate icon in the tool sidebar or hits the shortcut (R) while an object is selected.
|
||||
|
||||

|
||||
|
||||
The RotateTool contains the RotateToolHandle, which draws the donuts (tori) and arrow handles on the selected object(s). The RotateTool generates RotateOperations whenever the object is rotated or if a face is selected to be laid flat on the build plate. It also contains the `layFlat()` action, which generates the [LayFlatOperation](operations.md#layflatoperation).
|
||||
|
||||
|
||||
### MirrorTool
|
||||
|
||||
This tool allows the user to mirror the selected object(s) in the required direction. The MirrorTool is activated once the user presses the Mirror icon in the tool sidebar or hits the shortcut (M) while an object is selected.
|
||||
|
||||

|
||||
|
||||
The MirrorTool contains the MirrorToolHandle, which draws pyramid handles on the selected object(s). The MirrorTool generates MirrorOperations whenever the object is mirrored against an axis.
|
||||
|
||||
### PerObjectSettingsTool
|
||||
|
||||
This tool allows the user to change the mesh type of the object into one of the following:
|
||||
|
||||
* Normal Model
|
||||
* Print as support
|
||||
* Modify settings for overlaps
|
||||
- Infill mesh only
|
||||
- Cutting mesh
|
||||
* Don't support overlaps
|
||||
|
||||

|
||||
|
||||
Contrary to other tools, this tool doesn't have any handles and it does not generate any operations. This means that once an object's type is changed it cannot be undone/redone using the OperationStack. This tool adds a [SettingOverrideDecorator](scene.md#settingoverridedecorator) on the object's node instead, which allows the user to change certain settings only for this mesh.
|
||||
|
||||
### SupportEraser tool
|
||||
|
||||
This tool allows the user to add support blockers on the selected model. The SupportEraserTool is activated once the user pressed the Support Blocker icon in the tool sidebar or hits the shortcut (E) while an object is selected. With this tool active, the user can add support blockers (cubes) on the object by clicking on various places on the selected mesh.
|
||||
|
||||

|
||||
|
||||
The SupportEraser uses a GroupOperation to add a new CuraSceneNode (the eraser) in the scene and set the selected model as the parent of the eraser. This means that the addition of Erasers in the scene can be undone/redone. The SupportEraser does not have any tool handles.
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
|
@ -51,6 +51,10 @@ class ThreeMFReader(MeshReader):
|
|||
self._root = None
|
||||
self._base_name = ""
|
||||
self._unit = None
|
||||
self._empty_project = False
|
||||
|
||||
def emptyFileHintSet(self) -> bool:
|
||||
return self._empty_project
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
||||
if transformation == "":
|
||||
|
@ -159,9 +163,9 @@ class ThreeMFReader(MeshReader):
|
|||
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
|
||||
|
||||
setting_container = um_node.callDecoration("getStack").getTop()
|
||||
|
||||
known_setting_keys = um_node.callDecoration("getStack").getAllKeys()
|
||||
for key in settings:
|
||||
setting_value = settings[key]
|
||||
setting_value = settings[key].value
|
||||
|
||||
# Extruder_nr is a special case.
|
||||
if key == "extruder_nr":
|
||||
|
@ -171,7 +175,10 @@ class ThreeMFReader(MeshReader):
|
|||
else:
|
||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||
continue
|
||||
if key in known_setting_keys:
|
||||
setting_container.setProperty(key, "value", setting_value)
|
||||
else:
|
||||
um_node.metadata[key] = settings[key]
|
||||
|
||||
if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None:
|
||||
if len(um_node.getAllChildren()) == 1:
|
||||
|
@ -193,6 +200,7 @@ class ThreeMFReader(MeshReader):
|
|||
return um_node
|
||||
|
||||
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
|
||||
self._empty_project = False
|
||||
result = []
|
||||
# The base object of 3mf is a zipped archive.
|
||||
try:
|
||||
|
@ -201,6 +209,10 @@ class ThreeMFReader(MeshReader):
|
|||
parser = Savitar.ThreeMFParser()
|
||||
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
|
||||
self._unit = scene_3mf.getUnit()
|
||||
|
||||
for key, value in scene_3mf.getMetadata().items():
|
||||
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
||||
|
||||
for node in scene_3mf.getSceneNodes():
|
||||
um_node = self._convertSavitarNodeToUMNode(node, file_name)
|
||||
if um_node is None:
|
||||
|
@ -257,6 +269,9 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
result.append(um_node)
|
||||
|
||||
if len(result) == 0:
|
||||
self._empty_project = True
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||
return []
|
||||
|
|
|
@ -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"):
|
||||
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")
|
||||
|
@ -636,6 +641,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
except zipfile.BadZipFile as e:
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
|
||||
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"))
|
||||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
|
||||
|
@ -1150,7 +1162,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
return
|
||||
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
|
||||
else:
|
||||
self._quality_type_to_apply = self._quality_type_to_apply.lower()
|
||||
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
|
||||
quality_group_dict = container_tree.getCurrentQualityGroups()
|
||||
if self._quality_type_to_apply in quality_group_dict:
|
||||
quality_group = quality_group_dict[self._quality_type_to_apply]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2016 Ultimaker B.V.
|
||||
// Copyright (c) 2020 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
|
@ -7,6 +7,7 @@ import QtQuick.Layouts 1.3
|
|||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.1 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
|
@ -110,13 +111,14 @@ UM.Dialog
|
|||
height: visible ? comboboxHeight : 0
|
||||
visible: base.visible && machineResolveComboBox.model.count > 1
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||
ComboBox
|
||||
Cura.ComboBox
|
||||
{
|
||||
id: machineResolveComboBox
|
||||
model: manager.updatableMachinesModel
|
||||
visible: machineResolveStrategyTooltip.visible
|
||||
textRole: "displayName"
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onCurrentIndexChanged:
|
||||
{
|
||||
if (model.getItem(currentIndex).id == "new"
|
||||
|
@ -217,12 +219,13 @@ UM.Dialog
|
|||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.qualityChangesConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||
ComboBox
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: qualityChangesResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||
|
@ -323,12 +326,13 @@ UM.Dialog
|
|||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.materialConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||
ComboBox
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: materialResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||
|
@ -415,7 +419,7 @@ UM.Dialog
|
|||
width: warningLabel.height
|
||||
height: width
|
||||
|
||||
source: UM.Theme.getIcon("notice")
|
||||
source: UM.Theme.getIcon("Information")
|
||||
color: palette.text
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ from cura.CuraApplication import CuraApplication
|
|||
import Savitar
|
||||
|
||||
import numpy
|
||||
import datetime
|
||||
|
||||
MYPY = False
|
||||
try:
|
||||
|
@ -108,7 +109,11 @@ class ThreeMFWriter(MeshWriter):
|
|||
|
||||
# Get values for all changed settings & save them.
|
||||
for key in changed_setting_keys:
|
||||
savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
|
||||
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
|
||||
|
||||
# Store the metadata.
|
||||
for key, value in um_node.metadata.items():
|
||||
savitar_node.setSetting(key, value)
|
||||
|
||||
for child_node in um_node.getChildren():
|
||||
# only save the nodes on the active build plate
|
||||
|
@ -145,6 +150,22 @@ class ThreeMFWriter(MeshWriter):
|
|||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
||||
|
||||
savitar_scene = Savitar.Scene()
|
||||
|
||||
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
|
||||
|
||||
for key, value in metadata_to_store.items():
|
||||
savitar_scene.setMetaDataEntry(key, value)
|
||||
|
||||
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if "Application" not in metadata_to_store:
|
||||
# This might sound a bit strange, but this field should store the original application that created
|
||||
# the 3mf. So if it was already set, leave it to whatever it was.
|
||||
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
|
||||
if "CreationDate" not in metadata_to_store:
|
||||
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
|
||||
|
||||
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
|
||||
|
||||
transformation_matrix = Matrix()
|
||||
transformation_matrix._data[1, 1] = 0
|
||||
transformation_matrix._data[1, 2] = -1
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -157,22 +157,22 @@ class AMFReader(MeshReader):
|
|||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
indices = []
|
||||
vertices = []
|
||||
indices_list = []
|
||||
vertices_list = []
|
||||
|
||||
index_count = 0
|
||||
face_count = 0
|
||||
for tri_face in tri_faces:
|
||||
face = []
|
||||
for tri_index in tri_face:
|
||||
vertices.append(tri_vertices[tri_index])
|
||||
vertices_list.append(tri_vertices[tri_index])
|
||||
face.append(index_count)
|
||||
index_count += 1
|
||||
indices.append(face)
|
||||
indices_list.append(face)
|
||||
face_count += 1
|
||||
|
||||
vertices = numpy.asarray(vertices, dtype = numpy.float32)
|
||||
indices = numpy.asarray(indices, dtype = numpy.int32)
|
||||
vertices = numpy.asarray(vertices_list, dtype = numpy.float32)
|
||||
indices = numpy.asarray(indices_list, dtype = numpy.int32)
|
||||
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals,file_name = file_name)
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": "7.4.0"
|
||||
"api": 7
|
||||
}
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import threading
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import sentry_sdk
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.Job import Job
|
||||
|
@ -99,13 +98,7 @@ class CreateBackupJob(Job):
|
|||
if HttpRequestManager.safeHttpStatus(reply) == 400:
|
||||
errors = json.loads(replyText)["errors"]
|
||||
if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
|
||||
if self._backup_zip is None: # will never happen; keep mypy happy
|
||||
zip_error = "backup is None."
|
||||
else:
|
||||
zip_error = "{} exceeds max size.".format(str(len(self._backup_zip)))
|
||||
sentry_sdk.capture_message("backup failed: {}".format(zip_error), level ="warning")
|
||||
self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
self._job_done.set()
|
||||
return
|
||||
|
|
|
@ -43,6 +43,10 @@ class DriveApiService:
|
|||
return
|
||||
|
||||
backup_list_response = HttpRequestManager.readJSON(reply)
|
||||
if backup_list_response is None:
|
||||
Logger.error("List of back-ups can't be parsed.")
|
||||
changed([])
|
||||
return
|
||||
if "data" not in backup_list_response:
|
||||
Logger.log("w", "Could not get backups from remote, actual response body was: %s",
|
||||
str(backup_list_response))
|
||||
|
|
|
@ -34,6 +34,9 @@ class DrivePluginExtension(QObject, Extension):
|
|||
# Signal emitted when preferences changed (like auto-backup).
|
||||
preferencesChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when the id of the backup-to-be-restored is changed
|
||||
backupIdBeingRestoredChanged = pyqtSignal(arguments = ["backup_id_being_restored"])
|
||||
|
||||
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
@ -45,6 +48,7 @@ class DrivePluginExtension(QObject, Extension):
|
|||
self._backups = [] # type: List[Dict[str, Any]]
|
||||
self._is_restoring_backup = False
|
||||
self._is_creating_backup = False
|
||||
self._backup_id_being_restored = ""
|
||||
|
||||
# Initialize services.
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
|
@ -52,6 +56,7 @@ class DrivePluginExtension(QObject, Extension):
|
|||
|
||||
# Attach signals.
|
||||
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
CuraApplication.getInstance().applicationShuttingDown.connect(self._onApplicationShuttingDown)
|
||||
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
|
||||
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
|
||||
|
||||
|
@ -75,6 +80,10 @@ class DrivePluginExtension(QObject, Extension):
|
|||
if self._drive_window:
|
||||
self._drive_window.show()
|
||||
|
||||
def _onApplicationShuttingDown(self):
|
||||
if self._drive_window:
|
||||
self._drive_window.hide()
|
||||
|
||||
def _autoBackup(self) -> None:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
|
||||
|
@ -100,10 +109,11 @@ class DrivePluginExtension(QObject, Extension):
|
|||
if logged_in:
|
||||
self.refreshBackups()
|
||||
|
||||
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: Optional[str] = None) -> None:
|
||||
self._is_restoring_backup = is_restoring
|
||||
self.restoringStateChanged.emit()
|
||||
if error_message:
|
||||
self.backupIdBeingRestored = ""
|
||||
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
|
||||
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||
|
@ -152,6 +162,7 @@ class DrivePluginExtension(QObject, Extension):
|
|||
for backup in self._backups:
|
||||
if backup.get("backup_id") == backup_id:
|
||||
self._drive_api_service.restoreBackup(backup)
|
||||
self.setBackupIdBeingRestored(backup_id)
|
||||
return
|
||||
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
|
||||
|
||||
|
@ -166,3 +177,12 @@ class DrivePluginExtension(QObject, Extension):
|
|||
def _backupDeletedCallback(self, success: bool):
|
||||
if success:
|
||||
self.refreshBackups()
|
||||
|
||||
def setBackupIdBeingRestored(self, backup_id_being_restored: str) -> None:
|
||||
if backup_id_being_restored != self._backup_id_being_restored:
|
||||
self._backup_id_being_restored = backup_id_being_restored
|
||||
self.backupIdBeingRestoredChanged.emit()
|
||||
|
||||
@pyqtProperty(str, fset = setBackupIdBeingRestored, notify = backupIdBeingRestoredChanged)
|
||||
def backupIdBeingRestored(self) -> str:
|
||||
return self._backup_id_being_restored
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import threading
|
||||
|
@ -56,6 +59,7 @@ class RestoreBackupJob(Job):
|
|||
return
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
try:
|
||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
app = CuraApplication.getInstance()
|
||||
|
@ -64,6 +68,11 @@ class RestoreBackupJob(Job):
|
|||
write_backup.write(bytes_read)
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
app.processEvents()
|
||||
except EnvironmentError as e:
|
||||
Logger.log("e", f"Unable to save backed up files due to computer limitations: {str(e)}")
|
||||
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
|
||||
self._job_done.set()
|
||||
return
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
|
|
|
@ -20,7 +20,7 @@ RowLayout
|
|||
{
|
||||
id: infoButton
|
||||
text: catalog.i18nc("@button", "Want more?")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
iconSource: UM.Theme.getIcon("Information")
|
||||
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||
visible: backupListFooter.showInfoButton
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ RowLayout
|
|||
{
|
||||
id: createBackupButton
|
||||
text: catalog.i18nc("@button", "Backup Now")
|
||||
iconSource: UM.Theme.getIcon("plus")
|
||||
iconSource: UM.Theme.getIcon("Plus")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: CuraDrive.createBackup()
|
||||
busy: CuraDrive.isCreatingBackup
|
||||
|
|
|
@ -38,7 +38,7 @@ Item
|
|||
height: UM.Theme.getSize("section_icon").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
iconSource: UM.Theme.getIcon("Information")
|
||||
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||
}
|
||||
|
||||
|
@ -71,6 +71,7 @@ Item
|
|||
text: catalog.i18nc("@button", "Restore")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: confirmRestoreDialog.visible = true
|
||||
busy: CuraDrive.backupIdBeingRestored == modelData.backup_id && CuraDrive.isRestoringBackup
|
||||
}
|
||||
|
||||
UM.SimpleButton
|
||||
|
@ -79,7 +80,7 @@ Item
|
|||
height: UM.Theme.getSize("message_close").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("cross1")
|
||||
iconSource: UM.Theme.getIcon("Cancel")
|
||||
onClicked: confirmDeleteDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
import QtQuick 2.7
|
||||
|
@ -17,7 +17,7 @@ ColumnLayout
|
|||
// Cura version
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("application")
|
||||
iconSource: UM.Theme.getIcon("UltimakerCura")
|
||||
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||
value: backupDetailsData.metadata.cura_release
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ ColumnLayout
|
|||
// Machine count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("printer_single")
|
||||
iconSource: UM.Theme.getIcon("Printer")
|
||||
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||
value: backupDetailsData.metadata.machine_count
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ ColumnLayout
|
|||
// Material count
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("category_material")
|
||||
iconSource: UM.Theme.getIcon("Spool")
|
||||
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||
value: backupDetailsData.metadata.material_count
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ ColumnLayout
|
|||
// Profile count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("settings")
|
||||
iconSource: UM.Theme.getIcon("Sliders")
|
||||
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||
value: backupDetailsData.metadata.profile_count
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ ColumnLayout
|
|||
// Plugin count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("plugin")
|
||||
iconSource: UM.Theme.getIcon("Plugin")
|
||||
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||
value: backupDetailsData.metadata.plugin_count
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ Item
|
|||
BackupListFooter
|
||||
{
|
||||
id: backupListFooter
|
||||
showInfoButton: backupList.model.length > 4
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import argparse #To run the engine in debug mode if the front-end is in debug mode.
|
||||
from collections import defaultdict
|
||||
import os
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSlot
|
||||
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot
|
||||
import sys
|
||||
from time import time
|
||||
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import QDesktopServices, QImage
|
||||
|
||||
from UM.Backend.Backend import Backend, BackendState
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Signal import Signal
|
||||
|
@ -24,6 +26,8 @@ from UM.Tool import Tool #For typing.
|
|||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Snapshot import Snapshot
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
|
||||
from .StartSliceJob import StartSliceJob, StartJobResult
|
||||
|
||||
|
@ -153,6 +157,20 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.determineAutoSlicing()
|
||||
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._slicing_error_message = Message(
|
||||
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
|
||||
title = catalog.i18nc("@message:title", "Slicing failed")
|
||||
)
|
||||
self._slicing_error_message.addAction(
|
||||
action_id = "report_bug",
|
||||
name = catalog.i18nc("@message:button", "Report a bug"),
|
||||
description = catalog.i18nc("@message:description", "Report a bug on Ultimaker Cura's issue tracker."),
|
||||
icon = "[no_icon]"
|
||||
)
|
||||
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
|
||||
|
||||
self._snapshot = None #type: Optional[QImage]
|
||||
|
||||
application.initializationFinished.connect(self.initialize)
|
||||
|
||||
def initialize(self) -> None:
|
||||
|
@ -241,9 +259,27 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self.markSliceAll()
|
||||
self.slice()
|
||||
|
||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||
def _createSnapshot(self) -> None:
|
||||
self._snapshot = None
|
||||
if not CuraApplication.getInstance().isVisible:
|
||||
Logger.log("w", "Can't create snapshot when renderer not initialized.")
|
||||
return
|
||||
Logger.log("i", "Creating thumbnail image (just before slice)...")
|
||||
try:
|
||||
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
|
||||
except:
|
||||
Logger.logException("w", "Failed to create snapshot image")
|
||||
self._snapshot = None # Failing to create thumbnail should not fail creation of UFP
|
||||
|
||||
def getLatestSnapshot(self) -> Optional[QImage]:
|
||||
return self._snapshot
|
||||
|
||||
def slice(self) -> None:
|
||||
"""Perform a slice of the scene."""
|
||||
|
||||
self._createSnapshot()
|
||||
|
||||
Logger.log("i", "Starting to slice...")
|
||||
self._slice_start_time = time()
|
||||
if not self._build_plates_to_be_sliced:
|
||||
|
@ -331,7 +367,6 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
|
||||
"""Event handler to call when the job to initiate the slicing process is
|
||||
|
||||
completed.
|
||||
|
||||
When the start slice job is successfully completed, it will be happily
|
||||
|
@ -899,9 +934,22 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
if not self._restart:
|
||||
if self._process: # type: ignore
|
||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore
|
||||
return_code = self._process.wait()
|
||||
if return_code != 0:
|
||||
Logger.log("e", f"Backend exited abnormally with return code {return_code}!")
|
||||
self._slicing_error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
self.stopSlicing()
|
||||
else:
|
||||
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
|
||||
self._process = None # type: ignore
|
||||
|
||||
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
|
||||
"""
|
||||
Triggered when the user wants to report an error in the back-end.
|
||||
"""
|
||||
QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose"))
|
||||
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
"""Called when the global container stack changes"""
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"version": "1.0.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": "7.4.0",
|
||||
"api": 7,
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
|
17
plugins/DigitalLibrary/__init__.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
from .src import DigitalFactoryFileProvider, DigitalFactoryOutputDevicePlugin, DigitalFactoryController
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
df_controller = DigitalFactoryController.DigitalFactoryController(app)
|
||||
return {
|
||||
"file_provider": DigitalFactoryFileProvider.DigitalFactoryFileProvider(df_controller),
|
||||
"output_device": DigitalFactoryOutputDevicePlugin.DigitalFactoryOutputDevicePlugin(df_controller)
|
||||
}
|
8
plugins/DigitalLibrary/plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "Ultimaker Digital Library",
|
||||
"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,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
6
plugins/DigitalLibrary/resources/images/arrow_down.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<polygon style="fill:#000E1A;" points="19.7,13.3 18.3,11.9 13,17.2 13,3 11,3 11,17.2 5.7,11.9 4.3,13.3 12,21 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 470 B |
After Width: | Height: | Size: 19 KiB |
3
plugins/DigitalLibrary/resources/images/placeholder.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<path d="M24,44,7,33.4V14.6L24,4,41,14.6V33.4ZM9,32.3l15,9.3,15-9.3V15.7L24,6.4,9,15.7Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 184 B |
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 378.13 348.13" version="1.1">
|
||||
<defs
|
||||
id="defs7">
|
||||
<style
|
||||
id="style2">
|
||||
.cls-2,.cls-6{fill:#c5dbfb;}
|
||||
.cls-6,.cls-7{stroke-width:2px;}
|
||||
.cls-7{fill:#f3f8fe;}
|
||||
.cls-6,.cls-7{stroke:#061884;}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-2" d="M43,17V3H83a2,2,0,0,1,2,2V17Z" />
|
||||
<path fill="white" d="M 3 1 C 1.8954305 1 1 1.8954305 1 3 L 1 67 C 1 68.104569 1.8954305 69 3 69 L 72.152344 69 A 100 100 0 0 1 89 49.873047 L 89 19 C 89 17.895431 88.104569 17 87 17 L 56 17 L 40 1 L 3 1 z " />
|
||||
<path fill="#c5dbfb" d="M 3 0 C 1.3549904 0 0 1.3549904 0 3 L 0 67 C 0 68.64501 1.3549904 70 3 70 L 71.484375 70 A 100 100 0 0 1 72.835938 68 L 3 68 C 2.4358706 68 2 67.564129 2 67 L 2 3 C 2 2.4358706 2.4358706 2 3 2 L 39.585938 2 L 55.585938 18 L 87 18 C 87.564129 18 88 18.435871 88 19 L 88 50.765625 A 100 100 0 0 1 90 49.007812 L 90 19 C 90 17.35499 88.64501 16 87 16 L 56.414062 16 L 40.414062 0 L 3 0 z " />
|
||||
<path class="cls-2" d="M153,17V3h40a2,2,0,0,1,2,2V17Z" />
|
||||
<path fill="white" d="M 113 1 C 111.89543 1 111 1.8954305 111 3 L 111 35.201172 A 100 100 0 0 1 155 25 A 100 100 0 0 1 199 35.201172 L 199 19 C 199 17.895431 198.10457 17 197 17 L 166 17 L 150 1 L 113 1 z " />
|
||||
<path fill="#c5dbfb" d="M 113 0 C 111.35499 0 110 1.3549904 110 3 L 110 35.699219 A 100 100 0 0 1 112 34.716797 L 112 3 C 112 2.4358706 112.43587 2 113 2 L 149.58594 2 L 165.58594 18 L 197 18 C 197.56413 18 198 18.435871 198 19 L 198 34.716797 A 100 100 0 0 1 200 35.699219 L 200 19 C 200 17.35499 198.64501 16 197 16 L 166.41406 16 L 150.41406 0 L 113 0 z " />
|
||||
<path class="cls-2" d="M263,17V3h40a2,2,0,0,1,2,2V17Z" />
|
||||
<path fill="white" d="M 223 1 C 221.89543 1 221 1.8954305 221 3 L 221 49.875 A 100 100 0 0 1 237.84961 69 L 307 69 C 308.10457 69 309 68.104569 309 67 L 309 19 C 309 17.895431 308.10457 17 307 17 L 276 17 L 260 1 L 223 1 z " />
|
||||
<path fill="#c5dbfb" d="M 223 0 C 221.35499 0 220 1.3549904 220 3 L 220 49.005859 A 100 100 0 0 1 222 50.765625 L 222 3 C 222 2.4358706 222.43587 2 223 2 L 259.58594 2 L 275.58594 18 L 307 18 C 307.56413 18 308 18.435871 308 19 L 308 67 C 308 67.564129 307.56413 68 307 68 L 237.16406 68 A 100 100 0 0 1 238.51562 70 L 307 70 C 308.64501 70 310 68.64501 310 67 L 310 19 C 310 17.35499 308.64501 16 307 16 L 276.41406 16 L 260.41406 0 L 223 0 z " />
|
||||
<path fill="#c5dbfb" d="M 43 93 L 43 107 L 56.634766 107 A 100 100 0 0 1 60.259766 93 L 43 93 z " />
|
||||
<path fill="white" d="M 3 91 C 1.8954305 91 1 91.895431 1 93 L 1 157 C 1 158.10457 1.8954305 159 3 159 L 60.958984 159 A 100 100 0 0 1 55 125 A 100 100 0 0 1 56.634766 107 L 56 107 L 40 91 L 3 91 z " />
|
||||
<path fill="#c5dbfb" d="M 3 90 C 1.3549904 90 0 91.35499 0 93 L 0 157 C 0 158.64501 1.3549904 160 3 160 L 61.324219 160 A 100 100 0 0 1 60.603516 158 L 3 158 C 2.4358706 158 2 157.56413 2 157 L 2 93 C 2 92.435871 2.4358706 92 3 92 L 39.585938 92 L 55.585938 108 L 56.455078 108 A 100 100 0 0 1 56.822266 106 L 56.414062 106 L 40.414062 90 L 3 90 z " />
|
||||
<path class="cls-2" d="M263,107V93h40a2,2,0,0,1,2,2v12Z" />
|
||||
<path fill="white" d="M 249.04102 91 A 100 100 0 0 1 255 125 A 100 100 0 0 1 249.04102 159 L 307 159 C 308.10457 159 309 158.10457 309 157 L 309 109 C 309 107.89543 308.10457 107 307 107 L 276 107 L 260 91 L 249.04102 91 z " />
|
||||
<path fill="#c5dbfb" d="M 248.67578 90 A 100 100 0 0 1 249.39648 92 L 259.58594 92 L 275.58594 108 L 307 108 C 307.56413 108 308 108.43587 308 109 L 308 157 C 308 157.56413 307.56413 158 307 158 L 249.39844 158 A 100 100 0 0 1 248.67383 160 L 307 160 C 308.64501 160 310 158.64501 310 157 L 310 109 C 310 107.35499 308.64501 106 307 106 L 276.41406 106 L 260.41406 90 L 248.67578 90 z " />
|
||||
<path fill="#c5dbfb" d="M 43 183 L 43 197 L 85 197 L 85 196.41406 A 100 100 0 0 1 73.539062 183 L 43 183 z " />
|
||||
<path fill="white" d="M 3 181 C 1.8954305 181 1 181.89543 1 183 L 1 247 C 1 248.10457 1.8954305 249 3 249 L 87 249 C 88.104569 249 89 248.10457 89 247 L 89 200.125 A 100 100 0 0 1 85.603516 197 L 56 197 L 40 181 L 3 181 z " />
|
||||
<path fill="#c5dbfb" d="M 3 180 C 1.3549904 180 0 181.35499 0 183 L 0 247 C 0 248.64501 1.3549904 250 3 250 L 87 250 C 88.64501 250 90 248.64501 90 247 L 90 200.99414 A 100 100 0 0 1 88 199.23438 L 88 247 C 88 247.56413 87.564129 248 87 248 L 3 248 C 2.4358706 248 2 247.56413 2 247 L 2 183 C 2 182.43587 2.4358706 182 3 182 L 39.585938 182 L 55.585938 198 L 86.65625 198 A 100 100 0 0 1 84.580078 196 L 56.414062 196 L 40.414062 180 L 3 180 z " />
|
||||
<path fill="white" d="M 111 214.79883 L 111 247 C 111 248.10457 111.89543 249 113 249 L 197 249 C 198.10457 249 199 248.10457 199 247 L 199 214.79883 A 100 100 0 0 1 155 225 A 100 100 0 0 1 111 214.79883 z " />
|
||||
<path fill="#c5dbfb" d="M 110 214.30078 L 110 247 C 110 248.64501 111.35499 250 113 250 L 197 250 C 198.64501 250 200 248.64501 200 247 L 200 214.30078 A 100 100 0 0 1 198 215.2832 L 198 247 C 198 247.56413 197.56413 248 197 248 L 113 248 C 112.43587 248 112 247.56413 112 247 L 112 215.2832 A 100 100 0 0 1 110 214.30078 z " />
|
||||
<path class="cls-2" d="M263,197V183h40a2,2,0,0,1,2,2v12Z" />
|
||||
<path fill="white" d="M 237.84766 181 A 100 100 0 0 1 221 200.12695 L 221 247 C 221 248.10457 221.89543 249 223 249 L 307 249 C 308.10457 249 309 248.10457 309 247 L 309 199 C 309 197.89543 308.10457 197 307 197 L 276 197 L 260 181 L 237.84766 181 z " />
|
||||
<path fill="#c5dbfb" d="M 238.51562 180 A 100 100 0 0 1 237.16406 182 L 259.58594 182 L 275.58594 198 L 307 198 C 307.56413 198 308 198.43587 308 199 L 308 247 C 308 247.56413 307.56413 248 307 248 L 223 248 C 222.43587 248 222 247.56413 222 247 L 222 199.23438 A 100 100 0 0 1 220 200.99219 L 220 247 C 220 248.64501 221.35499 250 223 250 L 307 250 C 308.64501 250 310 248.64501 310 247 L 310 199 C 310 197.35499 308.64501 196 307 196 L 276.41406 196 L 260.41406 180 L 238.51562 180 z " />
|
||||
<path class="cls-6" d="M351.12,322.62h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7A10,10,0,0,1,351.12,322.62Z" transform="translate(850.61 309.91) rotate(135)" />
|
||||
<rect class="cls-7" x="293.75" y="225.25" width="40" height="117" transform="translate(-108.74 304.96) rotate(-45)" />
|
||||
<polyline class="cls-7" points="213.69 199.25 252.58 238.14 267.43 223.29 228.54 184.4" />
|
||||
<path fill="white" stroke="#061884" stroke-width="2px" d="M 154.94141 30 A 95 95 0 0 0 60 125 A 95 95 0 0 0 155 220 A 95 95 0 0 0 250 125 A 95 95 0 0 0 155 30 A 95 95 0 0 0 154.94141 30 z M 154.82812 40 A 85 85 0 0 1 155 40 A 85 85 0 0 1 240 125 A 85 85 0 0 1 155 210 A 85 85 0 0 1 70 125 A 85 85 0 0 1 154.82812 40 z " />
|
||||
<path class="cls-6" d="M256.37,227.87h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7a10,10,0,0,1,10-10Z" transform="translate(-89.12 257.58) rotate(-45)" />
|
||||
<path fill="white" d="M 154.94141 45 A 80 80 0 0 0 111 58.185547 L 111 67 C 111 68.104569 111.89543 69 113 69 L 197 69 C 198.10457 69 199 68.104569 199 67 L 199 58.1875 A 80 80 0 0 0 155 45 A 80 80 0 0 0 154.94141 45 z " />
|
||||
<path fill="#061884" d="M 112 57.539062 A 80 80 0 0 0 110 58.857422 L 110 67 C 110 68.64501 111.35499 70 113 70 L 197 70 C 198.64501 70 200 68.64501 200 67 L 200 58.857422 A 80 80 0 0 0 198 57.541016 L 198 67 C 198 67.564129 197.56413 68 197 68 L 113 68 C 112.43587 68 112 67.564129 112 67 L 112 57.539062 z " />
|
||||
<path fill="#196ef0" d="M 81.679688 93 A 80 80 0 0 0 77.050781 107 L 85 107 L 85 95 A 2 2 0 0 0 83 93 L 81.679688 93 z " />
|
||||
<path fill="white" d="M 77.050781 107 A 80 80 0 0 0 75 125 A 80 80 0 0 0 82.585938 159 L 87 159 C 88.104569 159 89 158.10457 89 157 L 89 109 C 89 107.89543 88.104569 107 87 107 L 77.050781 107 z " />
|
||||
<path fill="#061884" d="M 77.289062 106 A 80 80 0 0 0 76.828125 108 L 87 108 C 87.564129 108 88 108.43587 88 109 L 88 157 C 88 157.56413 87.564129 158 87 158 L 82.125 158 A 80 80 0 0 0 83.0625 160 L 87 160 C 88.64501 160 90 158.64501 90 157 L 90 109 C 90 107.35499 88.64501 106 87 106 L 77.289062 106 z " />
|
||||
<path fill="white" d="M 223 91 C 221.89543 91 221 91.895431 221 93 L 221 157 C 221 158.10457 221.89543 159 223 159 L 227.41406 159 A 80 80 0 0 0 235 125 A 80 80 0 0 0 227.41406 91 L 223 91 z " />
|
||||
<path fill="#061884" d="M 223 90 C 221.35499 90 220 91.35499 220 93 L 220 157 C 220 158.64501 221.35499 160 223 160 L 226.9375 160 A 80 80 0 0 0 227.87695 158 L 223 158 C 222.43587 158 222 157.56413 222 157 L 222 93 C 222 92.435871 222.43587 92 223 92 L 227.875 92 A 80 80 0 0 0 226.9375 90 L 223 90 z " />
|
||||
<path fill="#196ef0" d="M 153 183 L 153 197 L 189.86914 197 A 80 80 0 0 0 195 194.28125 L 195 185 A 2 2 0 0 0 193 183 L 153 183 z "/>
|
||||
<path fill="white" d="M 113 181 C 111.89543 181 111 181.89543 111 183 L 111 191.8125 A 80 80 0 0 0 155 205 A 80 80 0 0 0 189.86914 197 L 166 197 L 150 181 L 113 181 z " />
|
||||
<path fill="#061884" d="M 113 180 C 111.35499 180 110 181.35499 110 183 L 110 191.14258 A 80 80 0 0 0 112 192.45898 L 112 183 C 112 182.43587 112.43587 182 113 182 L 149.58594 182 L 165.58594 198 L 187.72461 198 A 80 80 0 0 0 191.86328 196 L 166.41406 196 L 150.41406 180 L 113 180 z " />
|
||||
<path fill="#061884" d="m 149.18,133.69 v -3.48 a 14.36,14.36 0 0 1 1.74,-7.25 20.17,20.17 0 0 1 6.4,-6.17 25.87,25.87 0 0 0 5.68,-4.79 7,7 0 0 0 1.48,-4.34 4.13,4.13 0 0 0 -1.93,-3.62 9,9 0 0 0 -5.14,-1.3 24.94,24.94 0 0 0 -7.34,1.16 45.2,45.2 0 0 0 -7.78,3.31 l -5.37,-10.64 a 48.41,48.41 0 0 1 9.89,-4.21 40.25,40.25 0 0 1 11.67,-1.61 q 9.57,0 14.9,4.43 a 14.16,14.16 0 0 1 5.32,11.41 15.41,15.41 0 0 1 -2.55,9 30.38,30.38 0 0 1 -7.92,7.34 32.11,32.11 0 0 0 -5.23,4.37 5.91,5.91 0 0 0 -1.34,4 v 2.41 z m -1.61,15.12 q 0,-4.38 2.46,-6.12 a 10,10 0 0 1 5.95,-1.75 9.69,9.69 0 0 1 5.77,1.75 q 2.46,1.74 2.46,6.12 0,4.22 -2.46,6 a 9.42,9.42 0 0 1 -5.77,1.84 9.69,9.69 0 0 1 -5.95,-1.84 q -2.46,-1.81 -2.46,-6 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 9.6 KiB |
9
plugins/DigitalLibrary/resources/images/update.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
|
||||
<path d="M12,19.6L4.4,12L7,9.4V12h2V6H3v2h2.6L3.7,9.9c-1.2,1.2-1.2,3.1,0,4.2l6.2,6.2c1.2,1.2,3.1,1.2,4.2,0l0.6-0.6l-1.4-1.4
|
||||
L12,19.6z"/>
|
||||
<path d="M20.3,9.9l-6.2-6.2c-1.2-1.2-3.1-1.2-4.2,0L9.3,4.3l1.4,1.4L12,4.4l7.6,7.6L17,14.6V12h-2v6h6v-2h-2.6l1.9-1.9
|
||||
C21.5,12.9,21.5,11.1,20.3,9.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 646 B |
159
plugins/DigitalLibrary/resources/qml/CreateNewProjectPopup.qml
Normal file
|
@ -0,0 +1,159 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
|
||||
Popup
|
||||
{
|
||||
id: base
|
||||
|
||||
padding: UM.Theme.getSize("default_margin").width
|
||||
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
focus: true
|
||||
modal: true
|
||||
background: Cura.RoundedRectangle
|
||||
{
|
||||
cornerSide: Cura.RoundedRectangle.Direction.All
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
color: UM.Theme.getColor("main_background")
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager
|
||||
|
||||
function onCreatingNewProjectStatusChanged(status)
|
||||
{
|
||||
if (status == DF.RetrievalStatus.Success)
|
||||
{
|
||||
base.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened:
|
||||
{
|
||||
newProjectNameTextField.text = ""
|
||||
newProjectNameTextField.focus = true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: createNewLibraryProjectLabel
|
||||
text: "Create new Library project"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: projectNameLabel
|
||||
text: "Project Name"
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
anchors
|
||||
{
|
||||
top: createNewLibraryProjectLabel.bottom
|
||||
topMargin: UM.Theme.getSize("default_margin").width
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Cura.TextField
|
||||
{
|
||||
id: newProjectNameTextField
|
||||
width: parent.width
|
||||
anchors
|
||||
{
|
||||
top: projectNameLabel.bottom
|
||||
topMargin: UM.Theme.getSize("thin_margin").width
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
validator: RegExpValidator
|
||||
{
|
||||
regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/
|
||||
}
|
||||
|
||||
text: PrintInformation.jobName
|
||||
font: UM.Theme.getFont("default")
|
||||
placeholderText: "Enter a name for your new project."
|
||||
onAccepted:
|
||||
{
|
||||
if (verifyProjectCreationButton.enabled)
|
||||
{
|
||||
verifyProjectCreationButton.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: errorWhileCreatingProjectLabel
|
||||
text: manager.projectCreationErrorText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("error")
|
||||
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
|
||||
anchors
|
||||
{
|
||||
top: newProjectNameTextField.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: cancelProjectCreationButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
text: "Cancel"
|
||||
|
||||
onClicked:
|
||||
{
|
||||
base.close()
|
||||
}
|
||||
busy: false
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: verifyProjectCreationButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
text: "Create"
|
||||
enabled: newProjectNameTextField.text.length >= 2 && !busy
|
||||
|
||||
onClicked:
|
||||
{
|
||||
manager.createLibraryProjectAndSetAsPreselected(newProjectNameTextField.text)
|
||||
}
|
||||
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
Window
|
||||
{
|
||||
id: digitalFactoryOpenDialogBase
|
||||
title: "Open file from Library"
|
||||
|
||||
modality: Qt.ApplicationModal
|
||||
width: 800 * screenScaleFactor
|
||||
height: 600 * screenScaleFactor
|
||||
minimumWidth: 800 * screenScaleFactor
|
||||
minimumHeight: 600 * screenScaleFactor
|
||||
|
||||
Shortcut
|
||||
{
|
||||
sequence: "Esc"
|
||||
onActivated: digitalFactoryOpenDialogBase.close()
|
||||
}
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
SelectProjectPage
|
||||
{
|
||||
visible: manager.selectedProjectIndex == -1
|
||||
createNewProjectButtonVisible: false
|
||||
}
|
||||
|
||||
OpenProjectFilesPage
|
||||
{
|
||||
visible: manager.selectedProjectIndex >= 0
|
||||
onOpenFilePressed: digitalFactoryOpenDialogBase.close()
|
||||
}
|
||||
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
// Shows up while Cura is waiting to receive the user's projects from the digital factory library
|
||||
id: retrievingProjectsBusyIndicator
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: parent.width / 4
|
||||
height: width
|
||||
visible: manager.retrievingProjectsStatus == DF.RetrievalStatus.InProgress
|
||||
running: visible
|
||||
palette.dark: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
Window
|
||||
{
|
||||
id: digitalFactorySaveDialogBase
|
||||
title: "Save Cura project to Library"
|
||||
|
||||
modality: Qt.ApplicationModal
|
||||
width: 800 * screenScaleFactor
|
||||
height: 600 * screenScaleFactor
|
||||
minimumWidth: 800 * screenScaleFactor
|
||||
minimumHeight: 600 * screenScaleFactor
|
||||
|
||||
Shortcut
|
||||
{
|
||||
sequence: "Esc"
|
||||
onActivated: digitalFactorySaveDialogBase.close()
|
||||
}
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
SelectProjectPage
|
||||
{
|
||||
visible: manager.selectedProjectIndex == -1
|
||||
createNewProjectButtonVisible: true
|
||||
}
|
||||
|
||||
SaveProjectFilesPage
|
||||
{
|
||||
visible: manager.selectedProjectIndex >= 0
|
||||
onSavePressed: digitalFactorySaveDialogBase.close()
|
||||
onSelectDifferentProjectPressed: manager.clearProjectSelection()
|
||||
}
|
||||
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
// Shows up while Cura is waiting to receive the user's projects from the digital factory library
|
||||
id: retrievingProjectsBusyIndicator
|
||||
|
||||
anchors {
|
||||
verticalCenter: parent.verticalCenter
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: parent.width / 4
|
||||
height: width
|
||||
visible: manager.retrievingProjectsStatus == DF.RetrievalStatus.InProgress
|
||||
running: visible
|
||||
palette.dark: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
129
plugins/DigitalLibrary/resources/qml/LoadMoreProjectsCard.qml
Normal file
|
@ -0,0 +1,129 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
Cura.RoundedRectangle
|
||||
{
|
||||
id: base
|
||||
cornerSide: Cura.RoundedRectangle.Direction.All
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
signal clicked()
|
||||
property var hasMoreProjectsToLoad
|
||||
enabled: hasMoreProjectsToLoad
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
MouseArea
|
||||
{
|
||||
id: cardMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
id: projectInformationRow
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: projectImage
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: UM.Theme.getSize("section").height
|
||||
height: width
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: displayNameLabel
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Load more projects"
|
||||
color: UM.Theme.getColor("text_link")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
cardMouseArea.clicked.connect(base.clicked)
|
||||
}
|
||||
|
||||
states:
|
||||
[
|
||||
State
|
||||
{
|
||||
name: "canLoadMoreProjectsAndHovered";
|
||||
when: base.hasMoreProjectsToLoad && cardMouseArea.containsMouse
|
||||
PropertyChanges
|
||||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: displayNameLabel
|
||||
color: UM.Theme.getColor("text_link")
|
||||
text: "Load more projects"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: base
|
||||
color: UM.Theme.getColor("action_button_hovered")
|
||||
}
|
||||
},
|
||||
|
||||
State
|
||||
{
|
||||
name: "canLoadMoreProjectsAndNotHovered";
|
||||
when: base.hasMoreProjectsToLoad && !cardMouseArea.containsMouse
|
||||
PropertyChanges
|
||||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: displayNameLabel
|
||||
color: UM.Theme.getColor("text_link")
|
||||
text: "Load more projects"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: base
|
||||
color: UM.Theme.getColor("main_background")
|
||||
}
|
||||
},
|
||||
|
||||
State
|
||||
{
|
||||
name: "noMoreProjectsToLoad"
|
||||
when: !base.hasMoreProjectsToLoad
|
||||
PropertyChanges
|
||||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("action_button_disabled_text")
|
||||
source: "../images/update.svg"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: displayNameLabel
|
||||
color: UM.Theme.getColor("action_button_disabled_text")
|
||||
text: "No more projects to load"
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
target: base
|
||||
color: UM.Theme.getColor("action_button_disabled")
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
203
plugins/DigitalLibrary/resources/qml/OpenProjectFilesPage.qml
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
property var fileModel: manager.digitalFactoryFileModel
|
||||
|
||||
signal openFilePressed()
|
||||
signal selectDifferentProjectPressed()
|
||||
|
||||
anchors
|
||||
{
|
||||
fill: parent
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
ProjectSummaryCard
|
||||
{
|
||||
id: projectSummaryCard
|
||||
|
||||
anchors.top: parent.top
|
||||
|
||||
property var selectedItem: manager.digitalFactoryProjectModel.getItem(manager.selectedProjectIndex)
|
||||
|
||||
imageSource: selectedItem.thumbnailUrl || "../images/placeholder.svg"
|
||||
projectNameText: selectedItem.displayName || ""
|
||||
projectUsernameText: selectedItem.username || ""
|
||||
projectLastUpdatedText: "Last updated: " + selectedItem.lastUpdated
|
||||
cardMouseAreaEnabled: false
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: projectFilesContent
|
||||
width: parent.width
|
||||
anchors.top: projectSummaryCard.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.bottom: selectDifferentProjectButton.top
|
||||
anchors.bottomMargin: UM.Theme.getSize("default_margin").width
|
||||
|
||||
color: UM.Theme.getColor("main_background")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
|
||||
|
||||
Cura.TableView
|
||||
{
|
||||
id: filesTableView
|
||||
anchors.fill: parent
|
||||
model: manager.digitalFactoryFileModel
|
||||
visible: model.count != 0 && manager.retrievingFileStatus != DF.RetrievalStatus.InProgress
|
||||
selectionMode: OldControls.SelectionMode.SingleSelection
|
||||
onDoubleClicked:
|
||||
{
|
||||
manager.setSelectedFileIndices([row]);
|
||||
openFilesButton.clicked();
|
||||
}
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
id: fileNameColumn
|
||||
role: "fileName"
|
||||
title: "Name"
|
||||
width: Math.round(filesTableView.width / 3)
|
||||
}
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
id: usernameColumn
|
||||
role: "username"
|
||||
title: "Uploaded by"
|
||||
width: Math.round(filesTableView.width / 3)
|
||||
}
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
role: "uploadedAt"
|
||||
title: "Uploaded at"
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: filesTableView.selection
|
||||
function onSelectionChanged()
|
||||
{
|
||||
let newSelection = [];
|
||||
filesTableView.selection.forEach(function(rowIndex) { newSelection.push(rowIndex); });
|
||||
manager.setSelectedFileIndices(newSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager
|
||||
function onSelectedProjectIndexChanged(newProjectIndex)
|
||||
{
|
||||
emptyProjectLabel.visible = (newProjectIndex == -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
// Shows up while Cura is waiting to receive the files of a project from the digital factory library
|
||||
id: retrievingFilesBusyIndicator
|
||||
|
||||
anchors
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: parent.width / 4
|
||||
height: width
|
||||
visible: manager.retrievingFilesStatus == DF.RetrievalStatus.InProgress
|
||||
running: visible
|
||||
palette.dark: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager.digitalFactoryFileModel
|
||||
|
||||
function onItemsChanged()
|
||||
{
|
||||
// Make sure no files are selected when the file model changes
|
||||
filesTableView.currentRow = -1
|
||||
filesTableView.selection.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: selectDifferentProjectButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
text: "Change Library project"
|
||||
|
||||
onClicked:
|
||||
{
|
||||
manager.clearProjectSelection()
|
||||
}
|
||||
busy: false
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: openFilesButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
text: "Open"
|
||||
enabled: filesTableView.selection.count > 0
|
||||
onClicked:
|
||||
{
|
||||
manager.openSelectedFiles()
|
||||
}
|
||||
busy: false
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
openFilesButton.clicked.connect(base.openFilePressed)
|
||||
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
|
||||
}
|
||||
}
|
92
plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
Cura.RoundedRectangle
|
||||
{
|
||||
id: base
|
||||
width: parent.width
|
||||
height: projectImage.height + 2 * UM.Theme.getSize("default_margin").width
|
||||
cornerSide: Cura.RoundedRectangle.Direction.All
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
color: UM.Theme.getColor("main_background")
|
||||
signal clicked()
|
||||
property alias imageSource: projectImage.source
|
||||
property alias projectNameText: displayNameLabel.text
|
||||
property alias projectUsernameText: usernameLabel.text
|
||||
property alias projectLastUpdatedText: lastUpdatedLabel.text
|
||||
property alias cardMouseAreaEnabled: cardMouseArea.enabled
|
||||
|
||||
onVisibleChanged: color = UM.Theme.getColor("main_background")
|
||||
|
||||
MouseArea
|
||||
{
|
||||
id: cardMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: base.color = UM.Theme.getColor("action_button_hovered")
|
||||
onExited: base.color = UM.Theme.getColor("main_background")
|
||||
onClicked: base.clicked()
|
||||
}
|
||||
Row
|
||||
{
|
||||
id: projectInformationRow
|
||||
width: parent.width
|
||||
padding: UM.Theme.getSize("default_margin").width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Image
|
||||
{
|
||||
id: projectImage
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: UM.Theme.getSize("toolbox_thumbnail_small").width
|
||||
height: Math.round(width * 3/4)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
}
|
||||
Column
|
||||
{
|
||||
id: projectLabelsColumn
|
||||
height: projectImage.height
|
||||
width: parent.width - x - UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Label
|
||||
{
|
||||
id: displayNameLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: usernameLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: lastUpdatedLabel
|
||||
width: parent.width
|
||||
height: Math.round(parent.height / 3)
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
259
plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml
Normal file
|
@ -0,0 +1,259 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
property var fileModel: manager.digitalFactoryFileModel
|
||||
|
||||
signal savePressed()
|
||||
signal selectDifferentProjectPressed()
|
||||
|
||||
anchors
|
||||
{
|
||||
fill: parent
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
ProjectSummaryCard
|
||||
{
|
||||
id: projectSummaryCard
|
||||
|
||||
anchors.top: parent.top
|
||||
|
||||
property var selectedItem: manager.digitalFactoryProjectModel.getItem(manager.selectedProjectIndex)
|
||||
|
||||
imageSource: selectedItem.thumbnailUrl || "../images/placeholder.svg"
|
||||
projectNameText: selectedItem.displayName || ""
|
||||
projectUsernameText: selectedItem.username || ""
|
||||
projectLastUpdatedText: "Last updated: " + selectedItem.lastUpdated
|
||||
cardMouseAreaEnabled: false
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: fileNameLabel
|
||||
anchors.top: projectSummaryCard.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
text: "Cura project name"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
|
||||
Cura.TextField
|
||||
{
|
||||
id: dfFilenameTextfield
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
anchors.top: fileNameLabel.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("thin_margin").height
|
||||
validator: RegExpValidator
|
||||
{
|
||||
regExp: /^[\w\-\. ()]{0,255}$/
|
||||
}
|
||||
|
||||
text: PrintInformation.jobName
|
||||
font: UM.Theme.getFont("medium")
|
||||
placeholderText: "Enter the name of the file."
|
||||
onAccepted: { if (saveButton.enabled) {saveButton.clicked()}}
|
||||
}
|
||||
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: projectFilesContent
|
||||
width: parent.width
|
||||
anchors.top: dfFilenameTextfield.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("wide_margin").height
|
||||
anchors.bottom: selectDifferentProjectButton.top
|
||||
anchors.bottomMargin: UM.Theme.getSize("default_margin").width
|
||||
|
||||
color: UM.Theme.getColor("main_background")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
|
||||
|
||||
Cura.TableView
|
||||
{
|
||||
id: filesTableView
|
||||
anchors.fill: parent
|
||||
model: manager.digitalFactoryFileModel
|
||||
visible: model.count != 0 && manager.retrievingFileStatus != DF.RetrievalStatus.InProgress
|
||||
selectionMode: OldControls.SelectionMode.NoSelection
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
id: fileNameColumn
|
||||
role: "fileName"
|
||||
title: "@tableViewColumn:title", "Name"
|
||||
width: Math.round(filesTableView.width / 3)
|
||||
}
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
id: usernameColumn
|
||||
role: "username"
|
||||
title: "Uploaded by"
|
||||
width: Math.round(filesTableView.width / 3)
|
||||
}
|
||||
|
||||
OldControls.TableViewColumn
|
||||
{
|
||||
role: "uploadedAt"
|
||||
title: "Uploaded at"
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager
|
||||
function onSelectedProjectIndexChanged()
|
||||
{
|
||||
emptyProjectLabel.visible = (manager.newProjectIndex == -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
// Shows up while Cura is waiting to receive the files of a project from the digital factory library
|
||||
id: retrievingFilesBusyIndicator
|
||||
|
||||
anchors
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: parent.width / 4
|
||||
height: width
|
||||
visible: manager.retrievingFilesStatus == DF.RetrievalStatus.InProgress
|
||||
running: visible
|
||||
palette.dark: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: manager.digitalFactoryFileModel
|
||||
|
||||
function onItemsChanged()
|
||||
{
|
||||
// Make sure no files are selected when the file model changes
|
||||
filesTableView.currentRow = -1
|
||||
filesTableView.selection.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: selectDifferentProjectButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
text: "Change Library project"
|
||||
|
||||
onClicked:
|
||||
{
|
||||
manager.selectedProjectIndex = -1
|
||||
}
|
||||
busy: false
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: saveButton
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
text: "Save"
|
||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1
|
||||
|
||||
onClicked:
|
||||
{
|
||||
let saveAsFormats = [];
|
||||
if (asProjectCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("3mf");
|
||||
}
|
||||
if (asSlicedCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("ufp");
|
||||
}
|
||||
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
|
||||
}
|
||||
busy: false
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
|
||||
id: saveAsFormatRow
|
||||
anchors.verticalCenter: saveButton.verticalCenter
|
||||
anchors.right: saveButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
||||
width: childrenRect.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Cura.CheckBox
|
||||
{
|
||||
id: asProjectCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: true
|
||||
text: "Save Cura project"
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
|
||||
Cura.CheckBox
|
||||
{
|
||||
id: asSlicedCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
enabled: UM.Backend.state == UM.Backend.Done
|
||||
checked: UM.Backend.state == UM.Backend.Done
|
||||
text: "Save print file"
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
{
|
||||
saveButton.clicked.connect(base.savePressed)
|
||||
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
|
||||
}
|
||||
}
|
231
plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml
Normal file
|
@ -0,0 +1,231 @@
|
|||
// Copyright (C) 2021 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtQuick.Layouts 1.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
property bool createNewProjectButtonVisible: true
|
||||
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: headerRow
|
||||
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Cura.TextField
|
||||
{
|
||||
id: searchBar
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: createNewProjectButton.height
|
||||
|
||||
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
|
||||
|
||||
leftIcon: UM.Theme.getIcon("Magnifier")
|
||||
placeholderText: "Search"
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: createNewProjectButton
|
||||
|
||||
text: "New Library project"
|
||||
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
|
||||
|
||||
onClicked:
|
||||
{
|
||||
createNewProjectPopup.open()
|
||||
}
|
||||
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
|
||||
}
|
||||
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: upgradePlanButton
|
||||
|
||||
text: "Upgrade plan"
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
|
||||
tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects."
|
||||
tooltipWidth: parent.width * 0.5
|
||||
|
||||
onClicked: Qt.openUrlExternally("https://ultimaker.com/software/ultimaker-essentials/sign-up-cura?utm_source=cura&utm_medium=software&utm_campaign=lib-max")
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: noLibraryProjectsContainer
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
visible: manager.digitalFactoryProjectModel.count == 0 && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
|
||||
|
||||
Column
|
||||
{
|
||||
anchors.centerIn: parent
|
||||
spacing: UM.Theme.getSize("thin_margin").height
|
||||
Image
|
||||
{
|
||||
id: digitalFactoryImage
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: noLibraryProjectsLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: visitDigitalLibraryButton
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Visit Digital Library"
|
||||
onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library")
|
||||
visible: searchBar.text === "" //Show the link to Digital Library when there are no projects in the user's Library.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: projectListContainer
|
||||
anchors
|
||||
{
|
||||
top: headerRow.bottom
|
||||
topMargin: UM.Theme.getSize("default_margin").height
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
visible: manager.digitalFactoryProjectModel.count > 0
|
||||
|
||||
// Use a flickable and a column with a repeater instead of a ListView in a ScrollView, because the ScrollView cannot
|
||||
// have additional children (aside from the view inside it), which wouldn't allow us to add the LoadMoreProjectsCard
|
||||
// in it.
|
||||
Flickable
|
||||
{
|
||||
id: flickableView
|
||||
clip: true
|
||||
contentWidth: parent.width
|
||||
contentHeight: projectsListView.implicitHeight
|
||||
anchors.fill: parent
|
||||
|
||||
ScrollBar.vertical: ScrollBar
|
||||
{
|
||||
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
|
||||
id: verticalScrollBar
|
||||
visible: flickableView.contentHeight > flickableView.height
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
color: UM.Theme.getColor("scrollbar_background")
|
||||
}
|
||||
|
||||
contentItem: Rectangle
|
||||
{
|
||||
id: scrollViewHandle
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
|
||||
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
id: projectsListView
|
||||
width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
|
||||
anchors.top: parent.top
|
||||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Repeater
|
||||
{
|
||||
model: manager.digitalFactoryProjectModel
|
||||
delegate: ProjectSummaryCard
|
||||
{
|
||||
id: projectSummaryCard
|
||||
imageSource: model.thumbnailUrl || "../images/placeholder.svg"
|
||||
projectNameText: model.displayName
|
||||
projectUsernameText: model.username
|
||||
projectLastUpdatedText: "Last updated: " + model.lastUpdated
|
||||
|
||||
onClicked:
|
||||
{
|
||||
manager.selectedProjectIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoadMoreProjectsCard
|
||||
{
|
||||
id: loadMoreProjectsCard
|
||||
height: UM.Theme.getSize("toolbox_thumbnail_small").height
|
||||
width: parent.width
|
||||
visible: manager.digitalFactoryProjectModel.count > 0
|
||||
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad
|
||||
|
||||
onClicked:
|
||||
{
|
||||
manager.loadMoreProjects()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CreateNewProjectPopup
|
||||
{
|
||||
id: createNewProjectPopup
|
||||
width: 400 * screenScaleFactor
|
||||
height: 220 * screenScaleFactor
|
||||
x: Math.round((parent.width - width) / 2)
|
||||
y: Math.round((parent.height - height) / 2)
|
||||
}
|
||||
}
|
74
plugins/DigitalLibrary/src/BaseModel.py
Normal file
|
@ -0,0 +1,74 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import TypeVar, Dict, List, Any, Type, Union
|
||||
|
||||
|
||||
# Type variable used in the parse methods below, which should be a subclass of BaseModel.
|
||||
T = TypeVar("T", bound="BaseModel")
|
||||
|
||||
|
||||
class BaseModel:
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
self.validate()
|
||||
|
||||
# Validates the model, raising an exception if the model is invalid.
|
||||
def validate(self) -> None:
|
||||
pass
|
||||
|
||||
def __eq__(self, other):
|
||||
"""Checks whether the two models are equal.
|
||||
|
||||
:param other: The other model.
|
||||
:return: True if they are equal, False if they are different.
|
||||
"""
|
||||
return type(self) == type(other) and self.toDict() == other.toDict()
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
"""Checks whether the two models are different.
|
||||
|
||||
:param other: The other model.
|
||||
:return: True if they are different, False if they are the same.
|
||||
"""
|
||||
return type(self) != type(other) or self.toDict() != other.toDict()
|
||||
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
"""Converts the model into a serializable dictionary"""
|
||||
|
||||
return self.__dict__
|
||||
|
||||
@staticmethod
|
||||
def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T:
|
||||
"""Parses a single model.
|
||||
|
||||
:param model_class: The model class.
|
||||
:param values: The value of the model, which is usually a dictionary, but may also be already parsed.
|
||||
:return: An instance of the model_class given.
|
||||
"""
|
||||
if isinstance(values, dict):
|
||||
return model_class(**values)
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]:
|
||||
"""Parses a list of models.
|
||||
|
||||
:param model_class: The model class.
|
||||
:param values: The value of the list. Each value is usually a dictionary, but may also be already parsed.
|
||||
:return: A list of instances of the model_class given.
|
||||
"""
|
||||
return [cls.parseModel(model_class, value) for value in values]
|
||||
|
||||
@staticmethod
|
||||
def parseDate(date: Union[str, datetime]) -> datetime:
|
||||
"""Parses the given date string.
|
||||
|
||||
:param date: The date to parse.
|
||||
:return: The parsed date.
|
||||
"""
|
||||
if isinstance(date, datetime):
|
||||
return date
|
||||
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
|
31
plugins/DigitalLibrary/src/CloudError.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Dict, Optional, Any
|
||||
|
||||
from .BaseModel import BaseModel
|
||||
|
||||
|
||||
class CloudError(BaseModel):
|
||||
"""Class representing errors generated by the servers, according to the JSON-API standard."""
|
||||
|
||||
def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None,
|
||||
meta: Optional[Dict[str, Any]] = None, **kwargs) -> None:
|
||||
"""Creates a new error object.
|
||||
|
||||
:param id: Unique identifier for this particular occurrence of the problem.
|
||||
:param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence
|
||||
of the problem, except for purposes of localization.
|
||||
:param code: An application-specific error code, expressed as a string value.
|
||||
:param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's
|
||||
value can be localized.
|
||||
:param http_status: The HTTP status code applicable to this problem, converted to string.
|
||||
:param meta: Non-standard meta-information about the error, depending on the error code.
|
||||
"""
|
||||
|
||||
self.id = id
|
||||
self.code = code
|
||||
self.http_status = http_status
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.meta = meta
|
||||
super().__init__(**kwargs)
|