mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 03:55:55 +08:00
Merge branch 'master' into master
This commit is contained in:
commit
74ca8a0d8e
2
.github/ISSUE_TEMPLATE/bug-report.md
vendored
2
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@ -40,7 +40,7 @@ Thank you for using Cura!
|
|||||||
(What should happen after the above steps have been followed.)
|
(What should happen after the above steps have been followed.)
|
||||||
|
|
||||||
**Project file**
|
**Project file**
|
||||||
(For slicing bugs, provide a project which clearly shows the bug, by going to File->Save. For big files you may need to use WeTransfer or similar file sharing sites.)
|
(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**
|
**Log file**
|
||||||
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
(See https://github.com/Ultimaker/Cura#logging-issues to find the log file to upload, or copy a relevant snippet from it.)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: CI/CD
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@ -10,11 +10,12 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build and test
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ultimaker/cura-build-environment
|
container: ultimaker/cura-build-environment
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Cura
|
- name: Checkout Cura
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Build and test
|
- name: Build
|
||||||
run: docker/build.sh
|
run: docker/build.sh
|
||||||
|
- name: Test
|
||||||
|
run: docker/test.sh
|
@ -16,6 +16,8 @@ if(CURA_DEBUGMODE)
|
|||||||
set(_cura_debugmode "ON")
|
set(_cura_debugmode "ON")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
option(GENERATE_TRANSLATIONS "Should the translations be generated?" ON)
|
||||||
|
|
||||||
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
|
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
|
||||||
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
||||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||||
@ -24,30 +26,23 @@ set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
|||||||
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||||
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
|
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
|
||||||
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
|
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}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||||
|
|
||||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||||
|
|
||||||
|
|
||||||
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
|
||||||
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
# So we're using the old method here, with FindPythonInterp for now.
|
||||||
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
|
||||||
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
|
||||||
find_package(PythonInterp 3 REQUIRED)
|
|
||||||
|
|
||||||
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
|
|
||||||
set(Python3_VERSION ${PYTHON_VERSION_STRING})
|
|
||||||
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
|
|
||||||
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
|
|
||||||
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
|
|
||||||
else()
|
|
||||||
# Use FindPython3 for CMake >=3.12
|
|
||||||
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
|
set(Python3_VERSION ${PYTHON_VERSION_STRING})
|
||||||
|
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
|
||||||
|
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
|
||||||
|
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
|
||||||
|
|
||||||
if(NOT ${URANIUM_DIR} STREQUAL "")
|
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||||
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
|
set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${URANIUM_DIR}/cmake")
|
||||||
@ -58,7 +53,9 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
|||||||
# Extract Strings
|
# Extract Strings
|
||||||
add_custom_target(extract-messages ${URANIUM_SCRIPTS_DIR}/extract-messages ${CMAKE_SOURCE_DIR} cura)
|
add_custom_target(extract-messages ${URANIUM_SCRIPTS_DIR}/extract-messages ${CMAKE_SOURCE_DIR} cura)
|
||||||
# Build Translations
|
# Build Translations
|
||||||
CREATE_TRANSLATION_TARGETS()
|
if(${GENERATE_TRANSLATIONS})
|
||||||
|
CREATE_TRANSLATION_TARGETS()
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
|
# form of "a;b;c" or "a,b,c". By default all plugins will be installed.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
option(PRINT_PLUGIN_LIST "Should the list of plugins that are installed be printed?" ON)
|
||||||
|
|
||||||
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||||
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||||
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||||
@ -81,7 +83,9 @@ foreach(_plugin_json_path ${_plugin_json_list})
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(_add_plugin)
|
if(_add_plugin)
|
||||||
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
|
if(${PRINT_PLUGIN_LIST})
|
||||||
|
message(STATUS "[+] PLUGIN TO INSTALL: ${_rel_plugin_dir}")
|
||||||
|
endif()
|
||||||
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
|
get_filename_component(_rel_plugin_parent_dir ${_rel_plugin_dir} DIRECTORY)
|
||||||
install(DIRECTORY ${_rel_plugin_dir}
|
install(DIRECTORY ${_rel_plugin_dir}
|
||||||
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
|
DESTINATION lib${LIB_SUFFIX}/cura/${_rel_plugin_parent_dir}
|
||||||
@ -90,7 +94,9 @@ foreach(_plugin_json_path ${_plugin_json_list})
|
|||||||
)
|
)
|
||||||
list(APPEND _install_plugin_list ${_plugin_dir})
|
list(APPEND _install_plugin_list ${_plugin_dir})
|
||||||
elseif(_is_no_install_plugin)
|
elseif(_is_no_install_plugin)
|
||||||
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
|
if(${PRINT_PLUGIN_LIST})
|
||||||
|
message(STATUS "[-] PLUGIN TO REMOVE : ${_rel_plugin_dir}")
|
||||||
|
endif()
|
||||||
execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
|
execute_process(COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mod_bundled_packages_json.py
|
||||||
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
|
-d ${CMAKE_CURRENT_SOURCE_DIR}/resources/bundled_packages
|
||||||
${_plugin_dir_name}
|
${_plugin_dir_name}
|
||||||
|
@ -4,18 +4,11 @@
|
|||||||
include(CTest)
|
include(CTest)
|
||||||
include(CMakeParseArguments)
|
include(CMakeParseArguments)
|
||||||
|
|
||||||
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
# FIXME: The new FindPython3 finds the system's Python3.6 reather than the Python3.5 that we built for Cura's environment.
|
||||||
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
# So we're using the old method here, with FindPythonInterp for now.
|
||||||
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
|
||||||
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
|
||||||
find_package(PythonInterp 3 REQUIRED)
|
|
||||||
|
|
||||||
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
else()
|
|
||||||
# Use FindPython3 for CMake >=3.12
|
|
||||||
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
||||||
|
|
||||||
@ -56,6 +49,14 @@ function(cura_add_test)
|
|||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
|
||||||
|
#Add code style test.
|
||||||
|
add_test(
|
||||||
|
NAME "code-style"
|
||||||
|
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
#Add test for import statements which are not compatible with all builds
|
#Add test for import statements which are not compatible with all builds
|
||||||
add_test(
|
add_test(
|
||||||
NAME "invalid-imports"
|
NAME "invalid-imports"
|
||||||
@ -74,13 +75,6 @@ foreach(_plugin ${_plugins})
|
|||||||
endif()
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
#Add code style test.
|
|
||||||
add_test(
|
|
||||||
NAME "code-style"
|
|
||||||
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
|
||||||
)
|
|
||||||
|
|
||||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||||
add_test(
|
add_test(
|
||||||
NAME "shortcut-keys"
|
NAME "shortcut-keys"
|
||||||
|
@ -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
|
# 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
|
# 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.
|
# CuraVersion.py.in template.
|
||||||
CuraSDKVersion = "7.2.0"
|
CuraSDKVersion = "7.4.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cura.CuraVersion import CuraAppName # type: ignore
|
from cura.CuraVersion import CuraAppName # type: ignore
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
@ -32,6 +33,7 @@ class Arrange:
|
|||||||
|
|
||||||
build_volume = None # type: Optional[BuildVolume]
|
build_volume = None # type: Optional[BuildVolume]
|
||||||
|
|
||||||
|
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
|
||||||
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
|
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
|
||||||
self._scale = scale # convert input coordinates to arrange coordinates
|
self._scale = scale # convert input coordinates to arrange coordinates
|
||||||
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
||||||
@ -45,6 +47,7 @@ class Arrange:
|
|||||||
self._is_empty = True
|
self._is_empty = True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
|
||||||
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange":
|
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange":
|
||||||
"""Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance
|
"""Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance
|
||||||
|
|
||||||
@ -101,6 +104,7 @@ class Arrange:
|
|||||||
|
|
||||||
self._last_priority = 0
|
self._last_priority = 0
|
||||||
|
|
||||||
|
@deprecated("Use the functions in Nest2dArrange instead", "4.8")
|
||||||
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
|
def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
|
||||||
"""Find placement for a node (using offset shape) and place it (using hull shape)
|
"""Find placement for a node (using offset shape) and place it (using hull shape)
|
||||||
|
|
||||||
|
@ -1,24 +1,17 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from PyQt5.QtCore import QCoreApplication
|
from typing import List
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from UM.Operations.TranslateOperation import TranslateOperation
|
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.Arranging.Nest2DArrange import arrange
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
|
||||||
from cura.Arranging.Arrange import Arrange
|
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
|
||||||
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
class ArrangeObjectsJob(Job):
|
class ArrangeObjectsJob(Job):
|
||||||
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
|
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
|
||||||
@ -30,80 +23,22 @@ class ArrangeObjectsJob(Job):
|
|||||||
def run(self):
|
def run(self):
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
|
||||||
lifetime = 0,
|
lifetime = 0,
|
||||||
dismissable=False,
|
dismissable = False,
|
||||||
progress = 0,
|
progress = 0,
|
||||||
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
|
||||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
|
||||||
|
|
||||||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
|
found_solution_for_all = None
|
||||||
|
try:
|
||||||
# Build set to exclude children (those get arranged together with the parents).
|
found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes)
|
||||||
included_as_child = set()
|
except: # If the thread crashes, the message should still close
|
||||||
for node in self._nodes:
|
Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")
|
||||||
included_as_child.update(node.getAllChildren())
|
|
||||||
|
|
||||||
# Collect nodes to be placed
|
|
||||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
|
||||||
for node in self._nodes:
|
|
||||||
if node in included_as_child:
|
|
||||||
continue
|
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True)
|
|
||||||
if offset_shape_arr is None:
|
|
||||||
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
|
|
||||||
continue
|
|
||||||
nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
|
|
||||||
|
|
||||||
# Sort the nodes with the biggest area first.
|
|
||||||
nodes_arr.sort(key=lambda item: item[0])
|
|
||||||
nodes_arr.reverse()
|
|
||||||
|
|
||||||
# Place nodes one at a time
|
|
||||||
start_priority = 0
|
|
||||||
last_priority = start_priority
|
|
||||||
last_size = None
|
|
||||||
grouped_operation = GroupedOperation()
|
|
||||||
found_solution_for_all = True
|
|
||||||
not_fit_count = 0
|
|
||||||
for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
|
|
||||||
# For performance reasons, we assume that when a location does not fit,
|
|
||||||
# it will also not fit for the next object (while what can be untrue).
|
|
||||||
if last_size == size: # This optimization works if many of the objects have the same size
|
|
||||||
start_priority = last_priority
|
|
||||||
else:
|
|
||||||
start_priority = 0
|
|
||||||
best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority)
|
|
||||||
x, y = best_spot.x, best_spot.y
|
|
||||||
node.removeDecorator(ZOffsetDecorator)
|
|
||||||
if node.getBoundingBox():
|
|
||||||
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
|
|
||||||
else:
|
|
||||||
center_y = 0
|
|
||||||
if x is not None: # We could find a place
|
|
||||||
last_size = size
|
|
||||||
last_priority = best_spot.priority
|
|
||||||
|
|
||||||
arranger.place(x, y, offset_shape_arr) # take place before the next one
|
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
|
|
||||||
else:
|
|
||||||
Logger.log("d", "Arrange all: could not find spot!")
|
|
||||||
found_solution_for_all = False
|
|
||||||
grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
|
|
||||||
not_fit_count += 1
|
|
||||||
|
|
||||||
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
|
||||||
Job.yieldThread()
|
|
||||||
QCoreApplication.processEvents()
|
|
||||||
|
|
||||||
grouped_operation.push()
|
|
||||||
|
|
||||||
status_message.hide()
|
status_message.hide()
|
||||||
|
if found_solution_for_all is not None and not found_solution_for_all:
|
||||||
if not found_solution_for_all:
|
no_full_solution_message = Message(
|
||||||
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
|
i18n_catalog.i18nc("@info:status",
|
||||||
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
|
"Unable to find a location within the build volume for all objects"),
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
|
||||||
no_full_solution_message.show()
|
no_full_solution_message.show()
|
||||||
|
|
||||||
self.finished.emit(self)
|
self.finished.emit(self)
|
||||||
|
147
cura/Arranging/Nest2DArrange.py
Normal file
147
cura/Arranging/Nest2DArrange.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
from pynest2d import Point, Box, Item, NfpConfig, nest
|
||||||
|
from typing import List, TYPE_CHECKING, Optional, Tuple
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Math.Matrix import Matrix
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
from UM.Math.Quaternion import Quaternion
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
from UM.Operations.RotateOperation import RotateOperation
|
||||||
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from cura.BuildVolume import BuildVolume
|
||||||
|
|
||||||
|
|
||||||
|
def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]:
|
||||||
|
"""
|
||||||
|
Find placement for a set of scene nodes, but don't actually move them just yet.
|
||||||
|
:param nodes_to_arrange: The list of nodes that need to be moved.
|
||||||
|
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
||||||
|
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
||||||
|
are placed.
|
||||||
|
:param factor: The library that we use is int based. This factor defines how accurate we want it to be.
|
||||||
|
|
||||||
|
:return: tuple (found_solution_for_all, node_items)
|
||||||
|
WHERE
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
machine_width = build_volume.getWidth()
|
||||||
|
machine_depth = build_volume.getDepth()
|
||||||
|
build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor)
|
||||||
|
|
||||||
|
if fixed_nodes is None:
|
||||||
|
fixed_nodes = []
|
||||||
|
|
||||||
|
# Add all the items we want to arrange
|
||||||
|
node_items = []
|
||||||
|
for node in nodes_to_arrange:
|
||||||
|
hull_polygon = node.callDecoration("getConvexHull")
|
||||||
|
if not hull_polygon or hull_polygon.getPoints is None:
|
||||||
|
Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
|
||||||
|
continue
|
||||||
|
converted_points = []
|
||||||
|
for point in hull_polygon.getPoints():
|
||||||
|
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
||||||
|
item = Item(converted_points)
|
||||||
|
node_items.append(item)
|
||||||
|
|
||||||
|
# Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
|
||||||
|
half_machine_width = 0.5 * machine_width - 1
|
||||||
|
half_machine_depth = 0.5 * machine_depth - 1
|
||||||
|
build_plate_polygon = Polygon(numpy.array([
|
||||||
|
[half_machine_width, -half_machine_depth],
|
||||||
|
[-half_machine_width, -half_machine_depth],
|
||||||
|
[-half_machine_width, half_machine_depth],
|
||||||
|
[half_machine_width, half_machine_depth]
|
||||||
|
], numpy.float32))
|
||||||
|
|
||||||
|
disallowed_areas = build_volume.getDisallowedAreas()
|
||||||
|
num_disallowed_areas_added = 0
|
||||||
|
for area in disallowed_areas:
|
||||||
|
converted_points = []
|
||||||
|
|
||||||
|
# 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
|
||||||
|
for point in clipped_area.getPoints():
|
||||||
|
converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
|
||||||
|
|
||||||
|
disallowed_area = Item(converted_points)
|
||||||
|
disallowed_area.markAsDisallowedAreaInBin(0)
|
||||||
|
node_items.append(disallowed_area)
|
||||||
|
num_disallowed_areas_added += 1
|
||||||
|
|
||||||
|
for node in fixed_nodes:
|
||||||
|
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
|
||||||
|
for point in hull_polygon.getPoints():
|
||||||
|
converted_points.append(Point(point[0] * factor, point[1] * factor))
|
||||||
|
item = Item(converted_points)
|
||||||
|
item.markAsFixedInBin(0)
|
||||||
|
node_items.append(item)
|
||||||
|
num_disallowed_areas_added += 1
|
||||||
|
|
||||||
|
config = NfpConfig()
|
||||||
|
config.accuracy = 1.0
|
||||||
|
|
||||||
|
num_bins = nest(node_items, build_plate_bounding_box, 10000, 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))
|
||||||
|
|
||||||
|
found_solution_for_all = num_bins == 1
|
||||||
|
|
||||||
|
return found_solution_for_all, node_items
|
||||||
|
|
||||||
|
|
||||||
|
def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Find placement for a set of scene nodes, and move them by using a single grouped operation.
|
||||||
|
:param nodes_to_arrange: The list of nodes that need to be moved.
|
||||||
|
:param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
|
||||||
|
:param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
|
||||||
|
are placed.
|
||||||
|
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
|
||||||
|
:param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations
|
||||||
|
|
||||||
|
:return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects
|
||||||
|
"""
|
||||||
|
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)
|
||||||
|
|
||||||
|
not_fit_count = 0
|
||||||
|
grouped_operation = GroupedOperation()
|
||||||
|
for node, node_item in zip(nodes_to_arrange, node_items):
|
||||||
|
if add_new_nodes_in_scene:
|
||||||
|
grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
|
||||||
|
|
||||||
|
if node_item.binId() == 0:
|
||||||
|
# We found a spot for it
|
||||||
|
rotation_matrix = Matrix()
|
||||||
|
rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
|
||||||
|
grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
|
||||||
|
grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0,
|
||||||
|
node_item.translation().y() / factor)))
|
||||||
|
else:
|
||||||
|
# We didn't find a spot
|
||||||
|
grouped_operation.addOperation(
|
||||||
|
TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
|
||||||
|
not_fit_count += 1
|
||||||
|
grouped_operation.push()
|
||||||
|
|
||||||
|
return found_solution_for_all
|
@ -64,9 +64,9 @@ class Backup:
|
|||||||
files = archive.namelist()
|
files = archive.namelist()
|
||||||
|
|
||||||
# Count the metadata items. We do this in a rather naive way at the moment.
|
# Count the metadata items. We do this in a rather naive way at the moment.
|
||||||
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
|
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 = len([s for s in files if "materials/" in s]) - 1
|
material_count = max(len([s for s in files if "materials/" in s]) - 1, 0)
|
||||||
profile_count = len([s for s in files if "quality_changes/" in s]) - 1
|
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])
|
plugin_count = len([s for s in files if "plugin.json" in s])
|
||||||
|
|
||||||
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
||||||
|
@ -180,12 +180,21 @@ class BuildVolume(SceneNode):
|
|||||||
def setWidth(self, width: float) -> None:
|
def setWidth(self, width: float) -> None:
|
||||||
self._width = width
|
self._width = width
|
||||||
|
|
||||||
|
def getWidth(self) -> float:
|
||||||
|
return self._width
|
||||||
|
|
||||||
def setHeight(self, height: float) -> None:
|
def setHeight(self, height: float) -> None:
|
||||||
self._height = height
|
self._height = height
|
||||||
|
|
||||||
|
def getHeight(self) -> float:
|
||||||
|
return self._height
|
||||||
|
|
||||||
def setDepth(self, depth: float) -> None:
|
def setDepth(self, depth: float) -> None:
|
||||||
self._depth = depth
|
self._depth = depth
|
||||||
|
|
||||||
|
def getDepth(self) -> float:
|
||||||
|
return self._depth
|
||||||
|
|
||||||
def setShape(self, shape: str) -> None:
|
def setShape(self, shape: str) -> None:
|
||||||
if shape:
|
if shape:
|
||||||
self._shape = shape
|
self._shape = shape
|
||||||
@ -781,7 +790,10 @@ class BuildVolume(SceneNode):
|
|||||||
if prime_tower_collision: # Already found a collision.
|
if prime_tower_collision: # Already found a collision.
|
||||||
break
|
break
|
||||||
if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
|
if self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
|
||||||
prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
brim_size = self._calculateBedAdhesionSize(used_extruders, "brim")
|
||||||
|
# Use 2x the brim size, since we need 1x brim size distance due to the object brim and another
|
||||||
|
# times the brim due to the brim of the prime tower
|
||||||
|
prime_tower_areas[extruder_id][area_index] = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(2 * brim_size, num_segments = 24))
|
||||||
if not prime_tower_collision:
|
if not prime_tower_collision:
|
||||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||||
@ -833,7 +845,7 @@ class BuildVolume(SceneNode):
|
|||||||
prime_tower_y += brim_size
|
prime_tower_y += brim_size
|
||||||
|
|
||||||
radius = prime_tower_size / 2
|
radius = prime_tower_size / 2
|
||||||
prime_tower_area = Polygon.approximatedCircle(radius)
|
prime_tower_area = Polygon.approximatedCircle(radius, num_segments = 24)
|
||||||
prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
|
prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
|
||||||
|
|
||||||
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
|
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
|
||||||
@ -1038,16 +1050,23 @@ class BuildVolume(SceneNode):
|
|||||||
all_values[i] = 0
|
all_values[i] = 0
|
||||||
return all_values
|
return all_values
|
||||||
|
|
||||||
def _calculateBedAdhesionSize(self, used_extruders):
|
def _calculateBedAdhesionSize(self, used_extruders, adhesion_override = None):
|
||||||
|
"""Get the bed adhesion size for the global container stack and used extruders
|
||||||
|
|
||||||
|
:param adhesion_override: override adhesion type.
|
||||||
|
Use None to use the global stack default, "none" for no adhesion, "brim" for brim etc.
|
||||||
|
"""
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
container_stack = self._global_container_stack
|
container_stack = self._global_container_stack
|
||||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
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 = self._global_container_stack.getProperty("skirt_brim_line_width", "value")
|
||||||
initial_layer_line_width_factor = self._global_container_stack.getProperty("initial_layer_line_width_factor", "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.
|
# Use brim width if brim is enabled OR the prime tower has a brim.
|
||||||
if adhesion_type == "brim" or (self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and adhesion_type != "raft"):
|
if adhesion_type == "brim":
|
||||||
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
||||||
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
|
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
|
||||||
|
|
||||||
@ -1056,7 +1075,7 @@ class BuildVolume(SceneNode):
|
|||||||
|
|
||||||
# We don't create an additional line for the extruder we're printing the brim with.
|
# We don't create an additional line for the extruder we're printing the brim with.
|
||||||
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
|
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
|
||||||
elif adhesion_type == "skirt": # No brim? Also not on prime tower? Then use whatever the adhesion type is saying: Skirt, raft or none.
|
elif adhesion_type == "skirt":
|
||||||
skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
|
skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
|
||||||
skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
|
skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
|
||||||
|
|
||||||
|
@ -250,7 +250,10 @@ class CrashHandler:
|
|||||||
|
|
||||||
scope.set_context("plugins", self.data["plugins"])
|
scope.set_context("plugins", self.data["plugins"])
|
||||||
|
|
||||||
scope.set_user({"id": str(uuid.getnode())})
|
user_id = uuid.getnode() # On all of Cura's supported platforms, this returns the MAC address which is pseudonymical information (!= anonymous).
|
||||||
|
user_id %= 2 ** 16 # So to make it anonymous, apply a bitmask selecting only the last 16 bits.
|
||||||
|
# This prevents it from being traceable to a specific user but still gives somewhat of an idea of whether it's just the same user hitting the same crash over and over again, or if it's widespread.
|
||||||
|
scope.set_user({"id": str(user_id)})
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class CuraActions(QObject):
|
|||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openBugReportPage(self) -> None:
|
def openBugReportPage(self) -> None:
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues/new/choose")], {})
|
||||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
|
@ -36,6 +36,7 @@ from UM.Scene.Camera import Camera
|
|||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Scene.SceneNodeSettings import SceneNodeSettings
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.ToolHandle import ToolHandle
|
from UM.Scene.ToolHandle import ToolHandle
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
@ -52,7 +53,7 @@ from cura.API.Account import Account
|
|||||||
from cura.Arranging.Arrange import Arrange
|
from cura.Arranging.Arrange import Arrange
|
||||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.Nest2DArrange import arrange
|
||||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||||
@ -126,7 +127,7 @@ class CuraApplication(QtApplication):
|
|||||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
# 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
|
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||||
# changes of the settings.
|
# changes of the settings.
|
||||||
SettingVersion = 15
|
SettingVersion = 16
|
||||||
|
|
||||||
Created = False
|
Created = False
|
||||||
|
|
||||||
@ -755,7 +756,7 @@ class CuraApplication(QtApplication):
|
|||||||
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
|
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
|
||||||
if not hasattr(sys, "frozen"):
|
if not hasattr(sys, "frozen"):
|
||||||
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
|
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()
|
self._plugin_registry.loadPlugins()
|
||||||
|
|
||||||
@ -801,6 +802,8 @@ class CuraApplication(QtApplication):
|
|||||||
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume..."))
|
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume..."))
|
||||||
root = self.getController().getScene().getRoot()
|
root = self.getController().getScene().getRoot()
|
||||||
self._volume = BuildVolume.BuildVolume(self, root)
|
self._volume = BuildVolume.BuildVolume(self, root)
|
||||||
|
|
||||||
|
# Ensure that the old style arranger still works.
|
||||||
Arrange.build_volume = self._volume
|
Arrange.build_volume = self._volume
|
||||||
|
|
||||||
# initialize info objects
|
# initialize info objects
|
||||||
@ -1379,6 +1382,7 @@ class CuraApplication(QtApplication):
|
|||||||
def arrangeAll(self) -> None:
|
def arrangeAll(self) -> None:
|
||||||
nodes_to_arrange = []
|
nodes_to_arrange = []
|
||||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
locked_nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
@ -1400,8 +1404,12 @@ class CuraApplication(QtApplication):
|
|||||||
# Skip nodes that are too big
|
# Skip nodes that are too big
|
||||||
bounding_box = node.getBoundingBox()
|
bounding_box = node.getBoundingBox()
|
||||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||||
nodes_to_arrange.append(node)
|
# Arrange only the unlocked nodes and keep the locked ones in place
|
||||||
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)):
|
||||||
|
locked_nodes.append(node)
|
||||||
|
else:
|
||||||
|
nodes_to_arrange.append(node)
|
||||||
|
self.arrange(nodes_to_arrange, locked_nodes)
|
||||||
|
|
||||||
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
||||||
"""Arrange a set of nodes given a set of fixed nodes
|
"""Arrange a set of nodes given a set of fixed nodes
|
||||||
@ -1514,13 +1522,10 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
# Move each node to the same position.
|
# Move each node to the same position.
|
||||||
for mesh, node in zip(meshes, group_node.getChildren()):
|
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||||
transformation = node.getLocalTransformation()
|
node.setTransformation(Matrix())
|
||||||
transformation.setTranslation(zero_translation)
|
|
||||||
transformed_mesh = mesh.getTransformed(transformation)
|
|
||||||
|
|
||||||
# Align the object around its zero position
|
# Align the object around its zero position
|
||||||
# and also apply the offset to center it inside the group.
|
# and also apply the offset to center it inside the group.
|
||||||
node.setPosition(-transformed_mesh.getZeroPosition() - offset)
|
node.setPosition(-mesh.getZeroPosition() - offset)
|
||||||
|
|
||||||
# Use the previously found center of the group bounding box as the new location of the group
|
# Use the previously found center of the group bounding box as the new location of the group
|
||||||
group_node.setPosition(group_node.getBoundingBox().center)
|
group_node.setPosition(group_node.getBoundingBox().center)
|
||||||
@ -1725,7 +1730,7 @@ class CuraApplication(QtApplication):
|
|||||||
:param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model"
|
: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.
|
or "open_as_project". This parameter is only considered if the file is a project file.
|
||||||
"""
|
"""
|
||||||
|
Logger.log("i", "Attempting to read file %s", file.toString())
|
||||||
if not file.isValid():
|
if not file.isValid():
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -1799,6 +1804,9 @@ class CuraApplication(QtApplication):
|
|||||||
return
|
return
|
||||||
|
|
||||||
nodes = job.getResult()
|
nodes = job.getResult()
|
||||||
|
if nodes is None:
|
||||||
|
Logger.error("Read mesh job returned None. Mesh loading must have failed.")
|
||||||
|
return
|
||||||
file_name = job.getFileName()
|
file_name = job.getFileName()
|
||||||
file_name_lower = file_name.lower()
|
file_name_lower = file_name.lower()
|
||||||
file_extension = file_name_lower.split(".")[-1]
|
file_extension = file_name_lower.split(".")[-1]
|
||||||
@ -1812,17 +1820,21 @@ class CuraApplication(QtApplication):
|
|||||||
for node_ in DepthFirstIterator(root):
|
for node_ in DepthFirstIterator(root):
|
||||||
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||||
fixed_nodes.append(node_)
|
fixed_nodes.append(node_)
|
||||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
|
||||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
|
||||||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
|
|
||||||
min_offset = 8
|
|
||||||
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
default_extruder_position = self.getMachineManager().defaultExtruderPosition
|
||||||
default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
|
default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
|
||||||
|
|
||||||
select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
|
select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
|
||||||
|
|
||||||
for original_node in nodes:
|
nodes_to_arrange = [] # type: List[CuraSceneNode]
|
||||||
|
|
||||||
|
fixed_nodes = []
|
||||||
|
for node_ in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
|
# Only count sliceable objects
|
||||||
|
if node_.callDecoration("isSliceable"):
|
||||||
|
fixed_nodes.append(node_)
|
||||||
|
|
||||||
|
for original_node in nodes:
|
||||||
# Create a CuraSceneNode just if the original node is not that type
|
# Create a CuraSceneNode just if the original node is not that type
|
||||||
if isinstance(original_node, CuraSceneNode):
|
if isinstance(original_node, CuraSceneNode):
|
||||||
node = original_node
|
node = original_node
|
||||||
@ -1830,8 +1842,8 @@ class CuraApplication(QtApplication):
|
|||||||
node = CuraSceneNode()
|
node = CuraSceneNode()
|
||||||
node.setMeshData(original_node.getMeshData())
|
node.setMeshData(original_node.getMeshData())
|
||||||
|
|
||||||
#Setting meshdata does not apply scaling.
|
# Setting meshdata does not apply scaling.
|
||||||
if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
|
if original_node.getScale() != Vector(1.0, 1.0, 1.0):
|
||||||
node.scale(original_node.getScale())
|
node.scale(original_node.getScale())
|
||||||
|
|
||||||
node.setSelectable(True)
|
node.setSelectable(True)
|
||||||
@ -1862,19 +1874,15 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
if file_extension != "3mf":
|
if file_extension != "3mf":
|
||||||
if node.callDecoration("isSliceable"):
|
if node.callDecoration("isSliceable"):
|
||||||
# Only check position if it's not already blatantly obvious that it won't fit.
|
# Ensure that the bottom of the bounding box is on the build plate
|
||||||
if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
if node.getBoundingBox():
|
||||||
# Find node location
|
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
|
else:
|
||||||
|
center_y = 0
|
||||||
|
|
||||||
# If a model is to small then it will not contain any points
|
node.translate(Vector(0, center_y, 0))
|
||||||
if offset_shape_arr is None and hull_shape_arr is None:
|
|
||||||
Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
|
|
||||||
title = self._i18n_catalog.i18nc("@info:title", "Warning")).show()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
|
nodes_to_arrange.append(node)
|
||||||
arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
|
|
||||||
|
|
||||||
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
|
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
|
||||||
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
|
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
|
||||||
@ -1893,7 +1901,10 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
if select_models_on_load:
|
if select_models_on_load:
|
||||||
Selection.add(node)
|
Selection.add(node)
|
||||||
|
try:
|
||||||
|
arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
|
||||||
|
except:
|
||||||
|
Logger.logException("e", "Failed to arrange the models")
|
||||||
self.fileCompleted.emit(file_name)
|
self.fileCompleted.emit(file_name)
|
||||||
|
|
||||||
def addNonSliceableExtension(self, extension):
|
def addNonSliceableExtension(self, extension):
|
||||||
|
@ -9,4 +9,5 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
|||||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||||
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"
|
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"
|
||||||
|
CuraDigitalFactoryURL = "@CURA_DIGITAL_FACTORY_URL@"
|
||||||
|
@ -171,7 +171,7 @@ class ContainerTree:
|
|||||||
|
|
||||||
The ``JobQueue`` will schedule this on a different thread.
|
The ``JobQueue`` will schedule this on a different thread.
|
||||||
"""
|
"""
|
||||||
|
Logger.log("d", "Started background loading of MachineNodes")
|
||||||
for stack in self.container_stacks: # Load all currently-added containers.
|
for stack in self.container_stacks: # Load all currently-added containers.
|
||||||
if not isinstance(stack, GlobalStack):
|
if not isinstance(stack, GlobalStack):
|
||||||
continue
|
continue
|
||||||
@ -182,3 +182,4 @@ class ContainerTree:
|
|||||||
definition_id = stack.definition.getId()
|
definition_id = stack.definition.getId()
|
||||||
if not self.tree_root.machines.is_loaded(definition_id):
|
if not self.tree_root.machines.is_loaded(definition_id):
|
||||||
_ = self.tree_root.machines[definition_id]
|
_ = self.tree_root.machines[definition_id]
|
||||||
|
Logger.log("d", "All MachineNode loading completed")
|
@ -135,9 +135,7 @@ class MachineNode(ContainerNode):
|
|||||||
groups_by_name[name] = QualityChangesGroup(name, quality_type = quality_changes["quality_type"],
|
groups_by_name[name] = QualityChangesGroup(name, quality_type = quality_changes["quality_type"],
|
||||||
intent_category = quality_changes.get("intent_category", "default"),
|
intent_category = quality_changes.get("intent_category", "default"),
|
||||||
parent = CuraApplication.getInstance())
|
parent = CuraApplication.getInstance())
|
||||||
# CURA-6882
|
|
||||||
# Custom qualities are always available, even if they are based on the "not supported" profile.
|
|
||||||
groups_by_name[name].is_available = True
|
|
||||||
elif groups_by_name[name].intent_category == "default": # Intent category should be stored as "default" if everything is default or as the intent if any of the extruder have an actual intent.
|
elif groups_by_name[name].intent_category == "default": # Intent category should be stored as "default" if everything is default or as the intent if any of the extruder have an actual intent.
|
||||||
groups_by_name[name].intent_category = quality_changes.get("intent_category", "default")
|
groups_by_name[name].intent_category = quality_changes.get("intent_category", "default")
|
||||||
|
|
||||||
@ -146,6 +144,18 @@ class MachineNode(ContainerNode):
|
|||||||
else: # Global profile.
|
else: # Global profile.
|
||||||
groups_by_name[name].metadata_for_global = quality_changes
|
groups_by_name[name].metadata_for_global = quality_changes
|
||||||
|
|
||||||
|
quality_groups = self.getQualityGroups(variant_names, material_bases, extruder_enabled)
|
||||||
|
for quality_changes_group in groups_by_name.values():
|
||||||
|
if quality_changes_group.quality_type not in quality_groups:
|
||||||
|
if quality_changes_group.quality_type == "not_supported":
|
||||||
|
# Quality changes based on an empty profile are always available.
|
||||||
|
quality_changes_group.is_available = True
|
||||||
|
else:
|
||||||
|
quality_changes_group.is_available = False
|
||||||
|
else:
|
||||||
|
# Quality changes group is available iff the quality group it depends on is available. Irrespective of whether the intent category is available.
|
||||||
|
quality_changes_group.is_available = quality_groups[quality_changes_group.quality_type].is_available
|
||||||
|
|
||||||
return list(groups_by_name.values())
|
return list(groups_by_name.values())
|
||||||
|
|
||||||
def preferredGlobalQuality(self) -> "QualityNode":
|
def preferredGlobalQuality(self) -> "QualityNode":
|
||||||
|
@ -72,8 +72,8 @@ class GlobalStacksModel(ListModel):
|
|||||||
section_name = self._catalog.i18nc("@info:title", section_name)
|
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||||
|
|
||||||
default_removal_warning = self._catalog.i18nc(
|
default_removal_warning = self._catalog.i18nc(
|
||||||
"@label ({} is object name)",
|
"@label {0} is the name of a printer that's about to be deleted.",
|
||||||
"Are you sure you wish to remove {}? This cannot be undone!", device_name
|
"Are you sure you wish to remove {0}? This cannot be undone!", device_name
|
||||||
)
|
)
|
||||||
removal_warning = container_stack.getMetaDataEntry("removal_warning", default_removal_warning)
|
removal_warning = container_stack.getMetaDataEntry("removal_warning", default_removal_warning)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Any, cast, Dict, Optional, TYPE_CHECKING
|
from typing import Any, cast, Dict, Optional, TYPE_CHECKING
|
||||||
@ -132,7 +132,7 @@ class QualityManagementModel(ListModel):
|
|||||||
for metadata in quality_changes_group.metadata_per_extruder.values():
|
for metadata in quality_changes_group.metadata_per_extruder.values():
|
||||||
extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0])
|
extruder_container = cast(InstanceContainer, container_registry.findContainers(id = metadata["id"])[0])
|
||||||
extruder_container.setName(new_name)
|
extruder_container.setName(new_name)
|
||||||
global_container = cast(InstanceContainer, container_registry.findContainers(id=quality_changes_group.metadata_for_global["id"])[0])
|
global_container = cast(InstanceContainer, container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"])[0])
|
||||||
global_container.setName(new_name)
|
global_container.setName(new_name)
|
||||||
|
|
||||||
quality_changes_group.name = new_name
|
quality_changes_group.name = new_name
|
||||||
@ -164,10 +164,16 @@ class QualityManagementModel(ListModel):
|
|||||||
quality_group = quality_model_item["quality_group"]
|
quality_group = quality_model_item["quality_group"]
|
||||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||||
if quality_changes_group is None:
|
if quality_changes_group is None:
|
||||||
# Create global quality changes only.
|
|
||||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
|
new_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category, new_name,
|
||||||
global_stack, extruder_stack = None)
|
global_stack, extruder_stack = None)
|
||||||
container_registry.addContainer(new_quality_changes)
|
container_registry.addContainer(new_quality_changes)
|
||||||
|
|
||||||
|
for extruder in global_stack.extruderList:
|
||||||
|
new_extruder_quality_changes = self._createQualityChanges(quality_group.quality_type, intent_category,
|
||||||
|
new_name,
|
||||||
|
global_stack, extruder_stack = extruder)
|
||||||
|
|
||||||
|
container_registry.addContainer(new_extruder_quality_changes)
|
||||||
else:
|
else:
|
||||||
for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
|
for metadata in [quality_changes_group.metadata_for_global] + list(quality_changes_group.metadata_per_extruder.values()):
|
||||||
containers = container_registry.findContainers(id = metadata["id"])
|
containers = container_registry.findContainers(id = metadata["id"])
|
||||||
@ -333,6 +339,7 @@ class QualityManagementModel(ListModel):
|
|||||||
"layer_height": layer_height, # layer_height is only used for sorting
|
"layer_height": layer_height, # layer_height is only used for sorting
|
||||||
}
|
}
|
||||||
item_list.append(item)
|
item_list.append(item)
|
||||||
|
|
||||||
# Sort by layer_height for built-in qualities
|
# Sort by layer_height for built-in qualities
|
||||||
item_list = sorted(item_list, key = lambda x: x["layer_height"])
|
item_list = sorted(item_list, key = lambda x: x["layer_height"])
|
||||||
|
|
||||||
@ -341,6 +348,9 @@ class QualityManagementModel(ListModel):
|
|||||||
available_intent_list = [i for i in available_intent_list if i[0] != "default"]
|
available_intent_list = [i for i in available_intent_list if i[0] != "default"]
|
||||||
result = []
|
result = []
|
||||||
for intent_category, quality_type in available_intent_list:
|
for intent_category, quality_type in available_intent_list:
|
||||||
|
if not quality_group_dict[quality_type].is_available:
|
||||||
|
continue
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
"name": quality_group_dict[quality_type].name, # Use the quality name as the display name
|
"name": quality_group_dict[quality_type].name, # Use the quality name as the display name
|
||||||
"is_read_only": True,
|
"is_read_only": True,
|
||||||
@ -361,6 +371,9 @@ class QualityManagementModel(ListModel):
|
|||||||
# CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
|
# CURA-6913 Note that custom qualities can be based on "not supported", so the quality group can be None.
|
||||||
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
|
quality_group = quality_group_dict.get(quality_changes_group.quality_type)
|
||||||
quality_type = quality_changes_group.quality_type
|
quality_type = quality_changes_group.quality_type
|
||||||
|
|
||||||
|
if not quality_changes_group.is_available:
|
||||||
|
continue
|
||||||
item = {"name": quality_changes_group.name,
|
item = {"name": quality_changes_group.name,
|
||||||
"is_read_only": False,
|
"is_read_only": False,
|
||||||
"quality_group": quality_group,
|
"quality_group": quality_group,
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
|
from UM import i18nCatalog
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class QualitySettingsModel(ListModel):
|
class QualitySettingsModel(ListModel):
|
||||||
"""This model is used to show details settings of the selected quality in the quality management page."""
|
"""This model is used to show details settings of the selected quality in the quality management page."""
|
||||||
@ -81,6 +84,12 @@ class QualitySettingsModel(ListModel):
|
|||||||
global_container_stack = self._application.getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
definition_container = global_container_stack.definition
|
definition_container = global_container_stack.definition
|
||||||
|
|
||||||
|
# Try and find a translation catalog for the definition
|
||||||
|
for file_name in definition_container.getInheritedFiles():
|
||||||
|
catalog = i18nCatalog(os.path.basename(file_name))
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
quality_group = self._selected_quality_item["quality_group"]
|
quality_group = self._selected_quality_item["quality_group"]
|
||||||
quality_changes_group = self._selected_quality_item["quality_changes_group"]
|
quality_changes_group = self._selected_quality_item["quality_changes_group"]
|
||||||
|
|
||||||
|
@ -4,21 +4,16 @@
|
|||||||
import copy
|
import copy
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication
|
from UM.Application import Application
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.Arranging.Nest2DArrange import arrange
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.Arranging.Arrange import Arrange
|
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
|
||||||
|
|
||||||
|
|
||||||
class MultiplyObjectsJob(Job):
|
class MultiplyObjectsJob(Job):
|
||||||
def __init__(self, objects, count, min_offset = 8):
|
def __init__(self, objects, count, min_offset = 8):
|
||||||
@ -28,28 +23,27 @@ class MultiplyObjectsJob(Job):
|
|||||||
self._min_offset = min_offset
|
self._min_offset = min_offset
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
|
||||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
dismissable = False, progress = 0,
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
scene = Application.getInstance().getController().getScene()
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
total_progress = len(self._objects) * self._count
|
|
||||||
current_progress = 0
|
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack is None:
|
if global_container_stack is None:
|
||||||
return # We can't do anything in this case.
|
return # We can't do anything in this case.
|
||||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
|
||||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
|
||||||
|
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
scale = 0.5
|
|
||||||
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
|
||||||
processed_nodes = [] # type: List[SceneNode]
|
processed_nodes = [] # type: List[SceneNode]
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
not_fit_count = 0
|
fixed_nodes = []
|
||||||
found_solution_for_all = False
|
for node_ in DepthFirstIterator(root):
|
||||||
|
# Only count sliceable objects
|
||||||
|
if node_.callDecoration("isSliceable"):
|
||||||
|
fixed_nodes.append(node_)
|
||||||
|
|
||||||
for node in self._objects:
|
for node in self._objects:
|
||||||
# If object is part of a group, multiply group
|
# If object is part of a group, multiply group
|
||||||
current_node = node
|
current_node = node
|
||||||
@ -60,31 +54,8 @@ class MultiplyObjectsJob(Job):
|
|||||||
continue
|
continue
|
||||||
processed_nodes.append(current_node)
|
processed_nodes.append(current_node)
|
||||||
|
|
||||||
node_too_big = False
|
|
||||||
if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth:
|
|
||||||
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale)
|
|
||||||
else:
|
|
||||||
node_too_big = True
|
|
||||||
|
|
||||||
found_solution_for_all = True
|
|
||||||
arranger.resetLastPriority()
|
|
||||||
for _ in range(self._count):
|
for _ in range(self._count):
|
||||||
# We do place the nodes one by one, as we want to yield in between.
|
|
||||||
new_node = copy.deepcopy(node)
|
new_node = copy.deepcopy(node)
|
||||||
solution_found = False
|
|
||||||
if not node_too_big:
|
|
||||||
if offset_shape_arr is not None and hull_shape_arr is not None:
|
|
||||||
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
|
||||||
else:
|
|
||||||
# The node has no shape, so no need to arrange it. The solution is simple: Do nothing.
|
|
||||||
solution_found = True
|
|
||||||
|
|
||||||
if node_too_big or not solution_found:
|
|
||||||
found_solution_for_all = False
|
|
||||||
new_location = new_node.getPosition()
|
|
||||||
new_location = new_location.set(z = - not_fit_count * 20)
|
|
||||||
new_node.setPosition(new_location)
|
|
||||||
not_fit_count += 1
|
|
||||||
|
|
||||||
# Same build plate
|
# Same build plate
|
||||||
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
|
||||||
@ -93,20 +64,15 @@ class MultiplyObjectsJob(Job):
|
|||||||
child.callDecoration("setBuildPlateNumber", build_plate_number)
|
child.callDecoration("setBuildPlateNumber", build_plate_number)
|
||||||
|
|
||||||
nodes.append(new_node)
|
nodes.append(new_node)
|
||||||
current_progress += 1
|
|
||||||
status_message.setProgress((current_progress / total_progress) * 100)
|
|
||||||
QCoreApplication.processEvents()
|
|
||||||
Job.yieldThread()
|
|
||||||
QCoreApplication.processEvents()
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
|
found_solution_for_all = True
|
||||||
if nodes:
|
if nodes:
|
||||||
operation = GroupedOperation()
|
found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes,
|
||||||
for new_node in nodes:
|
factor = 10000, add_new_nodes_in_scene = True)
|
||||||
operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
|
||||||
operation.push()
|
|
||||||
status_message.hide()
|
status_message.hide()
|
||||||
|
|
||||||
if not found_solution_for_all:
|
if not found_solution_for_all:
|
||||||
no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object"))
|
no_full_solution_message = Message(
|
||||||
|
i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Placing Object"))
|
||||||
no_full_solution_message.show()
|
no_full_solution_message.show()
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from typing import Optional
|
from typing import Optional, Any, Dict, Tuple
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin
|
|||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationHelpers:
|
class AuthorizationHelpers:
|
||||||
"""Class containing several helpers to deal with the authorization flow."""
|
"""Class containing several helpers to deal with the authorization flow."""
|
||||||
|
|
||||||
@ -121,10 +122,13 @@ class AuthorizationHelpers:
|
|||||||
if not user_data or not isinstance(user_data, dict):
|
if not user_data or not isinstance(user_data, dict):
|
||||||
Logger.log("w", "Could not parse user data from token: %s", user_data)
|
Logger.log("w", "Could not parse user data from token: %s", user_data)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return UserProfile(
|
return UserProfile(
|
||||||
user_id = user_data["user_id"],
|
user_id = user_data["user_id"],
|
||||||
username = user_data["username"],
|
username = user_data["username"],
|
||||||
profile_image_url = user_data.get("profile_image_url", "")
|
profile_image_url = user_data.get("profile_image_url", ""),
|
||||||
|
organization_id = user_data.get("organization", {}).get("organization_id"),
|
||||||
|
subscriptions = user_data.get("subscriptions", [])
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
|
||||||
class BaseModel:
|
class BaseModel:
|
||||||
@ -27,6 +27,8 @@ class UserProfile(BaseModel):
|
|||||||
user_id = None # type: Optional[str]
|
user_id = None # type: Optional[str]
|
||||||
username = None # type: Optional[str]
|
username = None # type: Optional[str]
|
||||||
profile_image_url = None # type: Optional[str]
|
profile_image_url = None # type: Optional[str]
|
||||||
|
organization_id = None # type: Optional[str]
|
||||||
|
subscriptions = None # type: Optional[List[Dict[str, Any]]]
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationResponse(BaseModel):
|
class AuthenticationResponse(BaseModel):
|
||||||
|
@ -27,10 +27,13 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||||||
if not issubclass(type(node), SceneNode):
|
if not issubclass(type(node), SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Node can't be printed, so don't bother sending it.
|
||||||
|
if getattr(node, "_outside_buildarea", False):
|
||||||
|
continue
|
||||||
|
|
||||||
if node.callDecoration("getConvexHull"):
|
if node.callDecoration("getConvexHull"):
|
||||||
node_list.append(node)
|
node_list.append(node)
|
||||||
|
|
||||||
|
|
||||||
if len(node_list) < 2:
|
if len(node_list) < 2:
|
||||||
self._node_stack = node_list[:]
|
self._node_stack = node_list[:]
|
||||||
return
|
return
|
||||||
@ -38,8 +41,8 @@ class OneAtATimeIterator(Iterator.Iterator):
|
|||||||
# Copy the list
|
# Copy the list
|
||||||
self._original_node_list = node_list[:]
|
self._original_node_list = node_list[:]
|
||||||
|
|
||||||
## Initialise the hit map (pre-compute all hits between all objects)
|
# Initialise the hit map (pre-compute all hits between all objects)
|
||||||
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
|
self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list]
|
||||||
|
|
||||||
# Check if we have to files that block each other. If this is the case, there is no solution!
|
# Check if we have to files that block each other. If this is the case, there is no solution!
|
||||||
for a in range(0, len(node_list)):
|
for a in range(0, len(node_list)):
|
||||||
|
@ -76,8 +76,8 @@ class PreviewPass(RenderPass):
|
|||||||
Logger.error("Unable to compile shader program: overhang.shader")
|
Logger.error("Unable to compile shader program: overhang.shader")
|
||||||
|
|
||||||
if not self._non_printing_shader:
|
if not self._non_printing_shader:
|
||||||
|
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
||||||
if self._non_printing_shader:
|
if self._non_printing_shader:
|
||||||
self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
|
||||||
self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5])
|
self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5])
|
||||||
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
|
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
|
||||||
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
|
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
@ -50,8 +49,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||||
|
|
||||||
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
controller = CuraApplication.getInstance().getController()
|
||||||
CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
controller.toolOperationStarted.connect(self._onChanged)
|
||||||
|
controller.toolOperationStopped.connect(self._onChanged)
|
||||||
|
#CuraApplication.getInstance().sceneBoundingBoxChanged.connect(self._onChanged)
|
||||||
|
|
||||||
self._root = Application.getInstance().getController().getScene().getRoot()
|
self._root = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
|
||||||
@ -188,7 +189,6 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
|
|
||||||
def recomputeConvexHullDelayed(self) -> None:
|
def recomputeConvexHullDelayed(self) -> None:
|
||||||
"""The same as recomputeConvexHull, but using a timer if it was set."""
|
"""The same as recomputeConvexHull, but using a timer if it was set."""
|
||||||
|
|
||||||
if self._recompute_convex_hull_timer is not None:
|
if self._recompute_convex_hull_timer is not None:
|
||||||
self._recompute_convex_hull_timer.start()
|
self._recompute_convex_hull_timer.start()
|
||||||
else:
|
else:
|
||||||
@ -263,16 +263,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
return offset_hull
|
return offset_hull
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
convex_hull = Polygon([])
|
||||||
offset_hull = Polygon([])
|
offset_hull = Polygon([])
|
||||||
mesh = self._node.getMeshData()
|
mesh = self._node.getMeshData()
|
||||||
if mesh is None:
|
if mesh is None:
|
||||||
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
||||||
|
|
||||||
world_transform = self._node.getWorldTransformation(copy= False)
|
world_transform = self._node.getWorldTransformation(copy = True)
|
||||||
|
|
||||||
# Check the cache
|
# Check the cache
|
||||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||||
return self._2d_convex_hull_mesh_result
|
return self._offsetHull(self._2d_convex_hull_mesh_result)
|
||||||
|
|
||||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||||
# Don't use data below 0.
|
# Don't use data below 0.
|
||||||
@ -307,7 +308,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
# Store the result in the cache
|
# Store the result in the cache
|
||||||
self._2d_convex_hull_mesh = mesh
|
self._2d_convex_hull_mesh = mesh
|
||||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||||
self._2d_convex_hull_mesh_result = offset_hull
|
self._2d_convex_hull_mesh_result = convex_hull
|
||||||
|
|
||||||
return offset_hull
|
return offset_hull
|
||||||
|
|
||||||
@ -379,12 +380,41 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
:return: New Polygon instance that is offset with everything that
|
:return: New Polygon instance that is offset with everything that
|
||||||
influences the collision area.
|
influences the collision area.
|
||||||
"""
|
"""
|
||||||
|
# Shrinkage compensation.
|
||||||
|
if not self._global_stack: # Should never happen.
|
||||||
|
return convex_hull
|
||||||
|
scale_factor = self._global_stack.getProperty("material_shrinkage_percentage", "value") / 100.0
|
||||||
|
result = convex_hull
|
||||||
|
if scale_factor != 1.0 and not self.getNode().callDecoration("isGroup"):
|
||||||
|
center = None
|
||||||
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||||
|
# Find the root node that's placed in the scene; the root of the mesh group.
|
||||||
|
ancestor = self.getNode()
|
||||||
|
while ancestor.getParent() != self._root:
|
||||||
|
ancestor = ancestor.getParent()
|
||||||
|
center = ancestor.getBoundingBox().center
|
||||||
|
else:
|
||||||
|
# Find the bounding box of the entire scene, which is all one mesh group then.
|
||||||
|
aabb = None
|
||||||
|
for printed_node in self._root.getChildren():
|
||||||
|
if not printed_node.callDecoration("isSliceable") and not printed_node.callDecoration("isGroup"):
|
||||||
|
continue # Not a printed node.
|
||||||
|
if aabb is None:
|
||||||
|
aabb = printed_node.getBoundingBox()
|
||||||
|
else:
|
||||||
|
aabb = aabb + printed_node.getBoundingBox()
|
||||||
|
if aabb:
|
||||||
|
center = aabb.center
|
||||||
|
if center:
|
||||||
|
result = convex_hull.scale(scale_factor, [center.x, center.z]) # Yes, use Z instead of Y. Mixed conventions there with how the OpenGL coordinates are transmitted.
|
||||||
|
|
||||||
|
# Horizontal expansion.
|
||||||
horizontal_expansion = max(
|
horizontal_expansion = max(
|
||||||
self._getSettingProperty("xy_offset", "value"),
|
self._getSettingProperty("xy_offset", "value"),
|
||||||
self._getSettingProperty("xy_offset_layer_0", "value")
|
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Mold.
|
||||||
mold_width = 0
|
mold_width = 0
|
||||||
if self._getSettingProperty("mold_enabled", "value"):
|
if self._getSettingProperty("mold_enabled", "value"):
|
||||||
mold_width = self._getSettingProperty("mold_width", "value")
|
mold_width = self._getSettingProperty("mold_width", "value")
|
||||||
@ -396,14 +426,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
[hull_offset, hull_offset],
|
[hull_offset, hull_offset],
|
||||||
[hull_offset, -hull_offset]
|
[hull_offset, -hull_offset]
|
||||||
], numpy.float32))
|
], numpy.float32))
|
||||||
return convex_hull.getMinkowskiHull(expansion_polygon)
|
return result.getMinkowskiHull(expansion_polygon)
|
||||||
else:
|
else:
|
||||||
return convex_hull
|
return result
|
||||||
|
|
||||||
def _onChanged(self, *args) -> None:
|
def _onChanged(self, *args) -> None:
|
||||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||||
if not args or args[0] == self._node:
|
self.recomputeConvexHullDelayed()
|
||||||
self.recomputeConvexHullDelayed()
|
|
||||||
|
|
||||||
def _onGlobalStackChanged(self) -> None:
|
def _onGlobalStackChanged(self) -> None:
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
@ -469,7 +498,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||||||
"adhesion_type", "raft_margin", "print_sequence",
|
"adhesion_type", "raft_margin", "print_sequence",
|
||||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||||
|
|
||||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh", "material_shrinkage_percentage"}
|
||||||
"""Settings that change the convex hull.
|
"""Settings that change the convex hull.
|
||||||
|
|
||||||
If these settings change, the convex hull should be recalculated.
|
If these settings change, the convex hull should be recalculated.
|
||||||
|
@ -122,6 +122,8 @@ class ContainerManager(QObject):
|
|||||||
root_material.setMetaDataEntry(entry_name, entry_value)
|
root_material.setMetaDataEntry(entry_name, entry_value)
|
||||||
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
|
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
|
||||||
root_material.metaDataChanged.emit(root_material)
|
root_material.metaDataChanged.emit(root_material)
|
||||||
|
|
||||||
|
cura.CuraApplication.CuraApplication.getInstance().getMachineManager().updateUponMaterialMetadataChange()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
@ -343,6 +345,9 @@ class ContainerManager(QObject):
|
|||||||
# user changes are possibly added to make the current setup match the current enabled extruders
|
# user changes are possibly added to make the current setup match the current enabled extruders
|
||||||
machine_manager.correctExtruderSettings()
|
machine_manager.correctExtruderSettings()
|
||||||
|
|
||||||
|
# The Print Sequence should be changed to match the current setup
|
||||||
|
machine_manager.correctPrintSequence()
|
||||||
|
|
||||||
for container in send_emits_containers:
|
for container in send_emits_containers:
|
||||||
container.sendPostponedEmits()
|
container.sendPostponedEmits()
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import Any, cast, Dict, Optional, List, Union
|
from typing import Any, cast, Dict, Optional, List, Union, Tuple
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
@ -45,7 +45,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
self.containerAdded.connect(self._onContainerAdded)
|
self.containerAdded.connect(self._onContainerAdded)
|
||||||
|
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def addContainer(self, container: ContainerInterface) -> None:
|
def addContainer(self, container: ContainerInterface) -> bool:
|
||||||
"""Overridden from ContainerRegistry
|
"""Overridden from ContainerRegistry
|
||||||
|
|
||||||
Adds a container to the registry.
|
Adds a container to the registry.
|
||||||
@ -64,9 +64,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||||
if required_setting_version != actual_setting_version:
|
if required_setting_version != actual_setting_version:
|
||||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||||
return # Don't add.
|
return False # Don't add.
|
||||||
|
|
||||||
super().addContainer(container)
|
return super().addContainer(container)
|
||||||
|
|
||||||
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
||||||
"""Create a name that is not empty and unique
|
"""Create a name that is not empty and unique
|
||||||
@ -179,7 +179,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
"""Imports a profile from a file
|
"""Imports a profile from a file
|
||||||
|
|
||||||
:param file_name: The full path and filename of the profile to import.
|
:param file_name: The full path and filename of the profile to import.
|
||||||
:return: Dict with a 'status' key containing the string 'ok' or 'error',
|
:return: Dict with a 'status' key containing the string 'ok', 'warning' or 'error',
|
||||||
and a 'message' key containing a message for the user.
|
and a 'message' key containing a message for the user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -305,6 +305,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
|
|
||||||
# Import all profiles
|
# Import all profiles
|
||||||
profile_ids_added = [] # type: List[str]
|
profile_ids_added = [] # type: List[str]
|
||||||
|
additional_message = None
|
||||||
for profile_index, profile in enumerate(profile_or_list):
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
if profile_index == 0:
|
if profile_index == 0:
|
||||||
# This is assumed to be the global profile
|
# This is assumed to be the global profile
|
||||||
@ -323,18 +324,26 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
else: # More extruders in the imported file than in the machine.
|
else: # More extruders in the imported file than in the machine.
|
||||||
continue # Delete the additional profiles.
|
continue # Delete the additional profiles.
|
||||||
|
|
||||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
configuration_successful, message = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||||
if result is not None:
|
if configuration_successful:
|
||||||
# Remove any profiles that did got added.
|
additional_message = message
|
||||||
for profile_id in profile_ids_added:
|
else:
|
||||||
|
# Remove any profiles that were added.
|
||||||
|
for profile_id in profile_ids_added + [profile.getId()]:
|
||||||
self.removeContainer(profile_id)
|
self.removeContainer(profile_id)
|
||||||
|
if not message:
|
||||||
|
message = ""
|
||||||
return {"status": "error", "message": catalog.i18nc(
|
return {"status": "error", "message": catalog.i18nc(
|
||||||
"@info:status Don't translate the XML tag <filename>!",
|
"@info:status Don't translate the XML tag <filename>!",
|
||||||
"Failed to import profile from <filename>{0}</filename>:",
|
"Failed to import profile from <filename>{0}</filename>:",
|
||||||
file_name) + " " + result}
|
file_name) + " " + message}
|
||||||
profile_ids_added.append(profile.getId())
|
profile_ids_added.append(profile.getId())
|
||||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
result_status = "ok"
|
||||||
|
success_message = catalog.i18nc("@info:status", "Successfully imported profile {0}.", profile_or_list[0].getName())
|
||||||
|
if additional_message:
|
||||||
|
result_status = "warning"
|
||||||
|
success_message += additional_message
|
||||||
|
return {"status": result_status, "message": success_message}
|
||||||
|
|
||||||
# This message is throw when the profile reader doesn't find any profile in the file
|
# This message is throw when the profile reader doesn't find any profile in the file
|
||||||
return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
|
return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)}
|
||||||
@ -348,6 +357,34 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||||
self._connectUpgradedExtruderStacksToMachines()
|
self._connectUpgradedExtruderStacksToMachines()
|
||||||
|
|
||||||
|
@override(ContainerRegistry)
|
||||||
|
def loadAllMetadata(self) -> None:
|
||||||
|
super().loadAllMetadata()
|
||||||
|
self._cleanUpInvalidQualityChanges()
|
||||||
|
|
||||||
|
def _cleanUpInvalidQualityChanges(self) -> None:
|
||||||
|
# We've seen cases where it was possible for quality_changes to be incorrectly added. This is to ensure that
|
||||||
|
# any such leftovers are purged from the registry.
|
||||||
|
quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type="quality_changes")
|
||||||
|
|
||||||
|
profile_count_by_name = {} # type: Dict[str, int]
|
||||||
|
|
||||||
|
for quality_change in quality_changes:
|
||||||
|
name = str(quality_change.get("name", ""))
|
||||||
|
if name == "empty":
|
||||||
|
continue
|
||||||
|
if name not in profile_count_by_name:
|
||||||
|
profile_count_by_name[name] = 0
|
||||||
|
profile_count_by_name[name] += 1
|
||||||
|
|
||||||
|
for profile_name, profile_count in profile_count_by_name.items():
|
||||||
|
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)
|
||||||
|
self.removeContainer(invalid_quality_changes[0]["id"])
|
||||||
|
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
|
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
|
||||||
"""Check if the metadata for a container is okay before adding it.
|
"""Check if the metadata for a container is okay before adding it.
|
||||||
@ -367,14 +404,18 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
|
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Tuple[bool, Optional[str]]:
|
||||||
"""Update an imported profile to match the current machine configuration.
|
"""Update an imported profile to match the current machine configuration.
|
||||||
|
|
||||||
:param profile: The profile to configure.
|
:param profile: The profile to configure.
|
||||||
:param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
|
:param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
|
||||||
:param new_name: The new name for the profile.
|
:param new_name: The new name for the profile.
|
||||||
|
|
||||||
:return: None if configuring was successful or an error message if an error occurred.
|
:returns: tuple (configuration_successful, message)
|
||||||
|
WHERE
|
||||||
|
bool configuration_successful: Whether the process of configuring the profile was successful
|
||||||
|
optional str message: A message indicating the outcome of configuring the profile. If the configuration
|
||||||
|
is successful, this message can be None or contain a warning
|
||||||
"""
|
"""
|
||||||
|
|
||||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||||
@ -395,25 +436,39 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||||||
|
|
||||||
quality_type = profile.getMetaDataEntry("quality_type")
|
quality_type = profile.getMetaDataEntry("quality_type")
|
||||||
if not quality_type:
|
if not quality_type:
|
||||||
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
return False, catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||||
|
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if not global_stack:
|
||||||
return None
|
return False, catalog.i18nc("@info:status", "There is no active printer yet.")
|
||||||
|
|
||||||
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||||
profile.setDefinition(definition_id)
|
profile.setDefinition(definition_id)
|
||||||
|
|
||||||
|
if not self.addContainer(profile):
|
||||||
|
return False, catalog.i18nc("@info:status", "Unable to add the profile.")
|
||||||
|
|
||||||
|
# "not_supported" profiles can be imported.
|
||||||
|
if quality_type == empty_quality_container.getMetaDataEntry("quality_type"):
|
||||||
|
return True, None
|
||||||
|
|
||||||
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
||||||
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
||||||
# successfully imported but then fail to show up.
|
# successfully imported but then fail to show up.
|
||||||
quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups()
|
available_quality_groups_dict = {name: quality_group for name, quality_group in ContainerTree.getInstance().getCurrentQualityGroups().items() if quality_group.is_available}
|
||||||
# "not_supported" profiles can be imported.
|
all_quality_groups_dict = ContainerTree.getInstance().getCurrentQualityGroups()
|
||||||
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
|
|
||||||
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addContainer(profile)
|
# If the quality type doesn't exist at all in the quality_groups of this machine, reject the profile
|
||||||
|
if quality_type not in all_quality_groups_dict:
|
||||||
|
return False, catalog.i18nc("@info:status", "Quality type '{0}' is not compatible with the current active machine definition '{1}'.", quality_type, definition_id)
|
||||||
|
|
||||||
return None
|
# If the quality_type exists in the quality_groups of this printer but it is not available with the current
|
||||||
|
# machine configuration (e.g. not available for the selected nozzles), accept it with a warning
|
||||||
|
if quality_type not in available_quality_groups_dict:
|
||||||
|
return True, "\n\n" + catalog.i18nc("@info:status", "Warning: The profile is not visible because its quality type '{0}' is not available for the current configuration. "
|
||||||
|
"Switch to a material/nozzle combination that can use this quality type.", quality_type)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def saveDirtyContainers(self) -> None:
|
def saveDirtyContainers(self) -> None:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Any, cast, List, Optional
|
from typing import Any, cast, List, Optional, Dict
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
@ -60,6 +60,8 @@ class CuraContainerStack(ContainerStack):
|
|||||||
import cura.CuraApplication #Here to prevent circular imports.
|
import cura.CuraApplication #Here to prevent circular imports.
|
||||||
self.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
self.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
||||||
|
|
||||||
|
self._settable_per_extruder_cache = {} # type: Dict[str, Any]
|
||||||
|
|
||||||
self.setDirty(False)
|
self.setDirty(False)
|
||||||
|
|
||||||
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
||||||
@ -387,6 +389,18 @@ class CuraContainerStack(ContainerStack):
|
|||||||
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
value = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def getProperty(self, key: str, property_name: str, context = None) -> Any:
|
||||||
|
if property_name == "settable_per_extruder":
|
||||||
|
# Setable per extruder isn't a value that can ever change. So once we requested it once, we can just keep
|
||||||
|
# that in memory.
|
||||||
|
try:
|
||||||
|
return self._settable_per_extruder_cache[key]
|
||||||
|
except KeyError:
|
||||||
|
self._settable_per_extruder_cache[key] = super().getProperty(key, property_name, context)
|
||||||
|
return self._settable_per_extruder_cache[key]
|
||||||
|
|
||||||
|
return super().getProperty(key, property_name, context)
|
||||||
|
|
||||||
|
|
||||||
class _ContainerIndexes:
|
class _ContainerIndexes:
|
||||||
"""Private helper class to keep track of container positions and their types."""
|
"""Private helper class to keep track of container positions and their types."""
|
||||||
|
@ -16,13 +16,13 @@ from .ExtruderStack import ExtruderStack
|
|||||||
class CuraStackBuilder:
|
class CuraStackBuilder:
|
||||||
"""Contains helper functions to create new machines."""
|
"""Contains helper functions to create new machines."""
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
|
def createMachine(cls, name: str, definition_id: str, machine_extruder_count: Optional[int] = None) -> Optional[GlobalStack]:
|
||||||
"""Create a new instance of a machine.
|
"""Create a new instance of a machine.
|
||||||
|
|
||||||
:param name: The name of the new machine.
|
:param name: The name of the new machine.
|
||||||
:param definition_id: The ID of the machine definition to use.
|
:param definition_id: The ID of the machine definition to use.
|
||||||
|
:param machine_extruder_count: The number of extruders in the machine.
|
||||||
|
|
||||||
:return: The new global stack or None if an error occurred.
|
:return: The new global stack or None if an error occurred.
|
||||||
"""
|
"""
|
||||||
@ -62,10 +62,18 @@ class CuraStackBuilder:
|
|||||||
for position in extruder_dict:
|
for position in extruder_dict:
|
||||||
try:
|
try:
|
||||||
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
|
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
|
||||||
except IndexError:
|
except IndexError as e:
|
||||||
|
Logger.logException("e", "Failed to create an extruder stack for position {pos}: {err}".format(pos = position, err = str(e)))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for new_extruder in new_global_stack.extruderList: # Only register the extruders if we're sure that all of them are correct.
|
# If given, set the machine_extruder_count when creating the machine, or else the extruderList used bellow will
|
||||||
|
# not return the correct extruder list (since by default, the machine_extruder_count is 1) in machines with
|
||||||
|
# settable number of extruders.
|
||||||
|
if machine_extruder_count and 0 <= machine_extruder_count <= len(extruder_dict):
|
||||||
|
new_global_stack.setProperty("machine_extruder_count", "value", machine_extruder_count)
|
||||||
|
|
||||||
|
# Only register the extruders if we're sure that all of them are correct.
|
||||||
|
for new_extruder in new_global_stack.extruderList:
|
||||||
registry.addContainer(new_extruder)
|
registry.addContainer(new_extruder)
|
||||||
|
|
||||||
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
|
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another
|
||||||
|
@ -289,7 +289,7 @@ class ExtruderManager(QObject):
|
|||||||
return global_stack.getProperty("adhesion_extruder_nr", "value")
|
return global_stack.getProperty("adhesion_extruder_nr", "value")
|
||||||
|
|
||||||
# No adhesion? Well maybe there is still support brim.
|
# No adhesion? Well maybe there is still support brim.
|
||||||
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_tree_enable", "value")) and global_stack.getProperty("support_brim_enable", "value"):
|
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_structure", "value") == "tree") and global_stack.getProperty("support_brim_enable", "value"):
|
||||||
return global_stack.getProperty("support_infill_extruder_nr", "value")
|
return global_stack.getProperty("support_infill_extruder_nr", "value")
|
||||||
|
|
||||||
# REALLY no adhesion? Use the first used extruder.
|
# REALLY no adhesion? Use the first used extruder.
|
||||||
|
@ -131,13 +131,13 @@ class ExtruderStack(CuraContainerStack):
|
|||||||
if not self._next_stack:
|
if not self._next_stack:
|
||||||
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))
|
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))
|
||||||
|
|
||||||
if context is None:
|
if context:
|
||||||
context = PropertyEvaluationContext()
|
context.pushContainer(self)
|
||||||
context.pushContainer(self)
|
|
||||||
|
|
||||||
if not super().getProperty(key, "settable_per_extruder", context):
|
if not super().getProperty(key, "settable_per_extruder", context):
|
||||||
result = self.getNextStack().getProperty(key, property_name, context)
|
result = self.getNextStack().getProperty(key, property_name, context)
|
||||||
context.popContainer()
|
if context:
|
||||||
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
@ -150,13 +150,15 @@ class ExtruderStack(CuraContainerStack):
|
|||||||
try:
|
try:
|
||||||
result = self.getNextStack().extruderList[int(limit_to_extruder)].getProperty(key, property_name, context)
|
result = self.getNextStack().extruderList[int(limit_to_extruder)].getProperty(key, property_name, context)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
context.popContainer()
|
if context:
|
||||||
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
result = super().getProperty(key, property_name, context)
|
result = super().getProperty(key, property_name, context)
|
||||||
context.popContainer()
|
if context:
|
||||||
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@override(CuraContainerStack)
|
@override(CuraContainerStack)
|
||||||
|
@ -211,9 +211,8 @@ class GlobalStack(CuraContainerStack):
|
|||||||
if not self.definition.findDefinitions(key = key):
|
if not self.definition.findDefinitions(key = key):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if context is None:
|
if context:
|
||||||
context = PropertyEvaluationContext()
|
context.pushContainer(self)
|
||||||
context.pushContainer(self)
|
|
||||||
|
|
||||||
# Handle the "resolve" property.
|
# Handle the "resolve" property.
|
||||||
#TODO: Why the hell does this involve threading?
|
#TODO: Why the hell does this involve threading?
|
||||||
@ -238,13 +237,15 @@ class GlobalStack(CuraContainerStack):
|
|||||||
if super().getProperty(key, "settable_per_extruder", context):
|
if super().getProperty(key, "settable_per_extruder", context):
|
||||||
result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
|
result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
context.popContainer()
|
if context:
|
||||||
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)
|
Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)
|
||||||
|
|
||||||
result = super().getProperty(key, property_name, context)
|
result = super().getProperty(key, property_name, context)
|
||||||
context.popContainer()
|
if context:
|
||||||
|
context.popContainer()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@override(ContainerStack)
|
@override(ContainerStack)
|
||||||
@ -256,8 +257,6 @@ class GlobalStack(CuraContainerStack):
|
|||||||
|
|
||||||
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
||||||
|
|
||||||
# protected:
|
|
||||||
|
|
||||||
# Determine whether or not we should try to get the "resolve" property instead of the
|
# Determine whether or not we should try to get the "resolve" property instead of the
|
||||||
# requested property.
|
# requested property.
|
||||||
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
||||||
@ -265,6 +264,10 @@ class GlobalStack(CuraContainerStack):
|
|||||||
# Do not try to resolve anything but the "value" property
|
# Do not try to resolve anything but the "value" property
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if not self.definition.getProperty(key, "resolve"):
|
||||||
|
# If there isn't a resolve set for this setting, there isn't anything to do here.
|
||||||
|
return False
|
||||||
|
|
||||||
current_thread = threading.current_thread()
|
current_thread = threading.current_thread()
|
||||||
if key in self._resolving_settings[current_thread.name]:
|
if key in self._resolving_settings[current_thread.name]:
|
||||||
# To prevent infinite recursion, if getProperty is called with the same key as
|
# To prevent infinite recursion, if getProperty is called with the same key as
|
||||||
@ -273,10 +276,8 @@ class GlobalStack(CuraContainerStack):
|
|||||||
# track all settings that are being resolved.
|
# track all settings that are being resolved.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
setting_state = super().getProperty(key, "state", context = context)
|
if self.hasUserValue(key):
|
||||||
if setting_state is not None and setting_state != InstanceState.Default:
|
# When the user has explicitly set a value, we should ignore any resolve and just return that value.
|
||||||
# When the user has explicitly set a value, we should ignore any resolve and
|
|
||||||
# just return that value.
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -63,6 +63,7 @@ class MachineManager(QObject):
|
|||||||
self._current_root_material_id = {} # type: Dict[str, str]
|
self._current_root_material_id = {} # type: Dict[str, str]
|
||||||
|
|
||||||
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||||
|
self._num_user_settings = 0
|
||||||
|
|
||||||
self._instance_container_timer = QTimer() # type: QTimer
|
self._instance_container_timer = QTimer() # type: QTimer
|
||||||
self._instance_container_timer.setInterval(250)
|
self._instance_container_timer.setInterval(250)
|
||||||
@ -126,6 +127,9 @@ class MachineManager(QObject):
|
|||||||
self.activeQualityGroupChanged.connect(self.activeQualityDisplayNameChanged)
|
self.activeQualityGroupChanged.connect(self.activeQualityDisplayNameChanged)
|
||||||
self.activeQualityChangesGroupChanged.connect(self.activeQualityDisplayNameChanged)
|
self.activeQualityChangesGroupChanged.connect(self.activeQualityDisplayNameChanged)
|
||||||
|
|
||||||
|
self.activeStackValueChanged.connect(self._reCalculateNumUserSettings)
|
||||||
|
self.numberExtrudersEnabledChanged.connect(self.correctPrintSequence)
|
||||||
|
|
||||||
activeQualityDisplayNameChanged = pyqtSignal()
|
activeQualityDisplayNameChanged = pyqtSignal()
|
||||||
|
|
||||||
activeQualityGroupChanged = pyqtSignal()
|
activeQualityGroupChanged = pyqtSignal()
|
||||||
@ -151,6 +155,22 @@ class MachineManager(QObject):
|
|||||||
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
||||||
|
|
||||||
rootMaterialChanged = pyqtSignal()
|
rootMaterialChanged = pyqtSignal()
|
||||||
|
numUserSettingsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def _reCalculateNumUserSettings(self):
|
||||||
|
if not self._global_container_stack:
|
||||||
|
if self._num_user_settings != 0:
|
||||||
|
self.numUserSettingsChanged.emit()
|
||||||
|
self._num_user_settings = 0
|
||||||
|
return
|
||||||
|
num_user_settings = self._global_container_stack.getTop().getNumInstances()
|
||||||
|
stacks = self._global_container_stack.extruderList
|
||||||
|
for stack in stacks:
|
||||||
|
num_user_settings += stack.getTop().getNumInstances()
|
||||||
|
|
||||||
|
if self._num_user_settings != num_user_settings:
|
||||||
|
self._num_user_settings = num_user_settings
|
||||||
|
self.numUserSettingsChanged.emit()
|
||||||
|
|
||||||
def setInitialActiveMachine(self) -> None:
|
def setInitialActiveMachine(self) -> None:
|
||||||
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
||||||
@ -329,11 +349,16 @@ class MachineManager(QObject):
|
|||||||
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
|
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
|
||||||
extruder_manager.activeExtruderChanged.emit()
|
extruder_manager.activeExtruderChanged.emit()
|
||||||
|
|
||||||
|
self._validateVariantsAndMaterials(global_stack)
|
||||||
|
|
||||||
|
def _validateVariantsAndMaterials(self, global_stack) -> None:
|
||||||
# Validate if the machine has the correct variants and materials.
|
# Validate if the machine has the correct variants and materials.
|
||||||
# It can happen that a variant or material is empty, even though the machine has them. This will ensure that
|
# It can happen that a variant or material is empty, even though the machine has them. This will ensure that
|
||||||
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant or
|
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant or
|
||||||
# variant instead!)
|
# variant instead!)
|
||||||
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
||||||
|
if not self._global_container_stack:
|
||||||
|
return
|
||||||
for extruder in self._global_container_stack.extruderList:
|
for extruder in self._global_container_stack.extruderList:
|
||||||
variant_name = extruder.variant.getName()
|
variant_name = extruder.variant.getName()
|
||||||
variant_node = machine_node.variants.get(variant_name)
|
variant_node = machine_node.variants.get(variant_name)
|
||||||
@ -345,7 +370,9 @@ class MachineManager(QObject):
|
|||||||
material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
|
material_node = variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
|
||||||
if material_node is None:
|
if material_node is None:
|
||||||
Logger.log("w", "An extruder has an unknown material, switching it to the preferred material")
|
Logger.log("w", "An extruder has an unknown material, switching it to the preferred material")
|
||||||
self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material)
|
if not self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material):
|
||||||
|
Logger.log("w", "Failed to switch to %s keeping old material instead", machine_node.preferred_material)
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
|
def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
|
||||||
@ -409,31 +436,13 @@ class MachineManager(QObject):
|
|||||||
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
|
Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = activeStackValueChanged)
|
@pyqtProperty(bool, notify = numUserSettingsChanged)
|
||||||
def hasUserSettings(self) -> bool:
|
def hasUserSettings(self) -> bool:
|
||||||
"""Check if the global_container has instances in the user container"""
|
return self._num_user_settings != 0
|
||||||
|
|
||||||
if not self._global_container_stack:
|
@pyqtProperty(int, notify = numUserSettingsChanged)
|
||||||
return False
|
|
||||||
|
|
||||||
if self._global_container_stack.getTop().getNumInstances() != 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
for stack in self._global_container_stack.extruderList:
|
|
||||||
if stack.getTop().getNumInstances() != 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = activeStackValueChanged)
|
|
||||||
def numUserSettings(self) -> int:
|
def numUserSettings(self) -> int:
|
||||||
if not self._global_container_stack:
|
return self._num_user_settings
|
||||||
return 0
|
|
||||||
num_user_settings = self._global_container_stack.getTop().getNumInstances()
|
|
||||||
stacks = self._global_container_stack.extruderList
|
|
||||||
for stack in stacks:
|
|
||||||
num_user_settings += stack.getTop().getNumInstances()
|
|
||||||
return num_user_settings
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def clearUserSettingAllCurrentStacks(self, key: str) -> None:
|
def clearUserSettingAllCurrentStacks(self, key: str) -> None:
|
||||||
@ -584,14 +593,14 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||||
def activeQualityType(self) -> str:
|
def activeQualityType(self) -> str:
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return ""
|
return ""
|
||||||
return global_stack.quality.getMetaDataEntry("quality_type")
|
return global_stack.quality.getMetaDataEntry("quality_type")
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
def isActiveQualitySupported(self) -> bool:
|
def isActiveQualitySupported(self) -> bool:
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return False
|
return False
|
||||||
active_quality_group = self.activeQualityGroup()
|
active_quality_group = self.activeQualityGroup()
|
||||||
@ -599,9 +608,10 @@ class MachineManager(QObject):
|
|||||||
return False
|
return False
|
||||||
return active_quality_group.is_available
|
return active_quality_group.is_available
|
||||||
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
def isActiveQualityExperimental(self) -> bool:
|
def isActiveQualityExperimental(self) -> bool:
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return False
|
return False
|
||||||
active_quality_group = self.activeQualityGroup()
|
active_quality_group = self.activeQualityGroup()
|
||||||
@ -611,7 +621,7 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(str, notify = activeIntentChanged)
|
@pyqtProperty(str, notify = activeIntentChanged)
|
||||||
def activeIntentCategory(self) -> str:
|
def activeIntentCategory(self) -> str:
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return ""
|
return ""
|
||||||
@ -620,7 +630,7 @@ class MachineManager(QObject):
|
|||||||
# Provies a list of extruder positions that have a different intent from the active one.
|
# Provies a list of extruder positions that have a different intent from the active one.
|
||||||
@pyqtProperty("QStringList", notify=activeIntentChanged)
|
@pyqtProperty("QStringList", notify=activeIntentChanged)
|
||||||
def extruderPositionsWithNonActiveIntent(self):
|
def extruderPositionsWithNonActiveIntent(self):
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
|
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return []
|
return []
|
||||||
@ -689,7 +699,7 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
:returns: DefinitionID (string) if found, empty string otherwise
|
:returns: DefinitionID (string) if found, empty string otherwise
|
||||||
"""
|
"""
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return ""
|
return ""
|
||||||
return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||||
@ -817,11 +827,6 @@ class MachineManager(QObject):
|
|||||||
result = [] # type: List[str]
|
result = [] # type: List[str]
|
||||||
for setting_instance in container.findInstances():
|
for setting_instance in container.findInstances():
|
||||||
setting_key = setting_instance.definition.key
|
setting_key = setting_instance.definition.key
|
||||||
if setting_key == "print_sequence":
|
|
||||||
old_value = container.getProperty(setting_key, "value")
|
|
||||||
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
|
|
||||||
result.append(setting_key)
|
|
||||||
continue
|
|
||||||
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
|
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -853,6 +858,41 @@ class MachineManager(QObject):
|
|||||||
title = catalog.i18nc("@info:title", "Settings updated"))
|
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||||
caution_message.show()
|
caution_message.show()
|
||||||
|
|
||||||
|
def correctPrintSequence(self) -> None:
|
||||||
|
"""
|
||||||
|
Sets the Print Sequence setting to "all-at-once" when there are more than one enabled extruders.
|
||||||
|
|
||||||
|
This setting has to be explicitly changed whenever we have more than one enabled extruders to make sure that the
|
||||||
|
Cura UI is properly updated to reset all the UI elements changes that occur due to the one-at-a-time mode (such
|
||||||
|
as the reduced build volume, the different convex hulls of the objects etc.).
|
||||||
|
"""
|
||||||
|
|
||||||
|
setting_key = "print_sequence"
|
||||||
|
new_value = "all_at_once"
|
||||||
|
|
||||||
|
if self._global_container_stack is None \
|
||||||
|
or self._global_container_stack.getProperty(setting_key, "value") == new_value \
|
||||||
|
or self.numberExtrudersEnabled < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
user_changes_container = self._global_container_stack.userChanges
|
||||||
|
quality_changes_container = self._global_container_stack.qualityChanges
|
||||||
|
print_sequence_quality_changes = quality_changes_container.getProperty(setting_key, "value")
|
||||||
|
print_sequence_user_changes = user_changes_container.getProperty(setting_key, "value")
|
||||||
|
|
||||||
|
# If the user changes container has a value and its the incorrect value, then reset the setting in the user
|
||||||
|
# changes (so that the circular revert-changes arrow will now show up in the interface)
|
||||||
|
if print_sequence_user_changes and print_sequence_user_changes != new_value:
|
||||||
|
user_changes_container.removeInstance(setting_key)
|
||||||
|
Logger.log("d", "Resetting '{}' in container '{}' because there are more than 1 enabled extruders.".format(setting_key, user_changes_container))
|
||||||
|
# If the print sequence doesn't exist in either the user changes or the quality changes (yet it still has the
|
||||||
|
# wrong value in the global stack), or it exists in the quality changes and it has the wrong value, then set it
|
||||||
|
# in the user changes
|
||||||
|
elif (not print_sequence_quality_changes and not print_sequence_user_changes) \
|
||||||
|
or (print_sequence_quality_changes and print_sequence_quality_changes != new_value):
|
||||||
|
user_changes_container.setProperty(setting_key, "value", new_value)
|
||||||
|
Logger.log("d", "Setting '{}' in '{}' to '{}' because there are more than 1 enabled extruders.".format(setting_key, user_changes_container, new_value))
|
||||||
|
|
||||||
def setActiveMachineExtruderCount(self, extruder_count: int) -> None:
|
def setActiveMachineExtruderCount(self, extruder_count: int) -> None:
|
||||||
"""Set the amount of extruders on the active machine (global stack)
|
"""Set the amount of extruders on the active machine (global stack)
|
||||||
|
|
||||||
@ -960,11 +1000,10 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
property_names = ["value", "resolve", "validationState"]
|
||||||
property_names = ["value", "resolve", "validationState"]
|
for container in [self._global_container_stack] + self._global_container_stack.extruderList:
|
||||||
for container in [self._global_container_stack] + self._global_container_stack.extruderList:
|
for setting_key in container.getAllKeys():
|
||||||
for setting_key in container.getAllKeys():
|
container.propertiesChanged.emit(setting_key, property_names)
|
||||||
container.propertiesChanged.emit(setting_key, property_names)
|
|
||||||
|
|
||||||
@pyqtSlot(int, bool)
|
@pyqtSlot(int, bool)
|
||||||
def setExtruderEnabled(self, position: int, enabled: bool) -> None:
|
def setExtruderEnabled(self, position: int, enabled: bool) -> None:
|
||||||
@ -978,10 +1017,6 @@ class MachineManager(QObject):
|
|||||||
self.updateNumberExtrudersEnabled()
|
self.updateNumberExtrudersEnabled()
|
||||||
self.correctExtruderSettings()
|
self.correctExtruderSettings()
|
||||||
|
|
||||||
# In case this extruder is being disabled and it's the currently selected one, switch to the default extruder
|
|
||||||
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
|
||||||
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
|
||||||
|
|
||||||
# Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
# Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
self.extruderChanged.emit()
|
self.extruderChanged.emit()
|
||||||
@ -989,12 +1024,16 @@ class MachineManager(QObject):
|
|||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
# Update items in SettingExtruder
|
# Update items in SettingExtruder
|
||||||
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||||
# Make sure the front end reflects changes
|
|
||||||
self.forceUpdateAllSettings()
|
|
||||||
# Also trigger the build plate compatibility to update
|
# Also trigger the build plate compatibility to update
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
self.activeIntentChanged.emit()
|
self.activeIntentChanged.emit()
|
||||||
|
|
||||||
|
# Force an update of resolve values
|
||||||
|
property_names = ["resolve", "validationState"]
|
||||||
|
for setting_key in self._global_container_stack.getAllKeys():
|
||||||
|
self._global_container_stack.propertiesChanged.emit(setting_key, property_names)
|
||||||
|
|
||||||
def _onMaterialNameChanged(self) -> None:
|
def _onMaterialNameChanged(self) -> None:
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
@ -1114,7 +1153,7 @@ class MachineManager(QObject):
|
|||||||
if quality_group is None:
|
if quality_group is None:
|
||||||
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
|
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
|
||||||
|
|
||||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
container_registry = self._application.getContainerRegistry()
|
||||||
quality_changes_container = empty_quality_changes_container
|
quality_changes_container = empty_quality_changes_container
|
||||||
quality_container = empty_quality_container # type: InstanceContainer
|
quality_container = empty_quality_container # type: InstanceContainer
|
||||||
if quality_changes_group.metadata_for_global:
|
if quality_changes_group.metadata_for_global:
|
||||||
@ -1150,6 +1189,7 @@ class MachineManager(QObject):
|
|||||||
extruder.qualityChanges = quality_changes_container
|
extruder.qualityChanges = quality_changes_container
|
||||||
|
|
||||||
self.setIntentByCategory(quality_changes_group.intent_category)
|
self.setIntentByCategory(quality_changes_group.intent_category)
|
||||||
|
self._reCalculateNumUserSettings()
|
||||||
|
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
self.activeQualityChangesGroupChanged.emit()
|
self.activeQualityChangesGroupChanged.emit()
|
||||||
@ -1196,7 +1236,7 @@ class MachineManager(QObject):
|
|||||||
def _updateQualityWithMaterial(self, *args: Any) -> None:
|
def _updateQualityWithMaterial(self, *args: Any) -> None:
|
||||||
"""Update current quality type and machine after setting material"""
|
"""Update current quality type and machine after setting material"""
|
||||||
|
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return
|
return
|
||||||
Logger.log("d", "Updating quality/quality_changes due to material change")
|
Logger.log("d", "Updating quality/quality_changes due to material change")
|
||||||
@ -1212,9 +1252,8 @@ class MachineManager(QObject):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not available_quality_types:
|
if not available_quality_types:
|
||||||
if global_stack.qualityChanges == empty_quality_changes_container:
|
Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).")
|
||||||
Logger.log("i", "No available quality types found, setting all qualities to empty (Not Supported).")
|
self._setEmptyQuality()
|
||||||
self._setEmptyQuality()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_quality_type in available_quality_types:
|
if current_quality_type in available_quality_types:
|
||||||
@ -1239,7 +1278,7 @@ class MachineManager(QObject):
|
|||||||
def _updateIntentWithQuality(self):
|
def _updateIntentWithQuality(self):
|
||||||
"""Update the current intent after the quality changed"""
|
"""Update the current intent after the quality changed"""
|
||||||
|
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return
|
return
|
||||||
Logger.log("d", "Updating intent due to quality change")
|
Logger.log("d", "Updating intent due to quality change")
|
||||||
@ -1390,6 +1429,9 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
for extruder_configuration in configuration.extruderConfigurations:
|
for extruder_configuration in configuration.extruderConfigurations:
|
||||||
position = str(extruder_configuration.position)
|
position = str(extruder_configuration.position)
|
||||||
|
if int(position) >= len(self._global_container_stack.extruderList):
|
||||||
|
Logger.warning("Received a configuration for extruder {position}, which is out of bounds for this printer.".format(position=position))
|
||||||
|
continue # Remote printer gave more extruders than what Cura had locally, e.g. because the user switched to a single-extruder printer while the sync was being processed.
|
||||||
|
|
||||||
# If the machine doesn't have a hotend or material, disable this extruder
|
# If the machine doesn't have a hotend or material, disable this extruder
|
||||||
if int(position) in extruders_to_disable:
|
if int(position) in extruders_to_disable:
|
||||||
@ -1448,17 +1490,21 @@ class MachineManager(QObject):
|
|||||||
self.updateMaterialWithVariant(None) # Update all materials
|
self.updateMaterialWithVariant(None) # Update all materials
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str, result = bool)
|
||||||
def setMaterialById(self, position: str, root_material_id: str) -> None:
|
def setMaterialById(self, position: str, root_material_id: str) -> bool:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return False
|
||||||
|
|
||||||
machine_definition_id = self._global_container_stack.definition.id
|
machine_definition_id = self._global_container_stack.definition.id
|
||||||
position = str(position)
|
position = str(position)
|
||||||
extruder_stack = self._global_container_stack.extruderList[int(position)]
|
extruder_stack = self._global_container_stack.extruderList[int(position)]
|
||||||
nozzle_name = extruder_stack.variant.getName()
|
nozzle_name = extruder_stack.variant.getName()
|
||||||
material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id]
|
|
||||||
self.setMaterial(position, material_node)
|
materials = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials
|
||||||
|
if root_material_id in materials:
|
||||||
|
self.setMaterial(position, materials[root_material_id])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariant")
|
@pyqtSlot(str, "QVariant")
|
||||||
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||||
@ -1552,7 +1598,7 @@ class MachineManager(QObject):
|
|||||||
# - "my_profile - Engineering - Fine" (based on an intent)
|
# - "my_profile - Engineering - Fine" (based on an intent)
|
||||||
@pyqtProperty("QVariantMap", notify = activeQualityDisplayNameChanged)
|
@pyqtProperty("QVariantMap", notify = activeQualityDisplayNameChanged)
|
||||||
def activeQualityDisplayNameMap(self) -> Dict[str, str]:
|
def activeQualityDisplayNameMap(self) -> Dict[str, str]:
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return {"main": "",
|
return {"main": "",
|
||||||
"suffix": ""}
|
"suffix": ""}
|
||||||
@ -1589,7 +1635,7 @@ class MachineManager(QObject):
|
|||||||
:param intent_category: The intent category to change to.
|
:param intent_category: The intent category to change to.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return
|
return
|
||||||
container_tree = ContainerTree.getInstance()
|
container_tree = ContainerTree.getInstance()
|
||||||
@ -1628,7 +1674,7 @@ class MachineManager(QObject):
|
|||||||
:return: The currently active quality group.
|
:return: The currently active quality group.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack or global_stack.quality == empty_quality_container:
|
if not global_stack or global_stack.quality == empty_quality_container:
|
||||||
return None
|
return None
|
||||||
return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType)
|
return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType)
|
||||||
@ -1665,7 +1711,7 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
|
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
|
||||||
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None or global_stack.qualityChanges == empty_quality_changes_container:
|
if global_stack is None or global_stack.qualityChanges == empty_quality_changes_container:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -1680,12 +1726,12 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
|
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
|
||||||
def hasCustomQuality(self) -> bool:
|
def hasCustomQuality(self) -> bool:
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
return global_stack is None or global_stack.qualityChanges != empty_quality_changes_container
|
return global_stack is None or global_stack.qualityChanges != empty_quality_changes_container
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||||
def activeQualityOrQualityChangesName(self) -> str:
|
def activeQualityOrQualityChangesName(self) -> str:
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_container_stack:
|
if not global_container_stack:
|
||||||
return empty_quality_container.getName()
|
return empty_quality_container.getName()
|
||||||
if global_container_stack.qualityChanges != empty_quality_changes_container:
|
if global_container_stack.qualityChanges != empty_quality_changes_container:
|
||||||
@ -1694,17 +1740,17 @@ class MachineManager(QObject):
|
|||||||
|
|
||||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
def hasNotSupportedQuality(self) -> bool:
|
def hasNotSupportedQuality(self) -> bool:
|
||||||
global_container_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_container_stack = self._application.getGlobalContainerStack()
|
||||||
return (not global_container_stack is None) and global_container_stack.quality == empty_quality_container and global_container_stack.qualityChanges == empty_quality_changes_container
|
return (not global_container_stack is None) and global_container_stack.quality == empty_quality_container and global_container_stack.qualityChanges == empty_quality_changes_container
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
def isActiveQualityCustom(self) -> bool:
|
def isActiveQualityCustom(self) -> bool:
|
||||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
return False
|
return False
|
||||||
return global_stack.qualityChanges != empty_quality_changes_container
|
return global_stack.qualityChanges != empty_quality_changes_container
|
||||||
|
|
||||||
def _updateUponMaterialMetadataChange(self) -> None:
|
def updateUponMaterialMetadataChange(self) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
|
@ -31,7 +31,7 @@ class MachineNameValidator(QObject):
|
|||||||
# special character, and that up to [machine_name_max_length / 12] times.
|
# special character, and that up to [machine_name_max_length / 12] times.
|
||||||
maximum_special_characters = int(machine_name_max_length / 12)
|
maximum_special_characters = int(machine_name_max_length / 12)
|
||||||
unescaped = r"[a-zA-Z0-9_\-\.\/]"
|
unescaped = r"[a-zA-Z0-9_\-\.\/]"
|
||||||
self.machine_name_regex = r"^((" + unescaped + "){0,12}|.){0," + str(maximum_special_characters) + r"}$"
|
self.machine_name_regex = r"^[^\.]((" + unescaped + "){0,12}|.){0," + str(maximum_special_characters) + r"}$"
|
||||||
|
|
||||||
validationChanged = pyqtSignal()
|
validationChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
|
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
|
||||||
@ -70,20 +70,20 @@ class CuraSplashScreen(QSplashScreen):
|
|||||||
font = QFont() # Using system-default font here
|
font = QFont() # Using system-default font here
|
||||||
font.setPixelSize(18)
|
font.setPixelSize(18)
|
||||||
painter.setFont(font)
|
painter.setFont(font)
|
||||||
painter.drawText(60, 70 + self._version_y_offset, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
|
painter.drawText(60, 70 + self._version_y_offset, round(330 * self._scale), round(230 * self._scale), Qt.AlignLeft | Qt.AlignTop, version[0])
|
||||||
if len(version) > 1:
|
if len(version) > 1:
|
||||||
font.setPixelSize(16)
|
font.setPixelSize(16)
|
||||||
painter.setFont(font)
|
painter.setFont(font)
|
||||||
painter.setPen(QColor(200, 200, 200, 255))
|
painter.setPen(QColor(200, 200, 200, 255))
|
||||||
painter.drawText(247, 105 + self._version_y_offset, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1])
|
painter.drawText(247, 105 + self._version_y_offset, round(330 * self._scale), round(255 * self._scale), Qt.AlignLeft | Qt.AlignTop, version[1])
|
||||||
painter.setPen(QColor(255, 255, 255, 255))
|
painter.setPen(QColor(255, 255, 255, 255))
|
||||||
|
|
||||||
# Draw the loading image
|
# Draw the loading image
|
||||||
pen = QPen()
|
pen = QPen()
|
||||||
pen.setWidth(6 * self._scale)
|
pen.setWidthF(6 * self._scale)
|
||||||
pen.setColor(QColor(32, 166, 219, 255))
|
pen.setColor(QColor(32, 166, 219, 255))
|
||||||
painter.setPen(pen)
|
painter.setPen(pen)
|
||||||
painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16)
|
painter.drawArc(60, 150, round(32 * self._scale), round(32 * self._scale), round(self._loading_image_rotation_angle * 16), 300 * 16)
|
||||||
|
|
||||||
# Draw message text
|
# Draw message text
|
||||||
if self._current_message:
|
if self._current_message:
|
||||||
|
@ -7,10 +7,9 @@ from cura.CuraApplication import CuraApplication
|
|||||||
|
|
||||||
|
|
||||||
class UltimakerCloudScope(DefaultUserAgentScope):
|
class UltimakerCloudScope(DefaultUserAgentScope):
|
||||||
"""Add an Authorization header to the request for Ultimaker Cloud Api requests.
|
"""
|
||||||
|
Add an Authorization header to the request for Ultimaker Cloud Api requests, if available.
|
||||||
When the user is not logged in or a token is not available, a warning will be logged
|
Also add the user agent headers (see DefaultUserAgentScope).
|
||||||
Also add the user agent headers (see DefaultUserAgentScope)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, application: CuraApplication):
|
def __init__(self, application: CuraApplication):
|
||||||
@ -22,7 +21,7 @@ class UltimakerCloudScope(DefaultUserAgentScope):
|
|||||||
super().requestHook(request)
|
super().requestHook(request)
|
||||||
token = self._account.accessToken
|
token = self._account.accessToken
|
||||||
if not self._account.isLoggedIn or token is None:
|
if not self._account.isLoggedIn or token is None:
|
||||||
Logger.warning("Cannot add authorization to Cloud Api request")
|
Logger.debug("User is not logged in for Cloud API request to {url}".format(url = request.url().toDisplayString()))
|
||||||
return
|
return
|
||||||
|
|
||||||
header_dict = {
|
header_dict = {
|
||||||
|
@ -22,6 +22,7 @@ import os
|
|||||||
# tries to create PyQt objects on a non-main thread.
|
# tries to create PyQt objects on a non-main thread.
|
||||||
import Arcus # @UnusedImport
|
import Arcus # @UnusedImport
|
||||||
import Savitar # @UnusedImport
|
import Savitar # @UnusedImport
|
||||||
|
import pynest2d # @UnusedImport
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
|
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ cmake3 \
|
|||||||
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
||||||
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
||||||
-DBUILD_TESTS=ON \
|
-DBUILD_TESTS=ON \
|
||||||
|
-DPRINT_PLUGIN_LIST=OFF \
|
||||||
|
-DGENERATE_TRANSLATIONS=OFF \
|
||||||
..
|
..
|
||||||
make
|
make
|
||||||
ctest3 -j4 --output-on-failure -T Test
|
|
||||||
|
3
docker/test.sh
Executable file
3
docker/test.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
cd build
|
||||||
|
ctest3 -j4 --output-on-failure -T Test
|
@ -19,6 +19,7 @@ from UM.Scene.SceneNode import SceneNode # For typing.
|
|||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
from cura.Machines.ContainerTree import ContainerTree
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
||||||
@ -108,6 +109,10 @@ class ThreeMFReader(MeshReader):
|
|||||||
|
|
||||||
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
|
try:
|
||||||
|
um_node.addDecorator(ConvexHullDecorator())
|
||||||
|
except:
|
||||||
|
pass
|
||||||
um_node.setName(node_name)
|
um_node.setName(node_name)
|
||||||
um_node.setId(node_id)
|
um_node.setId(node_id)
|
||||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||||
|
@ -5,7 +5,7 @@ from configparser import ConfigParser
|
|||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from typing import cast, Dict, List, Optional, Tuple, Any
|
from typing import cast, Dict, List, Optional, Tuple, Any, Set
|
||||||
|
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
@ -41,6 +41,18 @@ from .WorkspaceDialog import WorkspaceDialog
|
|||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
_ignored_machine_network_metadata = {
|
||||||
|
"um_cloud_cluster_id",
|
||||||
|
"um_network_key",
|
||||||
|
"um_linked_to_account",
|
||||||
|
"host_guid",
|
||||||
|
"removal_warning",
|
||||||
|
"group_name",
|
||||||
|
"group_size",
|
||||||
|
"connection_type"
|
||||||
|
} # type: Set[str]
|
||||||
|
|
||||||
|
|
||||||
class ContainerInfo:
|
class ContainerInfo:
|
||||||
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
|
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
|
||||||
self.file_name = file_name
|
self.file_name = file_name
|
||||||
@ -121,12 +133,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
|
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
|
||||||
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
|
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
|
||||||
|
|
||||||
self._is_same_machine_type = False
|
|
||||||
self._old_new_materials = {} # type: Dict[str, str]
|
self._old_new_materials = {} # type: Dict[str, str]
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
|
|
||||||
def _clearState(self):
|
def _clearState(self):
|
||||||
self._is_same_machine_type = False
|
|
||||||
self._id_mapping = {}
|
self._id_mapping = {}
|
||||||
self._old_new_materials = {}
|
self._old_new_materials = {}
|
||||||
self._machine_info = None
|
self._machine_info = None
|
||||||
@ -217,6 +227,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
# Read definition containers
|
# Read definition containers
|
||||||
#
|
#
|
||||||
machine_definition_id = None
|
machine_definition_id = None
|
||||||
|
updatable_machines = []
|
||||||
machine_definition_container_count = 0
|
machine_definition_container_count = 0
|
||||||
extruder_definition_container_count = 0
|
extruder_definition_container_count = 0
|
||||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||||
@ -233,6 +244,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
definition_container_type = definition_container.get("type")
|
definition_container_type = definition_container.get("type")
|
||||||
if definition_container_type == "machine":
|
if definition_container_type == "machine":
|
||||||
machine_definition_id = container_id
|
machine_definition_id = container_id
|
||||||
|
machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
|
||||||
|
if machine_definition_containers:
|
||||||
|
updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
|
||||||
machine_type = definition_container["name"]
|
machine_type = definition_container["name"]
|
||||||
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
variant_type_name = definition_container.get("variants_name", variant_type_name)
|
||||||
|
|
||||||
@ -248,7 +262,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if machine_definition_container_count != 1:
|
if machine_definition_container_count != 1:
|
||||||
return WorkspaceReader.PreReadResult.failed # Not a workspace file but ordinary 3MF.
|
return WorkspaceReader.PreReadResult.failed # Not a workspace file but ordinary 3MF.
|
||||||
|
|
||||||
material_labels = []
|
material_ids_to_names_map = {}
|
||||||
material_conflict = False
|
material_conflict = False
|
||||||
xml_material_profile = self._getXmlProfileClass()
|
xml_material_profile = self._getXmlProfileClass()
|
||||||
reverse_material_id_dict = {}
|
reverse_material_id_dict = {}
|
||||||
@ -264,7 +278,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
reverse_map = {metadata["id"]: container_id for metadata in metadata_list}
|
reverse_map = {metadata["id"]: container_id for metadata in metadata_list}
|
||||||
reverse_material_id_dict.update(reverse_map)
|
reverse_material_id_dict.update(reverse_map)
|
||||||
|
|
||||||
material_labels.append(self._getMaterialLabelFromSerialized(serialized))
|
material_ids_to_names_map[container_id] = self._getMaterialLabelFromSerialized(serialized)
|
||||||
if self._container_registry.findContainersMetadata(id = container_id): #This material already exists.
|
if self._container_registry.findContainersMetadata(id = container_id): #This material already exists.
|
||||||
containers_found_dict["material"] = True
|
containers_found_dict["material"] = True
|
||||||
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
|
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
|
||||||
@ -374,8 +388,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
machine_definition_id = id_list[7]
|
machine_definition_id = id_list[7]
|
||||||
|
|
||||||
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
|
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
|
||||||
self._is_same_machine_type = True
|
|
||||||
existing_global_stack = None
|
existing_global_stack = None
|
||||||
|
global_stack = None
|
||||||
|
|
||||||
if stacks:
|
if stacks:
|
||||||
global_stack = stacks[0]
|
global_stack = stacks[0]
|
||||||
@ -388,7 +402,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if global_stack.getContainer(index).getId() != container_id:
|
if global_stack.getContainer(index).getId() != container_id:
|
||||||
machine_conflict = True
|
machine_conflict = True
|
||||||
break
|
break
|
||||||
self._is_same_machine_type = global_stack.definition.getId() == machine_definition_id
|
|
||||||
|
if updatable_machines and not containers_found_dict["machine"]:
|
||||||
|
containers_found_dict["machine"] = True
|
||||||
|
|
||||||
# Get quality type
|
# Get quality type
|
||||||
parser = ConfigParser(interpolation = None)
|
parser = ConfigParser(interpolation = None)
|
||||||
@ -431,6 +447,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
materials_in_extruders_dict = {} # Which material is in which extruder
|
||||||
|
|
||||||
# if the global stack is found, we check if there are conflicts in the extruder stacks
|
# if the global stack is found, we check if there are conflicts in the extruder stacks
|
||||||
for extruder_stack_file in extruder_stack_files:
|
for extruder_stack_file in extruder_stack_files:
|
||||||
serialized = archive.open(extruder_stack_file).read().decode("utf-8")
|
serialized = archive.open(extruder_stack_file).read().decode("utf-8")
|
||||||
@ -456,6 +474,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if material_id not in ("empty", "empty_material"):
|
if material_id not in ("empty", "empty_material"):
|
||||||
root_material_id = reverse_material_id_dict[material_id]
|
root_material_id = reverse_material_id_dict[material_id]
|
||||||
extruder_info.root_material_id = root_material_id
|
extruder_info.root_material_id = root_material_id
|
||||||
|
materials_in_extruders_dict[position] = material_ids_to_names_map[reverse_material_id_dict[material_id]]
|
||||||
|
|
||||||
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
|
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
|
||||||
if definition_changes_id not in ("empty", "empty_definition_changes"):
|
if definition_changes_id not in ("empty", "empty_definition_changes"):
|
||||||
@ -470,7 +489,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
if intent_id not in ("empty", "empty_intent"):
|
if intent_id not in ("empty", "empty_intent"):
|
||||||
extruder_info.intent_info = instance_container_info_dict[intent_id]
|
extruder_info.intent_info = instance_container_info_dict[intent_id]
|
||||||
|
|
||||||
if not machine_conflict and containers_found_dict["machine"]:
|
if not machine_conflict and containers_found_dict["machine"] and global_stack:
|
||||||
if int(position) >= len(global_stack.extruderList):
|
if int(position) >= len(global_stack.extruderList):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -484,6 +503,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
machine_conflict = True
|
machine_conflict = True
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Now we know which material is in which extruder. Let's use that to sort the material_labels according to
|
||||||
|
# their extruder position
|
||||||
|
material_labels = [material_name for pos, material_name in sorted(materials_in_extruders_dict.items())]
|
||||||
|
machine_extruder_count = self._getMachineExtruderCount()
|
||||||
|
if machine_extruder_count:
|
||||||
|
material_labels = material_labels[:machine_extruder_count]
|
||||||
|
|
||||||
num_visible_settings = 0
|
num_visible_settings = 0
|
||||||
try:
|
try:
|
||||||
temp_preferences = Preferences()
|
temp_preferences = Preferences()
|
||||||
@ -536,9 +562,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._machine_info.custom_quality_name = quality_name
|
self._machine_info.custom_quality_name = quality_name
|
||||||
self._machine_info.intent_category = intent_category
|
self._machine_info.intent_category = intent_category
|
||||||
|
|
||||||
if machine_conflict and not self._is_same_machine_type:
|
|
||||||
machine_conflict = False
|
|
||||||
|
|
||||||
is_printer_group = False
|
is_printer_group = False
|
||||||
if machine_conflict:
|
if machine_conflict:
|
||||||
group_name = existing_global_stack.getMetaDataEntry("group_name")
|
group_name = existing_global_stack.getMetaDataEntry("group_name")
|
||||||
@ -559,6 +582,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
|
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
|
||||||
self._dialog.setNumUserSettings(num_user_settings)
|
self._dialog.setNumUserSettings(num_user_settings)
|
||||||
self._dialog.setActiveMode(active_mode)
|
self._dialog.setActiveMode(active_mode)
|
||||||
|
self._dialog.setUpdatableMachines(updatable_machines)
|
||||||
self._dialog.setMachineName(machine_name)
|
self._dialog.setMachineName(machine_name)
|
||||||
self._dialog.setMaterialLabels(material_labels)
|
self._dialog.setMaterialLabels(material_labels)
|
||||||
self._dialog.setMachineType(machine_type)
|
self._dialog.setMachineType(machine_type)
|
||||||
@ -612,6 +636,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
message.show()
|
message.show()
|
||||||
self.setWorkspaceName("")
|
self.setWorkspaceName("")
|
||||||
return [], {}
|
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/")]
|
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||||
|
|
||||||
@ -639,21 +670,28 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
|
|
||||||
application.expandedCategoriesChanged.emit() # Notify the GUI of the change
|
application.expandedCategoriesChanged.emit() # Notify the GUI of the change
|
||||||
|
|
||||||
# If a machine with the same name is of a different type, always create a new one.
|
# If there are no machines of the same type, create a new machine.
|
||||||
if not self._is_same_machine_type or self._resolve_strategies["machine"] != "override":
|
if self._resolve_strategies["machine"] != "override" or self._dialog.updatableMachinesModel.count <= 1:
|
||||||
# We need to create a new machine
|
# We need to create a new machine
|
||||||
machine_name = self._container_registry.uniqueName(self._machine_info.name)
|
machine_name = self._container_registry.uniqueName(self._machine_info.name)
|
||||||
|
|
||||||
global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id)
|
# Printers with modifiable number of extruders (such as CFFF) will specify a machine_extruder_count in their
|
||||||
|
# quality_changes file. If that's the case, take the extruder count into account when creating the machine
|
||||||
|
# or else the extruderList will return only the first extruder, leading to missing non-global settings in
|
||||||
|
# the other extruders.
|
||||||
|
machine_extruder_count = self._getMachineExtruderCount() # type: Optional[int]
|
||||||
|
global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id, machine_extruder_count)
|
||||||
if global_stack: # Only switch if creating the machine was successful.
|
if global_stack: # Only switch if creating the machine was successful.
|
||||||
extruder_stack_dict = {str(position): extruder for position, extruder in enumerate(global_stack.extruderList)}
|
extruder_stack_dict = {str(position): extruder for position, extruder in enumerate(global_stack.extruderList)}
|
||||||
|
|
||||||
self._container_registry.addContainer(global_stack)
|
self._container_registry.addContainer(global_stack)
|
||||||
else:
|
else:
|
||||||
# Find the machine
|
# Find the machine which will be overridden
|
||||||
global_stacks = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine")
|
global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
|
||||||
if not global_stacks:
|
if not global_stacks:
|
||||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!", "Project file <filename>{0}</filename> is made using profiles that are unknown to this version of Ultimaker Cura.", file_name))
|
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
|
||||||
|
"Project file <filename>{0}</filename> is made using profiles that"
|
||||||
|
" are unknown to this version of Ultimaker Cura.", file_name))
|
||||||
message.show()
|
message.show()
|
||||||
self.setWorkspaceName("")
|
self.setWorkspaceName("")
|
||||||
return [], {}
|
return [], {}
|
||||||
@ -739,21 +777,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
# Handle quality changes if any
|
if global_stack:
|
||||||
self._processQualityChanges(global_stack)
|
# Handle quality changes if any
|
||||||
|
self._processQualityChanges(global_stack)
|
||||||
|
|
||||||
# Prepare the machine
|
# Prepare the machine
|
||||||
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
||||||
|
|
||||||
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
||||||
# Actually change the active machine.
|
# Actually change the active machine.
|
||||||
#
|
#
|
||||||
# This is scheduled for later is because it depends on the Variant/Material/Qualitiy Managers to have the latest
|
# This is scheduled for later is because it depends on the Variant/Material/Qualitiy Managers to have the latest
|
||||||
# data, but those managers will only update upon a container/container metadata changed signal. Because this
|
# data, but those managers will only update upon a container/container metadata changed signal. Because this
|
||||||
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
|
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
|
||||||
# they won't take effect until this function is done.
|
# they won't take effect until this function is done.
|
||||||
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
|
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
|
||||||
self._updateActiveMachine(global_stack)
|
self._updateActiveMachine(global_stack)
|
||||||
|
|
||||||
# Load all the nodes / mesh data of the workspace
|
# Load all the nodes / mesh data of the workspace
|
||||||
nodes = self._3mf_mesh_reader.read(file_name)
|
nodes = self._3mf_mesh_reader.read(file_name)
|
||||||
@ -773,6 +812,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
except zipfile.BadZipFile:
|
except zipfile.BadZipFile:
|
||||||
Logger.logException("w", "Unable to retrieve metadata from {fname}: 3MF archive is corrupt.".format(fname = file_name))
|
Logger.logException("w", "Unable to retrieve metadata from {fname}: 3MF archive is corrupt.".format(fname = file_name))
|
||||||
return result
|
return result
|
||||||
|
except EnvironmentError as e:
|
||||||
|
Logger.logException("w", "Unable to retrieve metadata from {fname}: File is inaccessible. Error: {err}".format(fname = file_name, err = str(e)))
|
||||||
|
return result
|
||||||
|
|
||||||
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
|
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
|
||||||
|
|
||||||
@ -898,6 +940,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
|
|
||||||
self._machine_info.quality_changes_info.name = quality_changes_name
|
self._machine_info.quality_changes_info.name = quality_changes_name
|
||||||
|
|
||||||
|
def _getMachineExtruderCount(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Extracts the machine extruder count from the definition_changes file of the printer. If it is not specified in
|
||||||
|
the file, None is returned instead.
|
||||||
|
|
||||||
|
:return: The count of the machine's extruders
|
||||||
|
"""
|
||||||
|
machine_extruder_count = None
|
||||||
|
if self._machine_info \
|
||||||
|
and self._machine_info.definition_changes_info \
|
||||||
|
and "values" in self._machine_info.definition_changes_info.parser \
|
||||||
|
and "machine_extruder_count" in self._machine_info.definition_changes_info.parser["values"]:
|
||||||
|
try:
|
||||||
|
# Theoretically, if the machine_extruder_count is a setting formula (e.g. "=3"), this will produce a
|
||||||
|
# value error and the project file loading will load the settings in the first extruder only.
|
||||||
|
# This is not expected to happen though, since all machine definitions define the machine_extruder_count
|
||||||
|
# as an integer.
|
||||||
|
machine_extruder_count = int(self._machine_info.definition_changes_info.parser["values"]["machine_extruder_count"])
|
||||||
|
except ValueError:
|
||||||
|
Logger.log("w", "'machine_extruder_count' in file '{file_name}' is not a number."
|
||||||
|
.format(file_name = self._machine_info.definition_changes_info.file_name))
|
||||||
|
return machine_extruder_count
|
||||||
|
|
||||||
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
|
def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer:
|
||||||
"""Helper class to create a new quality changes profile.
|
"""Helper class to create a new quality changes profile.
|
||||||
|
|
||||||
@ -1069,7 +1134,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
|
|
||||||
# Set metadata fields that are missing from the global stack
|
# Set metadata fields that are missing from the global stack
|
||||||
for key, value in self._machine_info.metadata_dict.items():
|
for key, value in self._machine_info.metadata_dict.items():
|
||||||
global_stack.setMetaDataEntry(key, value)
|
if key not in _ignored_machine_network_metadata:
|
||||||
|
global_stack.setMetaDataEntry(key, value)
|
||||||
|
|
||||||
def _updateActiveMachine(self, global_stack):
|
def _updateActiveMachine(self, global_stack):
|
||||||
# Actually change the active machine.
|
# Actually change the active machine.
|
||||||
@ -1080,7 +1146,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||||||
|
|
||||||
# Set metadata fields that are missing from the global stack
|
# Set metadata fields that are missing from the global stack
|
||||||
for key, value in self._machine_info.metadata_dict.items():
|
for key, value in self._machine_info.metadata_dict.items():
|
||||||
if key not in global_stack.getMetaData():
|
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
|
||||||
global_stack.setMetaDataEntry(key, value)
|
global_stack.setMetaDataEntry(key, value)
|
||||||
|
|
||||||
if self._quality_changes_to_apply:
|
if self._quality_changes_to_apply:
|
||||||
|
43
plugins/3MFReader/UpdatableMachinesModel.py
Normal file
43
plugins/3MFReader/UpdatableMachinesModel.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
create_new_list_item = {
|
||||||
|
"id": "new",
|
||||||
|
"name": "Create new",
|
||||||
|
"displayName": "Create new",
|
||||||
|
"type": "default_option" # to make sure we are not mixing the "Create new" option with a printer with id "new"
|
||||||
|
} # type: Dict[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatableMachinesModel(ListModel):
|
||||||
|
"""Model that holds cura packages.
|
||||||
|
|
||||||
|
By setting the filter property the instances held by this model can be changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.addRoleName(Qt.UserRole + 1, "id")
|
||||||
|
self.addRoleName(Qt.UserRole + 2, "name")
|
||||||
|
self.addRoleName(Qt.UserRole + 3, "displayName")
|
||||||
|
self.addRoleName(Qt.UserRole + 4, "type") # Either "default_option" or "machine"
|
||||||
|
|
||||||
|
def update(self, machines: List[GlobalStack]) -> None:
|
||||||
|
items = [create_new_list_item] # type: List[Dict[str, str]]
|
||||||
|
|
||||||
|
for machine in sorted(machines, key = lambda printer: printer.name):
|
||||||
|
items.append({
|
||||||
|
"id": machine.id,
|
||||||
|
"name": machine.name,
|
||||||
|
"displayName": "Update " + machine.name,
|
||||||
|
"type": "machine"
|
||||||
|
})
|
||||||
|
self.setItems(items)
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import List, Optional, Dict, cast
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
@ -7,10 +8,15 @@ from UM.PluginRegistry import PluginRegistry
|
|||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
from .UpdatableMachinesModel import UpdatableMachinesModel
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
@ -29,6 +35,7 @@ class WorkspaceDialog(QObject):
|
|||||||
"quality_changes": self._default_strategy,
|
"quality_changes": self._default_strategy,
|
||||||
"definition_changes": self._default_strategy,
|
"definition_changes": self._default_strategy,
|
||||||
"material": self._default_strategy}
|
"material": self._default_strategy}
|
||||||
|
self._override_machine = None
|
||||||
self._visible = False
|
self._visible = False
|
||||||
self.showDialogSignal.connect(self.__show)
|
self.showDialogSignal.connect(self.__show)
|
||||||
|
|
||||||
@ -51,6 +58,7 @@ class WorkspaceDialog(QObject):
|
|||||||
self._extruders = []
|
self._extruders = []
|
||||||
self._objects_on_plate = False
|
self._objects_on_plate = False
|
||||||
self._is_printer_group = False
|
self._is_printer_group = False
|
||||||
|
self._updatable_machines_model = UpdatableMachinesModel(self)
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
@ -63,6 +71,7 @@ class WorkspaceDialog(QObject):
|
|||||||
qualityTypeChanged = pyqtSignal()
|
qualityTypeChanged = pyqtSignal()
|
||||||
intentNameChanged = pyqtSignal()
|
intentNameChanged = pyqtSignal()
|
||||||
machineNameChanged = pyqtSignal()
|
machineNameChanged = pyqtSignal()
|
||||||
|
updatableMachinesChanged = pyqtSignal()
|
||||||
materialLabelsChanged = pyqtSignal()
|
materialLabelsChanged = pyqtSignal()
|
||||||
objectsOnPlateChanged = pyqtSignal()
|
objectsOnPlateChanged = pyqtSignal()
|
||||||
numUserSettingsChanged = pyqtSignal()
|
numUserSettingsChanged = pyqtSignal()
|
||||||
@ -81,33 +90,33 @@ class WorkspaceDialog(QObject):
|
|||||||
self.isPrinterGroupChanged.emit()
|
self.isPrinterGroupChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=variantTypeChanged)
|
@pyqtProperty(str, notify=variantTypeChanged)
|
||||||
def variantType(self):
|
def variantType(self) -> str:
|
||||||
return self._variant_type
|
return self._variant_type
|
||||||
|
|
||||||
def setVariantType(self, variant_type):
|
def setVariantType(self, variant_type: str) -> None:
|
||||||
if self._variant_type != variant_type:
|
if self._variant_type != variant_type:
|
||||||
self._variant_type = variant_type
|
self._variant_type = variant_type
|
||||||
self.variantTypeChanged.emit()
|
self.variantTypeChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=machineTypeChanged)
|
@pyqtProperty(str, notify=machineTypeChanged)
|
||||||
def machineType(self):
|
def machineType(self) -> str:
|
||||||
return self._machine_type
|
return self._machine_type
|
||||||
|
|
||||||
def setMachineType(self, machine_type):
|
def setMachineType(self, machine_type: str) -> None:
|
||||||
self._machine_type = machine_type
|
self._machine_type = machine_type
|
||||||
self.machineTypeChanged.emit()
|
self.machineTypeChanged.emit()
|
||||||
|
|
||||||
def setNumUserSettings(self, num_user_settings):
|
def setNumUserSettings(self, num_user_settings: int) -> None:
|
||||||
if self._num_user_settings != num_user_settings:
|
if self._num_user_settings != num_user_settings:
|
||||||
self._num_user_settings = num_user_settings
|
self._num_user_settings = num_user_settings
|
||||||
self.numVisibleSettingsChanged.emit()
|
self.numVisibleSettingsChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify=numUserSettingsChanged)
|
@pyqtProperty(int, notify=numUserSettingsChanged)
|
||||||
def numUserSettings(self):
|
def numUserSettings(self) -> int:
|
||||||
return self._num_user_settings
|
return self._num_user_settings
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=objectsOnPlateChanged)
|
@pyqtProperty(bool, notify=objectsOnPlateChanged)
|
||||||
def hasObjectsOnPlate(self):
|
def hasObjectsOnPlate(self) -> bool:
|
||||||
return self._objects_on_plate
|
return self._objects_on_plate
|
||||||
|
|
||||||
def setHasObjectsOnPlate(self, objects_on_plate):
|
def setHasObjectsOnPlate(self, objects_on_plate):
|
||||||
@ -116,10 +125,10 @@ class WorkspaceDialog(QObject):
|
|||||||
self.objectsOnPlateChanged.emit()
|
self.objectsOnPlateChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = materialLabelsChanged)
|
@pyqtProperty("QVariantList", notify = materialLabelsChanged)
|
||||||
def materialLabels(self):
|
def materialLabels(self) -> List[str]:
|
||||||
return self._material_labels
|
return self._material_labels
|
||||||
|
|
||||||
def setMaterialLabels(self, material_labels):
|
def setMaterialLabels(self, material_labels: List[str]) -> None:
|
||||||
if self._material_labels != material_labels:
|
if self._material_labels != material_labels:
|
||||||
self._material_labels = material_labels
|
self._material_labels = material_labels
|
||||||
self.materialLabelsChanged.emit()
|
self.materialLabelsChanged.emit()
|
||||||
@ -134,36 +143,44 @@ class WorkspaceDialog(QObject):
|
|||||||
self.extrudersChanged.emit()
|
self.extrudersChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify = machineNameChanged)
|
@pyqtProperty(str, notify = machineNameChanged)
|
||||||
def machineName(self):
|
def machineName(self) -> str:
|
||||||
return self._machine_name
|
return self._machine_name
|
||||||
|
|
||||||
def setMachineName(self, machine_name):
|
def setMachineName(self, machine_name: str) -> None:
|
||||||
if self._machine_name != machine_name:
|
if self._machine_name != machine_name:
|
||||||
self._machine_name = machine_name
|
self._machine_name = machine_name
|
||||||
self.machineNameChanged.emit()
|
self.machineNameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = updatableMachinesChanged)
|
||||||
|
def updatableMachinesModel(self) -> UpdatableMachinesModel:
|
||||||
|
return cast(UpdatableMachinesModel, self._updatable_machines_model)
|
||||||
|
|
||||||
|
def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None:
|
||||||
|
self._updatable_machines_model.update(updatable_machines)
|
||||||
|
self.updatableMachinesChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=qualityTypeChanged)
|
@pyqtProperty(str, notify=qualityTypeChanged)
|
||||||
def qualityType(self):
|
def qualityType(self) -> str:
|
||||||
return self._quality_type
|
return self._quality_type
|
||||||
|
|
||||||
def setQualityType(self, quality_type):
|
def setQualityType(self, quality_type: str) -> None:
|
||||||
if self._quality_type != quality_type:
|
if self._quality_type != quality_type:
|
||||||
self._quality_type = quality_type
|
self._quality_type = quality_type
|
||||||
self.qualityTypeChanged.emit()
|
self.qualityTypeChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
||||||
def numSettingsOverridenByQualityChanges(self):
|
def numSettingsOverridenByQualityChanges(self) -> int:
|
||||||
return self._num_settings_overridden_by_quality_changes
|
return self._num_settings_overridden_by_quality_changes
|
||||||
|
|
||||||
def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes):
|
def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes: int) -> None:
|
||||||
self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes
|
self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes
|
||||||
self.numSettingsOverridenByQualityChangesChanged.emit()
|
self.numSettingsOverridenByQualityChangesChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=qualityNameChanged)
|
@pyqtProperty(str, notify=qualityNameChanged)
|
||||||
def qualityName(self):
|
def qualityName(self) -> str:
|
||||||
return self._quality_name
|
return self._quality_name
|
||||||
|
|
||||||
def setQualityName(self, quality_name):
|
def setQualityName(self, quality_name: str) -> None:
|
||||||
if self._quality_name != quality_name:
|
if self._quality_name != quality_name:
|
||||||
self._quality_name = quality_name
|
self._quality_name = quality_name
|
||||||
self.qualityNameChanged.emit()
|
self.qualityNameChanged.emit()
|
||||||
@ -178,80 +195,87 @@ class WorkspaceDialog(QObject):
|
|||||||
self.intentNameChanged.emit()
|
self.intentNameChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=activeModeChanged)
|
@pyqtProperty(str, notify=activeModeChanged)
|
||||||
def activeMode(self):
|
def activeMode(self) -> str:
|
||||||
return self._active_mode
|
return self._active_mode
|
||||||
|
|
||||||
def setActiveMode(self, active_mode):
|
def setActiveMode(self, active_mode: int) -> None:
|
||||||
if active_mode == 0:
|
if active_mode == 0:
|
||||||
self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended")
|
self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended")
|
||||||
else:
|
else:
|
||||||
self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom")
|
self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom")
|
||||||
self.activeModeChanged.emit()
|
self.activeModeChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = hasVisibleSettingsFieldChanged)
|
@pyqtProperty(bool, notify = hasVisibleSettingsFieldChanged)
|
||||||
def hasVisibleSettingsField(self):
|
def hasVisibleSettingsField(self) -> bool:
|
||||||
return self._has_visible_settings_field
|
return self._has_visible_settings_field
|
||||||
|
|
||||||
def setHasVisibleSettingsField(self, has_visible_settings_field):
|
def setHasVisibleSettingsField(self, has_visible_settings_field: bool) -> None:
|
||||||
self._has_visible_settings_field = has_visible_settings_field
|
self._has_visible_settings_field = has_visible_settings_field
|
||||||
self.hasVisibleSettingsFieldChanged.emit()
|
self.hasVisibleSettingsFieldChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, constant = True)
|
@pyqtProperty(int, constant = True)
|
||||||
def totalNumberOfSettings(self):
|
def totalNumberOfSettings(self) -> int:
|
||||||
general_definition_containers = ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")
|
general_definition_containers = ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")
|
||||||
if not general_definition_containers:
|
if not general_definition_containers:
|
||||||
return 0
|
return 0
|
||||||
return len(general_definition_containers[0].getAllKeys())
|
return len(general_definition_containers[0].getAllKeys())
|
||||||
|
|
||||||
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
||||||
def numVisibleSettings(self):
|
def numVisibleSettings(self) -> int:
|
||||||
return self._num_visible_settings
|
return self._num_visible_settings
|
||||||
|
|
||||||
def setNumVisibleSettings(self, num_visible_settings):
|
def setNumVisibleSettings(self, num_visible_settings: int) -> None:
|
||||||
if self._num_visible_settings != num_visible_settings:
|
if self._num_visible_settings != num_visible_settings:
|
||||||
self._num_visible_settings = num_visible_settings
|
self._num_visible_settings = num_visible_settings
|
||||||
self.numVisibleSettingsChanged.emit()
|
self.numVisibleSettingsChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = machineConflictChanged)
|
@pyqtProperty(bool, notify = machineConflictChanged)
|
||||||
def machineConflict(self):
|
def machineConflict(self) -> bool:
|
||||||
return self._has_machine_conflict
|
return self._has_machine_conflict
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=qualityChangesConflictChanged)
|
@pyqtProperty(bool, notify=qualityChangesConflictChanged)
|
||||||
def qualityChangesConflict(self):
|
def qualityChangesConflict(self) -> bool:
|
||||||
return self._has_quality_changes_conflict
|
return self._has_quality_changes_conflict
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=materialConflictChanged)
|
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||||
def materialConflict(self):
|
def materialConflict(self) -> bool:
|
||||||
return self._has_material_conflict
|
return self._has_material_conflict
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def setResolveStrategy(self, key, strategy):
|
def setResolveStrategy(self, key: str, strategy: Optional[str]) -> None:
|
||||||
if key in self._result:
|
if key in self._result:
|
||||||
self._result[key] = strategy
|
self._result[key] = strategy
|
||||||
|
|
||||||
|
def getMachineToOverride(self) -> str:
|
||||||
|
return self._override_machine
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setMachineToOverride(self, machine_name: str) -> None:
|
||||||
|
self._override_machine = machine_name
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def closeBackend(self):
|
def closeBackend(self) -> None:
|
||||||
"""Close the backend: otherwise one could end up with "Slicing..."""
|
"""Close the backend: otherwise one could end up with "Slicing..."""
|
||||||
|
|
||||||
Application.getInstance().getBackend().close()
|
Application.getInstance().getBackend().close()
|
||||||
|
|
||||||
def setMaterialConflict(self, material_conflict):
|
def setMaterialConflict(self, material_conflict: bool) -> None:
|
||||||
if self._has_material_conflict != material_conflict:
|
if self._has_material_conflict != material_conflict:
|
||||||
self._has_material_conflict = material_conflict
|
self._has_material_conflict = material_conflict
|
||||||
self.materialConflictChanged.emit()
|
self.materialConflictChanged.emit()
|
||||||
|
|
||||||
def setMachineConflict(self, machine_conflict):
|
def setMachineConflict(self, machine_conflict: bool) -> None:
|
||||||
if self._has_machine_conflict != machine_conflict:
|
if self._has_machine_conflict != machine_conflict:
|
||||||
self._has_machine_conflict = machine_conflict
|
self._has_machine_conflict = machine_conflict
|
||||||
self.machineConflictChanged.emit()
|
self.machineConflictChanged.emit()
|
||||||
|
|
||||||
def setQualityChangesConflict(self, quality_changes_conflict):
|
def setQualityChangesConflict(self, quality_changes_conflict: bool) -> None:
|
||||||
if self._has_quality_changes_conflict != quality_changes_conflict:
|
if self._has_quality_changes_conflict != quality_changes_conflict:
|
||||||
self._has_quality_changes_conflict = quality_changes_conflict
|
self._has_quality_changes_conflict = quality_changes_conflict
|
||||||
self.qualityChangesConflictChanged.emit()
|
self.qualityChangesConflictChanged.emit()
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self) -> Dict[str, Optional[str]]:
|
||||||
if "machine" in self._result and not self._has_machine_conflict:
|
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
|
||||||
self._result["machine"] = None
|
self._result["machine"] = None
|
||||||
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
||||||
self._result["quality_changes"] = None
|
self._result["quality_changes"] = None
|
||||||
@ -267,11 +291,13 @@ class WorkspaceDialog(QObject):
|
|||||||
|
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
def _createViewFromQML(self):
|
def _createViewFromQML(self) -> None:
|
||||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)
|
three_mf_reader_path = PluginRegistry.getInstance().getPluginPath("3MFReader")
|
||||||
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
if three_mf_reader_path:
|
||||||
|
path = os.path.join(three_mf_reader_path, self._qml_url)
|
||||||
|
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
# Emit signal so the right thread actually shows the view.
|
# Emit signal so the right thread actually shows the view.
|
||||||
if threading.current_thread() != threading.main_thread():
|
if threading.current_thread() != threading.main_thread():
|
||||||
self._lock.acquire()
|
self._lock.acquire()
|
||||||
@ -284,7 +310,7 @@ class WorkspaceDialog(QObject):
|
|||||||
self.showDialogSignal.emit()
|
self.showDialogSignal.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def notifyClosed(self):
|
def notifyClosed(self) -> None:
|
||||||
"""Used to notify the dialog so the lock can be released."""
|
"""Used to notify the dialog so the lock can be released."""
|
||||||
|
|
||||||
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
||||||
@ -294,7 +320,7 @@ class WorkspaceDialog(QObject):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def hide(self):
|
def hide(self) -> None:
|
||||||
self._visible = False
|
self._visible = False
|
||||||
self._view.hide()
|
self._view.hide()
|
||||||
try:
|
try:
|
||||||
@ -303,7 +329,7 @@ class WorkspaceDialog(QObject):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@pyqtSlot(bool)
|
@pyqtSlot(bool)
|
||||||
def _onVisibilityChanged(self, visible):
|
def _onVisibilityChanged(self, visible: bool) -> None:
|
||||||
if not visible:
|
if not visible:
|
||||||
try:
|
try:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
@ -311,17 +337,17 @@ class WorkspaceDialog(QObject):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def onOkButtonClicked(self):
|
def onOkButtonClicked(self) -> None:
|
||||||
self._view.hide()
|
self._view.hide()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def onCancelButtonClicked(self):
|
def onCancelButtonClicked(self) -> None:
|
||||||
self._result = {}
|
self._result = {}
|
||||||
self._view.hide()
|
self._view.hide()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
def waitForClose(self):
|
def waitForClose(self) -> None:
|
||||||
"""Block thread until the dialog is closed."""
|
"""Block thread until the dialog is closed."""
|
||||||
|
|
||||||
if self._visible:
|
if self._visible:
|
||||||
@ -334,7 +360,7 @@ class WorkspaceDialog(QObject):
|
|||||||
time.sleep(1 / 50)
|
time.sleep(1 / 50)
|
||||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
def __show(self):
|
def __show(self) -> None:
|
||||||
if self._view is None:
|
if self._view is None:
|
||||||
self._createViewFromQML()
|
self._createViewFromQML()
|
||||||
if self._view:
|
if self._view:
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
// Copyright (c) 2016 Ultimaker B.V.
|
// Copyright (c) 2020 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
||||||
@ -16,10 +17,11 @@ UM.Dialog
|
|||||||
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
||||||
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
||||||
width: minimumWidth
|
width: minimumWidth
|
||||||
height: minimumHeight
|
height: Math.max(dialogSummaryItem.height + 2 * buttonsItem.height, minimumHeight) // 2 * button height to also have some extra space around the button relative to the button size
|
||||||
|
|
||||||
property int comboboxHeight: 15 * screenScaleFactor
|
property int comboboxHeight: 15 * screenScaleFactor
|
||||||
property int spacerHeight: 10 * screenScaleFactor
|
property int spacerHeight: 10 * screenScaleFactor
|
||||||
|
property int doubleSpacerHeight: 20 * screenScaleFactor
|
||||||
|
|
||||||
onClosing: manager.notifyClosed()
|
onClosing: manager.notifyClosed()
|
||||||
onVisibleChanged:
|
onVisibleChanged:
|
||||||
@ -34,8 +36,10 @@ UM.Dialog
|
|||||||
|
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
id: dialogSummaryItem
|
||||||
anchors.margins: 20 * screenScaleFactor
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
anchors.margins: 10 * screenScaleFactor
|
||||||
|
|
||||||
UM.I18nCatalog
|
UM.I18nCatalog
|
||||||
{
|
{
|
||||||
@ -62,7 +66,8 @@ UM.Dialog
|
|||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
spacing: 2 * screenScaleFactor
|
spacing: 2 * screenScaleFactor
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
@ -79,7 +84,7 @@ UM.Dialog
|
|||||||
}
|
}
|
||||||
Item // Spacer
|
Item // Spacer
|
||||||
{
|
{
|
||||||
height: spacerHeight
|
height: doubleSpacerHeight
|
||||||
width: height
|
width: height
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,35 +106,54 @@ UM.Dialog
|
|||||||
}
|
}
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
id: machineResolveTooltip
|
id: machineResolveStrategyTooltip
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
height: visible ? comboboxHeight : 0
|
height: visible ? comboboxHeight : 0
|
||||||
visible: manager.machineConflict
|
visible: base.visible && machineResolveComboBox.model.count > 1
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||||
ComboBox
|
Cura.ComboBox
|
||||||
{
|
{
|
||||||
model: ListModel
|
|
||||||
{
|
|
||||||
Component.onCompleted:
|
|
||||||
{
|
|
||||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName});
|
|
||||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Connections
|
|
||||||
{
|
|
||||||
target: manager
|
|
||||||
onMachineNameChanged:
|
|
||||||
{
|
|
||||||
machineResolveComboBox.model.get(0).label = catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
textRole: "label"
|
|
||||||
id: machineResolveComboBox
|
id: machineResolveComboBox
|
||||||
|
model: manager.updatableMachinesModel
|
||||||
|
visible: machineResolveStrategyTooltip.visible
|
||||||
|
textRole: "displayName"
|
||||||
width: parent.width
|
width: parent.width
|
||||||
onActivated:
|
height: UM.Theme.getSize("button").height
|
||||||
|
onCurrentIndexChanged:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
|
if (model.getItem(currentIndex).id == "new"
|
||||||
|
&& model.getItem(currentIndex).type == "default_option")
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("machine", "new")
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("machine", "override")
|
||||||
|
manager.setMachineToOverride(model.getItem(currentIndex).id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged:
|
||||||
|
{
|
||||||
|
if (!visible) {return}
|
||||||
|
|
||||||
|
currentIndex = 0
|
||||||
|
// If the project printer exists in Cura, set it as the default dropdown menu option.
|
||||||
|
// No need to check object 0, which is the "Create new" option
|
||||||
|
for (var i = 1; i < model.count; i++)
|
||||||
|
{
|
||||||
|
if (model.getItem(i).name == manager.machineName)
|
||||||
|
{
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The project printer does not exist in Cura. If there is at least one printer of the same
|
||||||
|
// type, select the first one, else set the index to "Create new"
|
||||||
|
if (currentIndex == 0 && model.count > 1)
|
||||||
|
{
|
||||||
|
currentIndex = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,12 +187,13 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
text: manager.machineName
|
text: manager.machineName
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item // Spacer
|
Item // Spacer
|
||||||
{
|
{
|
||||||
height: spacerHeight
|
height: doubleSpacerHeight
|
||||||
width: height
|
width: height
|
||||||
}
|
}
|
||||||
Row
|
Row
|
||||||
@ -194,12 +219,13 @@ UM.Dialog
|
|||||||
height: visible ? comboboxHeight : 0
|
height: visible ? comboboxHeight : 0
|
||||||
visible: manager.qualityChangesConflict
|
visible: manager.qualityChangesConflict
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||||
ComboBox
|
Cura.ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: qualityChangesResolveComboBox
|
id: qualityChangesResolveComboBox
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("button").height
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||||
@ -220,6 +246,7 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
text: manager.qualityName
|
text: manager.qualityName
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row
|
Row
|
||||||
@ -235,6 +262,7 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
text: manager.intentName
|
text: manager.intentName
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row
|
Row
|
||||||
@ -266,12 +294,13 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||||
}
|
}
|
||||||
Item // Spacer
|
Item // Spacer
|
||||||
{
|
{
|
||||||
height: spacerHeight
|
height: doubleSpacerHeight
|
||||||
width: height
|
width: height
|
||||||
}
|
}
|
||||||
Row
|
Row
|
||||||
@ -297,12 +326,13 @@ UM.Dialog
|
|||||||
height: visible ? comboboxHeight : 0
|
height: visible ? comboboxHeight : 0
|
||||||
visible: manager.materialConflict
|
visible: manager.materialConflict
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||||
ComboBox
|
Cura.ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: materialResolveComboBox
|
id: materialResolveComboBox
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("button").height
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||||
@ -327,13 +357,14 @@ UM.Dialog
|
|||||||
{
|
{
|
||||||
text: modelData
|
text: modelData
|
||||||
width: (parent.width / 3) | 0
|
width: (parent.width / 3) | 0
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item // Spacer
|
Item // Spacer
|
||||||
{
|
{
|
||||||
height: spacerHeight
|
height: doubleSpacerHeight
|
||||||
width: height
|
width: height
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,6 +431,14 @@ UM.Dialog
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: buttonsItem
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
id: cancel_button
|
id: cancel_button
|
||||||
@ -413,13 +452,14 @@ UM.Dialog
|
|||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
id: ok_button
|
id: ok_button
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
text: catalog.i18nc("@action:button","Open");
|
text: catalog.i18nc("@action:button","Open");
|
||||||
onClicked: { manager.closeBackend(); manager.onOkButtonClicked() }
|
onClicked: { manager.closeBackend(); manager.onOkButtonClicked() }
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function accept() {
|
function accept() {
|
||||||
manager.closeBackend();
|
manager.closeBackend();
|
||||||
manager.onOkButtonClicked();
|
manager.onOkButtonClicked();
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for reading 3MF files.",
|
"description": "Provides support for reading 3MF files.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
# Write preferences to archive
|
# Write preferences to archive
|
||||||
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
||||||
temp_preferences = Preferences()
|
temp_preferences = Preferences()
|
||||||
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded"}:
|
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}:
|
||||||
temp_preferences.addPreference(preference, None)
|
temp_preferences.addPreference(preference, None)
|
||||||
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
||||||
preferences_string = StringIO()
|
preferences_string = StringIO()
|
||||||
@ -92,6 +92,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||||
Logger.error("No permission to write workspace to this stream.")
|
Logger.error("No permission to write workspace to this stream.")
|
||||||
return False
|
return False
|
||||||
|
except EnvironmentError as e:
|
||||||
|
self.setInformation(catalog.i18nc("@error:zip", "The operating system does not allow saving a project file to this location or with this file name."))
|
||||||
|
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e)))
|
||||||
|
return False
|
||||||
mesh_writer.setStoreArchive(False)
|
mesh_writer.setStoreArchive(False)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -136,7 +140,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||||||
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
# Do not include the network authentication keys
|
# Do not include the network authentication keys
|
||||||
ignore_keys = {"network_authentication_id", "network_authentication_key", "octoprint_api_key"}
|
ignore_keys = {
|
||||||
|
"um_cloud_cluster_id",
|
||||||
|
"um_network_key",
|
||||||
|
"um_linked_to_account",
|
||||||
|
"removal_warning",
|
||||||
|
"host_guid",
|
||||||
|
"group_name",
|
||||||
|
"group_size",
|
||||||
|
"connection_type",
|
||||||
|
"octoprint_api_key"
|
||||||
|
}
|
||||||
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
|
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
|
||||||
|
|
||||||
archive.writestr(file_in_archive, serialized_data)
|
archive.writestr(file_in_archive, serialized_data)
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for writing 3MF files.",
|
"description": "Provides support for writing 3MF files.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,5 +3,5 @@
|
|||||||
"author": "fieldOfView",
|
"author": "fieldOfView",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Provides support for reading AMF files.",
|
"description": "Provides support for reading AMF files.",
|
||||||
"api": "7.2.0"
|
"api": "7.4.0"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Backup and restore your configuration.",
|
"description": "Backup and restore your configuration.",
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
default_engine_location = execpath
|
default_engine_location = execpath
|
||||||
break
|
break
|
||||||
|
|
||||||
self._application = CuraApplication.getInstance() #type: CuraApplication
|
application = CuraApplication.getInstance() #type: CuraApplication
|
||||||
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||||
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
||||||
|
|
||||||
default_engine_location = os.path.abspath(default_engine_location)
|
default_engine_location = os.path.abspath(default_engine_location)
|
||||||
self._application.getPreferences().addPreference("backend/location", default_engine_location)
|
application.getPreferences().addPreference("backend/location", default_engine_location)
|
||||||
|
|
||||||
# Workaround to disable layer view processing if layer view is not active.
|
# Workaround to disable layer view processing if layer view is not active.
|
||||||
self._layer_view_active = False #type: bool
|
self._layer_view_active = False #type: bool
|
||||||
@ -101,7 +101,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
|
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
|
||||||
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||||
|
|
||||||
self._scene = self._application.getController().getScene() #type: Scene
|
self._scene = application.getController().getScene() #type: Scene
|
||||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
||||||
@ -141,7 +141,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self._slice_start_time = None #type: Optional[float]
|
self._slice_start_time = None #type: Optional[float]
|
||||||
self._is_disabled = False #type: bool
|
self._is_disabled = False #type: bool
|
||||||
|
|
||||||
self._application.getPreferences().addPreference("general/auto_slice", False)
|
application.getPreferences().addPreference("general/auto_slice", False)
|
||||||
|
|
||||||
self._use_timer = False #type: bool
|
self._use_timer = False #type: bool
|
||||||
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
||||||
@ -151,19 +151,20 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self._change_timer.setSingleShot(True)
|
self._change_timer.setSingleShot(True)
|
||||||
self._change_timer.setInterval(500)
|
self._change_timer.setInterval(500)
|
||||||
self.determineAutoSlicing()
|
self.determineAutoSlicing()
|
||||||
self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
self._application.initializationFinished.connect(self.initialize)
|
application.initializationFinished.connect(self.initialize)
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
application = CuraApplication.getInstance()
|
||||||
|
self._multi_build_plate_model = application.getMultiBuildPlateModel()
|
||||||
|
|
||||||
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
|
application.getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
||||||
if self._multi_build_plate_model:
|
if self._multi_build_plate_model:
|
||||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
||||||
self._application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
|
application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
|
||||||
self._onGlobalStackChanged()
|
self._onGlobalStackChanged()
|
||||||
|
|
||||||
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
|
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
|
||||||
@ -173,10 +174,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self.backendConnected.connect(self._onBackendConnected)
|
self.backendConnected.connect(self._onBackendConnected)
|
||||||
|
|
||||||
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
|
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
|
||||||
self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
|
application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
|
||||||
self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||||
|
|
||||||
self._machine_error_checker = self._application.getMachineErrorChecker()
|
self._machine_error_checker = application.getMachineErrorChecker()
|
||||||
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
|
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
@ -195,7 +196,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
This is useful for debugging and used to actually start the engine.
|
This is useful for debugging and used to actually start the engine.
|
||||||
:return: list of commands and args / parameters.
|
:return: list of commands and args / parameters.
|
||||||
"""
|
"""
|
||||||
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||||
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||||
@ -259,7 +260,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
|
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
|
||||||
|
|
||||||
# see if we really have to slice
|
# see if we really have to slice
|
||||||
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
application = CuraApplication.getInstance()
|
||||||
|
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
|
||||||
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
|
||||||
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
|
||||||
num_objects = self._numObjectsPerBuildPlate()
|
num_objects = self._numObjectsPerBuildPlate()
|
||||||
@ -274,8 +276,8 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self.slice()
|
self.slice()
|
||||||
return
|
return
|
||||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||||
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||||
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||||
|
|
||||||
if self._process is None: # type: ignore
|
if self._process is None: # type: ignore
|
||||||
self._createSocket()
|
self._createSocket()
|
||||||
@ -314,7 +316,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self.processingProgress.emit(0)
|
self.processingProgress.emit(0)
|
||||||
Logger.log("d", "Attempting to kill the engine process")
|
Logger.log("d", "Attempting to kill the engine process")
|
||||||
|
|
||||||
if self._application.getUseExternalBackend():
|
if CuraApplication.getInstance().getUseExternalBackend():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._process is not None: # type: ignore
|
if self._process is not None: # type: ignore
|
||||||
@ -350,8 +352,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
if job.getResult() == StartJobResult.MaterialIncompatible:
|
if job.getResult() == StartJobResult.MaterialIncompatible:
|
||||||
if self._application.platformActivity:
|
if application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status",
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
@ -362,7 +365,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.SettingError:
|
if job.getResult() == StartJobResult.SettingError:
|
||||||
if self._application.platformActivity:
|
if application.platformActivity:
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
|
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
|
||||||
return
|
return
|
||||||
@ -394,7 +397,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
|
|
||||||
elif job.getResult() == StartJobResult.ObjectSettingError:
|
elif job.getResult() == StartJobResult.ObjectSettingError:
|
||||||
errors = {}
|
errors = {}
|
||||||
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(application.getController().getScene().getRoot()):
|
||||||
stack = node.callDecoration("getStack")
|
stack = node.callDecoration("getStack")
|
||||||
if not stack:
|
if not stack:
|
||||||
continue
|
continue
|
||||||
@ -415,7 +418,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.BuildPlateError:
|
if job.getResult() == StartJobResult.BuildPlateError:
|
||||||
if self._application.platformActivity:
|
if application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
@ -433,7 +436,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.NothingToSlice:
|
if job.getResult() == StartJobResult.NothingToSlice:
|
||||||
if self._application.platformActivity:
|
if application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
|
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
|
||||||
"\n- Fit within the build volume"
|
"\n- Fit within the build volume"
|
||||||
"\n- Are assigned to an enabled extruder"
|
"\n- Are assigned to an enabled extruder"
|
||||||
@ -466,7 +469,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
enable_timer = True
|
enable_timer = True
|
||||||
self._is_disabled = False
|
self._is_disabled = False
|
||||||
|
|
||||||
if not self._application.getPreferences().getValue("general/auto_slice"):
|
if not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("isBlockSlicing"):
|
if node.callDecoration("isBlockSlicing"):
|
||||||
@ -560,7 +563,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
:param error: The exception that occurred.
|
:param error: The exception that occurred.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self._application.isShuttingDown():
|
if CuraApplication.getInstance().isShuttingDown():
|
||||||
return
|
return
|
||||||
|
|
||||||
super()._onSocketError(error)
|
super()._onSocketError(error)
|
||||||
@ -600,7 +603,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
cast(SceneNode, node.getParent()).removeChild(node)
|
cast(SceneNode, node.getParent()).removeChild(node)
|
||||||
|
|
||||||
def markSliceAll(self) -> None:
|
def markSliceAll(self) -> None:
|
||||||
for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
|
for build_plate_number in range(CuraApplication.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
|
||||||
if build_plate_number not in self._build_plates_to_be_sliced:
|
if build_plate_number not in self._build_plates_to_be_sliced:
|
||||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||||
|
|
||||||
@ -696,12 +699,13 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
||||||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||||
gcode_list = []
|
gcode_list = []
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
for index, line in enumerate(gcode_list):
|
for index, line in enumerate(gcode_list):
|
||||||
replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
replaced = line.replace("{print_time}", str(application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
||||||
replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths))
|
replaced = replaced.replace("{filament_amount}", str(application.getPrintInformation().materialLengths))
|
||||||
replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights))
|
replaced = replaced.replace("{filament_weight}", str(application.getPrintInformation().materialWeights))
|
||||||
replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts))
|
replaced = replaced.replace("{filament_cost}", str(application.getPrintInformation().materialCosts))
|
||||||
replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName))
|
replaced = replaced.replace("{jobname}", str(application.getPrintInformation().jobName))
|
||||||
|
|
||||||
gcode_list[index] = replaced
|
gcode_list[index] = replaced
|
||||||
|
|
||||||
@ -711,7 +715,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
|
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
|
||||||
|
|
||||||
# See if we need to process the sliced layers job.
|
# See if we need to process the sliced layers job.
|
||||||
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
|
||||||
if (
|
if (
|
||||||
self._layer_view_active and
|
self._layer_view_active and
|
||||||
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
|
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
|
||||||
@ -870,9 +874,9 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
def _onActiveViewChanged(self) -> None:
|
def _onActiveViewChanged(self) -> None:
|
||||||
"""Called when the user changes the active view mode."""
|
"""Called when the user changes the active view mode."""
|
||||||
|
|
||||||
view = self._application.getController().getActiveView()
|
view = CuraApplication.getInstance().getController().getActiveView()
|
||||||
if view:
|
if view:
|
||||||
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
|
||||||
self._layer_view_active = True
|
self._layer_view_active = True
|
||||||
# There is data and we're not slicing at the moment
|
# There is data and we're not slicing at the moment
|
||||||
@ -909,7 +913,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||||||
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
||||||
extruder.containersChanged.disconnect(self._onChanged)
|
extruder.containersChanged.disconnect(self._onChanged)
|
||||||
|
|
||||||
self._global_container_stack = self._application.getMachineManager().activeMachine
|
self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine
|
||||||
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
||||||
|
@ -205,10 +205,6 @@ class StartSliceJob(Job):
|
|||||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||||
temp_list = []
|
temp_list = []
|
||||||
|
|
||||||
# Node can't be printed, so don't bother sending it.
|
|
||||||
if getattr(node, "_outside_buildarea", False):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Filter on current build plate
|
# Filter on current build plate
|
||||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "CuraEngine Backend",
|
"name": "CuraEngine Backend",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing Cura profiles.",
|
"description": "Provides support for importing Cura profiles.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for exporting Cura profiles.",
|
"description": "Provides support for exporting Cura profiles.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog":"cura"
|
"i18n-catalog":"cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Checks for firmware updates.",
|
"description": "Checks for firmware updates.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a machine actions for updating firmware.",
|
"description": "Provides a machine actions for updating firmware.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
@ -19,7 +19,7 @@ class GCodeGzReader(MeshReader):
|
|||||||
MimeTypeDatabase.addMimeType(
|
MimeTypeDatabase.addMimeType(
|
||||||
MimeType(
|
MimeType(
|
||||||
name = "application/x-cura-compressed-gcode-file",
|
name = "application/x-cura-compressed-gcode-file",
|
||||||
comment = "Cura Compressed GCode File",
|
comment = "Cura Compressed G-code File",
|
||||||
suffixes = ["gcode.gz"]
|
suffixes = ["gcode.gz"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Reads g-code from a compressed archive.",
|
"description": "Reads g-code from a compressed archive.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Writes g-code to a compressed archive.",
|
"description": "Writes g-code to a compressed archive.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import re #Regular expressions for parsing escape characters in the settings.
|
import re # Regular expressions for parsing escape characters in the settings.
|
||||||
import json
|
import json
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -9,9 +9,10 @@ from UM.Settings.ContainerFormatError import ContainerFormatError
|
|||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException
|
|
||||||
|
|
||||||
class GCodeProfileReader(ProfileReader):
|
class GCodeProfileReader(ProfileReader):
|
||||||
"""A class that reads profile data from g-code files.
|
"""A class that reads profile data from g-code files.
|
||||||
@ -29,9 +30,9 @@ class GCodeProfileReader(ProfileReader):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
escape_characters = {
|
escape_characters = {
|
||||||
re.escape("\\\\"): "\\", #The escape character.
|
re.escape("\\\\"): "\\", # The escape character.
|
||||||
re.escape("\\n"): "\n", #Newlines. They break off the comment.
|
re.escape("\\n"): "\n", # Newlines. They break off the comment.
|
||||||
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
|
re.escape("\\r"): "\r" # Carriage return. Windows users may need this for visualisation in their editors.
|
||||||
}
|
}
|
||||||
"""Dictionary that defines how characters are escaped when embedded in
|
"""Dictionary that defines how characters are escaped when embedded in
|
||||||
|
|
||||||
@ -41,11 +42,6 @@ class GCodeProfileReader(ProfileReader):
|
|||||||
not.
|
not.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialises the g-code reader as a profile reader."""
|
|
||||||
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
def read(self, file_name):
|
def read(self, file_name):
|
||||||
"""Reads a g-code file, loading the profile from it.
|
"""Reads a g-code file, loading the profile from it.
|
||||||
|
|
||||||
@ -54,6 +50,7 @@ class GCodeProfileReader(ProfileReader):
|
|||||||
specified file was no g-code or contained no parsable profile,
|
specified file was no g-code or contained no parsable profile,
|
||||||
None is returned.
|
None is returned.
|
||||||
"""
|
"""
|
||||||
|
Logger.log("i", "Attempting to read a profile from the g-code")
|
||||||
|
|
||||||
if file_name.split(".")[-1] != "gcode":
|
if file_name.split(".")[-1] != "gcode":
|
||||||
return None
|
return None
|
||||||
@ -70,7 +67,7 @@ class GCodeProfileReader(ProfileReader):
|
|||||||
for line in f:
|
for line in f:
|
||||||
if line.startswith(prefix):
|
if line.startswith(prefix):
|
||||||
# Remove the prefix and the newline from the line and add it to the rest.
|
# Remove the prefix and the newline from the line and add it to the rest.
|
||||||
serialized += line[prefix_length : -1]
|
serialized += line[prefix_length: -1]
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
|
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
|
||||||
return None
|
return None
|
||||||
@ -79,10 +76,10 @@ class GCodeProfileReader(ProfileReader):
|
|||||||
serialized = serialized.strip()
|
serialized = serialized.strip()
|
||||||
|
|
||||||
if not serialized:
|
if not serialized:
|
||||||
Logger.log("i", "No custom profile to import from this g-code: %s", file_name)
|
Logger.log("w", "No custom profile to import from this g-code: %s", file_name)
|
||||||
raise NoProfileException()
|
raise NoProfileException()
|
||||||
|
|
||||||
# serialized data can be invalid JSON
|
# Serialized data can be invalid JSON
|
||||||
try:
|
try:
|
||||||
json_data = json.loads(serialized)
|
json_data = json.loads(serialized)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing profiles from g-code files.",
|
"description": "Provides support for importing profiles from g-code files.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -312,7 +312,7 @@ class FlavorParser:
|
|||||||
# F5, that gcode SceneNode will be removed because it doesn't have a file to be reloaded from.
|
# F5, that gcode SceneNode will be removed because it doesn't have a file to be reloaded from.
|
||||||
#
|
#
|
||||||
def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
|
def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
|
||||||
Logger.log("d", "Preparing to load GCode")
|
Logger.log("d", "Preparing to load g-code")
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
# We obtain the filament diameter from the selected extruder to calculate line widths
|
# We obtain the filament diameter from the selected extruder to calculate line widths
|
||||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
@ -352,7 +352,7 @@ class FlavorParser:
|
|||||||
self._message.setProgress(0)
|
self._message.setProgress(0)
|
||||||
self._message.show()
|
self._message.show()
|
||||||
|
|
||||||
Logger.log("d", "Parsing Gcode...")
|
Logger.log("d", "Parsing g-code...")
|
||||||
|
|
||||||
current_position = Position(0, 0, 0, 0, [0])
|
current_position = Position(0, 0, 0, 0, [0])
|
||||||
current_path = [] #type: List[List[float]]
|
current_path = [] #type: List[List[float]]
|
||||||
@ -363,7 +363,7 @@ class FlavorParser:
|
|||||||
|
|
||||||
for line in stream.split("\n"):
|
for line in stream.split("\n"):
|
||||||
if self._cancelled:
|
if self._cancelled:
|
||||||
Logger.log("d", "Parsing Gcode file cancelled")
|
Logger.log("d", "Parsing g-code file cancelled.")
|
||||||
return None
|
return None
|
||||||
current_line += 1
|
current_line += 1
|
||||||
|
|
||||||
@ -482,7 +482,7 @@ class FlavorParser:
|
|||||||
gcode_dict = {active_build_plate_id: gcode_list}
|
gcode_dict = {active_build_plate_id: gcode_list}
|
||||||
CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
|
CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
|
||||||
|
|
||||||
Logger.log("d", "Finished parsing Gcode")
|
Logger.log("d", "Finished parsing g-code.")
|
||||||
self._message.hide()
|
self._message.hide()
|
||||||
|
|
||||||
if self._layer_number == 0:
|
if self._layer_number == 0:
|
||||||
@ -493,7 +493,7 @@ class FlavorParser:
|
|||||||
machine_depth = global_stack.getProperty("machine_depth", "value")
|
machine_depth = global_stack.getProperty("machine_depth", "value")
|
||||||
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
||||||
|
|
||||||
Logger.log("d", "GCode loading finished")
|
Logger.log("d", "G-code loading finished.")
|
||||||
|
|
||||||
if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
|
if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
|
||||||
caution_message = Message(catalog.i18nc(
|
caution_message = Message(catalog.i18nc(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Copyright (c) 2017 Aleph Objects, Inc.
|
# Copyright (c) 2017 Aleph Objects, Inc.
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, Union, List, TYPE_CHECKING
|
from typing import Optional, Union, List, TYPE_CHECKING
|
||||||
@ -32,7 +32,7 @@ class GCodeReader(MeshReader):
|
|||||||
MimeTypeDatabase.addMimeType(
|
MimeTypeDatabase.addMimeType(
|
||||||
MimeType(
|
MimeType(
|
||||||
name = "application/x-cura-gcode-file",
|
name = "application/x-cura-gcode-file",
|
||||||
comment = "Cura GCode File",
|
comment = "Cura G-code File",
|
||||||
suffixes = ["gcode"]
|
suffixes = ["gcode"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Victor Larchenko, Ultimaker B.V.",
|
"author": "Victor Larchenko, Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Allows loading and displaying G-code files.",
|
"description": "Allows loading and displaying G-code files.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -143,6 +143,7 @@ class GCodeWriter(MeshWriter):
|
|||||||
if stack.getMetaDataEntry("position") is not None: # For extruder stacks, the quality changes should include an intent category.
|
if stack.getMetaDataEntry("position") is not None: # For extruder stacks, the quality changes should include an intent category.
|
||||||
container_with_profile.setMetaDataEntry("intent_category", stack.intent.getMetaDataEntry("intent_category", "default"))
|
container_with_profile.setMetaDataEntry("intent_category", stack.intent.getMetaDataEntry("intent_category", "default"))
|
||||||
container_with_profile.setDefinition(machine_definition_id_for_quality)
|
container_with_profile.setDefinition(machine_definition_id_for_quality)
|
||||||
|
container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
|
||||||
|
|
||||||
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
|
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
|
||||||
# If the quality changes is not set, we need to set type manually
|
# If the quality changes is not set, we need to set type manually
|
||||||
@ -171,6 +172,7 @@ class GCodeWriter(MeshWriter):
|
|||||||
extruder_quality.setMetaDataEntry("type", "quality_changes")
|
extruder_quality.setMetaDataEntry("type", "quality_changes")
|
||||||
extruder_quality.setMetaDataEntry("quality_type", quality_type)
|
extruder_quality.setMetaDataEntry("quality_type", quality_type)
|
||||||
extruder_quality.setDefinition(machine_definition_id_for_quality)
|
extruder_quality.setDefinition(machine_definition_id_for_quality)
|
||||||
|
extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
|
||||||
|
|
||||||
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
|
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
|
||||||
# If the quality changes is not set, we need to set type manually
|
# If the quality changes is not set, we need to set type manually
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Writes g-code to a file.",
|
"description": "Writes g-code to a file.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ class MachineSettingsAction(MachineAction):
|
|||||||
# Force rebuilding the build volume by reloading the global container stack.
|
# Force rebuilding the build volume by reloading the global container stack.
|
||||||
# This is a bit of a hack, but it seems quick enough.
|
# This is a bit of a hack, but it seems quick enough.
|
||||||
self._application.getMachineManager().globalContainerChanged.emit()
|
self._application.getMachineManager().globalContainerChanged.emit()
|
||||||
|
self._application.getMachineManager().forceUpdateAllSettings()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def updateHasMaterialsMetadata(self) -> None:
|
def updateHasMaterialsMetadata(self) -> None:
|
||||||
|
@ -330,6 +330,14 @@ Item
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The "Shared Heater" feature is temporarily disabled because its
|
||||||
|
implementation is incomplete. Printers with multiple filaments going
|
||||||
|
into one nozzle will keep the inactive filaments retracted at the
|
||||||
|
start of a print. However CuraEngine assumes that all filaments
|
||||||
|
start at the nozzle tip. So it'll start printing the second filament
|
||||||
|
without unretracting it.
|
||||||
|
See: https://github.com/Ultimaker/Cura/issues/8148
|
||||||
|
|
||||||
Cura.SimpleCheckBox // "Shared Heater"
|
Cura.SimpleCheckBox // "Shared Heater"
|
||||||
{
|
{
|
||||||
id: sharedHeaterCheckBox
|
id: sharedHeaterCheckBox
|
||||||
@ -341,6 +349,7 @@ Item
|
|||||||
labelWidth: base.labelWidth
|
labelWidth: base.labelWidth
|
||||||
forceUpdateOnChangeFunction: forceUpdateFunction
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "fieldOfView, Ultimaker B.V.",
|
"author": "fieldOfView, Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ class ModelChecker(QObject, Extension):
|
|||||||
self._createView()
|
self._createView()
|
||||||
|
|
||||||
def checkObjectsForShrinkage(self):
|
def checkObjectsForShrinkage(self):
|
||||||
shrinkage_threshold = 0.5 #From what shrinkage percentage a warning will be issued about the model size.
|
shrinkage_threshold = 100.5 #From what shrinkage percentage a warning will be issued about the model size.
|
||||||
warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials.
|
warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials.
|
||||||
warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials.
|
warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials.
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ class ModelChecker(QObject, Extension):
|
|||||||
Application.getInstance().callLater(lambda: self.onChanged.emit())
|
Application.getInstance().callLater(lambda: self.onChanged.emit())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if material_shrinkage[node_extruder_position] > shrinkage_threshold:
|
if material_shrinkage > shrinkage_threshold:
|
||||||
bbox = node.getBoundingBox()
|
bbox = node.getBoundingBox()
|
||||||
if bbox is not None and (bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z):
|
if bbox is not None and (bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z):
|
||||||
warning_nodes.append(node)
|
warning_nodes.append(node)
|
||||||
@ -134,16 +134,8 @@ class ModelChecker(QObject, Extension):
|
|||||||
def showWarnings(self):
|
def showWarnings(self):
|
||||||
self._caution_message.show()
|
self._caution_message.show()
|
||||||
|
|
||||||
def _getMaterialShrinkage(self):
|
def _getMaterialShrinkage(self) -> float:
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack is None:
|
if global_container_stack is None:
|
||||||
return {}
|
return 100
|
||||||
|
return global_container_stack.getProperty("material_shrinkage_percentage", "value")
|
||||||
material_shrinkage = {}
|
|
||||||
# Get all shrinkage values of materials used
|
|
||||||
for extruder_position, extruder in enumerate(global_container_stack.extruderList):
|
|
||||||
shrinkage = extruder.material.getProperty("material_shrinkage_percentage", "value")
|
|
||||||
if shrinkage is None:
|
|
||||||
shrinkage = 0
|
|
||||||
material_shrinkage[str(extruder_position)] = shrinkage
|
|
||||||
return material_shrinkage
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "Model Checker",
|
"name": "Model Checker",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ Rectangle
|
|||||||
visible: isNetworkConfigured && !isConnected
|
visible: isNetworkConfigured && !isConnected
|
||||||
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.")
|
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("text")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
|
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
|
||||||
lineHeightMode: Text.FixedHeight
|
lineHeightMode: Text.FixedHeight
|
||||||
@ -116,7 +116,7 @@ Rectangle
|
|||||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||||
text: catalog.i18nc("@info", "Please connect your printer to the network.")
|
text: catalog.i18nc("@info", "Please connect your printer to the network.")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("text")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: contentWidth
|
width: contentWidth
|
||||||
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
|
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
|
||||||
@ -136,7 +136,7 @@ Rectangle
|
|||||||
{
|
{
|
||||||
id: externalLinkIcon
|
id: externalLinkIcon
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
color: UM.Theme.getColor("monitor_text_link")
|
color: UM.Theme.getColor("text_link")
|
||||||
source: UM.Theme.getIcon("external_link")
|
source: UM.Theme.getIcon("external_link")
|
||||||
width: UM.Theme.getSize("monitor_external_link_icon").width
|
width: UM.Theme.getSize("monitor_external_link_icon").width
|
||||||
height: UM.Theme.getSize("monitor_external_link_icon").height
|
height: UM.Theme.getSize("monitor_external_link_icon").height
|
||||||
@ -150,9 +150,8 @@ Rectangle
|
|||||||
leftMargin: UM.Theme.getSize("narrow_margin").width
|
leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||||
verticalCenter: externalLinkIcon.verticalCenter
|
verticalCenter: externalLinkIcon.verticalCenter
|
||||||
}
|
}
|
||||||
color: UM.Theme.getColor("monitor_text_link")
|
color: UM.Theme.getColor("text_link")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
linkColor: UM.Theme.getColor("monitor_text_link")
|
|
||||||
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a monitor stage in Cura.",
|
"description": "Provides a monitor stage in Cura.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
@ -103,17 +103,20 @@ class PerObjectSettingsTool(Tool):
|
|||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
settings.addInstance(new_instance)
|
settings.addInstance(new_instance)
|
||||||
|
|
||||||
for property_key in ["top_bottom_thickness", "wall_thickness"]:
|
for property_key in ["top_bottom_thickness", "wall_thickness", "wall_line_count"]:
|
||||||
if mesh_type == "infill_mesh":
|
if mesh_type == "infill_mesh":
|
||||||
if settings.getInstance(property_key) is None:
|
if settings.getInstance(property_key) is None:
|
||||||
definition = stack.getSettingDefinition(property_key)
|
definition = stack.getSettingDefinition(property_key)
|
||||||
new_instance = SettingInstance(definition, settings)
|
new_instance = SettingInstance(definition, settings)
|
||||||
new_instance.setProperty("value", 0)
|
# We just want the wall_line count to be there in case it was overriden in the global stack.
|
||||||
|
# as such, we don't need to set a value.
|
||||||
|
if property_key != "wall_line_count":
|
||||||
|
new_instance.setProperty("value", 0)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
settings.addInstance(new_instance)
|
settings.addInstance(new_instance)
|
||||||
settings_visibility_changed = True
|
settings_visibility_changed = True
|
||||||
|
|
||||||
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and settings.getProperty(property_key, "value") == 0:
|
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and (settings.getProperty(property_key, "value") == 0 or property_key == "wall_line_count"):
|
||||||
settings.removeInstance(property_key)
|
settings.removeInstance(property_key)
|
||||||
settings_visibility_changed = True
|
settings_visibility_changed = True
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides the Per Model Settings.",
|
"description": "Provides the Per Model Settings.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -261,7 +261,11 @@ class PostProcessingPlugin(QObject, Extension):
|
|||||||
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
|
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
|
||||||
script_parser = configparser.ConfigParser(interpolation=None)
|
script_parser = configparser.ConfigParser(interpolation=None)
|
||||||
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
|
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
|
||||||
script_parser.read_string(script_str)
|
try:
|
||||||
|
script_parser.read_string(script_str)
|
||||||
|
except configparser.Error as e:
|
||||||
|
Logger.error("Stored post-processing scripts have syntax errors: {err}".format(err = str(e)))
|
||||||
|
continue
|
||||||
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
|
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
|
||||||
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
|
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
|
||||||
continue
|
continue
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
# tries to create PyQt objects on a non-main thread.
|
# tries to create PyQt objects on a non-main thread.
|
||||||
import Arcus # @UnusedImport
|
import Arcus # @UnusedImport
|
||||||
import Savitar # @UnusedImport
|
import Savitar # @UnusedImport
|
||||||
|
import pynest2d # @UnusedImport
|
||||||
|
|
||||||
from . import PostProcessingPlugin
|
from . import PostProcessingPlugin
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "Post Processing",
|
"name": "Post Processing",
|
||||||
"author": "Ultimaker",
|
"author": "Ultimaker",
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"description": "Extension that allows for user created scripts for post processing",
|
"description": "Extension that allows for user created scripts for post processing",
|
||||||
"catalog": "cura"
|
"catalog": "cura"
|
||||||
}
|
}
|
@ -51,7 +51,7 @@
|
|||||||
# M207 S<mm> F<mm/m> - set the retract length <S> or feed rate <F>
|
# M207 S<mm> F<mm/m> - set the retract length <S> or feed rate <F>
|
||||||
# M117 - output the current changes
|
# M117 - output the current changes
|
||||||
|
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Dict
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@ -336,7 +336,7 @@ class ChangeAtZ(Script):
|
|||||||
|
|
||||||
caz_instance = ChangeAtZProcessor()
|
caz_instance = ChangeAtZProcessor()
|
||||||
|
|
||||||
caz_instance.TargetValues = {}
|
caz_instance.targetValues = {}
|
||||||
|
|
||||||
# copy over our settings to our change z class
|
# copy over our settings to our change z class
|
||||||
self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed")
|
self.setIntSettingIfEnabled(caz_instance, "e1_Change_speed", "speed", "e2_speed")
|
||||||
@ -352,23 +352,23 @@ class ChangeAtZ(Script):
|
|||||||
self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
|
self.setFloatSettingIfEnabled(caz_instance, "caz_change_retractlength", "retractlength", "caz_retractlength")
|
||||||
|
|
||||||
# is this mod enabled?
|
# is this mod enabled?
|
||||||
caz_instance.IsEnabled = self.getSettingValueByKey("caz_enabled")
|
caz_instance.enabled = self.getSettingValueByKey("caz_enabled")
|
||||||
|
|
||||||
# are we emitting data to the LCD?
|
# are we emitting data to the LCD?
|
||||||
caz_instance.IsDisplayingChangesToLcd = self.getSettingValueByKey("caz_output_to_display")
|
caz_instance.displayChangesToLcd = self.getSettingValueByKey("caz_output_to_display")
|
||||||
|
|
||||||
# are we doing linear move retractions?
|
# are we doing linear move retractions?
|
||||||
caz_instance.IsLinearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear"
|
caz_instance.linearRetraction = self.getSettingValueByKey("caz_retractstyle") == "linear"
|
||||||
|
|
||||||
# see if we're applying to a single layer or to all layers hence forth
|
# see if we're applying to a single layer or to all layers hence forth
|
||||||
caz_instance.IsApplyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
|
caz_instance.applyToSingleLayer = self.getSettingValueByKey("c_behavior") == "single_layer"
|
||||||
|
|
||||||
# used for easy reference of layer or height targeting
|
# used for easy reference of layer or height targeting
|
||||||
caz_instance.IsTargetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
|
caz_instance.targetByLayer = self.getSettingValueByKey("a_trigger") == "layer_no"
|
||||||
|
|
||||||
# change our target based on what we're targeting
|
# change our target based on what we're targeting
|
||||||
caz_instance.TargetLayer = self.getIntSettingByKey("b_targetL", None)
|
caz_instance.targetLayer = self.getIntSettingByKey("b_targetL", None)
|
||||||
caz_instance.TargetZ = self.getFloatSettingByKey("b_targetZ", None)
|
caz_instance.targetZ = self.getFloatSettingByKey("b_targetZ", None)
|
||||||
|
|
||||||
# run our script
|
# run our script
|
||||||
return caz_instance.execute(data)
|
return caz_instance.execute(data)
|
||||||
@ -388,7 +388,7 @@ class ChangeAtZ(Script):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# set our value in the target settings
|
# set our value in the target settings
|
||||||
caz_instance.TargetValues[target] = value
|
caz_instance.targetValues[target] = value
|
||||||
|
|
||||||
# Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
|
# Sets the given TargetValue in the ChangeAtZ instance if the trigger is specified
|
||||||
def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting):
|
def setFloatSettingIfEnabled(self, caz_instance, trigger, target, setting):
|
||||||
@ -405,7 +405,7 @@ class ChangeAtZ(Script):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# set our value in the target settings
|
# set our value in the target settings
|
||||||
caz_instance.TargetValues[target] = value
|
caz_instance.targetValues[target] = value
|
||||||
|
|
||||||
# Returns the given settings value as an integer or the default if it cannot parse it
|
# Returns the given settings value as an integer or the default if it cannot parse it
|
||||||
def getIntSettingByKey(self, key, default):
|
def getIntSettingByKey(self, key, default):
|
||||||
@ -430,13 +430,13 @@ class ChangeAtZ(Script):
|
|||||||
class GCodeCommand:
|
class GCodeCommand:
|
||||||
|
|
||||||
# The GCode command itself (ex: G10)
|
# The GCode command itself (ex: G10)
|
||||||
Command = None,
|
command = None,
|
||||||
|
|
||||||
# Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument.
|
# Contains any arguments passed to the command. The key is the argument name, the value is the value of the argument.
|
||||||
Arguments = {}
|
arguments = {}
|
||||||
|
|
||||||
# Contains the components of the command broken into pieces
|
# Contains the components of the command broken into pieces
|
||||||
Components = []
|
components = []
|
||||||
|
|
||||||
# Constructor. Sets up defaults
|
# Constructor. Sets up defaults
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -468,10 +468,10 @@ class GCodeCommand:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# stores all the components of the command within the class for later
|
# stores all the components of the command within the class for later
|
||||||
command.Components = command_pieces
|
command.components = command_pieces
|
||||||
|
|
||||||
# set the actual command
|
# set the actual command
|
||||||
command.Command = command_pieces[0]
|
command.command = command_pieces[0]
|
||||||
|
|
||||||
# stop here if we don't have any parameters
|
# stop here if we don't have any parameters
|
||||||
if len(command_pieces) == 1:
|
if len(command_pieces) == 1:
|
||||||
@ -488,15 +488,15 @@ class GCodeCommand:
|
|||||||
linear_command = GCodeCommand.getFromLine(line)
|
linear_command = GCodeCommand.getFromLine(line)
|
||||||
|
|
||||||
# if it's not a linear move, we don't care
|
# if it's not a linear move, we don't care
|
||||||
if linear_command is None or (linear_command.Command != "G0" and linear_command.Command != "G1"):
|
if linear_command is None or (linear_command.command != "G0" and linear_command.command != "G1"):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# convert our values to floats (or defaults)
|
# convert our values to floats (or defaults)
|
||||||
linear_command.Arguments["F"] = linear_command.getArgumentAsFloat("F", None)
|
linear_command.arguments["F"] = linear_command.getArgumentAsFloat("F", None)
|
||||||
linear_command.Arguments["X"] = linear_command.getArgumentAsFloat("X", None)
|
linear_command.arguments["X"] = linear_command.getArgumentAsFloat("X", None)
|
||||||
linear_command.Arguments["Y"] = linear_command.getArgumentAsFloat("Y", None)
|
linear_command.arguments["Y"] = linear_command.getArgumentAsFloat("Y", None)
|
||||||
linear_command.Arguments["Z"] = linear_command.getArgumentAsFloat("Z", None)
|
linear_command.arguments["Z"] = linear_command.getArgumentAsFloat("Z", None)
|
||||||
linear_command.Arguments["E"] = linear_command.getArgumentAsFloat("E", None)
|
linear_command.arguments["E"] = linear_command.getArgumentAsFloat("E", None)
|
||||||
|
|
||||||
# return our new command
|
# return our new command
|
||||||
return linear_command
|
return linear_command
|
||||||
@ -508,11 +508,11 @@ class GCodeCommand:
|
|||||||
self.parseArguments()
|
self.parseArguments()
|
||||||
|
|
||||||
# if we don't have the parameter, return the default
|
# if we don't have the parameter, return the default
|
||||||
if name not in self.Arguments:
|
if name not in self.arguments:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
# otherwise return the value
|
# otherwise return the value
|
||||||
return self.Arguments[name]
|
return self.arguments[name]
|
||||||
|
|
||||||
# Gets the value of a parameter as a float or returns the default
|
# Gets the value of a parameter as a float or returns the default
|
||||||
def getArgumentAsFloat(self, name: str, default: float = None) -> float:
|
def getArgumentAsFloat(self, name: str, default: float = None) -> float:
|
||||||
@ -593,14 +593,14 @@ class GCodeCommand:
|
|||||||
def parseArguments(self):
|
def parseArguments(self):
|
||||||
|
|
||||||
# stop here if we don't have any remaining components
|
# stop here if we don't have any remaining components
|
||||||
if len(self.Components) <= 1:
|
if len(self.components) <= 1:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# iterate and index all of our parameters, skip the first component as it's the command
|
# iterate and index all of our parameters, skip the first component as it's the command
|
||||||
for i in range(1, len(self.Components)):
|
for i in range(1, len(self.components)):
|
||||||
|
|
||||||
# get our component
|
# get our component
|
||||||
component = self.Components[i]
|
component = self.components[i]
|
||||||
|
|
||||||
# get the first character of the parameter, which is the name
|
# get the first character of the parameter, which is the name
|
||||||
component_name = component[0]
|
component_name = component[0]
|
||||||
@ -613,10 +613,10 @@ class GCodeCommand:
|
|||||||
component_value = component[1:]
|
component_value = component[1:]
|
||||||
|
|
||||||
# index the argument
|
# index the argument
|
||||||
self.Arguments[component_name] = component_value
|
self.arguments[component_name] = component_value
|
||||||
|
|
||||||
# clear the components to we don't process again
|
# clear the components to we don't process again
|
||||||
self.Components = []
|
self.components = []
|
||||||
|
|
||||||
# Easy function for replacing any GCODE parameter variable in a given GCODE command
|
# Easy function for replacing any GCODE parameter variable in a given GCODE command
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -625,8 +625,8 @@ class GCodeCommand:
|
|||||||
|
|
||||||
# Resets the model back to defaults
|
# Resets the model back to defaults
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.Command = None
|
self.command = None
|
||||||
self.Arguments = {}
|
self.arguments = {}
|
||||||
|
|
||||||
|
|
||||||
# The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
|
# The primary ChangeAtZ class that does all the gcode editing. This was broken out into an
|
||||||
@ -634,55 +634,55 @@ class GCodeCommand:
|
|||||||
class ChangeAtZProcessor:
|
class ChangeAtZProcessor:
|
||||||
|
|
||||||
# Holds our current height
|
# Holds our current height
|
||||||
CurrentZ = None
|
currentZ = None
|
||||||
|
|
||||||
# Holds our current layer number
|
# Holds our current layer number
|
||||||
CurrentLayer = None
|
currentLayer = None
|
||||||
|
|
||||||
# Indicates if we're only supposed to apply our settings to a single layer or multiple layers
|
# Indicates if we're only supposed to apply our settings to a single layer or multiple layers
|
||||||
IsApplyToSingleLayer = False
|
applyToSingleLayer = False
|
||||||
|
|
||||||
# Indicates if this should emit the changes as they happen to the LCD
|
# Indicates if this should emit the changes as they happen to the LCD
|
||||||
IsDisplayingChangesToLcd = False
|
displayChangesToLcd = False
|
||||||
|
|
||||||
# Indicates that this mod is still enabled (or not)
|
# Indicates that this mod is still enabled (or not)
|
||||||
IsEnabled = True
|
enabled = True
|
||||||
|
|
||||||
# Indicates if we're processing inside the target layer or not
|
# Indicates if we're processing inside the target layer or not
|
||||||
IsInsideTargetLayer = False
|
insideTargetLayer = False
|
||||||
|
|
||||||
# Indicates if we have restored the previous values from before we started our pass
|
# Indicates if we have restored the previous values from before we started our pass
|
||||||
IsLastValuesRestored = False
|
lastValuesRestored = False
|
||||||
|
|
||||||
# Indicates if the user has opted for linear move retractions or firmware retractions
|
# Indicates if the user has opted for linear move retractions or firmware retractions
|
||||||
IsLinearRetraction = True
|
linearRetraction = True
|
||||||
|
|
||||||
# Indicates if we're targetting by layer or height value
|
# Indicates if we're targetting by layer or height value
|
||||||
IsTargetByLayer = True
|
targetByLayer = True
|
||||||
|
|
||||||
# Indicates if we have injected our changed values for the given layer yet
|
# Indicates if we have injected our changed values for the given layer yet
|
||||||
IsTargetValuesInjected = False
|
targetValuesInjected = False
|
||||||
|
|
||||||
# Holds the last extrusion value, used with detecting when a retraction is made
|
# Holds the last extrusion value, used with detecting when a retraction is made
|
||||||
LastE = None
|
lastE = None
|
||||||
|
|
||||||
# An index of our gcodes which we're monitoring
|
# An index of our gcodes which we're monitoring
|
||||||
LastValues = {}
|
lastValues = {}
|
||||||
|
|
||||||
# The detected layer height from the gcode
|
# The detected layer height from the gcode
|
||||||
LayerHeight = None
|
layerHeight = None
|
||||||
|
|
||||||
# The target layer
|
# The target layer
|
||||||
TargetLayer = None
|
targetLayer = None
|
||||||
|
|
||||||
# Holds the values the user has requested to change
|
# Holds the values the user has requested to change
|
||||||
TargetValues = {}
|
targetValues = {}
|
||||||
|
|
||||||
# The target height in mm
|
# The target height in mm
|
||||||
TargetZ = None
|
targetZ = None
|
||||||
|
|
||||||
# Used to track if we've been inside our target layer yet
|
# Used to track if we've been inside our target layer yet
|
||||||
WasInsideTargetLayer = False
|
wasInsideTargetLayer = False
|
||||||
|
|
||||||
# boots up the class with defaults
|
# boots up the class with defaults
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -692,7 +692,7 @@ class ChangeAtZProcessor:
|
|||||||
def execute(self, data):
|
def execute(self, data):
|
||||||
|
|
||||||
# short cut the whole thing if we're not enabled
|
# short cut the whole thing if we're not enabled
|
||||||
if not self.IsEnabled:
|
if not self.enabled:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# our layer cursor
|
# our layer cursor
|
||||||
@ -750,14 +750,14 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# for each of our target values, get the value to restore
|
# for each of our target values, get the value to restore
|
||||||
# no point in restoring values we haven't changed
|
# no point in restoring values we haven't changed
|
||||||
for key in self.TargetValues:
|
for key in self.targetValues:
|
||||||
|
|
||||||
# skip target values we can't restore
|
# skip target values we can't restore
|
||||||
if key not in self.LastValues:
|
if key not in self.lastValues:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# save into our changed
|
# save into our changed
|
||||||
changed[key] = self.LastValues[key]
|
changed[key] = self.lastValues[key]
|
||||||
|
|
||||||
# return our collection of changed values
|
# return our collection of changed values
|
||||||
return changed
|
return changed
|
||||||
@ -766,7 +766,7 @@ class ChangeAtZProcessor:
|
|||||||
def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str:
|
def getDisplayChangesFromValues(self, values: Dict[str, any]) -> str:
|
||||||
|
|
||||||
# stop here if we're not outputting data
|
# stop here if we're not outputting data
|
||||||
if not self.IsDisplayingChangesToLcd:
|
if not self.displayChangesToLcd:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# will hold all the default settings for the target layer
|
# will hold all the default settings for the target layer
|
||||||
@ -833,7 +833,7 @@ class ChangeAtZProcessor:
|
|||||||
def getTargetDisplayValues(self) -> str:
|
def getTargetDisplayValues(self) -> str:
|
||||||
|
|
||||||
# convert our target values to something we can output
|
# convert our target values to something we can output
|
||||||
return self.getDisplayChangesFromValues(self.TargetValues)
|
return self.getDisplayChangesFromValues(self.targetValues)
|
||||||
|
|
||||||
# Builds the the relevant GCODE lines from the given collection of values
|
# Builds the the relevant GCODE lines from the given collection of values
|
||||||
def getCodeFromValues(self, values: Dict[str, any]) -> str:
|
def getCodeFromValues(self, values: Dict[str, any]) -> str:
|
||||||
@ -889,7 +889,7 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# set feedrate percentage
|
# set feedrate percentage
|
||||||
if "speed" in values:
|
if "speed" in values:
|
||||||
codes.append("M220 S" + str(values["speed"]) + " T1")
|
codes.append("M220 S" + str(values["speed"]) + "")
|
||||||
|
|
||||||
# set print rate percentage
|
# set print rate percentage
|
||||||
if "printspeed" in values:
|
if "printspeed" in values:
|
||||||
@ -898,7 +898,7 @@ class ChangeAtZProcessor:
|
|||||||
# set retract rate
|
# set retract rate
|
||||||
if "retractfeedrate" in values:
|
if "retractfeedrate" in values:
|
||||||
|
|
||||||
if self.IsLinearRetraction:
|
if self.linearRetraction:
|
||||||
codes.append(";RETRACTFEEDRATE " + str(values["retractfeedrate"] * 60) + "")
|
codes.append(";RETRACTFEEDRATE " + str(values["retractfeedrate"] * 60) + "")
|
||||||
else:
|
else:
|
||||||
codes.append("M207 F" + str(values["retractfeedrate"] * 60) + "")
|
codes.append("M207 F" + str(values["retractfeedrate"] * 60) + "")
|
||||||
@ -906,7 +906,7 @@ class ChangeAtZProcessor:
|
|||||||
# set retract length
|
# set retract length
|
||||||
if "retractlength" in values:
|
if "retractlength" in values:
|
||||||
|
|
||||||
if self.IsLinearRetraction:
|
if self.linearRetraction:
|
||||||
codes.append(";RETRACTLENGTH " + str(values["retractlength"]) + "")
|
codes.append(";RETRACTLENGTH " + str(values["retractlength"]) + "")
|
||||||
else:
|
else:
|
||||||
codes.append("M207 S" + str(values["retractlength"]) + "")
|
codes.append("M207 S" + str(values["retractlength"]) + "")
|
||||||
@ -923,19 +923,19 @@ class ChangeAtZProcessor:
|
|||||||
def getInjectCode(self) -> str:
|
def getInjectCode(self) -> str:
|
||||||
|
|
||||||
# if we're now outside of our target layer and haven't restored our last values, do so now
|
# if we're now outside of our target layer and haven't restored our last values, do so now
|
||||||
if not self.IsInsideTargetLayer and self.WasInsideTargetLayer and not self.IsLastValuesRestored:
|
if not self.insideTargetLayer and self.wasInsideTargetLayer and not self.lastValuesRestored:
|
||||||
|
|
||||||
# mark that we've injected the last values
|
# mark that we've injected the last values
|
||||||
self.IsLastValuesRestored = True
|
self.lastValuesRestored = True
|
||||||
|
|
||||||
# inject the defaults
|
# inject the defaults
|
||||||
return self.getLastValues() + "\n" + self.getLastDisplayValues()
|
return self.getLastValues() + "\n" + self.getLastDisplayValues()
|
||||||
|
|
||||||
# if we're inside our target layer but haven't added our values yet, do so now
|
# if we're inside our target layer but haven't added our values yet, do so now
|
||||||
if self.IsInsideTargetLayer and not self.IsTargetValuesInjected:
|
if self.insideTargetLayer and not self.targetValuesInjected:
|
||||||
|
|
||||||
# mark that we've injected the target values
|
# mark that we've injected the target values
|
||||||
self.IsTargetValuesInjected = True
|
self.targetValuesInjected = True
|
||||||
|
|
||||||
# inject the defaults
|
# inject the defaults
|
||||||
return self.getTargetValues() + "\n" + self.getTargetDisplayValues()
|
return self.getTargetValues() + "\n" + self.getTargetDisplayValues()
|
||||||
@ -960,35 +960,35 @@ class ChangeAtZProcessor:
|
|||||||
def getTargetValues(self) -> str:
|
def getTargetValues(self) -> str:
|
||||||
|
|
||||||
# build the gcode to change our current values
|
# build the gcode to change our current values
|
||||||
return self.getCodeFromValues(self.TargetValues)
|
return self.getCodeFromValues(self.targetValues)
|
||||||
|
|
||||||
# Determines if the current line is at or below the target required to start modifying
|
# Determines if the current line is at or below the target required to start modifying
|
||||||
def isTargetLayerOrHeight(self) -> bool:
|
def isTargetLayerOrHeight(self) -> bool:
|
||||||
|
|
||||||
# target selected by layer no.
|
# target selected by layer no.
|
||||||
if self.IsTargetByLayer:
|
if self.targetByLayer:
|
||||||
|
|
||||||
# if we don't have a current layer, we're not there yet
|
# if we don't have a current layer, we're not there yet
|
||||||
if self.CurrentLayer is None:
|
if self.currentLayer is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# if we're applying to a single layer, stop if our layer is not identical
|
# if we're applying to a single layer, stop if our layer is not identical
|
||||||
if self.IsApplyToSingleLayer:
|
if self.applyToSingleLayer:
|
||||||
return self.CurrentLayer == self.TargetLayer
|
return self.currentLayer == self.targetLayer
|
||||||
else:
|
else:
|
||||||
return self.CurrentLayer >= self.TargetLayer
|
return self.currentLayer >= self.targetLayer
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
# if we don't have a current Z, we're not there yet
|
# if we don't have a current Z, we're not there yet
|
||||||
if self.CurrentZ is None:
|
if self.currentZ is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# if we're applying to a single layer, stop if our Z is not identical
|
# if we're applying to a single layer, stop if our Z is not identical
|
||||||
if self.IsApplyToSingleLayer:
|
if self.applyToSingleLayer:
|
||||||
return self.CurrentZ == self.TargetZ
|
return self.currentZ == self.targetZ
|
||||||
else:
|
else:
|
||||||
return self.CurrentZ >= self.TargetZ
|
return self.currentZ >= self.targetZ
|
||||||
|
|
||||||
# Marks any current ChangeZ layer defaults in the layer for deletion
|
# Marks any current ChangeZ layer defaults in the layer for deletion
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -999,7 +999,7 @@ class ChangeAtZProcessor:
|
|||||||
def processLayerHeight(self, line: str):
|
def processLayerHeight(self, line: str):
|
||||||
|
|
||||||
# stop here if we haven't entered a layer yet
|
# stop here if we haven't entered a layer yet
|
||||||
if self.CurrentLayer is None:
|
if self.currentLayer is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# get our gcode command
|
# get our gcode command
|
||||||
@ -1010,7 +1010,7 @@ class ChangeAtZProcessor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# stop here if this isn't a linear move command
|
# stop here if this isn't a linear move command
|
||||||
if command.Command != "G0" and command.Command != "G1":
|
if command.command != "G0" and command.command != "G1":
|
||||||
return
|
return
|
||||||
|
|
||||||
# get our value from the command
|
# get our value from the command
|
||||||
@ -1021,15 +1021,15 @@ class ChangeAtZProcessor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# stop if there's no change
|
# stop if there's no change
|
||||||
if current_z == self.CurrentZ:
|
if current_z == self.currentZ:
|
||||||
return
|
return
|
||||||
|
|
||||||
# set our current Z value
|
# set our current Z value
|
||||||
self.CurrentZ = current_z
|
self.currentZ = current_z
|
||||||
|
|
||||||
# if we don't have a layer height yet, set it based on the current Z value
|
# if we don't have a layer height yet, set it based on the current Z value
|
||||||
if self.LayerHeight is None:
|
if self.layerHeight is None:
|
||||||
self.LayerHeight = self.CurrentZ
|
self.layerHeight = self.currentZ
|
||||||
|
|
||||||
# Grabs the current layer number
|
# Grabs the current layer number
|
||||||
def processLayerNumber(self, line: str):
|
def processLayerNumber(self, line: str):
|
||||||
@ -1042,11 +1042,11 @@ class ChangeAtZProcessor:
|
|||||||
current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None)
|
current_layer = GCodeCommand.getDirectArgumentAsInt(line, ";LAYER:", None)
|
||||||
|
|
||||||
# this should never happen, but if our layer number hasn't changed, stop here
|
# this should never happen, but if our layer number hasn't changed, stop here
|
||||||
if current_layer == self.CurrentLayer:
|
if current_layer == self.currentLayer:
|
||||||
return
|
return
|
||||||
|
|
||||||
# update our current layer
|
# update our current layer
|
||||||
self.CurrentLayer = current_layer
|
self.currentLayer = current_layer
|
||||||
|
|
||||||
# Makes any linear move changes and also injects either target or restored values depending on the plugin state
|
# Makes any linear move changes and also injects either target or restored values depending on the plugin state
|
||||||
def processLine(self, line: str) -> str:
|
def processLine(self, line: str) -> str:
|
||||||
@ -1059,10 +1059,10 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# if we're not inside the target layer, simply read the any
|
# if we're not inside the target layer, simply read the any
|
||||||
# settings we can and revert any ChangeAtZ deletions
|
# settings we can and revert any ChangeAtZ deletions
|
||||||
if not self.IsInsideTargetLayer:
|
if not self.insideTargetLayer:
|
||||||
|
|
||||||
# read any settings if we haven't hit our target layer yet
|
# read any settings if we haven't hit our target layer yet
|
||||||
if not self.WasInsideTargetLayer:
|
if not self.wasInsideTargetLayer:
|
||||||
self.processSetting(line)
|
self.processSetting(line)
|
||||||
|
|
||||||
# if we haven't hit our target yet, leave the defaults as is (unmark them for deletion)
|
# if we haven't hit our target yet, leave the defaults as is (unmark them for deletion)
|
||||||
@ -1074,7 +1074,7 @@ class ChangeAtZProcessor:
|
|||||||
modified_gcode += self.getInjectCode()
|
modified_gcode += self.getInjectCode()
|
||||||
|
|
||||||
# modify our command if we're still inside our target layer, otherwise pass unmodified
|
# modify our command if we're still inside our target layer, otherwise pass unmodified
|
||||||
if self.IsInsideTargetLayer:
|
if self.insideTargetLayer:
|
||||||
modified_gcode += self.processLinearMove(line) + "\n"
|
modified_gcode += self.processLinearMove(line) + "\n"
|
||||||
else:
|
else:
|
||||||
modified_gcode += line + "\n"
|
modified_gcode += line + "\n"
|
||||||
@ -1101,14 +1101,14 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# if it's not a linear move, we don't care
|
# if it's not a linear move, we don't care
|
||||||
if linear_command is None:
|
if linear_command is None:
|
||||||
return
|
return line
|
||||||
|
|
||||||
# get our linear move parameters
|
# get our linear move parameters
|
||||||
feed_rate = linear_command.Arguments["F"]
|
feed_rate = linear_command.arguments["F"]
|
||||||
x_coord = linear_command.Arguments["X"]
|
x_coord = linear_command.arguments["X"]
|
||||||
y_coord = linear_command.Arguments["Y"]
|
y_coord = linear_command.arguments["Y"]
|
||||||
z_coord = linear_command.Arguments["Z"]
|
z_coord = linear_command.arguments["Z"]
|
||||||
extrude_length = linear_command.Arguments["E"]
|
extrude_length = linear_command.arguments["E"]
|
||||||
|
|
||||||
# set our new line to our old line
|
# set our new line to our old line
|
||||||
new_line = line
|
new_line = line
|
||||||
@ -1120,10 +1120,11 @@ class ChangeAtZProcessor:
|
|||||||
new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
|
new_line = self.processRetractFeedRate(extrude_length, feed_rate, new_line, x_coord, y_coord, z_coord)
|
||||||
|
|
||||||
# handle print speed adjustments
|
# handle print speed adjustments
|
||||||
new_line = self.processPrintSpeed(feed_rate, new_line)
|
if extrude_length is not None: # Only for extrusion moves.
|
||||||
|
new_line = self.processPrintSpeed(feed_rate, new_line)
|
||||||
|
|
||||||
# set our current extrude position
|
# set our current extrude position
|
||||||
self.LastE = extrude_length if extrude_length is not None else self.LastE
|
self.lastE = extrude_length if extrude_length is not None else self.lastE
|
||||||
|
|
||||||
# if no changes have been made, stop here
|
# if no changes have been made, stop here
|
||||||
if new_line == line:
|
if new_line == line:
|
||||||
@ -1136,11 +1137,11 @@ class ChangeAtZProcessor:
|
|||||||
def processPrintSpeed(self, feed_rate: float, new_line: str) -> str:
|
def processPrintSpeed(self, feed_rate: float, new_line: str) -> str:
|
||||||
|
|
||||||
# if we're not setting print speed or we don't have a feed rate, stop here
|
# if we're not setting print speed or we don't have a feed rate, stop here
|
||||||
if "printspeed" not in self.TargetValues or feed_rate is None:
|
if "printspeed" not in self.targetValues or feed_rate is None:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# get our requested print speed
|
# get our requested print speed
|
||||||
print_speed = int(self.TargetValues["printspeed"])
|
print_speed = int(self.targetValues["printspeed"])
|
||||||
|
|
||||||
# if they requested no change to print speed (ie: 100%), stop here
|
# if they requested no change to print speed (ie: 100%), stop here
|
||||||
if print_speed == 100:
|
if print_speed == 100:
|
||||||
@ -1156,11 +1157,11 @@ class ChangeAtZProcessor:
|
|||||||
def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
|
def processRetractLength(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
|
||||||
|
|
||||||
# if we don't have a retract length in the file we can't add one
|
# if we don't have a retract length in the file we can't add one
|
||||||
if "retractlength" not in self.LastValues or self.LastValues["retractlength"] == 0:
|
if "retractlength" not in self.lastValues or self.lastValues["retractlength"] == 0:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# if we're not changing retraction length, stop here
|
# if we're not changing retraction length, stop here
|
||||||
if "retractlength" not in self.TargetValues:
|
if "retractlength" not in self.targetValues:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# retractions are only F (feed rate) and E (extrude), at least in cura
|
# retractions are only F (feed rate) and E (extrude), at least in cura
|
||||||
@ -1172,22 +1173,22 @@ class ChangeAtZProcessor:
|
|||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# stop here if we don't know our last extrude value
|
# stop here if we don't know our last extrude value
|
||||||
if self.LastE is None:
|
if self.lastE is None:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# if there's no change in extrude we have nothing to change
|
# if there's no change in extrude we have nothing to change
|
||||||
if self.LastE == extrude_length:
|
if self.lastE == extrude_length:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# if our last extrude was lower than our current, we're restoring, so skip
|
# if our last extrude was lower than our current, we're restoring, so skip
|
||||||
if self.LastE < extrude_length:
|
if self.lastE < extrude_length:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# get our desired retract length
|
# get our desired retract length
|
||||||
retract_length = float(self.TargetValues["retractlength"])
|
retract_length = float(self.targetValues["retractlength"])
|
||||||
|
|
||||||
# subtract the difference between the default and the desired
|
# subtract the difference between the default and the desired
|
||||||
extrude_length -= (retract_length - self.LastValues["retractlength"])
|
extrude_length -= (retract_length - self.lastValues["retractlength"])
|
||||||
|
|
||||||
# replace our extrude amount
|
# replace our extrude amount
|
||||||
return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length)
|
return GCodeCommand.replaceDirectArgument(new_line, "E", extrude_length)
|
||||||
@ -1196,7 +1197,7 @@ class ChangeAtZProcessor:
|
|||||||
def processRetractLengthSetting(self, line: str):
|
def processRetractLengthSetting(self, line: str):
|
||||||
|
|
||||||
# skip if we're not doing linear retractions
|
# skip if we're not doing linear retractions
|
||||||
if not self.IsLinearRetraction:
|
if not self.linearRetraction:
|
||||||
return
|
return
|
||||||
|
|
||||||
# get our command from the line
|
# get our command from the line
|
||||||
@ -1207,11 +1208,11 @@ class ChangeAtZProcessor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# get our linear move parameters
|
# get our linear move parameters
|
||||||
feed_rate = linear_command.Arguments["F"]
|
feed_rate = linear_command.arguments["F"]
|
||||||
x_coord = linear_command.Arguments["X"]
|
x_coord = linear_command.arguments["X"]
|
||||||
y_coord = linear_command.Arguments["Y"]
|
y_coord = linear_command.arguments["Y"]
|
||||||
z_coord = linear_command.Arguments["Z"]
|
z_coord = linear_command.arguments["Z"]
|
||||||
extrude_length = linear_command.Arguments["E"]
|
extrude_length = linear_command.arguments["E"]
|
||||||
|
|
||||||
# the command we're looking for only has extrude and feed rate
|
# the command we're looking for only has extrude and feed rate
|
||||||
if x_coord is not None or y_coord is not None or z_coord is not None:
|
if x_coord is not None or y_coord is not None or z_coord is not None:
|
||||||
@ -1229,17 +1230,17 @@ class ChangeAtZProcessor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# what ever the last negative retract length is it wins
|
# what ever the last negative retract length is it wins
|
||||||
self.LastValues["retractlength"] = extrude_length
|
self.lastValues["retractlength"] = extrude_length
|
||||||
|
|
||||||
# Handles any changes to retraction feed rate for the given linear motion command
|
# Handles any changes to retraction feed rate for the given linear motion command
|
||||||
def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
|
def processRetractFeedRate(self, extrude_length: float, feed_rate: float, new_line: str, x_coord: float, y_coord: float, z_coord: float) -> str:
|
||||||
|
|
||||||
# skip if we're not doing linear retractions
|
# skip if we're not doing linear retractions
|
||||||
if not self.IsLinearRetraction:
|
if not self.linearRetraction:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# if we're not changing retraction length, stop here
|
# if we're not changing retraction length, stop here
|
||||||
if "retractfeedrate" not in self.TargetValues:
|
if "retractfeedrate" not in self.targetValues:
|
||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# retractions are only F (feed rate) and E (extrude), at least in cura
|
# retractions are only F (feed rate) and E (extrude), at least in cura
|
||||||
@ -1251,7 +1252,7 @@ class ChangeAtZProcessor:
|
|||||||
return new_line
|
return new_line
|
||||||
|
|
||||||
# get our desired retract feed rate
|
# get our desired retract feed rate
|
||||||
retract_feed_rate = float(self.TargetValues["retractfeedrate"])
|
retract_feed_rate = float(self.targetValues["retractfeedrate"])
|
||||||
|
|
||||||
# convert to units/min
|
# convert to units/min
|
||||||
retract_feed_rate *= 60
|
retract_feed_rate *= 60
|
||||||
@ -1263,7 +1264,7 @@ class ChangeAtZProcessor:
|
|||||||
def processSetting(self, line: str):
|
def processSetting(self, line: str):
|
||||||
|
|
||||||
# if we're in layers already we're out of settings
|
# if we're in layers already we're out of settings
|
||||||
if self.CurrentLayer is not None:
|
if self.currentLayer is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# check our retract length
|
# check our retract length
|
||||||
@ -1276,16 +1277,16 @@ class ChangeAtZProcessor:
|
|||||||
if not self.isTargetLayerOrHeight():
|
if not self.isTargetLayerOrHeight():
|
||||||
|
|
||||||
# flag that we're outside our target layer
|
# flag that we're outside our target layer
|
||||||
self.IsInsideTargetLayer = False
|
self.insideTargetLayer = False
|
||||||
|
|
||||||
# skip to the next line
|
# skip to the next line
|
||||||
return
|
return
|
||||||
|
|
||||||
# flip if we hit our target layer
|
# flip if we hit our target layer
|
||||||
self.WasInsideTargetLayer = True
|
self.wasInsideTargetLayer = True
|
||||||
|
|
||||||
# flag that we're inside our target layer
|
# flag that we're inside our target layer
|
||||||
self.IsInsideTargetLayer = True
|
self.insideTargetLayer = True
|
||||||
|
|
||||||
# Removes all the ChangeZ layer defaults from the given layer
|
# Removes all the ChangeZ layer defaults from the given layer
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1295,22 +1296,22 @@ class ChangeAtZProcessor:
|
|||||||
# Resets the class contents to defaults
|
# Resets the class contents to defaults
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
|
||||||
self.TargetValues = {}
|
self.targetValues = {}
|
||||||
self.IsApplyToSingleLayer = False
|
self.applyToSingleLayer = False
|
||||||
self.LastE = None
|
self.lastE = None
|
||||||
self.CurrentZ = None
|
self.currentZ = None
|
||||||
self.CurrentLayer = None
|
self.currentLayer = None
|
||||||
self.IsTargetByLayer = True
|
self.targetByLayer = True
|
||||||
self.TargetLayer = None
|
self.targetLayer = None
|
||||||
self.TargetZ = None
|
self.targetZ = None
|
||||||
self.LayerHeight = None
|
self.layerHeight = None
|
||||||
self.LastValues = {}
|
self.lastValues = {"speed": 100}
|
||||||
self.IsLinearRetraction = True
|
self.linearRetraction = True
|
||||||
self.IsInsideTargetLayer = False
|
self.insideTargetLayer = False
|
||||||
self.IsTargetValuesInjected = False
|
self.targetValuesInjected = False
|
||||||
self.IsLastValuesRestored = False
|
self.lastValuesRestored = False
|
||||||
self.WasInsideTargetLayer = False
|
self.wasInsideTargetLayer = False
|
||||||
self.IsEnabled = True
|
self.enabled = True
|
||||||
|
|
||||||
# Sets the original GCODE line in a given GCODE command
|
# Sets the original GCODE line in a given GCODE command
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -1340,31 +1341,31 @@ class ChangeAtZProcessor:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# handle retract length changes
|
# handle retract length changes
|
||||||
if command.Command == "M207":
|
if command.command == "M207":
|
||||||
|
|
||||||
# get our retract length if provided
|
# get our retract length if provided
|
||||||
if "S" in command.Arguments:
|
if "S" in command.arguments:
|
||||||
self.LastValues["retractlength"] = command.getArgumentAsFloat("S")
|
self.lastValues["retractlength"] = command.getArgumentAsFloat("S")
|
||||||
|
|
||||||
# get our retract feedrate if provided, convert from mm/m to mm/s
|
# get our retract feedrate if provided, convert from mm/m to mm/s
|
||||||
if "F" in command.Arguments:
|
if "F" in command.arguments:
|
||||||
self.LastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0
|
self.lastValues["retractfeedrate"] = command.getArgumentAsFloat("F") / 60.0
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
|
||||||
# handle bed temp changes
|
# handle bed temp changes
|
||||||
if command.Command == "M140" or command.Command == "M190":
|
if command.command == "M140" or command.command == "M190":
|
||||||
|
|
||||||
# get our bed temp if provided
|
# get our bed temp if provided
|
||||||
if "S" in command.Arguments:
|
if "S" in command.arguments:
|
||||||
self.LastValues["bedTemp"] = command.getArgumentAsFloat("S")
|
self.lastValues["bedTemp"] = command.getArgumentAsFloat("S")
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
|
||||||
# handle extruder temp changes
|
# handle extruder temp changes
|
||||||
if command.Command == "M104" or command.Command == "M109":
|
if command.command == "M104" or command.command == "M109":
|
||||||
|
|
||||||
# get our tempurature
|
# get our tempurature
|
||||||
tempurature = command.getArgumentAsFloat("S")
|
tempurature = command.getArgumentAsFloat("S")
|
||||||
@ -1378,26 +1379,26 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# set our extruder temp based on the extruder
|
# set our extruder temp based on the extruder
|
||||||
if extruder is None or extruder == 0:
|
if extruder is None or extruder == 0:
|
||||||
self.LastValues["extruderOne"] = tempurature
|
self.lastValues["extruderOne"] = tempurature
|
||||||
|
|
||||||
if extruder is None or extruder == 1:
|
if extruder is None or extruder == 1:
|
||||||
self.LastValues["extruderTwo"] = tempurature
|
self.lastValues["extruderTwo"] = tempurature
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
|
||||||
# handle fan speed changes
|
# handle fan speed changes
|
||||||
if command.Command == "M106":
|
if command.command == "M106":
|
||||||
|
|
||||||
# get our bed temp if provided
|
# get our bed temp if provided
|
||||||
if "S" in command.Arguments:
|
if "S" in command.arguments:
|
||||||
self.LastValues["fanSpeed"] = (command.getArgumentAsInt("S") / 255.0) * 100
|
self.lastValues["fanSpeed"] = (command.getArgumentAsInt("S") / 255.0) * 100
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
|
||||||
# handle flow rate changes
|
# handle flow rate changes
|
||||||
if command.Command == "M221":
|
if command.command == "M221":
|
||||||
|
|
||||||
# get our flow rate
|
# get our flow rate
|
||||||
tempurature = command.getArgumentAsFloat("S")
|
tempurature = command.getArgumentAsFloat("S")
|
||||||
@ -1411,21 +1412,21 @@ class ChangeAtZProcessor:
|
|||||||
|
|
||||||
# set our extruder temp based on the extruder
|
# set our extruder temp based on the extruder
|
||||||
if extruder is None:
|
if extruder is None:
|
||||||
self.LastValues["flowrate"] = tempurature
|
self.lastValues["flowrate"] = tempurature
|
||||||
elif extruder == 1:
|
elif extruder == 1:
|
||||||
self.LastValues["flowrateOne"] = tempurature
|
self.lastValues["flowrateOne"] = tempurature
|
||||||
elif extruder == 1:
|
elif extruder == 1:
|
||||||
self.LastValues["flowrateTwo"] = tempurature
|
self.lastValues["flowrateTwo"] = tempurature
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
|
||||||
# handle print speed changes
|
# handle print speed changes
|
||||||
if command.Command == "M220":
|
if command.command == "M220":
|
||||||
|
|
||||||
# get our speed if provided
|
# get our speed if provided
|
||||||
if "S" in command.Arguments:
|
if "S" in command.arguments:
|
||||||
self.LastValues["speed"] = command.getArgumentAsInt("S")
|
self.lastValues["speed"] = command.getArgumentAsInt("S")
|
||||||
|
|
||||||
# move to the next command
|
# move to the next command
|
||||||
return
|
return
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Cura PostProcessingPlugin
|
# Cura PostProcessingPlugin
|
||||||
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee
|
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen
|
||||||
# Date: July 31, 2019
|
# Date: July 31, 2019
|
||||||
# Modified: May 22, 2020
|
# Modified: Okt 22, 2020
|
||||||
|
|
||||||
# Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage.
|
# Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage.
|
||||||
|
|
||||||
@ -26,10 +26,31 @@ class DisplayProgressOnLCD(Script):
|
|||||||
"time_remaining":
|
"time_remaining":
|
||||||
{
|
{
|
||||||
"label": "Time Remaining",
|
"label": "Time Remaining",
|
||||||
"description": "When enabled, write Time Left: HHMMSS on the display using M117. This is updated every layer.",
|
"description": "Select to write remaining time to the display.Select to write remaining time on the display using M117 status line message (almost all printers) or using M73 command (Prusa and Marlin 2 if enabled).",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"default_value": false
|
"default_value": false
|
||||||
},
|
},
|
||||||
|
"time_remaining_method":
|
||||||
|
{
|
||||||
|
"label": "Time Reporting Method",
|
||||||
|
"description": "How should remaining time be shown on the display? It could use a generic message command (M117, almost all printers), or a specialised time remaining command (M73, Prusa and Marlin 2).",
|
||||||
|
"type": "enum",
|
||||||
|
"options": {
|
||||||
|
"m117":"M117 - All printers",
|
||||||
|
"m73":"M73 - Prusa, Marlin 2"
|
||||||
|
},
|
||||||
|
"enabled": "time_remaining",
|
||||||
|
"default_value": "m117"
|
||||||
|
},
|
||||||
|
"update_frequency":
|
||||||
|
{
|
||||||
|
"label": "Update frequency",
|
||||||
|
"description": "Update remaining time for every layer or periodically every minute or faster.",
|
||||||
|
"type": "enum",
|
||||||
|
"options": {"0":"Every layer","15":"Every 15 seconds","30":"Every 30 seconds","60":"Every minute"},
|
||||||
|
"default_value": "0",
|
||||||
|
"enabled": "time_remaining"
|
||||||
|
},
|
||||||
"percentage":
|
"percentage":
|
||||||
{
|
{
|
||||||
"label": "Percentage",
|
"label": "Percentage",
|
||||||
@ -46,34 +67,44 @@ class DisplayProgressOnLCD(Script):
|
|||||||
list_split = re.split(":", line) # Split at ":" so we can get the numerical value
|
list_split = re.split(":", line) # Split at ":" so we can get the numerical value
|
||||||
return float(list_split[1]) # Convert the numerical portion to a float
|
return float(list_split[1]) # Convert the numerical portion to a float
|
||||||
|
|
||||||
def outputTime(self, lines, line_index, time_left):
|
def outputTime(self, lines, line_index, time_left, mode):
|
||||||
# Do some math to get the time left in seconds into the right format. (HH,MM,SS)
|
# Do some math to get the time left in seconds into the right format. (HH,MM,SS)
|
||||||
|
time_left = max(time_left, 0)
|
||||||
m, s = divmod(time_left, 60)
|
m, s = divmod(time_left, 60)
|
||||||
h, m = divmod(m, 60)
|
h, m = divmod(m, 60)
|
||||||
# Create the string
|
# Create the string
|
||||||
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
if mode == "m117":
|
||||||
# And now insert that into the GCODE
|
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||||
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
|
# And now insert that into the GCODE
|
||||||
|
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
|
||||||
|
else: # Must be m73.
|
||||||
|
mins = int(60 * h + m + s / 30)
|
||||||
|
lines.insert(line_index, "M73 R{}".format(mins))
|
||||||
|
|
||||||
def execute(self, data):
|
def execute(self, data):
|
||||||
output_time = self.getSettingValueByKey("time_remaining")
|
output_time = self.getSettingValueByKey("time_remaining")
|
||||||
|
output_time_method = self.getSettingValueByKey("time_remaining_method")
|
||||||
|
output_frequency = int(self.getSettingValueByKey("update_frequency"))
|
||||||
output_percentage = self.getSettingValueByKey("percentage")
|
output_percentage = self.getSettingValueByKey("percentage")
|
||||||
line_set = {}
|
line_set = {}
|
||||||
if output_percentage or output_time:
|
if output_percentage or output_time:
|
||||||
total_time = -1
|
total_time = -1
|
||||||
previous_layer_end_percentage = 0
|
previous_layer_end_percentage = 0
|
||||||
|
previous_layer_end_time = 0
|
||||||
for layer in data:
|
for layer in data:
|
||||||
layer_index = data.index(layer)
|
layer_index = data.index(layer)
|
||||||
lines = layer.split("\n")
|
lines = layer.split("\n")
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.startswith(";TIME:") and total_time == -1:
|
if (line.startswith(";TIME:") or line.startswith(";PRINT.TIME:")) and total_time == -1:
|
||||||
# This line represents the total time required to print the gcode
|
# This line represents the total time required to print the gcode
|
||||||
total_time = self.getTimeValue(line)
|
total_time = self.getTimeValue(line)
|
||||||
line_index = lines.index(line)
|
line_index = lines.index(line)
|
||||||
|
|
||||||
|
# In the beginning we may have 2 M73 lines, but it makes logic less complicated
|
||||||
if output_time:
|
if output_time:
|
||||||
self.outputTime(lines, line_index, total_time)
|
self.outputTime(lines, line_index, total_time, output_time_method)
|
||||||
|
|
||||||
if output_percentage:
|
if output_percentage:
|
||||||
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
|
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
|
||||||
lines.insert(line_index, "M73 P0")
|
lines.insert(line_index, "M73 P0")
|
||||||
@ -96,8 +127,34 @@ class DisplayProgressOnLCD(Script):
|
|||||||
line_index = lines.index(line)
|
line_index = lines.index(line)
|
||||||
|
|
||||||
if output_time:
|
if output_time:
|
||||||
# Here we calculate remaining time
|
if output_frequency == 0:
|
||||||
self.outputTime(lines, line_index, total_time - current_time)
|
# Here we calculate remaining time
|
||||||
|
self.outputTime(lines, line_index, total_time - current_time, output_time_method)
|
||||||
|
else:
|
||||||
|
# Here we calculate remaining time and how many outputs are expected for the layer
|
||||||
|
layer_time_delta = int(current_time - previous_layer_end_time)
|
||||||
|
layer_step_delta = int((current_time - previous_layer_end_time) / output_frequency)
|
||||||
|
# If this layer represents less than 1 step then we don't need to emit anything, continue to the next layer
|
||||||
|
if layer_step_delta != 0:
|
||||||
|
# Grab the index of the current line and figure out how many lines represent one second
|
||||||
|
step = line_index / layer_time_delta
|
||||||
|
# Move new lines further as we add new lines above it
|
||||||
|
lines_added = 1
|
||||||
|
# Run through layer in seconds
|
||||||
|
for seconds in range(1, layer_time_delta + 1):
|
||||||
|
# Time from start to decide when to update
|
||||||
|
line_time = int(previous_layer_end_time + seconds)
|
||||||
|
# Output every X seconds and after last layer
|
||||||
|
if line_time % output_frequency == 0 or line_time == total_time:
|
||||||
|
# Line to add the output
|
||||||
|
time_line_index = int((seconds * step) + lines_added)
|
||||||
|
|
||||||
|
# Insert remaining time into the GCODE
|
||||||
|
self.outputTime(lines, time_line_index, total_time - line_time, output_time_method)
|
||||||
|
# Next line will be again lower
|
||||||
|
lines_added = lines_added + 1
|
||||||
|
|
||||||
|
previous_layer_end_time = int(current_time)
|
||||||
|
|
||||||
if output_percentage:
|
if output_percentage:
|
||||||
# Calculate percentage value this layer ends at
|
# Calculate percentage value this layer ends at
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
# Created by Wayne Porter
|
# Created by Wayne Porter
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
@ -24,8 +26,8 @@ class InsertAtLayerChange(Script):
|
|||||||
},
|
},
|
||||||
"gcode_to_add":
|
"gcode_to_add":
|
||||||
{
|
{
|
||||||
"label": "GCODE to insert.",
|
"label": "G-code to insert.",
|
||||||
"description": "GCODE to add before or after layer change.",
|
"description": "G-code to add before or after layer change.",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"default_value": ""
|
"default_value": ""
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
@ -182,7 +182,22 @@ class PauseAtHeight(Script):
|
|||||||
"Repetier": "Repetier"
|
"Repetier": "Repetier"
|
||||||
},
|
},
|
||||||
"default_value": "RepRap (Marlin/Sprinter)",
|
"default_value": "RepRap (Marlin/Sprinter)",
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"default_value": ""
|
||||||
|
},
|
||||||
|
"custom_gcode_before_pause":
|
||||||
|
{
|
||||||
|
"label": "G-code Before Pause",
|
||||||
|
"description": "Any custom g-code to run before the pause, for example, M300 S440 P200 to beep.",
|
||||||
|
"type": "str",
|
||||||
|
"default_value": ""
|
||||||
|
},
|
||||||
|
"custom_gcode_after_pause":
|
||||||
|
{
|
||||||
|
"label": "G-code After Pause",
|
||||||
|
"description": "Any custom g-code to run after the pause, for example, M300 S440 P200 to beep.",
|
||||||
|
"type": "str",
|
||||||
|
"default_value": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
@ -235,6 +250,8 @@ class PauseAtHeight(Script):
|
|||||||
control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value")
|
control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value")
|
||||||
initial_layer_height = Application.getInstance().getGlobalContainerStack().getProperty("layer_height_0", "value")
|
initial_layer_height = Application.getInstance().getGlobalContainerStack().getProperty("layer_height_0", "value")
|
||||||
display_text = self.getSettingValueByKey("display_text")
|
display_text = self.getSettingValueByKey("display_text")
|
||||||
|
gcode_before = self.getSettingValueByKey("custom_gcode_before_pause")
|
||||||
|
gcode_after = self.getSettingValueByKey("custom_gcode_after_pause")
|
||||||
|
|
||||||
pause_method = self.getSettingValueByKey("pause_method")
|
pause_method = self.getSettingValueByKey("pause_method")
|
||||||
pause_command = {
|
pause_command = {
|
||||||
@ -411,9 +428,17 @@ class PauseAtHeight(Script):
|
|||||||
if disarm_timeout > 0:
|
if disarm_timeout > 0:
|
||||||
prepend_gcode += self.putValue(M = 18, S = disarm_timeout) + " ; Set the disarm timeout\n"
|
prepend_gcode += self.putValue(M = 18, S = disarm_timeout) + " ; Set the disarm timeout\n"
|
||||||
|
|
||||||
|
# Set a custom GCODE section before pause
|
||||||
|
if gcode_before:
|
||||||
|
prepend_gcode += gcode_before + "\n"
|
||||||
|
|
||||||
# Wait till the user continues printing
|
# Wait till the user continues printing
|
||||||
prepend_gcode += pause_command + " ; Do the actual pause\n"
|
prepend_gcode += pause_command + " ; Do the actual pause\n"
|
||||||
|
|
||||||
|
# Set a custom GCODE section before pause
|
||||||
|
if gcode_after:
|
||||||
|
prepend_gcode += gcode_after + "\n"
|
||||||
|
|
||||||
if pause_method == "repetier":
|
if pause_method == "repetier":
|
||||||
#Push the filament back,
|
#Push the filament back,
|
||||||
if retraction_amount != 0:
|
if retraction_amount != 0:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
# Created by Wayne Porter
|
# Created by Wayne Porter
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
@ -18,7 +20,7 @@ class TimeLapse(Script):
|
|||||||
"trigger_command":
|
"trigger_command":
|
||||||
{
|
{
|
||||||
"label": "Trigger camera command",
|
"label": "Trigger camera command",
|
||||||
"description": "Gcode command used to trigger camera.",
|
"description": "G-code command used to trigger camera.",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"default_value": "M240"
|
"default_value": "M240"
|
||||||
},
|
},
|
||||||
|
@ -13,7 +13,7 @@ from ..PostProcessingPlugin import PostProcessingPlugin
|
|||||||
# not sure if needed
|
# not sure if needed
|
||||||
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||||
|
|
||||||
""" In this file, commnunity refers to regular Cura for makers."""
|
""" In this file, community refers to regular Cura for makers."""
|
||||||
|
|
||||||
mock_plugin_registry = MagicMock()
|
mock_plugin_registry = MagicMock()
|
||||||
mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path")
|
mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path")
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a prepare stage in Cura.",
|
"description": "Provides a prepare stage in Cura.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a preview stage in Cura.",
|
"description": "Provides a preview stage in Cura.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
@ -59,7 +59,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||||||
|
|
||||||
# Take the intersection between file_formats and machine_file_formats.
|
# Take the intersection between file_formats and machine_file_formats.
|
||||||
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
|
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
|
||||||
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
|
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats if mimetype in format_by_mimetype] # Keep them ordered according to the preference in machine_file_formats.
|
||||||
|
|
||||||
if len(file_formats) == 0:
|
if len(file_formats) == 0:
|
||||||
Logger.log("e", "There are no file formats available to write with!")
|
Logger.log("e", "There are no file formats available to write with!")
|
||||||
@ -79,7 +79,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||||||
|
|
||||||
if extension: # Not empty string.
|
if extension: # Not empty string.
|
||||||
extension = "." + extension
|
extension = "." + extension
|
||||||
file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)
|
file_name = os.path.join(self.getId(), file_name + extension)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Logger.log("d", "Writing to %s", file_name)
|
Logger.log("d", "Writing to %s", file_name)
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Provides removable drive hotplugging and writing support.",
|
"description": "Provides removable drive hotplugging and writing support.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Logs certain events so that they can be used by the crash reporter",
|
"description": "Logs certain events so that they can be used by the crash reporter",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
@ -32,6 +32,7 @@ class SimulationPass(RenderPass):
|
|||||||
self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
|
self._current_shader = None # This shader will be the shadow or the normal depending if the user wants to see the paths or the layers
|
||||||
self._tool_handle_shader = None
|
self._tool_handle_shader = None
|
||||||
self._nozzle_shader = None
|
self._nozzle_shader = None
|
||||||
|
self._disabled_shader = None
|
||||||
self._old_current_layer = 0
|
self._old_current_layer = 0
|
||||||
self._old_current_path = 0
|
self._old_current_path = 0
|
||||||
self._switching_layers = True # It tracks when the user is moving the layers' slider
|
self._switching_layers = True # It tracks when the user is moving the layers' slider
|
||||||
@ -90,9 +91,17 @@ class SimulationPass(RenderPass):
|
|||||||
self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
|
self._nozzle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
|
||||||
self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
|
self._nozzle_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_nozzle").getRgb()))
|
||||||
|
|
||||||
|
if not self._disabled_shader:
|
||||||
|
self._disabled_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
|
||||||
|
self._disabled_shader.setUniformValue("u_diffuseColor1", Color(*Application.getInstance().getTheme().getColor("model_unslicable").getRgb()))
|
||||||
|
self._disabled_shader.setUniformValue("u_diffuseColor2", Color(*Application.getInstance().getTheme().getColor("model_unslicable_alt").getRgb()))
|
||||||
|
self._disabled_shader.setUniformValue("u_width", 50.0)
|
||||||
|
self._disabled_shader.setUniformValue("u_opacity", 0.6)
|
||||||
|
|
||||||
self.bind()
|
self.bind()
|
||||||
|
|
||||||
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
|
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
|
||||||
|
disabled_batch = RenderBatch(self._disabled_shader)
|
||||||
head_position = None # Indicates the current position of the print head
|
head_position = None # Indicates the current position of the print head
|
||||||
nozzle_node = None
|
nozzle_node = None
|
||||||
|
|
||||||
@ -103,7 +112,10 @@ class SimulationPass(RenderPass):
|
|||||||
|
|
||||||
elif isinstance(node, NozzleNode):
|
elif isinstance(node, NozzleNode):
|
||||||
nozzle_node = node
|
nozzle_node = node
|
||||||
nozzle_node.setVisible(False)
|
nozzle_node.setVisible(False) # Don't set to true, we render it separately!
|
||||||
|
|
||||||
|
elif getattr(node, "_outside_buildarea", False) and isinstance(node, SceneNode) and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"):
|
||||||
|
disabled_batch.addItem(node.getWorldTransformation(copy=False), node.getMeshData())
|
||||||
|
|
||||||
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
|
elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible():
|
||||||
layer_data = node.callDecoration("getLayerData")
|
layer_data = node.callDecoration("getLayerData")
|
||||||
@ -177,12 +189,14 @@ class SimulationPass(RenderPass):
|
|||||||
# but the user is not using the layer slider, and the compatibility mode is not enabled
|
# but the user is not using the layer slider, and the compatibility mode is not enabled
|
||||||
if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
|
if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None:
|
||||||
if head_position is not None:
|
if head_position is not None:
|
||||||
nozzle_node.setVisible(True)
|
|
||||||
nozzle_node.setPosition(head_position)
|
nozzle_node.setPosition(head_position)
|
||||||
nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
|
nozzle_batch = RenderBatch(self._nozzle_shader, type = RenderBatch.RenderType.Transparent)
|
||||||
nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
|
nozzle_batch.addItem(nozzle_node.getWorldTransformation(), mesh = nozzle_node.getMeshData())
|
||||||
nozzle_batch.render(self._scene.getActiveCamera())
|
nozzle_batch.render(self._scene.getActiveCamera())
|
||||||
|
|
||||||
|
if len(disabled_batch.items) > 0:
|
||||||
|
disabled_batch.render(self._scene.getActiveCamera())
|
||||||
|
|
||||||
# Render toolhandles on top of the layerview
|
# Render toolhandles on top of the layerview
|
||||||
if len(tool_handle_batch.items) > 0:
|
if len(tool_handle_batch.items) > 0:
|
||||||
tool_handle_batch.render(self._scene.getActiveCamera())
|
tool_handle_batch.render(self._scene.getActiveCamera())
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides the Simulation view.",
|
"description": "Provides the Simulation view.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2020 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -116,6 +116,7 @@ class SliceInfo(QObject, Extension):
|
|||||||
|
|
||||||
machine_manager = self._application.getMachineManager()
|
machine_manager = self._application.getMachineManager()
|
||||||
print_information = self._application.getPrintInformation()
|
print_information = self._application.getPrintInformation()
|
||||||
|
user_profile = self._application.getCuraAPI().account.userProfile
|
||||||
|
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
|
|
||||||
@ -124,6 +125,9 @@ class SliceInfo(QObject, Extension):
|
|||||||
data["schema_version"] = 0
|
data["schema_version"] = 0
|
||||||
data["cura_version"] = self._application.getVersion()
|
data["cura_version"] = self._application.getVersion()
|
||||||
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||||
|
org_id = user_profile.get("organization_id", None) if user_profile else None
|
||||||
|
data["organization_id"] = org_id if org_id else None
|
||||||
|
data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else []
|
||||||
|
|
||||||
active_mode = self._application.getPreferences().getValue("cura/active_mode")
|
active_mode = self._application.getPreferences().getValue("cura/active_mode")
|
||||||
if active_mode == 0:
|
if active_mode == 0:
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
<b>Cura Version:</b> 4.0<br/>
|
<b>Cura Version:</b> 4.8<br/>
|
||||||
<b>Operating System:</b> Windows 10<br/>
|
<b>Operating System:</b> Windows 10<br/>
|
||||||
<b>Language:</b> en_US<br/>
|
<b>Language:</b> en_US<br/>
|
||||||
<b>Machine Type:</b> Ultimaker S5<br/>
|
<b>Machine Type:</b> Ultimaker S5<br/>
|
||||||
<b>Intent Profile:</b> Default<br/>
|
<b>Intent Profile:</b> Default<br/>
|
||||||
<b>Quality Profile:</b> Fast<br/>
|
<b>Quality Profile:</b> Fast<br/>
|
||||||
<b>Using Custom Settings:</b> No
|
<b>Using Custom Settings:</b> No<br/>
|
||||||
|
<b>Organization ID (if any):</b> ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=<br/>
|
||||||
|
<b>Subscriptions (if any):</b>
|
||||||
|
<ul>
|
||||||
|
<li><b>Level:</b> 10, <b>Type:</b> Enterprise, <b>Plan:</b> Basic</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
<h3>Extruder 1:</h3>
|
<h3>Extruder 1:</h3>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Submits anonymous slice info. Can be disabled through preferences.",
|
"description": "Submits anonymous slice info. Can be disabled through preferences.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Provides a normal solid mesh view.",
|
"description": "Provides a normal solid mesh view.",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
@ -3,6 +3,6 @@
|
|||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Creates an eraser mesh to block the printing of support in certain places",
|
"description": "Creates an eraser mesh to block the printing of support in certain places",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
"name": "Toolbox",
|
"name": "Toolbox",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"api": "7.2.0",
|
"api": "7.4.0",
|
||||||
"description": "Find, manage and install new Cura packages."
|
"description": "Find, manage and install new Cura packages."
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user