mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-11 16:00:47 -07:00
Merge branch 'master' into refactor_singleton_settingsbase
This commit is contained in:
commit
7d7227cfc8
113 changed files with 22707 additions and 15197 deletions
|
|
@ -1,3 +1,103 @@
|
|||
[3.5.0]
|
||||
*Monitor page
|
||||
The monitor page of Ultimaker Cura has been remodeled for better consistency with the Cura Connect ‘Print jobs’ interface. This means less switching between interfaces, and more control from within Ultimaker Cura.
|
||||
|
||||
*Open recent projects
|
||||
Project files can now be found in the ‘Open Recent’ menu.
|
||||
|
||||
*New tool hotkeys
|
||||
New hotkeys have been assigned for quick toggling between the translate (T), scale (S), rotate (R) and mirror (M) tools.
|
||||
|
||||
*Project files use 3MF only
|
||||
A 3MF extension is now used for project files. The ‘.curaproject’ extension is no longer used.
|
||||
|
||||
*Camera maximum zoom
|
||||
The maximum zoom has been adjusted to scale with the size of the selected printer. This fixes third-party printers with huge build volumes to be correctly visible.
|
||||
|
||||
*Corrected width of layer number box
|
||||
The layer number indicator in the layer view now displays numbers above 999 correctly.
|
||||
|
||||
*Materials preferences
|
||||
This screen has been redesigned to improve user experience. Materials can now be set as a favorites, so they can be easily accessed in the material selection panel at the top-right of the screen.
|
||||
|
||||
*Installed packages checkmark
|
||||
Packages that are already installed in the Toolbox are now have a checkmark for easy reference.
|
||||
|
||||
*Mac OSX save dialog
|
||||
The save dialog has been restored to its native behavior and bugs have been fixed.
|
||||
|
||||
*Removed .gz extension
|
||||
Saving compressed g-code files from the save dialog has been removed because of incompatibility with MacOS. If sending jobs over Wi-Fi, g-code is still compressed.
|
||||
|
||||
*Updates to Chinese translations
|
||||
Improved and updated Chinese translations. Contributed by MarmaladeForMeat.
|
||||
|
||||
*Save project
|
||||
Saving the project no longer triggers the project to reslice.
|
||||
|
||||
*File menu
|
||||
The Save option in the file menu now saves project files. The export option now saves other types of files, such as STL.
|
||||
|
||||
*Improved processing of overhang walls
|
||||
Overhang walls are detected and printed with different speeds. It will not start a perimeter on an overhanging wall. The quality of overhanging walls may be improved by printing those at a different speed. Contributed by smartavionics.
|
||||
|
||||
*Prime tower reliability
|
||||
The prime tower has been improved for better reliability. This is especially useful when printing with two materials that do not adhere well.
|
||||
|
||||
*Support infill line direction
|
||||
The support infill lines can now be rotated to increase the supporting capabilities and reduce artifacts on the model. This setting rotates existing patterns, like triangle support infill. Contributed by fieldOfView.
|
||||
|
||||
*Minimum polygon circumference
|
||||
Polygons in sliced layers that have a circumference smaller than the setting value will be filtered out. Lower values lead to higher resolution meshes at the cost of increased slicing time. This setting is ideal for very tiny prints with a lot of detail, or for SLA printers. Contributed by cubiq.
|
||||
|
||||
*Initial layer support line distance
|
||||
This setting enables the user to reduce or increase the density of the support initial layer in order to increase or reduce adhesion to the build plate and the overall strength.
|
||||
|
||||
*Extra infill wall line count
|
||||
Adds extra walls around infill. Contributed by BagelOrb.
|
||||
|
||||
*Multiply infill
|
||||
Creates multiple infill lines on the same pattern for sturdier infill. Contributed by BagelOrb.
|
||||
|
||||
*Connected infill polygons
|
||||
Connecting infill lines now also works with concentric and cross infill patterns. The benefit would be stronger infill and more consistent material flow/saving retractions. Contributed by BagelOrb.
|
||||
|
||||
*Fan speed override
|
||||
New setting to modify the fan speed of supported areas. This setting can be found in Support settings > Fan Speed Override when support is enabled. Contributed by smartavionics.
|
||||
|
||||
*Minimum wall flow
|
||||
New setting to define a minimum flow for thin printed walls. Contributed by smartavionics.
|
||||
|
||||
*Custom support plugin
|
||||
A tool downloadable from the toolbox, similar to the support blocker, that adds cubes of support to the model manually by clicking parts of it. Contributed by Lokster.
|
||||
|
||||
*Quickly toggle autoslicing
|
||||
Adds a pause/play button to the progress bar to quickly toggle autoslicing. Contributed by fieldOfview.
|
||||
|
||||
*Cura-DuetRRFPlugin
|
||||
Adds output devices for a Duet RepRapFirmware printer: "Print", "Simulate", and "Upload". Contributed by Kriechi.
|
||||
|
||||
*Dremel 3D20
|
||||
This plugin adds the Dremel printer to Ultimaker Cura. Contributed by Kriechi.
|
||||
|
||||
*Bug fixes
|
||||
- Removed extra M109 commands. Older versions would generate superfluous M109 commands. This has been fixed for better temperature stability when printing.
|
||||
- Fixed minor mesh handling bugs. A few combinations of modifier meshes now lead to expected behavior.
|
||||
- Removed unnecessary travels. Connected infill lines are now always printed completely connected, without unnecessary travel moves.
|
||||
- Removed concentric 3D infill. This infill type has been removed due to lack of reliability.
|
||||
- Extra skin wall count. Fixed an issue that caused extra print moves with this setting enabled.
|
||||
- Concentric skin. Small gaps in concentric skin are now filled correctly.
|
||||
- Order of printed models. The order of a large batch of printed models is now more consistent, instead of random.
|
||||
|
||||
*Third party printers
|
||||
- TiZYX
|
||||
- Winbo
|
||||
- Tevo Tornado
|
||||
- Creality CR-10S
|
||||
- Wanhao Duplicator
|
||||
- Deltacomb (update)
|
||||
- Dacoma (update)
|
||||
|
||||
[3.4.1]
|
||||
*Bug fixes
|
||||
- Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time.
|
||||
|
|
|
|||
|
|
@ -891,7 +891,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: machineHeadPolygonProvider
|
||||
|
||||
containerStackId: base.acthiveMachineId
|
||||
containerStackId: base.activeMachineId
|
||||
key: "machine_head_with_fans_polygon"
|
||||
watchedProperties: [ "value" ]
|
||||
storeIndex: manager.containerIndex
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
from ..Script import Script
|
||||
|
||||
class PauseAtHeightRepRapFirmwareDuet(Script):
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name": "Pause at height for RepRapFirmware DuetWifi / Duet Ethernet / Duet Maestro",
|
||||
"key": "PauseAtHeightRepRapFirmwareDuet",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"pause_height":
|
||||
{
|
||||
"label": "Pause height",
|
||||
"description": "At what height should the pause occur",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 5.0
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
current_z = 0.
|
||||
pause_z = self.getSettingValueByKey("pause_height")
|
||||
|
||||
layers_started = False
|
||||
for layer_number, layer in enumerate(data):
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if ";LAYER:0" in line:
|
||||
layers_started = True
|
||||
continue
|
||||
|
||||
if not layers_started:
|
||||
continue
|
||||
|
||||
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
|
||||
current_z = self.getValue(line, 'Z')
|
||||
if current_z != None:
|
||||
if current_z >= pause_z:
|
||||
prepend_gcode = ";TYPE:CUSTOM\n"
|
||||
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
|
||||
prepend_gcode += self.putValue(M = 226) + "\n"
|
||||
layer = prepend_gcode + layer
|
||||
|
||||
data[layer_number] = layer # Override the data of this layer with the modified data
|
||||
return data
|
||||
break
|
||||
return data
|
||||
|
|
@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
|
|||
import UM 1.0 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Item {
|
||||
Item
|
||||
{
|
||||
id: sliderRoot
|
||||
|
||||
// handle properties
|
||||
|
|
@ -39,40 +40,49 @@ Item {
|
|||
property real lowerValue: minimumValue
|
||||
|
||||
property bool layersVisible: true
|
||||
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
|
||||
|
||||
function getUpperValueFromSliderHandle() {
|
||||
function getUpperValueFromSliderHandle()
|
||||
{
|
||||
return upperHandle.getValue()
|
||||
}
|
||||
|
||||
function setUpperValue(value) {
|
||||
function setUpperValue(value)
|
||||
{
|
||||
upperHandle.setValue(value)
|
||||
updateRangeHandle()
|
||||
}
|
||||
|
||||
function getLowerValueFromSliderHandle() {
|
||||
function getLowerValueFromSliderHandle()
|
||||
{
|
||||
return lowerHandle.getValue()
|
||||
}
|
||||
|
||||
function setLowerValue(value) {
|
||||
function setLowerValue(value)
|
||||
{
|
||||
lowerHandle.setValue(value)
|
||||
updateRangeHandle()
|
||||
}
|
||||
|
||||
function updateRangeHandle() {
|
||||
function updateRangeHandle()
|
||||
{
|
||||
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
|
||||
}
|
||||
|
||||
// set the active handle to show only one label at a time
|
||||
function setActiveHandle(handle) {
|
||||
function setActiveHandle(handle)
|
||||
{
|
||||
activeHandle = handle
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
function normalizeValue(value)
|
||||
{
|
||||
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
|
||||
}
|
||||
|
||||
// slider track
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
id: track
|
||||
|
||||
width: sliderRoot.trackThickness
|
||||
|
|
@ -86,7 +96,8 @@ Item {
|
|||
}
|
||||
|
||||
// Range handle
|
||||
Item {
|
||||
Item
|
||||
{
|
||||
id: rangeHandle
|
||||
|
||||
y: upperHandle.y + upperHandle.height
|
||||
|
|
@ -96,7 +107,9 @@ Item {
|
|||
visible: sliderRoot.layersVisible
|
||||
|
||||
// set the new value when dragging
|
||||
function onHandleDragged () {
|
||||
function onHandleDragged()
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
|
||||
upperHandle.y = y - upperHandle.height
|
||||
lowerHandle.y = y + height
|
||||
|
|
@ -109,7 +122,14 @@ Item {
|
|||
UM.SimulationView.setMinimumLayer(lowerValue)
|
||||
}
|
||||
|
||||
function setValue (value) {
|
||||
function setValueManually(value)
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
upperHandle.setValue(value)
|
||||
}
|
||||
|
||||
function setValue(value)
|
||||
{
|
||||
var range = sliderRoot.upperValue - sliderRoot.lowerValue
|
||||
value = Math.min(value, sliderRoot.maximumValue)
|
||||
value = Math.max(value, sliderRoot.minimumValue + range)
|
||||
|
|
@ -118,17 +138,20 @@ Item {
|
|||
UM.SimulationView.setMinimumLayer(value - range)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
|
||||
height: parent.height + sliderRoot.handleSize
|
||||
anchors.centerIn: parent
|
||||
color: sliderRoot.rangeHandleColor
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
drag {
|
||||
drag
|
||||
{
|
||||
target: parent
|
||||
axis: Drag.YAxis
|
||||
minimumY: upperHandle.height
|
||||
|
|
@ -139,7 +162,8 @@ Item {
|
|||
onPressed: sliderRoot.setActiveHandle(rangeHandle)
|
||||
}
|
||||
|
||||
SimulationSliderLabel {
|
||||
SimulationSliderLabel
|
||||
{
|
||||
id: rangleHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
|
|
@ -152,12 +176,13 @@ Item {
|
|||
maximumValue: sliderRoot.maximumValue
|
||||
value: sliderRoot.upperValue
|
||||
busy: UM.SimulationView.busy
|
||||
setValue: rangeHandle.setValue // connect callback functions
|
||||
setValue: rangeHandle.setValueManually // connect callback functions
|
||||
}
|
||||
}
|
||||
|
||||
// Upper handle
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
id: upperHandle
|
||||
|
||||
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
|
||||
|
|
@ -168,10 +193,13 @@ Item {
|
|||
color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
|
||||
visible: sliderRoot.layersVisible
|
||||
|
||||
function onHandleDragged () {
|
||||
function onHandleDragged()
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
|
||||
// don't allow the lower handle to be heigher than the upper handle
|
||||
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
|
||||
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize)
|
||||
{
|
||||
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
|
||||
}
|
||||
|
||||
|
|
@ -183,15 +211,23 @@ Item {
|
|||
}
|
||||
|
||||
// get the upper value based on the slider position
|
||||
function getValue () {
|
||||
function getValue()
|
||||
{
|
||||
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
|
||||
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
|
||||
result = sliderRoot.roundValues ? Math.round(result) : result
|
||||
return result
|
||||
}
|
||||
|
||||
function setValueManually(value)
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
upperHandle.setValue(value)
|
||||
}
|
||||
|
||||
// set the slider position based on the upper value
|
||||
function setValue (value) {
|
||||
function setValue(value)
|
||||
{
|
||||
// Normalize values between range, since using arrow keys will create out-of-the-range values
|
||||
value = sliderRoot.normalizeValue(value)
|
||||
|
||||
|
|
@ -209,10 +245,12 @@ Item {
|
|||
Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
|
||||
|
||||
// dragging
|
||||
MouseArea {
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
drag {
|
||||
drag
|
||||
{
|
||||
target: parent
|
||||
axis: Drag.YAxis
|
||||
minimumY: 0
|
||||
|
|
@ -220,13 +258,15 @@ Item {
|
|||
}
|
||||
|
||||
onPositionChanged: parent.onHandleDragged()
|
||||
onPressed: {
|
||||
onPressed:
|
||||
{
|
||||
sliderRoot.setActiveHandle(upperHandle)
|
||||
upperHandleLabel.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
SimulationSliderLabel {
|
||||
SimulationSliderLabel
|
||||
{
|
||||
id: upperHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
|
|
@ -239,12 +279,13 @@ Item {
|
|||
maximumValue: sliderRoot.maximumValue
|
||||
value: sliderRoot.upperValue
|
||||
busy: UM.SimulationView.busy
|
||||
setValue: upperHandle.setValue // connect callback functions
|
||||
setValue: upperHandle.setValueManually // connect callback functions
|
||||
}
|
||||
}
|
||||
|
||||
// Lower handle
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
id: lowerHandle
|
||||
|
||||
y: sliderRoot.height - sliderRoot.handleSize
|
||||
|
|
@ -256,10 +297,13 @@ Item {
|
|||
|
||||
visible: sliderRoot.layersVisible
|
||||
|
||||
function onHandleDragged () {
|
||||
function onHandleDragged()
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
|
||||
// don't allow the upper handle to be lower than the lower handle
|
||||
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
|
||||
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize)
|
||||
{
|
||||
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
|
||||
}
|
||||
|
||||
|
|
@ -271,15 +315,24 @@ Item {
|
|||
}
|
||||
|
||||
// get the lower value from the current slider position
|
||||
function getValue () {
|
||||
function getValue()
|
||||
{
|
||||
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
|
||||
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
|
||||
result = sliderRoot.roundValues ? Math.round(result) : result
|
||||
return result
|
||||
}
|
||||
|
||||
function setValueManually(value)
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
lowerHandle.setValue(value)
|
||||
}
|
||||
|
||||
// set the slider position based on the lower value
|
||||
function setValue (value) {
|
||||
function setValue(value)
|
||||
{
|
||||
|
||||
// Normalize values between range, since using arrow keys will create out-of-the-range values
|
||||
value = sliderRoot.normalizeValue(value)
|
||||
|
||||
|
|
@ -297,10 +350,12 @@ Item {
|
|||
Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
|
||||
|
||||
// dragging
|
||||
MouseArea {
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
drag {
|
||||
drag
|
||||
{
|
||||
target: parent
|
||||
axis: Drag.YAxis
|
||||
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
|
||||
|
|
@ -308,13 +363,15 @@ Item {
|
|||
}
|
||||
|
||||
onPositionChanged: parent.onHandleDragged()
|
||||
onPressed: {
|
||||
onPressed:
|
||||
{
|
||||
sliderRoot.setActiveHandle(lowerHandle)
|
||||
lowerHandleLabel.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
SimulationSliderLabel {
|
||||
SimulationSliderLabel
|
||||
{
|
||||
id: lowerHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
|
|
@ -327,7 +384,7 @@ Item {
|
|||
maximumValue: sliderRoot.maximumValue
|
||||
value: sliderRoot.lowerValue
|
||||
busy: UM.SimulationView.busy
|
||||
setValue: lowerHandle.setValue // connect callback functions
|
||||
setValue: lowerHandle.setValueManually // connect callback functions
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
|
|||
import UM 1.0 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Item {
|
||||
Item
|
||||
{
|
||||
id: sliderRoot
|
||||
|
||||
// handle properties
|
||||
|
|
@ -34,26 +35,32 @@ Item {
|
|||
property real handleValue: maximumValue
|
||||
|
||||
property bool pathsVisible: true
|
||||
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
|
||||
|
||||
function getHandleValueFromSliderHandle () {
|
||||
function getHandleValueFromSliderHandle()
|
||||
{
|
||||
return handle.getValue()
|
||||
}
|
||||
|
||||
function setHandleValue (value) {
|
||||
function setHandleValue(value)
|
||||
{
|
||||
handle.setValue(value)
|
||||
updateRangeHandle()
|
||||
}
|
||||
|
||||
function updateRangeHandle () {
|
||||
function updateRangeHandle()
|
||||
{
|
||||
rangeHandle.width = handle.x - sliderRoot.handleSize
|
||||
}
|
||||
|
||||
function normalizeValue(value) {
|
||||
function normalizeValue(value)
|
||||
{
|
||||
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
|
||||
}
|
||||
|
||||
// slider track
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
id: track
|
||||
|
||||
width: sliderRoot.width - sliderRoot.handleSize
|
||||
|
|
@ -67,7 +74,8 @@ Item {
|
|||
}
|
||||
|
||||
// Progress indicator
|
||||
Item {
|
||||
Item
|
||||
{
|
||||
id: rangeHandle
|
||||
|
||||
x: handle.width
|
||||
|
|
@ -76,7 +84,8 @@ Item {
|
|||
anchors.verticalCenter: sliderRoot.verticalCenter
|
||||
visible: sliderRoot.pathsVisible
|
||||
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
|
||||
width: parent.width + sliderRoot.handleSize
|
||||
anchors.centerIn: parent
|
||||
|
|
@ -85,7 +94,8 @@ Item {
|
|||
}
|
||||
|
||||
// Handle
|
||||
Rectangle {
|
||||
Rectangle
|
||||
{
|
||||
id: handle
|
||||
|
||||
x: sliderRoot.handleSize
|
||||
|
|
@ -96,7 +106,9 @@ Item {
|
|||
color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
|
||||
visible: sliderRoot.pathsVisible
|
||||
|
||||
function onHandleDragged () {
|
||||
function onHandleDragged()
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
|
||||
// update the range handle
|
||||
sliderRoot.updateRangeHandle()
|
||||
|
|
@ -106,15 +118,23 @@ Item {
|
|||
}
|
||||
|
||||
// get the value based on the slider position
|
||||
function getValue () {
|
||||
function getValue()
|
||||
{
|
||||
var result = x / (sliderRoot.width - sliderRoot.handleSize)
|
||||
result = result * sliderRoot.maximumValue
|
||||
result = sliderRoot.roundValues ? Math.round(result) : result
|
||||
return result
|
||||
}
|
||||
|
||||
function setValueManually(value)
|
||||
{
|
||||
sliderRoot.manuallyChanged = true
|
||||
handle.setValue(value)
|
||||
}
|
||||
|
||||
// set the slider position based on the value
|
||||
function setValue (value) {
|
||||
function setValue(value)
|
||||
{
|
||||
// Normalize values between range, since using arrow keys will create out-of-the-range values
|
||||
value = sliderRoot.normalizeValue(value)
|
||||
|
||||
|
|
@ -132,23 +152,23 @@ Item {
|
|||
Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
|
||||
|
||||
// dragging
|
||||
MouseArea {
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
drag {
|
||||
drag
|
||||
{
|
||||
target: parent
|
||||
axis: Drag.XAxis
|
||||
minimumX: 0
|
||||
maximumX: sliderRoot.width - sliderRoot.handleSize
|
||||
}
|
||||
onPressed: {
|
||||
handleLabel.forceActiveFocus()
|
||||
}
|
||||
|
||||
onPressed: handleLabel.forceActiveFocus()
|
||||
onPositionChanged: parent.onHandleDragged()
|
||||
}
|
||||
|
||||
SimulationSliderLabel {
|
||||
SimulationSliderLabel
|
||||
{
|
||||
id: handleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
|
|
@ -162,7 +182,7 @@ Item {
|
|||
maximumValue: sliderRoot.maximumValue
|
||||
value: sliderRoot.handleValue
|
||||
busy: UM.SimulationView.busy
|
||||
setValue: handle.setValue // connect callback functions
|
||||
setValue: handle.setValueManually // connect callback functions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ from UM.Platform import Platform
|
|||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Signal import Signal
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||
|
||||
|
||||
from UM.View.View import View
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Scene.ConvexHullNode import ConvexHullNode
|
||||
|
|
@ -30,11 +33,20 @@ from cura.CuraApplication import CuraApplication
|
|||
from .NozzleNode import NozzleNode
|
||||
from .SimulationPass import SimulationPass
|
||||
from .SimulationViewProxy import SimulationViewProxy
|
||||
import numpy
|
||||
import os.path
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Scene import Scene
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
from UM.View.RenderPass import RenderPass
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import numpy
|
||||
import os.path
|
||||
|
||||
## View used to display g-code paths.
|
||||
class SimulationView(View):
|
||||
|
|
@ -44,7 +56,7 @@ class SimulationView(View):
|
|||
LAYER_VIEW_TYPE_FEEDRATE = 2
|
||||
LAYER_VIEW_TYPE_THICKNESS = 3
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._max_layers = 0
|
||||
|
|
@ -64,21 +76,21 @@ class SimulationView(View):
|
|||
self._busy = False
|
||||
self._simulation_running = False
|
||||
|
||||
self._ghost_shader = None
|
||||
self._layer_pass = None
|
||||
self._composite_pass = None
|
||||
self._ghost_shader = None # type: Optional["ShaderProgram"]
|
||||
self._layer_pass = None # type: Optional[SimulationPass]
|
||||
self._composite_pass = None # type: Optional[RenderPass]
|
||||
self._old_layer_bindings = None
|
||||
self._simulationview_composite_shader = None
|
||||
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
|
||||
self._old_composite_shader = None
|
||||
|
||||
self._global_container_stack = None
|
||||
self._global_container_stack = None # type: Optional[ContainerStack]
|
||||
self._proxy = SimulationViewProxy()
|
||||
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
|
||||
|
||||
self._resetSettings()
|
||||
self._legend_items = None
|
||||
self._show_travel_moves = False
|
||||
self._nozzle_node = None
|
||||
self._nozzle_node = None # type: Optional[NozzleNode]
|
||||
|
||||
Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
|
||||
Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
|
||||
|
|
@ -102,29 +114,29 @@ class SimulationView(View):
|
|||
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
|
||||
title = catalog.i18nc("@info:title", "Simulation View"))
|
||||
|
||||
def _evaluateCompatibilityMode(self):
|
||||
def _evaluateCompatibilityMode(self) -> bool:
|
||||
return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode"))
|
||||
|
||||
def _resetSettings(self):
|
||||
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
|
||||
def _resetSettings(self) -> None:
|
||||
self._layer_view_type = 0 # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
|
||||
self._extruder_count = 0
|
||||
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
|
||||
self._show_travel_moves = 0
|
||||
self._show_helpers = 1
|
||||
self._show_skin = 1
|
||||
self._show_infill = 1
|
||||
self._show_travel_moves = False
|
||||
self._show_helpers = True
|
||||
self._show_skin = True
|
||||
self._show_infill = True
|
||||
self.resetLayerData()
|
||||
|
||||
def getActivity(self):
|
||||
def getActivity(self) -> bool:
|
||||
return self._activity
|
||||
|
||||
def setActivity(self, activity):
|
||||
def setActivity(self, activity: bool) -> None:
|
||||
if self._activity == activity:
|
||||
return
|
||||
self._activity = activity
|
||||
self.activityChanged.emit()
|
||||
|
||||
def getSimulationPass(self):
|
||||
def getSimulationPass(self) -> SimulationPass:
|
||||
if not self._layer_pass:
|
||||
# Currently the RenderPass constructor requires a size > 0
|
||||
# This should be fixed in RenderPass's constructor.
|
||||
|
|
@ -133,30 +145,30 @@ class SimulationView(View):
|
|||
self._layer_pass.setSimulationView(self)
|
||||
return self._layer_pass
|
||||
|
||||
def getCurrentLayer(self):
|
||||
def getCurrentLayer(self) -> int:
|
||||
return self._current_layer_num
|
||||
|
||||
def getMinimumLayer(self):
|
||||
def getMinimumLayer(self) -> int:
|
||||
return self._minimum_layer_num
|
||||
|
||||
def getMaxLayers(self):
|
||||
def getMaxLayers(self) -> int:
|
||||
return self._max_layers
|
||||
|
||||
def getCurrentPath(self):
|
||||
def getCurrentPath(self) -> int:
|
||||
return self._current_path_num
|
||||
|
||||
def getMinimumPath(self):
|
||||
def getMinimumPath(self) -> int:
|
||||
return self._minimum_path_num
|
||||
|
||||
def getMaxPaths(self):
|
||||
def getMaxPaths(self) -> int:
|
||||
return self._max_paths
|
||||
|
||||
def getNozzleNode(self):
|
||||
def getNozzleNode(self) -> NozzleNode:
|
||||
if not self._nozzle_node:
|
||||
self._nozzle_node = NozzleNode()
|
||||
return self._nozzle_node
|
||||
|
||||
def _onSceneChanged(self, node):
|
||||
def _onSceneChanged(self, node: "SceneNode") -> None:
|
||||
if node.getMeshData() is None:
|
||||
self.resetLayerData()
|
||||
|
||||
|
|
@ -164,21 +176,21 @@ class SimulationView(View):
|
|||
self.calculateMaxLayers()
|
||||
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
||||
|
||||
def isBusy(self):
|
||||
def isBusy(self) -> bool:
|
||||
return self._busy
|
||||
|
||||
def setBusy(self, busy):
|
||||
def setBusy(self, busy: bool) -> None:
|
||||
if busy != self._busy:
|
||||
self._busy = busy
|
||||
self.busyChanged.emit()
|
||||
|
||||
def isSimulationRunning(self):
|
||||
def isSimulationRunning(self) -> bool:
|
||||
return self._simulation_running
|
||||
|
||||
def setSimulationRunning(self, running):
|
||||
def setSimulationRunning(self, running: bool) -> None:
|
||||
self._simulation_running = running
|
||||
|
||||
def resetLayerData(self):
|
||||
def resetLayerData(self) -> None:
|
||||
self._current_layer_mesh = None
|
||||
self._current_layer_jumps = None
|
||||
self._max_feedrate = sys.float_info.min
|
||||
|
|
@ -186,7 +198,7 @@ class SimulationView(View):
|
|||
self._max_thickness = sys.float_info.min
|
||||
self._min_thickness = sys.float_info.max
|
||||
|
||||
def beginRendering(self):
|
||||
def beginRendering(self) -> None:
|
||||
scene = self.getController().getScene()
|
||||
renderer = self.getRenderer()
|
||||
|
||||
|
|
@ -204,7 +216,7 @@ class SimulationView(View):
|
|||
if (node.getMeshData()) and node.isVisible():
|
||||
renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
|
||||
|
||||
def setLayer(self, value):
|
||||
def setLayer(self, value: int) -> None:
|
||||
if self._current_layer_num != value:
|
||||
self._current_layer_num = value
|
||||
if self._current_layer_num < 0:
|
||||
|
|
@ -218,7 +230,7 @@ class SimulationView(View):
|
|||
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def setMinimumLayer(self, value):
|
||||
def setMinimumLayer(self, value: int) -> None:
|
||||
if self._minimum_layer_num != value:
|
||||
self._minimum_layer_num = value
|
||||
if self._minimum_layer_num < 0:
|
||||
|
|
@ -232,7 +244,7 @@ class SimulationView(View):
|
|||
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def setPath(self, value):
|
||||
def setPath(self, value: int) -> None:
|
||||
if self._current_path_num != value:
|
||||
self._current_path_num = value
|
||||
if self._current_path_num < 0:
|
||||
|
|
@ -246,7 +258,7 @@ class SimulationView(View):
|
|||
|
||||
self.currentPathNumChanged.emit()
|
||||
|
||||
def setMinimumPath(self, value):
|
||||
def setMinimumPath(self, value: int) -> None:
|
||||
if self._minimum_path_num != value:
|
||||
self._minimum_path_num = value
|
||||
if self._minimum_path_num < 0:
|
||||
|
|
@ -263,24 +275,24 @@ class SimulationView(View):
|
|||
## Set the layer view type
|
||||
#
|
||||
# \param layer_view_type integer as in SimulationView.qml and this class
|
||||
def setSimulationViewType(self, layer_view_type):
|
||||
def setSimulationViewType(self, layer_view_type: int) -> None:
|
||||
self._layer_view_type = layer_view_type
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
## Return the layer view type, integer as in SimulationView.qml and this class
|
||||
def getSimulationViewType(self):
|
||||
def getSimulationViewType(self) -> int:
|
||||
return self._layer_view_type
|
||||
|
||||
## Set the extruder opacity
|
||||
#
|
||||
# \param extruder_nr 0..3
|
||||
# \param opacity 0.0 .. 1.0
|
||||
def setExtruderOpacity(self, extruder_nr, opacity):
|
||||
def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
|
||||
if 0 <= extruder_nr <= 3:
|
||||
self._extruder_opacity[extruder_nr] = opacity
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def getExtruderOpacities(self):
|
||||
def getExtruderOpacities(self)-> List[float]:
|
||||
return self._extruder_opacity
|
||||
|
||||
def setShowTravelMoves(self, show):
|
||||
|
|
@ -290,46 +302,46 @@ class SimulationView(View):
|
|||
def getShowTravelMoves(self):
|
||||
return self._show_travel_moves
|
||||
|
||||
def setShowHelpers(self, show):
|
||||
def setShowHelpers(self, show: bool) -> None:
|
||||
self._show_helpers = show
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def getShowHelpers(self):
|
||||
def getShowHelpers(self) -> bool:
|
||||
return self._show_helpers
|
||||
|
||||
def setShowSkin(self, show):
|
||||
def setShowSkin(self, show: bool) -> None:
|
||||
self._show_skin = show
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def getShowSkin(self):
|
||||
def getShowSkin(self) -> bool:
|
||||
return self._show_skin
|
||||
|
||||
def setShowInfill(self, show):
|
||||
def setShowInfill(self, show: bool) -> None:
|
||||
self._show_infill = show
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def getShowInfill(self):
|
||||
def getShowInfill(self) -> bool:
|
||||
return self._show_infill
|
||||
|
||||
def getCompatibilityMode(self):
|
||||
def getCompatibilityMode(self) -> bool:
|
||||
return self._compatibility_mode
|
||||
|
||||
def getExtruderCount(self):
|
||||
def getExtruderCount(self) -> int:
|
||||
return self._extruder_count
|
||||
|
||||
def getMinFeedrate(self):
|
||||
def getMinFeedrate(self) -> float:
|
||||
return self._min_feedrate
|
||||
|
||||
def getMaxFeedrate(self):
|
||||
def getMaxFeedrate(self) -> float:
|
||||
return self._max_feedrate
|
||||
|
||||
def getMinThickness(self):
|
||||
def getMinThickness(self) -> float:
|
||||
return self._min_thickness
|
||||
|
||||
def getMaxThickness(self):
|
||||
def getMaxThickness(self) -> float:
|
||||
return self._max_thickness
|
||||
|
||||
def calculateMaxLayers(self):
|
||||
def calculateMaxLayers(self) -> None:
|
||||
scene = self.getController().getScene()
|
||||
|
||||
self._old_max_layers = self._max_layers
|
||||
|
|
@ -383,7 +395,7 @@ class SimulationView(View):
|
|||
self.maxLayersChanged.emit()
|
||||
self._startUpdateTopLayers()
|
||||
|
||||
def calculateMaxPathsOnLayer(self, layer_num):
|
||||
def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
|
||||
# Update the currentPath
|
||||
scene = self.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
|
|
@ -415,10 +427,10 @@ class SimulationView(View):
|
|||
def getProxy(self, engine, script_engine):
|
||||
return self._proxy
|
||||
|
||||
def endRendering(self):
|
||||
def endRendering(self) -> None:
|
||||
pass
|
||||
|
||||
def event(self, event):
|
||||
def event(self, event) -> bool:
|
||||
modifiers = QApplication.keyboardModifiers()
|
||||
ctrl_is_active = modifiers & Qt.ControlModifier
|
||||
shift_is_active = modifiers & Qt.ShiftModifier
|
||||
|
|
@ -447,7 +459,7 @@ class SimulationView(View):
|
|||
if QOpenGLContext.currentContext() is None:
|
||||
Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
|
||||
CuraApplication.getInstance().callLater(lambda e=event: self.event(e))
|
||||
return
|
||||
return False
|
||||
|
||||
# Make sure the SimulationPass is created
|
||||
layer_pass = self.getSimulationPass()
|
||||
|
|
@ -480,11 +492,14 @@ class SimulationView(View):
|
|||
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
||||
self._nozzle_node.setParent(None)
|
||||
if self._nozzle_node:
|
||||
self._nozzle_node.setParent(None)
|
||||
self.getRenderer().removeRenderPass(self._layer_pass)
|
||||
self._composite_pass.setLayerBindings(self._old_layer_bindings)
|
||||
self._composite_pass.setCompositeShader(self._old_composite_shader)
|
||||
if self._composite_pass:
|
||||
self._composite_pass.setLayerBindings(self._old_layer_bindings)
|
||||
self._composite_pass.setCompositeShader(self._old_composite_shader)
|
||||
|
||||
return False
|
||||
|
||||
def getCurrentLayerMesh(self):
|
||||
return self._current_layer_mesh
|
||||
|
|
@ -492,7 +507,7 @@ class SimulationView(View):
|
|||
def getCurrentLayerJumps(self):
|
||||
return self._current_layer_jumps
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
|
@ -504,17 +519,17 @@ class SimulationView(View):
|
|||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
def _onPropertyChanged(self, key: str, property_name: str) -> None:
|
||||
if key == "wireframe_enabled" and property_name == "value":
|
||||
if self._global_container_stack.getProperty("wireframe_enabled", "value"):
|
||||
if self._global_container_stack and self._global_container_stack.getProperty("wireframe_enabled", "value"):
|
||||
self._wireprint_warning_message.show()
|
||||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _onCurrentLayerNumChanged(self):
|
||||
def _onCurrentLayerNumChanged(self) -> None:
|
||||
self.calculateMaxPathsOnLayer(self._current_layer_num)
|
||||
|
||||
def _startUpdateTopLayers(self):
|
||||
def _startUpdateTopLayers(self) -> None:
|
||||
if not self._compatibility_mode:
|
||||
return
|
||||
|
||||
|
|
@ -525,10 +540,10 @@ class SimulationView(View):
|
|||
self.setBusy(True)
|
||||
|
||||
self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
|
||||
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
|
||||
self._top_layers_job.start()
|
||||
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) # type: ignore # mypy doesn't understand the whole private class thing that's going on here.
|
||||
self._top_layers_job.start() # type: ignore
|
||||
|
||||
def _updateCurrentLayerMesh(self, job):
|
||||
def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None:
|
||||
self.setBusy(False)
|
||||
|
||||
if not job.getResult():
|
||||
|
|
@ -539,9 +554,9 @@ class SimulationView(View):
|
|||
self._current_layer_jumps = job.getResult().get("jumps")
|
||||
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
|
||||
|
||||
self._top_layers_job = None
|
||||
self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
|
||||
|
||||
def _updateWithPreferences(self):
|
||||
def _updateWithPreferences(self) -> None:
|
||||
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
|
||||
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
|
||||
self._compatibility_mode = self._evaluateCompatibilityMode()
|
||||
|
|
@ -563,7 +578,7 @@ class SimulationView(View):
|
|||
self._startUpdateTopLayers()
|
||||
self.preferencesChanged.emit()
|
||||
|
||||
def _onPreferencesChanged(self, preference):
|
||||
def _onPreferencesChanged(self, preference: str) -> None:
|
||||
if preference not in {
|
||||
"view/top_layer_count",
|
||||
"view/only_show_top_layers",
|
||||
|
|
@ -581,7 +596,7 @@ class SimulationView(View):
|
|||
|
||||
|
||||
class _CreateTopLayersJob(Job):
|
||||
def __init__(self, scene, layer_number, solid_layers):
|
||||
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._scene = scene
|
||||
|
|
@ -589,7 +604,7 @@ class _CreateTopLayersJob(Job):
|
|||
self._solid_layers = solid_layers
|
||||
self._cancel = False
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
layer_data = None
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
|
|
@ -638,6 +653,6 @@ class _CreateTopLayersJob(Job):
|
|||
|
||||
self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
self._cancel = True
|
||||
super().cancel()
|
||||
|
|
|
|||
|
|
@ -623,7 +623,15 @@ Item
|
|||
{
|
||||
target: UM.SimulationView
|
||||
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
|
||||
onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
|
||||
onCurrentPathChanged:
|
||||
{
|
||||
// Only pause the simulation when the layer was changed manually, not when the simulation is running
|
||||
if (pathSlider.manuallyChanged)
|
||||
{
|
||||
playButton.pauseSimulation()
|
||||
}
|
||||
pathSlider.setHandleValue(UM.SimulationView.currentPath)
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the slider handlers show the correct value after switching views
|
||||
|
|
@ -667,9 +675,14 @@ Item
|
|||
{
|
||||
target: UM.SimulationView
|
||||
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
|
||||
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
|
||||
onCurrentLayerChanged:
|
||||
{
|
||||
playButton.pauseSimulation()
|
||||
// Only pause the simulation when the layer was changed manually, not when the simulation is running
|
||||
if (layerSlider.manuallyChanged)
|
||||
{
|
||||
playButton.pauseSimulation()
|
||||
}
|
||||
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
|
||||
}
|
||||
}
|
||||
|
|
@ -719,6 +732,8 @@ Item
|
|||
iconSource = "./resources/simulation_resume.svg"
|
||||
simulationTimer.stop()
|
||||
status = 0
|
||||
layerSlider.manuallyChanged = true
|
||||
pathSlider.manuallyChanged = true
|
||||
}
|
||||
|
||||
function resumeSimulation()
|
||||
|
|
@ -726,7 +741,8 @@ Item
|
|||
UM.SimulationView.setSimulationRunning(true)
|
||||
iconSource = "./resources/simulation_pause.svg"
|
||||
simulationTimer.start()
|
||||
status = 1
|
||||
layerSlider.manuallyChanged = false
|
||||
pathSlider.manuallyChanged = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -770,7 +786,6 @@ Item
|
|||
{
|
||||
UM.SimulationView.setCurrentLayer(currentLayer+1)
|
||||
UM.SimulationView.setCurrentPath(0)
|
||||
playButton.resumeSimulation()
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -778,6 +793,8 @@ Item
|
|||
UM.SimulationView.setCurrentPath(currentPath+1)
|
||||
}
|
||||
}
|
||||
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
|
||||
// correctly, because part of the logic is in this trigger function.
|
||||
playButton.status = 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import json
|
|||
import os
|
||||
import platform
|
||||
import time
|
||||
from typing import cast, Optional, Set
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ from UM.i18n import i18nCatalog
|
|||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
from typing import cast, Optional
|
||||
|
||||
from .SliceInfoJob import SliceInfoJob
|
||||
|
||||
|
||||
|
|
@ -95,13 +96,29 @@ class SliceInfo(QObject, Extension):
|
|||
def setSendSliceInfo(self, enabled: bool):
|
||||
Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)
|
||||
|
||||
def _getUserModifiedSettingKeys(self) -> list:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = cast(CuraApplication, Application.getInstance())
|
||||
machine_manager = application.getMachineManager()
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
user_modified_setting_keys = set() # type: Set[str]
|
||||
|
||||
for stack in [global_stack] + list(global_stack.extruders.values()):
|
||||
# Get all settings in user_changes and quality_changes
|
||||
all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys()
|
||||
user_modified_setting_keys |= all_keys
|
||||
|
||||
return list(sorted(user_modified_setting_keys))
|
||||
|
||||
def _onWriteStarted(self, output_device):
|
||||
try:
|
||||
if not Application.getInstance().getPreferences().getValue("info/send_slice_info"):
|
||||
Logger.log("d", "'info/send_slice_info' is turned off.")
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
application = Application.getInstance()
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = cast(CuraApplication, Application.getInstance())
|
||||
machine_manager = application.getMachineManager()
|
||||
print_information = application.getPrintInformation()
|
||||
|
||||
|
|
@ -164,6 +181,8 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type")
|
||||
|
||||
data["user_modified_setting_keys"] = self._getUserModifiedSettingKeys()
|
||||
|
||||
data["models"] = []
|
||||
# Listing all files placed on the build plate
|
||||
for node in DepthFirstIterator(application.getController().getScene().getRoot()):
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
}
|
||||
],
|
||||
"quality_profile": "fast",
|
||||
"user_modified_setting_keys": ["layer_height", "wall_line_width", "infill_sparse_density"],
|
||||
"models": [
|
||||
{
|
||||
"hash": "b72789b9beb5366dff20b1cf501020c3d4d4df7dc2295ecd0fddd0a6436df070",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ Window
|
|||
{
|
||||
id: header
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: mainView
|
||||
|
|
@ -75,6 +76,7 @@ Window
|
|||
visible: toolbox.viewCategory == "installed"
|
||||
}
|
||||
}
|
||||
|
||||
ToolboxFooter
|
||||
{
|
||||
id: footer
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import UM 1.1 as UM
|
|||
|
||||
Item
|
||||
{
|
||||
id: toolboxDownloadsGridTile
|
||||
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
|
||||
property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
|
||||
height: childrenRect.height
|
||||
|
|
|
|||
|
|
@ -21,8 +21,10 @@ Item
|
|||
left: parent.left
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
ToolboxTabButton
|
||||
{
|
||||
id: pluginsTabButton
|
||||
text: catalog.i18nc("@title:tab", "Plugins")
|
||||
active: toolbox.viewCategory == "plugin" && enabled
|
||||
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
|
||||
|
|
@ -36,6 +38,7 @@ Item
|
|||
|
||||
ToolboxTabButton
|
||||
{
|
||||
id: materialsTabButton
|
||||
text: catalog.i18nc("@title:tab", "Materials")
|
||||
active: toolbox.viewCategory == "material" && enabled
|
||||
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
|
||||
|
|
@ -49,6 +52,7 @@ Item
|
|||
}
|
||||
ToolboxTabButton
|
||||
{
|
||||
id: installedTabButton
|
||||
text: catalog.i18nc("@title:tab", "Installed")
|
||||
active: toolbox.viewCategory == "installed"
|
||||
anchors
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Dict, Optional, Union, Any, cast
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import platform
|
||||
from typing import cast, List, TYPE_CHECKING, Tuple, Optional
|
||||
from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
|
|
@ -40,7 +39,7 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
self._application = application # type: CuraApplication
|
||||
|
||||
self._sdk_version = None # type: Optional[int]
|
||||
self._sdk_version = None # type: Optional[Union[str, int]]
|
||||
self._cloud_api_version = None # type: Optional[int]
|
||||
self._cloud_api_root = None # type: Optional[str]
|
||||
self._api_url = None # type: Optional[str]
|
||||
|
|
@ -64,7 +63,8 @@ class Toolbox(QObject, Extension):
|
|||
]
|
||||
self._request_urls = {} # type: Dict[str, QUrl]
|
||||
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
|
||||
self._old_plugin_ids = [] # type: List[str]
|
||||
self._old_plugin_ids = set() # type: Set[str]
|
||||
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
# Data:
|
||||
self._metadata = {
|
||||
|
|
@ -207,14 +207,14 @@ class Toolbox(QObject, Extension):
|
|||
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
|
||||
|
||||
# Get the packages version depending on Cura version settings.
|
||||
def _getSDKVersion(self) -> int:
|
||||
def _getSDKVersion(self) -> Union[int, str]:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self._plugin_registry.APIVersion
|
||||
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
|
||||
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
|
||||
return self._plugin_registry.APIVersion
|
||||
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
|
||||
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
|
||||
return self._plugin_registry.APIVersion
|
||||
return cura.CuraVersion.CuraSDKVersion # type: ignore
|
||||
return cura.CuraVersion.CuraSDKVersion # type: ignore
|
||||
|
||||
@pyqtSlot()
|
||||
def browsePackages(self) -> None:
|
||||
|
|
@ -289,8 +289,8 @@ class Toolbox(QObject, Extension):
|
|||
installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
|
||||
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
|
||||
|
||||
self._old_plugin_ids = []
|
||||
self._old_plugin_metadata = [] # type: List[Dict[str, Any]]
|
||||
self._old_plugin_ids = set()
|
||||
self._old_plugin_metadata = dict()
|
||||
|
||||
for plugin_id in old_plugin_ids:
|
||||
# Neither the installed packages nor the packages that are scheduled to remove are old plugins
|
||||
|
|
@ -300,12 +300,20 @@ class Toolbox(QObject, Extension):
|
|||
old_metadata = self._plugin_registry.getMetaData(plugin_id)
|
||||
new_metadata = self._convertPluginMetadata(old_metadata)
|
||||
|
||||
self._old_plugin_ids.append(plugin_id)
|
||||
self._old_plugin_metadata.append(new_metadata)
|
||||
self._old_plugin_ids.add(plugin_id)
|
||||
self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata
|
||||
|
||||
all_packages = self._package_manager.getAllInstalledPackagesInfo()
|
||||
if "plugin" in all_packages:
|
||||
self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata
|
||||
# For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
|
||||
# The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
|
||||
# plugins should be excluded from the old plugins list/dict.
|
||||
all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
|
||||
self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
|
||||
if plugin_id not in all_plugin_package_ids)
|
||||
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
|
||||
|
||||
self._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values())
|
||||
self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
|
||||
self.metadataChanged.emit()
|
||||
if "material" in all_packages:
|
||||
|
|
@ -475,12 +483,14 @@ class Toolbox(QObject, Extension):
|
|||
# --------------------------------------------------------------------------
|
||||
@pyqtSlot(str, result = bool)
|
||||
def canUpdate(self, package_id: str) -> bool:
|
||||
if self.isOldPlugin(package_id):
|
||||
return True
|
||||
|
||||
local_package = self._package_manager.getInstalledPackageInfo(package_id)
|
||||
if local_package is None:
|
||||
return False
|
||||
Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins",
|
||||
package_id)
|
||||
local_package = self.getOldPluginPackageMetadata(package_id)
|
||||
if local_package is None:
|
||||
Logger.log("i", "Could not find package [%s] in the old plugins", package_id)
|
||||
return False
|
||||
|
||||
remote_package = self.getRemotePackage(package_id)
|
||||
if remote_package is None:
|
||||
|
|
@ -488,7 +498,16 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
local_version = Version(local_package["package_version"])
|
||||
remote_version = Version(remote_package["package_version"])
|
||||
return remote_version > local_version
|
||||
can_upgrade = False
|
||||
if remote_version > local_version:
|
||||
can_upgrade = True
|
||||
# A package with the same version can be built to have different SDK versions. So, for a package with the same
|
||||
# version, we also need to check if the current one has a lower SDK version. If so, this package should also
|
||||
# be upgradable.
|
||||
elif remote_version == local_version:
|
||||
can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0)
|
||||
|
||||
return can_upgrade
|
||||
|
||||
@pyqtSlot(str, result = bool)
|
||||
def canDowngrade(self, package_id: str) -> bool:
|
||||
|
|
@ -508,7 +527,11 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isInstalled(self, package_id: str) -> bool:
|
||||
return self._package_manager.isPackageInstalled(package_id)
|
||||
result = self._package_manager.isPackageInstalled(package_id)
|
||||
# Also check the old plugins list if it's not found in the package manager.
|
||||
if not result:
|
||||
result = self.isOldPlugin(package_id)
|
||||
return result
|
||||
|
||||
@pyqtSlot(str, result = int)
|
||||
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
|
||||
|
|
@ -535,12 +558,14 @@ class Toolbox(QObject, Extension):
|
|||
return False
|
||||
|
||||
# Check for plugins that were installed with the old plugin browser
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isOldPlugin(self, plugin_id: str) -> bool:
|
||||
if plugin_id in self._old_plugin_ids:
|
||||
return True
|
||||
return False
|
||||
|
||||
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
|
||||
return self._old_plugin_metadata.get(plugin_id)
|
||||
|
||||
def loadingComplete(self) -> bool:
|
||||
populated = 0
|
||||
for list in self._metadata.items():
|
||||
|
|
@ -626,7 +651,7 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
# HACK: Do nothing because we'll handle these from the "packages" call
|
||||
if type in do_not_handle:
|
||||
return
|
||||
continue
|
||||
|
||||
if reply.url() == url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick 2.3
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls.Styles 1.3
|
||||
import QtGraphicalEffects 1.0
|
||||
|
|
@ -443,8 +444,8 @@ Component
|
|||
text: catalog.i18nc("@label", "Abort")
|
||||
onClicked:
|
||||
{
|
||||
modelData.activePrintJob.setState("abort")
|
||||
popup.close()
|
||||
abortConfirmationDialog.visible = true;
|
||||
popup.close();
|
||||
}
|
||||
width: parent.width
|
||||
anchors.top: pauseButton.bottom
|
||||
|
|
@ -456,6 +457,17 @@ Component
|
|||
color: UM.Theme.getColor("viewport_background")
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: abortConfirmationDialog
|
||||
title: catalog.i18nc("@window:title", "Abort print")
|
||||
icon: StandardIcon.Warning
|
||||
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to abort %1?").arg(modelData.activePrintJob.name)
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
Component.onCompleted: visible = false
|
||||
onYes: modelData.activePrintJob.setState("abort")
|
||||
}
|
||||
}
|
||||
|
||||
background: Item
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick 2.2
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Controls 2.0
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
import QtGraphicalEffects 1.0
|
||||
|
|
@ -212,8 +213,8 @@ Item
|
|||
text: catalog.i18nc("@label", "Move to top")
|
||||
onClicked:
|
||||
{
|
||||
OutputDevice.sendJobToTop(printJob.key)
|
||||
popup.close()
|
||||
sendToTopConfirmationDialog.visible = true;
|
||||
popup.close();
|
||||
}
|
||||
width: parent.width
|
||||
enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key
|
||||
|
|
@ -227,14 +228,25 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: sendToTopConfirmationDialog
|
||||
title: catalog.i18nc("@window:title", "Move print job to top")
|
||||
icon: StandardIcon.Warning
|
||||
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to move %1 to the top of the queue?").arg(printJob.name)
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
Component.onCompleted: visible = false
|
||||
onYes: OutputDevice.sendJobToTop(printJob.key)
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: deleteButton
|
||||
text: catalog.i18nc("@label", "Delete")
|
||||
onClicked:
|
||||
{
|
||||
OutputDevice.deleteJobFromQueue(printJob.key)
|
||||
popup.close()
|
||||
deleteConfirmationDialog.visible = true;
|
||||
popup.close();
|
||||
}
|
||||
width: parent.width
|
||||
anchors.top: sendToTopButton.bottom
|
||||
|
|
@ -245,6 +257,17 @@ Item
|
|||
color: UM.Theme.getColor("viewport_background")
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: deleteConfirmationDialog
|
||||
title: catalog.i18nc("@window:title", "Delete print job")
|
||||
icon: StandardIcon.Warning
|
||||
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to delete %1?").arg(printJob.name)
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
Component.onCompleted: visible = false
|
||||
onYes: OutputDevice.deleteJobFromQueue(printJob.key)
|
||||
}
|
||||
}
|
||||
|
||||
background: Item
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ class AutoDetectBaudJob(Job):
|
|||
self.setResult(baud_rate)
|
||||
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(
|
||||
serial = self._serial_port, baud_rate = baud_rate, retry = retry, time_elapsed = time() - start_timeout_time))
|
||||
serial.close() # close serial port so it can be opened by the USBPrinterOutputDevice
|
||||
return
|
||||
|
||||
serial.write(b"M105\n")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser #To parse the files we need to upgrade and write the new files.
|
||||
|
|
@ -9,8 +9,6 @@ from urllib.parse import quote_plus
|
|||
from UM.Resources import Resources
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
_removed_settings = { #Settings that were removed in 2.5.
|
||||
"start_layers_at_same_position",
|
||||
"sub_div_rad_mult"
|
||||
|
|
@ -152,7 +150,7 @@ class VersionUpgrade25to26(VersionUpgrade):
|
|||
|
||||
## Acquires the next unique extruder stack index number for the Custom FDM Printer.
|
||||
def _acquireNextUniqueCustomFdmPrinterExtruderStackIdIndex(self):
|
||||
extruder_stack_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
|
||||
extruder_stack_dir = os.path.join(Resources.getDataStoragePath(), "extruders")
|
||||
file_name_list = os.listdir(extruder_stack_dir)
|
||||
file_name_list = [os.path.basename(file_name) for file_name in file_name_list]
|
||||
while True:
|
||||
|
|
@ -173,7 +171,7 @@ class VersionUpgrade25to26(VersionUpgrade):
|
|||
|
||||
def _checkCustomFdmPrinterHasExtruderStack(self, machine_id):
|
||||
# go through all extruders and make sure that this custom FDM printer has extruder stacks.
|
||||
extruder_stack_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
|
||||
extruder_stack_dir = os.path.join(Resources.getDataStoragePath(), "extruders")
|
||||
has_extruders = False
|
||||
for item in os.listdir(extruder_stack_dir):
|
||||
file_path = os.path.join(extruder_stack_dir, item)
|
||||
|
|
@ -245,9 +243,9 @@ class VersionUpgrade25to26(VersionUpgrade):
|
|||
parser.write(extruder_output)
|
||||
extruder_filename = quote_plus(stack_id) + ".extruder.cfg"
|
||||
|
||||
extruder_stack_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
|
||||
definition_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
|
||||
user_settings_dir = Resources.getPath(CuraApplication.ResourceTypes.UserInstanceContainer)
|
||||
extruder_stack_dir = os.path.join(Resources.getDataStoragePath(), "extruders")
|
||||
definition_changes_dir = os.path.join(Resources.getDataStoragePath(), "definition_changes")
|
||||
user_settings_dir = os.path.join(Resources.getDataStoragePath(), "user")
|
||||
|
||||
with open(os.path.join(definition_changes_dir, definition_changes_filename), "w", encoding = "utf-8") as f:
|
||||
f.write(definition_changes_output.getvalue())
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser #To parse the files we need to upgrade and write the new files.
|
||||
import io #To serialise configparser output to a string.
|
||||
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
# a dict of renamed quality profiles: <old_id> : <new_id>
|
||||
_renamed_quality_profiles = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue