mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-05 16:51:12 -07:00
commit
2d619fb491
971 changed files with 205799 additions and 61599 deletions
40
.gitignore
vendored
40
.gitignore
vendored
|
|
@ -5,11 +5,18 @@ __pycache__
|
||||||
*.mo
|
*.mo
|
||||||
docs/html
|
docs/html
|
||||||
*.log
|
*.log
|
||||||
resources/i18n/en
|
resources/i18n/en_US
|
||||||
|
resources/i18n/en_7S
|
||||||
resources/i18n/x-test
|
resources/i18n/x-test
|
||||||
resources/firmware
|
resources/firmware
|
||||||
resources/materials
|
resources/materials
|
||||||
|
CuraEngine.exe
|
||||||
LC_MESSAGES
|
LC_MESSAGES
|
||||||
|
.cache
|
||||||
|
*.qmlc
|
||||||
|
|
||||||
|
#MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Editors and IDEs.
|
# Editors and IDEs.
|
||||||
*kdev*
|
*kdev*
|
||||||
|
|
@ -18,18 +25,41 @@ LC_MESSAGES
|
||||||
*~
|
*~
|
||||||
*.qm
|
*.qm
|
||||||
.idea
|
.idea
|
||||||
|
cura.desktop
|
||||||
|
|
||||||
# Eclipse+PyDev
|
# Eclipse+PyDev
|
||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
.settings
|
.settings
|
||||||
|
|
||||||
# Debian packaging
|
|
||||||
debian*
|
|
||||||
|
|
||||||
#Externally located plug-ins.
|
#Externally located plug-ins.
|
||||||
|
plugins/CuraSolidWorksPlugin
|
||||||
plugins/Doodle3D-cura-plugin
|
plugins/Doodle3D-cura-plugin
|
||||||
plugins/GodMode
|
plugins/GodMode
|
||||||
plugins/PostProcessingPlugin
|
plugins/PostProcessingPlugin
|
||||||
plugins/UM3NetworkPrinting
|
|
||||||
plugins/X3GWriter
|
plugins/X3GWriter
|
||||||
|
plugins/FlatProfileExporter
|
||||||
|
plugins/ProfileFlattener
|
||||||
|
plugins/cura-god-mode-plugin
|
||||||
|
plugins/cura-big-flame-graph
|
||||||
|
plugins/cura-siemensnx-plugin
|
||||||
|
plugins/CuraVariSlicePlugin
|
||||||
|
plugins/CuraLiveScriptingPlugin
|
||||||
|
|
||||||
|
#Build stuff
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles
|
||||||
|
CPackSourceConfig.cmake
|
||||||
|
Testing/
|
||||||
|
CTestTestfile.cmake
|
||||||
|
Makefile*
|
||||||
|
junit-pytest-*
|
||||||
|
CuraVersion.py
|
||||||
|
cmake_install.cmake
|
||||||
|
|
||||||
|
#Debug
|
||||||
|
*.gcode
|
||||||
|
run.sh
|
||||||
|
.scannerwork/
|
||||||
|
CuraEngine
|
||||||
|
|
||||||
|
|
|
||||||
173
CHANGES
173
CHANGES
|
|
@ -1,173 +0,0 @@
|
||||||
Cura 15.06 Beta
|
|
||||||
===============
|
|
||||||
|
|
||||||
This is the *Beta* version of Cura 15.06.
|
|
||||||
|
|
||||||
Cura 15.06 is a new release built from the ground up on a completely new
|
|
||||||
framework called Uranium. This framework has been designed to make it easier to
|
|
||||||
extend Cura with additional functionality as well as provide a cleaner UI.
|
|
||||||
|
|
||||||
Changes since 15.05.95
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
* Fixed: Selection ghost remains visible after deleting an object
|
|
||||||
* Fixed: Window does not show up immediately after starting application on OSX
|
|
||||||
* Fixed: Added display of rotation angle during rotation
|
|
||||||
* Fixed: Object changes position while rotating/scaling
|
|
||||||
* Fixed: Loading improvements in the layer view
|
|
||||||
* Fixed: Added application icons
|
|
||||||
* Fixed: Improved feedback when loading models
|
|
||||||
* Fixed: Eject device on MacOSX now provides proper feedback
|
|
||||||
* Fixed: Make it possible to show retraction settings for UM2
|
|
||||||
* Fixed: Opening the machine preferences page will switch to the first available machine
|
|
||||||
* Fixed: Improved tool handle hit area size
|
|
||||||
* Fixed: Render lines with a thickness based on screen DPI
|
|
||||||
|
|
||||||
Changes since 15.05.94
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
* Added Russian translations
|
|
||||||
* Fixed: Infill not displayed in layer view
|
|
||||||
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
|
|
||||||
* Fixed: Improved font rendering on Windows
|
|
||||||
* Fixed: Help > Show Documentation crashes Cura on Windows
|
|
||||||
* Fixed: "There is no disk in the drive" repeating messages on Windows
|
|
||||||
* Fixed: Retraction settings not visible for Ultimaker2
|
|
||||||
* Fixed: Display rotation angle when rotating an object
|
|
||||||
* Fixed: Time/Quality slider values are properly rounded
|
|
||||||
* Fixed: Improved clarity of buttons and text
|
|
||||||
* Fixed: No indication that anything is happening when loading a model
|
|
||||||
* Fixed: Eject device now works on Windows
|
|
||||||
|
|
||||||
Changes since 15.05.93
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
* Fixed: No shortcuts for moving up/down layers in layer view.
|
|
||||||
* Fixed: Last view layers could not be scrolled through in layer view.
|
|
||||||
* Fixed: Files provided on command line would not actually show up on the build
|
|
||||||
platform.
|
|
||||||
* Fixed: Render a ghost of the selection in Layer view to make the actual object
|
|
||||||
position clear.
|
|
||||||
* Fixed: Showing a menu would clear the selection.
|
|
||||||
* Fixed: Size and scaling factor display for scale tool.
|
|
||||||
* Fixed: Missing background for additional tool controls.
|
|
||||||
* Fixed: Loading message times out when loading large files.
|
|
||||||
* Fixed: Show recent files in the file menu.
|
|
||||||
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
|
|
||||||
prevent issues with missing DLL's.
|
|
||||||
* Fixed: Collapsed/expanded state of setting categories not stored.
|
|
||||||
|
|
||||||
Changes since 15.05.91
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
|
|
||||||
higher.
|
|
||||||
* Fixed: Need to deselect before selecting a different object.
|
|
||||||
* Fixed: Object can be moved on Z axis.
|
|
||||||
* Fixed: Error values should be considered invalid values and will not trigger a
|
|
||||||
slice.
|
|
||||||
* Fixed: Text fields used a locale-aware validator while the underlying code did
|
|
||||||
not.
|
|
||||||
* Fixed: Text fields will trigger a slice on text change, not only after focus
|
|
||||||
change/enter press.
|
|
||||||
* Fixed: Rotate Tool snaps to incorrect value.
|
|
||||||
* Fixed: Object Collision would only moved objects to the right.
|
|
||||||
* Fixed: Object Collision would move the selected object when it should not.
|
|
||||||
* Fixed: Camera panning now works correctly instead of doing nothing.
|
|
||||||
* Fixed: Camera would flip around center point at maximum rotation.
|
|
||||||
* Fixed: Build platform grid blocked view from below objects.
|
|
||||||
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
|
|
||||||
window
|
|
||||||
|
|
||||||
Changes since 15.05.90
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
* Fixed: Additional UI elements for tools and views not loading.
|
|
||||||
* Fixed: Double click needed to change setting dialog page.
|
|
||||||
* Fixed: Context menu entries (reload, center object, etc.) not working.
|
|
||||||
* Fixed: "Open With" or passing files from command line not working.
|
|
||||||
* Fixed: "Reload All" would not reload files.
|
|
||||||
|
|
||||||
In addition, a lot of work has gone into getting a usable Mac OSX version.
|
|
||||||
|
|
||||||
New Features
|
|
||||||
------------
|
|
||||||
|
|
||||||
* Plugin based system
|
|
||||||
The Uranium framework provides us with a plugin-based system
|
|
||||||
that provides additional flexibility when extending Cura. Think
|
|
||||||
of new views, tools, file formats, etc. This is probably the
|
|
||||||
biggest new feature.
|
|
||||||
* Improved UI
|
|
||||||
The UI has received a complete overhaul.
|
|
||||||
* Time-Quality Slider
|
|
||||||
The 4 static quick print profiles have been replaced with
|
|
||||||
a slider that should make it easier to find the right spot
|
|
||||||
between print time and print quality.
|
|
||||||
* More Settings
|
|
||||||
The Advanced mode is now configurable and can show many
|
|
||||||
additional settings that were previously not available, while at
|
|
||||||
the same time not overwhelming new users with too many settings.
|
|
||||||
Custom set of visible settings can be created by the user.
|
|
||||||
* Support for high-DPI screens
|
|
||||||
The refreshed UI has been designed with high-DPI screens in
|
|
||||||
mind which should improve the experience of Cura on such
|
|
||||||
devices.
|
|
||||||
* Improved language support
|
|
||||||
(Not yet available for the Beta release.)
|
|
||||||
* Improved support structure generation
|
|
||||||
The new version of the CuraEngine now features improved
|
|
||||||
support generation algorithms and additional options for support
|
|
||||||
structure generation.
|
|
||||||
* Experimental Feature: Wire Printing
|
|
||||||
Wire Printing has been added as an experimental new feature. It
|
|
||||||
will print objects as a structure of lines. It can be enabled by
|
|
||||||
from Advanced Mode -> Fixes -> Wire Printing.
|
|
||||||
* Undo/Redo
|
|
||||||
It is now possible to undo and redo most scene operations, like
|
|
||||||
moving or rotating objects.
|
|
||||||
|
|
||||||
Features from earlier versions not (yet) in this release
|
|
||||||
--------------------------------------------------------
|
|
||||||
|
|
||||||
* The All-at-once/One-at-a-time toggle is not available.
|
|
||||||
We are working on an improved implementation of this mechanism
|
|
||||||
but it will not be available for this release.
|
|
||||||
* No dual extrusion features are available yet.
|
|
||||||
We are working on a completely new workflow for this but this
|
|
||||||
needs additional time.
|
|
||||||
* “Lay Flat” has been removed.
|
|
||||||
The existing implementation was unfortunately not salvageable.
|
|
||||||
We will be looking into an improved implementation for this
|
|
||||||
feature.
|
|
||||||
* "Split Object Into Parts" has been removed.
|
|
||||||
Due to the same reason as Lay Flat.
|
|
||||||
* Support for AMF and DAE file formats has been removed.
|
|
||||||
Both of these will be implemented as plugins in the future.
|
|
||||||
* Support for directly loading a GCode file is not yet available.
|
|
||||||
This will be implemented as a plugin in the future.
|
|
||||||
* Support for PNG, JPG and other image formats has been removed.
|
|
||||||
These can be supported by a plugin with an improved UI.
|
|
||||||
* Support for loading Minecraft levels has been removed.
|
|
||||||
This can be implemented as a plugin.
|
|
||||||
* Windows XP support has been dropped.
|
|
||||||
Microsoft is no longer supporting xp, so they no longer back
|
|
||||||
port certain features that we require.
|
|
||||||
* X-Ray view is missing.
|
|
||||||
Will be implemented as a (you might have guessed it) plugin.
|
|
||||||
* Fixes: Follow Mesh Surface
|
|
||||||
Has been removed from the engine, the same result can be
|
|
||||||
achieved using no infill or top/bottom layers.
|
|
||||||
|
|
||||||
Known Issues
|
|
||||||
------------
|
|
||||||
|
|
||||||
For an up to date list of all known issues, please see
|
|
||||||
https://github.com/Ultimaker/Cura/issues and
|
|
||||||
https://github.com/Ultimaker/Uranium/issues .
|
|
||||||
|
|
||||||
* Some OBJ files are rendered as black objects due to missing
|
|
||||||
normals.
|
|
||||||
* Disabling plugins does not work correctly yet.
|
|
||||||
* Unicorn occasionally still requires feeding. Do not feed it
|
|
||||||
after midnight.
|
|
||||||
|
|
@ -1,23 +1,32 @@
|
||||||
|
|
||||||
project(cura NONE)
|
project(cura NONE)
|
||||||
cmake_minimum_required(VERSION 2.8.12)
|
cmake_minimum_required(VERSION 2.8.12)
|
||||||
|
|
||||||
|
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
||||||
|
${CMAKE_MODULE_PATH})
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(URANIUM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/../uranium/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||||
|
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
# Note that we use exit 0 here to not mark the build as a failure on test failure
|
include(CuraTests)
|
||||||
add_custom_target(tests)
|
|
||||||
add_custom_command(TARGET tests POST_BUILD COMMAND "PYTHONPATH=${CMAKE_SOURCE_DIR}/../Uranium/:${CMAKE_SOURCE_DIR}" ${PYTHON_EXECUTABLE} -m pytest -r a --junitxml=${CMAKE_BINARY_DIR}/junit.xml ${CMAKE_SOURCE_DIR} || exit 0)
|
|
||||||
|
|
||||||
|
option(CURA_DEBUGMODE "Enable debug dialog and other debug features" OFF)
|
||||||
|
if(CURA_DEBUGMODE)
|
||||||
|
set(_cura_debugmode "ON")
|
||||||
|
endif()
|
||||||
|
|
||||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||||
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)
|
||||||
|
|
||||||
|
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||||
|
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||||
|
endif()
|
||||||
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
|
||||||
include(UraniumTranslationTools)
|
include(UraniumTranslationTools)
|
||||||
# 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)
|
||||||
|
|
@ -30,21 +39,31 @@ find_package(PythonInterp 3.5.0 REQUIRED)
|
||||||
install(DIRECTORY resources
|
install(DIRECTORY resources
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||||
install(DIRECTORY plugins
|
install(DIRECTORY plugins
|
||||||
DESTINATION lib/cura)
|
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||||
if(NOT APPLE AND NOT WIN32)
|
if(NOT APPLE AND NOT WIN32)
|
||||||
install(FILES cura_app.py
|
install(FILES cura_app.py
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
|
||||||
RENAME cura)
|
RENAME cura)
|
||||||
|
if(EXISTS /etc/debian_version)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
||||||
|
else()
|
||||||
|
install(DIRECTORY cura
|
||||||
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
||||||
|
FILES_MATCHING PATTERN *.py)
|
||||||
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
||||||
|
endif()
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
|
install(FILES ${CMAKE_SOURCE_DIR}/resources/images/cura-icon.png
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/)
|
||||||
install(FILES cura.appdata.xml
|
install(FILES cura.appdata.xml
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/appdata)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
|
||||||
install(FILES cura.sharedmimeinfo
|
install(FILES cura.sharedmimeinfo
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
|
||||||
RENAME cura.xml )
|
RENAME cura.xml )
|
||||||
|
|
@ -53,10 +72,8 @@ else()
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
include(CPackConfig.cmake)
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
set(CPACK_PACKAGE_VENDOR "Ultimaker B.V.")
|
|
||||||
set(CPACK_PACKAGE_CONTACT "Arjen Hiemstra <a.hiemstra@ultimaker.com>")
|
|
||||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Cura application to drive the CuraEngine")
|
|
||||||
set(CPACK_PACKAGE_VERSION_MAJOR 15)
|
|
||||||
set(CPACK_PACKAGE_VERSION_MINOR 05)
|
|
||||||
set(CPACK_PACKAGE_VERSION_PATCH 90)
|
|
||||||
set(CPACK_PACKAGE_VERSION_REVISION 1)
|
|
||||||
set(CPACK_GENERATOR "DEB")
|
|
||||||
|
|
||||||
set(DEB_DEPENDS
|
|
||||||
"uranium (>= 15.05.93)"
|
|
||||||
)
|
|
||||||
string(REPLACE ";" ", " DEB_DEPENDS "${DEB_DEPENDS}")
|
|
||||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS ${DEB_DEPENDS})
|
|
||||||
|
|
||||||
include(CPack)
|
|
||||||
45
Jenkinsfile
vendored
Normal file
45
Jenkinsfile
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||||
|
// Prepare building
|
||||||
|
stage('Prepare') {
|
||||||
|
// Ensure we start with a clean build directory.
|
||||||
|
step([$class: 'WsCleanup'])
|
||||||
|
|
||||||
|
// Checkout whatever sources are linked to this pipeline.
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
||||||
|
catchError {
|
||||||
|
// Building and testing should happen in a subdirectory.
|
||||||
|
dir('build') {
|
||||||
|
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||||
|
stage('Build') {
|
||||||
|
def branch = env.BRANCH_NAME
|
||||||
|
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||||
|
branch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
|
||||||
|
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||||
|
cmake("..", "-DCMAKE_PREFIX_PATH=\"${env.CURA_ENVIRONMENT_PATH}/${branch}\" -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=\"${uranium_dir}\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||||
|
stage('Unit Test') {
|
||||||
|
try {
|
||||||
|
make('test')
|
||||||
|
} catch(e) {
|
||||||
|
currentBuild.result = "UNSTABLE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform any post-build actions like notification and publishing of unit tests.
|
||||||
|
stage('Finalize') {
|
||||||
|
// Publish the test results to Jenkins.
|
||||||
|
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
||||||
|
|
||||||
|
notify_build_result(env.CURA_EMAIL_RECIPIENTS, '#cura-dev', ['master', '2.'])
|
||||||
|
}
|
||||||
|
}
|
||||||
810
LICENSE
810
LICENSE
|
|
@ -1,661 +1,165 @@
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
of this license document, but changing it is not allowed.
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
This version of the GNU Lesser General Public License incorporates
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
the terms and conditions of version 3 of the GNU General Public
|
||||||
software and other kinds of works, specifically designed to ensure
|
License, supplemented by the additional permissions listed below.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
0. Additional Definitions.
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||||
share and change all versions of a program--to make sure it remains free
|
General Public License.
|
||||||
software for all its users.
|
|
||||||
|
"The Library" refers to a covered work governed by this License,
|
||||||
When we speak of free software, we are referring to freedom, not
|
other than an Application or a Combined Work as defined below.
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
An "Application" is any work that makes use of an interface provided
|
||||||
them if you wish), that you receive source code or can get it if you
|
by the Library, but which is not otherwise based on the Library.
|
||||||
want it, that you can change the software or use pieces of it in new
|
Defining a subclass of a class defined by the Library is deemed a mode
|
||||||
free programs, and that you know you can do these things.
|
of using an interface provided by the Library.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
A "Combined Work" is a work produced by combining or linking an
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
Application with the Library. The particular version of the Library
|
||||||
you this License which gives you legal permission to copy, distribute
|
with which the Combined Work was made is also called the "Linked
|
||||||
and/or modify the software.
|
Version".
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
The "Minimal Corresponding Source" for a Combined Work means the
|
||||||
improvements made in alternate versions of the program, if they
|
Corresponding Source for the Combined Work, excluding any source code
|
||||||
receive widespread use, become available for other developers to
|
for portions of the Combined Work that, considered in isolation, are
|
||||||
incorporate. Many developers of free software are heartened and
|
based on the Application, and not on the Linked Version.
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
The "Corresponding Application Code" for a Combined Work means the
|
||||||
The GNU General Public License permits making a modified version and
|
object code and/or source code for the Application, including any data
|
||||||
letting the public access it on a server without ever releasing its
|
and utility programs needed for reproducing the Combined Work from the
|
||||||
source code to the public.
|
Application, but excluding the System Libraries of the Combined Work.
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
1. Exception to Section 3 of the GNU GPL.
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
You may convey a covered work under sections 3 and 4 of this License
|
||||||
provide the source code of the modified version running there to the
|
without being bound by section 3 of the GNU GPL.
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
2. Conveying Modified Versions.
|
||||||
code of the modified version.
|
|
||||||
|
If you modify a copy of the Library, and, in your modifications, a
|
||||||
An older license, called the Affero General Public License and
|
facility refers to a function or data to be supplied by an Application
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that uses the facility (other than as an argument passed when the
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
facility is invoked), then you may convey a copy of the modified
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
version:
|
||||||
this license.
|
|
||||||
|
a) under this License, provided that you make a good faith effort to
|
||||||
The precise terms and conditions for copying, distribution and
|
ensure that, in the event an Application does not supply the
|
||||||
modification follow.
|
function or data, the facility still operates, and performs
|
||||||
|
whatever part of its purpose remains meaningful, or
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
b) under the GNU GPL, with none of the additional permissions of
|
||||||
0. Definitions.
|
this License applicable to that copy.
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
3. Object Code Incorporating Material from Library Header Files.
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
The object code form of an Application may incorporate material from
|
||||||
works, such as semiconductor masks.
|
a header file that is part of the Library. You may convey such object
|
||||||
|
code under terms of your choice, provided that, if the incorporated
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
material is not limited to numerical parameters, data structure
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
layouts and accessors, or small macros, inline functions and templates
|
||||||
"recipients" may be individuals or organizations.
|
(ten or fewer lines in length), you do both of the following:
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
a) Give prominent notice with each copy of the object code that the
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
Library is used in it and that the Library and its use are
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
covered by this License.
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||||
A "covered work" means either the unmodified Program or a work based
|
document.
|
||||||
on the Program.
|
|
||||||
|
4. Combined Works.
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
You may convey a Combined Work under terms of your choice that,
|
||||||
infringement under applicable copyright law, except executing it on a
|
taken together, effectively do not restrict modification of the
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
portions of the Library contained in the Combined Work and reverse
|
||||||
distribution (with or without modification), making available to the
|
engineering for debugging such modifications, if you also do each of
|
||||||
public, and in some countries other activities as well.
|
the following:
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
a) Give prominent notice with each copy of the Combined Work that
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
the Library is used in it and that the Library and its use are
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
covered by this License.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||||
to the extent that it includes a convenient and prominently visible
|
document.
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
c) For a Combined Work that displays copyright notices during
|
||||||
extent that warranties are provided), that licensees may convey the
|
execution, include the copyright notice for the Library among
|
||||||
work under this License, and how to view a copy of this License. If
|
these notices, as well as a reference directing the user to the
|
||||||
the interface presents a list of user commands or options, such as a
|
copies of the GNU GPL and this license document.
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
d) Do one of the following:
|
||||||
1. Source Code.
|
|
||||||
|
0) Convey the Minimal Corresponding Source under the terms of this
|
||||||
The "source code" for a work means the preferred form of the work
|
License, and the Corresponding Application Code in a form
|
||||||
for making modifications to it. "Object code" means any non-source
|
suitable for, and under terms that permit, the user to
|
||||||
form of a work.
|
recombine or relink the Application with a modified version of
|
||||||
|
the Linked Version to produce a modified Combined Work, in the
|
||||||
A "Standard Interface" means an interface that either is an official
|
manner specified by section 6 of the GNU GPL for conveying
|
||||||
standard defined by a recognized standards body, or, in the case of
|
Corresponding Source.
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
1) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (a) uses at run time
|
||||||
The "System Libraries" of an executable work include anything, other
|
a copy of the Library already present on the user's computer
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
system, and (b) will operate properly with a modified version
|
||||||
packaging a Major Component, but which is not part of that Major
|
of the Library that is interface-compatible with the Linked
|
||||||
Component, and (b) serves only to enable use of the work with that
|
Version.
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
e) Provide Installation Information, but only if you would otherwise
|
||||||
"Major Component", in this context, means a major essential component
|
be required to provide such information under section 6 of the
|
||||||
(kernel, window system, and so on) of the specific operating system
|
GNU GPL, and only to the extent that such information is
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
necessary to install and execute a modified version of the
|
||||||
produce the work, or an object code interpreter used to run it.
|
Combined Work produced by recombining or relinking the
|
||||||
|
Application with a modified version of the Linked Version. (If
|
||||||
The "Corresponding Source" for a work in object code form means all
|
you use option 4d0, the Installation Information must accompany
|
||||||
the source code needed to generate, install, and (for an executable
|
the Minimal Corresponding Source and Corresponding Application
|
||||||
work) run the object code and to modify the work, including scripts to
|
Code. If you use option 4d1, you must provide the Installation
|
||||||
control those activities. However, it does not include the work's
|
Information in the manner specified by section 6 of the GNU GPL
|
||||||
System Libraries, or general-purpose tools or generally available free
|
for conveying Corresponding Source.)
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
5. Combined Libraries.
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
You may place library facilities that are a work based on the
|
||||||
linked subprograms that the work is specifically designed to require,
|
Library side by side in a single library together with other library
|
||||||
such as by intimate data communication or control flow between those
|
facilities that are not Applications and are not covered by this
|
||||||
subprograms and other parts of the work.
|
License, and convey such a combined library under terms of your
|
||||||
|
choice, if you do both of the following:
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
a) Accompany the combined library with a copy of the same work based
|
||||||
Source.
|
on the Library, uncombined with any other library facilities,
|
||||||
|
conveyed under the terms of this License.
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
b) Give prominent notice with the combined library that part of it
|
||||||
|
is a work based on the Library, and explaining where to find the
|
||||||
2. Basic Permissions.
|
accompanying uncombined form of the same work.
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
6. Revised Versions of the GNU Lesser General Public License.
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
The Free Software Foundation may publish revised and/or new versions
|
||||||
permission to run the unmodified Program. The output from running a
|
of the GNU Lesser General Public License from time to time. Such new
|
||||||
covered work is covered by this License only if the output, given its
|
versions will be similar in spirit to the present version, but may
|
||||||
content, constitutes a covered work. This License acknowledges your
|
differ in detail to address new problems or concerns.
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
Each version is given a distinguishing version number. If the
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
Library as you received it specifies that a certain numbered version
|
||||||
Public License "or any later version" applies to it, you have the
|
of the GNU Lesser General Public License "or any later version"
|
||||||
option of following the terms and conditions either of that numbered
|
applies to it, you have the option of following the terms and
|
||||||
version or of any later version published by the Free Software
|
conditions either of that published version or of any later version
|
||||||
Foundation. If the Program does not specify a version number of the
|
published by the Free Software Foundation. If the Library as you
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
received it does not specify a version number of the GNU Lesser
|
||||||
by the Free Software Foundation.
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
|
General Public License ever published by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
If the Library as you received it specifies that a proxy can decide
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
public statement of acceptance of a version permanently authorizes you
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
to choose that version for the Program.
|
permanent authorization for you to choose that version for the
|
||||||
|
Library.
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published
|
|
||||||
by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<http://www.gnu.org/licenses/>.
|
|
||||||
50
README.md
50
README.md
|
|
@ -14,10 +14,13 @@ For crashes and similar issues, please attach the following information:
|
||||||
|
|
||||||
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
||||||
* The Cura GUI log file, located at
|
* The Cura GUI log file, located at
|
||||||
* $User/AppData/Local/cura/cura.log (Windows)
|
* %APPDATA%\cura\\`<Cura version>`\cura.log (Windows), or usually C:\Users\\`<your username>`\AppData\Roaming\cura\\`<Cura version>`\cura.log
|
||||||
* $User/Library/Application Support/cura (OSX)
|
* $User/Library/Application Support/cura/`<Cura version>`/cura.log (OSX)
|
||||||
* $USER/.local/share/cura (Ubuntu/Linux)
|
* $USER/.local/share/cura/`<Cura version>`/cura.log (Ubuntu/Linux)
|
||||||
* The Cura Engine log, using Help -> Show Engine Log
|
|
||||||
|
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
||||||
|
|
||||||
|
For additional support, you could also ask in the #cura channel on FreeNode IRC. For help with development, there is also the #cura-dev channel.
|
||||||
|
|
||||||
Dependencies
|
Dependencies
|
||||||
------------
|
------------
|
||||||
|
|
@ -28,12 +31,16 @@ Dependencies
|
||||||
This will be needed at runtime to perform the actual slicing.
|
This will be needed at runtime to perform the actual slicing.
|
||||||
* [PySerial](https://github.com/pyserial/pyserial)
|
* [PySerial](https://github.com/pyserial/pyserial)
|
||||||
Only required for USB printing support.
|
Only required for USB printing support.
|
||||||
|
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf)
|
||||||
|
Only required to detect mDNS-enabled printers
|
||||||
|
|
||||||
Configuring Cura
|
Configuring Cura
|
||||||
----------------
|
----------------
|
||||||
* Link your CuraEngine backend by inserting the following line in home/.config/cura/config.cfg :
|
Link your CuraEngine backend by inserting the following lines in `$HOME/.config/cura/config.cfg` :
|
||||||
|
```
|
||||||
[backend]
|
[backend]
|
||||||
location = /[path_to_the..]/CuraEngine/build/CuraEngine
|
location = /[path_to_the..]/CuraEngine/build/CuraEngine
|
||||||
|
```
|
||||||
|
|
||||||
Build scripts
|
Build scripts
|
||||||
-------------
|
-------------
|
||||||
|
|
@ -42,15 +49,16 @@ Please checkout [cura-build](https://github.com/Ultimaker/cura-build)
|
||||||
|
|
||||||
Third party plugins
|
Third party plugins
|
||||||
-------------
|
-------------
|
||||||
* [Print Cost Calculator](https://github.com/nallath/PrintCostCalculator): Calculates weight and monetary cost of your print.
|
|
||||||
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code.
|
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code.
|
||||||
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
|
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
|
||||||
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
|
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
|
||||||
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
|
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
|
||||||
|
* [OctoPrint Plugin](https://github.com/fieldofview/OctoPrintPlugin): Send printjobs directly to OctoPrint and monitor their progress in Cura.
|
||||||
|
* [Electric Print Cost Calculator Plugin](https://github.com/zoff99/ElectricPrintCostCalculator): Calculate the electric costs of a print.
|
||||||
|
|
||||||
Making profiles for other printers
|
Making profiles for other printers
|
||||||
----------------------------------
|
----------------------------------
|
||||||
There are two ways of doing it. You can either use the generator [here](http://quillford.github.io/CuraProfileMaker/) or you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json) as a template.
|
If your make of printer is not in the list of supported printers, and using the "Custom FDM Printer" does not offer enough flexibility, you can use [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original.def.json) as a template.
|
||||||
|
|
||||||
* Change the machine ID to something unique
|
* Change the machine ID to something unique
|
||||||
* Change the machine_name to your printer's name
|
* Change the machine_name to your printer's name
|
||||||
|
|
@ -58,8 +66,30 @@ There are two ways of doing it. You can either use the generator [here](http://q
|
||||||
* Set your machine's dimensions with machine_width, machine_depth, and machine_height
|
* Set your machine's dimensions with machine_width, machine_depth, and machine_height
|
||||||
* If your printer's origin is in the center of the bed, set machine_center_is_zero to true.
|
* If your printer's origin is in the center of the bed, set machine_center_is_zero to true.
|
||||||
* Set your print head dimensions with the machine_head_shape parameters
|
* Set your print head dimensions with the machine_head_shape parameters
|
||||||
* Set the nozzle offset with machine_nozzle_offset_x_1 and machine_nozzle_offset_y1
|
|
||||||
* Set the start and end gcode in machine_start_gcode and machine_end_gcode
|
* Set the start and end gcode in machine_start_gcode and machine_end_gcode
|
||||||
* If your printer has a heated bed, set visible to true under material_bed_temperature
|
|
||||||
|
|
||||||
Once you are done, put the profile you have made into resources/machines, or in machines in your cura profile folder.
|
Once you are done, put the profile you have made into resources/definitions, or in definitions in your cura profile folder.
|
||||||
|
|
||||||
|
If you want to make a definition for a multi-extrusion printer, have a look at [this](https://github.com/Ultimaker/Cura/blob/master/resources/definitions/ultimaker_original_dual.def.json) as a template, along with the two extruder definitions it references [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_1st.def.json) and [here](https://github.com/Ultimaker/Cura/blob/master/resources/extruders/ultimaker_original_dual_2nd.def.json)
|
||||||
|
|
||||||
|
Translating Cura
|
||||||
|
----------------
|
||||||
|
If you'd like to contribute a translation of Cura, please first look for [any existing translation](https://github.com/Ultimaker/Cura/tree/master/resources/i18n). If your language is already there in the source code but not in Cura's interface, it may be partially translated.
|
||||||
|
|
||||||
|
There are four files that need to be translated for Cura:
|
||||||
|
1. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot
|
||||||
|
2. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmextruder.def.json.pot
|
||||||
|
3. https://github.com/Ultimaker/Cura/blob/master/resources/i18n/fdmprinter.def.json.pot (This one is the most work.)
|
||||||
|
4. https://github.com/Ultimaker/Uranium/blob/master/resources/i18n/uranium.pot
|
||||||
|
|
||||||
|
Copy these files and rename them to `*.po` (remove the `t`). Then create the actual translations by filling in the empty `msgstr` entries. These are gettext files, which are plain text so you can open them with any text editor such as Notepad or GEdit, but it is probably easier with a specialised tool such as [POEdit](https://poedit.net/) or [Virtaal](http://virtaal.translatehouse.org/).
|
||||||
|
|
||||||
|
Do not hestiate to ask us about a translation or the meaning of some text via Github Issues.
|
||||||
|
|
||||||
|
Once the translation is complete, it's probably best to test them in Cura. Use your favourite software to convert the .po file to a .mo file (such as [GetText](https://www.gnu.org/software/gettext/)). Then put the .mo files in the `.../resources/i18n/<language code>/LC_MESSAGES` folder in your Cura installation. Then find your Cura configuration file (next to the log as described above, except on Linux where it is located in `~/.config/cura`) and change the language preference to the name of the folder you just created. Then start Cura. If working correctly, your Cura should now be translated.
|
||||||
|
|
||||||
|
To submit your translation, ideally you would make two pull requests where all `*.po` files are located in that same `<language code>` folder in the resources of both the Cura and Uranium repositories. Put `cura.po`, `fdmprinter.def.json.po` and `fdmextruder.def.json.po` in the Cura repository, and put `uranium.po` in the Uranium repository. Then submit the pull requests to Github. For people with less experience with Git, you can also e-mail the translations to the e-mail address listed at the top of the [cura.pot](https://github.com/Ultimaker/Cura/blob/master/resources/i18n/cura.pot) file as the `Report-Msgid-Bugs-To` entry and we'll make sure it gets checked and included.
|
||||||
|
|
||||||
|
After the translation is submitted, the Cura maintainers will check for its completeness and check whether it is consistent. We will take special care to look for common mistakes, such as translating mark-up `<message>` code and such. We are often not fluent in every language, so we expect the translator and the international users to make corrections where necessary. Of course, there will always be some mistakes in every translation.
|
||||||
|
|
||||||
|
When the next Cura release comes around, some of the texts will have changed and some new texts will have been added. Around the time when the beta is released we will invoke a string freeze, meaning that no developer is allowed to make changes to the texts. Then we will update the translation template `.pot` files and ask all our translators to update their translations. If you are unable to update the translation in time for the actual release, we will remove the language from the drop-down menu in the Preferences window. The translation stays in Cura however, so that someone might pick it up again later and update it with the newest texts. Also, users who had previously selected the language can still continue Cura in their language but English text will appear among the original text.
|
||||||
|
|
|
||||||
48
cmake/CuraTests.cmake
Normal file
48
cmake/CuraTests.cmake
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
include(CMakeParseArguments)
|
||||||
|
|
||||||
|
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||||
|
|
||||||
|
function(cura_add_test)
|
||||||
|
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||||
|
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
|
||||||
|
|
||||||
|
if(NOT _NAME)
|
||||||
|
message(FATAL_ERROR "cura_add_test requires a test name argument")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT _DIRECTORY)
|
||||||
|
message(FATAL_ERROR "cura_add_test requires a directory to test")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT _PYTHONPATH)
|
||||||
|
set(_PYTHONPATH ${_DIRECTORY})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WIN32)
|
||||||
|
string(REPLACE "|" "\\;" _PYTHONPATH ${_PYTHONPATH})
|
||||||
|
else()
|
||||||
|
string(REPLACE "|" ":" _PYTHONPATH ${_PYTHONPATH})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ${_NAME}
|
||||||
|
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
||||||
|
)
|
||||||
|
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
|
||||||
|
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||||
|
|
||||||
|
file(GLOB_RECURSE _plugins plugins/*/__init__.py)
|
||||||
|
foreach(_plugin ${_plugins})
|
||||||
|
get_filename_component(_plugin_directory ${_plugin} DIRECTORY)
|
||||||
|
if(EXISTS ${_plugin_directory}/tests)
|
||||||
|
get_filename_component(_plugin_name ${_plugin_directory} NAME)
|
||||||
|
cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1
|
Name=Ultimaker Cura
|
||||||
Name=Cura
|
Name[de]=Ultimaker Cura
|
||||||
Name[de]=Cura
|
|
||||||
GenericName=3D Printing Software
|
GenericName=3D Printing Software
|
||||||
GenericName[de]=3D-Druck-Software
|
GenericName[de]=3D-Druck-Software
|
||||||
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
Comment=Cura converts 3D models into paths for a 3D printer. It prepares your print for maximum accuracy, minimum printing time and good reliability with many extra features that make your print come out great.
|
||||||
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
|
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
|
||||||
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
||||||
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||||
Icon=@CMAKE_INSTALL_FULL_DATADIR@/cura/resources/images/cura-icon.png
|
Icon=cura-icon
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png
|
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
||||||
Categories=Graphics;
|
Categories=Graphics;
|
||||||
Keywords=3D;Printing;
|
Keywords=3D;Printing;
|
||||||
|
|
|
||||||
190
cura/Arrange.py
Executable file
190
cura/Arrange.py
Executable file
|
|
@ -0,0 +1,190 @@
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from cura.ShapeArray import ShapeArray
|
||||||
|
from cura import ZOffsetDecorator
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
## Return object for bestSpot
|
||||||
|
LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
|
||||||
|
|
||||||
|
|
||||||
|
## The Arrange classed is used together with ShapeArray. Use it to find
|
||||||
|
# good locations for objects that you try to put on a build place.
|
||||||
|
# Different priority schemes can be defined so it alters the behavior while using
|
||||||
|
# the same logic.
|
||||||
|
class Arrange:
|
||||||
|
build_volume = None
|
||||||
|
|
||||||
|
def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
|
||||||
|
self.shape = (y, x)
|
||||||
|
self._priority = numpy.zeros((x, y), dtype=numpy.int32)
|
||||||
|
self._priority_unique_values = []
|
||||||
|
self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
|
||||||
|
self._scale = scale # convert input coordinates to arrange coordinates
|
||||||
|
self._offset_x = offset_x
|
||||||
|
self._offset_y = offset_y
|
||||||
|
self._last_priority = 0
|
||||||
|
|
||||||
|
## Helper to create an Arranger instance
|
||||||
|
#
|
||||||
|
# Either fill in scene_root and create will find all sliceable nodes by itself,
|
||||||
|
# or use fixed_nodes to provide the nodes yourself.
|
||||||
|
# \param scene_root Root for finding all scene nodes
|
||||||
|
# \param fixed_nodes Scene nodes to be placed
|
||||||
|
@classmethod
|
||||||
|
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5):
|
||||||
|
arranger = Arrange(220, 220, 110, 110, scale = scale)
|
||||||
|
arranger.centerFirst()
|
||||||
|
|
||||||
|
if fixed_nodes is None:
|
||||||
|
fixed_nodes = []
|
||||||
|
for node_ in DepthFirstIterator(scene_root):
|
||||||
|
# Only count sliceable objects
|
||||||
|
if node_.callDecoration("isSliceable"):
|
||||||
|
fixed_nodes.append(node_)
|
||||||
|
|
||||||
|
# Place all objects fixed nodes
|
||||||
|
for fixed_node in fixed_nodes:
|
||||||
|
vertices = fixed_node.callDecoration("getConvexHull")
|
||||||
|
if not vertices:
|
||||||
|
continue
|
||||||
|
points = copy.deepcopy(vertices._points)
|
||||||
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
|
arranger.place(0, 0, shape_arr)
|
||||||
|
|
||||||
|
# If a build volume was set, add the disallowed areas
|
||||||
|
if Arrange.build_volume:
|
||||||
|
disallowed_areas = Arrange.build_volume.getDisallowedAreas()
|
||||||
|
for area in disallowed_areas:
|
||||||
|
points = copy.deepcopy(area._points)
|
||||||
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
|
arranger.place(0, 0, shape_arr)
|
||||||
|
return arranger
|
||||||
|
|
||||||
|
## Find placement for a node (using offset shape) and place it (using hull shape)
|
||||||
|
# return the nodes that should be placed
|
||||||
|
# \param node
|
||||||
|
# \param offset_shape_arr ShapeArray with offset, used to find location
|
||||||
|
# \param hull_shape_arr ShapeArray without offset, for placing the shape
|
||||||
|
def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
|
||||||
|
new_node = copy.deepcopy(node)
|
||||||
|
best_spot = self.bestSpot(
|
||||||
|
offset_shape_arr, start_prio = self._last_priority, step = step)
|
||||||
|
x, y = best_spot.x, best_spot.y
|
||||||
|
|
||||||
|
# Save the last priority.
|
||||||
|
self._last_priority = best_spot.priority
|
||||||
|
|
||||||
|
# Ensure that the object is above the build platform
|
||||||
|
new_node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||||
|
if new_node.getBoundingBox():
|
||||||
|
center_y = new_node.getWorldPosition().y - new_node.getBoundingBox().bottom
|
||||||
|
else:
|
||||||
|
center_y = 0
|
||||||
|
|
||||||
|
if x is not None: # We could find a place
|
||||||
|
new_node.setPosition(Vector(x, center_y, y))
|
||||||
|
found_spot = True
|
||||||
|
self.place(x, y, hull_shape_arr) # place the object in arranger
|
||||||
|
else:
|
||||||
|
Logger.log("d", "Could not find spot!"),
|
||||||
|
found_spot = False
|
||||||
|
new_node.setPosition(Vector(200, center_y, 100))
|
||||||
|
return new_node, found_spot
|
||||||
|
|
||||||
|
## Fill priority, center is best. Lower value is better
|
||||||
|
# This is a strategy for the arranger.
|
||||||
|
def centerFirst(self):
|
||||||
|
# Square distance: creates a more round shape
|
||||||
|
self._priority = numpy.fromfunction(
|
||||||
|
lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
|
||||||
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
## Fill priority, back is best. Lower value is better
|
||||||
|
# This is a strategy for the arranger.
|
||||||
|
def backFirst(self):
|
||||||
|
self._priority = numpy.fromfunction(
|
||||||
|
lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
|
||||||
|
self._priority_unique_values = numpy.unique(self._priority)
|
||||||
|
self._priority_unique_values.sort()
|
||||||
|
|
||||||
|
## Return the amount of "penalty points" for polygon, which is the sum of priority
|
||||||
|
# None if occupied
|
||||||
|
# \param x x-coordinate to check shape
|
||||||
|
# \param y y-coordinate
|
||||||
|
# \param shape_arr the ShapeArray object to place
|
||||||
|
def checkShape(self, x, y, shape_arr):
|
||||||
|
x = int(self._scale * x)
|
||||||
|
y = int(self._scale * y)
|
||||||
|
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||||
|
offset_y = y + self._offset_y + shape_arr.offset_y
|
||||||
|
occupied_slice = self._occupied[
|
||||||
|
offset_y:offset_y + shape_arr.arr.shape[0],
|
||||||
|
offset_x:offset_x + shape_arr.arr.shape[1]]
|
||||||
|
try:
|
||||||
|
if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
|
||||||
|
return None
|
||||||
|
except IndexError: # out of bounds if you try to place an object outside
|
||||||
|
return None
|
||||||
|
prio_slice = self._priority[
|
||||||
|
offset_y:offset_y + shape_arr.arr.shape[0],
|
||||||
|
offset_x:offset_x + shape_arr.arr.shape[1]]
|
||||||
|
return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
|
||||||
|
|
||||||
|
## Find "best" spot for ShapeArray
|
||||||
|
# Return namedtuple with properties x, y, penalty_points, priority
|
||||||
|
# \param shape_arr ShapeArray
|
||||||
|
# \param start_prio Start with this priority value (and skip the ones before)
|
||||||
|
# \param step Slicing value, higher = more skips = faster but less accurate
|
||||||
|
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
|
||||||
|
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
|
||||||
|
if start_idx_list:
|
||||||
|
start_idx = start_idx_list[0][0]
|
||||||
|
else:
|
||||||
|
start_idx = 0
|
||||||
|
for priority in self._priority_unique_values[start_idx::step]:
|
||||||
|
tryout_idx = numpy.where(self._priority == priority)
|
||||||
|
for idx in range(len(tryout_idx[0])):
|
||||||
|
x = tryout_idx[0][idx]
|
||||||
|
y = tryout_idx[1][idx]
|
||||||
|
projected_x = x - self._offset_x
|
||||||
|
projected_y = y - self._offset_y
|
||||||
|
|
||||||
|
# array to "world" coordinates
|
||||||
|
penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
|
||||||
|
if penalty_points is not None:
|
||||||
|
return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
|
||||||
|
return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
|
||||||
|
|
||||||
|
## Place the object.
|
||||||
|
# Marks the locations in self._occupied and self._priority
|
||||||
|
# \param x x-coordinate
|
||||||
|
# \param y y-coordinate
|
||||||
|
# \param shape_arr ShapeArray object
|
||||||
|
def place(self, x, y, shape_arr):
|
||||||
|
x = int(self._scale * x)
|
||||||
|
y = int(self._scale * y)
|
||||||
|
offset_x = x + self._offset_x + shape_arr.offset_x
|
||||||
|
offset_y = y + self._offset_y + shape_arr.offset_y
|
||||||
|
shape_y, shape_x = self._occupied.shape
|
||||||
|
|
||||||
|
min_x = min(max(offset_x, 0), shape_x - 1)
|
||||||
|
min_y = min(max(offset_y, 0), shape_y - 1)
|
||||||
|
max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1)
|
||||||
|
max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
|
||||||
|
occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
|
||||||
|
# we use a slice of shape because it can be out of bounds
|
||||||
|
occupied_slice[numpy.where(shape_arr.arr[
|
||||||
|
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1
|
||||||
|
|
||||||
|
# Set priority to low (= high number), so it won't get picked at trying out.
|
||||||
|
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||||
|
prio_slice[numpy.where(shape_arr.arr[
|
||||||
|
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
|
||||||
91
cura/ArrangeObjectsJob.py
Executable file
91
cura/ArrangeObjectsJob.py
Executable file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
from cura.ZOffsetDecorator import ZOffsetDecorator
|
||||||
|
from cura.Arrange import Arrange
|
||||||
|
from cura.ShapeArray import ShapeArray
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class ArrangeObjectsJob(Job):
|
||||||
|
def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8):
|
||||||
|
super().__init__()
|
||||||
|
self._nodes = nodes
|
||||||
|
self._fixed_nodes = fixed_nodes
|
||||||
|
self._min_offset = min_offset
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
|
||||||
|
lifetime = 0,
|
||||||
|
dismissable=False,
|
||||||
|
progress = 0,
|
||||||
|
title = i18n_catalog.i18nc("@info:title", "Finding Location"))
|
||||||
|
status_message.show()
|
||||||
|
arranger = Arrange.create(fixed_nodes = self._fixed_nodes)
|
||||||
|
|
||||||
|
# Collect nodes to be placed
|
||||||
|
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||||
|
for node in self._nodes:
|
||||||
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
|
||||||
|
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
|
||||||
|
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).
|
||||||
|
# We also skip possibilities by slicing through the possibilities (step = 10)
|
||||||
|
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(offset_shape_arr, start_prio=start_priority, step=10)
|
||||||
|
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, hull_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, - idx * 20), set_position = True))
|
||||||
|
|
||||||
|
status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
|
grouped_operation.push()
|
||||||
|
|
||||||
|
status_message.hide()
|
||||||
|
|
||||||
|
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", "Can't Find Location"))
|
||||||
|
no_full_solution_message.show()
|
||||||
9
cura/BlockSlicingDecorator.py
Normal file
9
cura/BlockSlicingDecorator.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
class BlockSlicingDecorator(SceneNodeDecorator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def isBlockSlicing(self):
|
||||||
|
return True
|
||||||
586
cura/BuildVolume.py
Normal file → Executable file
586
cura/BuildVolume.py
Normal file → Executable file
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Scene.Platform import Platform
|
from UM.Scene.Platform import Platform
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
|
@ -10,6 +11,7 @@ from UM.Application import Application
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
|
|
@ -21,10 +23,9 @@ from UM.View.GL.OpenGL import OpenGL
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import copy
|
import math
|
||||||
|
|
||||||
import UM.Settings.ContainerRegistry
|
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
# Setting for clearance around the prime
|
# Setting for clearance around the prime
|
||||||
PRIME_CLEARANCE = 6.5
|
PRIME_CLEARANCE = 6.5
|
||||||
|
|
@ -32,19 +33,22 @@ PRIME_CLEARANCE = 6.5
|
||||||
|
|
||||||
## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
|
## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
|
||||||
class BuildVolume(SceneNode):
|
class BuildVolume(SceneNode):
|
||||||
VolumeOutlineColor = Color(12, 169, 227, 255)
|
|
||||||
XAxisColor = Color(255, 0, 0, 255)
|
|
||||||
YAxisColor = Color(0, 0, 255, 255)
|
|
||||||
ZAxisColor = Color(0, 255, 0, 255)
|
|
||||||
|
|
||||||
raftThicknessChanged = Signal()
|
raftThicknessChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._volume_outline_color = None
|
||||||
|
self._x_axis_color = None
|
||||||
|
self._y_axis_color = None
|
||||||
|
self._z_axis_color = None
|
||||||
|
self._disallowed_area_color = None
|
||||||
|
self._error_area_color = None
|
||||||
|
|
||||||
self._width = 0
|
self._width = 0
|
||||||
self._height = 0
|
self._height = 0
|
||||||
self._depth = 0
|
self._depth = 0
|
||||||
|
self._shape = ""
|
||||||
|
|
||||||
self._shader = None
|
self._shader = None
|
||||||
|
|
||||||
|
|
@ -65,6 +69,7 @@ class BuildVolume(SceneNode):
|
||||||
self._volume_aabb = None
|
self._volume_aabb = None
|
||||||
|
|
||||||
self._raft_thickness = 0.0
|
self._raft_thickness = 0.0
|
||||||
|
self._extra_z_clearance = 0.0
|
||||||
self._adhesion_type = None
|
self._adhesion_type = None
|
||||||
self._platform = Platform(self)
|
self._platform = Platform(self)
|
||||||
|
|
||||||
|
|
@ -72,40 +77,53 @@ class BuildVolume(SceneNode):
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
|
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
|
||||||
self._onStackChanged()
|
self._onStackChanged()
|
||||||
|
|
||||||
|
self._engine_ready = False
|
||||||
|
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
|
|
||||||
self._has_errors = False
|
self._has_errors = False
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
#Objects loaded at the moment. We are connected to the property changed events of these objects.
|
#Objects loaded at the moment. We are connected to the property changed events of these objects.
|
||||||
self._scene_objects = set()
|
self._scene_objects = set()
|
||||||
|
|
||||||
self._change_timer = QTimer()
|
self._scene_change_timer = QTimer()
|
||||||
self._change_timer.setInterval(100)
|
self._scene_change_timer.setInterval(100)
|
||||||
self._change_timer.setSingleShot(True)
|
self._scene_change_timer.setSingleShot(True)
|
||||||
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
self._scene_change_timer.timeout.connect(self._onSceneChangeTimerFinished)
|
||||||
|
|
||||||
|
self._setting_change_timer = QTimer()
|
||||||
|
self._setting_change_timer.setInterval(150)
|
||||||
|
self._setting_change_timer.setSingleShot(True)
|
||||||
|
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
|
||||||
|
|
||||||
self._build_volume_message = Message(catalog.i18nc("@info:status",
|
self._build_volume_message = Message(catalog.i18nc("@info:status",
|
||||||
"The build volume height has been reduced due to the value of the"
|
"The build volume height has been reduced due to the value of the"
|
||||||
" \"Print Sequence\" setting to prevent the gantry from colliding"
|
" \"Print Sequence\" setting to prevent the gantry from colliding"
|
||||||
" with printed models."))
|
" with printed models."), title = catalog.i18nc("@info:title","Build Volume"))
|
||||||
|
|
||||||
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
||||||
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
||||||
# Therefore this works.
|
# Therefore this works.
|
||||||
Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
|
Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
# This should also ways work, and it is semantically more correct,
|
# This should also ways work, and it is semantically more correct,
|
||||||
# but it does not update the disallowed areas after material change
|
# but it does not update the disallowed areas after material change
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
|
# list of settings which were updated
|
||||||
|
self._changed_settings_since_last_rebuild = []
|
||||||
|
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._change_timer.start()
|
self._scene_change_timer.start()
|
||||||
|
|
||||||
def _onChangeTimerFinished(self):
|
def _onSceneChangeTimerFinished(self):
|
||||||
root = Application.getInstance().getController().getScene().getRoot()
|
root = Application.getInstance().getController().getScene().getRoot()
|
||||||
new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode)
|
new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.callDecoration("isSliceable"))
|
||||||
if new_scene_objects != self._scene_objects:
|
if new_scene_objects != self._scene_objects:
|
||||||
for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
|
for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
|
||||||
node.decoratorsChanged.connect(self._onNodeDecoratorChanged)
|
self._updateNodeListeners(node)
|
||||||
|
node.decoratorsChanged.connect(self._updateNodeListeners) # Make sure that decoration changes afterwards also receive the same treatment
|
||||||
for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
|
for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if per_mesh_stack:
|
if per_mesh_stack:
|
||||||
|
|
@ -113,7 +131,7 @@ class BuildVolume(SceneNode):
|
||||||
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
||||||
if active_extruder_changed is not None:
|
if active_extruder_changed is not None:
|
||||||
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
||||||
node.decoratorsChanged.disconnect(self._onNodeDecoratorChanged)
|
node.decoratorsChanged.disconnect(self._updateNodeListeners)
|
||||||
|
|
||||||
self._scene_objects = new_scene_objects
|
self._scene_objects = new_scene_objects
|
||||||
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
||||||
|
|
@ -121,7 +139,7 @@ class BuildVolume(SceneNode):
|
||||||
## Updates the listeners that listen for changes in per-mesh stacks.
|
## Updates the listeners that listen for changes in per-mesh stacks.
|
||||||
#
|
#
|
||||||
# \param node The node for which the decorators changed.
|
# \param node The node for which the decorators changed.
|
||||||
def _onNodeDecoratorChanged(self, node):
|
def _updateNodeListeners(self, node: SceneNode):
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if per_mesh_stack:
|
if per_mesh_stack:
|
||||||
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||||
|
|
@ -131,18 +149,25 @@ class BuildVolume(SceneNode):
|
||||||
self._updateDisallowedAreasAndRebuild()
|
self._updateDisallowedAreasAndRebuild()
|
||||||
|
|
||||||
def setWidth(self, width):
|
def setWidth(self, width):
|
||||||
if width: self._width = width
|
if width is not None:
|
||||||
|
self._width = width
|
||||||
|
|
||||||
def setHeight(self, height):
|
def setHeight(self, height):
|
||||||
if height: self._height = height
|
if height is not None:
|
||||||
|
self._height = height
|
||||||
|
|
||||||
def setDepth(self, depth):
|
def setDepth(self, depth):
|
||||||
if depth: self._depth = depth
|
if depth is not None:
|
||||||
|
self._depth = depth
|
||||||
|
|
||||||
def getDisallowedAreas(self):
|
def setShape(self, shape: str):
|
||||||
|
if shape:
|
||||||
|
self._shape = shape
|
||||||
|
|
||||||
|
def getDisallowedAreas(self) -> List[Polygon]:
|
||||||
return self._disallowed_areas
|
return self._disallowed_areas
|
||||||
|
|
||||||
def setDisallowedAreas(self, areas):
|
def setDisallowedAreas(self, areas: List[Polygon]):
|
||||||
self._disallowed_areas = areas
|
self._disallowed_areas = areas
|
||||||
|
|
||||||
def render(self, renderer):
|
def render(self, renderer):
|
||||||
|
|
@ -152,6 +177,10 @@ class BuildVolume(SceneNode):
|
||||||
if not self._shader:
|
if not self._shader:
|
||||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
|
||||||
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
||||||
|
theme = Application.getInstance().getTheme()
|
||||||
|
self._grid_shader.setUniformValue("u_plateColor", Color(*theme.getColor("buildplate").getRgb()))
|
||||||
|
self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate_grid").getRgb()))
|
||||||
|
self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_grid_minor").getRgb()))
|
||||||
|
|
||||||
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
||||||
renderer.queueNode(self, mesh = self._origin_mesh)
|
renderer.queueNode(self, mesh = self._origin_mesh)
|
||||||
|
|
@ -165,11 +194,70 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
## For every sliceable node, update node._outside_buildarea
|
||||||
|
#
|
||||||
|
def updateNodeBoundaryCheck(self):
|
||||||
|
root = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
nodes = list(BreadthFirstIterator(root))
|
||||||
|
group_nodes = []
|
||||||
|
|
||||||
|
build_volume_bounding_box = self.getBoundingBox()
|
||||||
|
if build_volume_bounding_box:
|
||||||
|
# It's over 9000!
|
||||||
|
build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
|
||||||
|
else:
|
||||||
|
# No bounding box. This is triggered when running Cura from command line with a model for the first time
|
||||||
|
# In that situation there is a model, but no machine (and therefore no build volume.
|
||||||
|
return
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
# Need to check group nodes later
|
||||||
|
if node.callDecoration("isGroup"):
|
||||||
|
group_nodes.append(node) # Keep list of affected group_nodes
|
||||||
|
|
||||||
|
if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
|
||||||
|
node._outside_buildarea = False
|
||||||
|
bbox = node.getBoundingBox()
|
||||||
|
|
||||||
|
# Mark the node as outside the build volume if the bounding box test fails.
|
||||||
|
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||||
|
node._outside_buildarea = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
convex_hull = node.callDecoration("getConvexHull")
|
||||||
|
if convex_hull:
|
||||||
|
if not convex_hull.isValid():
|
||||||
|
return
|
||||||
|
# Check for collisions between disallowed areas and the object
|
||||||
|
for area in self.getDisallowedAreas():
|
||||||
|
overlap = convex_hull.intersectsPolygon(area)
|
||||||
|
if overlap is None:
|
||||||
|
continue
|
||||||
|
node._outside_buildarea = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Group nodes should override the _outside_buildarea property of their children.
|
||||||
|
for group_node in group_nodes:
|
||||||
|
for child_node in group_node.getAllChildren():
|
||||||
|
child_node._outside_buildarea = group_node._outside_buildarea
|
||||||
|
|
||||||
## Recalculates the build volume & disallowed areas.
|
## Recalculates the build volume & disallowed areas.
|
||||||
def rebuild(self):
|
def rebuild(self):
|
||||||
if not self._width or not self._height or not self._depth:
|
if not self._width or not self._height or not self._depth:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not Application.getInstance()._engine:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._volume_outline_color:
|
||||||
|
theme = Application.getInstance().getTheme()
|
||||||
|
self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
|
||||||
|
self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
|
||||||
|
self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
|
||||||
|
self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
|
||||||
|
self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
|
||||||
|
self._error_area_color = Color(*theme.getColor("error_area").getRgb())
|
||||||
|
|
||||||
min_w = -self._width / 2
|
min_w = -self._width / 2
|
||||||
max_w = self._width / 2
|
max_w = self._width / 2
|
||||||
min_h = 0.0
|
min_h = 0.0
|
||||||
|
|
@ -177,63 +265,35 @@ class BuildVolume(SceneNode):
|
||||||
min_d = -self._depth / 2
|
min_d = -self._depth / 2
|
||||||
max_d = self._depth / 2
|
max_d = self._depth / 2
|
||||||
|
|
||||||
mb = MeshBuilder()
|
z_fight_distance = 0.2 # Distance between buildplate and disallowed area meshes to prevent z-fighting
|
||||||
|
|
||||||
|
if self._shape != "elliptic":
|
||||||
# Outline 'cube' of the build volume
|
# Outline 'cube' of the build volume
|
||||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
|
mb = MeshBuilder()
|
||||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
|
||||||
|
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
|
||||||
|
|
||||||
mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
|
||||||
|
|
||||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
|
||||||
mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
|
||||||
|
|
||||||
self.setMeshData(mb.build())
|
self.setMeshData(mb.build())
|
||||||
|
|
||||||
mb = MeshBuilder()
|
# Build plate grid mesh
|
||||||
|
|
||||||
# Indication of the machine origin
|
|
||||||
if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
|
||||||
origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
|
|
||||||
else:
|
|
||||||
origin = Vector(min_w, min_h, max_d)
|
|
||||||
|
|
||||||
mb.addCube(
|
|
||||||
width = self._origin_line_length,
|
|
||||||
height = self._origin_line_width,
|
|
||||||
depth = self._origin_line_width,
|
|
||||||
center = origin + Vector(self._origin_line_length / 2, 0, 0),
|
|
||||||
color = self.XAxisColor
|
|
||||||
)
|
|
||||||
mb.addCube(
|
|
||||||
width = self._origin_line_width,
|
|
||||||
height = self._origin_line_length,
|
|
||||||
depth = self._origin_line_width,
|
|
||||||
center = origin + Vector(0, self._origin_line_length / 2, 0),
|
|
||||||
color = self.YAxisColor
|
|
||||||
)
|
|
||||||
mb.addCube(
|
|
||||||
width = self._origin_line_width,
|
|
||||||
height = self._origin_line_width,
|
|
||||||
depth = self._origin_line_length,
|
|
||||||
center = origin - Vector(0, 0, self._origin_line_length / 2),
|
|
||||||
color = self.ZAxisColor
|
|
||||||
)
|
|
||||||
self._origin_mesh = mb.build()
|
|
||||||
|
|
||||||
mb = MeshBuilder()
|
mb = MeshBuilder()
|
||||||
mb.addQuad(
|
mb.addQuad(
|
||||||
Vector(min_w, min_h - 0.2, min_d),
|
Vector(min_w, min_h - z_fight_distance, min_d),
|
||||||
Vector(max_w, min_h - 0.2, min_d),
|
Vector(max_w, min_h - z_fight_distance, min_d),
|
||||||
Vector(max_w, min_h - 0.2, max_d),
|
Vector(max_w, min_h - z_fight_distance, max_d),
|
||||||
Vector(min_w, min_h - 0.2, max_d)
|
Vector(min_w, min_h - z_fight_distance, max_d)
|
||||||
)
|
)
|
||||||
|
|
||||||
for n in range(0, 6):
|
for n in range(0, 6):
|
||||||
|
|
@ -241,13 +301,75 @@ class BuildVolume(SceneNode):
|
||||||
mb.setVertexUVCoordinates(n, v[0], v[2])
|
mb.setVertexUVCoordinates(n, v[0], v[2])
|
||||||
self._grid_mesh = mb.build()
|
self._grid_mesh = mb.build()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Bottom and top 'ellipse' of the build volume
|
||||||
|
aspect = 1.0
|
||||||
|
scale_matrix = Matrix()
|
||||||
|
if self._width != 0:
|
||||||
|
# Scale circular meshes by aspect ratio if width != height
|
||||||
|
aspect = self._depth / self._width
|
||||||
|
scale_matrix.compose(scale = Vector(1, 1, aspect))
|
||||||
|
mb = MeshBuilder()
|
||||||
|
mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color)
|
||||||
|
mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0), color = self._volume_outline_color)
|
||||||
|
self.setMeshData(mb.build().getTransformed(scale_matrix))
|
||||||
|
|
||||||
|
# Build plate grid mesh
|
||||||
|
mb = MeshBuilder()
|
||||||
|
mb.addVertex(0, min_h - z_fight_distance, 0)
|
||||||
|
mb.addArc(max_w, Vector.Unit_Y, center = Vector(0, min_h - z_fight_distance, 0))
|
||||||
|
sections = mb.getVertexCount() - 1 # Center point is not an arc section
|
||||||
|
indices = []
|
||||||
|
for n in range(0, sections - 1):
|
||||||
|
indices.append([0, n + 2, n + 1])
|
||||||
|
mb.addIndices(numpy.asarray(indices, dtype = numpy.int32))
|
||||||
|
mb.calculateNormals()
|
||||||
|
|
||||||
|
for n in range(0, mb.getVertexCount()):
|
||||||
|
v = mb.getVertex(n)
|
||||||
|
mb.setVertexUVCoordinates(n, v[0], v[2] * aspect)
|
||||||
|
self._grid_mesh = mb.build().getTransformed(scale_matrix)
|
||||||
|
|
||||||
|
# Indication of the machine origin
|
||||||
|
if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
||||||
|
origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
|
||||||
|
else:
|
||||||
|
origin = Vector(min_w, min_h, max_d)
|
||||||
|
|
||||||
|
mb = MeshBuilder()
|
||||||
|
mb.addCube(
|
||||||
|
width = self._origin_line_length,
|
||||||
|
height = self._origin_line_width,
|
||||||
|
depth = self._origin_line_width,
|
||||||
|
center = origin + Vector(self._origin_line_length / 2, 0, 0),
|
||||||
|
color = self._x_axis_color
|
||||||
|
)
|
||||||
|
mb.addCube(
|
||||||
|
width = self._origin_line_width,
|
||||||
|
height = self._origin_line_length,
|
||||||
|
depth = self._origin_line_width,
|
||||||
|
center = origin + Vector(0, self._origin_line_length / 2, 0),
|
||||||
|
color = self._y_axis_color
|
||||||
|
)
|
||||||
|
mb.addCube(
|
||||||
|
width = self._origin_line_width,
|
||||||
|
height = self._origin_line_width,
|
||||||
|
depth = self._origin_line_length,
|
||||||
|
center = origin - Vector(0, 0, self._origin_line_length / 2),
|
||||||
|
color = self._z_axis_color
|
||||||
|
)
|
||||||
|
self._origin_mesh = mb.build()
|
||||||
|
|
||||||
disallowed_area_height = 0.1
|
disallowed_area_height = 0.1
|
||||||
disallowed_area_size = 0
|
disallowed_area_size = 0
|
||||||
if self._disallowed_areas:
|
if self._disallowed_areas:
|
||||||
mb = MeshBuilder()
|
mb = MeshBuilder()
|
||||||
color = Color(0.0, 0.0, 0.0, 0.15)
|
color = self._disallowed_area_color
|
||||||
for polygon in self._disallowed_areas:
|
for polygon in self._disallowed_areas:
|
||||||
points = polygon.getPoints()
|
points = polygon.getPoints()
|
||||||
|
if len(points) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
||||||
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
||||||
for point in points:
|
for point in points:
|
||||||
|
|
@ -271,7 +393,7 @@ class BuildVolume(SceneNode):
|
||||||
if self._error_areas:
|
if self._error_areas:
|
||||||
mb = MeshBuilder()
|
mb = MeshBuilder()
|
||||||
for error_area in self._error_areas:
|
for error_area in self._error_areas:
|
||||||
color = Color(1.0, 0.0, 0.0, 0.5)
|
color = self._error_area_color
|
||||||
points = error_area.getPoints()
|
points = error_area.getPoints()
|
||||||
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
|
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
|
||||||
self._clamp(points[0][1], min_d, max_d))
|
self._clamp(points[0][1], min_d, max_d))
|
||||||
|
|
@ -288,7 +410,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
self._volume_aabb = AxisAlignedBox(
|
self._volume_aabb = AxisAlignedBox(
|
||||||
minimum = Vector(min_w, min_h - 1.0, min_d),
|
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||||
maximum = Vector(max_w, max_h - self._raft_thickness, max_d))
|
maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
|
||||||
|
|
||||||
bed_adhesion_size = self._getEdgeDisallowedSize()
|
bed_adhesion_size = self._getEdgeDisallowedSize()
|
||||||
|
|
||||||
|
|
@ -297,15 +419,17 @@ class BuildVolume(SceneNode):
|
||||||
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
|
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
|
||||||
scale_to_max_bounds = AxisAlignedBox(
|
scale_to_max_bounds = AxisAlignedBox(
|
||||||
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1),
|
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1),
|
||||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
|
Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||||
|
|
||||||
def getBoundingBox(self):
|
self.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
|
def getBoundingBox(self) -> AxisAlignedBox:
|
||||||
return self._volume_aabb
|
return self._volume_aabb
|
||||||
|
|
||||||
def getRaftThickness(self):
|
def getRaftThickness(self) -> float:
|
||||||
return self._raft_thickness
|
return self._raft_thickness
|
||||||
|
|
||||||
def _updateRaftThickness(self):
|
def _updateRaftThickness(self):
|
||||||
|
|
@ -318,13 +442,31 @@ class BuildVolume(SceneNode):
|
||||||
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
|
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
|
||||||
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
||||||
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
||||||
self._global_container_stack.getProperty("raft_airgap", "value"))
|
self._global_container_stack.getProperty("raft_airgap", "value") -
|
||||||
|
self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
|
||||||
|
|
||||||
# Rounding errors do not matter, we check if raft_thickness has changed at all
|
# Rounding errors do not matter, we check if raft_thickness has changed at all
|
||||||
if old_raft_thickness != self._raft_thickness:
|
if old_raft_thickness != self._raft_thickness:
|
||||||
self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
|
self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
|
||||||
self.raftThicknessChanged.emit()
|
self.raftThicknessChanged.emit()
|
||||||
|
|
||||||
|
def _updateExtraZClearance(self) -> None:
|
||||||
|
extra_z = 0.0
|
||||||
|
extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
|
||||||
|
use_extruders = False
|
||||||
|
for extruder in extruders:
|
||||||
|
if extruder.getProperty("retraction_hop_enabled", "value"):
|
||||||
|
retraction_hop = extruder.getProperty("retraction_hop", "value")
|
||||||
|
if extra_z is None or retraction_hop > extra_z:
|
||||||
|
extra_z = retraction_hop
|
||||||
|
use_extruders = True
|
||||||
|
if not use_extruders:
|
||||||
|
# If no extruders, take global value.
|
||||||
|
if self._global_container_stack.getProperty("retraction_hop_enabled", "value"):
|
||||||
|
extra_z = self._global_container_stack.getProperty("retraction_hop", "value")
|
||||||
|
if extra_z != self._extra_z_clearance:
|
||||||
|
self._extra_z_clearance = extra_z
|
||||||
|
|
||||||
## Update the build volume visualization
|
## Update the build volume visualization
|
||||||
def _onStackChanged(self):
|
def _onStackChanged(self):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
|
|
@ -353,17 +495,27 @@ class BuildVolume(SceneNode):
|
||||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
self._build_volume_message.hide()
|
self._build_volume_message.hide()
|
||||||
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||||
|
|
||||||
self._updateDisallowedAreas()
|
self._updateDisallowedAreas()
|
||||||
self._updateRaftThickness()
|
self._updateRaftThickness()
|
||||||
|
self._updateExtraZClearance()
|
||||||
|
|
||||||
|
if self._engine_ready:
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
def _onSettingPropertyChanged(self, setting_key, property_name):
|
def _onEngineCreated(self):
|
||||||
if property_name != "value":
|
self._engine_ready = True
|
||||||
return
|
self.rebuild()
|
||||||
|
|
||||||
|
def _onSettingChangeTimerFinished(self):
|
||||||
rebuild_me = False
|
rebuild_me = False
|
||||||
|
update_disallowed_areas = False
|
||||||
|
update_raft_thickness = False
|
||||||
|
update_extra_z_clearance = True
|
||||||
|
|
||||||
|
for setting_key in self._changed_settings_since_last_rebuild:
|
||||||
|
|
||||||
if setting_key == "print_sequence":
|
if setting_key == "print_sequence":
|
||||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||||
|
|
@ -375,20 +527,60 @@ class BuildVolume(SceneNode):
|
||||||
else:
|
else:
|
||||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
self._build_volume_message.hide()
|
self._build_volume_message.hide()
|
||||||
|
update_disallowed_areas = True
|
||||||
rebuild_me = True
|
rebuild_me = True
|
||||||
|
|
||||||
if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings:
|
# sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
|
||||||
self._updateDisallowedAreas()
|
if setting_key in self._machine_settings:
|
||||||
|
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||||
|
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
|
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
self._shape = self._global_container_stack.getProperty("machine_shape", "value")
|
||||||
|
update_extra_z_clearance = True
|
||||||
|
update_disallowed_areas = True
|
||||||
|
rebuild_me = True
|
||||||
|
|
||||||
|
if setting_key in self._skirt_settings + self._prime_settings + self._tower_settings + self._ooze_shield_settings + self._distance_settings + self._extruder_settings:
|
||||||
|
update_disallowed_areas = True
|
||||||
rebuild_me = True
|
rebuild_me = True
|
||||||
|
|
||||||
if setting_key in self._raft_settings:
|
if setting_key in self._raft_settings:
|
||||||
self._updateRaftThickness()
|
update_raft_thickness = True
|
||||||
rebuild_me = True
|
rebuild_me = True
|
||||||
|
|
||||||
|
if setting_key in self._extra_z_settings:
|
||||||
|
update_extra_z_clearance = True
|
||||||
|
rebuild_me = True
|
||||||
|
|
||||||
|
if setting_key in self._limit_to_extruder_settings:
|
||||||
|
update_disallowed_areas = True
|
||||||
|
rebuild_me = True
|
||||||
|
|
||||||
|
# We only want to update all of them once.
|
||||||
|
if update_disallowed_areas:
|
||||||
|
self._updateDisallowedAreas()
|
||||||
|
|
||||||
|
if update_raft_thickness:
|
||||||
|
self._updateRaftThickness()
|
||||||
|
|
||||||
|
if update_extra_z_clearance:
|
||||||
|
self._updateExtraZClearance()
|
||||||
|
|
||||||
if rebuild_me:
|
if rebuild_me:
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
def hasErrors(self):
|
# We just did a rebuild, reset the list.
|
||||||
|
self._changed_settings_since_last_rebuild = []
|
||||||
|
|
||||||
|
def _onSettingPropertyChanged(self, setting_key: str, property_name: str):
|
||||||
|
if property_name != "value":
|
||||||
|
return
|
||||||
|
|
||||||
|
if setting_key not in self._changed_settings_since_last_rebuild:
|
||||||
|
self._changed_settings_since_last_rebuild.append(setting_key)
|
||||||
|
self._setting_change_timer.start()
|
||||||
|
|
||||||
|
def hasErrors(self) -> bool:
|
||||||
return self._has_errors
|
return self._has_errors
|
||||||
|
|
||||||
## Calls _updateDisallowedAreas and makes sure the changes appear in the
|
## Calls _updateDisallowedAreas and makes sure the changes appear in the
|
||||||
|
|
@ -400,6 +592,8 @@ class BuildVolume(SceneNode):
|
||||||
# would hit performance.
|
# would hit performance.
|
||||||
def _updateDisallowedAreasAndRebuild(self):
|
def _updateDisallowedAreasAndRebuild(self):
|
||||||
self._updateDisallowedAreas()
|
self._updateDisallowedAreas()
|
||||||
|
self._updateRaftThickness()
|
||||||
|
self._updateExtraZClearance()
|
||||||
self.rebuild()
|
self.rebuild()
|
||||||
|
|
||||||
def _updateDisallowedAreas(self):
|
def _updateDisallowedAreas(self):
|
||||||
|
|
@ -412,8 +606,15 @@ class BuildVolume(SceneNode):
|
||||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
disallowed_border_size = self._getEdgeDisallowedSize()
|
||||||
|
|
||||||
|
if not used_extruders:
|
||||||
|
# If no extruder is used, assume that the active extruder is used (else nothing is drawn)
|
||||||
|
if extruder_manager.getActiveExtruderStack():
|
||||||
|
used_extruders = [extruder_manager.getActiveExtruderStack()]
|
||||||
|
else:
|
||||||
|
used_extruders = [self._global_container_stack]
|
||||||
|
|
||||||
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
||||||
prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
|
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
||||||
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
||||||
|
|
||||||
#Check if prime positions intersect with disallowed areas.
|
#Check if prime positions intersect with disallowed areas.
|
||||||
|
|
@ -451,6 +652,7 @@ class BuildVolume(SceneNode):
|
||||||
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
||||||
|
|
||||||
# Add prime tower location as disallowed area.
|
# Add prime tower location as disallowed area.
|
||||||
|
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
||||||
prime_tower_collision = False
|
prime_tower_collision = False
|
||||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||||
for extruder_id in prime_tower_areas:
|
for extruder_id in prime_tower_areas:
|
||||||
|
|
@ -486,12 +688,15 @@ class BuildVolume(SceneNode):
|
||||||
result[extruder.getId()] = []
|
result[extruder.getId()] = []
|
||||||
|
|
||||||
#Currently, the only normally printed object is the prime tower.
|
#Currently, the only normally printed object is the prime tower.
|
||||||
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable") == True:
|
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable"):
|
||||||
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
|
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
|
||||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value")
|
||||||
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
|
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value")
|
||||||
|
if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
||||||
|
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||||
|
prime_tower_y = prime_tower_y + machine_depth / 2
|
||||||
|
|
||||||
prime_tower_area = Polygon([
|
prime_tower_area = Polygon([
|
||||||
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
|
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
|
||||||
|
|
@ -505,7 +710,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Computes the disallowed areas for the prime locations.
|
## Computes the disallowed areas for the prime blobs.
|
||||||
#
|
#
|
||||||
# These are special because they are not subject to things like brim or
|
# These are special because they are not subject to things like brim or
|
||||||
# travel avoidance. They do get a dilute with the border size though
|
# travel avoidance. They do get a dilute with the border size though
|
||||||
|
|
@ -516,18 +721,29 @@ class BuildVolume(SceneNode):
|
||||||
# \param used_extruders The extruder stacks to generate disallowed areas
|
# \param used_extruders The extruder stacks to generate disallowed areas
|
||||||
# for.
|
# for.
|
||||||
# \return A dictionary with for each used extruder ID the prime areas.
|
# \return A dictionary with for each used extruder ID the prime areas.
|
||||||
def _computeDisallowedAreasPrime(self, border_size, used_extruders):
|
def _computeDisallowedAreasPrimeBlob(self, border_size, used_extruders):
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||||
for extruder in used_extruders:
|
for extruder in used_extruders:
|
||||||
prime_x = extruder.getProperty("extruder_prime_pos_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
prime_blob_enabled = extruder.getProperty("prime_blob_enable", "value")
|
||||||
prime_y = machine_depth / 2 - extruder.getProperty("extruder_prime_pos_y", "value")
|
prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
|
||||||
|
prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
|
||||||
|
|
||||||
|
#Ignore extruder prime position if it is not set or if blob is disabled
|
||||||
|
if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
|
||||||
|
result[extruder.getId()] = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
||||||
|
prime_x = prime_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||||
|
prime_y = prime_y + machine_depth / 2
|
||||||
|
|
||||||
prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
|
prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
|
||||||
prime_polygon = prime_polygon.translate(prime_x, prime_y)
|
|
||||||
prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||||
|
|
||||||
|
prime_polygon = prime_polygon.translate(prime_x, prime_y)
|
||||||
result[extruder.getId()] = [prime_polygon]
|
result[extruder.getId()] = [prime_polygon]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -552,6 +768,11 @@ class BuildVolume(SceneNode):
|
||||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||||
machine_disallowed_polygons.append(polygon)
|
machine_disallowed_polygons.append(polygon)
|
||||||
|
|
||||||
|
# For certain machines we don't need to compute disallowed areas for each nozzle.
|
||||||
|
# So we check here and only do the nozzle offsetting if needed.
|
||||||
|
nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry(
|
||||||
|
"nozzle_offsetting_for_disallowed_areas", True)
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
for extruder in used_extruders:
|
for extruder in used_extruders:
|
||||||
extruder_id = extruder.getId()
|
extruder_id = extruder.getId()
|
||||||
|
|
@ -571,16 +792,26 @@ class BuildVolume(SceneNode):
|
||||||
right_unreachable_border = 0
|
right_unreachable_border = 0
|
||||||
top_unreachable_border = 0
|
top_unreachable_border = 0
|
||||||
bottom_unreachable_border = 0
|
bottom_unreachable_border = 0
|
||||||
|
|
||||||
|
# Only do nozzle offsetting if needed
|
||||||
|
if nozzle_offsetting_for_disallowed_areas:
|
||||||
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
|
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
|
||||||
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||||
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
|
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
|
||||||
|
if other_offset_x is None:
|
||||||
|
other_offset_x = 0
|
||||||
other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
|
other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||||
|
if other_offset_y is None:
|
||||||
|
other_offset_y = 0
|
||||||
|
other_offset_y = -other_offset_y
|
||||||
left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
|
left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
|
||||||
right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
|
right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
|
||||||
top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
|
top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
|
||||||
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
|
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
|
||||||
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
||||||
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
||||||
|
|
||||||
|
if self._shape != "elliptic":
|
||||||
if border_size - left_unreachable_border > 0:
|
if border_size - left_unreachable_border > 0:
|
||||||
result[extruder_id].append(Polygon(numpy.array([
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
[-half_machine_width, -half_machine_depth],
|
[-half_machine_width, -half_machine_depth],
|
||||||
|
|
@ -609,18 +840,52 @@ class BuildVolume(SceneNode):
|
||||||
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
|
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
|
||||||
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
|
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
|
||||||
], numpy.float32)))
|
], numpy.float32)))
|
||||||
|
else:
|
||||||
|
sections = 32
|
||||||
|
arc_vertex = [0, half_machine_depth - border_size]
|
||||||
|
for i in range(0, sections):
|
||||||
|
quadrant = math.floor(4 * i / sections)
|
||||||
|
vertices = []
|
||||||
|
if quadrant == 0:
|
||||||
|
vertices.append([-half_machine_width, half_machine_depth])
|
||||||
|
elif quadrant == 1:
|
||||||
|
vertices.append([-half_machine_width, -half_machine_depth])
|
||||||
|
elif quadrant == 2:
|
||||||
|
vertices.append([half_machine_width, -half_machine_depth])
|
||||||
|
elif quadrant == 3:
|
||||||
|
vertices.append([half_machine_width, half_machine_depth])
|
||||||
|
vertices.append(arc_vertex)
|
||||||
|
|
||||||
|
angle = 2 * math.pi * (i + 1) / sections
|
||||||
|
arc_vertex = [-(half_machine_width - border_size) * math.sin(angle), (half_machine_depth - border_size) * math.cos(angle)]
|
||||||
|
vertices.append(arc_vertex)
|
||||||
|
|
||||||
|
result[extruder_id].append(Polygon(numpy.array(vertices, numpy.float32)))
|
||||||
|
|
||||||
|
if border_size > 0:
|
||||||
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
|
[-half_machine_width, -half_machine_depth],
|
||||||
|
[-half_machine_width, half_machine_depth],
|
||||||
|
[-half_machine_width + border_size, 0]
|
||||||
|
], numpy.float32)))
|
||||||
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
|
[-half_machine_width, half_machine_depth],
|
||||||
|
[ half_machine_width, half_machine_depth],
|
||||||
|
[ 0, half_machine_depth - border_size]
|
||||||
|
], numpy.float32)))
|
||||||
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
|
[ half_machine_width, half_machine_depth],
|
||||||
|
[ half_machine_width, -half_machine_depth],
|
||||||
|
[ half_machine_width - border_size, 0]
|
||||||
|
], numpy.float32)))
|
||||||
|
result[extruder_id].append(Polygon(numpy.array([
|
||||||
|
[ half_machine_width,-half_machine_depth],
|
||||||
|
[-half_machine_width,-half_machine_depth],
|
||||||
|
[ 0, -half_machine_depth + border_size]
|
||||||
|
], numpy.float32)))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Private convenience function to get a setting from the adhesion
|
|
||||||
# extruder.
|
|
||||||
#
|
|
||||||
# \param setting_key The key of the setting to get.
|
|
||||||
# \param property The property to get from the setting.
|
|
||||||
# \return The property of the specified setting in the adhesion extruder.
|
|
||||||
def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"):
|
|
||||||
return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property)
|
|
||||||
|
|
||||||
## Private convenience function to get a setting from every extruder.
|
## Private convenience function to get a setting from every extruder.
|
||||||
#
|
#
|
||||||
# For single extrusion machines, this gets the setting from the global
|
# For single extrusion machines, this gets the setting from the global
|
||||||
|
|
@ -628,40 +893,12 @@ class BuildVolume(SceneNode):
|
||||||
#
|
#
|
||||||
# \return A sequence of setting values, one for each extruder.
|
# \return A sequence of setting values, one for each extruder.
|
||||||
def _getSettingFromAllExtruders(self, setting_key, property = "value"):
|
def _getSettingFromAllExtruders(self, setting_key, property = "value"):
|
||||||
return ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)
|
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)
|
||||||
|
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
|
||||||
## Private convenience function to get a setting from the support infill
|
for i in range(len(all_values)):
|
||||||
# extruder.
|
if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"):
|
||||||
#
|
all_values[i] = 0
|
||||||
# \param setting_key The key of the setting to get.
|
return all_values
|
||||||
# \param property The property to get from the setting.
|
|
||||||
# \return The property of the specified setting in the support infill
|
|
||||||
# extruder.
|
|
||||||
def _getSettingFromSupportInfillExtruder(self, setting_key, property = "value"):
|
|
||||||
return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property)
|
|
||||||
|
|
||||||
## Helper function to get a setting from an extruder specified in another
|
|
||||||
# setting.
|
|
||||||
#
|
|
||||||
# \param setting_key The key of the setting to get.
|
|
||||||
# \param extruder_setting_key The key of the setting that specifies from
|
|
||||||
# which extruder to get the setting, if there are multiple extruders.
|
|
||||||
# \param property The property to get from the setting.
|
|
||||||
# \return The property of the specified setting in the specified extruder.
|
|
||||||
def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"):
|
|
||||||
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
|
||||||
|
|
||||||
if not multi_extrusion:
|
|
||||||
return self._global_container_stack.getProperty(setting_key, property)
|
|
||||||
|
|
||||||
extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value")
|
|
||||||
|
|
||||||
if extruder_index == "-1": # If extruder index is -1 use global instead
|
|
||||||
return self._global_container_stack.getProperty(setting_key, property)
|
|
||||||
|
|
||||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
|
||||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
|
||||||
return stack.getProperty(setting_key, property)
|
|
||||||
|
|
||||||
## Convenience function to calculate the disallowed radius around the edge.
|
## Convenience function to calculate the disallowed radius around the edge.
|
||||||
#
|
#
|
||||||
|
|
@ -671,41 +908,53 @@ class BuildVolume(SceneNode):
|
||||||
def _getEdgeDisallowedSize(self):
|
def _getEdgeDisallowedSize(self):
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
container_stack = self._global_container_stack
|
container_stack = self._global_container_stack
|
||||||
|
used_extruders = ExtruderManager.getInstance().getUsedExtruderStacks()
|
||||||
|
|
||||||
# If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
|
# If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
|
||||||
if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||||
return 0.1 # Return a very small value, so we do draw disallowed area's near the edges.
|
return 0.1 # Return a very small value, so we do draw disallowed area's near the edges.
|
||||||
|
|
||||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
adhesion_type = container_stack.getProperty("adhesion_type", "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")
|
||||||
if adhesion_type == "skirt":
|
if adhesion_type == "skirt":
|
||||||
skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap")
|
skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
|
||||||
skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count")
|
skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
|
||||||
bed_adhesion_size = skirt_distance + (skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width"))
|
|
||||||
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
bed_adhesion_size = skirt_distance + (skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
|
||||||
adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
|
|
||||||
extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
|
for extruder_stack in used_extruders:
|
||||||
del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr.
|
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
|
||||||
for value in extruder_values:
|
|
||||||
bed_adhesion_size += value
|
# We don't create an additional line for the extruder we're printing the skirt with.
|
||||||
|
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
|
||||||
|
|
||||||
elif adhesion_type == "brim":
|
elif adhesion_type == "brim":
|
||||||
bed_adhesion_size = self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("skirt_brim_line_width")
|
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
|
||||||
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
|
||||||
adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
|
|
||||||
extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
|
for extruder_stack in used_extruders:
|
||||||
del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr.
|
bed_adhesion_size += extruder_stack.getProperty("skirt_brim_line_width", "value") * extruder_stack.getProperty("initial_layer_line_width_factor", "value") / 100.0
|
||||||
for value in extruder_values:
|
|
||||||
bed_adhesion_size += value
|
# 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
|
||||||
|
|
||||||
elif adhesion_type == "raft":
|
elif adhesion_type == "raft":
|
||||||
bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin")
|
bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
|
||||||
|
|
||||||
elif adhesion_type == "none":
|
elif adhesion_type == "none":
|
||||||
bed_adhesion_size = 0
|
bed_adhesion_size = 0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
||||||
|
|
||||||
support_expansion = 0
|
support_expansion = 0
|
||||||
if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"):
|
support_enabled = self._global_container_stack.getProperty("support_enable", "value")
|
||||||
support_expansion += self._getSettingFromSupportInfillExtruder("support_offset")
|
support_offset = self._global_container_stack.getProperty("support_offset", "value")
|
||||||
|
if support_enabled and support_offset:
|
||||||
|
support_expansion += support_offset
|
||||||
|
|
||||||
farthest_shield_distance = 0
|
farthest_shield_distance = 0
|
||||||
if container_stack.getProperty("draft_shield_enabled", "value"):
|
if container_stack.getProperty("draft_shield_enabled", "value"):
|
||||||
|
|
@ -715,11 +964,11 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
move_from_wall_radius = 0 # Moves that start from outer wall.
|
move_from_wall_radius = 0 # Moves that start from outer wall.
|
||||||
move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
|
move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
|
||||||
avoid_enabled_per_extruder = self._getSettingFromAllExtruders(("travel_avoid_other_parts"))
|
avoid_enabled_per_extruder = [stack.getProperty("travel_avoid_other_parts","value") for stack in used_extruders]
|
||||||
avoid_distance_per_extruder = self._getSettingFromAllExtruders("travel_avoid_distance")
|
travel_avoid_distance_per_extruder = [stack.getProperty("travel_avoid_distance", "value") for stack in used_extruders]
|
||||||
for index, avoid_other_parts_enabled in enumerate(avoid_enabled_per_extruder): #For each extruder (or just global).
|
for avoid_other_parts_enabled, avoid_distance in zip(avoid_enabled_per_extruder, travel_avoid_distance_per_extruder): #For each extruder (or just global).
|
||||||
if avoid_other_parts_enabled:
|
if avoid_other_parts_enabled:
|
||||||
move_from_wall_radius = max(move_from_wall_radius, avoid_distance_per_extruder[index]) #Index of the same extruder.
|
move_from_wall_radius = max(move_from_wall_radius, avoid_distance)
|
||||||
|
|
||||||
# Now combine our different pieces of data to get the final border size.
|
# Now combine our different pieces of data to get the final border size.
|
||||||
# Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
|
# Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
|
||||||
|
|
@ -730,10 +979,13 @@ class BuildVolume(SceneNode):
|
||||||
def _clamp(self, value, min_value, max_value):
|
def _clamp(self, value, min_value, max_value):
|
||||||
return max(min(value, max_value), min_value)
|
return max(min(value, max_value), min_value)
|
||||||
|
|
||||||
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist"]
|
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
|
||||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
|
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
|
||||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
|
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
||||||
|
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
||||||
|
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
||||||
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
||||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
||||||
_extruder_settings = ["support_enable", "support_interface_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_interface_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
||||||
|
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QVariantAnimation, QEasingCurve
|
from PyQt5.QtCore import QVariantAnimation, QEasingCurve
|
||||||
|
|
@ -7,12 +7,13 @@ from PyQt5.QtGui import QVector3D
|
||||||
|
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
|
||||||
|
|
||||||
class CameraAnimation(QVariantAnimation):
|
class CameraAnimation(QVariantAnimation):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._camera_tool = None
|
self._camera_tool = None
|
||||||
self.setDuration(500)
|
self.setDuration(300)
|
||||||
self.setEasingCurve(QEasingCurve.InOutQuad)
|
self.setEasingCurve(QEasingCurve.OutQuad)
|
||||||
|
|
||||||
def setCameraTool(self, camera_tool):
|
def setCameraTool(self, camera_tool):
|
||||||
self._camera_tool = camera_tool
|
self._camera_tool = camera_tool
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
from . import ConvexHullNode
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
import UM.Settings.ContainerRegistry
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from . import ConvexHullNode
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
@ -56,10 +56,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
if self._node is None:
|
if self._node is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if getattr(self._node, "_non_printing_mesh", False):
|
||||||
|
# infill_mesh, cutting_mesh and anti_overhang_mesh do not need a convex hull
|
||||||
|
# node._non_printing_mesh is set in SettingOverrideDecorator
|
||||||
|
return None
|
||||||
|
|
||||||
hull = self._compute2DConvexHull()
|
hull = self._compute2DConvexHull()
|
||||||
|
|
||||||
if self._global_stack and self._node:
|
if self._global_stack and self._node:
|
||||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
|
# Parent can be None if node is just loaded.
|
||||||
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||||
hull = self._add2DAdhesionMargin(hull)
|
hull = self._add2DAdhesionMargin(hull)
|
||||||
return hull
|
return hull
|
||||||
|
|
@ -79,7 +85,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||||
head_with_fans = self._compute2DConvexHeadMin()
|
head_with_fans = self._compute2DConvexHeadMin()
|
||||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||||
return head_with_fans_with_adhesion_margin
|
return head_with_fans_with_adhesion_margin
|
||||||
|
|
@ -93,8 +99,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
|
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||||
|
|
||||||
# Printing one at a time and it's not an object in a group
|
# Printing one at a time and it's not an object in a group
|
||||||
return self._compute2DConvexHull()
|
return self._compute2DConvexHull()
|
||||||
return None
|
return None
|
||||||
|
|
@ -197,7 +202,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
hull = Polygon(vertex_data)
|
hull = Polygon(vertex_data)
|
||||||
|
|
||||||
if len(vertex_data) >= 4:
|
if len(vertex_data) >= 3:
|
||||||
convex_hull = hull.getConvexHull()
|
convex_hull = hull.getConvexHull()
|
||||||
offset_hull = self._offsetHull(convex_hull)
|
offset_hull = self._offsetHull(convex_hull)
|
||||||
else:
|
else:
|
||||||
|
|
@ -253,21 +258,29 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
## Offset the convex hull with settings that influence the collision area.
|
## Offset the convex hull with settings that influence the collision area.
|
||||||
#
|
#
|
||||||
# This also applies a minimum offset of 0.5mm, because of edge cases due
|
|
||||||
# to the rounding we apply.
|
|
||||||
#
|
|
||||||
# \param convex_hull Polygon of the original convex hull.
|
# \param convex_hull Polygon of the original convex hull.
|
||||||
# \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.
|
||||||
def _offsetHull(self, convex_hull):
|
def _offsetHull(self, convex_hull):
|
||||||
horizontal_expansion = max(0.5, self._getSettingProperty("xy_offset", "value"))
|
horizontal_expansion = max(
|
||||||
|
self._getSettingProperty("xy_offset", "value"),
|
||||||
|
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||||
|
)
|
||||||
|
|
||||||
|
mold_width = 0
|
||||||
|
if self._getSettingProperty("mold_enabled", "value"):
|
||||||
|
mold_width = self._getSettingProperty("mold_width", "value")
|
||||||
|
hull_offset = horizontal_expansion + mold_width
|
||||||
|
if hull_offset > 0: #TODO: Implement Minkowski subtraction for if the offset < 0.
|
||||||
expansion_polygon = Polygon(numpy.array([
|
expansion_polygon = Polygon(numpy.array([
|
||||||
[-horizontal_expansion, -horizontal_expansion],
|
[-hull_offset, -hull_offset],
|
||||||
[-horizontal_expansion, horizontal_expansion],
|
[-hull_offset, hull_offset],
|
||||||
[horizontal_expansion, horizontal_expansion],
|
[hull_offset, hull_offset],
|
||||||
[horizontal_expansion, -horizontal_expansion]
|
[hull_offset, -hull_offset]
|
||||||
], numpy.float32))
|
], numpy.float32))
|
||||||
return convex_hull.getMinkowskiHull(expansion_polygon)
|
return convex_hull.getMinkowskiHull(expansion_polygon)
|
||||||
|
else:
|
||||||
|
return convex_hull
|
||||||
|
|
||||||
def _onChanged(self, *args):
|
def _onChanged(self, *args):
|
||||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||||
|
|
@ -294,26 +307,23 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
||||||
def _getSettingProperty(self, setting_key, property="value"):
|
def _getSettingProperty(self, setting_key, prop = "value"):
|
||||||
per_mesh_stack = self._node.callDecoration("getStack")
|
per_mesh_stack = self._node.callDecoration("getStack")
|
||||||
if per_mesh_stack:
|
if per_mesh_stack:
|
||||||
return per_mesh_stack.getProperty(setting_key, property)
|
return per_mesh_stack.getProperty(setting_key, prop)
|
||||||
|
|
||||||
multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1
|
|
||||||
if not multi_extrusion:
|
|
||||||
return self._global_stack.getProperty(setting_key, property)
|
|
||||||
|
|
||||||
extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder")
|
extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder")
|
||||||
if extruder_index == "-1": #No limit_to_extruder.
|
if extruder_index == "-1":
|
||||||
|
# No limit_to_extruder
|
||||||
extruder_stack_id = self._node.callDecoration("getActiveExtruder")
|
extruder_stack_id = self._node.callDecoration("getActiveExtruder")
|
||||||
if not extruder_stack_id: #Decoration doesn't exist.
|
if not extruder_stack_id:
|
||||||
|
# Decoration doesn't exist
|
||||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
|
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
|
||||||
extruder_stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||||
return extruder_stack.getProperty(setting_key, property)
|
return extruder_stack.getProperty(setting_key, prop)
|
||||||
else: #Limit_to_extruder is set. Use that one.
|
else:
|
||||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
# Limit_to_extruder is set. The global stack handles this then
|
||||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
return self._global_stack.getProperty(setting_key, prop)
|
||||||
return stack.getProperty(setting_key, property)
|
|
||||||
|
|
||||||
## Returns true if node is a descendant or the same as the root node.
|
## Returns true if node is a descendant or the same as the root node.
|
||||||
def __isDescendant(self, root, node):
|
def __isDescendant(self, root, node):
|
||||||
|
|
@ -324,11 +334,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
return self.__isDescendant(root, node.getParent())
|
return self.__isDescendant(root, node.getParent())
|
||||||
|
|
||||||
_affected_settings = [
|
_affected_settings = [
|
||||||
"adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers",
|
"adhesion_type", "raft_margin", "print_sequence",
|
||||||
"raft_surface_thickness", "raft_airgap", "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"]
|
||||||
|
|
||||||
## 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.
|
||||||
_influencing_settings = {"xy_offset"}
|
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width"}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
|
|
@ -8,7 +9,10 @@ from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the c
|
||||||
|
|
||||||
from UM.View.GL.OpenGL import OpenGL
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
|
||||||
|
|
||||||
class ConvexHullNode(SceneNode):
|
class ConvexHullNode(SceneNode):
|
||||||
|
shader = None # To prevent the shader from being re-built over and over again, only load it once.
|
||||||
|
|
||||||
## Convex hull node is a special type of scene node that is used to display an area, to indicate the
|
## Convex hull node is a special type of scene node that is used to display an area, to indicate the
|
||||||
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||||
|
|
@ -18,12 +22,10 @@ class ConvexHullNode(SceneNode):
|
||||||
|
|
||||||
self.setCalculateBoundingBox(False)
|
self.setCalculateBoundingBox(False)
|
||||||
|
|
||||||
self._shader = None
|
|
||||||
|
|
||||||
self._original_parent = parent
|
self._original_parent = parent
|
||||||
|
|
||||||
# Color of the drawn convex hull
|
# Color of the drawn convex hull
|
||||||
self._color = Color(0.4, 0.4, 0.4, 1.0)
|
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
||||||
|
|
||||||
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
|
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
|
||||||
self._mesh_height = 0.1
|
self._mesh_height = 0.1
|
||||||
|
|
@ -58,22 +60,20 @@ class ConvexHullNode(SceneNode):
|
||||||
return self._node
|
return self._node
|
||||||
|
|
||||||
def render(self, renderer):
|
def render(self, renderer):
|
||||||
if not self._shader:
|
if not ConvexHullNode.shader:
|
||||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
ConvexHullNode.shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
||||||
self._shader.setUniformValue("u_diffuseColor", self._color)
|
ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color)
|
||||||
self._shader.setUniformValue("u_opacity", 0.6)
|
ConvexHullNode.shader.setUniformValue("u_opacity", 0.6)
|
||||||
|
|
||||||
if self.getParent():
|
if self.getParent():
|
||||||
if self.getMeshData():
|
if self.getMeshData():
|
||||||
renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8)
|
renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8)
|
||||||
if self._convex_hull_head_mesh:
|
if self._convex_hull_head_mesh:
|
||||||
renderer.queueNode(self, shader = self._shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
|
renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _onNodeDecoratorsChanged(self, node):
|
def _onNodeDecoratorsChanged(self, node):
|
||||||
self._color = Color(35, 35, 35, 0.5)
|
|
||||||
|
|
||||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||||
if convex_hull_head:
|
if convex_hull_head:
|
||||||
convex_hull_head_builder = MeshBuilder()
|
convex_hull_head_builder = MeshBuilder()
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,39 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import faulthandler
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
|
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
|
||||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit
|
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Platform import Platform
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
CuraDebugMode = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraDebugMode
|
||||||
|
except ImportError:
|
||||||
|
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
# List of exceptions that should be considered "fatal" and abort the program.
|
# List of exceptions that should be considered "fatal" and abort the program.
|
||||||
# These are primarily some exception types that we simply cannot really recover from
|
# These are primarily some exception types that we simply cannot really recover from
|
||||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
||||||
|
|
@ -21,53 +45,247 @@ fatal_exception_types = [
|
||||||
SystemError,
|
SystemError,
|
||||||
]
|
]
|
||||||
|
|
||||||
def show(exception_type, value, tb):
|
|
||||||
debug_mode = False
|
class CrashHandler:
|
||||||
if QCoreApplication.instance():
|
crash_url = "https://stats.ultimaker.com/api/cura"
|
||||||
debug_mode = QCoreApplication.instance().getCommandLineOption("debug-mode", False)
|
|
||||||
|
def __init__(self, exception_type, value, tb):
|
||||||
|
self.exception_type = exception_type
|
||||||
|
self.value = value
|
||||||
|
self.traceback = tb
|
||||||
|
self.dialog = QDialog()
|
||||||
|
|
||||||
|
# While we create the GUI, the information will be stored for sending afterwards
|
||||||
|
self.data = dict()
|
||||||
|
self.data["time_stamp"] = time.time()
|
||||||
|
|
||||||
Logger.log("c", "An uncaught exception has occurred!")
|
Logger.log("c", "An uncaught exception has occurred!")
|
||||||
for line in traceback.format_exception(exception_type, value, tb):
|
for line in traceback.format_exception(exception_type, value, tb):
|
||||||
for part in line.rstrip("\n").split("\n"):
|
for part in line.rstrip("\n").split("\n"):
|
||||||
Logger.log("c", part)
|
Logger.log("c", part)
|
||||||
|
|
||||||
if not debug_mode and exception_type not in fatal_exception_types:
|
if not CuraDebugMode and exception_type not in fatal_exception_types:
|
||||||
return
|
return
|
||||||
|
|
||||||
application = QCoreApplication.instance()
|
application = QCoreApplication.instance()
|
||||||
if not application:
|
if not application:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
dialog = QDialog()
|
self._createDialog()
|
||||||
dialog.setWindowTitle(catalog.i18nc("@title:window", "Oops!"))
|
|
||||||
|
|
||||||
layout = QVBoxLayout(dialog)
|
## Creates a modal dialog.
|
||||||
|
def _createDialog(self):
|
||||||
|
self.dialog.setMinimumWidth(640)
|
||||||
|
self.dialog.setMinimumHeight(640)
|
||||||
|
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
|
||||||
|
|
||||||
label = QLabel(dialog)
|
layout = QVBoxLayout(self.dialog)
|
||||||
layout.addWidget(label)
|
|
||||||
label.setText(catalog.i18nc("@label", "<p>A fatal exception has occurred that we could not recover from!</p><p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>"))
|
|
||||||
|
|
||||||
textarea = QTextEdit(dialog)
|
layout.addWidget(self._messageWidget())
|
||||||
layout.addWidget(textarea)
|
layout.addWidget(self._informationWidget())
|
||||||
|
layout.addWidget(self._exceptionInfoWidget())
|
||||||
|
layout.addWidget(self._logInfoWidget())
|
||||||
|
layout.addWidget(self._userDescriptionWidget())
|
||||||
|
layout.addWidget(self._buttonsWidget())
|
||||||
|
|
||||||
|
def _messageWidget(self):
|
||||||
|
label = QLabel()
|
||||||
|
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal exception has occurred. Please send us this Crash Report to fix the problem</p></b>
|
||||||
|
<p>Please use the "Send report" button to post a bug report automatically to our servers</p>
|
||||||
|
"""))
|
||||||
|
|
||||||
|
return label
|
||||||
|
|
||||||
|
def _informationWidget(self):
|
||||||
|
group = QGroupBox()
|
||||||
|
group.setTitle(catalog.i18nc("@title:groupbox", "System information"))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
label = QLabel()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
version = Application.getInstance().getVersion()
|
self.cura_version = Application.getInstance().getVersion()
|
||||||
except:
|
except:
|
||||||
version = "Unknown"
|
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
|
||||||
|
|
||||||
trace = "".join(traceback.format_exception(exception_type, value, tb))
|
crash_info = "<b>" + catalog.i18nc("@label Cura version number", "Cura version") + ":</b> " + str(self.cura_version) + "<br/>"
|
||||||
|
crash_info += "<b>" + catalog.i18nc("@label Type of platform", "Platform") + ":</b> " + str(platform.platform()) + "<br/>"
|
||||||
|
crash_info += "<b>" + catalog.i18nc("@label", "Qt version") + ":</b> " + str(QT_VERSION_STR) + "<br/>"
|
||||||
|
crash_info += "<b>" + catalog.i18nc("@label", "PyQt version") + ":</b> " + str(PYQT_VERSION_STR) + "<br/>"
|
||||||
|
crash_info += "<b>" + catalog.i18nc("@label OpenGL version", "OpenGL") + ":</b> " + str(self._getOpenGLInfo()) + "<br/>"
|
||||||
|
label.setText(crash_info)
|
||||||
|
|
||||||
crash_info = "Version: {0}\nPlatform: {1}\nQt: {2}\nPyQt: {3}\n\nException:\n{4}"
|
layout.addWidget(label)
|
||||||
crash_info = crash_info.format(version, platform.platform(), QT_VERSION_STR, PYQT_VERSION_STR, trace)
|
group.setLayout(layout)
|
||||||
|
|
||||||
textarea.setText(crash_info)
|
self.data["cura_version"] = self.cura_version
|
||||||
|
self.data["os"] = {"type": platform.system(), "version": platform.version()}
|
||||||
|
self.data["qt_version"] = QT_VERSION_STR
|
||||||
|
self.data["pyqt_version"] = PYQT_VERSION_STR
|
||||||
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.Close, dialog)
|
return group
|
||||||
layout.addWidget(buttons)
|
|
||||||
buttons.addButton(catalog.i18nc("@action:button", "Open Web Page"), QDialogButtonBox.HelpRole)
|
|
||||||
buttons.rejected.connect(dialog.close)
|
|
||||||
buttons.helpRequested.connect(lambda: webbrowser.open("http://github.com/Ultimaker/Cura/issues"))
|
|
||||||
|
|
||||||
dialog.exec_()
|
def _getOpenGLInfo(self):
|
||||||
sys.exit(1)
|
opengl_instance = OpenGL.getInstance()
|
||||||
|
if not opengl_instance:
|
||||||
|
self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
|
||||||
|
return catalog.i18nc("@label", "not yet initialised<br/>")
|
||||||
|
|
||||||
|
info = "<ul>"
|
||||||
|
info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
|
||||||
|
info += catalog.i18nc("@label OpenGL vendor", "<li>OpenGL Vendor: {vendor}</li>").format(vendor = opengl_instance.getGPUVendorName())
|
||||||
|
info += catalog.i18nc("@label OpenGL renderer", "<li>OpenGL Renderer: {renderer}</li>").format(renderer = opengl_instance.getGPUType())
|
||||||
|
info += "</ul>"
|
||||||
|
|
||||||
|
self.data["opengl"] = {"version": opengl_instance.getOpenGLVersion(), "vendor": opengl_instance.getGPUVendorName(), "type": opengl_instance.getGPUType()}
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
|
def _exceptionInfoWidget(self):
|
||||||
|
group = QGroupBox()
|
||||||
|
group.setTitle(catalog.i18nc("@title:groupbox", "Exception traceback"))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
text_area = QTextEdit()
|
||||||
|
trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback)
|
||||||
|
trace = "".join(trace_dict)
|
||||||
|
text_area.setText(trace)
|
||||||
|
text_area.setReadOnly(True)
|
||||||
|
|
||||||
|
layout.addWidget(text_area)
|
||||||
|
group.setLayout(layout)
|
||||||
|
|
||||||
|
# Parsing all the information to fill the dictionary
|
||||||
|
summary = trace_dict[len(trace_dict)-1].rstrip("\n")
|
||||||
|
module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n")
|
||||||
|
module_split = module[0].split(", ")
|
||||||
|
filepath = module_split[0].split("\"")[1]
|
||||||
|
directory, filename = os.path.split(filepath)
|
||||||
|
line = int(module_split[1].lstrip("line "))
|
||||||
|
function = module_split[2].lstrip("in ")
|
||||||
|
code = module[1].lstrip(" ")
|
||||||
|
|
||||||
|
# Using this workaround for a cross-platform path splitting
|
||||||
|
split_path = []
|
||||||
|
folder_name = ""
|
||||||
|
# Split until reach folder "cura"
|
||||||
|
while folder_name != "cura":
|
||||||
|
directory, folder_name = os.path.split(directory)
|
||||||
|
if not folder_name:
|
||||||
|
break
|
||||||
|
split_path.append(folder_name)
|
||||||
|
|
||||||
|
# Look for plugins. If it's not a plugin, the current cura version is set
|
||||||
|
isPlugin = False
|
||||||
|
module_version = self.cura_version
|
||||||
|
module_name = "Cura"
|
||||||
|
if split_path.__contains__("plugins"):
|
||||||
|
isPlugin = True
|
||||||
|
# Look backwards until plugin.json is found
|
||||||
|
directory, name = os.path.split(filepath)
|
||||||
|
while not os.listdir(directory).__contains__("plugin.json"):
|
||||||
|
directory, name = os.path.split(directory)
|
||||||
|
|
||||||
|
json_metadata_file = os.path.join(directory, "plugin.json")
|
||||||
|
try:
|
||||||
|
with open(json_metadata_file, "r") as f:
|
||||||
|
try:
|
||||||
|
metadata = json.loads(f.read())
|
||||||
|
module_version = metadata["version"]
|
||||||
|
module_name = metadata["name"]
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
# Not throw new exceptions
|
||||||
|
Logger.logException("e", "Failed to parse plugin.json for plugin %s", name)
|
||||||
|
except:
|
||||||
|
# Not throw new exceptions
|
||||||
|
pass
|
||||||
|
|
||||||
|
exception_dict = dict()
|
||||||
|
exception_dict["traceback"] = {"summary": summary, "full_trace": trace}
|
||||||
|
exception_dict["location"] = {"path": filepath, "file": filename, "function": function, "code": code, "line": line,
|
||||||
|
"module_name": module_name, "version": module_version, "is_plugin": isPlugin}
|
||||||
|
self.data["exception"] = exception_dict
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _logInfoWidget(self):
|
||||||
|
group = QGroupBox()
|
||||||
|
group.setTitle(catalog.i18nc("@title:groupbox", "Logs"))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
text_area = QTextEdit()
|
||||||
|
tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
|
||||||
|
os.close(tmp_file_fd)
|
||||||
|
with open(tmp_file_path, "w") as f:
|
||||||
|
faulthandler.dump_traceback(f, all_threads=True)
|
||||||
|
with open(tmp_file_path, "r") as f:
|
||||||
|
logdata = f.read()
|
||||||
|
|
||||||
|
text_area.setText(logdata)
|
||||||
|
text_area.setReadOnly(True)
|
||||||
|
|
||||||
|
layout.addWidget(text_area)
|
||||||
|
group.setLayout(layout)
|
||||||
|
|
||||||
|
self.data["log"] = logdata
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _userDescriptionWidget(self):
|
||||||
|
group = QGroupBox()
|
||||||
|
group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# When sending the report, the user comments will be collected
|
||||||
|
self.user_description_text_area = QTextEdit()
|
||||||
|
self.user_description_text_area.setFocus(True)
|
||||||
|
|
||||||
|
layout.addWidget(self.user_description_text_area)
|
||||||
|
group.setLayout(layout)
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _buttonsWidget(self):
|
||||||
|
buttons = QDialogButtonBox()
|
||||||
|
buttons.addButton(QDialogButtonBox.Close)
|
||||||
|
buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole)
|
||||||
|
buttons.rejected.connect(self.dialog.close)
|
||||||
|
buttons.accepted.connect(self._sendCrashReport)
|
||||||
|
|
||||||
|
return buttons
|
||||||
|
|
||||||
|
def _sendCrashReport(self):
|
||||||
|
# Before sending data, the user comments are stored
|
||||||
|
self.data["user_info"] = self.user_description_text_area.toPlainText()
|
||||||
|
|
||||||
|
# Convert data to bytes
|
||||||
|
binary_data = json.dumps(self.data).encode("utf-8")
|
||||||
|
|
||||||
|
# Submit data
|
||||||
|
kwoptions = {"data": binary_data, "timeout": 5}
|
||||||
|
|
||||||
|
if Platform.isOSX():
|
||||||
|
kwoptions["context"] = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = urllib.request.urlopen(self.crash_url, **kwoptions)
|
||||||
|
Logger.log("i", "Sent crash report info.")
|
||||||
|
f.close()
|
||||||
|
except urllib.error.HTTPError:
|
||||||
|
Logger.logException("e", "An HTTP error occurred while trying to send crash report")
|
||||||
|
except Exception: # We don't want any exception to cause problems
|
||||||
|
Logger.logException("e", "An exception occurred while trying to send crash report")
|
||||||
|
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
# must run the GUI code on the Qt thread, otherwise the widgets on the dialog won't react correctly.
|
||||||
|
Application.getInstance().callLater(self._show)
|
||||||
|
|
||||||
|
def _show(self):
|
||||||
|
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
|
||||||
|
if self.dialog:
|
||||||
|
self.dialog.exec_()
|
||||||
|
os._exit(1)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, QUrl
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||||
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
|
||||||
|
from cura.SetParentOperation import SetParentOperation
|
||||||
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
|
from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
class CuraActions(QObject):
|
class CuraActions(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
|
|
@ -22,5 +36,93 @@ class CuraActions(QObject):
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
||||||
Application.getInstance().functionEvent(event)
|
Application.getInstance().functionEvent(event)
|
||||||
|
|
||||||
|
## Reset camera position and direction to default
|
||||||
|
@pyqtSlot()
|
||||||
|
def homeCamera(self) -> None:
|
||||||
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
camera = scene.getActiveCamera()
|
||||||
|
camera.setPosition(Vector(-80, 250, 700))
|
||||||
|
camera.setPerspective(True)
|
||||||
|
camera.lookAt(Vector(0, 0, 0))
|
||||||
|
|
||||||
|
## Center all objects in the selection
|
||||||
|
@pyqtSlot()
|
||||||
|
def centerSelection(self) -> None:
|
||||||
|
operation = GroupedOperation()
|
||||||
|
for node in Selection.getAllSelectedObjects():
|
||||||
|
current_node = node
|
||||||
|
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||||
|
current_node = current_node.getParent()
|
||||||
|
|
||||||
|
center_operation = SetTransformOperation(current_node, Vector())
|
||||||
|
operation.addOperation(center_operation)
|
||||||
|
operation.push()
|
||||||
|
|
||||||
|
## Multiply all objects in the selection
|
||||||
|
#
|
||||||
|
# \param count The number of times to multiply the selection.
|
||||||
|
@pyqtSlot(int)
|
||||||
|
def multiplySelection(self, count: int) -> None:
|
||||||
|
job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, 8)
|
||||||
|
job.start()
|
||||||
|
|
||||||
|
## Delete all selected objects.
|
||||||
|
@pyqtSlot()
|
||||||
|
def deleteSelection(self) -> None:
|
||||||
|
if not Application.getInstance().getController().getToolsEnabled():
|
||||||
|
return
|
||||||
|
|
||||||
|
removed_group_nodes = []
|
||||||
|
op = GroupedOperation()
|
||||||
|
nodes = Selection.getAllSelectedObjects()
|
||||||
|
for node in nodes:
|
||||||
|
op.addOperation(RemoveSceneNodeOperation(node))
|
||||||
|
group_node = node.getParent()
|
||||||
|
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
|
||||||
|
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
|
||||||
|
if len(remaining_nodes_in_group) == 1:
|
||||||
|
removed_group_nodes.append(group_node)
|
||||||
|
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
|
||||||
|
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||||
|
op.push()
|
||||||
|
|
||||||
|
## Set the extruder that should be used to print the selection.
|
||||||
|
#
|
||||||
|
# \param extruder_id The ID of the extruder stack to use for the selected objects.
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setExtruderForSelection(self, extruder_id: str) -> None:
|
||||||
|
operation = GroupedOperation()
|
||||||
|
|
||||||
|
nodes_to_change = []
|
||||||
|
for node in Selection.getAllSelectedObjects():
|
||||||
|
# Do not change any nodes that already have the right extruder set.
|
||||||
|
if node.callDecoration("getActiveExtruder") == extruder_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If the node is a group, apply the active extruder to all children of the group.
|
||||||
|
if node.callDecoration("isGroup"):
|
||||||
|
for grouped_node in BreadthFirstIterator(node):
|
||||||
|
if grouped_node.callDecoration("getActiveExtruder") == extruder_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if grouped_node.callDecoration("isGroup"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
nodes_to_change.append(grouped_node)
|
||||||
|
continue
|
||||||
|
|
||||||
|
nodes_to_change.append(node)
|
||||||
|
|
||||||
|
if not nodes_to_change:
|
||||||
|
# If there are no changes to make, we still need to reset the selected extruders.
|
||||||
|
# This is a workaround for checked menu items being deselected while still being
|
||||||
|
# selected.
|
||||||
|
ExtruderManager.getInstance().resetSelectedObjectExtruders()
|
||||||
|
return
|
||||||
|
|
||||||
|
for node in nodes_to_change:
|
||||||
|
operation.addOperation(SetObjectExtruderOperation(node, extruder_id))
|
||||||
|
operation.push()
|
||||||
|
|
||||||
def _openUrl(self, url):
|
def _openUrl(self, url):
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
769
cura/CuraApplication.py
Normal file → Executable file
769
cura/CuraApplication.py
Normal file → Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -1,38 +1,106 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication
|
from PyQt5.QtCore import Qt, QCoreApplication, QTimer
|
||||||
from PyQt5.QtGui import QPixmap, QColor, QFont, QFontMetrics
|
from PyQt5.QtGui import QPixmap, QColor, QFont, QPen, QPainter
|
||||||
from PyQt5.QtWidgets import QSplashScreen
|
from PyQt5.QtWidgets import QSplashScreen
|
||||||
|
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
|
|
||||||
class CuraSplashScreen(QSplashScreen):
|
class CuraSplashScreen(QSplashScreen):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._scale = round(QFontMetrics(QCoreApplication.instance().font()).ascent() / 12)
|
self._scale = 0.7
|
||||||
|
|
||||||
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png"))
|
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png"))
|
||||||
self.setPixmap(splash_image.scaled(splash_image.size() * self._scale))
|
self.setPixmap(splash_image)
|
||||||
|
|
||||||
|
self._current_message = ""
|
||||||
|
|
||||||
|
self._loading_image_rotation_angle = 0
|
||||||
|
|
||||||
|
self._to_stop = False
|
||||||
|
self._change_timer = QTimer()
|
||||||
|
self._change_timer.setInterval(50)
|
||||||
|
self._change_timer.setSingleShot(False)
|
||||||
|
self._change_timer.timeout.connect(self.updateLoadingImage)
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
super().show()
|
||||||
|
self._change_timer.start()
|
||||||
|
|
||||||
|
def updateLoadingImage(self):
|
||||||
|
if self._to_stop:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._loading_image_rotation_angle -= 10
|
||||||
|
self.repaint()
|
||||||
|
|
||||||
|
# Override the mousePressEvent so the splashscreen doesn't disappear when clicked
|
||||||
|
def mousePressEvent(self, mouse_event):
|
||||||
|
pass
|
||||||
|
|
||||||
def drawContents(self, painter):
|
def drawContents(self, painter):
|
||||||
|
if self._to_stop:
|
||||||
|
return
|
||||||
|
|
||||||
painter.save()
|
painter.save()
|
||||||
painter.setPen(QColor(0, 0, 0, 255))
|
painter.setPen(QColor(255, 255, 255, 255))
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing, True)
|
||||||
|
|
||||||
version = Application.getInstance().getVersion().split("-")
|
version = Application.getInstance().getVersion().split("-")
|
||||||
buildtype = Application.getInstance().getBuildType()
|
buildtype = Application.getInstance().getBuildType()
|
||||||
if buildtype:
|
if buildtype:
|
||||||
version[0] += " (%s)" %(buildtype)
|
version[0] += " (%s)" % buildtype
|
||||||
|
|
||||||
|
# draw version text
|
||||||
font = QFont() # Using system-default font here
|
font = QFont() # Using system-default font here
|
||||||
font.setPointSize(20)
|
font.setPixelSize(37)
|
||||||
painter.setFont(font)
|
painter.setFont(font)
|
||||||
painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])
|
painter.drawText(215, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
|
||||||
if len(version) > 1:
|
if len(version) > 1:
|
||||||
font.setPointSize(12)
|
font.setPixelSize(16)
|
||||||
painter.setFont(font)
|
painter.setFont(font)
|
||||||
painter.drawText(0, 0, 330 * self._scale, 255 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[1])
|
painter.setPen(QColor(200, 200, 200, 255))
|
||||||
|
painter.drawText(247, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1])
|
||||||
|
painter.setPen(QColor(255, 255, 255, 255))
|
||||||
|
|
||||||
|
# draw the loading image
|
||||||
|
pen = QPen()
|
||||||
|
pen.setWidth(6 * self._scale)
|
||||||
|
pen.setColor(QColor(32, 166, 219, 255))
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16)
|
||||||
|
|
||||||
|
# draw message text
|
||||||
|
if self._current_message:
|
||||||
|
font = QFont() # Using system-default font here
|
||||||
|
font.setPixelSize(13)
|
||||||
|
pen = QPen()
|
||||||
|
pen.setColor(QColor(255, 255, 255, 255))
|
||||||
|
painter.setPen(pen)
|
||||||
|
painter.setFont(font)
|
||||||
|
painter.drawText(100, 128, 170, 64,
|
||||||
|
Qt.AlignLeft | Qt.AlignVCenter | Qt.TextWordWrap,
|
||||||
|
self._current_message)
|
||||||
|
|
||||||
painter.restore()
|
painter.restore()
|
||||||
super().drawContents(painter)
|
super().drawContents(painter)
|
||||||
|
|
||||||
|
def showMessage(self, message, *args, **kwargs):
|
||||||
|
if self._to_stop:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._current_message = message
|
||||||
|
self.messageChanged.emit(message)
|
||||||
|
QCoreApplication.flush()
|
||||||
|
self.repaint()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
# set stop flags
|
||||||
|
self._to_stop = True
|
||||||
|
self._change_timer.stop()
|
||||||
|
super().close()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
CuraVersion = "@CURA_VERSION@"
|
CuraVersion = "@CURA_VERSION@"
|
||||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||||
|
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||||
|
|
|
||||||
13
cura/GCodeListDecorator.py
Normal file
13
cura/GCodeListDecorator.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
class GCodeListDecorator(SceneNodeDecorator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._gcode_list = []
|
||||||
|
|
||||||
|
def getGCodeList(self):
|
||||||
|
return self._gcode_list
|
||||||
|
|
||||||
|
def setGCodeList(self, list):
|
||||||
|
self._gcode_list = list
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
from .LayerPolygon import LayerPolygon
|
|
||||||
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
class Layer:
|
class Layer:
|
||||||
def __init__(self, layer_id):
|
def __init__(self, layer_id):
|
||||||
self._id = layer_id
|
self._id = layer_id
|
||||||
|
|
@ -49,12 +47,12 @@ class Layer:
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def build(self, vertex_offset, index_offset, vertices, colors, indices):
|
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
|
||||||
result_vertex_offset = vertex_offset
|
result_vertex_offset = vertex_offset
|
||||||
result_index_offset = index_offset
|
result_index_offset = index_offset
|
||||||
self._element_count = 0
|
self._element_count = 0
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
polygon.build(result_vertex_offset, result_index_offset, vertices, colors, indices)
|
polygon.build(result_vertex_offset, result_index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
|
||||||
result_vertex_offset += polygon.lineMeshVertexCount()
|
result_vertex_offset += polygon.lineMeshVertexCount()
|
||||||
result_index_offset += polygon.lineMeshElementCount()
|
result_index_offset += polygon.lineMeshElementCount()
|
||||||
self._element_count += polygon.elementCount
|
self._element_count += polygon.elementCount
|
||||||
|
|
@ -81,7 +79,6 @@ class Layer:
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
line_count += polygon.jumpCount
|
line_count += polygon.jumpCount
|
||||||
|
|
||||||
|
|
||||||
# Reserve the neccesary space for the data upfront
|
# Reserve the neccesary space for the data upfront
|
||||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||||
|
|
||||||
|
|
@ -106,13 +103,14 @@ class Layer:
|
||||||
# Scale all normals by the line width of the current line so we can easily offset.
|
# Scale all normals by the line width of the current line so we can easily offset.
|
||||||
normals *= (polygon.lineWidths[index_mask.ravel()] / 2)
|
normals *= (polygon.lineWidths[index_mask.ravel()] / 2)
|
||||||
|
|
||||||
# Create 4 points to draw each line segment, points +- normals results in 2 points each. Reshape to one point per line
|
# Create 4 points to draw each line segment, points +- normals results in 2 points each.
|
||||||
|
# After this we reshape to one point per line.
|
||||||
f_points = numpy.concatenate((points-normals, points+normals), 1).reshape((-1, 3))
|
f_points = numpy.concatenate((points-normals, points+normals), 1).reshape((-1, 3))
|
||||||
|
|
||||||
# __index_pattern defines which points to use to draw the two faces for each lines egment, the following linesegment is offset by 4
|
# __index_pattern defines which points to use to draw the two faces for each lines egment, the following linesegment is offset by 4
|
||||||
f_indices = ( self.__index_pattern + numpy.arange(0, 4 * len(normals), 4, dtype=numpy.int32).reshape((-1, 1)) ).reshape((-1, 3))
|
f_indices = ( self.__index_pattern + numpy.arange(0, 4 * len(normals), 4, dtype=numpy.int32).reshape((-1, 1)) ).reshape((-1, 3))
|
||||||
f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0)
|
f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0)
|
||||||
|
|
||||||
builder.addFacesWithColor(f_points, f_indices, f_colors)
|
builder.addFacesWithColor(f_points, f_indices, f_colors)
|
||||||
|
|
||||||
|
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Mesh.MeshData import MeshData
|
from UM.Mesh.MeshData import MeshData
|
||||||
|
|
||||||
|
|
||||||
## Class to holds the layer mesh and information about the layers.
|
## Class to holds the layer mesh and information about the layers.
|
||||||
# Immutable, use LayerDataBuilder to create one of these.
|
# Immutable, use LayerDataBuilder to create one of these.
|
||||||
class LayerData(MeshData):
|
class LayerData(MeshData):
|
||||||
def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None,
|
def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None,
|
||||||
center_position = None, layers=None, element_counts=None):
|
center_position = None, layers=None, element_counts=None, attributes=None):
|
||||||
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,
|
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,
|
||||||
file_name=file_name, center_position=center_position)
|
file_name=file_name, center_position=center_position, attributes=attributes)
|
||||||
self._layers = layers
|
self._layers = layers
|
||||||
self._element_counts = element_counts
|
self._element_counts = element_counts
|
||||||
|
|
||||||
|
|
|
||||||
61
cura/LayerDataBuilder.py
Normal file → Executable file
61
cura/LayerDataBuilder.py
Normal file → Executable file
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from .Layer import Layer
|
from .Layer import Layer
|
||||||
from .LayerPolygon import LayerPolygon
|
from .LayerPolygon import LayerPolygon
|
||||||
|
|
@ -8,6 +8,7 @@ from .LayerData import LayerData
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
## Builder class for constructing a LayerData object
|
## Builder class for constructing a LayerData object
|
||||||
class LayerDataBuilder(MeshBuilder):
|
class LayerDataBuilder(MeshBuilder):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -19,11 +20,11 @@ class LayerDataBuilder(MeshBuilder):
|
||||||
if layer not in self._layers:
|
if layer not in self._layers:
|
||||||
self._layers[layer] = Layer(layer)
|
self._layers[layer] = Layer(layer)
|
||||||
|
|
||||||
def addPolygon(self, layer, polygon_type, data, line_width):
|
def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate):
|
||||||
if layer not in self._layers:
|
if layer not in self._layers:
|
||||||
self.addLayer(layer)
|
self.addLayer(layer)
|
||||||
|
|
||||||
p = LayerPolygon(self, polygon_type, data, line_width)
|
p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate)
|
||||||
self._layers[layer].polygons.append(p)
|
self._layers[layer].polygons.append(p)
|
||||||
|
|
||||||
def getLayer(self, layer):
|
def getLayer(self, layer):
|
||||||
|
|
@ -48,7 +49,11 @@ class LayerDataBuilder(MeshBuilder):
|
||||||
|
|
||||||
self._layers[layer].setThickness(thickness)
|
self._layers[layer].setThickness(thickness)
|
||||||
|
|
||||||
def build(self):
|
## Return the layer data as LayerData.
|
||||||
|
#
|
||||||
|
# \param material_color_map: [r, g, b, a] for each extruder row.
|
||||||
|
# \param line_type_brightness: compatibility layer view uses line type brightness of 0.5
|
||||||
|
def build(self, material_color_map, line_type_brightness = 1.0):
|
||||||
vertex_count = 0
|
vertex_count = 0
|
||||||
index_count = 0
|
index_count = 0
|
||||||
for layer, data in self._layers.items():
|
for layer, data in self._layers.items():
|
||||||
|
|
@ -56,20 +61,62 @@ class LayerDataBuilder(MeshBuilder):
|
||||||
index_count += data.lineMeshElementCount()
|
index_count += data.lineMeshElementCount()
|
||||||
|
|
||||||
vertices = numpy.empty((vertex_count, 3), numpy.float32)
|
vertices = numpy.empty((vertex_count, 3), numpy.float32)
|
||||||
|
line_dimensions = numpy.empty((vertex_count, 2), numpy.float32)
|
||||||
colors = numpy.empty((vertex_count, 4), numpy.float32)
|
colors = numpy.empty((vertex_count, 4), numpy.float32)
|
||||||
indices = numpy.empty((index_count, 2), numpy.int32)
|
indices = numpy.empty((index_count, 2), numpy.int32)
|
||||||
|
feedrates = numpy.empty((vertex_count), numpy.float32)
|
||||||
|
extruders = numpy.empty((vertex_count), numpy.float32)
|
||||||
|
line_types = numpy.empty((vertex_count), numpy.float32)
|
||||||
|
|
||||||
vertex_offset = 0
|
vertex_offset = 0
|
||||||
index_offset = 0
|
index_offset = 0
|
||||||
for layer, data in self._layers.items():
|
for layer, data in sorted(self._layers.items()):
|
||||||
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, indices)
|
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
|
||||||
self._element_counts[layer] = data.elementCount
|
self._element_counts[layer] = data.elementCount
|
||||||
|
|
||||||
self.addVertices(vertices)
|
self.addVertices(vertices)
|
||||||
|
colors[:, 0:3] *= line_type_brightness
|
||||||
self.addColors(colors)
|
self.addColors(colors)
|
||||||
self.addIndices(indices.flatten())
|
self.addIndices(indices.flatten())
|
||||||
|
|
||||||
|
# Note: we're using numpy indexing here.
|
||||||
|
# See also: https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html
|
||||||
|
material_colors = numpy.zeros((line_dimensions.shape[0], 4), dtype=numpy.float32)
|
||||||
|
for extruder_nr in range(material_color_map.shape[0]):
|
||||||
|
material_colors[extruders == extruder_nr] = material_color_map[extruder_nr]
|
||||||
|
# Set material_colors with indices where line_types (also numpy array) == MoveCombingType
|
||||||
|
material_colors[line_types == LayerPolygon.MoveCombingType] = colors[line_types == LayerPolygon.MoveCombingType]
|
||||||
|
material_colors[line_types == LayerPolygon.MoveRetractionType] = colors[line_types == LayerPolygon.MoveRetractionType]
|
||||||
|
|
||||||
|
attributes = {
|
||||||
|
"line_dimensions": {
|
||||||
|
"value": line_dimensions,
|
||||||
|
"opengl_name": "a_line_dim",
|
||||||
|
"opengl_type": "vector2f"
|
||||||
|
},
|
||||||
|
"extruders": {
|
||||||
|
"value": extruders,
|
||||||
|
"opengl_name": "a_extruder",
|
||||||
|
"opengl_type": "float" # Strangely enough, the type has to be float while it is actually an int.
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
"value": material_colors,
|
||||||
|
"opengl_name": "a_material_color",
|
||||||
|
"opengl_type": "vector4f"
|
||||||
|
},
|
||||||
|
"line_types": {
|
||||||
|
"value": line_types,
|
||||||
|
"opengl_name": "a_line_type",
|
||||||
|
"opengl_type": "float"
|
||||||
|
},
|
||||||
|
"feedrates": {
|
||||||
|
"value": feedrates,
|
||||||
|
"opengl_name": "a_feedrate",
|
||||||
|
"opengl_type": "float"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return LayerData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(),
|
return LayerData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(),
|
||||||
colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(),
|
colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(),
|
||||||
center_position=self.getCenterPosition(), layers=self._layers,
|
center_position=self.getCenterPosition(), layers=self._layers,
|
||||||
element_counts=self._element_counts)
|
element_counts=self._element_counts, attributes=attributes)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
from UM.Math.Color import Color
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from typing import Any
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -15,15 +18,27 @@ class LayerPolygon:
|
||||||
MoveCombingType = 8
|
MoveCombingType = 8
|
||||||
MoveRetractionType = 9
|
MoveRetractionType = 9
|
||||||
SupportInterfaceType = 10
|
SupportInterfaceType = 10
|
||||||
|
__number_of_types = 11
|
||||||
|
|
||||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType)
|
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
||||||
|
|
||||||
def __init__(self, mesh, extruder, line_types, data, line_widths):
|
## LayerPolygon, used in ProcessSlicedLayersJob
|
||||||
self._mesh = mesh
|
# \param extruder
|
||||||
|
# \param line_types array with line_types
|
||||||
|
# \param data new_points
|
||||||
|
# \param line_widths array with line widths
|
||||||
|
# \param line_thicknesses: array with type as index and thickness as value
|
||||||
|
# \param line_feedrates array with line feedrates
|
||||||
|
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates):
|
||||||
self._extruder = extruder
|
self._extruder = extruder
|
||||||
self._types = line_types
|
self._types = line_types
|
||||||
|
for i in range(len(self._types)):
|
||||||
|
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine.
|
||||||
|
self._types[i] = self.NoneType
|
||||||
self._data = data
|
self._data = data
|
||||||
self._line_widths = line_widths
|
self._line_widths = line_widths
|
||||||
|
self._line_thicknesses = line_thicknesses
|
||||||
|
self._line_feedrates = line_feedrates
|
||||||
|
|
||||||
self._vertex_begin = 0
|
self._vertex_begin = 0
|
||||||
self._vertex_end = 0
|
self._vertex_end = 0
|
||||||
|
|
@ -37,7 +52,7 @@ class LayerPolygon:
|
||||||
|
|
||||||
# Buffering the colors shouldn't be necessary as it is not
|
# Buffering the colors shouldn't be necessary as it is not
|
||||||
# re-used and can save alot of memory usage.
|
# re-used and can save alot of memory usage.
|
||||||
self._color_map = self.__color_map * [1, 1, 1, self._extruder] # The alpha component is used to store the extruder nr
|
self._color_map = LayerPolygon.getColorMap()
|
||||||
self._colors = self._color_map[self._types]
|
self._colors = self._color_map[self._types]
|
||||||
|
|
||||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||||
|
|
@ -49,7 +64,7 @@ class LayerPolygon:
|
||||||
|
|
||||||
def buildCache(self):
|
def buildCache(self):
|
||||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||||
self._build_cache_line_mesh_mask = numpy.logical_not(numpy.logical_or(self._jump_mask, self._types == LayerPolygon.InfillType ))
|
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
|
||||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||||
self._index_begin = 0
|
self._index_begin = 0
|
||||||
self._index_end = mesh_line_count
|
self._index_end = mesh_line_count
|
||||||
|
|
@ -63,9 +78,20 @@ class LayerPolygon:
|
||||||
self._vertex_begin = 0
|
self._vertex_begin = 0
|
||||||
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
||||||
|
|
||||||
|
## Set all the arrays provided by the function caller, representing the LayerPolygon
|
||||||
def build(self, vertex_offset, index_offset, vertices, colors, indices):
|
# The arrays are either by vertex or by indices.
|
||||||
if (self._build_cache_line_mesh_mask is None) or (self._build_cache_needed_points is None ):
|
#
|
||||||
|
# \param vertex_offset : determines where to start and end filling the arrays
|
||||||
|
# \param index_offset : determines where to start and end filling the arrays
|
||||||
|
# \param vertices : vertex numpy array to be filled
|
||||||
|
# \param colors : vertex numpy array to be filled
|
||||||
|
# \param line_dimensions : vertex numpy array to be filled
|
||||||
|
# \param feedrates : vertex numpy array to be filled
|
||||||
|
# \param extruders : vertex numpy array to be filled
|
||||||
|
# \param line_types : vertex numpy array to be filled
|
||||||
|
# \param indices : index numpy array to be filled
|
||||||
|
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices):
|
||||||
|
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
|
||||||
self.buildCache()
|
self.buildCache()
|
||||||
|
|
||||||
line_mesh_mask = self._build_cache_line_mesh_mask
|
line_mesh_mask = self._build_cache_line_mesh_mask
|
||||||
|
|
@ -82,9 +108,21 @@ class LayerPolygon:
|
||||||
|
|
||||||
# Points are picked based on the index list to get the vertices needed.
|
# Points are picked based on the index list to get the vertices needed.
|
||||||
vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
|
vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :]
|
||||||
|
|
||||||
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
|
# Create an array with colors for each vertex and remove the color data for the points that has been thrown away.
|
||||||
colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
|
colors[self._vertex_begin:self._vertex_end, :] = numpy.tile(self._colors, (1, 2)).reshape((-1, 4))[needed_points_list.ravel()]
|
||||||
colors[self._vertex_begin:self._vertex_end, :] *= numpy.array([[0.5, 0.5, 0.5, 1.0]], numpy.float32)
|
|
||||||
|
# Create an array with line widths and thicknesses for each vertex.
|
||||||
|
line_dimensions[self._vertex_begin:self._vertex_end, 0] = numpy.tile(self._line_widths, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
|
||||||
|
line_dimensions[self._vertex_begin:self._vertex_end, 1] = numpy.tile(self._line_thicknesses, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
|
||||||
|
|
||||||
|
# Create an array with feedrates for each line
|
||||||
|
feedrates[self._vertex_begin:self._vertex_end] = numpy.tile(self._line_feedrates, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
|
||||||
|
|
||||||
|
extruders[self._vertex_begin:self._vertex_end] = self._extruder
|
||||||
|
|
||||||
|
# Convert type per vertex to type per line
|
||||||
|
line_types[self._vertex_begin:self._vertex_end] = numpy.tile(self._types, (1, 2)).reshape((-1, 1))[needed_points_list.ravel()][:, 0]
|
||||||
|
|
||||||
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
|
# The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset.
|
||||||
self._index_begin += index_offset
|
self._index_begin += index_offset
|
||||||
|
|
@ -135,6 +173,14 @@ class LayerPolygon:
|
||||||
def lineWidths(self):
|
def lineWidths(self):
|
||||||
return self._line_widths
|
return self._line_widths
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lineThicknesses(self):
|
||||||
|
return self._line_thicknesses
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lineFeedrates(self):
|
||||||
|
return self._line_feedrates
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def jumpMask(self):
|
def jumpMask(self):
|
||||||
return self._jump_mask
|
return self._jump_mask
|
||||||
|
|
@ -172,17 +218,25 @@ class LayerPolygon:
|
||||||
|
|
||||||
return normals
|
return normals
|
||||||
|
|
||||||
# Should be generated in better way, not hardcoded.
|
__color_map = None # type: numpy.ndarray[Any]
|
||||||
__color_map = numpy.array([
|
|
||||||
[1.0, 1.0, 1.0, 1.0], # NoneType
|
## Gets the instance of the VersionUpgradeManager, or creates one.
|
||||||
[1.0, 0.0, 0.0, 1.0], # Inset0Type
|
@classmethod
|
||||||
[0.0, 1.0, 0.0, 1.0], # InsetXType
|
def getColorMap(cls):
|
||||||
[1.0, 1.0, 0.0, 1.0], # SkinType
|
if cls.__color_map is None:
|
||||||
[0.0, 1.0, 1.0, 1.0], # SupportType
|
theme = Application.getInstance().getTheme()
|
||||||
[0.0, 1.0, 1.0, 1.0], # SkirtType
|
cls.__color_map = numpy.array([
|
||||||
[1.0, 0.75, 0.0, 1.0], # InfillType
|
theme.getColor("layerview_none").getRgbF(), # NoneType
|
||||||
[0.0, 1.0, 1.0, 1.0], # SupportInfillType
|
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
|
||||||
[0.0, 0.0, 1.0, 1.0], # MoveCombingType
|
theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
|
||||||
[0.5, 0.5, 1.0, 1.0], # MoveRetractionType
|
theme.getColor("layerview_skin").getRgbF(), # SkinType
|
||||||
[0.25, 0.75, 1.0, 1.0] # SupportInterfaceType
|
theme.getColor("layerview_support").getRgbF(), # SupportType
|
||||||
|
theme.getColor("layerview_skirt").getRgbF(), # SkirtType
|
||||||
|
theme.getColor("layerview_infill").getRgbF(), # InfillType
|
||||||
|
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
|
||||||
|
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||||
|
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||||
|
theme.getColor("layerview_support_interface").getRgbF() # SupportInterfaceType
|
||||||
])
|
])
|
||||||
|
|
||||||
|
return cls.__color_map
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
|
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
|
||||||
|
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
|
||||||
|
# updating the firmware, connecting with remote devices or doing bed leveling. A machine action can also have a
|
||||||
|
# qml, which should contain a "Cura.MachineAction" item. When activated, the item will be displayed in a dialog
|
||||||
|
# and this object will be added as "manager" (so all pyqtSlot() functions can be called by calling manager.func())
|
||||||
class MachineAction(QObject, PluginObject):
|
class MachineAction(QObject, PluginObject):
|
||||||
|
|
||||||
|
## Create a new Machine action.
|
||||||
|
# \param key unique key of the machine action
|
||||||
|
# \param label Human readable label used to identify the machine action.
|
||||||
def __init__(self, key, label = ""):
|
def __init__(self, key, label = ""):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._key = key
|
self._key = key
|
||||||
self._label = label
|
self._label = label
|
||||||
self._qml_url = ""
|
self._qml_url = ""
|
||||||
|
|
||||||
self._component = None
|
|
||||||
self._context = None
|
|
||||||
self._view = None
|
self._view = None
|
||||||
self._finished = False
|
self._finished = False
|
||||||
|
|
||||||
|
|
@ -44,7 +47,6 @@ class MachineAction(QObject, PluginObject):
|
||||||
# /sa _reset
|
# /sa _reset
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._component = None
|
|
||||||
self._finished = False
|
self._finished = False
|
||||||
self._reset()
|
self._reset()
|
||||||
|
|
||||||
|
|
@ -63,17 +65,13 @@ class MachineAction(QObject, PluginObject):
|
||||||
def finished(self):
|
def finished(self):
|
||||||
return self._finished
|
return self._finished
|
||||||
|
|
||||||
|
## Protected helper to create a view object based on provided QML.
|
||||||
def _createViewFromQML(self):
|
def _createViewFromQML(self):
|
||||||
path = QUrl.fromLocalFile(
|
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
|
||||||
os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url))
|
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
self._component = QQmlComponent(Application.getInstance()._engine, path)
|
|
||||||
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
|
|
||||||
self._context.setContextProperty("manager", self)
|
|
||||||
self._view = self._component.create(self._context)
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
@pyqtProperty(QObject, constant = True)
|
||||||
def displayItem(self):
|
def displayItem(self):
|
||||||
if not self._component:
|
if not self._view:
|
||||||
self._createViewFromQML()
|
self._createViewFromQML()
|
||||||
|
|
||||||
return self._view
|
return self._view
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
||||||
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
## Raised when trying to add an unknown machine action as a required action
|
## Raised when trying to add an unknown machine action as a required action
|
||||||
class UnknownMachineActionError(Exception):
|
class UnknownMachineActionError(Exception):
|
||||||
|
|
@ -104,7 +105,7 @@ class MachineActionManager(QObject):
|
||||||
if definition_id in self._supported_actions:
|
if definition_id in self._supported_actions:
|
||||||
return list(self._supported_actions[definition_id])
|
return list(self._supported_actions[definition_id])
|
||||||
else:
|
else:
|
||||||
return set()
|
return list()
|
||||||
|
|
||||||
## Get all actions required by given machine
|
## Get all actions required by given machine
|
||||||
# \param definition_id The ID of the definition you want the required actions of
|
# \param definition_id The ID of the definition you want the required actions of
|
||||||
|
|
|
||||||
84
cura/MultiplyObjectsJob.py
Normal file
84
cura/MultiplyObjectsJob.py
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
from UM.Operations.TranslateOperation import TranslateOperation
|
||||||
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
from cura.ZOffsetDecorator import ZOffsetDecorator
|
||||||
|
from cura.Arrange import Arrange
|
||||||
|
from cura.ShapeArray import ShapeArray
|
||||||
|
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
|
|
||||||
|
|
||||||
|
class MultiplyObjectsJob(Job):
|
||||||
|
def __init__(self, objects, count, min_offset = 8):
|
||||||
|
super().__init__()
|
||||||
|
self._objects = objects
|
||||||
|
self._count = count
|
||||||
|
self._min_offset = min_offset
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
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 Object"))
|
||||||
|
status_message.show()
|
||||||
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
total_progress = len(self._objects) * self._count
|
||||||
|
current_progress = 0
|
||||||
|
|
||||||
|
root = scene.getRoot()
|
||||||
|
arranger = Arrange.create(scene_root=root)
|
||||||
|
nodes = []
|
||||||
|
for node in self._objects:
|
||||||
|
# If object is part of a group, multiply group
|
||||||
|
current_node = node
|
||||||
|
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||||
|
current_node = current_node.getParent()
|
||||||
|
|
||||||
|
node_too_big = False
|
||||||
|
if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300:
|
||||||
|
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset)
|
||||||
|
else:
|
||||||
|
node_too_big = True
|
||||||
|
|
||||||
|
found_solution_for_all = True
|
||||||
|
for i in range(self._count):
|
||||||
|
# We do place the nodes one by one, as we want to yield in between.
|
||||||
|
if not node_too_big:
|
||||||
|
node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr)
|
||||||
|
if node_too_big or not solution_found:
|
||||||
|
found_solution_for_all = False
|
||||||
|
new_location = node.getPosition()
|
||||||
|
new_location = new_location.set(z = 100 - i * 20)
|
||||||
|
node.setPosition(new_location)
|
||||||
|
|
||||||
|
nodes.append(node)
|
||||||
|
current_progress += 1
|
||||||
|
status_message.setProgress((current_progress / total_progress) * 100)
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
|
if nodes:
|
||||||
|
op = GroupedOperation()
|
||||||
|
for new_node in nodes:
|
||||||
|
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||||
|
op.push()
|
||||||
|
status_message.hide()
|
||||||
|
|
||||||
|
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.show()
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Scene.Iterator import Iterator
|
from UM.Scene.Iterator import Iterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
|
||||||
55
cura/PlatformPhysics.py
Normal file → Executable file
55
cura/PlatformPhysics.py
Normal file → Executable file
|
|
@ -1,12 +1,12 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 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.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ class PlatformPhysics:
|
||||||
def _onSceneChanged(self, source):
|
def _onSceneChanged(self, source):
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
def _onChangeTimerFinished(self):
|
def _onChangeTimerFinished(self, was_triggered_by_tool=False):
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -51,10 +51,13 @@ class PlatformPhysics:
|
||||||
# same direction.
|
# same direction.
|
||||||
transformed_nodes = []
|
transformed_nodes = []
|
||||||
|
|
||||||
group_nodes = []
|
|
||||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||||
nodes = list(BreadthFirstIterator(root))
|
nodes = list(BreadthFirstIterator(root))
|
||||||
|
|
||||||
|
# Only check nodes inside build area.
|
||||||
|
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
|
||||||
|
|
||||||
random.shuffle(nodes)
|
random.shuffle(nodes)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
|
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
|
||||||
|
|
@ -62,26 +65,9 @@ class PlatformPhysics:
|
||||||
|
|
||||||
bbox = node.getBoundingBox()
|
bbox = node.getBoundingBox()
|
||||||
|
|
||||||
# Ignore intersections with the bottom
|
|
||||||
build_volume_bounding_box = self._build_volume.getBoundingBox()
|
|
||||||
if build_volume_bounding_box:
|
|
||||||
# It's over 9000!
|
|
||||||
build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
|
|
||||||
else:
|
|
||||||
# No bounding box. This is triggered when running Cura from command line with a model for the first time
|
|
||||||
# In that situation there is a model, but no machine (and therefore no build volume.
|
|
||||||
return
|
|
||||||
node._outside_buildarea = False
|
|
||||||
|
|
||||||
# Mark the node as outside the build volume if the bounding box test fails.
|
|
||||||
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
|
||||||
node._outside_buildarea = True
|
|
||||||
|
|
||||||
if node.callDecoration("isGroup"):
|
|
||||||
group_nodes.append(node) # Keep list of affected group_nodes
|
|
||||||
|
|
||||||
# Move it downwards if bottom is above platform
|
# Move it downwards if bottom is above platform
|
||||||
move_vector = Vector()
|
move_vector = Vector()
|
||||||
|
|
||||||
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
|
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||||
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
|
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
|
||||||
|
|
@ -145,32 +131,23 @@ class PlatformPhysics:
|
||||||
# Simply waiting for the next tick seems to resolve this correctly.
|
# Simply waiting for the next tick seems to resolve this correctly.
|
||||||
overlap = None
|
overlap = None
|
||||||
|
|
||||||
convex_hull = node.callDecoration("getConvexHull")
|
|
||||||
if convex_hull:
|
|
||||||
if not convex_hull.isValid():
|
|
||||||
return
|
|
||||||
# Check for collisions between disallowed areas and the object
|
|
||||||
for area in self._build_volume.getDisallowedAreas():
|
|
||||||
overlap = convex_hull.intersectsPolygon(area)
|
|
||||||
if overlap is None:
|
|
||||||
continue
|
|
||||||
node._outside_buildarea = True
|
|
||||||
|
|
||||||
if not Vector.Null.equals(move_vector, epsilon=1e-5):
|
if not Vector.Null.equals(move_vector, epsilon=1e-5):
|
||||||
transformed_nodes.append(node)
|
transformed_nodes.append(node)
|
||||||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
# Group nodes should override the _outside_buildarea property of their children.
|
# After moving, we have to evaluate the boundary checks for nodes
|
||||||
for group_node in group_nodes:
|
build_volume = Application.getInstance().getBuildVolume()
|
||||||
for child_node in group_node.getAllChildren():
|
build_volume.updateNodeBoundaryCheck()
|
||||||
child_node._outside_buildarea = group_node._outside_buildarea
|
|
||||||
|
|
||||||
|
|
||||||
def _onToolOperationStarted(self, tool):
|
def _onToolOperationStarted(self, tool):
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
|
|
||||||
def _onToolOperationStopped(self, tool):
|
def _onToolOperationStopped(self, tool):
|
||||||
|
# Selection tool should not trigger an update.
|
||||||
|
if tool.getPluginId() == "SelectionTool":
|
||||||
|
return
|
||||||
|
|
||||||
if tool.getPluginId() == "TranslateTool":
|
if tool.getPluginId() == "TranslateTool":
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
if node.getBoundingBox().bottom < 0:
|
if node.getBoundingBox().bottom < 0:
|
||||||
|
|
@ -183,4 +160,4 @@ class PlatformPhysics:
|
||||||
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||||
|
|
||||||
self._enabled = True
|
self._enabled = True
|
||||||
self._onChangeTimerFinished()
|
self._onChangeTimerFinished(True)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Operations.Operation import Operation
|
from UM.Operations.Operation import Operation
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
|
|
@ -23,8 +23,8 @@ class PlatformPhysicsOperation(Operation):
|
||||||
def mergeWith(self, other):
|
def mergeWith(self, other):
|
||||||
group = GroupedOperation()
|
group = GroupedOperation()
|
||||||
|
|
||||||
group.addOperation(self)
|
|
||||||
group.addOperation(other)
|
group.addOperation(other)
|
||||||
|
group.addOperation(self)
|
||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
|
|
|
||||||
57
cura/PreviewPass.py
Normal file
57
cura/PreviewPass.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
from UM.View.RenderPass import RenderPass
|
||||||
|
from UM.View.GL.OpenGL import OpenGL
|
||||||
|
from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
|
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from UM.Scene.Camera import Camera
|
||||||
|
|
||||||
|
## A render pass subclass that renders slicable objects with default parameters.
|
||||||
|
# It uses the active camera by default, but it can be overridden to use a different camera.
|
||||||
|
#
|
||||||
|
# This is useful to get a preview image of a scene taken from a different location as the active camera.
|
||||||
|
class PreviewPass(RenderPass):
|
||||||
|
def __init__(self, width: int, height: int):
|
||||||
|
super().__init__("preview", width, height, 0)
|
||||||
|
|
||||||
|
self._camera = None # type: Optional[Camera]
|
||||||
|
|
||||||
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
|
self._shader = None
|
||||||
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
# Set the camera to be used by this render pass
|
||||||
|
# if it's None, the active camera is used
|
||||||
|
def setCamera(self, camera: Optional["Camera"]):
|
||||||
|
self._camera = camera
|
||||||
|
|
||||||
|
def render(self) -> None:
|
||||||
|
if not self._shader:
|
||||||
|
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "object.shader"))
|
||||||
|
|
||||||
|
# Create a new batch to be rendered
|
||||||
|
batch = RenderBatch(self._shader)
|
||||||
|
|
||||||
|
# Fill up the batch with objects that can be sliced. `
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
|
batch.addItem(node.getWorldTransformation(), node.getMeshData())
|
||||||
|
|
||||||
|
self.bind()
|
||||||
|
if self._camera is None:
|
||||||
|
batch.render(Application.getInstance().getController().getScene().getActiveCamera())
|
||||||
|
else:
|
||||||
|
batch.render(self._camera)
|
||||||
|
self.release()
|
||||||
|
|
@ -1,17 +1,24 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
from UM.Qt.Duration import Duration
|
from UM.Qt.Duration import Duration
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
import cura.Settings.ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import os.path
|
import os.path
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
import json
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
||||||
#
|
#
|
||||||
|
|
@ -24,8 +31,8 @@ import unicodedata
|
||||||
# - This triggers a new slice with the current settings - this is the "current settings pass".
|
# - This triggers a new slice with the current settings - this is the "current settings pass".
|
||||||
# - When the slice is done, we update the current print time and material amount.
|
# - When the slice is done, we update the current print time and material amount.
|
||||||
# - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
|
# - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
|
||||||
# - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
|
# - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
|
||||||
# - When the high quality pass is done, we update the maximum print time.
|
# - When the Extra Fine pass is done, we update the maximum print time.
|
||||||
#
|
#
|
||||||
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||||
# This job name is requested by the JobSpecs qml file.
|
# This job name is requested by the JobSpecs qml file.
|
||||||
|
|
@ -44,23 +51,72 @@ class PrintInformation(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._current_print_time = Duration(None, self)
|
self.initializeCuraMessagePrintTimeProperties()
|
||||||
|
|
||||||
self._material_lengths = []
|
self._material_lengths = []
|
||||||
self._material_weights = []
|
self._material_weights = []
|
||||||
|
self._material_costs = []
|
||||||
|
self._material_names = []
|
||||||
|
|
||||||
|
self._pre_sliced = False
|
||||||
|
|
||||||
self._backend = Application.getInstance().getBackend()
|
self._backend = Application.getInstance().getBackend()
|
||||||
if self._backend:
|
if self._backend:
|
||||||
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
||||||
|
|
||||||
self._job_name = ""
|
self._base_name = ""
|
||||||
self._abbr_machine = ""
|
self._abbr_machine = ""
|
||||||
|
self._job_name = ""
|
||||||
|
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._updateJobName)
|
||||||
|
Application.getInstance().fileLoaded.connect(self.setBaseName)
|
||||||
|
Application.getInstance().workspaceLoaded.connect(self.setProjectName)
|
||||||
|
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
|
self._active_material_container = None
|
||||||
|
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged)
|
||||||
|
self._onActiveMaterialChanged()
|
||||||
|
|
||||||
|
self._material_amounts = []
|
||||||
|
|
||||||
|
# Crate cura message translations and using translation keys initialize empty time Duration object for total time
|
||||||
|
# and time for each feature
|
||||||
|
def initializeCuraMessagePrintTimeProperties(self):
|
||||||
|
self._current_print_time = Duration(None, self)
|
||||||
|
|
||||||
|
self._print_time_message_translations = {
|
||||||
|
"inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
|
||||||
|
"inset_x": catalog.i18nc("@tooltip", "Inner Walls"),
|
||||||
|
"skin": catalog.i18nc("@tooltip", "Skin"),
|
||||||
|
"infill": catalog.i18nc("@tooltip", "Infill"),
|
||||||
|
"support_infill": catalog.i18nc("@tooltip", "Support Infill"),
|
||||||
|
"support_interface": catalog.i18nc("@tooltip", "Support Interface"),
|
||||||
|
"support": catalog.i18nc("@tooltip", "Support"),
|
||||||
|
"skirt": catalog.i18nc("@tooltip", "Skirt"),
|
||||||
|
"travel": catalog.i18nc("@tooltip", "Travel"),
|
||||||
|
"retract": catalog.i18nc("@tooltip", "Retractions"),
|
||||||
|
"none": catalog.i18nc("@tooltip", "Other")
|
||||||
|
}
|
||||||
|
|
||||||
|
self._print_time_message_values = {}
|
||||||
|
|
||||||
|
# Full fill message values using keys from _print_time_message_translations
|
||||||
|
for key in self._print_time_message_translations.keys():
|
||||||
|
self._print_time_message_values[key] = Duration(None, self)
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
|
|
||||||
Application.getInstance().fileLoaded.connect(self.setJobName)
|
|
||||||
|
|
||||||
currentPrintTimeChanged = pyqtSignal()
|
currentPrintTimeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
preSlicedChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=preSlicedChanged)
|
||||||
|
def preSliced(self):
|
||||||
|
return self._pre_sliced
|
||||||
|
|
||||||
|
def setPreSliced(self, pre_sliced):
|
||||||
|
self._pre_sliced = pre_sliced
|
||||||
|
self.preSlicedChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
|
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
|
||||||
def currentPrintTime(self):
|
def currentPrintTime(self):
|
||||||
return self._current_print_time
|
return self._current_print_time
|
||||||
|
|
@ -77,38 +133,122 @@ class PrintInformation(QObject):
|
||||||
def materialWeights(self):
|
def materialWeights(self):
|
||||||
return self._material_weights
|
return self._material_weights
|
||||||
|
|
||||||
def _onPrintDurationMessage(self, total_time, material_amounts):
|
materialCostsChanged = pyqtSignal()
|
||||||
self._current_print_time.setDuration(total_time)
|
|
||||||
|
@pyqtProperty("QVariantList", notify = materialCostsChanged)
|
||||||
|
def materialCosts(self):
|
||||||
|
return self._material_costs
|
||||||
|
|
||||||
|
materialNamesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = materialNamesChanged)
|
||||||
|
def materialNames(self):
|
||||||
|
return self._material_names
|
||||||
|
|
||||||
|
def _onPrintDurationMessage(self, print_time, material_amounts):
|
||||||
|
|
||||||
|
self._updateTotalPrintTimePerFeature(print_time)
|
||||||
self.currentPrintTimeChanged.emit()
|
self.currentPrintTimeChanged.emit()
|
||||||
|
|
||||||
|
self._material_amounts = material_amounts
|
||||||
|
self._calculateInformation()
|
||||||
|
|
||||||
|
def _updateTotalPrintTimePerFeature(self, print_time):
|
||||||
|
total_estimated_time = 0
|
||||||
|
|
||||||
|
for feature, time in print_time.items():
|
||||||
|
if time != time: # Check for NaN. Engine can sometimes give us weird values.
|
||||||
|
self._print_time_message_values.get(feature).setDuration(0)
|
||||||
|
Logger.log("w", "Received NaN for print duration message")
|
||||||
|
continue
|
||||||
|
|
||||||
|
total_estimated_time += time
|
||||||
|
self._print_time_message_values.get(feature).setDuration(time)
|
||||||
|
|
||||||
|
self._current_print_time.setDuration(total_estimated_time)
|
||||||
|
|
||||||
|
def _calculateInformation(self):
|
||||||
|
if Application.getInstance().getGlobalContainerStack() is None:
|
||||||
|
return
|
||||||
|
|
||||||
# Material amount is sent as an amount of mm^3, so calculate length from that
|
# Material amount is sent as an amount of mm^3, so calculate length from that
|
||||||
r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
||||||
self._material_lengths = []
|
self._material_lengths = []
|
||||||
self._material_weights = []
|
self._material_weights = []
|
||||||
extruder_stacks = list(cura.Settings.ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
|
self._material_costs = []
|
||||||
for index, amount in enumerate(material_amounts):
|
self._material_names = []
|
||||||
|
|
||||||
|
material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
|
||||||
|
|
||||||
|
extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
|
||||||
|
for index, amount in enumerate(self._material_amounts):
|
||||||
## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
|
## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
|
||||||
# list comprehension filtering to solve this for us.
|
# list comprehension filtering to solve this for us.
|
||||||
|
material = None
|
||||||
if extruder_stacks: # Multi extrusion machine
|
if extruder_stacks: # Multi extrusion machine
|
||||||
extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
|
extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
|
||||||
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
|
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
|
||||||
|
material = extruder_stack.findContainer({"type": "material"})
|
||||||
else: # Machine with no extruder stacks
|
else: # Machine with no extruder stacks
|
||||||
density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
|
density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
|
||||||
|
material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"})
|
||||||
|
|
||||||
|
weight = float(amount) * float(density) / 1000
|
||||||
|
cost = 0
|
||||||
|
material_name = catalog.i18nc("@label unknown material", "Unknown")
|
||||||
|
if material:
|
||||||
|
material_guid = material.getMetaDataEntry("GUID")
|
||||||
|
material_name = material.getName()
|
||||||
|
if material_guid in material_preference_values:
|
||||||
|
material_values = material_preference_values[material_guid]
|
||||||
|
|
||||||
|
weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
|
||||||
|
cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
|
||||||
|
|
||||||
|
if weight_per_spool != 0:
|
||||||
|
cost = cost_per_spool * weight / weight_per_spool
|
||||||
|
else:
|
||||||
|
cost = 0
|
||||||
|
|
||||||
|
if radius != 0:
|
||||||
|
length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
|
||||||
|
else:
|
||||||
|
length = 0
|
||||||
|
self._material_weights.append(weight)
|
||||||
|
self._material_lengths.append(length)
|
||||||
|
self._material_costs.append(cost)
|
||||||
|
self._material_names.append(material_name)
|
||||||
|
|
||||||
self._material_weights.append(float(amount) * float(density) / 1000)
|
|
||||||
self._material_lengths.append(round((amount / (math.pi * r ** 2)) / 1000, 2))
|
|
||||||
self.materialLengthsChanged.emit()
|
self.materialLengthsChanged.emit()
|
||||||
self.materialWeightsChanged.emit()
|
self.materialWeightsChanged.emit()
|
||||||
|
self.materialCostsChanged.emit()
|
||||||
|
self.materialNamesChanged.emit()
|
||||||
|
|
||||||
|
def _onPreferencesChanged(self, preference):
|
||||||
|
if preference != "cura/material_settings":
|
||||||
|
return
|
||||||
|
|
||||||
|
self._calculateInformation()
|
||||||
|
|
||||||
|
def _onActiveMaterialChanged(self):
|
||||||
|
if self._active_material_container:
|
||||||
|
try:
|
||||||
|
self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged)
|
||||||
|
except TypeError: #pyQtSignal gives a TypeError when disconnecting from something that is already disconnected.
|
||||||
|
pass
|
||||||
|
|
||||||
|
active_material_id = Application.getInstance().getMachineManager().activeMaterialId
|
||||||
|
active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
|
||||||
|
|
||||||
|
if active_material_containers:
|
||||||
|
self._active_material_container = active_material_containers[0]
|
||||||
|
self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
|
||||||
|
|
||||||
|
def _onMaterialMetaDataChanged(self, *args, **kwargs):
|
||||||
|
self._calculateInformation()
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setJobName(self, name):
|
def setJobName(self, name):
|
||||||
# Ensure that we don't use entire path but only filename
|
|
||||||
name = os.path.basename(name)
|
|
||||||
|
|
||||||
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
|
||||||
# extension. This cuts the extension off if necessary.
|
|
||||||
name = os.path.splitext(name)[0]
|
|
||||||
if self._job_name != name:
|
|
||||||
self._job_name = name
|
self._job_name = name
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
|
|
@ -118,14 +258,54 @@ class PrintInformation(QObject):
|
||||||
def jobName(self):
|
def jobName(self):
|
||||||
return self._job_name
|
return self._job_name
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
def _updateJobName(self):
|
||||||
def createJobName(self, base_name):
|
if self._base_name == "":
|
||||||
base_name = self._stripAccents(base_name)
|
self._job_name = ""
|
||||||
|
self.jobNameChanged.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
base_name = self._stripAccents(self._base_name)
|
||||||
self._setAbbreviatedMachineName()
|
self._setAbbreviatedMachineName()
|
||||||
if Preferences.getInstance().getValue("cura/jobname_prefix"):
|
if self._pre_sliced:
|
||||||
return self._abbr_machine + "_" + base_name
|
self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name)
|
||||||
|
elif Preferences.getInstance().getValue("cura/jobname_prefix"):
|
||||||
|
# Don't add abbreviation if it already has the exact same abbreviation.
|
||||||
|
if base_name.startswith(self._abbr_machine + "_"):
|
||||||
|
self._job_name = base_name
|
||||||
else:
|
else:
|
||||||
return base_name
|
self._job_name = self._abbr_machine + "_" + base_name
|
||||||
|
else:
|
||||||
|
self._job_name = base_name
|
||||||
|
|
||||||
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str)
|
||||||
|
def baseName(self):
|
||||||
|
return self._base_name
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setProjectName(self, name):
|
||||||
|
self.setBaseName(name, is_project_file = True)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setBaseName(self, base_name, is_project_file = False):
|
||||||
|
# Ensure that we don't use entire path but only filename
|
||||||
|
name = os.path.basename(base_name)
|
||||||
|
|
||||||
|
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
||||||
|
# extension. This cuts the extension off if necessary.
|
||||||
|
name = os.path.splitext(name)[0]
|
||||||
|
|
||||||
|
# if this is a profile file, always update the job name
|
||||||
|
# name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
|
||||||
|
is_empty = name == ""
|
||||||
|
if is_project_file or (is_empty or (self._base_name == "" and self._base_name != name)):
|
||||||
|
# remove ".curaproject" suffix from (imported) the file name
|
||||||
|
if name.endswith(".curaproject"):
|
||||||
|
name = name[:name.rfind(".curaproject")]
|
||||||
|
self._base_name = name
|
||||||
|
self._updateJobName()
|
||||||
|
|
||||||
|
|
||||||
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
||||||
# Called each time the global stack is switched
|
# Called each time the global stack is switched
|
||||||
|
|
@ -144,10 +324,34 @@ class PrintInformation(QObject):
|
||||||
elif word.isdigit():
|
elif word.isdigit():
|
||||||
abbr_machine += word
|
abbr_machine += word
|
||||||
else:
|
else:
|
||||||
abbr_machine += self._stripAccents(word.strip("()[]{}#").upper())[0]
|
stripped_word = self._stripAccents(word.strip("()[]{}#").upper())
|
||||||
|
# - use only the first character if the word is too long (> 3 characters)
|
||||||
|
# - use the whole word if it's not too long (<= 3 characters)
|
||||||
|
if len(stripped_word) > 3:
|
||||||
|
stripped_word = stripped_word[0]
|
||||||
|
abbr_machine += stripped_word
|
||||||
|
|
||||||
self._abbr_machine = abbr_machine
|
self._abbr_machine = abbr_machine
|
||||||
|
|
||||||
## Utility method that strips accents from characters (eg: â -> a)
|
## Utility method that strips accents from characters (eg: â -> a)
|
||||||
def _stripAccents(self, str):
|
def _stripAccents(self, str):
|
||||||
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
|
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
|
||||||
|
|
||||||
|
@pyqtSlot(result = "QVariantMap")
|
||||||
|
def getFeaturePrintTimes(self):
|
||||||
|
result = {}
|
||||||
|
for feature, time in self._print_time_message_values.items():
|
||||||
|
if feature in self._print_time_message_translations:
|
||||||
|
result[self._print_time_message_translations[feature]] = time
|
||||||
|
else:
|
||||||
|
result[feature] = time
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Simulate message with zero time duration
|
||||||
|
def setToZeroPrintInformation(self):
|
||||||
|
temp_message = {}
|
||||||
|
for key in self._print_time_message_values.keys():
|
||||||
|
temp_message[key] = 0
|
||||||
|
|
||||||
|
temp_material_amounts = [0]
|
||||||
|
self._onPrintDurationMessage(temp_message, temp_material_amounts)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSlot, QObject, QTimer, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
import UM.Settings.ContainerRegistry
|
|
||||||
|
|
||||||
from enum import IntEnum # For the connection state tracking.
|
from enum import IntEnum # For the connection state tracking.
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Signal import signalemitter
|
from UM.Signal import signalemitter
|
||||||
|
from UM.Application import Application
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
@ -25,7 +28,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
def __init__(self, device_id, parent = None):
|
def __init__(self, device_id, parent = None):
|
||||||
super().__init__(device_id = device_id, parent = parent)
|
super().__init__(device_id = device_id, parent = parent)
|
||||||
|
|
||||||
self._container_registry = UM.Settings.ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
self._target_bed_temperature = 0
|
self._target_bed_temperature = 0
|
||||||
self._bed_temperature = 0
|
self._bed_temperature = 0
|
||||||
self._num_extruders = 1
|
self._num_extruders = 1
|
||||||
|
|
@ -45,12 +48,28 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._job_name = ""
|
self._job_name = ""
|
||||||
self._error_text = ""
|
self._error_text = ""
|
||||||
self._accepts_commands = True
|
self._accepts_commands = True
|
||||||
|
self._preheat_bed_timeout = 900 # Default time-out for pre-heating the bed, in seconds.
|
||||||
|
self._preheat_bed_timer = QTimer() # Timer that tracks how long to preheat still.
|
||||||
|
self._preheat_bed_timer.setSingleShot(True)
|
||||||
|
self._preheat_bed_timer.timeout.connect(self.cancelPreheatBed)
|
||||||
|
|
||||||
self._printer_state = ""
|
self._printer_state = ""
|
||||||
self._printer_type = "unknown"
|
self._printer_type = "unknown"
|
||||||
|
|
||||||
self._camera_active = False
|
self._camera_active = False
|
||||||
|
|
||||||
|
self._monitor_view_qml_path = ""
|
||||||
|
self._monitor_item = None
|
||||||
|
|
||||||
|
self._control_view_qml_path = ""
|
||||||
|
self._control_item = None
|
||||||
|
|
||||||
|
self._qml_context = None
|
||||||
|
self._can_pause = True
|
||||||
|
self._can_abort = True
|
||||||
|
self._can_pre_heat_bed = True
|
||||||
|
self._can_control_manually = True
|
||||||
|
|
||||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
|
|
@ -102,6 +121,60 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
printerTypeChanged = pyqtSignal()
|
printerTypeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally).
|
||||||
|
preheatBedRemainingTimeChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canPreHeatBed(self):
|
||||||
|
return self._can_pre_heat_bed
|
||||||
|
|
||||||
|
# Does the printer support pause at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canPause(self):
|
||||||
|
return self._can_pause
|
||||||
|
|
||||||
|
# Does the printer support abort at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canAbort(self):
|
||||||
|
return self._can_abort
|
||||||
|
|
||||||
|
# Does the printer support manual control at all
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def canControlManually(self):
|
||||||
|
return self._can_control_manually
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant=True)
|
||||||
|
def monitorItem(self):
|
||||||
|
# Note that we specifically only check if the monitor component is created.
|
||||||
|
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||||
|
# create the item (and fail) every time.
|
||||||
|
if not self._monitor_item:
|
||||||
|
self._createMonitorViewFromQML()
|
||||||
|
return self._monitor_item
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant=True)
|
||||||
|
def controlItem(self):
|
||||||
|
if not self._control_item:
|
||||||
|
self._createControlViewFromQML()
|
||||||
|
return self._control_item
|
||||||
|
|
||||||
|
def _createControlViewFromQML(self):
|
||||||
|
if not self._control_view_qml_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {
|
||||||
|
"OutputDevice": self
|
||||||
|
})
|
||||||
|
|
||||||
|
def _createMonitorViewFromQML(self):
|
||||||
|
if not self._monitor_view_qml_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {
|
||||||
|
"OutputDevice": self
|
||||||
|
})
|
||||||
|
|
||||||
@pyqtProperty(str, notify=printerTypeChanged)
|
@pyqtProperty(str, notify=printerTypeChanged)
|
||||||
def printerType(self):
|
def printerType(self):
|
||||||
return self._printer_type
|
return self._printer_type
|
||||||
|
|
@ -161,6 +234,17 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._job_name = name
|
self._job_name = name
|
||||||
self.jobNameChanged.emit()
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
|
## Gives a human-readable address where the device can be found.
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def address(self):
|
||||||
|
Logger.log("w", "address is not implemented by this output device.")
|
||||||
|
|
||||||
|
## A human-readable name for the device.
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def name(self):
|
||||||
|
Logger.log("w", "name is not implemented by this output device.")
|
||||||
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = errorTextChanged)
|
@pyqtProperty(str, notify = errorTextChanged)
|
||||||
def errorText(self):
|
def errorText(self):
|
||||||
return self._error_text
|
return self._error_text
|
||||||
|
|
@ -199,6 +283,30 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._target_bed_temperature = temperature
|
self._target_bed_temperature = temperature
|
||||||
self.targetBedTemperatureChanged.emit()
|
self.targetBedTemperatureChanged.emit()
|
||||||
|
|
||||||
|
## The total duration of the time-out to pre-heat the bed, in seconds.
|
||||||
|
#
|
||||||
|
# \return The duration of the time-out to pre-heat the bed, in seconds.
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
|
def preheatBedTimeout(self):
|
||||||
|
return self._preheat_bed_timeout
|
||||||
|
|
||||||
|
## The remaining duration of the pre-heating of the bed.
|
||||||
|
#
|
||||||
|
# This is formatted in M:SS format.
|
||||||
|
# \return The duration of the time-out to pre-heat the bed, formatted.
|
||||||
|
@pyqtProperty(str, notify = preheatBedRemainingTimeChanged)
|
||||||
|
def preheatBedRemainingTime(self):
|
||||||
|
if not self._preheat_bed_timer.isActive():
|
||||||
|
return ""
|
||||||
|
period = self._preheat_bed_timer.remainingTime()
|
||||||
|
if period <= 0:
|
||||||
|
return ""
|
||||||
|
minutes, period = divmod(period, 60000) #60000 milliseconds in a minute.
|
||||||
|
seconds, _ = divmod(period, 1000) #1000 milliseconds in a second.
|
||||||
|
if minutes <= 0 and seconds <= 0:
|
||||||
|
return ""
|
||||||
|
return "%d:%02d" % (minutes, seconds)
|
||||||
|
|
||||||
## Time the print has been printing.
|
## Time the print has been printing.
|
||||||
# Note that timeTotal - timeElapsed should give time remaining.
|
# Note that timeTotal - timeElapsed should give time remaining.
|
||||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||||
|
|
@ -254,6 +362,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
def _setTargetBedTemperature(self, temperature):
|
def _setTargetBedTemperature(self, temperature):
|
||||||
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
|
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
|
||||||
|
|
||||||
|
## Pre-heats the heated bed of the printer.
|
||||||
|
#
|
||||||
|
# \param temperature The temperature to heat the bed to, in degrees
|
||||||
|
# Celsius.
|
||||||
|
# \param duration How long the bed should stay warm, in seconds.
|
||||||
|
@pyqtSlot(float, float)
|
||||||
|
def preheatBed(self, temperature, duration):
|
||||||
|
Logger.log("w", "preheatBed is not implemented by this output device.")
|
||||||
|
|
||||||
|
## Cancels pre-heating the heated bed of the printer.
|
||||||
|
#
|
||||||
|
# If the bed is not pre-heated, nothing happens.
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelPreheatBed(self):
|
||||||
|
Logger.log("w", "cancelPreheatBed is not implemented by this output device.")
|
||||||
|
|
||||||
## Protected setter for the current bed temperature.
|
## Protected setter for the current bed temperature.
|
||||||
# This simply sets the bed temperature, but ensures that a signal is emitted.
|
# This simply sets the bed temperature, but ensures that a signal is emitted.
|
||||||
# /param temperature temperature of the bed.
|
# /param temperature temperature of the bed.
|
||||||
|
|
@ -323,6 +447,28 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
|
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
## List of the colours of the currently loaded materials.
|
||||||
|
#
|
||||||
|
# The list is in order of extruders. If there is no material in an
|
||||||
|
# extruder, the colour is shown as transparent.
|
||||||
|
#
|
||||||
|
# The colours are returned in hex-format AARRGGBB or RRGGBB
|
||||||
|
# (e.g. #800000ff for transparent blue or #00ff00 for pure green).
|
||||||
|
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||||
|
def materialColors(self):
|
||||||
|
result = []
|
||||||
|
for material_id in self._material_ids:
|
||||||
|
if material_id is None:
|
||||||
|
result.append("#00000000") #No material.
|
||||||
|
continue
|
||||||
|
|
||||||
|
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
|
||||||
|
if containers:
|
||||||
|
result.append(containers[0].getMetaDataEntry("color_code"))
|
||||||
|
else:
|
||||||
|
result.append("#00000000") #Unknown material.
|
||||||
|
return result
|
||||||
|
|
||||||
## Protected setter for the current material id.
|
## Protected setter for the current material id.
|
||||||
# /param index Index of the extruder
|
# /param index Index of the extruder
|
||||||
# /param material_id id of the material
|
# /param material_id id of the material
|
||||||
|
|
@ -340,10 +486,14 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
# /param index Index of the extruder
|
# /param index Index of the extruder
|
||||||
# /param hotend_id id of the hotend
|
# /param hotend_id id of the hotend
|
||||||
def _setHotendId(self, index, hotend_id):
|
def _setHotendId(self, index, hotend_id):
|
||||||
if hotend_id and hotend_id != "" and hotend_id != self._hotend_ids[index]:
|
if hotend_id and hotend_id != self._hotend_ids[index]:
|
||||||
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
|
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
|
||||||
self._hotend_ids[index] = hotend_id
|
self._hotend_ids[index] = hotend_id
|
||||||
self.hotendIdChanged.emit(index, hotend_id)
|
self.hotendIdChanged.emit(index, hotend_id)
|
||||||
|
elif not hotend_id:
|
||||||
|
Logger.log("d", "Removing hotend id of hotend %d.", index)
|
||||||
|
self._hotend_ids[index] = None
|
||||||
|
self.hotendIdChanged.emit(index, None)
|
||||||
|
|
||||||
## Let the user decide if the hotends and/or material should be synced with the printer
|
## Let the user decide if the hotends and/or material should be synced with the printer
|
||||||
# NB: the UX needs to be implemented by the plugin
|
# NB: the UX needs to be implemented by the plugin
|
||||||
|
|
@ -462,7 +612,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
@pyqtSlot("long")
|
@pyqtSlot("long")
|
||||||
@pyqtSlot("long", "long")
|
@pyqtSlot("long", "long")
|
||||||
def setHeadZ(self, z, speed = 3000):
|
def setHeadZ(self, z, speed = 3000):
|
||||||
self._setHeadY(z, speed)
|
self._setHeadZ(z, speed)
|
||||||
|
|
||||||
## Move the head of the printer.
|
## Move the head of the printer.
|
||||||
# Note that this is a relative move. If you want to move the head to a specific position you can use
|
# Note that this is a relative move. If you want to move the head to a specific position you can use
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,42 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import UM.Application
|
|
||||||
import cura.Settings.ExtruderManager
|
|
||||||
import UM.Settings.ContainerRegistry
|
|
||||||
|
|
||||||
# This collects a lot of quality and quality changes related code which was split between ContainerManager
|
# This collects a lot of quality and quality changes related code which was split between ContainerManager
|
||||||
# and the MachineManager and really needs to usable from both.
|
# and the MachineManager and really needs to usable from both.
|
||||||
|
from typing import List, Optional, Dict, TYPE_CHECKING
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainerInterface
|
||||||
|
|
||||||
class QualityManager:
|
class QualityManager:
|
||||||
|
|
||||||
## Get the singleton instance for this class.
|
## Get the singleton instance for this class.
|
||||||
@classmethod
|
@classmethod
|
||||||
def getInstance(cls):
|
def getInstance(cls) -> "QualityManager":
|
||||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||||
if QualityManager.__instance is None:
|
if not QualityManager.__instance:
|
||||||
QualityManager.__instance = cls()
|
QualityManager.__instance = cls()
|
||||||
return QualityManager.__instance
|
return QualityManager.__instance
|
||||||
|
|
||||||
__instance = None
|
__instance = None # type: "QualityManager"
|
||||||
|
|
||||||
## Find a quality by name for a specific machine definition and materials.
|
## Find a quality by name for a specific machine definition and materials.
|
||||||
#
|
#
|
||||||
# \param quality_name
|
# \param quality_name
|
||||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
|
||||||
# specified then the currently selected machine definition is used.
|
# specified then the currently selected machine definition is used.
|
||||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||||
# the current set of selected materials is used.
|
# the current set of selected materials is used.
|
||||||
# \return the matching quality container \type{ContainerInstance}
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
def findQualityByName(self, quality_name, machine_definition=None, material_containers=None):
|
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[InstanceContainer]:
|
||||||
criteria = {"type": "quality", "name": quality_name}
|
criteria = {"type": "quality", "name": quality_name}
|
||||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||||
|
|
||||||
|
|
@ -42,15 +50,18 @@ class QualityManager:
|
||||||
## Find a quality changes container by name.
|
## Find a quality changes container by name.
|
||||||
#
|
#
|
||||||
# \param quality_changes_name \type{str} the name of the quality changes container.
|
# \param quality_changes_name \type{str} the name of the quality changes container.
|
||||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
# \param machine_definition (Optional) \type{DefinitionContainer} If nothing is
|
||||||
# specified then the currently selected machine definition is used.
|
# specified then the currently selected machine definition is used..
|
||||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
# \return the matching quality changes containers \type{List[InstanceContainer]}
|
||||||
# the current set of selected materials is used.
|
def findQualityChangesByName(self, quality_changes_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None):
|
||||||
# \return the matching quality changes containers \type{List[ContainerInstance]}
|
if not machine_definition:
|
||||||
def findQualityChangesByName(self, quality_changes_name, machine_definition=None):
|
global_stack = Application.getGlobalContainerStack()
|
||||||
criteria = {"type": "quality_changes", "name": quality_changes_name}
|
if not global_stack:
|
||||||
result = self._getFilteredContainersForStack(machine_definition, [], **criteria)
|
return [] #No stack, so no current definition could be found, so there are no quality changes either.
|
||||||
|
machine_definition = global_stack.definition
|
||||||
|
|
||||||
|
result = self.findAllQualityChangesForMachine(machine_definition)
|
||||||
|
result = [quality_change for quality_change in result if quality_change.getName() == quality_changes_name]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Fetch the list of available quality types for this combination of machine definition and materials.
|
## Fetch the list of available quality types for this combination of machine definition and materials.
|
||||||
|
|
@ -58,7 +69,7 @@ class QualityManager:
|
||||||
# \param machine_definition \type{DefinitionContainer}
|
# \param machine_definition \type{DefinitionContainer}
|
||||||
# \param material_containers \type{List[InstanceContainer]}
|
# \param material_containers \type{List[InstanceContainer]}
|
||||||
# \return \type{List[str]}
|
# \return \type{List[str]}
|
||||||
def findAllQualityTypesForMachineAndMaterials(self, machine_definition, material_containers):
|
def findAllQualityTypesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[str]:
|
||||||
# Determine the common set of quality types which can be
|
# Determine the common set of quality types which can be
|
||||||
# applied to all of the materials for this machine.
|
# applied to all of the materials for this machine.
|
||||||
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
|
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
|
||||||
|
|
@ -69,12 +80,23 @@ class QualityManager:
|
||||||
|
|
||||||
return list(common_quality_types)
|
return list(common_quality_types)
|
||||||
|
|
||||||
|
def findAllQualitiesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[InstanceContainer]:
|
||||||
|
# Determine the common set of quality types which can be
|
||||||
|
# applied to all of the materials for this machine.
|
||||||
|
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
|
||||||
|
qualities = set(quality_type_dict.values())
|
||||||
|
for material_container in material_containers[1:]:
|
||||||
|
next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
|
||||||
|
qualities.intersection_update(set(next_quality_type_dict.values()))
|
||||||
|
|
||||||
|
return list(qualities)
|
||||||
|
|
||||||
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
|
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
|
||||||
#
|
#
|
||||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
# \param material \type{ContainerInstance} the material.
|
# \param material \type{InstanceContainer} the material.
|
||||||
# \return \type{Dict[str, ContainerInstance]} the dict of suitable quality type names mapping to qualities.
|
# \return \type{Dict[str, InstanceContainer]} the dict of suitable quality type names mapping to qualities.
|
||||||
def __fetchQualityTypeDictForMaterial(self, machine_definition, material):
|
def __fetchQualityTypeDictForMaterial(self, machine_definition: "DefinitionContainerInterface", material: InstanceContainer) -> Dict[str, InstanceContainer]:
|
||||||
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
|
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
|
||||||
quality_type_dict = {}
|
quality_type_dict = {}
|
||||||
for quality in qualities:
|
for quality in qualities:
|
||||||
|
|
@ -84,35 +106,35 @@ class QualityManager:
|
||||||
## Find a quality container by quality type.
|
## Find a quality container by quality type.
|
||||||
#
|
#
|
||||||
# \param quality_type \type{str} the name of the quality type to search for.
|
# \param quality_type \type{str} the name of the quality type to search for.
|
||||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
|
||||||
# specified then the currently selected machine definition is used.
|
# specified then the currently selected machine definition is used.
|
||||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||||
# the current set of selected materials is used.
|
# the current set of selected materials is used.
|
||||||
# \return the matching quality container \type{ContainerInstance}
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
def findQualityByQualityType(self, quality_type, machine_definition=None, material_containers=None, **kwargs):
|
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> InstanceContainer:
|
||||||
criteria = kwargs
|
criteria = kwargs
|
||||||
criteria["type"] = "quality"
|
criteria["type"] = "quality"
|
||||||
if quality_type:
|
if quality_type:
|
||||||
criteria["quality_type"] = quality_type
|
criteria["quality_type"] = quality_type
|
||||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||||
|
|
||||||
# Fall back to using generic materials and qualities if nothing could be found.
|
# Fall back to using generic materials and qualities if nothing could be found.
|
||||||
if not result and material_containers and len(material_containers) == 1:
|
if not result and material_containers and len(material_containers) == 1:
|
||||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||||
|
if basic_materials:
|
||||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
return result[0] if result else None
|
return result[0] if result else None
|
||||||
|
|
||||||
## Find all suitable qualities for a combination of machine and material.
|
## Find all suitable qualities for a combination of machine and material.
|
||||||
#
|
#
|
||||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
# \param material_container \type{ContainerInstance} the material.
|
# \param material_container \type{InstanceContainer} the material.
|
||||||
# \return \type{List[ContainerInstance]} the list of suitable qualities.
|
# \return \type{List[InstanceContainer]} the list of suitable qualities.
|
||||||
def findAllQualitiesForMachineMaterial(self, machine_definition, material_container):
|
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
|
||||||
criteria = {"type": "quality"}
|
criteria = {"type": "quality"}
|
||||||
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
||||||
if not result:
|
if not result:
|
||||||
basic_materials = self._getBasicMaterials(material_container)
|
basic_materials = self._getBasicMaterials(material_container)
|
||||||
|
if basic_materials:
|
||||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -121,35 +143,51 @@ class QualityManager:
|
||||||
#
|
#
|
||||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
# \return \type{List[InstanceContainer]} the list of quality changes
|
# \return \type{List[InstanceContainer]} the list of quality changes
|
||||||
def findAllQualityChangesForMachine(self, machine_definition):
|
def findAllQualityChangesForMachine(self, machine_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
|
||||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
definition_id = machine_definition.getId()
|
definition_id = machine_definition.getId()
|
||||||
else:
|
else:
|
||||||
definition_id = "fdmprinter"
|
definition_id = "fdmprinter"
|
||||||
|
|
||||||
filter_dict = { "type": "quality_changes", "extruder": None, "definition": definition_id }
|
filter_dict = { "type": "quality_changes", "definition": definition_id }
|
||||||
quality_changes_list = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
quality_changes_list = ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||||
return quality_changes_list
|
return quality_changes_list
|
||||||
|
|
||||||
|
def findAllExtruderDefinitionsForMachine(self, machine_definition: "DefinitionContainerInterface") -> List["DefinitionContainerInterface"]:
|
||||||
|
filter_dict = { "machine": machine_definition.getId() }
|
||||||
|
return ContainerRegistry.getInstance().findDefinitionContainers(**filter_dict)
|
||||||
|
|
||||||
|
## Find all quality changes for a given extruder.
|
||||||
|
#
|
||||||
|
# \param extruder_definition The extruder to find the quality changes for.
|
||||||
|
# \return The list of quality changes for the given extruder.
|
||||||
|
def findAllQualityChangesForExtruder(self, extruder_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
|
||||||
|
filter_dict = {"type": "quality_changes", "extruder": extruder_definition.getId()}
|
||||||
|
return ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||||
|
|
||||||
## Find all usable qualities for a machine and extruders.
|
## Find all usable qualities for a machine and extruders.
|
||||||
#
|
#
|
||||||
# Finds all of the qualities for this combination of machine and extruders.
|
# Finds all of the qualities for this combination of machine and extruders.
|
||||||
# Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
|
# Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
|
||||||
# then only one of then is returned (at random).
|
# then only one of then is returned (at random).
|
||||||
#
|
#
|
||||||
# \param global_container_stack \type{ContainerStack} the global machine definition
|
# \param global_container_stack \type{GlobalStack} the global machine definition
|
||||||
# \param extruder_stacks \type{List[ContainerStack]} the list of extruder stacks
|
# \param extruder_stacks \type{List[ExtruderStack]} the list of extruder stacks
|
||||||
# \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
|
# \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
|
||||||
# return come from the first extruder in the given list of extruders.
|
# return come from the first extruder in the given list of extruders.
|
||||||
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack, extruder_stacks):
|
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack: "GlobalStack", extruder_stacks: List["ExtruderStack"]) -> List[InstanceContainer]:
|
||||||
global_machine_definition = global_container_stack.getBottom()
|
global_machine_definition = global_container_stack.getBottom()
|
||||||
|
|
||||||
if extruder_stacks:
|
machine_manager = Application.getInstance().getMachineManager()
|
||||||
# Multi-extruder machine detected.
|
active_stack_id = machine_manager.activeStackId
|
||||||
materials = [stack.findContainer(type="material") for stack in extruder_stacks]
|
|
||||||
|
materials = []
|
||||||
|
|
||||||
|
for stack in extruder_stacks:
|
||||||
|
if stack.getId() == active_stack_id and machine_manager.newMaterial:
|
||||||
|
materials.append(machine_manager.newMaterial)
|
||||||
else:
|
else:
|
||||||
# Machine with one extruder.
|
materials.append(stack.material)
|
||||||
materials = [global_container_stack.findContainer(type="material")]
|
|
||||||
|
|
||||||
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
|
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
|
||||||
|
|
||||||
|
|
@ -166,18 +204,17 @@ class QualityManager:
|
||||||
# This tries to find a generic or basic version of the given material.
|
# This tries to find a generic or basic version of the given material.
|
||||||
# \param material_container \type{InstanceContainer} the material
|
# \param material_container \type{InstanceContainer} the material
|
||||||
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
|
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
|
||||||
def _getBasicMaterials(self, material_container):
|
def _getBasicMaterials(self, material_container: InstanceContainer):
|
||||||
base_material = material_container.getMetaDataEntry("material")
|
base_material = material_container.getMetaDataEntry("material")
|
||||||
material_container_definition = material_container.getDefinition()
|
material_container_definition = material_container.getDefinition()
|
||||||
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
||||||
else:
|
else:
|
||||||
definition_id = "fdmprinter"
|
definition_id = "fdmprinter"
|
||||||
|
|
||||||
if base_material:
|
if base_material:
|
||||||
# There is a basic material specified
|
# There is a basic material specified
|
||||||
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
containers = [basic_material for basic_material in containers if
|
containers = [basic_material for basic_material in containers if
|
||||||
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
|
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
|
||||||
"variant")]
|
"variant")]
|
||||||
|
|
@ -188,48 +225,57 @@ class QualityManager:
|
||||||
def _getFilteredContainers(self, **kwargs):
|
def _getFilteredContainers(self, **kwargs):
|
||||||
return self._getFilteredContainersForStack(None, None, **kwargs)
|
return self._getFilteredContainersForStack(None, None, **kwargs)
|
||||||
|
|
||||||
def _getFilteredContainersForStack(self, machine_definition=None, material_containers=None, **kwargs):
|
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
|
||||||
# Fill in any default values.
|
# Fill in any default values.
|
||||||
if machine_definition is None:
|
if machine_definition is None:
|
||||||
machine_definition = UM.Application.getInstance().getGlobalContainerStack().getBottom()
|
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
if quality_definition_id is not None:
|
if quality_definition_id is not None:
|
||||||
machine_definition = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
||||||
|
|
||||||
|
# for convenience
|
||||||
if material_containers is None:
|
if material_containers is None:
|
||||||
active_stacks = cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
material_containers = []
|
||||||
material_containers = [stack.findContainer(type="material") for stack in active_stacks]
|
|
||||||
|
if not material_containers:
|
||||||
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
|
if active_stacks:
|
||||||
|
material_containers = [stack.material for stack in active_stacks]
|
||||||
|
|
||||||
criteria = kwargs
|
criteria = kwargs
|
||||||
filter_by_material = False
|
filter_by_material = False
|
||||||
|
|
||||||
machine_definition = self.getParentMachineDefinition(machine_definition)
|
machine_definition = self.getParentMachineDefinition(machine_definition)
|
||||||
|
criteria["definition"] = machine_definition.getId()
|
||||||
|
found_containers_with_machine_definition = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
|
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
|
||||||
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
||||||
criteria["definition"] = definition_id
|
criteria["definition"] = definition_id
|
||||||
|
|
||||||
filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
|
filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
|
||||||
else:
|
# only fall back to "fdmprinter" when there is no container for this machine
|
||||||
|
elif not found_containers_with_machine_definition:
|
||||||
criteria["definition"] = "fdmprinter"
|
criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
# Stick the material IDs in a set
|
# Stick the material IDs in a set
|
||||||
if material_containers is None or len(material_containers) == 0:
|
|
||||||
filter_by_material = False
|
|
||||||
else:
|
|
||||||
material_ids = set()
|
material_ids = set()
|
||||||
|
|
||||||
for material_instance in material_containers:
|
for material_instance in material_containers:
|
||||||
if material_instance is not None:
|
if material_instance is not None:
|
||||||
|
# Add the parent material too.
|
||||||
|
for basic_material in self._getBasicMaterials(material_instance):
|
||||||
|
material_ids.add(basic_material.getId())
|
||||||
material_ids.add(material_instance.getId())
|
material_ids.add(material_instance.getId())
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for container in containers:
|
for container in containers:
|
||||||
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
|
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
|
||||||
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and not "global_quality" in kwargs:
|
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and "global_quality" not in kwargs:
|
||||||
continue
|
continue
|
||||||
result.append(container)
|
result.append(container)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Get the parent machine definition of a machine definition.
|
## Get the parent machine definition of a machine definition.
|
||||||
|
|
@ -238,8 +284,8 @@ class QualityManager:
|
||||||
# an extruder definition.
|
# an extruder definition.
|
||||||
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
|
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
|
||||||
# definition doesn't have a parent then it is simply returned.
|
# definition doesn't have a parent then it is simply returned.
|
||||||
def getParentMachineDefinition(self, machine_definition):
|
def getParentMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||||
if machine_entry is None:
|
if machine_entry is None:
|
||||||
|
|
@ -267,13 +313,13 @@ class QualityManager:
|
||||||
#
|
#
|
||||||
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||||
# an extruder definition.
|
# an extruder definition.
|
||||||
# \return \type{DefinitionContainer}
|
# \return \type{DefinitionContainerInterface}
|
||||||
def getWholeMachineDefinition(self, machine_definition):
|
def getWholeMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||||
if machine_entry is None:
|
if machine_entry is None:
|
||||||
# This already is a 'global' machine definition.
|
# This already is a 'global' machine definition.
|
||||||
return machine_definition
|
return machine_definition
|
||||||
else:
|
else:
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
||||||
return whole_machine
|
return whole_machine
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Operations import Operation
|
from UM.Operations import Operation
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,35 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import urllib
|
import urllib
|
||||||
|
import uuid
|
||||||
|
from typing import Dict, Union
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl, QVariant
|
from PyQt5.QtCore import QObject, QUrl, QVariant
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
import UM.PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
import UM.Settings
|
from UM.SaveFile import SaveFile
|
||||||
import UM.SaveFile
|
from UM.Platform import Platform
|
||||||
import UM.Platform
|
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||||
import UM.MimeTypeDatabase
|
|
||||||
import UM.Logger
|
|
||||||
|
|
||||||
import cura.Settings
|
from UM.Logger import Logger
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
|
|
||||||
from UM.MimeTypeDatabase import MimeTypeNotFoundError
|
from UM.MimeTypeDatabase import MimeTypeNotFoundError
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
## Manager class that contains common actions to deal with containers in Cura.
|
## Manager class that contains common actions to deal with containers in Cura.
|
||||||
|
|
@ -31,9 +41,8 @@ class ContainerManager(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._container_registry = UM.Settings.ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
self._machine_manager = UM.Application.getInstance().getMachineManager()
|
self._machine_manager = Application.getInstance().getMachineManager()
|
||||||
|
|
||||||
self._container_name_filters = {}
|
self._container_name_filters = {}
|
||||||
|
|
||||||
## Create a duplicate of the specified container
|
## Create a duplicate of the specified container
|
||||||
|
|
@ -48,11 +57,21 @@ class ContainerManager(QObject):
|
||||||
def duplicateContainer(self, container_id):
|
def duplicateContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
|
Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
new_container = self.duplicateContainerInstance(container)
|
||||||
|
return new_container.getId()
|
||||||
|
|
||||||
|
## Create a duplicate of the given container instance
|
||||||
|
#
|
||||||
|
# This will create and add a duplicate of the container that was passed.
|
||||||
|
#
|
||||||
|
# \param container \type{ContainerInterface} The container to duplicate.
|
||||||
|
#
|
||||||
|
# \return The duplicated container, or None if duplication failed.
|
||||||
|
def duplicateContainerInstance(self, container):
|
||||||
new_container = None
|
new_container = None
|
||||||
new_name = self._container_registry.uniqueName(container.getName())
|
new_name = self._container_registry.uniqueName(container.getName())
|
||||||
# Only InstanceContainer has a duplicate method at the moment.
|
# Only InstanceContainer has a duplicate method at the moment.
|
||||||
|
|
@ -64,10 +83,11 @@ class ContainerManager(QObject):
|
||||||
new_container.deserialize(container.serialize())
|
new_container.deserialize(container.serialize())
|
||||||
new_container.setName(new_name)
|
new_container.setName(new_name)
|
||||||
|
|
||||||
|
# TODO: we probably don't want to add it to the registry here!
|
||||||
if new_container:
|
if new_container:
|
||||||
self._container_registry.addContainer(new_container)
|
self._container_registry.addContainer(new_container)
|
||||||
|
|
||||||
return new_container.getId()
|
return new_container
|
||||||
|
|
||||||
## Change the name of a specified container to a new name.
|
## Change the name of a specified container to a new name.
|
||||||
#
|
#
|
||||||
|
|
@ -80,7 +100,7 @@ class ContainerManager(QObject):
|
||||||
def renameContainer(self, container_id, new_id, new_name):
|
def renameContainer(self, container_id, new_id, new_name):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could rename container %s because it was not found.", container_id)
|
Logger.log("w", "Could rename container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
@ -108,7 +128,7 @@ class ContainerManager(QObject):
|
||||||
def removeContainer(self, container_id):
|
def removeContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could remove container %s because it was not found.", container_id)
|
Logger.log("w", "Could remove container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._container_registry.removeContainer(containers[0].getId())
|
self._container_registry.removeContainer(containers[0].getId())
|
||||||
|
|
@ -128,20 +148,20 @@ class ContainerManager(QObject):
|
||||||
def mergeContainers(self, merge_into_id, merge_id):
|
def mergeContainers(self, merge_into_id, merge_id):
|
||||||
containers = self._container_registry.findContainers(None, id = merge_into_id)
|
containers = self._container_registry.findContainers(None, id = merge_into_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
|
Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
merge_into = containers[0]
|
merge_into = containers[0]
|
||||||
|
|
||||||
containers = self._container_registry.findContainers(None, id = merge_id)
|
containers = self._container_registry.findContainers(None, id = merge_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not merge container %s because it was not found", merge_id)
|
Logger.log("w", "Could not merge container %s because it was not found", merge_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
merge = containers[0]
|
merge = containers[0]
|
||||||
|
|
||||||
if not isinstance(merge, type(merge_into)):
|
if not isinstance(merge, type(merge_into)):
|
||||||
UM.Logger.log("w", "Cannot merge two containers of different types")
|
Logger.log("w", "Cannot merge two containers of different types")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._performMerge(merge_into, merge)
|
self._performMerge(merge_into, merge)
|
||||||
|
|
@ -157,11 +177,11 @@ class ContainerManager(QObject):
|
||||||
def clearContainer(self, container_id):
|
def clearContainer(self, container_id):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could clear container %s because it was not found.", container_id)
|
Logger.log("w", "Could clear container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if containers[0].isReadOnly():
|
if containers[0].isReadOnly():
|
||||||
UM.Logger.log("w", "Cannot clear read-only container %s", container_id)
|
Logger.log("w", "Cannot clear read-only container %s", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
containers[0].clear()
|
containers[0].clear()
|
||||||
|
|
@ -172,7 +192,7 @@ class ContainerManager(QObject):
|
||||||
def getContainerMetaDataEntry(self, container_id, entry_name):
|
def getContainerMetaDataEntry(self, container_id, entry_name):
|
||||||
containers = self._container_registry.findContainers(None, id=container_id)
|
containers = self._container_registry.findContainers(None, id=container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
result = containers[0].getMetaDataEntry(entry_name)
|
result = containers[0].getMetaDataEntry(entry_name)
|
||||||
|
|
@ -197,47 +217,109 @@ class ContainerManager(QObject):
|
||||||
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
|
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
||||||
if container.isReadOnly():
|
if container.isReadOnly():
|
||||||
UM.Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
|
Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
entries = entry_name.split("/")
|
entries = entry_name.split("/")
|
||||||
entry_name = entries.pop()
|
entry_name = entries.pop()
|
||||||
|
|
||||||
|
sub_item_changed = False
|
||||||
if entries:
|
if entries:
|
||||||
root_name = entries.pop(0)
|
root_name = entries.pop(0)
|
||||||
root = container.getMetaDataEntry(root_name)
|
root = container.getMetaDataEntry(root_name)
|
||||||
|
|
||||||
item = root
|
item = root
|
||||||
for entry in entries:
|
for _ in range(len(entries)):
|
||||||
item = item.get(entries.pop(0), { })
|
item = item.get(entries.pop(0), { })
|
||||||
|
|
||||||
|
if item[entry_name] != entry_value:
|
||||||
|
sub_item_changed = True
|
||||||
item[entry_name] = entry_value
|
item[entry_name] = entry_value
|
||||||
|
|
||||||
entry_name = root_name
|
entry_name = root_name
|
||||||
entry_value = root
|
entry_value = root
|
||||||
|
|
||||||
container.setMetaDataEntry(entry_name, entry_value)
|
container.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.
|
||||||
|
container.metaDataChanged.emit(container)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
## Set a setting property of the specified container.
|
||||||
|
#
|
||||||
|
# This will set the specified property of the specified setting of the container
|
||||||
|
# and all containers that share the same base_file (if any). The latter only
|
||||||
|
# happens for material containers.
|
||||||
|
#
|
||||||
|
# \param container_id \type{str} The ID of the container to change.
|
||||||
|
# \param setting_key \type{str} The key of the setting.
|
||||||
|
# \param property_name \type{str} The name of the property, eg "value".
|
||||||
|
# \param property_value \type{str} The new value of the property.
|
||||||
|
#
|
||||||
|
# \return True if successful, False if not.
|
||||||
|
@pyqtSlot(str, str, str, str, result = bool)
|
||||||
|
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
|
||||||
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
container = containers[0]
|
||||||
|
|
||||||
|
if container.isReadOnly():
|
||||||
|
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
container.setProperty(setting_key, property_name, property_value)
|
||||||
|
|
||||||
|
basefile = container.getMetaDataEntry("base_file", container_id)
|
||||||
|
for sibbling_container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||||
|
if sibbling_container != container:
|
||||||
|
sibbling_container.setProperty(setting_key, property_name, property_value)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
## Get a setting property of the specified container.
|
||||||
|
#
|
||||||
|
# This will get the specified property of the specified setting of the
|
||||||
|
# specified container.
|
||||||
|
#
|
||||||
|
# \param container_id The ID of the container to get the setting property
|
||||||
|
# of.
|
||||||
|
# \param setting_key The key of the setting to get the property of.
|
||||||
|
# \param property_name The property to obtain.
|
||||||
|
# \return The value of the specified property. The type of this property
|
||||||
|
# value depends on the type of the property. For instance, the "value"
|
||||||
|
# property of an integer setting will be a Python int, but the "value"
|
||||||
|
# property of an enum setting will be a Python str.
|
||||||
|
@pyqtSlot(str, str, str, result = QVariant)
|
||||||
|
def getContainerProperty(self, container_id: str, setting_key: str, property_name: str):
|
||||||
|
containers = self._container_registry.findContainers(id = container_id)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("w", "Could not get properties of container %s because it was not found.", container_id)
|
||||||
|
return ""
|
||||||
|
container = containers[0]
|
||||||
|
|
||||||
|
return container.getProperty(setting_key, property_name)
|
||||||
|
|
||||||
## Set the name of the specified container.
|
## Set the name of the specified container.
|
||||||
@pyqtSlot(str, str, result = bool)
|
@pyqtSlot(str, str, result = bool)
|
||||||
def setContainerName(self, container_id, new_name):
|
def setContainerName(self, container_id, new_name):
|
||||||
containers = self._container_registry.findContainers(None, id = container_id)
|
containers = self._container_registry.findContainers(None, id = container_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
|
Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
||||||
if container.isReadOnly():
|
if container.isReadOnly():
|
||||||
UM.Logger.log("w", "Cannot set name of read-only container %s.", container_id)
|
Logger.log("w", "Cannot set name of read-only container %s.", container_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
container.setName(new_name)
|
container.setName(new_name)
|
||||||
|
|
@ -261,11 +343,25 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def isContainerUsed(self, container_id):
|
def isContainerUsed(self, container_id):
|
||||||
UM.Logger.log("d", "Checking if container %s is currently used", container_id)
|
Logger.log("d", "Checking if container %s is currently used", container_id)
|
||||||
containers = self._container_registry.findContainerStacks()
|
# check if this is a material container. If so, check if any material with the same base is being used by any
|
||||||
for stack in containers:
|
# stacks.
|
||||||
if container_id in [child.getId() for child in stack.getContainers()]:
|
container_ids_to_check = [container_id]
|
||||||
UM.Logger.log("d", "The container is in use by %s", stack.getId())
|
container_results = self._container_registry.findInstanceContainers(id = container_id, type = "material")
|
||||||
|
if container_results:
|
||||||
|
this_container = container_results[0]
|
||||||
|
material_base_file = this_container.getMetaDataEntry("base_file", this_container.getId())
|
||||||
|
# check all material container IDs with the same base
|
||||||
|
material_containers = self._container_registry.findInstanceContainers(base_file = material_base_file,
|
||||||
|
type = "material")
|
||||||
|
if material_containers:
|
||||||
|
container_ids_to_check = [container.getId() for container in material_containers]
|
||||||
|
|
||||||
|
all_stacks = self._container_registry.findContainerStacks()
|
||||||
|
for stack in all_stacks:
|
||||||
|
for used_container_id in container_ids_to_check:
|
||||||
|
if used_container_id in [child.getId() for child in stack.getContainers()]:
|
||||||
|
Logger.log("d", "The container is in use by %s", stack.getId())
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -300,18 +396,20 @@ class ContainerManager(QObject):
|
||||||
#
|
#
|
||||||
# \param container_id The ID of the container to export
|
# \param container_id The ID of the container to export
|
||||||
# \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
|
# \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
|
||||||
# \param file_url The URL where to save the file.
|
# \param file_url_or_string The URL where to save the file.
|
||||||
#
|
#
|
||||||
# \return A dictionary containing a key "status" with a status code and a key "message" with a message
|
# \return A dictionary containing a key "status" with a status code and a key "message" with a message
|
||||||
# explaining the status.
|
# explaining the status.
|
||||||
# The status code can be one of "error", "cancelled", "success"
|
# The status code can be one of "error", "cancelled", "success"
|
||||||
@pyqtSlot(str, str, QUrl, result = "QVariantMap")
|
@pyqtSlot(str, str, QUrl, result = "QVariantMap")
|
||||||
def exportContainer(self, container_id, file_type, file_url):
|
def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
|
||||||
if not container_id or not file_type or not file_url:
|
if not container_id or not file_type or not file_url_or_string:
|
||||||
return { "status": "error", "message": "Invalid arguments"}
|
return { "status": "error", "message": "Invalid arguments"}
|
||||||
|
|
||||||
if isinstance(file_url, QUrl):
|
if isinstance(file_url_or_string, QUrl):
|
||||||
file_url = file_url.toLocalFile()
|
file_url = file_url_or_string.toLocalFile()
|
||||||
|
else:
|
||||||
|
file_url = file_url_or_string
|
||||||
|
|
||||||
if not file_url:
|
if not file_url:
|
||||||
return { "status": "error", "message": "Invalid path"}
|
return { "status": "error", "message": "Invalid path"}
|
||||||
|
|
@ -319,7 +417,7 @@ class ContainerManager(QObject):
|
||||||
mime_type = None
|
mime_type = None
|
||||||
if not file_type in self._container_name_filters:
|
if not file_type in self._container_name_filters:
|
||||||
try:
|
try:
|
||||||
mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
|
||||||
except MimeTypeNotFoundError:
|
except MimeTypeNotFoundError:
|
||||||
return { "status": "error", "message": "Unknown File Type" }
|
return { "status": "error", "message": "Unknown File Type" }
|
||||||
else:
|
else:
|
||||||
|
|
@ -330,7 +428,7 @@ class ContainerManager(QObject):
|
||||||
return { "status": "error", "message": "Container not found"}
|
return { "status": "error", "message": "Container not found"}
|
||||||
container = containers[0]
|
container = containers[0]
|
||||||
|
|
||||||
if UM.Platform.isOSX() and "." in file_url:
|
if Platform.isOSX() and "." in file_url:
|
||||||
file_url = file_url[:file_url.rfind(".")]
|
file_url = file_url[:file_url.rfind(".")]
|
||||||
|
|
||||||
for suffix in mime_type.suffixes:
|
for suffix in mime_type.suffixes:
|
||||||
|
|
@ -339,10 +437,10 @@ class ContainerManager(QObject):
|
||||||
else:
|
else:
|
||||||
file_url += "." + mime_type.preferredSuffix
|
file_url += "." + mime_type.preferredSuffix
|
||||||
|
|
||||||
if not UM.Platform.isWindows():
|
if not Platform.isWindows():
|
||||||
if os.path.exists(file_url):
|
if os.path.exists(file_url):
|
||||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||||
catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
|
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
|
||||||
if result == QMessageBox.No:
|
if result == QMessageBox.No:
|
||||||
return { "status": "cancelled", "message": "User cancelled"}
|
return { "status": "cancelled", "message": "User cancelled"}
|
||||||
|
|
||||||
|
|
@ -354,7 +452,7 @@ class ContainerManager(QObject):
|
||||||
if contents is None:
|
if contents is None:
|
||||||
return {"status": "error", "message": "Serialization returned None. Unable to write to file"}
|
return {"status": "error", "message": "Serialization returned None. Unable to write to file"}
|
||||||
|
|
||||||
with UM.SaveFile(file_url, "w") as f:
|
with SaveFile(file_url, "w") as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
|
|
||||||
return { "status": "success", "message": "Succesfully exported container", "path": file_url}
|
return { "status": "success", "message": "Succesfully exported container", "path": file_url}
|
||||||
|
|
@ -366,22 +464,24 @@ class ContainerManager(QObject):
|
||||||
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
|
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
|
||||||
# containing a message for the user
|
# containing a message for the user
|
||||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||||
def importContainer(self, file_url):
|
def importContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]:
|
||||||
if not file_url:
|
if not file_url_or_string:
|
||||||
return { "status": "error", "message": "Invalid path"}
|
return { "status": "error", "message": "Invalid path"}
|
||||||
|
|
||||||
if isinstance(file_url, QUrl):
|
if isinstance(file_url_or_string, QUrl):
|
||||||
file_url = file_url.toLocalFile()
|
file_url = file_url_or_string.toLocalFile()
|
||||||
|
else:
|
||||||
|
file_url = file_url_or_string
|
||||||
|
|
||||||
if not file_url or not os.path.exists(file_url):
|
if not file_url or not os.path.exists(file_url):
|
||||||
return { "status": "error", "message": "Invalid path" }
|
return { "status": "error", "message": "Invalid path" }
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_url)
|
||||||
except MimeTypeNotFoundError:
|
except MimeTypeNotFoundError:
|
||||||
return { "status": "error", "message": "Could not determine mime type of file" }
|
return { "status": "error", "message": "Could not determine mime type of file" }
|
||||||
|
|
||||||
container_type = UM.Settings.ContainerRegistry.getContainerForMimeType(mime_type)
|
container_type = self._container_registry.getContainerForMimeType(mime_type)
|
||||||
if not container_type:
|
if not container_type:
|
||||||
return { "status": "error", "message": "Could not find a container to handle the specified file."}
|
return { "status": "error", "message": "Could not find a container to handle the specified file."}
|
||||||
|
|
||||||
|
|
@ -410,17 +510,17 @@ class ContainerManager(QObject):
|
||||||
# \return \type{bool} True if successful, False if not.
|
# \return \type{bool} True if successful, False if not.
|
||||||
@pyqtSlot(result = bool)
|
@pyqtSlot(result = bool)
|
||||||
def updateQualityChanges(self):
|
def updateQualityChanges(self):
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._machine_manager.blurSettings.emit()
|
self._machine_manager.blurSettings.emit()
|
||||||
|
|
||||||
for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||||
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
||||||
quality_changes = stack.findContainer(type = "quality_changes")
|
quality_changes = stack.qualityChanges
|
||||||
if not quality_changes or quality_changes.isReadOnly():
|
if not quality_changes or quality_changes.isReadOnly():
|
||||||
UM.Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._performMerge(quality_changes, stack.getTop())
|
self._performMerge(quality_changes, stack.getTop())
|
||||||
|
|
@ -431,13 +531,13 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
## Clear the top-most (user) containers of the active stacks.
|
## Clear the top-most (user) containers of the active stacks.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def clearUserContainers(self):
|
def clearUserContainers(self) -> None:
|
||||||
self._machine_manager.blurSettings.emit()
|
self._machine_manager.blurSettings.emit()
|
||||||
|
|
||||||
send_emits_containers = []
|
send_emits_containers = []
|
||||||
|
|
||||||
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
||||||
for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||||
container = stack.getTop()
|
container = stack.getTop()
|
||||||
container.clear()
|
container.clear()
|
||||||
send_emits_containers.append(container)
|
send_emits_containers.append(container)
|
||||||
|
|
@ -454,13 +554,13 @@ class ContainerManager(QObject):
|
||||||
# \return \type{bool} True if the operation was successfully, False if not.
|
# \return \type{bool} True if the operation was successfully, False if not.
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def createQualityChanges(self, base_name):
|
def createQualityChanges(self, base_name):
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
active_quality_name = self._machine_manager.activeQualityName
|
active_quality_name = self._machine_manager.activeQualityName
|
||||||
if active_quality_name == "":
|
if active_quality_name == "":
|
||||||
UM.Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
|
Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._machine_manager.blurSettings.emit()
|
self._machine_manager.blurSettings.emit()
|
||||||
|
|
@ -469,17 +569,17 @@ class ContainerManager(QObject):
|
||||||
unique_name = self._container_registry.uniqueName(base_name)
|
unique_name = self._container_registry.uniqueName(base_name)
|
||||||
|
|
||||||
# Go through the active stacks and create quality_changes containers from the user containers.
|
# Go through the active stacks and create quality_changes containers from the user containers.
|
||||||
for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||||
user_container = stack.getTop()
|
user_container = stack.getTop()
|
||||||
quality_container = stack.findContainer(type = "quality")
|
quality_container = stack.quality
|
||||||
quality_changes_container = stack.findContainer(type = "quality_changes")
|
quality_changes_container = stack.qualityChanges
|
||||||
if not quality_container or not quality_changes_container:
|
if not quality_container or not quality_changes_container:
|
||||||
UM.Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
extruder_id = None if stack is global_stack else QualityManager.getInstance().getParentMachineDefinition(stack.getBottom()).getId()
|
extruder_id = None if stack is global_stack else QualityManager.getInstance().getParentMachineDefinition(stack.getBottom()).getId()
|
||||||
new_changes = self._createQualityChanges(quality_container, unique_name,
|
new_changes = self._createQualityChanges(quality_container, unique_name,
|
||||||
UM.Application.getInstance().getGlobalContainerStack().getBottom(),
|
Application.getInstance().getGlobalContainerStack().getBottom(),
|
||||||
extruder_id)
|
extruder_id)
|
||||||
self._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
self._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
||||||
self._performMerge(new_changes, user_container)
|
self._performMerge(new_changes, user_container)
|
||||||
|
|
@ -501,7 +601,7 @@ class ContainerManager(QObject):
|
||||||
# \return \type{bool} True if successful, False if not.
|
# \return \type{bool} True if successful, False if not.
|
||||||
@pyqtSlot(str, result = bool)
|
@pyqtSlot(str, result = bool)
|
||||||
def removeQualityChanges(self, quality_name):
|
def removeQualityChanges(self, quality_name):
|
||||||
UM.Logger.log("d", "Attempting to remove the quality change containers with name %s", quality_name)
|
Logger.log("d", "Attempting to remove the quality change containers with name %s", quality_name)
|
||||||
containers_found = False
|
containers_found = False
|
||||||
|
|
||||||
if not quality_name:
|
if not quality_name:
|
||||||
|
|
@ -511,10 +611,10 @@ class ContainerManager(QObject):
|
||||||
activate_quality = quality_name == self._machine_manager.activeQualityName
|
activate_quality = quality_name == self._machine_manager.activeQualityName
|
||||||
activate_quality_type = None
|
activate_quality_type = None
|
||||||
|
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack or not quality_name:
|
if not global_stack or not quality_name:
|
||||||
return ""
|
return ""
|
||||||
machine_definition = global_stack.getBottom()
|
machine_definition = QualityManager.getInstance().getParentMachineDefinition(global_stack.getBottom())
|
||||||
|
|
||||||
for container in QualityManager.getInstance().findQualityChangesByName(quality_name, machine_definition):
|
for container in QualityManager.getInstance().findQualityChangesByName(quality_name, machine_definition):
|
||||||
containers_found = True
|
containers_found = True
|
||||||
|
|
@ -523,7 +623,7 @@ class ContainerManager(QObject):
|
||||||
self._container_registry.removeContainer(container.getId())
|
self._container_registry.removeContainer(container.getId())
|
||||||
|
|
||||||
if not containers_found:
|
if not containers_found:
|
||||||
UM.Logger.log("d", "Unable to remove quality containers, as we did not find any by the name of %s", quality_name)
|
Logger.log("d", "Unable to remove quality containers, as we did not find any by the name of %s", quality_name)
|
||||||
|
|
||||||
elif activate_quality:
|
elif activate_quality:
|
||||||
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
|
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
|
||||||
|
|
@ -546,15 +646,15 @@ class ContainerManager(QObject):
|
||||||
# \return True if successful, False if not.
|
# \return True if successful, False if not.
|
||||||
@pyqtSlot(str, str, result = bool)
|
@pyqtSlot(str, str, result = bool)
|
||||||
def renameQualityChanges(self, quality_name, new_name):
|
def renameQualityChanges(self, quality_name, new_name):
|
||||||
UM.Logger.log("d", "User requested QualityChanges container rename of %s to %s", quality_name, new_name)
|
Logger.log("d", "User requested QualityChanges container rename of %s to %s", quality_name, new_name)
|
||||||
if not quality_name or not new_name:
|
if not quality_name or not new_name:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if quality_name == new_name:
|
if quality_name == new_name:
|
||||||
UM.Logger.log("w", "Unable to rename %s to %s, because they are the same.", quality_name, new_name)
|
Logger.log("w", "Unable to rename %s to %s, because they are the same.", quality_name, new_name)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -571,7 +671,7 @@ class ContainerManager(QObject):
|
||||||
container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
|
container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
|
||||||
|
|
||||||
if not containers_to_rename:
|
if not containers_to_rename:
|
||||||
UM.Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
|
Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
|
||||||
|
|
||||||
self._machine_manager.activeQualityChanged.emit()
|
self._machine_manager.activeQualityChanged.emit()
|
||||||
return True
|
return True
|
||||||
|
|
@ -587,13 +687,13 @@ class ContainerManager(QObject):
|
||||||
# \return A string containing the name of the duplicated containers, or an empty string if it failed.
|
# \return A string containing the name of the duplicated containers, or an empty string if it failed.
|
||||||
@pyqtSlot(str, str, result = str)
|
@pyqtSlot(str, str, result = str)
|
||||||
def duplicateQualityOrQualityChanges(self, quality_name, base_name):
|
def duplicateQualityOrQualityChanges(self, quality_name, base_name):
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack or not quality_name:
|
if not global_stack or not quality_name:
|
||||||
return ""
|
return ""
|
||||||
machine_definition = global_stack.getBottom()
|
machine_definition = global_stack.getBottom()
|
||||||
|
|
||||||
active_stacks = cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
material_containers = [stack.findContainer(type="material") for stack in active_stacks]
|
material_containers = [stack.material for stack in active_stacks]
|
||||||
|
|
||||||
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
|
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
|
||||||
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
|
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
|
||||||
|
|
@ -608,16 +708,16 @@ class ContainerManager(QObject):
|
||||||
# \param material_instances \type{List[InstanceContainer]}
|
# \param material_instances \type{List[InstanceContainer]}
|
||||||
# \return \type{str} the name of the newly created container.
|
# \return \type{str} the name of the newly created container.
|
||||||
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
|
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
|
||||||
UM.Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
|
Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
|
||||||
|
|
||||||
if base_name is None:
|
if base_name is None:
|
||||||
base_name = quality_name
|
base_name = quality_name
|
||||||
# Try to find a Quality with the name.
|
# Try to find a Quality with the name.
|
||||||
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
|
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
|
||||||
if container:
|
if container:
|
||||||
UM.Logger.log("d", "We found a quality to duplicate.")
|
Logger.log("d", "We found a quality to duplicate.")
|
||||||
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
|
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
|
||||||
UM.Logger.log("d", "We found a quality_changes to duplicate.")
|
Logger.log("d", "We found a quality_changes to duplicate.")
|
||||||
# Assume it is a quality changes.
|
# Assume it is a quality changes.
|
||||||
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
|
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
|
||||||
|
|
||||||
|
|
@ -660,15 +760,18 @@ class ContainerManager(QObject):
|
||||||
|
|
||||||
return new_change_instances
|
return new_change_instances
|
||||||
|
|
||||||
|
## Create a duplicate of a material, which has the same GUID and base_file metadata
|
||||||
|
#
|
||||||
|
# \return \type{str} the id of the newly created container.
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def duplicateMaterial(self, material_id):
|
def duplicateMaterial(self, material_id: str) -> str:
|
||||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
|
Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Ensure all settings are saved.
|
# Ensure all settings are saved.
|
||||||
UM.Application.getInstance().saveSettings()
|
Application.getInstance().saveSettings()
|
||||||
|
|
||||||
# Create a new ID & container to hold the data.
|
# Create a new ID & container to hold the data.
|
||||||
new_id = self._container_registry.uniqueName(material_id)
|
new_id = self._container_registry.uniqueName(material_id)
|
||||||
|
|
@ -682,16 +785,127 @@ class ContainerManager(QObject):
|
||||||
duplicated_container.deserialize(f.read())
|
duplicated_container.deserialize(f.read())
|
||||||
duplicated_container.setDirty(True)
|
duplicated_container.setDirty(True)
|
||||||
self._container_registry.addContainer(duplicated_container)
|
self._container_registry.addContainer(duplicated_container)
|
||||||
|
return self._getMaterialContainerIdForActiveMachine(new_id)
|
||||||
|
|
||||||
|
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
|
||||||
|
#
|
||||||
|
# \return \type{str} the id of the newly created container.
|
||||||
|
@pyqtSlot(result = str)
|
||||||
|
def createMaterial(self) -> str:
|
||||||
|
# Ensure all settings are saved.
|
||||||
|
Application.getInstance().saveSettings()
|
||||||
|
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
|
||||||
|
containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
base_file = containers[0].getMetaDataEntry("base_file")
|
||||||
|
containers = self._container_registry.findInstanceContainers(id = base_file)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Create a new ID & container to hold the data.
|
||||||
|
new_id = self._container_registry.uniqueName("custom_material")
|
||||||
|
container_type = type(containers[0]) # Always XMLMaterialProfile, since we specifically clone the base_file
|
||||||
|
duplicated_container = container_type(new_id)
|
||||||
|
|
||||||
|
# Instead of duplicating we load the data from the basefile again.
|
||||||
|
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
|
||||||
|
# are also correctly created.
|
||||||
|
with open(containers[0].getPath(), encoding="utf-8") as f:
|
||||||
|
duplicated_container.deserialize(f.read())
|
||||||
|
|
||||||
|
duplicated_container.setMetaDataEntry("GUID", str(uuid.uuid4()))
|
||||||
|
duplicated_container.setMetaDataEntry("brand", catalog.i18nc("@label", "Custom"))
|
||||||
|
# We're defaulting to PLA, as machines with material profiles don't like material types they don't know.
|
||||||
|
# TODO: This is a hack, the only reason this is in now is to bandaid the problem as we're close to a release!
|
||||||
|
duplicated_container.setMetaDataEntry("material", "PLA")
|
||||||
|
duplicated_container.setName(catalog.i18nc("@label", "Custom Material"))
|
||||||
|
|
||||||
|
self._container_registry.addContainer(duplicated_container)
|
||||||
|
return self._getMaterialContainerIdForActiveMachine(new_id)
|
||||||
|
|
||||||
|
## Find the id of a material container based on the new material
|
||||||
|
# Utilty function that is shared between duplicateMaterial and createMaterial
|
||||||
|
#
|
||||||
|
# \param base_file \type{str} the id of the created container.
|
||||||
|
def _getMaterialContainerIdForActiveMachine(self, base_file):
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return base_file
|
||||||
|
|
||||||
|
has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False))
|
||||||
|
has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False))
|
||||||
|
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
|
||||||
|
if has_machine_materials or has_variant_materials:
|
||||||
|
if has_variants:
|
||||||
|
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
|
||||||
|
else:
|
||||||
|
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
|
||||||
|
|
||||||
|
if materials:
|
||||||
|
return materials[0].getId()
|
||||||
|
|
||||||
|
Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file)
|
||||||
|
return "" # do not activate a new material if a container can not be found
|
||||||
|
|
||||||
|
return base_file
|
||||||
|
|
||||||
|
## Get a list of materials that have the same GUID as the reference material
|
||||||
|
#
|
||||||
|
# \param material_id \type{str} the id of the material for which to get the linked materials.
|
||||||
|
# \return \type{list} a list of names of materials with the same GUID
|
||||||
|
@pyqtSlot(str, result = "QStringList")
|
||||||
|
def getLinkedMaterials(self, material_id: str):
|
||||||
|
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
|
||||||
|
return []
|
||||||
|
|
||||||
|
material_container = containers[0]
|
||||||
|
material_base_file = material_container.getMetaDataEntry("base_file", "")
|
||||||
|
material_guid = material_container.getMetaDataEntry("GUID", "")
|
||||||
|
if not material_guid:
|
||||||
|
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
|
||||||
|
return []
|
||||||
|
|
||||||
|
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid)
|
||||||
|
linked_material_names = []
|
||||||
|
for container in containers:
|
||||||
|
if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId():
|
||||||
|
continue
|
||||||
|
|
||||||
|
linked_material_names.append(container.getName())
|
||||||
|
return linked_material_names
|
||||||
|
|
||||||
|
## Unlink a material from all other materials by creating a new GUID
|
||||||
|
# \param material_id \type{str} the id of the material to create a new GUID for.
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def unlinkMaterial(self, material_id: str):
|
||||||
|
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
containers[0].setMetaDataEntry("GUID", str(uuid.uuid4()))
|
||||||
|
|
||||||
|
|
||||||
## Get the singleton instance for this class.
|
## Get the singleton instance for this class.
|
||||||
@classmethod
|
@classmethod
|
||||||
def getInstance(cls):
|
def getInstance(cls) -> "ContainerManager":
|
||||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||||
if ContainerManager.__instance is None:
|
if ContainerManager.__instance is None:
|
||||||
ContainerManager.__instance = cls()
|
ContainerManager.__instance = cls()
|
||||||
return ContainerManager.__instance
|
return ContainerManager.__instance
|
||||||
|
|
||||||
__instance = None
|
__instance = None # type: "ContainerManager"
|
||||||
|
|
||||||
# Factory function, used by QML
|
# Factory function, used by QML
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -710,16 +924,16 @@ class ContainerManager(QObject):
|
||||||
if clear_settings:
|
if clear_settings:
|
||||||
merge.clear()
|
merge.clear()
|
||||||
|
|
||||||
def _updateContainerNameFilters(self):
|
def _updateContainerNameFilters(self) -> None:
|
||||||
self._container_name_filters = {}
|
self._container_name_filters = {}
|
||||||
for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
|
for plugin_id, container_type in self._container_registry.getContainerTypes():
|
||||||
# Ignore default container types since those are not plugins
|
# Ignore default container types since those are not plugins
|
||||||
if container_type in (UM.Settings.InstanceContainer, UM.Settings.ContainerStack, UM.Settings.DefinitionContainer):
|
if container_type in (InstanceContainer, ContainerStack, DefinitionContainer):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
serialize_type = ""
|
serialize_type = ""
|
||||||
try:
|
try:
|
||||||
plugin_metadata = UM.PluginRegistry.getInstance().getMetaData(plugin_id)
|
plugin_metadata = PluginRegistry.getInstance().getMetaData(plugin_id)
|
||||||
if plugin_metadata:
|
if plugin_metadata:
|
||||||
serialize_type = plugin_metadata["settings_container"]["type"]
|
serialize_type = plugin_metadata["settings_container"]["type"]
|
||||||
else:
|
else:
|
||||||
|
|
@ -727,7 +941,7 @@ class ContainerManager(QObject):
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mime_type = UM.Settings.ContainerRegistry.getMimeTypeForContainer(container_type)
|
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||||
|
|
||||||
entry = {
|
entry = {
|
||||||
"type": serialize_type,
|
"type": serialize_type,
|
||||||
|
|
@ -736,7 +950,7 @@ class ContainerManager(QObject):
|
||||||
}
|
}
|
||||||
|
|
||||||
suffix = mime_type.preferredSuffix
|
suffix = mime_type.preferredSuffix
|
||||||
if UM.Platform.isOSX() and "." in suffix:
|
if Platform.isOSX() and "." in suffix:
|
||||||
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
||||||
suffix = suffix[suffix.index(".") + 1:]
|
suffix = suffix[suffix.index(".") + 1:]
|
||||||
|
|
||||||
|
|
@ -745,7 +959,7 @@ class ContainerManager(QObject):
|
||||||
if suffix == mime_type.preferredSuffix:
|
if suffix == mime_type.preferredSuffix:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if UM.Platform.isOSX() and "." in suffix:
|
if Platform.isOSX() and "." in suffix:
|
||||||
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
||||||
suffix = suffix[suffix.index("."):]
|
suffix = suffix[suffix.index("."):]
|
||||||
|
|
||||||
|
|
@ -790,7 +1004,7 @@ class ContainerManager(QObject):
|
||||||
base_id = machine_definition.getId() if extruder_id is None else extruder_id
|
base_id = machine_definition.getId() if extruder_id is None else extruder_id
|
||||||
|
|
||||||
# Create a new quality_changes container for the quality.
|
# Create a new quality_changes container for the quality.
|
||||||
quality_changes = UM.Settings.InstanceContainer(self._createUniqueId(base_id, new_name))
|
quality_changes = InstanceContainer(self._createUniqueId(base_id, new_name))
|
||||||
quality_changes.setName(new_name)
|
quality_changes.setName(new_name)
|
||||||
quality_changes.addMetaDataEntry("type", "quality_changes")
|
quality_changes.addMetaDataEntry("type", "quality_changes")
|
||||||
quality_changes.addMetaDataEntry("quality_type", quality_container.getMetaDataEntry("quality_type"))
|
quality_changes.addMetaDataEntry("quality_type", quality_container.getMetaDataEntry("quality_type"))
|
||||||
|
|
@ -804,6 +1018,9 @@ class ContainerManager(QObject):
|
||||||
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
|
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
|
||||||
else:
|
else:
|
||||||
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
|
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
return quality_changes
|
return quality_changes
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -812,7 +1029,7 @@ class ContainerManager(QObject):
|
||||||
#
|
#
|
||||||
# \param QVariant<QUrl>, essentially a list with QUrl objects.
|
# \param QVariant<QUrl>, essentially a list with QUrl objects.
|
||||||
# \return Dict with keys status, text
|
# \return Dict with keys status, text
|
||||||
@pyqtSlot(QVariant, result="QVariantMap")
|
@pyqtSlot("QVariantList", result="QVariantMap")
|
||||||
def importProfiles(self, file_urls):
|
def importProfiles(self, file_urls):
|
||||||
status = "ok"
|
status = "ok"
|
||||||
results = {"ok": [], "error": []}
|
results = {"ok": [], "error": []}
|
||||||
|
|
@ -825,7 +1042,7 @@ class ContainerManager(QObject):
|
||||||
if not path.endswith(".curaprofile"):
|
if not path.endswith(".curaprofile"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
single_result = UM.Settings.ContainerRegistry.getInstance().importProfile(path)
|
single_result = self._container_registry.importProfile(path)
|
||||||
if single_result["status"] == "error":
|
if single_result["status"] == "error":
|
||||||
status = "error"
|
status = "error"
|
||||||
results[single_result["status"]].append(single_result["message"])
|
results[single_result["status"]].append(single_result["message"])
|
||||||
|
|
@ -842,13 +1059,13 @@ class ContainerManager(QObject):
|
||||||
path = file_url.toLocalFile()
|
path = file_url.toLocalFile()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
return UM.Settings.ContainerRegistry.getInstance().importProfile(path)
|
return self._container_registry.importProfile(path)
|
||||||
|
|
||||||
@pyqtSlot("QVariantList", QUrl, str)
|
@pyqtSlot("QVariantList", QUrl, str)
|
||||||
def exportProfile(self, instance_id, file_url, file_type):
|
def exportProfile(self, instance_id: str, file_url: QUrl, file_type: str) -> None:
|
||||||
if not file_url.isValid():
|
if not file_url.isValid():
|
||||||
return
|
return
|
||||||
path = file_url.toLocalFile()
|
path = file_url.toLocalFile()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
UM.Settings.ContainerRegistry.getInstance().exportProfile(instance_id, path, file_type)
|
self._container_registry.exportProfile(instance_id, path, file_type)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from UM.Decorators import override
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
@ -15,9 +20,14 @@ from UM.Message import Message
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from . import ExtruderStack
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
from . import GlobalStack
|
||||||
|
from .ContainerManager import ContainerManager
|
||||||
|
from .ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
@ -26,6 +36,29 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
## Overridden from ContainerRegistry
|
||||||
|
#
|
||||||
|
# Adds a container to the registry.
|
||||||
|
#
|
||||||
|
# This will also try to convert a ContainerStack to either Extruder or
|
||||||
|
# Global stack based on metadata information.
|
||||||
|
@override(ContainerRegistry)
|
||||||
|
def addContainer(self, container):
|
||||||
|
|
||||||
|
# Note: Intentional check with type() because we want to ignore subclasses
|
||||||
|
if type(container) == ContainerStack:
|
||||||
|
container = self._convertContainerStack(container)
|
||||||
|
|
||||||
|
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||||
|
# Check against setting version of the definition.
|
||||||
|
required_setting_version = CuraApplication.SettingVersion
|
||||||
|
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||||
|
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))
|
||||||
|
return #Don't add.
|
||||||
|
|
||||||
|
super().addContainer(container)
|
||||||
|
|
||||||
## Create a name that is not empty and unique
|
## Create a name that is not empty and unique
|
||||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||||
# \param current_name \type{} Current name of the container, which may be an acceptable option
|
# \param current_name \type{} Current name of the container, which may be an acceptable option
|
||||||
|
|
@ -80,7 +113,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if not Platform.isWindows():
|
if not Platform.isWindows():
|
||||||
if os.path.exists(file_name):
|
if os.path.exists(file_name):
|
||||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||||
catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
||||||
if result == QMessageBox.No:
|
if result == QMessageBox.No:
|
||||||
return
|
return
|
||||||
found_containers = []
|
found_containers = []
|
||||||
|
|
@ -110,15 +143,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
success = profile_writer.write(file_name, found_containers)
|
success = profile_writer.write(file_name, found_containers)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
||||||
m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)), lifetime = 0)
|
m = Message(catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to export profile to <filename>{0}</filename>: <message>{1}</message>", file_name, str(e)),
|
||||||
|
lifetime = 0,
|
||||||
|
title = catalog.i18nc("@info:title", "Error"))
|
||||||
m.show()
|
m.show()
|
||||||
return
|
return
|
||||||
if not success:
|
if not success:
|
||||||
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
||||||
m = Message(catalog.i18nc("@info:status", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name), lifetime = 0)
|
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
|
||||||
|
lifetime = 0,
|
||||||
|
title = catalog.i18nc("@info:title", "Error"))
|
||||||
m.show()
|
m.show()
|
||||||
return
|
return
|
||||||
m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name))
|
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
|
||||||
|
title = catalog.i18nc("@info:title", "Export succeeded"))
|
||||||
m.show()
|
m.show()
|
||||||
|
|
||||||
## Gets the plugin object matching the criteria
|
## Gets the plugin object matching the criteria
|
||||||
|
|
@ -144,7 +182,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
def importProfile(self, file_name):
|
def importProfile(self, file_name):
|
||||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||||
if not file_name:
|
if not file_name:
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
||||||
|
|
||||||
plugin_registry = PluginRegistry.getInstance()
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
extension = file_name.split(".")[-1]
|
extension = file_name.split(".")[-1]
|
||||||
|
|
@ -166,53 +204,76 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
|
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
|
||||||
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
|
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
|
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, str(e))}
|
||||||
if profile_or_list: # Success!
|
|
||||||
|
if profile_or_list:
|
||||||
name_seed = os.path.splitext(os.path.basename(file_name))[0]
|
name_seed = os.path.splitext(os.path.basename(file_name))[0]
|
||||||
new_name = self.uniqueName(name_seed)
|
new_name = self.uniqueName(name_seed)
|
||||||
if type(profile_or_list) is not list:
|
|
||||||
profile = profile_or_list
|
|
||||||
self._configureProfile(profile, name_seed, new_name)
|
|
||||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
|
|
||||||
else:
|
|
||||||
profile_index = -1
|
|
||||||
global_profile = None
|
|
||||||
|
|
||||||
for profile in profile_or_list:
|
# Ensure it is always a list of profiles
|
||||||
if profile_index >= 0:
|
if type(profile_or_list) is not list:
|
||||||
if len(machine_extruders) > profile_index:
|
profile_or_list = [profile_or_list]
|
||||||
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
|
|
||||||
# Ensure the extruder profiles get non-conflicting names
|
if len(profile_or_list) == 1:
|
||||||
# NB: these are not user-facing
|
# If there is only 1 stack file it means we're loading a legacy (pre-3.1) .curaprofile.
|
||||||
if "extruder" in profile.getMetaData():
|
# In that case we find the per-extruder settings and put those in a new quality_changes container
|
||||||
profile.setMetaDataEntry("extruder", extruder_id)
|
# so that it is compatible with the new stack setup.
|
||||||
|
profile = profile_or_list[0]
|
||||||
|
extruder_stack_quality_changes_container = ContainerManager.getInstance().duplicateContainerInstance(profile)
|
||||||
|
extruder_stack_quality_changes_container.addMetaDataEntry("extruder", "fdmextruder")
|
||||||
|
|
||||||
|
for quality_changes_setting_key in extruder_stack_quality_changes_container.getAllKeys():
|
||||||
|
settable_per_extruder = extruder_stack_quality_changes_container.getProperty(quality_changes_setting_key, "settable_per_extruder")
|
||||||
|
if settable_per_extruder:
|
||||||
|
profile.removeInstance(quality_changes_setting_key, postpone_emit = True)
|
||||||
else:
|
else:
|
||||||
profile.addMetaDataEntry("extruder", extruder_id)
|
extruder_stack_quality_changes_container.removeInstance(quality_changes_setting_key, postpone_emit = True)
|
||||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
|
||||||
elif profile_index == 0:
|
# We add the new container to the profile list so things like extruder positions are taken care of
|
||||||
# Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
|
# in the next code segment.
|
||||||
profile._id = self.uniqueName("temporary_profile")
|
profile_or_list.append(extruder_stack_quality_changes_container)
|
||||||
self.addContainer(profile)
|
|
||||||
ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
|
# Import all profiles
|
||||||
self.removeContainer(profile.getId())
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
break
|
if profile_index == 0:
|
||||||
else:
|
# This is assumed to be the global profile
|
||||||
# The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
global_profile = profile
|
|
||||||
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
self._configureProfile(profile, profile_id, new_name)
|
elif len(machine_extruders) > profile_index:
|
||||||
|
# This is assumed to be an extruder profile
|
||||||
|
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index - 1].getBottom())
|
||||||
|
if not profile.getMetaDataEntry("extruder"):
|
||||||
|
profile.addMetaDataEntry("extruder", extruder_id)
|
||||||
|
else:
|
||||||
|
profile.setMetaDataEntry("extruder", extruder_id)
|
||||||
|
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
profile_index += 1
|
result = self._configureProfile(profile, profile_id, new_name)
|
||||||
|
if result is not None:
|
||||||
|
return {"status": "error", "message": catalog.i18nc(
|
||||||
|
"@info:status Don't translate the XML tags <filename> or <message>!",
|
||||||
|
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
|
||||||
|
file_name, result)}
|
||||||
|
|
||||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
||||||
|
|
||||||
# If it hasn't returned by now, none of the plugins loaded the profile successfully.
|
# If it hasn't returned by now, none of the plugins loaded the profile successfully.
|
||||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
||||||
|
|
||||||
def _configureProfile(self, profile, id_seed, new_name):
|
@override(ContainerRegistry)
|
||||||
|
def load(self):
|
||||||
|
super().load()
|
||||||
|
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||||
|
self._connectUpgradedExtruderStacksToMachines()
|
||||||
|
|
||||||
|
## Update an imported profile to match the current machine configuration.
|
||||||
|
#
|
||||||
|
# \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 new_name The new name for the profile.
|
||||||
|
#
|
||||||
|
# \return None if configuring was successful or an error message if an error occurred.
|
||||||
|
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
|
||||||
profile.setReadOnly(False)
|
profile.setReadOnly(False)
|
||||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||||
|
|
||||||
|
|
@ -225,15 +286,50 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
else:
|
else:
|
||||||
profile.addMetaDataEntry("type", "quality_changes")
|
profile.addMetaDataEntry("type", "quality_changes")
|
||||||
|
|
||||||
|
quality_type = profile.getMetaDataEntry("quality_type")
|
||||||
|
if not quality_type:
|
||||||
|
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||||
|
|
||||||
|
quality_type_criteria = {"quality_type": quality_type}
|
||||||
if self._machineHasOwnQualities():
|
if self._machineHasOwnQualities():
|
||||||
profile.setDefinition(self._activeQualityDefinition())
|
profile.setDefinition(self._activeQualityDefinition())
|
||||||
if self._machineHasOwnMaterials():
|
if self._machineHasOwnMaterials():
|
||||||
profile.addMetaDataEntry("material", self._activeMaterialId())
|
active_material_id = self._activeMaterialId()
|
||||||
|
if active_material_id and active_material_id != "empty": # only update if there is an active material
|
||||||
|
profile.addMetaDataEntry("material", active_material_id)
|
||||||
|
quality_type_criteria["material"] = active_material_id
|
||||||
|
|
||||||
|
quality_type_criteria["definition"] = profile.getDefinition().getId()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
|
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
|
||||||
|
quality_type_criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
|
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
del quality_type_criteria["definition"]
|
||||||
|
|
||||||
|
# materials = None
|
||||||
|
|
||||||
|
if "material" in quality_type_criteria:
|
||||||
|
# materials = ContainerRegistry.getInstance().findInstanceContainers(id = quality_type_criteria["material"])
|
||||||
|
del quality_type_criteria["material"]
|
||||||
|
|
||||||
|
# Do not filter quality containers here with materials because we are trying to import a profile, so it should
|
||||||
|
# NOT be restricted by the active materials on the current machine.
|
||||||
|
materials = None
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# successfully imported but then fail to show up.
|
||||||
|
from cura.QualityManager import QualityManager
|
||||||
|
qualities = QualityManager.getInstance()._getFilteredContainersForStack(machine_definition, materials, **quality_type_criteria)
|
||||||
|
if not qualities:
|
||||||
|
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
||||||
|
|
||||||
ContainerRegistry.getInstance().addContainer(profile)
|
ContainerRegistry.getInstance().addContainer(profile)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
## Gets a list of profile writer plugins
|
## Gets a list of profile writer plugins
|
||||||
# \return List of tuples of (plugin_id, meta_data).
|
# \return List of tuples of (plugin_id, meta_data).
|
||||||
def _getIOPlugins(self, io_type):
|
def _getIOPlugins(self, io_type):
|
||||||
|
|
@ -271,16 +367,181 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# \return the ID of the active material or the empty string
|
# \return the ID of the active material or the empty string
|
||||||
def _activeMaterialId(self):
|
def _activeMaterialId(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack and global_container_stack.material:
|
||||||
material = global_container_stack.findContainer({"type": "material"})
|
return global_container_stack.material.getId()
|
||||||
if material:
|
|
||||||
return material.getId()
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
## Returns true if the current machien requires its own quality profiles
|
## Returns true if the current machine requires its own quality profiles
|
||||||
# \return true if the current machien requires its own quality profiles
|
# \return true if the current machine requires its own quality profiles
|
||||||
def _machineHasOwnQualities(self):
|
def _machineHasOwnQualities(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
||||||
|
def _convertContainerStack(self, container):
|
||||||
|
assert type(container) == ContainerStack
|
||||||
|
|
||||||
|
container_type = container.getMetaDataEntry("type")
|
||||||
|
if container_type not in ("extruder_train", "machine"):
|
||||||
|
# It is not an extruder or machine, so do nothing with the stack
|
||||||
|
return container
|
||||||
|
|
||||||
|
Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type)
|
||||||
|
|
||||||
|
new_stack = None
|
||||||
|
if container_type == "extruder_train":
|
||||||
|
new_stack = ExtruderStack.ExtruderStack(container.getId())
|
||||||
|
else:
|
||||||
|
new_stack = GlobalStack.GlobalStack(container.getId())
|
||||||
|
|
||||||
|
container_contents = container.serialize()
|
||||||
|
new_stack.deserialize(container_contents)
|
||||||
|
|
||||||
|
# Delete the old configuration file so we do not get double stacks
|
||||||
|
if os.path.isfile(container.getPath()):
|
||||||
|
os.remove(container.getPath())
|
||||||
|
|
||||||
|
return new_stack
|
||||||
|
|
||||||
|
def _registerSingleExtrusionMachinesExtruderStacks(self):
|
||||||
|
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
|
||||||
|
for machine in machines:
|
||||||
|
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
|
||||||
|
if not extruder_stacks:
|
||||||
|
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
||||||
|
|
||||||
|
def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id):
|
||||||
|
new_extruder_id = extruder_id
|
||||||
|
|
||||||
|
extruder_definitions = self.findDefinitionContainers(id = new_extruder_id)
|
||||||
|
if not extruder_definitions:
|
||||||
|
Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
extruder_definition = extruder_definitions[0]
|
||||||
|
unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id)
|
||||||
|
|
||||||
|
extruder_stack = ExtruderStack.ExtruderStack(unique_name)
|
||||||
|
extruder_stack.setName(extruder_definition.getName())
|
||||||
|
extruder_stack.setDefinition(extruder_definition)
|
||||||
|
extruder_stack.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
|
||||||
|
extruder_stack.setNextStack(machine)
|
||||||
|
|
||||||
|
# create empty user changes container otherwise
|
||||||
|
user_container = InstanceContainer(extruder_stack.id + "_user")
|
||||||
|
user_container.addMetaDataEntry("type", "user")
|
||||||
|
user_container.addMetaDataEntry("machine", extruder_stack.getId())
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
user_container.setDefinition(machine.definition)
|
||||||
|
|
||||||
|
if machine.userChanges:
|
||||||
|
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
|
||||||
|
# container to the extruder stack.
|
||||||
|
for user_setting_key in machine.userChanges.getAllKeys():
|
||||||
|
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
|
||||||
|
if settable_per_extruder:
|
||||||
|
user_container.addInstance(machine.userChanges.getInstance(user_setting_key))
|
||||||
|
machine.userChanges.removeInstance(user_setting_key, postpone_emit = True)
|
||||||
|
|
||||||
|
extruder_stack.setUserChanges(user_container)
|
||||||
|
self.addContainer(user_container)
|
||||||
|
|
||||||
|
variant_id = "default"
|
||||||
|
if machine.variant.getId() not in ("empty", "empty_variant"):
|
||||||
|
variant_id = machine.variant.getId()
|
||||||
|
else:
|
||||||
|
variant_id = "empty_variant"
|
||||||
|
extruder_stack.setVariantById(variant_id)
|
||||||
|
|
||||||
|
material_id = "default"
|
||||||
|
if machine.material.getId() not in ("empty", "empty_material"):
|
||||||
|
material_id = machine.material.getId()
|
||||||
|
else:
|
||||||
|
material_id = "empty_material"
|
||||||
|
extruder_stack.setMaterialById(material_id)
|
||||||
|
|
||||||
|
quality_id = "default"
|
||||||
|
if machine.quality.getId() not in ("empty", "empty_quality"):
|
||||||
|
quality_id = machine.quality.getId()
|
||||||
|
else:
|
||||||
|
quality_id = "empty_quality"
|
||||||
|
extruder_stack.setQualityById(quality_id)
|
||||||
|
|
||||||
|
if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"):
|
||||||
|
extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id)
|
||||||
|
if extruder_quality_changes_container:
|
||||||
|
extruder_quality_changes_container = extruder_quality_changes_container[0]
|
||||||
|
quality_changes_id = extruder_quality_changes_container.getId()
|
||||||
|
extruder_stack.setQualityChangesById(quality_changes_id)
|
||||||
|
else:
|
||||||
|
# Some extruder quality_changes containers can be created at runtime as files in the qualities
|
||||||
|
# folder. Those files won't be loaded in the registry immediately. So we also need to search
|
||||||
|
# the folder to see if the quality_changes exists.
|
||||||
|
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName())
|
||||||
|
if extruder_quality_changes_container:
|
||||||
|
quality_changes_id = extruder_quality_changes_container.getId()
|
||||||
|
extruder_stack.setQualityChangesById(quality_changes_id)
|
||||||
|
|
||||||
|
if not extruder_quality_changes_container:
|
||||||
|
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
|
||||||
|
machine.qualityChanges.getName(), extruder_stack.getId())
|
||||||
|
else:
|
||||||
|
extruder_stack.setQualityChangesById("empty_quality_changes")
|
||||||
|
|
||||||
|
self.addContainer(extruder_stack)
|
||||||
|
|
||||||
|
return extruder_stack
|
||||||
|
|
||||||
|
def _findQualityChangesContainerInCuraFolder(self, name):
|
||||||
|
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
||||||
|
|
||||||
|
instance_container = None
|
||||||
|
|
||||||
|
for item in os.listdir(quality_changes_dir):
|
||||||
|
file_path = os.path.join(quality_changes_dir, item)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
continue
|
||||||
|
|
||||||
|
parser = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
parser.read([file_path])
|
||||||
|
except:
|
||||||
|
# skip, it is not a valid stack file
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not parser.has_option("general", "name"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if parser["general"]["name"] == name:
|
||||||
|
# load the container
|
||||||
|
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
|
||||||
|
|
||||||
|
instance_container = InstanceContainer(container_id)
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
serialized = f.read()
|
||||||
|
instance_container.deserialize(serialized, file_path)
|
||||||
|
self.addContainer(instance_container)
|
||||||
|
break
|
||||||
|
|
||||||
|
return instance_container
|
||||||
|
|
||||||
|
# Fix the extruders that were upgraded to ExtruderStack instances during addContainer.
|
||||||
|
# The stacks are now responsible for setting the next stack on deserialize. However,
|
||||||
|
# due to problems with loading order, some stacks may not have the proper next stack
|
||||||
|
# set after upgrading, because the proper global stack was not yet loaded. This method
|
||||||
|
# makes sure those extruders also get the right stack set.
|
||||||
|
def _connectUpgradedExtruderStacksToMachines(self):
|
||||||
|
extruder_stacks = self.findContainers(ExtruderStack.ExtruderStack)
|
||||||
|
for extruder_stack in extruder_stacks:
|
||||||
|
if extruder_stack.getNextStack():
|
||||||
|
# Has the right next stack, so ignore it.
|
||||||
|
continue
|
||||||
|
|
||||||
|
machines = ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack.getMetaDataEntry("machine", ""))
|
||||||
|
if machines:
|
||||||
|
extruder_stack.setNextStack(machines[0])
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
|
||||||
|
|
|
||||||
636
cura/Settings/CuraContainerStack.py
Executable file
636
cura/Settings/CuraContainerStack.py
Executable file
|
|
@ -0,0 +1,636 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
|
from UM.Decorators import override
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
|
|
||||||
|
from . import Exceptions
|
||||||
|
|
||||||
|
|
||||||
|
## Base class for Cura related stacks that want to enforce certain containers are available.
|
||||||
|
#
|
||||||
|
# This class makes sure that the stack has the following containers set: user changes, quality
|
||||||
|
# changes, quality, material, variant, definition changes and finally definition. Initially,
|
||||||
|
# these will be equal to the empty instance container.
|
||||||
|
#
|
||||||
|
# The container types are determined based on the following criteria:
|
||||||
|
# - user: An InstanceContainer with the metadata entry "type" set to "user".
|
||||||
|
# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes".
|
||||||
|
# - quality: An InstanceContainer with the metadata entry "type" set to "quality".
|
||||||
|
# - material: An InstanceContainer with the metadata entry "type" set to "material".
|
||||||
|
# - variant: An InstanceContainer with the metadata entry "type" set to "variant".
|
||||||
|
# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes".
|
||||||
|
# - definition: A DefinitionContainer.
|
||||||
|
#
|
||||||
|
# Internally, this class ensures the mentioned containers are always there and kept in a specific order.
|
||||||
|
# This also means that operations on the stack that modifies the container ordering is prohibited and
|
||||||
|
# will raise an exception.
|
||||||
|
class CuraContainerStack(ContainerStack):
|
||||||
|
def __init__(self, container_id: str, *args, **kwargs):
|
||||||
|
super().__init__(container_id, *args, **kwargs)
|
||||||
|
|
||||||
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
self._empty_instance_container = self._container_registry.getEmptyInstanceContainer()
|
||||||
|
|
||||||
|
self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
|
||||||
|
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
|
||||||
|
self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
|
||||||
|
self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0]
|
||||||
|
|
||||||
|
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))]
|
||||||
|
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
|
||||||
|
self._containers[_ContainerIndexes.Quality] = self._empty_quality
|
||||||
|
self._containers[_ContainerIndexes.Material] = self._empty_material
|
||||||
|
self._containers[_ContainerIndexes.Variant] = self._empty_variant
|
||||||
|
|
||||||
|
self.containersChanged.connect(self._onContainersChanged)
|
||||||
|
|
||||||
|
import cura.CuraApplication #Here to prevent circular imports.
|
||||||
|
self.addMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion)
|
||||||
|
|
||||||
|
# This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted.
|
||||||
|
pyqtContainersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
## Set the user changes container.
|
||||||
|
#
|
||||||
|
# \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user".
|
||||||
|
def setUserChanges(self, new_user_changes: InstanceContainer) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes)
|
||||||
|
|
||||||
|
## Get the user changes container.
|
||||||
|
#
|
||||||
|
# \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged)
|
||||||
|
def userChanges(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.UserChanges]
|
||||||
|
|
||||||
|
## Set the quality changes container.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
|
def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
|
||||||
|
|
||||||
|
## Set the quality changes container by an ID.
|
||||||
|
#
|
||||||
|
# This will search for the specified container and set it. If no container was found, an error will be raised.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes_id The ID of the new quality changes container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setQualityChangesById(self, new_quality_changes_id: str) -> None:
|
||||||
|
quality_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_changes_id)
|
||||||
|
if quality_changes:
|
||||||
|
self.setQualityChanges(quality_changes[0])
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_changes_id))
|
||||||
|
|
||||||
|
## Get the quality changes container.
|
||||||
|
#
|
||||||
|
# \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged)
|
||||||
|
def qualityChanges(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.QualityChanges]
|
||||||
|
|
||||||
|
## Set the quality container.
|
||||||
|
#
|
||||||
|
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||||
|
def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
|
||||||
|
|
||||||
|
## Set the quality container by an ID.
|
||||||
|
#
|
||||||
|
# This will search for the specified container and set it. If no container was found, an error will be raised.
|
||||||
|
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
|
||||||
|
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultQuality
|
||||||
|
# for details.
|
||||||
|
#
|
||||||
|
# \param new_quality_id The ID of the new quality container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setQualityById(self, new_quality_id: str) -> None:
|
||||||
|
quality = self._empty_quality
|
||||||
|
if new_quality_id == "default":
|
||||||
|
new_quality = self.findDefaultQuality()
|
||||||
|
if new_quality:
|
||||||
|
quality = new_quality
|
||||||
|
else:
|
||||||
|
qualities = ContainerRegistry.getInstance().findInstanceContainers(id = new_quality_id)
|
||||||
|
if qualities:
|
||||||
|
quality = qualities[0]
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_quality_id))
|
||||||
|
|
||||||
|
self.setQuality(quality)
|
||||||
|
|
||||||
|
## Get the quality container.
|
||||||
|
#
|
||||||
|
# \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged)
|
||||||
|
def quality(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.Quality]
|
||||||
|
|
||||||
|
## Set the material container.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes The new material container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
|
def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
|
||||||
|
|
||||||
|
## Set the material container by an ID.
|
||||||
|
#
|
||||||
|
# This will search for the specified container and set it. If no container was found, an error will be raised.
|
||||||
|
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
|
||||||
|
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultMaterial
|
||||||
|
# for details.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes_id The ID of the new material container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setMaterialById(self, new_material_id: str) -> None:
|
||||||
|
material = self._empty_material
|
||||||
|
if new_material_id == "default":
|
||||||
|
new_material = self.findDefaultMaterial()
|
||||||
|
if new_material:
|
||||||
|
material = new_material
|
||||||
|
else:
|
||||||
|
materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
|
||||||
|
if materials:
|
||||||
|
material = materials[0]
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_material_id))
|
||||||
|
|
||||||
|
self.setMaterial(material)
|
||||||
|
|
||||||
|
## Get the material container.
|
||||||
|
#
|
||||||
|
# \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged)
|
||||||
|
def material(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.Material]
|
||||||
|
|
||||||
|
## Set the variant container.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes The new variant container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
|
def setVariant(self, new_variant: InstanceContainer) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.Variant, new_variant)
|
||||||
|
|
||||||
|
## Set the variant container by an ID.
|
||||||
|
#
|
||||||
|
# This will search for the specified container and set it. If no container was found, an error will be raised.
|
||||||
|
# There is a special value for ID, which is "default". The "default" value indicates the quality should be set
|
||||||
|
# to whatever the machine definition specifies as "preferred" container, or a fallback value. See findDefaultVariant
|
||||||
|
# for details.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes_id The ID of the new variant container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setVariantById(self, new_variant_id: str) -> None:
|
||||||
|
variant = self._empty_variant
|
||||||
|
if new_variant_id == "default":
|
||||||
|
new_variant = self.findDefaultVariant()
|
||||||
|
if new_variant:
|
||||||
|
variant = new_variant
|
||||||
|
else:
|
||||||
|
variants = ContainerRegistry.getInstance().findInstanceContainers(id = new_variant_id)
|
||||||
|
if variants:
|
||||||
|
variant = variants[0]
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_variant_id))
|
||||||
|
|
||||||
|
self.setVariant(variant)
|
||||||
|
|
||||||
|
## Get the variant container.
|
||||||
|
#
|
||||||
|
# \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged)
|
||||||
|
def variant(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.Variant]
|
||||||
|
|
||||||
|
## Set the definition changes container.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
|
def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes)
|
||||||
|
|
||||||
|
## Set the definition changes container by an ID.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes_id The ID of the new definition changes container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setDefinitionChangesById(self, new_definition_changes_id: str) -> None:
|
||||||
|
new_definition_changes = ContainerRegistry.getInstance().findInstanceContainers(id = new_definition_changes_id)
|
||||||
|
if new_definition_changes:
|
||||||
|
self.setDefinitionChanges(new_definition_changes[0])
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_changes_id))
|
||||||
|
|
||||||
|
## Get the definition changes container.
|
||||||
|
#
|
||||||
|
# \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged)
|
||||||
|
def definitionChanges(self) -> InstanceContainer:
|
||||||
|
return self._containers[_ContainerIndexes.DefinitionChanges]
|
||||||
|
|
||||||
|
## Set the definition container.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes The new definition container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||||
|
def setDefinition(self, new_definition: DefinitionContainer) -> None:
|
||||||
|
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
|
||||||
|
|
||||||
|
## Set the definition container by an ID.
|
||||||
|
#
|
||||||
|
# \param new_quality_changes_id The ID of the new definition container.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID.
|
||||||
|
def setDefinitionById(self, new_definition_id: str) -> None:
|
||||||
|
new_definition = ContainerRegistry.getInstance().findDefinitionContainers(id = new_definition_id)
|
||||||
|
if new_definition:
|
||||||
|
self.setDefinition(new_definition[0])
|
||||||
|
else:
|
||||||
|
raise Exceptions.InvalidContainerError("Could not find container with id {id}".format(id = new_definition_id))
|
||||||
|
|
||||||
|
## Get the definition container.
|
||||||
|
#
|
||||||
|
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||||
|
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
||||||
|
def definition(self) -> DefinitionContainer:
|
||||||
|
return self._containers[_ContainerIndexes.Definition]
|
||||||
|
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getBottom(self) -> "DefinitionContainer":
|
||||||
|
return self.definition
|
||||||
|
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getTop(self) -> "InstanceContainer":
|
||||||
|
return self.userChanges
|
||||||
|
|
||||||
|
## Check whether the specified setting has a 'user' value.
|
||||||
|
#
|
||||||
|
# A user value here is defined as the setting having a value in either
|
||||||
|
# the UserChanges or QualityChanges container.
|
||||||
|
#
|
||||||
|
# \return True if the setting has a user value, False if not.
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def hasUserValue(self, key: str) -> bool:
|
||||||
|
if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self._containers[_ContainerIndexes.QualityChanges].hasProperty(key, "value"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
## Set a property of a setting.
|
||||||
|
#
|
||||||
|
# This will set a property of a specified setting. Since the container stack does not contain
|
||||||
|
# any settings itself, it is required to specify a container to set the property on. The target
|
||||||
|
# container is matched by container type.
|
||||||
|
#
|
||||||
|
# \param key The key of the setting to set.
|
||||||
|
# \param property_name The name of the property to set.
|
||||||
|
# \param new_value The new value to set the property to.
|
||||||
|
# \param target_container The type of the container to set the property of. Defaults to "user".
|
||||||
|
def setProperty(self, key: str, property_name: str, new_value: Any, target_container: str = "user") -> None:
|
||||||
|
container_index = _ContainerIndexes.TypeIndexMap.get(target_container, -1)
|
||||||
|
if container_index != -1:
|
||||||
|
self._containers[container_index].setProperty(key, property_name, new_value)
|
||||||
|
else:
|
||||||
|
raise IndexError("Invalid target container {type}".format(type = target_container))
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||||
|
# ordering, we disallow this operation.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def addContainer(self, container: ContainerInterface) -> None:
|
||||||
|
raise Exceptions.InvalidOperationError("Cannot add a container to Global stack")
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||||
|
# ordering, we disallow this operation.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def insertContainer(self, index: int, container: ContainerInterface) -> None:
|
||||||
|
raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack")
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# Since we have a fixed order of containers in the stack and this method would modify the container
|
||||||
|
# ordering, we disallow this operation.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def removeContainer(self, index: int = 0) -> None:
|
||||||
|
raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack")
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# Replaces the container at the specified index with another container.
|
||||||
|
# This version performs checks to make sure the new container has the expected metadata and type.
|
||||||
|
#
|
||||||
|
# \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None:
|
||||||
|
expected_type = _ContainerIndexes.IndexTypeMap[index]
|
||||||
|
if expected_type == "definition":
|
||||||
|
if not isinstance(container, DefinitionContainer):
|
||||||
|
raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not a DefinitionContainer".format(index = index))
|
||||||
|
elif container != self._empty_instance_container and container.getMetaDataEntry("type") != expected_type:
|
||||||
|
raise Exceptions.InvalidContainerError("Cannot replace container at index {index} with a container that is not of {type} type, but {actual_type} type.".format(index = index, type = expected_type, actual_type = container.getMetaDataEntry("type")))
|
||||||
|
|
||||||
|
super().replaceContainer(index, container, postpone_emit)
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# This deserialize will make sure the internal list of containers matches with what we expect.
|
||||||
|
# It will first check to see if the container at a certain index already matches with what we
|
||||||
|
# expect. If it does not, it will search for a matching container with the correct type. Should
|
||||||
|
# no container with the correct type be found, it will use the empty container.
|
||||||
|
#
|
||||||
|
# \throws InvalidContainerStackError Raised when no definition can be found for the stack.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
||||||
|
super().deserialize(contents, file_name)
|
||||||
|
|
||||||
|
new_containers = self._containers.copy()
|
||||||
|
while len(new_containers) < len(_ContainerIndexes.IndexTypeMap):
|
||||||
|
new_containers.append(self._empty_instance_container)
|
||||||
|
|
||||||
|
# Validate and ensure the list of containers matches with what we expect
|
||||||
|
for index, type_name in _ContainerIndexes.IndexTypeMap.items():
|
||||||
|
try:
|
||||||
|
container = new_containers[index]
|
||||||
|
except IndexError:
|
||||||
|
container = None
|
||||||
|
|
||||||
|
if type_name == "definition":
|
||||||
|
if not container or not isinstance(container, DefinitionContainer):
|
||||||
|
definition = self.findContainer(container_type = DefinitionContainer)
|
||||||
|
if not definition:
|
||||||
|
raise InvalidContainerStackError("Stack {id} does not have a definition!".format(id = self._id))
|
||||||
|
|
||||||
|
new_containers[index] = definition
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not container or container.getMetaDataEntry("type") != type_name:
|
||||||
|
actual_container = self.findContainer(type = type_name)
|
||||||
|
if actual_container:
|
||||||
|
new_containers[index] = actual_container
|
||||||
|
else:
|
||||||
|
new_containers[index] = self._empty_instance_container
|
||||||
|
|
||||||
|
self._containers = new_containers
|
||||||
|
|
||||||
|
## Find the variant that should be used as "default" variant.
|
||||||
|
#
|
||||||
|
# This will search for variants that match the current definition and pick the preferred one,
|
||||||
|
# if specified by the machine definition.
|
||||||
|
#
|
||||||
|
# The following criteria are used to find the default variant:
|
||||||
|
# - If the machine definition does not have a metadata entry "has_variants" set to True, return None
|
||||||
|
# - The definition of the variant should be the same as the machine definition for this stack.
|
||||||
|
# - The container should have a metadata entry "type" with value "variant".
|
||||||
|
# - If the machine definition has a metadata entry "preferred_variant", filter the variant IDs based on that.
|
||||||
|
#
|
||||||
|
# \return The container that should be used as default, or None if nothing was found or the machine does not use variants.
|
||||||
|
#
|
||||||
|
# \note This method assumes the stack has a valid machine definition.
|
||||||
|
def findDefaultVariant(self) -> Optional[ContainerInterface]:
|
||||||
|
definition = self._getMachineDefinition()
|
||||||
|
# has_variants can be overridden in other containers and stacks.
|
||||||
|
# In the case of UM2, it is overridden in the GlobalStack
|
||||||
|
if not self.getMetaDataEntry("has_variants"):
|
||||||
|
# If the machine does not use variants, we should never set a variant.
|
||||||
|
return None
|
||||||
|
|
||||||
|
# First add any variant. Later, overwrite with preference if the preference is valid.
|
||||||
|
variant = None
|
||||||
|
definition_id = self._findInstanceContainerDefinitionId(definition)
|
||||||
|
variants = ContainerRegistry.getInstance().findInstanceContainers(definition = definition_id, type = "variant")
|
||||||
|
if variants:
|
||||||
|
variant = variants[0]
|
||||||
|
|
||||||
|
preferred_variant_id = definition.getMetaDataEntry("preferred_variant")
|
||||||
|
if preferred_variant_id:
|
||||||
|
preferred_variants = ContainerRegistry.getInstance().findInstanceContainers(id = preferred_variant_id, definition = definition_id, type = "variant")
|
||||||
|
if preferred_variants:
|
||||||
|
variant = preferred_variants[0]
|
||||||
|
else:
|
||||||
|
Logger.log("w", "The preferred variant \"{variant}\" of stack {stack} does not exist or is not a variant.", variant = preferred_variant_id, stack = self.id)
|
||||||
|
# And leave it at the default variant.
|
||||||
|
|
||||||
|
if variant:
|
||||||
|
return variant
|
||||||
|
|
||||||
|
Logger.log("w", "Could not find a valid default variant for stack {stack}", stack = self.id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
## Find the material that should be used as "default" material.
|
||||||
|
#
|
||||||
|
# This will search for materials that match the current definition and pick the preferred one,
|
||||||
|
# if specified by the machine definition.
|
||||||
|
#
|
||||||
|
# The following criteria are used to find the default material:
|
||||||
|
# - If the machine definition does not have a metadata entry "has_materials" set to True, return None
|
||||||
|
# - If the machine definition has a metadata entry "has_machine_materials", the definition of the material should
|
||||||
|
# be the same as the machine definition for this stack. Otherwise, the definition should be "fdmprinter".
|
||||||
|
# - The container should have a metadata entry "type" with value "material".
|
||||||
|
# - The material should have an approximate diameter that matches the machine
|
||||||
|
# - If the machine definition has a metadata entry "has_variants" and set to True, the "variant" metadata entry of
|
||||||
|
# the material should be the same as the ID of the variant in the stack. Only applies if "has_machine_materials" is also True.
|
||||||
|
# - If the stack currently has a material set, try to find a material that matches the current material by name.
|
||||||
|
# - Otherwise, if the machine definition has a metadata entry "preferred_material", try to find a material that matches the specified ID.
|
||||||
|
#
|
||||||
|
# \return The container that should be used as default, or None if nothing was found or the machine does not use materials.
|
||||||
|
def findDefaultMaterial(self) -> Optional[ContainerInterface]:
|
||||||
|
definition = self._getMachineDefinition()
|
||||||
|
if not definition.getMetaDataEntry("has_materials"):
|
||||||
|
# Machine does not use materials, never try to set it.
|
||||||
|
return None
|
||||||
|
|
||||||
|
search_criteria = {"type": "material"}
|
||||||
|
if definition.getMetaDataEntry("has_machine_materials"):
|
||||||
|
search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
|
||||||
|
|
||||||
|
if definition.getMetaDataEntry("has_variants"):
|
||||||
|
search_criteria["variant"] = self.variant.id
|
||||||
|
else:
|
||||||
|
search_criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
|
if self.material != self._empty_material:
|
||||||
|
search_criteria["name"] = self.material.name
|
||||||
|
else:
|
||||||
|
preferred_material = definition.getMetaDataEntry("preferred_material")
|
||||||
|
if preferred_material:
|
||||||
|
search_criteria["id"] = preferred_material
|
||||||
|
|
||||||
|
approximate_material_diameter = str(round(self.getProperty("material_diameter", "value")))
|
||||||
|
search_criteria["approximate_diameter"] = approximate_material_diameter
|
||||||
|
|
||||||
|
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||||
|
if not materials:
|
||||||
|
Logger.log("w", "The preferred material \"{material}\" could not be found for stack {stack}", material = preferred_material, stack = self.id)
|
||||||
|
# We failed to find any materials matching the specified criteria, drop some specific criteria and try to find
|
||||||
|
# a material that sort-of matches what we want.
|
||||||
|
search_criteria.pop("variant", None)
|
||||||
|
search_criteria.pop("id", None)
|
||||||
|
search_criteria.pop("name", None)
|
||||||
|
materials = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||||
|
|
||||||
|
if materials:
|
||||||
|
return materials[0]
|
||||||
|
|
||||||
|
Logger.log("w", "Could not find a valid material for stack {stack}", stack = self.id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
## Find the quality that should be used as "default" quality.
|
||||||
|
#
|
||||||
|
# This will search for qualities that match the current definition and pick the preferred one,
|
||||||
|
# if specified by the machine definition.
|
||||||
|
#
|
||||||
|
# \return The container that should be used as default, or None if nothing was found.
|
||||||
|
def findDefaultQuality(self) -> Optional[ContainerInterface]:
|
||||||
|
definition = self._getMachineDefinition()
|
||||||
|
registry = ContainerRegistry.getInstance()
|
||||||
|
material_container = self.material if self.material != self._empty_instance_container else None
|
||||||
|
|
||||||
|
search_criteria = {"type": "quality"}
|
||||||
|
|
||||||
|
if definition.getMetaDataEntry("has_machine_quality"):
|
||||||
|
search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
|
||||||
|
|
||||||
|
if definition.getMetaDataEntry("has_materials") and material_container:
|
||||||
|
search_criteria["material"] = material_container.id
|
||||||
|
else:
|
||||||
|
search_criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
|
if self.quality != self._empty_quality:
|
||||||
|
search_criteria["name"] = self.quality.name
|
||||||
|
else:
|
||||||
|
preferred_quality = definition.getMetaDataEntry("preferred_quality")
|
||||||
|
if preferred_quality:
|
||||||
|
search_criteria["id"] = preferred_quality
|
||||||
|
|
||||||
|
containers = registry.findInstanceContainers(**search_criteria)
|
||||||
|
if containers:
|
||||||
|
return containers[0]
|
||||||
|
|
||||||
|
if "material" in search_criteria:
|
||||||
|
# First check if we can solve our material not found problem by checking if we can find quality containers
|
||||||
|
# that are assigned to the parents of this material profile.
|
||||||
|
try:
|
||||||
|
inherited_files = material_container.getInheritedFiles()
|
||||||
|
except AttributeError: # Material_container does not support inheritance.
|
||||||
|
inherited_files = []
|
||||||
|
|
||||||
|
if inherited_files:
|
||||||
|
for inherited_file in inherited_files:
|
||||||
|
# Extract the ID from the path we used to load the file.
|
||||||
|
search_criteria["material"] = os.path.basename(inherited_file).split(".")[0]
|
||||||
|
containers = registry.findInstanceContainers(**search_criteria)
|
||||||
|
if containers:
|
||||||
|
return containers[0]
|
||||||
|
|
||||||
|
# We still weren't able to find a quality for this specific material.
|
||||||
|
# Try to find qualities for a generic version of the material.
|
||||||
|
material_search_criteria = {"type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
|
||||||
|
if definition.getMetaDataEntry("has_machine_quality"):
|
||||||
|
if self.material != self._empty_instance_container:
|
||||||
|
material_search_criteria["definition"] = material_container.getDefinition().id
|
||||||
|
|
||||||
|
if definition.getMetaDataEntry("has_variants"):
|
||||||
|
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
|
||||||
|
else:
|
||||||
|
material_search_criteria["definition"] = self._findInstanceContainerDefinitionId(definition)
|
||||||
|
|
||||||
|
if definition.getMetaDataEntry("has_variants") and self.variant != self._empty_instance_container:
|
||||||
|
material_search_criteria["variant"] = self.variant.id
|
||||||
|
else:
|
||||||
|
material_search_criteria["definition"] = "fdmprinter"
|
||||||
|
material_containers = registry.findInstanceContainers(**material_search_criteria)
|
||||||
|
# Try all materials to see if there is a quality profile available.
|
||||||
|
for material_container in material_containers:
|
||||||
|
search_criteria["material"] = material_container.getId()
|
||||||
|
|
||||||
|
containers = registry.findInstanceContainers(**search_criteria)
|
||||||
|
if containers:
|
||||||
|
return containers[0]
|
||||||
|
|
||||||
|
if "name" in search_criteria or "id" in search_criteria:
|
||||||
|
# If a quality by this name can not be found, try a wider set of search criteria
|
||||||
|
search_criteria.pop("name", None)
|
||||||
|
search_criteria.pop("id", None)
|
||||||
|
|
||||||
|
containers = registry.findInstanceContainers(**search_criteria)
|
||||||
|
if containers:
|
||||||
|
return containers[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
## protected:
|
||||||
|
|
||||||
|
# Helper to make sure we emit a PyQt signal on container changes.
|
||||||
|
def _onContainersChanged(self, container: Any) -> None:
|
||||||
|
self.pyqtContainersChanged.emit()
|
||||||
|
|
||||||
|
# Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
|
||||||
|
# and its properties rather than, for example, the extruder. Defaults to simply returning the definition property.
|
||||||
|
def _getMachineDefinition(self) -> DefinitionContainer:
|
||||||
|
return self.definition
|
||||||
|
|
||||||
|
## Find the ID that should be used when searching for instance containers for a specified definition.
|
||||||
|
#
|
||||||
|
# This handles the situation where the definition specifies we should use a different definition when
|
||||||
|
# searching for instance containers.
|
||||||
|
#
|
||||||
|
# \param machine_definition The definition to find the "quality definition" for.
|
||||||
|
#
|
||||||
|
# \return The ID of the definition container to use when searching for instance containers.
|
||||||
|
@classmethod
|
||||||
|
def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainer) -> str:
|
||||||
|
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
|
if not quality_definition:
|
||||||
|
return machine_definition.id
|
||||||
|
|
||||||
|
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition)
|
||||||
|
if not definitions:
|
||||||
|
Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id)
|
||||||
|
return machine_definition.id
|
||||||
|
|
||||||
|
return cls._findInstanceContainerDefinitionId(definitions[0])
|
||||||
|
|
||||||
|
## private:
|
||||||
|
|
||||||
|
# Private helper class to keep track of container positions and their types.
|
||||||
|
class _ContainerIndexes:
|
||||||
|
UserChanges = 0
|
||||||
|
QualityChanges = 1
|
||||||
|
Quality = 2
|
||||||
|
Material = 3
|
||||||
|
Variant = 4
|
||||||
|
DefinitionChanges = 5
|
||||||
|
Definition = 6
|
||||||
|
|
||||||
|
# Simple hash map to map from index to "type" metadata entry
|
||||||
|
IndexTypeMap = {
|
||||||
|
UserChanges: "user",
|
||||||
|
QualityChanges: "quality_changes",
|
||||||
|
Quality: "quality",
|
||||||
|
Material: "material",
|
||||||
|
Variant: "variant",
|
||||||
|
DefinitionChanges: "definition_changes",
|
||||||
|
Definition: "definition",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reverse lookup: type -> index
|
||||||
|
TypeIndexMap = dict([(v, k) for k, v in IndexTypeMap.items()])
|
||||||
204
cura/Settings/CuraStackBuilder.py
Normal file
204
cura/Settings/CuraStackBuilder.py
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
|
from .GlobalStack import GlobalStack
|
||||||
|
from .ExtruderStack import ExtruderStack
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
## Contains helper functions to create new machines.
|
||||||
|
class CuraStackBuilder:
|
||||||
|
## Create a new instance of a machine.
|
||||||
|
#
|
||||||
|
# \param name The name of the new machine.
|
||||||
|
# \param definition_id The ID of the machine definition to use.
|
||||||
|
#
|
||||||
|
# \return The new global stack or None if an error occurred.
|
||||||
|
@classmethod
|
||||||
|
def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]:
|
||||||
|
registry = ContainerRegistry.getInstance()
|
||||||
|
definitions = registry.findDefinitionContainers(id = definition_id)
|
||||||
|
if not definitions:
|
||||||
|
Logger.log("w", "Definition {definition} was not found!", definition = definition_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
machine_definition = definitions[0]
|
||||||
|
|
||||||
|
generated_name = registry.createUniqueName("machine", "", name, machine_definition.name)
|
||||||
|
# Make sure the new name does not collide with any definition or (quality) profile
|
||||||
|
# createUniqueName() only looks at other stacks, but not at definitions or quality profiles
|
||||||
|
# Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true
|
||||||
|
if registry.findContainers(id = generated_name):
|
||||||
|
generated_name = registry.uniqueName(generated_name)
|
||||||
|
|
||||||
|
new_global_stack = cls.createGlobalStack(
|
||||||
|
new_stack_id = generated_name,
|
||||||
|
definition = machine_definition,
|
||||||
|
quality = "default",
|
||||||
|
material = "default",
|
||||||
|
variant = "default",
|
||||||
|
)
|
||||||
|
|
||||||
|
new_global_stack.setName(generated_name)
|
||||||
|
|
||||||
|
extruder_definition = registry.findDefinitionContainers(machine = machine_definition.getId())
|
||||||
|
|
||||||
|
if not extruder_definition:
|
||||||
|
# create extruder stack for single extrusion machines that have no separate extruder definition files
|
||||||
|
extruder_definition = registry.findDefinitionContainers(id = "fdmextruder")[0]
|
||||||
|
new_extruder_id = registry.uniqueName(machine_definition.getName() + " " + extruder_definition.id)
|
||||||
|
new_extruder = cls.createExtruderStack(
|
||||||
|
new_extruder_id,
|
||||||
|
definition=extruder_definition,
|
||||||
|
machine_definition=machine_definition,
|
||||||
|
quality="default",
|
||||||
|
material="default",
|
||||||
|
variant="default",
|
||||||
|
next_stack=new_global_stack
|
||||||
|
)
|
||||||
|
new_global_stack.addExtruder(new_extruder)
|
||||||
|
else:
|
||||||
|
# create extruder stack for each found extruder definition
|
||||||
|
for extruder_definition in registry.findDefinitionContainers(machine = machine_definition.id):
|
||||||
|
position = extruder_definition.getMetaDataEntry("position", None)
|
||||||
|
if not position:
|
||||||
|
Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.id)
|
||||||
|
|
||||||
|
new_extruder_id = registry.uniqueName(extruder_definition.id)
|
||||||
|
new_extruder = cls.createExtruderStack(
|
||||||
|
new_extruder_id,
|
||||||
|
definition = extruder_definition,
|
||||||
|
machine_definition = machine_definition,
|
||||||
|
quality = "default",
|
||||||
|
material = "default",
|
||||||
|
variant = "default",
|
||||||
|
next_stack = new_global_stack
|
||||||
|
)
|
||||||
|
new_global_stack.addExtruder(new_extruder)
|
||||||
|
|
||||||
|
return new_global_stack
|
||||||
|
|
||||||
|
## Create a new Extruder stack
|
||||||
|
#
|
||||||
|
# \param new_stack_id The ID of the new stack.
|
||||||
|
# \param definition The definition to base the new stack on.
|
||||||
|
# \param machine_definition The machine definition to use for the user container.
|
||||||
|
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
||||||
|
#
|
||||||
|
# \return A new Global stack instance with the specified parameters.
|
||||||
|
@classmethod
|
||||||
|
def createExtruderStack(cls, new_stack_id: str, definition: DefinitionContainer, machine_definition: DefinitionContainer, **kwargs) -> ExtruderStack:
|
||||||
|
stack = ExtruderStack(new_stack_id)
|
||||||
|
stack.setName(definition.getName())
|
||||||
|
stack.setDefinition(definition)
|
||||||
|
stack.addMetaDataEntry("position", definition.getMetaDataEntry("position"))
|
||||||
|
|
||||||
|
if "next_stack" in kwargs:
|
||||||
|
# Add stacks before containers are added, since they may trigger a setting update.
|
||||||
|
stack.setNextStack(kwargs["next_stack"])
|
||||||
|
|
||||||
|
user_container = InstanceContainer(new_stack_id + "_user")
|
||||||
|
user_container.addMetaDataEntry("type", "user")
|
||||||
|
user_container.addMetaDataEntry("extruder", new_stack_id)
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
user_container.setDefinition(machine_definition)
|
||||||
|
|
||||||
|
stack.setUserChanges(user_container)
|
||||||
|
|
||||||
|
# Important! The order here matters, because that allows the stack to
|
||||||
|
# assume the material and variant have already been set.
|
||||||
|
if "definition_changes" in kwargs:
|
||||||
|
stack.setDefinitionChangesById(kwargs["definition_changes"])
|
||||||
|
else:
|
||||||
|
stack.setDefinitionChanges(cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings"))
|
||||||
|
|
||||||
|
if "variant" in kwargs:
|
||||||
|
stack.setVariantById(kwargs["variant"])
|
||||||
|
|
||||||
|
if "material" in kwargs:
|
||||||
|
stack.setMaterialById(kwargs["material"])
|
||||||
|
|
||||||
|
if "quality" in kwargs:
|
||||||
|
stack.setQualityById(kwargs["quality"])
|
||||||
|
|
||||||
|
if "quality_changes" in kwargs:
|
||||||
|
stack.setQualityChangesById(kwargs["quality_changes"])
|
||||||
|
|
||||||
|
# Only add the created containers to the registry after we have set all the other
|
||||||
|
# properties. This makes the create operation more transactional, since any problems
|
||||||
|
# setting properties will not result in incomplete containers being added.
|
||||||
|
registry = ContainerRegistry.getInstance()
|
||||||
|
registry.addContainer(stack)
|
||||||
|
registry.addContainer(user_container)
|
||||||
|
|
||||||
|
return stack
|
||||||
|
|
||||||
|
## Create a new Global stack
|
||||||
|
#
|
||||||
|
# \param new_stack_id The ID of the new stack.
|
||||||
|
# \param definition The definition to base the new stack on.
|
||||||
|
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
||||||
|
#
|
||||||
|
# \return A new Global stack instance with the specified parameters.
|
||||||
|
@classmethod
|
||||||
|
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainer, **kwargs) -> GlobalStack:
|
||||||
|
stack = GlobalStack(new_stack_id)
|
||||||
|
stack.setDefinition(definition)
|
||||||
|
|
||||||
|
user_container = InstanceContainer(new_stack_id + "_user")
|
||||||
|
user_container.addMetaDataEntry("type", "user")
|
||||||
|
user_container.addMetaDataEntry("machine", new_stack_id)
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
user_container.setDefinition(definition)
|
||||||
|
|
||||||
|
stack.setUserChanges(user_container)
|
||||||
|
|
||||||
|
# Important! The order here matters, because that allows the stack to
|
||||||
|
# assume the material and variant have already been set.
|
||||||
|
if "definition_changes" in kwargs:
|
||||||
|
stack.setDefinitionChangesById(kwargs["definition_changes"])
|
||||||
|
else:
|
||||||
|
stack.setDefinitionChanges(cls.createDefinitionChangesContainer(stack, new_stack_id + "_settings"))
|
||||||
|
|
||||||
|
if "variant" in kwargs:
|
||||||
|
stack.setVariantById(kwargs["variant"])
|
||||||
|
|
||||||
|
if "material" in kwargs:
|
||||||
|
stack.setMaterialById(kwargs["material"])
|
||||||
|
|
||||||
|
if "quality" in kwargs:
|
||||||
|
stack.setQualityById(kwargs["quality"])
|
||||||
|
|
||||||
|
if "quality_changes" in kwargs:
|
||||||
|
stack.setQualityChangesById(kwargs["quality_changes"])
|
||||||
|
|
||||||
|
registry = ContainerRegistry.getInstance()
|
||||||
|
registry.addContainer(stack)
|
||||||
|
registry.addContainer(user_container)
|
||||||
|
|
||||||
|
return stack
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def createDefinitionChangesContainer(cls, container_stack, container_name, container_index = None):
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
unique_container_name = ContainerRegistry.getInstance().uniqueName(container_name)
|
||||||
|
|
||||||
|
definition_changes_container = InstanceContainer(unique_container_name)
|
||||||
|
definition = container_stack.getBottom()
|
||||||
|
definition_changes_container.setDefinition(definition)
|
||||||
|
definition_changes_container.addMetaDataEntry("type", "definition_changes")
|
||||||
|
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||||
|
|
||||||
|
ContainerRegistry.getInstance().addContainer(definition_changes_container)
|
||||||
|
container_stack.definitionChanges = definition_changes_container
|
||||||
|
|
||||||
|
return definition_changes_container
|
||||||
22
cura/Settings/Exceptions.py
Normal file
22
cura/Settings/Exceptions.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
|
||||||
|
## Raised when trying to perform an operation like add on a stack that does not allow that.
|
||||||
|
class InvalidOperationError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Raised when trying to replace a container with a container that does not have the expected type.
|
||||||
|
class InvalidContainerError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.
|
||||||
|
class TooManyExtrudersError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
## Raised when an extruder has no next stack set.
|
||||||
|
class NoGlobalStackError(Exception):
|
||||||
|
pass
|
||||||
604
cura/Settings/ExtruderManager.py
Normal file → Executable file
604
cura/Settings/ExtruderManager.py
Normal file → Executable file
|
|
@ -1,20 +1,44 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject, QVariant #For communicating data and events to Qt.
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
import UM.Application #To get the global container stack to find the current machine.
|
from UM.Application import Application # To get the global container stack to find the current machine.
|
||||||
import UM.Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator #To find which extruders are used in the scene.
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode #To find which extruders are used in the scene.
|
from UM.Scene.SceneNode import SceneNode
|
||||||
import UM.Settings.ContainerRegistry #Finding containers by ID.
|
from UM.Scene.Selection import Selection
|
||||||
import UM.Settings.SettingFunction
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||||
|
from typing import Optional, List, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
|
||||||
## Manages all existing extruder stacks.
|
## Manages all existing extruder stacks.
|
||||||
#
|
#
|
||||||
# This keeps a list of extruder stacks for each machine.
|
# This keeps a list of extruder stacks for each machine.
|
||||||
class ExtruderManager(QObject):
|
class ExtruderManager(QObject):
|
||||||
|
|
||||||
|
## Registers listeners and such to listen to changes to the extruders.
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
|
||||||
|
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
|
||||||
|
self._selected_object_extruders = []
|
||||||
|
self._global_container_stack_definition_id = None
|
||||||
|
self._addCurrentMachineExtruders()
|
||||||
|
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
||||||
|
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
||||||
|
|
||||||
## Signal to notify other components when the list of extruders for a machine definition changes.
|
## Signal to notify other components when the list of extruders for a machine definition changes.
|
||||||
extrudersChanged = pyqtSignal(QVariant)
|
extrudersChanged = pyqtSignal(QVariant)
|
||||||
|
|
||||||
|
|
@ -25,15 +49,6 @@ class ExtruderManager(QObject):
|
||||||
## Notify when the user switches the currently active extruder.
|
## Notify when the user switches the currently active extruder.
|
||||||
activeExtruderChanged = pyqtSignal()
|
activeExtruderChanged = pyqtSignal()
|
||||||
|
|
||||||
## Registers listeners and such to listen to changes to the extruders.
|
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
|
||||||
self._active_extruder_index = 0
|
|
||||||
UM.Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
|
||||||
self._global_container_stack_definition_id = None
|
|
||||||
self._addCurrentMachineExtruders()
|
|
||||||
|
|
||||||
## Gets the unique identifier of the currently active extruder stack.
|
## Gets the unique identifier of the currently active extruder stack.
|
||||||
#
|
#
|
||||||
# The currently active extruder stack is the stack that is currently being
|
# The currently active extruder stack is the stack that is currently being
|
||||||
|
|
@ -41,42 +56,53 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \return The unique ID of the currently active extruder stack.
|
# \return The unique ID of the currently active extruder stack.
|
||||||
@pyqtProperty(str, notify = activeExtruderChanged)
|
@pyqtProperty(str, notify = activeExtruderChanged)
|
||||||
def activeExtruderStackId(self):
|
def activeExtruderStackId(self) -> Optional[str]:
|
||||||
if not UM.Application.getInstance().getGlobalContainerStack():
|
if not Application.getInstance().getGlobalContainerStack():
|
||||||
return None # No active machine, so no active extruder.
|
return None # No active machine, so no active extruder.
|
||||||
try:
|
try:
|
||||||
return self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
||||||
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
## Return extruder count according to extruder trains.
|
||||||
@pyqtProperty(int, notify = extrudersChanged)
|
@pyqtProperty(int, notify = extrudersChanged)
|
||||||
def extruderCount(self):
|
def extruderCount(self):
|
||||||
if not UM.Application.getInstance().getGlobalContainerStack():
|
if not Application.getInstance().getGlobalContainerStack():
|
||||||
return 0 # No active machine, so no extruders.
|
return 0 # No active machine, so no extruders.
|
||||||
try:
|
try:
|
||||||
return len(self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()])
|
return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
||||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||||
def extruderIds(self):
|
def extruderIds(self):
|
||||||
map = {}
|
extruder_stack_ids = {}
|
||||||
for position in self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()]:
|
|
||||||
map[position] = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position].getId()
|
global_stack_id = Application.getInstance().getGlobalContainerStack().getId()
|
||||||
return map
|
|
||||||
|
if global_stack_id in self._extruder_trains:
|
||||||
|
for position in self._extruder_trains[global_stack_id]:
|
||||||
|
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
|
||||||
|
|
||||||
|
return extruder_stack_ids
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
@pyqtSlot(str, result = str)
|
||||||
def getQualityChangesIdByExtruderStackId(self, id):
|
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
|
||||||
for position in self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()]:
|
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
|
||||||
extruder = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position]
|
extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
|
||||||
if extruder.getId() == id:
|
if extruder.getId() == extruder_stack_id:
|
||||||
return extruder.findContainer(type = "quality_changes").getId()
|
return extruder.qualityChanges.getId()
|
||||||
|
|
||||||
## The instance of the singleton pattern.
|
## The instance of the singleton pattern.
|
||||||
#
|
#
|
||||||
# It's None if the extruder manager hasn't been created yet.
|
# It's None if the extruder manager hasn't been created yet.
|
||||||
__instance = None
|
__instance = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def createExtruderManager():
|
||||||
|
return ExtruderManager().getInstance()
|
||||||
|
|
||||||
## Gets an instance of the extruder manager, or creates one if no instance
|
## Gets an instance of the extruder manager, or creates one if no instance
|
||||||
# exists yet.
|
# exists yet.
|
||||||
#
|
#
|
||||||
|
|
@ -85,7 +111,7 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \return The extruder manager.
|
# \return The extruder manager.
|
||||||
@classmethod
|
@classmethod
|
||||||
def getInstance(cls):
|
def getInstance(cls) -> "ExtruderManager":
|
||||||
if not cls.__instance:
|
if not cls.__instance:
|
||||||
cls.__instance = ExtruderManager()
|
cls.__instance = ExtruderManager()
|
||||||
return cls.__instance
|
return cls.__instance
|
||||||
|
|
@ -94,63 +120,92 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \param index The index of the new active extruder.
|
# \param index The index of the new active extruder.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def setActiveExtruderIndex(self, index):
|
def setActiveExtruderIndex(self, index: int) -> None:
|
||||||
self._active_extruder_index = index
|
self._active_extruder_index = index
|
||||||
self.activeExtruderChanged.emit()
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = activeExtruderChanged)
|
@pyqtProperty(int, notify = activeExtruderChanged)
|
||||||
def activeExtruderIndex(self):
|
def activeExtruderIndex(self) -> int:
|
||||||
return self._active_extruder_index
|
return self._active_extruder_index
|
||||||
|
|
||||||
def getActiveExtruderStack(self):
|
## Gets the extruder name of an extruder of the currently active machine.
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
#
|
||||||
|
# \param index The index of the extruder whose name to get.
|
||||||
|
@pyqtSlot(int, result = str)
|
||||||
|
def getExtruderName(self, index):
|
||||||
|
try:
|
||||||
|
return list(self.getActiveExtruderStacks())[index].getName()
|
||||||
|
except IndexError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
## Emitted whenever the selectedObjectExtruders property changes.
|
||||||
|
selectedObjectExtrudersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
## Provides a list of extruder IDs used by the current selected objects.
|
||||||
|
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
|
||||||
|
def selectedObjectExtruders(self) -> List[str]:
|
||||||
|
if not self._selected_object_extruders:
|
||||||
|
object_extruders = set()
|
||||||
|
|
||||||
|
# First, build a list of the actual selected objects (including children of groups, excluding group nodes)
|
||||||
|
selected_nodes = []
|
||||||
|
for node in Selection.getAllSelectedObjects():
|
||||||
|
if node.callDecoration("isGroup"):
|
||||||
|
for grouped_node in BreadthFirstIterator(node):
|
||||||
|
if grouped_node.callDecoration("isGroup"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
selected_nodes.append(grouped_node)
|
||||||
|
else:
|
||||||
|
selected_nodes.append(node)
|
||||||
|
|
||||||
|
# Then, figure out which nodes are used by those selected nodes.
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
|
||||||
|
for node in selected_nodes:
|
||||||
|
extruder = node.callDecoration("getActiveExtruder")
|
||||||
|
if extruder:
|
||||||
|
object_extruders.add(extruder)
|
||||||
|
elif current_extruder_trains:
|
||||||
|
object_extruders.add(current_extruder_trains["0"].getId())
|
||||||
|
|
||||||
|
self._selected_object_extruders = list(object_extruders)
|
||||||
|
|
||||||
|
return self._selected_object_extruders
|
||||||
|
|
||||||
|
## Reset the internal list used for the selectedObjectExtruders property
|
||||||
|
#
|
||||||
|
# This will trigger a recalculation of the extruders used for the
|
||||||
|
# selection.
|
||||||
|
def resetSelectedObjectExtruders(self) -> None:
|
||||||
|
self._selected_object_extruders = []
|
||||||
|
self.selectedObjectExtrudersChanged.emit()
|
||||||
|
|
||||||
|
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
if global_container_stack.getId() in self._extruder_trains:
|
if global_container_stack.getId() in self._extruder_trains:
|
||||||
if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
|
if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
|
||||||
return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)]
|
return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
## Get an extruder stack by index
|
## Get an extruder stack by index
|
||||||
def getExtruderStack(self, index):
|
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
if global_container_stack.getId() in self._extruder_trains:
|
if global_container_stack.getId() in self._extruder_trains:
|
||||||
if str(index) in self._extruder_trains[global_container_stack.getId()]:
|
if str(index) in self._extruder_trains[global_container_stack.getId()]:
|
||||||
return self._extruder_trains[global_container_stack.getId()][str(index)]
|
return self._extruder_trains[global_container_stack.getId()][str(index)]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
## Adds all extruders of a specific machine definition to the extruder
|
## Get all extruder stacks
|
||||||
# manager.
|
def getExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
#
|
result = []
|
||||||
# \param machine_definition The machine definition to add the extruders for.
|
for i in range(self.extruderCount):
|
||||||
# \param machine_id The machine_id to add the extruders for.
|
result.append(self.getExtruderStack(i))
|
||||||
def addMachineExtruders(self, machine_definition, machine_id):
|
return result
|
||||||
changed = False
|
|
||||||
machine_definition_id = machine_definition.getId()
|
|
||||||
if machine_id not in self._extruder_trains:
|
|
||||||
self._extruder_trains[machine_id] = { }
|
|
||||||
changed = True
|
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
|
||||||
if container_registry:
|
|
||||||
# Add the extruder trains that don't exist yet.
|
|
||||||
for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition_id):
|
|
||||||
position = extruder_definition.getMetaDataEntry("position", None)
|
|
||||||
if not position:
|
|
||||||
UM.Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
|
|
||||||
if not container_registry.findContainerStacks(machine = machine_id, position = position): # Doesn't exist yet.
|
|
||||||
self.createExtruderTrain(extruder_definition, machine_definition, position, machine_id)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
# Gets the extruder trains that we just created as well as any that still existed.
|
|
||||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_id)
|
|
||||||
for extruder_train in extruder_trains:
|
|
||||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
|
||||||
|
|
||||||
# regardless of what the next stack is, we have to set it again, because of signal routing.
|
|
||||||
extruder_train.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
self.extrudersChanged.emit(machine_id)
|
|
||||||
|
|
||||||
def registerExtruder(self, extruder_train, machine_id):
|
def registerExtruder(self, extruder_train, machine_id):
|
||||||
changed = False
|
changed = False
|
||||||
|
|
@ -158,129 +213,19 @@ class ExtruderManager(QObject):
|
||||||
if machine_id not in self._extruder_trains:
|
if machine_id not in self._extruder_trains:
|
||||||
self._extruder_trains[machine_id] = {}
|
self._extruder_trains[machine_id] = {}
|
||||||
changed = True
|
changed = True
|
||||||
|
|
||||||
|
# do not register if an extruder has already been registered at the position on this machine
|
||||||
|
if any(item.getId() == extruder_train.getId() for item in self._extruder_trains[machine_id].values()):
|
||||||
|
Logger.log("w", "Extruder [%s] has already been registered on machine [%s], not doing anything",
|
||||||
|
extruder_train.getId(), machine_id)
|
||||||
|
return
|
||||||
|
|
||||||
if extruder_train:
|
if extruder_train:
|
||||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||||
changed = True
|
changed = True
|
||||||
if changed:
|
if changed:
|
||||||
self.extrudersChanged.emit(machine_id)
|
self.extrudersChanged.emit(machine_id)
|
||||||
|
|
||||||
## Creates a container stack for an extruder train.
|
|
||||||
#
|
|
||||||
# The container stack has an extruder definition at the bottom, which is
|
|
||||||
# linked to a machine definition. Then it has a variant profile, a material
|
|
||||||
# profile, a quality profile and a user profile, in that order.
|
|
||||||
#
|
|
||||||
# The resulting container stack is added to the registry.
|
|
||||||
#
|
|
||||||
# \param extruder_definition The extruder to create the extruder train for.
|
|
||||||
# \param machine_definition The machine that the extruder train belongs to.
|
|
||||||
# \param position The position of this extruder train in the extruder slots of the machine.
|
|
||||||
# \param machine_id The id of the "global" stack this extruder is linked to.
|
|
||||||
def createExtruderTrain(self, extruder_definition, machine_definition, position, machine_id):
|
|
||||||
# Cache some things.
|
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
|
||||||
machine_definition_id = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(machine_definition)
|
|
||||||
|
|
||||||
# Create a container stack for this extruder.
|
|
||||||
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
|
|
||||||
container_stack = UM.Settings.ContainerStack(extruder_stack_id)
|
|
||||||
container_stack.setName(extruder_definition.getName()) # Take over the display name to display the stack with.
|
|
||||||
container_stack.addMetaDataEntry("type", "extruder_train")
|
|
||||||
container_stack.addMetaDataEntry("machine", machine_id)
|
|
||||||
container_stack.addMetaDataEntry("position", position)
|
|
||||||
container_stack.addContainer(extruder_definition)
|
|
||||||
|
|
||||||
# Find the variant to use for this extruder.
|
|
||||||
variant = container_registry.findInstanceContainers(id = "empty_variant")[0]
|
|
||||||
if machine_definition.getMetaDataEntry("has_variants"):
|
|
||||||
# First add any variant. Later, overwrite with preference if the preference is valid.
|
|
||||||
variants = container_registry.findInstanceContainers(definition = machine_definition_id, type = "variant")
|
|
||||||
if len(variants) >= 1:
|
|
||||||
variant = variants[0]
|
|
||||||
preferred_variant_id = machine_definition.getMetaDataEntry("preferred_variant")
|
|
||||||
if preferred_variant_id:
|
|
||||||
preferred_variants = container_registry.findInstanceContainers(id = preferred_variant_id, definition = machine_definition_id, type = "variant")
|
|
||||||
if len(preferred_variants) >= 1:
|
|
||||||
variant = preferred_variants[0]
|
|
||||||
else:
|
|
||||||
UM.Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
|
|
||||||
# And leave it at the default variant.
|
|
||||||
container_stack.addContainer(variant)
|
|
||||||
|
|
||||||
# Find a material to use for this variant.
|
|
||||||
material = container_registry.findInstanceContainers(id = "empty_material")[0]
|
|
||||||
if machine_definition.getMetaDataEntry("has_materials"):
|
|
||||||
# First add any material. Later, overwrite with preference if the preference is valid.
|
|
||||||
machine_has_variant_materials = machine_definition.getMetaDataEntry("has_variant_materials", default = False)
|
|
||||||
if machine_has_variant_materials or machine_has_variant_materials == "True":
|
|
||||||
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id, variant = variant.getId())
|
|
||||||
else:
|
|
||||||
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id)
|
|
||||||
if len(materials) >= 1:
|
|
||||||
material = materials[0]
|
|
||||||
preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
|
||||||
if preferred_material_id:
|
|
||||||
search_criteria = { "type": "material", "id": preferred_material_id}
|
|
||||||
if machine_definition.getMetaDataEntry("has_machine_materials"):
|
|
||||||
search_criteria["definition"] = machine_definition_id
|
|
||||||
|
|
||||||
if machine_definition.getMetaDataEntry("has_variants") and variant:
|
|
||||||
search_criteria["variant"] = variant.id
|
|
||||||
else:
|
|
||||||
search_criteria["definition"] = "fdmprinter"
|
|
||||||
|
|
||||||
preferred_materials = container_registry.findInstanceContainers(**search_criteria)
|
|
||||||
if len(preferred_materials) >= 1:
|
|
||||||
material = preferred_materials[0]
|
|
||||||
else:
|
|
||||||
UM.Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
|
|
||||||
# And leave it at the default material.
|
|
||||||
container_stack.addContainer(material)
|
|
||||||
|
|
||||||
# Find a quality to use for this extruder.
|
|
||||||
quality = container_registry.getEmptyInstanceContainer()
|
|
||||||
|
|
||||||
search_criteria = { "type": "quality" }
|
|
||||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
|
||||||
search_criteria["definition"] = machine_definition_id
|
|
||||||
if machine_definition.getMetaDataEntry("has_materials") and material:
|
|
||||||
search_criteria["material"] = material.id
|
|
||||||
else:
|
|
||||||
search_criteria["definition"] = "fdmprinter"
|
|
||||||
|
|
||||||
preferred_quality = machine_definition.getMetaDataEntry("preferred_quality")
|
|
||||||
if preferred_quality:
|
|
||||||
search_criteria["id"] = preferred_quality
|
|
||||||
|
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
|
||||||
if not containers and preferred_quality:
|
|
||||||
UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
|
|
||||||
search_criteria.pop("id", None)
|
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
|
||||||
if containers:
|
|
||||||
quality = containers[0]
|
|
||||||
|
|
||||||
container_stack.addContainer(quality)
|
|
||||||
|
|
||||||
empty_quality_changes = container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
|
|
||||||
container_stack.addContainer(empty_quality_changes)
|
|
||||||
|
|
||||||
user_profile = container_registry.findInstanceContainers(type = "user", extruder = extruder_stack_id)
|
|
||||||
if user_profile: # There was already a user profile, loaded from settings.
|
|
||||||
user_profile = user_profile[0]
|
|
||||||
else:
|
|
||||||
user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings") # Add an empty user profile.
|
|
||||||
user_profile.addMetaDataEntry("type", "user")
|
|
||||||
user_profile.addMetaDataEntry("extruder", extruder_stack_id)
|
|
||||||
user_profile.setDefinition(machine_definition)
|
|
||||||
container_registry.addContainer(user_profile)
|
|
||||||
container_stack.addContainer(user_profile)
|
|
||||||
|
|
||||||
# regardless of what the next stack is, we have to set it again, because of signal routing.
|
|
||||||
container_stack.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
|
|
||||||
|
|
||||||
container_registry.addContainer(container_stack)
|
|
||||||
|
|
||||||
def getAllExtruderValues(self, setting_key):
|
def getAllExtruderValues(self, setting_key):
|
||||||
return self.getAllExtruderSettings(setting_key, "value")
|
return self.getAllExtruderSettings(setting_key, "value")
|
||||||
|
|
||||||
|
|
@ -289,16 +234,12 @@ class ExtruderManager(QObject):
|
||||||
# \param setting_key \type{str} The setting to get the property of.
|
# \param setting_key \type{str} The setting to get the property of.
|
||||||
# \param property \type{str} The property to get.
|
# \param property \type{str} The property to get.
|
||||||
# \return \type{List} the list of results
|
# \return \type{List} the list of results
|
||||||
def getAllExtruderSettings(self, setting_key, property):
|
def getAllExtruderSettings(self, setting_key: str, prop: str):
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
|
|
||||||
return [global_container_stack.getProperty(setting_key, property)]
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for index in self.extruderIds:
|
for index in self.extruderIds:
|
||||||
extruder_stack_id = self.extruderIds[str(index)]
|
extruder_stack_id = self.extruderIds[str(index)]
|
||||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||||
result.append(stack.getProperty(setting_key, property))
|
result.append(extruder_stack.getProperty(setting_key, prop))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Gets the extruder stacks that are actually being used at the moment.
|
## Gets the extruder stacks that are actually being used at the moment.
|
||||||
|
|
@ -311,75 +252,98 @@ class ExtruderManager(QObject):
|
||||||
# list.
|
# list.
|
||||||
#
|
#
|
||||||
# \return A list of extruder stacks.
|
# \return A list of extruder stacks.
|
||||||
def getUsedExtruderStacks(self):
|
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
if global_stack.getProperty("machine_extruder_count", "value") <= 1: #For single extrusion.
|
|
||||||
return [global_stack]
|
|
||||||
|
|
||||||
used_extruder_stack_ids = set()
|
used_extruder_stack_ids = set()
|
||||||
|
|
||||||
#Get the extruders of all meshes in the scene.
|
# Get the extruders of all meshes in the scene
|
||||||
support_enabled = False
|
support_enabled = False
|
||||||
support_interface_enabled = False
|
support_bottom_enabled = False
|
||||||
scene_root = UM.Application.getInstance().getController().getScene().getRoot()
|
support_roof_enabled = False
|
||||||
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()] #Only use the nodes that will be printed.
|
|
||||||
|
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||||
|
|
||||||
|
# If no extruders are registered in the extruder manager yet, return an empty array
|
||||||
|
if len(self.extruderIds) == 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get the extruders of all printable meshes in the scene
|
||||||
|
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()]
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||||
if not extruder_stack_id: #No per-object settings for this node.
|
if not extruder_stack_id:
|
||||||
|
# No per-object settings for this node
|
||||||
extruder_stack_id = self.extruderIds["0"]
|
extruder_stack_id = self.extruderIds["0"]
|
||||||
used_extruder_stack_ids.add(extruder_stack_id)
|
used_extruder_stack_ids.add(extruder_stack_id)
|
||||||
|
|
||||||
# Get whether any of them use support.
|
# Get whether any of them use support.
|
||||||
per_mesh_stack = mesh.callDecoration("getStack")
|
stack_to_use = mesh.callDecoration("getStack") # if there is a per-mesh stack, we use it
|
||||||
if per_mesh_stack:
|
if not stack_to_use:
|
||||||
support_enabled |= per_mesh_stack.getProperty("support_enable", "value")
|
# if there is no per-mesh stack, we use the build extruder for this mesh
|
||||||
support_interface_enabled |= per_mesh_stack.getProperty("support_interface_enable", "value")
|
stack_to_use = container_registry.findContainerStacks(id = extruder_stack_id)[0]
|
||||||
else: #Take the setting from the build extruder stack.
|
|
||||||
extruder_stack = container_registry.findContainerStacks(id = extruder_stack_id)[0]
|
|
||||||
support_enabled |= extruder_stack.getProperty("support_enable", "value")
|
|
||||||
support_interface_enabled |= extruder_stack.getProperty("support_enable", "value")
|
|
||||||
|
|
||||||
#The support extruders.
|
support_enabled |= stack_to_use.getProperty("support_enable", "value")
|
||||||
|
support_bottom_enabled |= stack_to_use.getProperty("support_bottom_enable", "value")
|
||||||
|
support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value")
|
||||||
|
|
||||||
|
# Check limit to extruders
|
||||||
|
limit_to_extruder_feature_list = ["wall_0_extruder_nr",
|
||||||
|
"wall_x_extruder_nr",
|
||||||
|
"roofing_extruder_nr",
|
||||||
|
"top_bottom_extruder_nr",
|
||||||
|
"infill_extruder_nr",
|
||||||
|
]
|
||||||
|
for extruder_nr_feature_name in limit_to_extruder_feature_list:
|
||||||
|
extruder_nr = int(global_stack.getProperty(extruder_nr_feature_name, "value"))
|
||||||
|
if extruder_nr == -1:
|
||||||
|
continue
|
||||||
|
used_extruder_stack_ids.add(self.extruderIds[str(extruder_nr)])
|
||||||
|
|
||||||
|
# Check support extruders
|
||||||
if support_enabled:
|
if support_enabled:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))])
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))])
|
||||||
if support_interface_enabled:
|
if support_bottom_enabled:
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_interface_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_bottom_extruder_nr", "value"))])
|
||||||
|
if support_roof_enabled:
|
||||||
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))])
|
||||||
|
|
||||||
# The platform adhesion extruder. Not used if using none.
|
# The platform adhesion extruder. Not used if using none.
|
||||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
|
||||||
|
|
||||||
|
try:
|
||||||
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||||
|
except IndexError: # One or more of the extruders was not found.
|
||||||
|
Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
|
||||||
|
return []
|
||||||
|
|
||||||
## Removes the container stack and user profile for the extruders for a specific machine.
|
## Removes the container stack and user profile for the extruders for a specific machine.
|
||||||
#
|
#
|
||||||
# \param machine_id The machine to remove the extruders for.
|
# \param machine_id The machine to remove the extruders for.
|
||||||
def removeMachineExtruders(self, machine_id):
|
def removeMachineExtruders(self, machine_id: str):
|
||||||
for extruder in self.getMachineExtruders(machine_id):
|
for extruder in self.getMachineExtruders(machine_id):
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
|
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
|
||||||
for container in containers:
|
ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
||||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
|
if machine_id in self._extruder_trains:
|
||||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
del self._extruder_trains[machine_id]
|
||||||
|
|
||||||
## Returns extruders for a specific machine.
|
## Returns extruders for a specific machine.
|
||||||
#
|
#
|
||||||
# \param machine_id The machine to get the extruders of.
|
# \param machine_id The machine to get the extruders of.
|
||||||
def getMachineExtruders(self, machine_id):
|
def getMachineExtruders(self, machine_id: str):
|
||||||
if machine_id not in self._extruder_trains:
|
if machine_id not in self._extruder_trains:
|
||||||
UM.Logger.log("w", "Tried to get the extruder trains for machine %s, which doesn't exist.", machine_id)
|
return []
|
||||||
return
|
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
|
||||||
for name in self._extruder_trains[machine_id]:
|
|
||||||
yield self._extruder_trains[machine_id][name]
|
|
||||||
|
|
||||||
## Returns a list containing the global stack and active extruder stacks.
|
## Returns a list containing the global stack and active extruder stacks.
|
||||||
#
|
#
|
||||||
# The first element is the global container stack, followed by any extruder stacks.
|
# The first element is the global container stack, followed by any extruder stacks.
|
||||||
# \return \type{List[ContainerStack]}
|
# \return \type{List[ContainerStack]}
|
||||||
def getActiveGlobalAndExtruderStacks(self):
|
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -387,47 +351,85 @@ class ExtruderManager(QObject):
|
||||||
result.extend(self.getActiveExtruderStacks())
|
result.extend(self.getActiveExtruderStacks())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Returns the list of active extruder stacks.
|
## Returns the list of active extruder stacks, taking into account the machine extruder count.
|
||||||
#
|
#
|
||||||
# \return \type{List[ContainerStack]} a list of
|
# \return \type{List[ContainerStack]} a list of
|
||||||
def getActiveExtruderStacks(self):
|
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
return list(self._extruder_trains[global_stack.getId()].values()) if global_stack else []
|
|
||||||
|
|
||||||
def __globalContainerStackChanged(self):
|
result = []
|
||||||
self._addCurrentMachineExtruders()
|
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
|
||||||
|
if global_stack and global_stack.getId() in self._extruder_trains:
|
||||||
|
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
|
||||||
|
result.append(self._extruder_trains[global_stack.getId()][extruder])
|
||||||
|
|
||||||
|
return result[:machine_extruder_count]
|
||||||
|
|
||||||
|
def __globalContainerStackChanged(self) -> None:
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack and global_container_stack.getBottom() and global_container_stack.getBottom().getId() != self._global_container_stack_definition_id:
|
if global_container_stack and global_container_stack.getBottom() and global_container_stack.getBottom().getId() != self._global_container_stack_definition_id:
|
||||||
self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
|
self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
|
||||||
self.globalContainerStackDefinitionChanged.emit()
|
self.globalContainerStackDefinitionChanged.emit()
|
||||||
self.activeExtruderChanged.emit()
|
|
||||||
|
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||||
|
self._addCurrentMachineExtruders()
|
||||||
|
|
||||||
|
self.resetSelectedObjectExtruders()
|
||||||
|
|
||||||
## Adds the extruders of the currently active machine.
|
## Adds the extruders of the currently active machine.
|
||||||
def _addCurrentMachineExtruders(self):
|
def _addCurrentMachineExtruders(self) -> None:
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_stack and global_stack.getBottom():
|
extruders_changed = False
|
||||||
self.addMachineExtruders(global_stack.getBottom(), global_stack.getId())
|
|
||||||
|
if global_stack:
|
||||||
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
global_stack_id = global_stack.getId()
|
||||||
|
|
||||||
|
# Gets the extruder trains that we just created as well as any that still existed.
|
||||||
|
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = global_stack_id)
|
||||||
|
|
||||||
|
# Make sure the extruder trains for the new machine can be placed in the set of sets
|
||||||
|
if global_stack_id not in self._extruder_trains:
|
||||||
|
self._extruder_trains[global_stack_id] = {}
|
||||||
|
extruders_changed = True
|
||||||
|
|
||||||
|
# Register the extruder trains by position
|
||||||
|
for extruder_train in extruder_trains:
|
||||||
|
self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||||
|
|
||||||
|
# regardless of what the next stack is, we have to set it again, because of signal routing. ???
|
||||||
|
extruder_train.setNextStack(global_stack)
|
||||||
|
extruders_changed = True
|
||||||
|
|
||||||
|
if extruders_changed:
|
||||||
|
self.extrudersChanged.emit(global_stack_id)
|
||||||
|
self.setActiveExtruderIndex(0)
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
#
|
#
|
||||||
# This is exposed to SettingFunction so it can be used in value functions.
|
# This is exposed to SettingFunction so it can be used in value functions.
|
||||||
#
|
#
|
||||||
# \param key The key of the setting to retieve values for.
|
# \param key The key of the setting to retrieve values for.
|
||||||
#
|
#
|
||||||
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
|
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
|
||||||
# If no extruder has the value, the list will contain the global value.
|
# If no extruder has the value, the list will contain the global value.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getExtruderValues(key):
|
def getExtruderValues(key):
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||||
|
# only include values from extruders that are "active" for the current machine instance
|
||||||
|
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
|
||||||
|
continue
|
||||||
|
|
||||||
value = extruder.getRawProperty(key, "value")
|
value = extruder.getRawProperty(key, "value")
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(value, UM.Settings.SettingFunction):
|
if isinstance(value, SettingFunction):
|
||||||
value = value(extruder)
|
value = value(extruder)
|
||||||
|
|
||||||
result.append(value)
|
result.append(value)
|
||||||
|
|
@ -437,11 +439,51 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
## Get all extruder values for a certain setting. This function will skip the user settings container.
|
||||||
|
#
|
||||||
|
# This is exposed to SettingFunction so it can be used in value functions.
|
||||||
|
#
|
||||||
|
# \param key The key of the setting to retrieve values for.
|
||||||
|
#
|
||||||
|
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
|
||||||
|
# If no extruder has the value, the list will contain the global value.
|
||||||
|
@staticmethod
|
||||||
|
def getDefaultExtruderValues(key):
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
context = PropertyEvaluationContext(global_stack)
|
||||||
|
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
|
context.context["override_operators"] = {
|
||||||
|
"extruderValue": ExtruderManager.getDefaultExtruderValue,
|
||||||
|
"extruderValues": ExtruderManager.getDefaultExtruderValues,
|
||||||
|
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||||
|
# only include values from extruders that are "active" for the current machine instance
|
||||||
|
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = extruder.getRawProperty(key, "value", context = context)
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(value, SettingFunction):
|
||||||
|
value = value(extruder, context = context)
|
||||||
|
|
||||||
|
result.append(value)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
result.append(global_stack.getProperty(key, "value", context = context))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
#
|
#
|
||||||
# This is exposed to qml for display purposes
|
# This is exposed to qml for display purposes
|
||||||
#
|
#
|
||||||
# \param key The key of the setting to retieve values for.
|
# \param key The key of the setting to retrieve values for.
|
||||||
#
|
#
|
||||||
# \return String representing the extruder values
|
# \return String representing the extruder values
|
||||||
@pyqtSlot(str, result="QVariant")
|
@pyqtSlot(str, result="QVariant")
|
||||||
|
|
@ -463,10 +505,40 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
if extruder:
|
if extruder:
|
||||||
value = extruder.getRawProperty(key, "value")
|
value = extruder.getRawProperty(key, "value")
|
||||||
if isinstance(value, UM.Settings.SettingFunction):
|
if isinstance(value, SettingFunction):
|
||||||
value = value(extruder)
|
value = value(extruder)
|
||||||
|
else:
|
||||||
|
# Just a value from global.
|
||||||
|
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
## Get the default value from the given extruder. This function will skip the user settings container.
|
||||||
|
#
|
||||||
|
# This is exposed to SettingFunction to use in value functions.
|
||||||
|
#
|
||||||
|
# \param extruder_index The index of the extruder to get the value from.
|
||||||
|
# \param key The key of the setting to get the value of.
|
||||||
|
#
|
||||||
|
# \return The value of the setting for the specified extruder or for the
|
||||||
|
# global stack if not found.
|
||||||
|
@staticmethod
|
||||||
|
def getDefaultExtruderValue(extruder_index, key):
|
||||||
|
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||||
|
context = PropertyEvaluationContext(extruder)
|
||||||
|
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
|
context.context["override_operators"] = {
|
||||||
|
"extruderValue": ExtruderManager.getDefaultExtruderValue,
|
||||||
|
"extruderValues": ExtruderManager.getDefaultExtruderValues,
|
||||||
|
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
if extruder:
|
||||||
|
value = extruder.getRawProperty(key, "value", context = context)
|
||||||
|
if isinstance(value, SettingFunction):
|
||||||
|
value = value(extruder, context = context)
|
||||||
else: # Just a value from global.
|
else: # Just a value from global.
|
||||||
value = UM.Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
@ -479,19 +551,29 @@ class ExtruderManager(QObject):
|
||||||
# \return The effective value
|
# \return The effective value
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getResolveOrValue(key):
|
def getResolveOrValue(key):
|
||||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
resolved_value = global_stack.getProperty(key, "value")
|
||||||
|
|
||||||
resolved_value = global_stack.getProperty(key, "resolve")
|
return resolved_value
|
||||||
if resolved_value is not None:
|
|
||||||
user_container = global_stack.findContainer({"type": "user"})
|
|
||||||
quality_changes_container = global_stack.findContainer({"type": "quality_changes"})
|
|
||||||
if user_container.hasProperty(key, "value") or quality_changes_container.hasProperty(key, "value"):
|
|
||||||
# Normal case
|
|
||||||
value = global_stack.getProperty(key, "value")
|
|
||||||
else:
|
|
||||||
# We have a resolved value and we're using it because of no user and quality_changes value
|
|
||||||
value = resolved_value
|
|
||||||
else:
|
|
||||||
value = global_stack.getRawProperty(key, "value")
|
|
||||||
|
|
||||||
return value
|
## Get the resolve value or value for a given key without looking the first container (user container)
|
||||||
|
#
|
||||||
|
# This is the effective value for a given key, it is used for values in the global stack.
|
||||||
|
# This is exposed to SettingFunction to use in value functions.
|
||||||
|
# \param key The key of the setting to get the value of.
|
||||||
|
#
|
||||||
|
# \return The effective value
|
||||||
|
@staticmethod
|
||||||
|
def getDefaultResolveOrValue(key):
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
context = PropertyEvaluationContext(global_stack)
|
||||||
|
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
|
context.context["override_operators"] = {
|
||||||
|
"extruderValue": ExtruderManager.getDefaultExtruderValue,
|
||||||
|
"extruderValues": ExtruderManager.getDefaultExtruderValues,
|
||||||
|
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved_value = global_stack.getProperty(key, "value", context = context)
|
||||||
|
|
||||||
|
return resolved_value
|
||||||
|
|
|
||||||
131
cura/Settings/ExtruderStack.py
Normal file
131
cura/Settings/ExtruderStack.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Any, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
from UM.Decorators import override
|
||||||
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext
|
||||||
|
|
||||||
|
from . import Exceptions
|
||||||
|
from .CuraContainerStack import CuraContainerStack
|
||||||
|
from .ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
## Represents an Extruder and its related containers.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
class ExtruderStack(CuraContainerStack):
|
||||||
|
def __init__(self, container_id: str, *args, **kwargs):
|
||||||
|
super().__init__(container_id, *args, **kwargs)
|
||||||
|
|
||||||
|
self.addMetaDataEntry("type", "extruder_train") # For backward compatibility
|
||||||
|
|
||||||
|
self.propertiesChanged.connect(self._onPropertiesChanged)
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# This will set the next stack and ensure that we register this stack as an extruder.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def setNextStack(self, stack: ContainerStack) -> None:
|
||||||
|
super().setNextStack(stack)
|
||||||
|
stack.addExtruder(self)
|
||||||
|
self.addMetaDataEntry("machine", stack.id)
|
||||||
|
|
||||||
|
# For backward compatibility: Register the extruder with the Extruder Manager
|
||||||
|
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
||||||
|
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||||
|
return super().getNextStack()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getLoadingPriority(cls) -> int:
|
||||||
|
return 3
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# It will perform a few extra checks when trying to get properties.
|
||||||
|
#
|
||||||
|
# The two extra checks it currently does is to ensure a next stack is set and to bypass
|
||||||
|
# the extruder when the property is not settable per extruder.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without
|
||||||
|
# having a next stack set.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||||
|
if not self._next_stack:
|
||||||
|
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))
|
||||||
|
|
||||||
|
if context is None:
|
||||||
|
context = PropertyEvaluationContext()
|
||||||
|
context.pushContainer(self)
|
||||||
|
|
||||||
|
if not super().getProperty(key, "settable_per_extruder", context):
|
||||||
|
result = self.getNextStack().getProperty(key, property_name, context)
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
|
if limit_to_extruder is not None:
|
||||||
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
|
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
||||||
|
if str(limit_to_extruder) in self.getNextStack().extruders:
|
||||||
|
result = self.getNextStack().extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
|
||||||
|
if result is not None:
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = super().getProperty(key, property_name, context)
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@override(CuraContainerStack)
|
||||||
|
def _getMachineDefinition(self) -> ContainerInterface:
|
||||||
|
if not self.getNextStack():
|
||||||
|
raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id))
|
||||||
|
|
||||||
|
return self.getNextStack()._getMachineDefinition()
|
||||||
|
|
||||||
|
@override(CuraContainerStack)
|
||||||
|
def deserialize(self, contents: str, file_name: Optional[str] = None) -> None:
|
||||||
|
super().deserialize(contents, file_name)
|
||||||
|
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
|
||||||
|
if stacks:
|
||||||
|
self.setNextStack(stacks[0])
|
||||||
|
|
||||||
|
def _onPropertiesChanged(self, key, properties):
|
||||||
|
# When there is a setting that is not settable per extruder that depends on a value from a setting that is,
|
||||||
|
# we do not always get properly informed that we should re-evaluate the setting. So make sure to indicate
|
||||||
|
# something changed for those settings.
|
||||||
|
if not self.getNextStack():
|
||||||
|
return #There are no global settings to depend on.
|
||||||
|
definitions = self.getNextStack().definition.findDefinitions(key = key)
|
||||||
|
if definitions:
|
||||||
|
has_global_dependencies = False
|
||||||
|
for relation in definitions[0].relations:
|
||||||
|
if not getattr(relation.target, "settable_per_extruder", True):
|
||||||
|
has_global_dependencies = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if has_global_dependencies:
|
||||||
|
self.getNextStack().propertiesChanged.emit(key, properties)
|
||||||
|
|
||||||
|
def findDefaultVariant(self):
|
||||||
|
# The default variant is defined in the machine stack and/or definition, so use the machine stack to find
|
||||||
|
# the default variant.
|
||||||
|
return self.getNextStack().findDefaultVariant()
|
||||||
|
|
||||||
|
|
||||||
|
extruder_stack_mime = MimeType(
|
||||||
|
name = "application/x-cura-extruderstack",
|
||||||
|
comment = "Cura Extruder Stack",
|
||||||
|
suffixes = ["extruder.cfg"]
|
||||||
|
)
|
||||||
|
|
||||||
|
MimeTypeDatabase.addMimeType(extruder_stack_mime)
|
||||||
|
ContainerRegistry.addContainerTypeByName(ExtruderStack, "extruder_stack", extruder_stack_mime.name)
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
import UM.Qt.ListModel
|
import UM.Qt.ListModel
|
||||||
|
from UM.Application import Application
|
||||||
|
import UM.FlameProfiler
|
||||||
|
|
||||||
from . import ExtruderManager
|
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
||||||
|
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
## Model that holds extruders.
|
## Model that holds extruders.
|
||||||
#
|
#
|
||||||
|
|
@ -32,6 +38,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
# The ID of the definition of the extruder.
|
# The ID of the definition of the extruder.
|
||||||
DefinitionRole = Qt.UserRole + 5
|
DefinitionRole = Qt.UserRole + 5
|
||||||
|
|
||||||
|
# The material of the extruder.
|
||||||
|
MaterialRole = Qt.UserRole + 6
|
||||||
|
|
||||||
|
# The variant of the extruder.
|
||||||
|
VariantRole = Qt.UserRole + 7
|
||||||
|
|
||||||
## List of colours to display if there is no material or the material has no known
|
## List of colours to display if there is no material or the material has no known
|
||||||
# colour.
|
# colour.
|
||||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||||
|
|
@ -48,32 +60,36 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
self.addRoleName(self.ColorRole, "color")
|
self.addRoleName(self.ColorRole, "color")
|
||||||
self.addRoleName(self.IndexRole, "index")
|
self.addRoleName(self.IndexRole, "index")
|
||||||
self.addRoleName(self.DefinitionRole, "definition")
|
self.addRoleName(self.DefinitionRole, "definition")
|
||||||
|
self.addRoleName(self.MaterialRole, "material")
|
||||||
|
self.addRoleName(self.VariantRole, "variant")
|
||||||
|
|
||||||
|
self._update_extruder_timer = QTimer()
|
||||||
|
self._update_extruder_timer.setInterval(100)
|
||||||
|
self._update_extruder_timer.setSingleShot(True)
|
||||||
|
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||||
|
|
||||||
self._add_global = False
|
|
||||||
self._simple_names = False
|
self._simple_names = False
|
||||||
|
|
||||||
self._active_extruder_stack = None
|
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||||
|
self._add_optional_extruder = False
|
||||||
|
|
||||||
#Listen to changes.
|
# Listen to changes
|
||||||
UM.Application.getInstance().globalContainerStackChanged.connect(self._updateExtruders)
|
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) # When the machine is swapped we must update the active machine extruders
|
||||||
manager = ExtruderManager.getInstance()
|
Application.getInstance().getExtruderManager().extrudersChanged.connect(self._extrudersChanged) # When the extruders change we must link to the stack-changed signal of the new extruder
|
||||||
|
Application.getInstance().getContainerRegistry().containerMetaDataChanged.connect(self._onExtruderStackContainersChanged) # When meta data from a material container changes we must update
|
||||||
|
self._extrudersChanged() # Also calls _updateExtruders
|
||||||
|
|
||||||
|
addOptionalExtruderChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def setAddOptionalExtruder(self, add_optional_extruder):
|
||||||
|
if add_optional_extruder != self._add_optional_extruder:
|
||||||
|
self._add_optional_extruder = add_optional_extruder
|
||||||
|
self.addOptionalExtruderChanged.emit()
|
||||||
self._updateExtruders()
|
self._updateExtruders()
|
||||||
|
|
||||||
manager.activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
@pyqtProperty(bool, fset = setAddOptionalExtruder, notify = addOptionalExtruderChanged)
|
||||||
self._onActiveExtruderChanged()
|
def addOptionalExtruder(self):
|
||||||
|
return self._add_optional_extruder
|
||||||
def setAddGlobal(self, add):
|
|
||||||
if add != self._add_global:
|
|
||||||
self._add_global = add
|
|
||||||
self._updateExtruders()
|
|
||||||
self.addGlobalChanged.emit()
|
|
||||||
|
|
||||||
addGlobalChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
|
|
||||||
def addGlobal(self):
|
|
||||||
return self._add_global
|
|
||||||
|
|
||||||
## Set the simpleNames property.
|
## Set the simpleNames property.
|
||||||
def setSimpleNames(self, simple_names):
|
def setSimpleNames(self, simple_names):
|
||||||
|
|
@ -90,76 +106,106 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
def simpleNames(self):
|
def simpleNames(self):
|
||||||
return self._simple_names
|
return self._simple_names
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
## Links to the stack-changed signal of the new extruders when an extruder
|
||||||
manager = ExtruderManager.getInstance()
|
# is swapped out or added in the current machine.
|
||||||
active_extruder_stack = manager.getActiveExtruderStack()
|
#
|
||||||
if self._active_extruder_stack != active_extruder_stack:
|
# \param machine_id The machine for which the extruders changed. This is
|
||||||
if self._active_extruder_stack:
|
# filled by the ExtruderManager.extrudersChanged signal when coming from
|
||||||
self._active_extruder_stack.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
# that signal. Application.globalContainerStackChanged doesn't fill this
|
||||||
|
# signal; it's assumed to be the current printer in that case.
|
||||||
|
def _extrudersChanged(self, machine_id = None):
|
||||||
|
if machine_id is not None:
|
||||||
|
if Application.getInstance().getGlobalContainerStack() is None:
|
||||||
|
# No machine, don't need to update the current machine's extruders
|
||||||
|
return
|
||||||
|
if machine_id != Application.getInstance().getGlobalContainerStack().getId():
|
||||||
|
# Not the current machine
|
||||||
|
return
|
||||||
|
|
||||||
if active_extruder_stack:
|
# Unlink from old extruders
|
||||||
# Update the model when the material container is changed
|
for extruder in self._active_machine_extruders:
|
||||||
active_extruder_stack.containersChanged.connect(self._onExtruderStackContainersChanged)
|
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||||
self._active_extruder_stack = active_extruder_stack
|
|
||||||
|
|
||||||
|
# Link to new extruders
|
||||||
|
self._active_machine_extruders = []
|
||||||
|
extruder_manager = Application.getInstance().getExtruderManager()
|
||||||
|
for extruder in extruder_manager.getExtruderStacks():
|
||||||
|
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||||
|
self._active_machine_extruders.append(extruder)
|
||||||
|
|
||||||
|
self._updateExtruders() # Since the new extruders may have different properties, update our own model.
|
||||||
|
|
||||||
def _onExtruderStackContainersChanged(self, container):
|
def _onExtruderStackContainersChanged(self, container):
|
||||||
|
# Update when there is an empty container or material change
|
||||||
|
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None:
|
||||||
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
||||||
if container.getMetaDataEntry("type") == "material":
|
|
||||||
self._updateExtruders()
|
self._updateExtruders()
|
||||||
|
|
||||||
modelChanged = pyqtSignal()
|
modelChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def _updateExtruders(self):
|
||||||
|
self._update_extruder_timer.start()
|
||||||
|
|
||||||
## Update the list of extruders.
|
## Update the list of extruders.
|
||||||
#
|
#
|
||||||
# This should be called whenever the list of extruders changes.
|
# This should be called whenever the list of extruders changes.
|
||||||
def _updateExtruders(self):
|
@UM.FlameProfiler.profile
|
||||||
changed = False
|
def __updateExtruders(self):
|
||||||
|
extruders_changed = False
|
||||||
|
|
||||||
if self.rowCount() != 0:
|
if self.rowCount() != 0:
|
||||||
changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
if self._add_global:
|
|
||||||
material = global_container_stack.findContainer({ "type": "material" })
|
# get machine extruder count for verification
|
||||||
color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0]
|
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
|
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()):
|
||||||
|
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||||
|
try:
|
||||||
|
position = int(position)
|
||||||
|
except ValueError:
|
||||||
|
# Not a proper int.
|
||||||
|
position = -1
|
||||||
|
if position >= machine_extruder_count:
|
||||||
|
continue
|
||||||
|
|
||||||
|
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
||||||
|
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
||||||
|
|
||||||
|
# construct an item with only the relevant information
|
||||||
item = {
|
item = {
|
||||||
"id": global_container_stack.getId(),
|
"id": extruder.getId(),
|
||||||
"name": "Global",
|
"name": extruder.getName(),
|
||||||
"color": color,
|
"color": color,
|
||||||
|
"index": position,
|
||||||
|
"definition": extruder.getBottom().getId(),
|
||||||
|
"material": extruder.material.getName() if extruder.material else "",
|
||||||
|
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||||
|
}
|
||||||
|
|
||||||
|
items.append(item)
|
||||||
|
extruders_changed = True
|
||||||
|
|
||||||
|
if extruders_changed:
|
||||||
|
# sort by extruder index
|
||||||
|
items.sort(key = lambda i: i["index"])
|
||||||
|
|
||||||
|
# We need optional extruder to be last, so add it after we do sorting.
|
||||||
|
# This way we can simply interpret the -1 of the index as the last item (which it now always is)
|
||||||
|
if self._add_optional_extruder:
|
||||||
|
item = {
|
||||||
|
"id": "",
|
||||||
|
"name": catalog.i18nc("@menuitem", "Not overridden"),
|
||||||
|
"color": "#ffffff",
|
||||||
"index": -1,
|
"index": -1,
|
||||||
"definition": ""
|
"definition": ""
|
||||||
}
|
}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
changed = True
|
|
||||||
|
|
||||||
manager = ExtruderManager.getInstance()
|
|
||||||
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
|
|
||||||
extruder_name = extruder.getName()
|
|
||||||
material = extruder.findContainer({ "type": "material" })
|
|
||||||
if material and not self._simple_names:
|
|
||||||
extruder_name = "%s (%s)" % (material.getName(), extruder_name)
|
|
||||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
|
||||||
try:
|
|
||||||
position = int(position)
|
|
||||||
except ValueError: #Not a proper int.
|
|
||||||
position = -1
|
|
||||||
default_color = self.defaultColors[position] if position >= 0 and position < len(self.defaultColors) else self.defaultColors[0]
|
|
||||||
color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color
|
|
||||||
item = { #Construct an item with only the relevant information.
|
|
||||||
"id": extruder.getId(),
|
|
||||||
"name": extruder_name,
|
|
||||||
"color": color,
|
|
||||||
"index": position,
|
|
||||||
"definition": extruder.getBottom().getId()
|
|
||||||
}
|
|
||||||
items.append(item)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if changed:
|
|
||||||
items.sort(key = lambda i: i["index"])
|
|
||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
self.modelChanged.emit()
|
self.modelChanged.emit()
|
||||||
|
|
|
||||||
176
cura/Settings/GlobalStack.py
Executable file
176
cura/Settings/GlobalStack.py
Executable file
|
|
@ -0,0 +1,176 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty
|
||||||
|
|
||||||
|
from UM.Decorators import override
|
||||||
|
|
||||||
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
from . import Exceptions
|
||||||
|
from .CuraContainerStack import CuraContainerStack
|
||||||
|
|
||||||
|
## Represents the Global or Machine stack and its related containers.
|
||||||
|
#
|
||||||
|
class GlobalStack(CuraContainerStack):
|
||||||
|
def __init__(self, container_id: str, *args, **kwargs):
|
||||||
|
super().__init__(container_id, *args, **kwargs)
|
||||||
|
|
||||||
|
self.addMetaDataEntry("type", "machine") # For backward compatibility
|
||||||
|
|
||||||
|
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
||||||
|
|
||||||
|
# This property is used to track which settings we are calculating the "resolve" for
|
||||||
|
# and if so, to bypass the resolve to prevent an infinite recursion that would occur
|
||||||
|
# if the resolve function tried to access the same property it is a resolve for.
|
||||||
|
self._resolving_settings = set()
|
||||||
|
|
||||||
|
## Get the list of extruders of this stack.
|
||||||
|
#
|
||||||
|
# \return The extruders registered with this stack.
|
||||||
|
@pyqtProperty("QVariantMap")
|
||||||
|
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||||
|
return self._extruders
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def getLoadingPriority(cls) -> int:
|
||||||
|
return 2
|
||||||
|
|
||||||
|
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
|
||||||
|
configuration_type = None
|
||||||
|
try:
|
||||||
|
parser = self._readAndValidateSerialized(serialized)
|
||||||
|
configuration_type = parser["metadata"].get("type")
|
||||||
|
if configuration_type == "machine":
|
||||||
|
configuration_type = "machine_stack"
|
||||||
|
except Exception as e:
|
||||||
|
Logger.log("e", "Could not get configuration type: %s", e)
|
||||||
|
return configuration_type
|
||||||
|
|
||||||
|
## Add an extruder to the list of extruders of this stack.
|
||||||
|
#
|
||||||
|
# \param extruder The extruder to add.
|
||||||
|
#
|
||||||
|
# \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we
|
||||||
|
# already have the maximum number of extruders.
|
||||||
|
def addExtruder(self, extruder: ContainerStack) -> None:
|
||||||
|
position = extruder.getMetaDataEntry("position")
|
||||||
|
if position is None:
|
||||||
|
Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
if any(item.getId() == extruder.id for item in self._extruders.values()):
|
||||||
|
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self._id)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._extruders[position] = extruder
|
||||||
|
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# This will return the value of the specified property for the specified setting,
|
||||||
|
# unless the property is "value" and that setting has a "resolve" function set.
|
||||||
|
# When a resolve is set, it will instead try and execute the resolve first and
|
||||||
|
# then fall back to the normal "value" property.
|
||||||
|
#
|
||||||
|
# \param key The setting key to get the property of.
|
||||||
|
# \param property_name The property to get the value of.
|
||||||
|
#
|
||||||
|
# \return The value of the property for the specified setting, or None if not found.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||||
|
if not self.definition.findDefinitions(key = key):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if context is None:
|
||||||
|
context = PropertyEvaluationContext()
|
||||||
|
context.pushContainer(self)
|
||||||
|
|
||||||
|
# Handle the "resolve" property.
|
||||||
|
if self._shouldResolve(key, property_name, context):
|
||||||
|
self._resolving_settings.add(key)
|
||||||
|
resolve = super().getProperty(key, "resolve", context)
|
||||||
|
self._resolving_settings.remove(key)
|
||||||
|
if resolve is not None:
|
||||||
|
return resolve
|
||||||
|
|
||||||
|
# Handle the "limit_to_extruder" property.
|
||||||
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
|
if limit_to_extruder is not None:
|
||||||
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
||||||
|
if super().getProperty(key, "settable_per_extruder", context):
|
||||||
|
result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
|
||||||
|
if result is not None:
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)
|
||||||
|
|
||||||
|
result = super().getProperty(key, property_name, context)
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Overridden from ContainerStack
|
||||||
|
#
|
||||||
|
# This will simply raise an exception since the Global stack cannot have a next stack.
|
||||||
|
@override(ContainerStack)
|
||||||
|
def setNextStack(self, next_stack: ContainerStack) -> None:
|
||||||
|
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
||||||
|
|
||||||
|
## Gets the approximate filament diameter that the machine requires.
|
||||||
|
#
|
||||||
|
# The approximate material diameter is the material diameter rounded to
|
||||||
|
# the nearest millimetre.
|
||||||
|
#
|
||||||
|
# If the machine has no requirement for the diameter, -1 is returned.
|
||||||
|
#
|
||||||
|
# \return The approximate filament diameter for the printer, as a string.
|
||||||
|
@pyqtProperty(str)
|
||||||
|
def approximateMaterialDiameter(self) -> str:
|
||||||
|
material_diameter = self.definition.getProperty("material_diameter", "value")
|
||||||
|
if material_diameter is None:
|
||||||
|
return "-1"
|
||||||
|
return str(round(float(material_diameter))) #Round, then convert back to string.
|
||||||
|
|
||||||
|
# protected:
|
||||||
|
|
||||||
|
# Determine whether or not we should try to get the "resolve" property instead of the
|
||||||
|
# requested property.
|
||||||
|
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
||||||
|
if property_name is not "value":
|
||||||
|
# Do not try to resolve anything but the "value" property
|
||||||
|
return False
|
||||||
|
|
||||||
|
if key in self._resolving_settings:
|
||||||
|
# To prevent infinite recursion, if getProperty is called with the same key as
|
||||||
|
# we are already trying to resolve, we should not try to resolve again. Since
|
||||||
|
# this can happen multiple times when trying to resolve a value, we need to
|
||||||
|
# track all settings that are being resolved.
|
||||||
|
return False
|
||||||
|
|
||||||
|
setting_state = super().getProperty(key, "state", context = context)
|
||||||
|
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.
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
## private:
|
||||||
|
global_stack_mime = MimeType(
|
||||||
|
name = "application/x-cura-globalstack",
|
||||||
|
comment = "Cura Global Stack",
|
||||||
|
suffixes = ["global.cfg"]
|
||||||
|
)
|
||||||
|
|
||||||
|
MimeTypeDatabase.addMimeType(global_stack_mime)
|
||||||
|
ContainerRegistry.addContainerTypeByName(GlobalStack, "global_stack", global_stack_mime.name)
|
||||||
1054
cura/Settings/MachineManager.py
Normal file → Executable file
1054
cura/Settings/MachineManager.py
Normal file → Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -1,14 +1,14 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QObject, pyqtSignal, QRegExp
|
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QObject, pyqtSignal, QRegExp
|
||||||
from PyQt5.QtGui import QValidator
|
from PyQt5.QtGui import QValidator
|
||||||
import os #For statvfs.
|
import os #For statvfs.
|
||||||
import urllib #To escape machine names for how they're saved to file.
|
import urllib #To escape machine names for how they're saved to file.
|
||||||
|
|
||||||
import UM.Resources
|
from UM.Resources import Resources
|
||||||
import UM.Settings.ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
import UM.Settings.InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
||||||
## Are machine names valid?
|
## Are machine names valid?
|
||||||
#
|
#
|
||||||
|
|
@ -19,10 +19,10 @@ class MachineNameValidator(QObject):
|
||||||
|
|
||||||
#Compute the validation regex for printer names. This is limited by the maximum file name length.
|
#Compute the validation regex for printer names. This is limited by the maximum file name length.
|
||||||
try:
|
try:
|
||||||
filename_max_length = os.statvfs(UM.Resources.getDataStoragePath()).f_namemax
|
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||||
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||||
filename_max_length = 255 #Assume it's Windows on NTFS.
|
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||||
machine_name_max_length = filename_max_length - len("_current_settings.") - len(UM.Settings.ContainerRegistry.getMimeTypeForContainer(UM.Settings.InstanceContainer).preferredSuffix)
|
machine_name_max_length = filename_max_length - len("_current_settings.") - len(ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix)
|
||||||
# Characters that urllib.parse.quote_plus escapes count for 12! So now
|
# Characters that urllib.parse.quote_plus escapes count for 12! So now
|
||||||
# we must devise a regex that allows only 12 normal characters or 1
|
# we must devise a regex that allows only 12 normal characters or 1
|
||||||
# special character, and that up to [machine_name_max_length / 12] times.
|
# special character, and that up to [machine_name_max_length / 12] times.
|
||||||
|
|
@ -41,11 +41,11 @@ class MachineNameValidator(QObject):
|
||||||
def validate(self, name, position):
|
def validate(self, name, position):
|
||||||
#Check for file name length of the current settings container (which is the longest file we're saving with the name).
|
#Check for file name length of the current settings container (which is the longest file we're saving with the name).
|
||||||
try:
|
try:
|
||||||
filename_max_length = os.statvfs(UM.Resources.getDataStoragePath()).f_namemax
|
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||||
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||||
filename_max_length = 255 #Assume it's Windows on NTFS.
|
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||||
escaped_name = urllib.parse.quote_plus(name)
|
escaped_name = urllib.parse.quote_plus(name)
|
||||||
current_settings_filename = escaped_name + "_current_settings." + UM.Settings.ContainerRegistry.getMimeTypeForContainer(UM.Settings.InstanceContainer).preferredSuffix
|
current_settings_filename = escaped_name + "_current_settings." + ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
||||||
if len(current_settings_filename) > filename_max_length:
|
if len(current_settings_filename) > filename_max_length:
|
||||||
return QValidator.Invalid
|
return QValidator.Invalid
|
||||||
|
|
||||||
|
|
|
||||||
57
cura/Settings/MaterialManager.py
Normal file
57
cura/Settings/MaterialManager.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot #To expose data to QML.
|
||||||
|
|
||||||
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message #To create a warning message about material diameter.
|
||||||
|
from UM.i18n import i18nCatalog #Translated strings.
|
||||||
|
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
## Handles material-related data, processing requests to change them and
|
||||||
|
# providing data for the GUI.
|
||||||
|
#
|
||||||
|
# TODO: Move material-related managing over from the machine manager to here.
|
||||||
|
class MaterialManager(QObject):
|
||||||
|
## Creates the global values for the material manager to use.
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
#Material diameter changed warning message.
|
||||||
|
self._material_diameter_warning_message = Message(catalog.i18nc("@info:status Has a cancel button next to it.",
|
||||||
|
"The selected material diameter causes the material to become incompatible with the current printer."), title = catalog.i18nc("@info:title", "Incompatible Material"))
|
||||||
|
self._material_diameter_warning_message.addAction("Undo", catalog.i18nc("@action:button", "Undo"), None, catalog.i18nc("@action", "Undo changing the material diameter."))
|
||||||
|
self._material_diameter_warning_message.actionTriggered.connect(self._materialWarningMessageAction)
|
||||||
|
|
||||||
|
## Creates an instance of the MaterialManager.
|
||||||
|
#
|
||||||
|
# This should only be called by PyQt to create the singleton instance of
|
||||||
|
# this class.
|
||||||
|
@staticmethod
|
||||||
|
def createMaterialManager(engine = None, script_engine = None):
|
||||||
|
return MaterialManager()
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def showMaterialWarningMessage(self, material_id, previous_diameter):
|
||||||
|
self._material_diameter_warning_message.previous_diameter = previous_diameter #Make sure that the undo button can properly undo the action.
|
||||||
|
self._material_diameter_warning_message.material_id = material_id
|
||||||
|
self._material_diameter_warning_message.show()
|
||||||
|
|
||||||
|
## Called when clicking "undo" on the warning dialogue for disappeared
|
||||||
|
# materials.
|
||||||
|
#
|
||||||
|
# This executes the undo action, restoring the material diameter.
|
||||||
|
#
|
||||||
|
# \param button The identifier of the button that was pressed.
|
||||||
|
def _materialWarningMessageAction(self, message, button):
|
||||||
|
if button == "Undo":
|
||||||
|
container_manager = ContainerManager.getInstance()
|
||||||
|
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "properties/diameter", self._material_diameter_warning_message.previous_diameter)
|
||||||
|
approximate_previous_diameter = str(round(float(self._material_diameter_warning_message.previous_diameter)))
|
||||||
|
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "approximate_diameter", approximate_previous_diameter)
|
||||||
|
container_manager.setContainerProperty(self._material_diameter_warning_message.material_id, "material_diameter", "value", self._material_diameter_warning_message.previous_diameter);
|
||||||
|
message.hide()
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unknown button action for material diameter warning message: {action}".format(action = button))
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import UM.Settings.Models
|
import UM.Settings.Models.SettingVisibilityHandler
|
||||||
|
|
||||||
class MaterialSettingsVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler):
|
class MaterialSettingsVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler):
|
||||||
def __init__(self, parent = None, *args, **kwargs):
|
def __init__(self, parent = None, *args, **kwargs):
|
||||||
super().__init__(parent = parent, *args, **kwargs)
|
super().__init__(parent = parent, *args, **kwargs)
|
||||||
|
|
||||||
material_settings = set([
|
material_settings = {
|
||||||
"material_print_temperature",
|
"default_material_print_temperature",
|
||||||
"material_bed_temperature",
|
"material_bed_temperature",
|
||||||
"material_standby_temperature",
|
"material_standby_temperature",
|
||||||
|
#"material_flow_temp_graph",
|
||||||
"cool_fan_speed",
|
"cool_fan_speed",
|
||||||
"retraction_amount",
|
"retraction_amount",
|
||||||
"retraction_speed",
|
"retraction_speed",
|
||||||
])
|
}
|
||||||
|
|
||||||
self.setVisible(material_settings)
|
self.setVisible(material_settings)
|
||||||
|
|
|
||||||
25
cura/Settings/MaterialsModel.py
Normal file
25
cura/Settings/MaterialsModel.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry #To listen for changes to the materials.
|
||||||
|
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel #We're extending this class.
|
||||||
|
|
||||||
|
## A model that shows a list of currently valid materials.
|
||||||
|
class MaterialsModel(InstanceContainersModel):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerMetaDataChanged)
|
||||||
|
|
||||||
|
## Called when the metadata of the container was changed.
|
||||||
|
#
|
||||||
|
# This makes sure that we only update when it was a material that changed.
|
||||||
|
#
|
||||||
|
# \param container The container whose metadata was changed.
|
||||||
|
def _onContainerMetaDataChanged(self, container):
|
||||||
|
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _onContainerChanged(self, container):
|
||||||
|
if container.getMetaDataEntry("type", "") == "material":
|
||||||
|
super()._onContainerChanged(container)
|
||||||
65
cura/Settings/PerObjectContainerStack.py
Normal file
65
cura/Settings/PerObjectContainerStack.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Decorators import override
|
||||||
|
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
|
|
||||||
|
|
||||||
|
class PerObjectContainerStack(ContainerStack):
|
||||||
|
|
||||||
|
@override(ContainerStack)
|
||||||
|
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||||
|
if context is None:
|
||||||
|
context = PropertyEvaluationContext()
|
||||||
|
context.pushContainer(self)
|
||||||
|
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
# Return the user defined value if present, otherwise, evaluate the value according to the default routine.
|
||||||
|
if self.getContainer(0).hasProperty(key, property_name):
|
||||||
|
if self.getContainer(0)._instances[key].state == InstanceState.User:
|
||||||
|
result = super().getProperty(key, property_name, context)
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Handle the "limit_to_extruder" property.
|
||||||
|
limit_to_extruder = super().getProperty(key, "limit_to_extruder", context)
|
||||||
|
if limit_to_extruder is not None:
|
||||||
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
|
|
||||||
|
# if this stack has the limit_to_extruder "not overriden", use the original limit_to_extruder as the current
|
||||||
|
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
|
||||||
|
# stack.
|
||||||
|
if limit_to_extruder == "-1":
|
||||||
|
if "original_limit_to_extruder" in context.context:
|
||||||
|
limit_to_extruder = context.context["original_limit_to_extruder"]
|
||||||
|
|
||||||
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
||||||
|
# set the original limit_to_extruder if this is the first stack that has a non-overriden limit_to_extruder
|
||||||
|
if "original_limit_to_extruder" not in context.context:
|
||||||
|
context.context["original_limit_to_extruder"] = limit_to_extruder
|
||||||
|
|
||||||
|
if super().getProperty(key, "settable_per_extruder", context):
|
||||||
|
result = global_stack.extruders[str(limit_to_extruder)].getProperty(key, property_name, context)
|
||||||
|
if result is not None:
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = super().getProperty(key, property_name, context)
|
||||||
|
context.popContainer()
|
||||||
|
return result
|
||||||
|
|
||||||
|
@override(ContainerStack)
|
||||||
|
def setNextStack(self, stack: ContainerStack):
|
||||||
|
super().setNextStack(stack)
|
||||||
|
|
||||||
|
# trigger signal to re-evaluate all default settings
|
||||||
|
for key, instance in self.getContainer(0)._instances.items():
|
||||||
|
# only evaluate default settings
|
||||||
|
if instance.state != InstanceState.Default:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._collectPropertyChanges(key, "value")
|
||||||
|
self._emitCollectedPropertyChanges()
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
|
@ -10,17 +12,26 @@ from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
from typing import List, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
|
||||||
|
|
||||||
## QML Model for listing the current list of valid quality profiles.
|
## QML Model for listing the current list of valid quality profiles.
|
||||||
#
|
#
|
||||||
class ProfilesModel(InstanceContainersModel):
|
class ProfilesModel(InstanceContainersModel):
|
||||||
LayerHeightRole = Qt.UserRole + 1001
|
LayerHeightRole = Qt.UserRole + 1001
|
||||||
|
LayerHeightWithoutUnitRole = Qt.UserRole + 1002
|
||||||
|
AvailableRole = Qt.UserRole + 1003
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.addRoleName(self.LayerHeightRole, "layer_height")
|
self.addRoleName(self.LayerHeightRole, "layer_height")
|
||||||
|
self.addRoleName(self.LayerHeightWithoutUnitRole, "layer_height_without_unit")
|
||||||
|
self.addRoleName(self.AvailableRole, "available")
|
||||||
|
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._update)
|
Application.getInstance().globalContainerStackChanged.connect(self._update)
|
||||||
|
|
||||||
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
|
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
|
||||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
||||||
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
||||||
|
|
@ -32,13 +43,13 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
|
|
||||||
## Get the singleton instance for this class.
|
## Get the singleton instance for this class.
|
||||||
@classmethod
|
@classmethod
|
||||||
def getInstance(cls):
|
def getInstance(cls) -> "ProfilesModel":
|
||||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||||
if ProfilesModel.__instance is None:
|
if not ProfilesModel.__instance:
|
||||||
ProfilesModel.__instance = cls()
|
ProfilesModel.__instance = cls()
|
||||||
return ProfilesModel.__instance
|
return ProfilesModel.__instance
|
||||||
|
|
||||||
__instance = None
|
__instance = None # type: "ProfilesModel"
|
||||||
|
|
||||||
## Fetch the list of containers to display.
|
## Fetch the list of containers to display.
|
||||||
#
|
#
|
||||||
|
|
@ -48,46 +59,117 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
if global_container_stack is None:
|
if global_container_stack is None:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Get the list of extruders and place the selected extruder at the front of the list.
|
global_stack_definition = global_container_stack.definition
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
|
||||||
active_extruder = extruder_manager.getActiveExtruderStack()
|
|
||||||
extruder_stacks = extruder_manager.getActiveExtruderStacks()
|
|
||||||
if active_extruder in extruder_stacks:
|
|
||||||
extruder_stacks.remove(active_extruder)
|
|
||||||
extruder_stacks = [active_extruder] + extruder_stacks
|
|
||||||
|
|
||||||
# Fetch the list of useable qualities across all extruders.
|
# Get the list of extruders and place the selected extruder at the front of the list.
|
||||||
|
extruder_stacks = self._getOrderedExtruderStacksList()
|
||||||
|
materials = [extruder.material for extruder in extruder_stacks]
|
||||||
|
|
||||||
|
# Fetch the list of usable qualities across all extruders.
|
||||||
# The actual list of quality profiles come from the first extruder in the extruder list.
|
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||||
return QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
||||||
extruder_stacks)
|
|
||||||
|
# The usable quality types are set
|
||||||
|
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in result])
|
||||||
|
|
||||||
|
# Fetch all qualities available for this machine and the materials selected in extruders
|
||||||
|
all_qualities = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition, materials)
|
||||||
|
|
||||||
|
# If in the all qualities there is some of them that are not available due to incompatibility with materials
|
||||||
|
# we also add it so that they will appear in the slide quality bar. However in recomputeItems will be marked as
|
||||||
|
# not available so they will be shown in gray
|
||||||
|
for quality in all_qualities:
|
||||||
|
if quality.getMetaDataEntry("quality_type") not in quality_type_set:
|
||||||
|
result.append(quality)
|
||||||
|
|
||||||
|
# if still profiles are found, add a single empty_quality ("Not supported") instance to the drop down list
|
||||||
|
if len(result) == 0:
|
||||||
|
# If not qualities are found we dynamically create a not supported container for this machine + material combination
|
||||||
|
not_supported_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0]
|
||||||
|
result.append(not_supported_container)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
## Re-computes the items in this model, and adds the layer height role.
|
## Re-computes the items in this model, and adds the layer height role.
|
||||||
def _recomputeItems(self):
|
def _recomputeItems(self):
|
||||||
|
|
||||||
# Some globals that we can re-use.
|
# Some globals that we can re-use.
|
||||||
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
|
||||||
|
|
||||||
|
extruder_stacks = self._getOrderedExtruderStacksList()
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
machine_manager = Application.getInstance().getMachineManager()
|
|
||||||
|
# Get a list of usable/available qualities for this machine and material
|
||||||
|
qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
||||||
|
|
||||||
unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
|
unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
|
||||||
if not unit:
|
if not unit:
|
||||||
unit = ""
|
unit = ""
|
||||||
|
|
||||||
|
# group all quality items according to quality_types, so we know which profile suits the currently
|
||||||
|
# active machine and material, and later yield the right ones.
|
||||||
|
tmp_all_quality_items = OrderedDict()
|
||||||
for item in super()._recomputeItems():
|
for item in super()._recomputeItems():
|
||||||
profile = container_registry.findContainers(id=item["id"])
|
profile = container_registry.findContainers(id=item["id"])
|
||||||
|
quality_type = profile[0].getMetaDataEntry("quality_type") if profile else ""
|
||||||
|
|
||||||
|
if quality_type not in tmp_all_quality_items:
|
||||||
|
tmp_all_quality_items[quality_type] = {"suitable_container": None, "all_containers": []}
|
||||||
|
|
||||||
|
tmp_all_quality_items[quality_type]["all_containers"].append(item)
|
||||||
|
if tmp_all_quality_items[quality_type]["suitable_container"] is None:
|
||||||
|
tmp_all_quality_items[quality_type]["suitable_container"] = item
|
||||||
|
|
||||||
|
# reverse the ordering (finest first, coarsest last)
|
||||||
|
all_quality_items = OrderedDict()
|
||||||
|
for key in reversed(tmp_all_quality_items.keys()):
|
||||||
|
all_quality_items[key] = tmp_all_quality_items[key]
|
||||||
|
|
||||||
|
# First the suitable containers are set in the model
|
||||||
|
containers = []
|
||||||
|
for data_item in all_quality_items.values():
|
||||||
|
suitable_item = data_item["suitable_container"]
|
||||||
|
if suitable_item is not None:
|
||||||
|
containers.append(suitable_item)
|
||||||
|
|
||||||
|
# Once the suitable containers are collected, the rest of the containers are appended
|
||||||
|
for data_item in all_quality_items.values():
|
||||||
|
for item in data_item["all_containers"]:
|
||||||
|
if item not in containers:
|
||||||
|
containers.append(item)
|
||||||
|
|
||||||
|
# Now all the containers are set
|
||||||
|
for item in containers:
|
||||||
|
profile = container_registry.findContainers(id = item["id"])
|
||||||
|
|
||||||
|
# When for some reason there is no profile container in the registry
|
||||||
if not profile:
|
if not profile:
|
||||||
item["layer_height"] = "" #Can't update a profile that is unknown.
|
self._setItemLayerHeight(item, "", "")
|
||||||
|
item["available"] = False
|
||||||
yield item
|
yield item
|
||||||
continue
|
continue
|
||||||
|
|
||||||
#Easy case: This profile defines its own layer height.
|
|
||||||
profile = profile[0]
|
profile = profile[0]
|
||||||
if profile.hasProperty("layer_height", "value"):
|
|
||||||
item["layer_height"] = str(profile.getProperty("layer_height", "value")) + unit
|
# When there is a profile but it's an empty quality should. It's shown in the list (they are "Not Supported" profiles)
|
||||||
|
if profile.getId() == "empty_quality":
|
||||||
|
self._setItemLayerHeight(item, "", "")
|
||||||
|
item["available"] = True
|
||||||
yield item
|
yield item
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
item["available"] = profile in qualities
|
||||||
|
|
||||||
|
# Easy case: This profile defines its own layer height.
|
||||||
|
if profile.hasProperty("layer_height", "value"):
|
||||||
|
self._setItemLayerHeight(item, profile.getProperty("layer_height", "value"), unit)
|
||||||
|
yield item
|
||||||
|
continue
|
||||||
|
|
||||||
|
machine_manager = Application.getInstance().getMachineManager()
|
||||||
|
|
||||||
# Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
|
# Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
|
||||||
quality_type = profile.getMetaDataEntry("quality_type", None)
|
quality_type = profile.getMetaDataEntry("quality_type", None)
|
||||||
if quality_type:
|
if quality_type:
|
||||||
|
|
@ -96,21 +178,41 @@ class ProfilesModel(InstanceContainersModel):
|
||||||
if quality_result["stack"] is global_container_stack:
|
if quality_result["stack"] is global_container_stack:
|
||||||
quality = quality_result["quality"]
|
quality = quality_result["quality"]
|
||||||
break
|
break
|
||||||
else: #No global container stack in the results:
|
else:
|
||||||
|
# No global container stack in the results:
|
||||||
if quality_results:
|
if quality_results:
|
||||||
quality = quality_results[0]["quality"] #Take any of the extruders.
|
# Take any of the extruders.
|
||||||
|
quality = quality_results[0]["quality"]
|
||||||
else:
|
else:
|
||||||
quality = None
|
quality = None
|
||||||
if quality and quality.hasProperty("layer_height", "value"):
|
if quality and quality.hasProperty("layer_height", "value"):
|
||||||
item["layer_height"] = str(quality.getProperty("layer_height", "value")) + unit
|
self._setItemLayerHeight(item, quality.getProperty("layer_height", "value"), unit)
|
||||||
yield item
|
yield item
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
|
# Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
|
||||||
skip_until_container = global_container_stack.findContainer({"type": "material"})
|
skip_until_container = global_container_stack.material
|
||||||
if not skip_until_container: #No material in stack.
|
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No material in stack.
|
||||||
skip_until_container = global_container_stack.findContainer({"type": "variant"})
|
skip_until_container = global_container_stack.variant
|
||||||
if not skip_until_container: #No variant in stack.
|
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No variant in stack.
|
||||||
skip_until_container = global_container_stack.getBottom()
|
skip_until_container = global_container_stack.getBottom()
|
||||||
item["layer_height"] = str(global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId())) + unit #Fall through to the currently loaded material.
|
self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material.
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
|
## Get a list of extruder stacks with the active extruder at the front of the list.
|
||||||
|
@staticmethod
|
||||||
|
def _getOrderedExtruderStacksList() -> List["ExtruderStack"]:
|
||||||
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
|
extruder_stacks = extruder_manager.getActiveExtruderStacks()
|
||||||
|
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||||
|
|
||||||
|
if active_extruder in extruder_stacks:
|
||||||
|
extruder_stacks.remove(active_extruder)
|
||||||
|
extruder_stacks = [active_extruder] + extruder_stacks
|
||||||
|
|
||||||
|
return extruder_stacks
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _setItemLayerHeight(item, value, unit):
|
||||||
|
item["layer_height"] = str(value) + unit
|
||||||
|
item["layer_height_without_unit"] = str(value)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
|
|
@ -22,24 +22,23 @@ class QualityAndUserProfilesModel(ProfilesModel):
|
||||||
|
|
||||||
# Fetch the list of quality changes.
|
# Fetch the list of quality changes.
|
||||||
quality_manager = QualityManager.getInstance()
|
quality_manager = QualityManager.getInstance()
|
||||||
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
|
||||||
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||||
|
|
||||||
# Get the list of extruders and place the selected extruder at the front of the list.
|
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
active_extruder = extruder_manager.getActiveExtruderStack()
|
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||||
extruder_stacks = extruder_manager.getActiveExtruderStacks()
|
extruder_stacks = self._getOrderedExtruderStacksList()
|
||||||
if active_extruder in extruder_stacks:
|
|
||||||
extruder_stacks.remove(active_extruder)
|
|
||||||
extruder_stacks = [active_extruder] + extruder_stacks
|
|
||||||
|
|
||||||
# Fetch the list of useable qualities across all extruders.
|
# Fetch the list of usable qualities across all extruders.
|
||||||
# The actual list of quality profiles come from the first extruder in the extruder list.
|
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||||
quality_list = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
||||||
extruder_stacks)
|
|
||||||
|
|
||||||
# Filter the quality_change by the list of available quality_types
|
# Filter the quality_change by the list of available quality_types
|
||||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
filtered_quality_changes = [qc for qc in quality_changes_list if
|
||||||
|
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
||||||
|
qc.getMetaDataEntry("extruder") is not None and
|
||||||
|
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
||||||
|
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
|
||||||
|
|
||||||
return quality_list + filtered_quality_changes
|
return quality_list + filtered_quality_changes
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||||
|
|
||||||
import UM.Application
|
from UM.Logger import Logger
|
||||||
import UM.Logger
|
|
||||||
import UM.Qt
|
import UM.Qt
|
||||||
import UM.Settings
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
import os
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
|
||||||
class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
@ -23,12 +26,13 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent = parent)
|
super().__init__(parent = parent)
|
||||||
|
|
||||||
self._container_registry = UM.Settings.ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
self._extruder_id = None
|
self._extruder_id = None
|
||||||
self._extruder_definition_id = None
|
self._extruder_definition_id = None
|
||||||
self._quality_id = None
|
self._quality_id = None
|
||||||
self._material_id = None
|
self._material_id = None
|
||||||
|
self._i18n_catalog = None
|
||||||
|
|
||||||
self.addRoleName(self.KeyRole, "key")
|
self.addRoleName(self.KeyRole, "key")
|
||||||
self.addRoleName(self.LabelRole, "label")
|
self.addRoleName(self.LabelRole, "label")
|
||||||
|
|
@ -89,11 +93,11 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
settings = collections.OrderedDict()
|
settings = collections.OrderedDict()
|
||||||
definition_container = UM.Application.getInstance().getGlobalContainerStack().getBottom()
|
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
|
||||||
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not find a quality container with id %s", self._quality_id)
|
Logger.log("w", "Could not find a quality container with id %s", self._quality_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
quality_container = None
|
quality_container = None
|
||||||
|
|
@ -112,12 +116,24 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||||
if not quality_container:
|
if not quality_container:
|
||||||
UM.Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||||
return
|
return
|
||||||
quality_container = quality_container[0]
|
quality_container = quality_container[0]
|
||||||
|
|
||||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||||
definition_id = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
||||||
|
definition = quality_container.getDefinition()
|
||||||
|
|
||||||
|
# Check if the definition container has a translation file.
|
||||||
|
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
||||||
|
catalog = i18nCatalog(os.path.basename(definition_id + "." + definition_suffix))
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
|
for file_name in quality_container.getDefinition().getInheritedFiles():
|
||||||
|
catalog = i18nCatalog(os.path.basename(file_name))
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
||||||
|
|
||||||
|
|
@ -144,7 +160,7 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
|
||||||
if not containers:
|
if not containers:
|
||||||
UM.Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||||
return
|
return
|
||||||
|
|
||||||
if quality_changes_container:
|
if quality_changes_container:
|
||||||
|
|
@ -152,7 +168,7 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
if self._extruder_definition_id != "":
|
if self._extruder_definition_id != "":
|
||||||
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
||||||
if extruder_definitions:
|
if extruder_definitions:
|
||||||
criteria["extruder"] = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
||||||
criteria["name"] = quality_changes_container.getName()
|
criteria["name"] = quality_changes_container.getName()
|
||||||
else:
|
else:
|
||||||
criteria["extruder"] = None
|
criteria["extruder"] = None
|
||||||
|
|
@ -161,23 +177,32 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
if changes:
|
if changes:
|
||||||
containers.extend(changes)
|
containers.extend(changes)
|
||||||
|
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||||
|
|
||||||
current_category = ""
|
current_category = ""
|
||||||
for definition in definition_container.findDefinitions():
|
for definition in definition_container.findDefinitions():
|
||||||
if definition.type == "category":
|
if definition.type == "category":
|
||||||
current_category = definition.label
|
current_category = definition.label
|
||||||
|
if self._i18n_catalog:
|
||||||
|
current_category = self._i18n_catalog.i18nc(definition.key + " label", definition.label)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
profile_value = None
|
profile_value = None
|
||||||
profile_value_source = ""
|
profile_value_source = ""
|
||||||
for container in containers:
|
for container in containers:
|
||||||
new_value = container.getProperty(definition.key, "value")
|
new_value = container.getProperty(definition.key, "value")
|
||||||
|
|
||||||
if new_value is not None:
|
if new_value is not None:
|
||||||
profile_value_source = container.getMetaDataEntry("type")
|
profile_value_source = container.getMetaDataEntry("type")
|
||||||
profile_value = new_value
|
profile_value = new_value
|
||||||
|
|
||||||
|
# Global tab should use resolve (if there is one)
|
||||||
|
if not self._extruder_id:
|
||||||
|
resolve_value = global_container_stack.getProperty(definition.key, "resolve")
|
||||||
|
if resolve_value is not None and profile_value is not None and profile_value_source != "quality_changes":
|
||||||
|
profile_value = resolve_value
|
||||||
|
|
||||||
user_value = None
|
user_value = None
|
||||||
if not self._extruder_id:
|
if not self._extruder_id:
|
||||||
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
|
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
|
||||||
|
|
@ -199,9 +224,13 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
if self._extruder_id == "" and settable_per_extruder:
|
if self._extruder_id == "" and settable_per_extruder:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
label = definition.label
|
||||||
|
if self._i18n_catalog:
|
||||||
|
label = self._i18n_catalog.i18nc(definition.key + " label", label)
|
||||||
|
|
||||||
items.append({
|
items.append({
|
||||||
"key": definition.key,
|
"key": definition.key,
|
||||||
"label": definition.label,
|
"label": label,
|
||||||
"unit": definition.unit,
|
"unit": definition.unit,
|
||||||
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
|
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
|
||||||
"profile_value_source": profile_value_source,
|
"profile_value_source": profile_value_source,
|
||||||
|
|
|
||||||
27
cura/Settings/SetObjectExtruderOperation.py
Normal file
27
cura/Settings/SetObjectExtruderOperation.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Operations.Operation import Operation
|
||||||
|
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
|
||||||
|
## Simple operation to set the extruder a certain object should be printed with.
|
||||||
|
class SetObjectExtruderOperation(Operation):
|
||||||
|
def __init__(self, node: SceneNode, extruder_id: str) -> None:
|
||||||
|
self._node = node
|
||||||
|
self._extruder_id = extruder_id
|
||||||
|
self._previous_extruder_id = None
|
||||||
|
self._decorator_added = False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self._previous_extruder_id:
|
||||||
|
self._node.callDecoration("setActiveExtruder", self._previous_extruder_id)
|
||||||
|
|
||||||
|
def redo(self):
|
||||||
|
stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||||
|
if not stack:
|
||||||
|
self._node.addDecorator(SettingOverrideDecorator())
|
||||||
|
|
||||||
|
self._previous_extruder_id = self._node.callDecoration("getActiveExtruder")
|
||||||
|
self._node.callDecoration("setActiveExtruder", self._extruder_id)
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||||
import UM.Settings
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
import cura.Settings
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,6 +12,12 @@ from UM.Logger import Logger
|
||||||
# because some profiles tend to have 'hardcoded' values that break our inheritance. A good example of that are the
|
# because some profiles tend to have 'hardcoded' values that break our inheritance. A good example of that are the
|
||||||
# speed settings. If all the children of print_speed have a single value override, changing the speed won't
|
# speed settings. If all the children of print_speed have a single value override, changing the speed won't
|
||||||
# actually do anything, as only the 'leaf' settings are used by the engine.
|
# actually do anything, as only the 'leaf' settings are used by the engine.
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Settings.SettingInstance import InstanceState
|
||||||
|
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
class SettingInheritanceManager(QObject):
|
class SettingInheritanceManager(QObject):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -22,7 +27,7 @@ class SettingInheritanceManager(QObject):
|
||||||
self._active_container_stack = None
|
self._active_container_stack = None
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||||
self._onActiveExtruderChanged()
|
self._onActiveExtruderChanged()
|
||||||
|
|
||||||
settingsWithIntheritanceChanged = pyqtSignal()
|
settingsWithIntheritanceChanged = pyqtSignal()
|
||||||
|
|
@ -30,9 +35,10 @@ class SettingInheritanceManager(QObject):
|
||||||
## Get the keys of all children settings with an override.
|
## Get the keys of all children settings with an override.
|
||||||
@pyqtSlot(str, result = "QStringList")
|
@pyqtSlot(str, result = "QStringList")
|
||||||
def getChildrenKeysWithOverride(self, key):
|
def getChildrenKeysWithOverride(self, key):
|
||||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
definitions = self._global_container_stack.definition.findDefinitions(key=key)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
return
|
Logger.log("w", "Could not find definition for key [%s]", key)
|
||||||
|
return []
|
||||||
result = []
|
result = []
|
||||||
for key in definitions[0].getAllKeys():
|
for key in definitions[0].getAllKeys():
|
||||||
if key in self._settings_with_inheritance_warning:
|
if key in self._settings_with_inheritance_warning:
|
||||||
|
|
@ -41,20 +47,20 @@ class SettingInheritanceManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(str, str, result = "QStringList")
|
@pyqtSlot(str, str, result = "QStringList")
|
||||||
def getOverridesForExtruder(self, key, extruder_index):
|
def getOverridesForExtruder(self, key, extruder_index):
|
||||||
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
|
||||||
if not multi_extrusion:
|
|
||||||
return self._settings_with_inheritance_warning
|
|
||||||
extruder = cura.Settings.ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
|
||||||
if not extruder:
|
|
||||||
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
|
|
||||||
return []
|
|
||||||
|
|
||||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
|
||||||
if not definitions:
|
|
||||||
return
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
|
extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||||
|
if not extruder_stack:
|
||||||
|
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
|
||||||
|
return result
|
||||||
|
|
||||||
|
definitions = self._global_container_stack.definition.findDefinitions(key = key)
|
||||||
|
if not definitions:
|
||||||
|
Logger.log("w", "Could not find definition for key [%s] (2)", key)
|
||||||
|
return result
|
||||||
|
|
||||||
for key in definitions[0].getAllKeys():
|
for key in definitions[0].getAllKeys():
|
||||||
if self._settingIsOverwritingInheritance(key, extruder):
|
if self._settingIsOverwritingInheritance(key, extruder_stack):
|
||||||
result.append(key)
|
result.append(key)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -70,9 +76,9 @@ class SettingInheritanceManager(QObject):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
def _onActiveExtruderChanged(self):
|
||||||
new_active_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
|
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||||
if not new_active_stack:
|
# if not new_active_stack:
|
||||||
new_active_stack = self._global_container_stack
|
# new_active_stack = self._global_container_stack
|
||||||
|
|
||||||
if new_active_stack != self._active_container_stack: # Check if changed
|
if new_active_stack != self._active_container_stack: # Check if changed
|
||||||
if self._active_container_stack: # Disconnect signal from old container (if any)
|
if self._active_container_stack: # Disconnect signal from old container (if any)
|
||||||
|
|
@ -86,7 +92,7 @@ class SettingInheritanceManager(QObject):
|
||||||
|
|
||||||
def _onPropertyChanged(self, key, property_name):
|
def _onPropertyChanged(self, key, property_name):
|
||||||
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
|
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
|
||||||
definitions = self._global_container_stack.getBottom().findDefinitions(key = key)
|
definitions = self._global_container_stack.definition.findDefinitions(key = key)
|
||||||
if not definitions:
|
if not definitions:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -102,10 +108,13 @@ class SettingInheritanceManager(QObject):
|
||||||
self._settings_with_inheritance_warning.remove(key)
|
self._settings_with_inheritance_warning.remove(key)
|
||||||
settings_with_inheritance_warning_changed = True
|
settings_with_inheritance_warning_changed = True
|
||||||
|
|
||||||
# Find the topmost parent (Assumed to be a category)
|
|
||||||
parent = definitions[0].parent
|
parent = definitions[0].parent
|
||||||
|
# Find the topmost parent (Assumed to be a category)
|
||||||
|
if parent is not None:
|
||||||
while parent.parent is not None:
|
while parent.parent is not None:
|
||||||
parent = parent.parent
|
parent = parent.parent
|
||||||
|
else:
|
||||||
|
parent = definitions[0] # Already at a category
|
||||||
|
|
||||||
if parent.key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
|
if parent.key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
|
||||||
# Category was not in the list yet, so needs to be added now.
|
# Category was not in the list yet, so needs to be added now.
|
||||||
|
|
@ -136,14 +145,14 @@ class SettingInheritanceManager(QObject):
|
||||||
return self._settings_with_inheritance_warning
|
return self._settings_with_inheritance_warning
|
||||||
|
|
||||||
## Check if a setting has an inheritance function that is overwritten
|
## Check if a setting has an inheritance function that is overwritten
|
||||||
def _settingIsOverwritingInheritance(self, key, stack = None):
|
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
|
||||||
has_setting_function = False
|
has_setting_function = False
|
||||||
if not stack:
|
if not stack:
|
||||||
stack = self._active_container_stack
|
stack = self._active_container_stack
|
||||||
containers = []
|
containers = []
|
||||||
|
|
||||||
## Check if the setting has a user state. If not, it is never overwritten.
|
## Check if the setting has a user state. If not, it is never overwritten.
|
||||||
has_user_state = stack.getProperty(key, "state") == UM.Settings.InstanceState.User
|
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||||
if not has_user_state:
|
if not has_user_state:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -152,7 +161,7 @@ class SettingInheritanceManager(QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
||||||
if isinstance(stack.getTop().getProperty(key, "value"), UM.Settings.SettingFunction):
|
if isinstance(stack.getTop().getProperty(key, "value"), SettingFunction):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Mash all containers for all the stacks together.
|
## Mash all containers for all the stacks together.
|
||||||
|
|
@ -167,7 +176,7 @@ class SettingInheritanceManager(QObject):
|
||||||
continue
|
continue
|
||||||
if value is not None:
|
if value is not None:
|
||||||
# If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value
|
# If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value
|
||||||
has_setting_function = isinstance(value, UM.Settings.SettingFunction)
|
has_setting_function = isinstance(value, SettingFunction)
|
||||||
if has_setting_function:
|
if has_setting_function:
|
||||||
for setting_key in value.getUsedSettingKeys():
|
for setting_key in value.getUsedSettingKeys():
|
||||||
if setting_key in self._active_container_stack.getAllKeys():
|
if setting_key in self._active_container_stack.getAllKeys():
|
||||||
|
|
@ -188,6 +197,10 @@ class SettingInheritanceManager(QObject):
|
||||||
def _update(self):
|
def _update(self):
|
||||||
self._settings_with_inheritance_warning = [] # Reset previous data.
|
self._settings_with_inheritance_warning = [] # Reset previous data.
|
||||||
|
|
||||||
|
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
|
||||||
|
if self._global_container_stack is None:
|
||||||
|
return
|
||||||
|
|
||||||
# Check all setting keys that we know of and see if they are overridden.
|
# Check all setting keys that we know of and see if they are overridden.
|
||||||
for setting_key in self._global_container_stack.getAllKeys():
|
for setting_key in self._global_container_stack.getAllKeys():
|
||||||
override = self._settingIsOverwritingInheritance(setting_key)
|
override = self._settingIsOverwritingInheritance(setting_key)
|
||||||
|
|
@ -195,7 +208,7 @@ class SettingInheritanceManager(QObject):
|
||||||
self._settings_with_inheritance_warning.append(setting_key)
|
self._settings_with_inheritance_warning.append(setting_key)
|
||||||
|
|
||||||
# Check all the categories if any of their children have their inheritance overwritten.
|
# Check all the categories if any of their children have their inheritance overwritten.
|
||||||
for category in self._global_container_stack.getBottom().findDefinitions(type = "category"):
|
for category in self._global_container_stack.definition.findDefinitions(type = "category"):
|
||||||
if self._recursiveCheck(category):
|
if self._recursiveCheck(category):
|
||||||
self._settings_with_inheritance_warning.append(category.key)
|
self._settings_with_inheritance_warning.append(category.key)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
from UM.Signal import Signal, signalemitter
|
from UM.Signal import Signal, signalemitter
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
import UM.Logger
|
from UM.Logger import Logger
|
||||||
|
|
||||||
import cura.Settings
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
|
from cura.Settings.PerObjectContainerStack import PerObjectContainerStack
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
|
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
|
||||||
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
|
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
|
||||||
# this stack still resolve.
|
# this stack still resolve.
|
||||||
|
|
@ -22,17 +22,20 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
## Event indicating that the user selected a different extruder.
|
## Event indicating that the user selected a different extruder.
|
||||||
activeExtruderChanged = Signal()
|
activeExtruderChanged = Signal()
|
||||||
|
|
||||||
|
## Non-printing meshes
|
||||||
|
#
|
||||||
|
# If these settings are True for any mesh, the mesh does not need a convex hull,
|
||||||
|
# and is sent to the slicer regardless of whether it fits inside the build volume.
|
||||||
|
# Note that Support Mesh is not in here because it actually generates
|
||||||
|
# g-code in the volume of the mesh.
|
||||||
|
_non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._stack = ContainerStack(stack_id = id(self))
|
self._stack = PerObjectContainerStack(stack_id = id(self))
|
||||||
self._stack.setDirty(False) # This stack does not need to be saved.
|
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||||
self._instance = InstanceContainer(container_id = "SettingOverrideInstanceContainer")
|
self._stack.addContainer(InstanceContainer(container_id = "SettingOverrideInstanceContainer"))
|
||||||
self._stack.addContainer(self._instance)
|
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||||
|
|
||||||
if cura.Settings.ExtruderManager.getInstance().extruderCount > 1:
|
|
||||||
self._extruder_stack = cura.Settings.ExtruderManager.getInstance().getExtruderStack(0).getId()
|
|
||||||
else:
|
|
||||||
self._extruder_stack = None
|
|
||||||
|
|
||||||
self._stack.propertyChanged.connect(self._onSettingChanged)
|
self._stack.propertyChanged.connect(self._onSettingChanged)
|
||||||
|
|
||||||
|
|
@ -46,13 +49,14 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
## Create a fresh decorator object
|
## Create a fresh decorator object
|
||||||
deep_copy = SettingOverrideDecorator()
|
deep_copy = SettingOverrideDecorator()
|
||||||
## Copy the instance
|
## Copy the instance
|
||||||
deep_copy._instance = copy.deepcopy(self._instance, memo)
|
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
|
||||||
|
|
||||||
|
## Set the copied instance as the first (and only) instance container of the stack.
|
||||||
|
deep_copy._stack.replaceContainer(0, instance_container)
|
||||||
|
|
||||||
# Properly set the right extruder on the copy
|
# Properly set the right extruder on the copy
|
||||||
deep_copy.setActiveExtruder(self._extruder_stack)
|
deep_copy.setActiveExtruder(self._extruder_stack)
|
||||||
|
|
||||||
## Set the copied instance as the first (and only) instance container of the stack.
|
|
||||||
deep_copy._stack.replaceContainer(0, deep_copy._instance)
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
## Gets the currently active extruder to print this object with.
|
## Gets the currently active extruder to print this object with.
|
||||||
|
|
@ -77,8 +81,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
return container_stack.getMetaDataEntry("position", default=None)
|
return container_stack.getMetaDataEntry("position", default=None)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||||
if property_name == "value": # Only reslice if the value has changed.
|
# Trigger slice/need slicing if the value has changed.
|
||||||
Application.getInstance().getBackend().forceSlice()
|
if property_name == "value":
|
||||||
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
||||||
|
self._node._non_printing_mesh = any(self._stack.getProperty(setting, "value") for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
## Makes sure that the stack upon which the container stack is placed is
|
## Makes sure that the stack upon which the container stack is placed is
|
||||||
# kept up to date.
|
# kept up to date.
|
||||||
|
|
@ -92,10 +100,12 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
old_extruder_stack_id = ""
|
old_extruder_stack_id = ""
|
||||||
|
|
||||||
self._stack.setNextStack(extruder_stack[0])
|
self._stack.setNextStack(extruder_stack[0])
|
||||||
if self._stack.getNextStack().getId() != old_extruder_stack_id: #Only reslice if the extruder changed.
|
# Trigger slice/need slicing if the extruder changed.
|
||||||
Application.getInstance().getBackend().forceSlice()
|
if self._stack.getNextStack().getId() != old_extruder_stack_id:
|
||||||
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
|
Application.getInstance().getBackend().tickle()
|
||||||
else:
|
else:
|
||||||
UM.Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
|
Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
|
||||||
else:
|
else:
|
||||||
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||||
|
|
||||||
|
|
@ -105,6 +115,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def setActiveExtruder(self, extruder_stack_id):
|
def setActiveExtruder(self, extruder_stack_id):
|
||||||
self._extruder_stack = extruder_stack_id
|
self._extruder_stack = extruder_stack_id
|
||||||
self._updateNextStack()
|
self._updateNextStack()
|
||||||
|
ExtruderManager.getInstance().resetSelectedObjectExtruders()
|
||||||
self.activeExtruderChanged.emit()
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
def getStack(self):
|
def getStack(self):
|
||||||
|
|
|
||||||
92
cura/Settings/SimpleModeSettingsManager.py
Normal file
92
cura/Settings/SimpleModeSettingsManager.py
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleModeSettingsManager(QObject):
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._machine_manager = Application.getInstance().getMachineManager()
|
||||||
|
self._is_profile_customized = False # True when default profile has user changes
|
||||||
|
self._is_profile_user_created = False # True when profile was custom created by user
|
||||||
|
|
||||||
|
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
||||||
|
self._machine_manager.activeQualityChanged.connect(self._updateIsProfileUserCreated)
|
||||||
|
|
||||||
|
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
||||||
|
self._updateIsProfileCustomized()
|
||||||
|
self._updateIsProfileUserCreated()
|
||||||
|
|
||||||
|
isProfileCustomizedChanged = pyqtSignal()
|
||||||
|
isProfileUserCreatedChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
||||||
|
def isProfileCustomized(self):
|
||||||
|
return self._is_profile_customized
|
||||||
|
|
||||||
|
def _updateIsProfileCustomized(self):
|
||||||
|
user_setting_keys = set()
|
||||||
|
|
||||||
|
if not self._machine_manager.activeMachine:
|
||||||
|
return False
|
||||||
|
|
||||||
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
|
# check user settings in the global stack
|
||||||
|
user_setting_keys.update(set(global_stack.userChanges.getAllKeys()))
|
||||||
|
|
||||||
|
# check user settings in the extruder stacks
|
||||||
|
if global_stack.extruders:
|
||||||
|
for extruder_stack in global_stack.extruders.values():
|
||||||
|
user_setting_keys.update(set(extruder_stack.userChanges.getAllKeys()))
|
||||||
|
|
||||||
|
# remove settings that are visible in recommended (we don't show the reset button for those)
|
||||||
|
for skip_key in self.__ignored_custom_setting_keys:
|
||||||
|
if skip_key in user_setting_keys:
|
||||||
|
user_setting_keys.remove(skip_key)
|
||||||
|
|
||||||
|
has_customized_user_settings = len(user_setting_keys) > 0
|
||||||
|
|
||||||
|
if has_customized_user_settings != self._is_profile_customized:
|
||||||
|
self._is_profile_customized = has_customized_user_settings
|
||||||
|
self.isProfileCustomizedChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = isProfileUserCreatedChanged)
|
||||||
|
def isProfileUserCreated(self):
|
||||||
|
return self._is_profile_user_created
|
||||||
|
|
||||||
|
def _updateIsProfileUserCreated(self):
|
||||||
|
quality_changes_keys = set()
|
||||||
|
|
||||||
|
if not self._machine_manager.activeMachine:
|
||||||
|
return False
|
||||||
|
|
||||||
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
|
# check quality changes settings in the global stack
|
||||||
|
quality_changes_keys.update(set(global_stack.qualityChanges.getAllKeys()))
|
||||||
|
|
||||||
|
# check quality changes settings in the extruder stacks
|
||||||
|
if global_stack.extruders:
|
||||||
|
for extruder_stack in global_stack.extruders.values():
|
||||||
|
quality_changes_keys.update(set(extruder_stack.qualityChanges.getAllKeys()))
|
||||||
|
|
||||||
|
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
||||||
|
has_quality_changes = len(quality_changes_keys) > 0
|
||||||
|
|
||||||
|
if has_quality_changes != self._is_profile_user_created:
|
||||||
|
self._is_profile_user_created = has_quality_changes
|
||||||
|
self.isProfileUserCreatedChanged.emit()
|
||||||
|
|
||||||
|
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
||||||
|
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
||||||
|
__ignored_custom_setting_keys = ["support_enable",
|
||||||
|
"infill_sparse_density",
|
||||||
|
"gradual_infill_steps",
|
||||||
|
"adhesion_type",
|
||||||
|
"support_extruder_nr"]
|
||||||
130
cura/Settings/UserChangesModel.py
Normal file
130
cura/Settings/UserChangesModel.py
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, Qt
|
||||||
|
from UM.Application import Application
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangesModel(ListModel):
|
||||||
|
KeyRole = Qt.UserRole + 1
|
||||||
|
LabelRole = Qt.UserRole + 2
|
||||||
|
ExtruderRole = Qt.UserRole + 3
|
||||||
|
OriginalValueRole = Qt.UserRole + 4
|
||||||
|
UserValueRole = Qt.UserRole + 6
|
||||||
|
CategoryRole = Qt.UserRole + 7
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent = parent)
|
||||||
|
self.addRoleName(self.KeyRole, "key")
|
||||||
|
self.addRoleName(self.LabelRole, "label")
|
||||||
|
self.addRoleName(self.ExtruderRole, "extruder")
|
||||||
|
self.addRoleName(self.OriginalValueRole, "original_value")
|
||||||
|
self.addRoleName(self.UserValueRole, "user_value")
|
||||||
|
self.addRoleName(self.CategoryRole, "category")
|
||||||
|
|
||||||
|
self._i18n_catalog = None
|
||||||
|
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def forceUpdate(self):
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
item_dict = OrderedDict()
|
||||||
|
item_list = []
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return
|
||||||
|
stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
|
|
||||||
|
# Check if the definition container has a translation file and ensure it's loaded.
|
||||||
|
definition = global_stack.getBottom()
|
||||||
|
|
||||||
|
definition_suffix = ContainerRegistry.getMimeTypeForContainer(type(definition)).preferredSuffix
|
||||||
|
catalog = i18nCatalog(os.path.basename(definition.getId() + "." + definition_suffix))
|
||||||
|
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
|
for file_name in definition.getInheritedFiles():
|
||||||
|
catalog = i18nCatalog(os.path.basename(file_name))
|
||||||
|
if catalog.hasTranslationLoaded():
|
||||||
|
self._i18n_catalog = catalog
|
||||||
|
|
||||||
|
for stack in stacks:
|
||||||
|
# Make a list of all containers in the stack.
|
||||||
|
containers = []
|
||||||
|
latest_stack = stack
|
||||||
|
while latest_stack:
|
||||||
|
containers.extend(latest_stack.getContainers())
|
||||||
|
latest_stack = latest_stack.getNextStack()
|
||||||
|
|
||||||
|
# Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values
|
||||||
|
user_changes = containers.pop(0)
|
||||||
|
default_value_resolve_context = PropertyEvaluationContext(stack)
|
||||||
|
default_value_resolve_context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||||
|
default_value_resolve_context.context["override_operators"] = {
|
||||||
|
"extruderValue": ExtruderManager.getDefaultExtruderValue,
|
||||||
|
"extruderValues": ExtruderManager.getDefaultExtruderValues,
|
||||||
|
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
|
||||||
|
}
|
||||||
|
|
||||||
|
for setting_key in user_changes.getAllKeys():
|
||||||
|
original_value = None
|
||||||
|
|
||||||
|
# Find the category of the instance by moving up until we find a category.
|
||||||
|
category = user_changes.getInstance(setting_key).definition
|
||||||
|
while category.type != "category":
|
||||||
|
category = category.parent
|
||||||
|
|
||||||
|
# Handle translation (and fallback if we weren't able to find any translation files.
|
||||||
|
if self._i18n_catalog:
|
||||||
|
category_label = self._i18n_catalog.i18nc(category.key + " label", category.label)
|
||||||
|
else:
|
||||||
|
category_label = category.label
|
||||||
|
|
||||||
|
if self._i18n_catalog:
|
||||||
|
label = self._i18n_catalog.i18nc(setting_key + " label", stack.getProperty(setting_key, "label"))
|
||||||
|
else:
|
||||||
|
label = stack.getProperty(setting_key, "label")
|
||||||
|
|
||||||
|
for container in containers:
|
||||||
|
if stack == global_stack:
|
||||||
|
resolve = global_stack.getProperty(setting_key, "resolve", default_value_resolve_context)
|
||||||
|
if resolve is not None:
|
||||||
|
original_value = resolve
|
||||||
|
break
|
||||||
|
|
||||||
|
original_value = container.getProperty(setting_key, "value", default_value_resolve_context)
|
||||||
|
|
||||||
|
# If a value is a function, ensure it's called with the stack it's in.
|
||||||
|
if isinstance(original_value, SettingFunction):
|
||||||
|
original_value = original_value(stack, default_value_resolve_context)
|
||||||
|
|
||||||
|
if original_value is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
item_to_add = {"key": setting_key,
|
||||||
|
"label": label,
|
||||||
|
"user_value": str(user_changes.getProperty(setting_key, "value")),
|
||||||
|
"original_value": str(original_value),
|
||||||
|
"extruder": "",
|
||||||
|
"category": category_label}
|
||||||
|
|
||||||
|
if stack != global_stack:
|
||||||
|
item_to_add["extruder"] = stack.getName()
|
||||||
|
|
||||||
|
if category_label not in item_dict:
|
||||||
|
item_dict[category_label] = []
|
||||||
|
item_dict[category_label].append(item_to_add)
|
||||||
|
for each_item_list in item_dict.values():
|
||||||
|
item_list += each_item_list
|
||||||
|
self.setItems(item_list)
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
from cura.QualityManager import QualityManager
|
from cura.QualityManager import QualityManager
|
||||||
|
|
@ -22,15 +22,23 @@ class UserProfilesModel(ProfilesModel):
|
||||||
|
|
||||||
# Fetch the list of quality changes.
|
# Fetch the list of quality changes.
|
||||||
quality_manager = QualityManager.getInstance()
|
quality_manager = QualityManager.getInstance()
|
||||||
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
|
||||||
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||||
|
|
||||||
# Fetch the list of qualities
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
quality_list = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||||
ExtruderManager.getInstance().getActiveExtruderStacks())
|
extruder_stacks = self._getOrderedExtruderStacksList()
|
||||||
|
|
||||||
|
# Fetch the list of usable qualities across all extruders.
|
||||||
|
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||||
|
quality_list = quality_manager.findAllUsableQualitiesForMachineAndExtruders(global_container_stack, extruder_stacks)
|
||||||
|
|
||||||
# Filter the quality_change by the list of available quality_types
|
# Filter the quality_change by the list of available quality_types
|
||||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
filtered_quality_changes = [qc for qc in quality_changes_list if
|
||||||
|
qc.getMetaDataEntry("quality_type") in quality_type_set and
|
||||||
|
qc.getMetaDataEntry("extruder") is not None and
|
||||||
|
(qc.getMetaDataEntry("extruder") == active_extruder.definition.getMetaDataEntry("quality_definition") or
|
||||||
|
qc.getMetaDataEntry("extruder") == active_extruder.definition.getId())]
|
||||||
|
|
||||||
return filtered_quality_changes
|
return filtered_quality_changes
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,2 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from .MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
|
||||||
from .ContainerManager import ContainerManager
|
|
||||||
from .ContainerSettingsModel import ContainerSettingsModel
|
|
||||||
from .CuraContainerRegistry import CuraContainerRegistry
|
|
||||||
from .ExtruderManager import ExtruderManager
|
|
||||||
from .ExtrudersModel import ExtrudersModel
|
|
||||||
from .MachineManager import MachineManager
|
|
||||||
from .MachineNameValidator import MachineNameValidator
|
|
||||||
from .MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
|
||||||
from .SettingOverrideDecorator import SettingOverrideDecorator
|
|
||||||
from .QualitySettingsModel import QualitySettingsModel
|
|
||||||
from .SettingInheritanceManager import SettingInheritanceManager
|
|
||||||
from .ProfilesModel import ProfilesModel
|
|
||||||
from .QualityAndUserProfilesModel import QualityAndUserProfilesModel
|
|
||||||
from .UserProfilesModel import UserProfilesModel
|
|
||||||
|
|
|
||||||
117
cura/ShapeArray.py
Executable file
117
cura/ShapeArray.py
Executable file
|
|
@ -0,0 +1,117 @@
|
||||||
|
import numpy
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
|
||||||
|
|
||||||
|
## Polygon representation as an array for use with Arrange
|
||||||
|
class ShapeArray:
|
||||||
|
def __init__(self, arr, offset_x, offset_y, scale = 1):
|
||||||
|
self.arr = arr
|
||||||
|
self.offset_x = offset_x
|
||||||
|
self.offset_y = offset_y
|
||||||
|
self.scale = scale
|
||||||
|
|
||||||
|
## Instantiate from a bunch of vertices
|
||||||
|
# \param vertices
|
||||||
|
# \param scale scale the coordinates
|
||||||
|
@classmethod
|
||||||
|
def fromPolygon(cls, vertices, scale = 1):
|
||||||
|
# scale
|
||||||
|
vertices = vertices * scale
|
||||||
|
# flip y, x -> x, y
|
||||||
|
flip_vertices = numpy.zeros((vertices.shape))
|
||||||
|
flip_vertices[:, 0] = vertices[:, 1]
|
||||||
|
flip_vertices[:, 1] = vertices[:, 0]
|
||||||
|
flip_vertices = flip_vertices[::-1]
|
||||||
|
# offset, we want that all coordinates have positive values
|
||||||
|
offset_y = int(numpy.amin(flip_vertices[:, 0]))
|
||||||
|
offset_x = int(numpy.amin(flip_vertices[:, 1]))
|
||||||
|
flip_vertices[:, 0] = numpy.add(flip_vertices[:, 0], -offset_y)
|
||||||
|
flip_vertices[:, 1] = numpy.add(flip_vertices[:, 1], -offset_x)
|
||||||
|
shape = [int(numpy.amax(flip_vertices[:, 0])), int(numpy.amax(flip_vertices[:, 1]))]
|
||||||
|
arr = cls.arrayFromPolygon(shape, flip_vertices)
|
||||||
|
return cls(arr, offset_x, offset_y)
|
||||||
|
|
||||||
|
## Instantiate an offset and hull ShapeArray from a scene node.
|
||||||
|
# \param node source node where the convex hull must be present
|
||||||
|
# \param min_offset offset for the offset ShapeArray
|
||||||
|
# \param scale scale the coordinates
|
||||||
|
@classmethod
|
||||||
|
def fromNode(cls, node, min_offset, scale = 0.5):
|
||||||
|
transform = node._transformation
|
||||||
|
transform_x = transform._data[0][3]
|
||||||
|
transform_y = transform._data[2][3]
|
||||||
|
hull_verts = node.callDecoration("getConvexHull")
|
||||||
|
# For one_at_a_time printing you need the convex hull head.
|
||||||
|
hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
|
||||||
|
|
||||||
|
# If a model is to small then it will not contain any points
|
||||||
|
if not hull_verts.getPoints().any():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||||
|
offset_points = copy.deepcopy(offset_verts._points) # x, y
|
||||||
|
offset_points[:, 0] = numpy.add(offset_points[:, 0], -transform_x)
|
||||||
|
offset_points[:, 1] = numpy.add(offset_points[:, 1], -transform_y)
|
||||||
|
offset_shape_arr = ShapeArray.fromPolygon(offset_points, scale = scale)
|
||||||
|
|
||||||
|
hull_points = copy.deepcopy(hull_verts._points)
|
||||||
|
hull_points[:, 0] = numpy.add(hull_points[:, 0], -transform_x)
|
||||||
|
hull_points[:, 1] = numpy.add(hull_points[:, 1], -transform_y)
|
||||||
|
hull_shape_arr = ShapeArray.fromPolygon(hull_points, scale = scale) # x, y
|
||||||
|
|
||||||
|
return offset_shape_arr, hull_shape_arr
|
||||||
|
|
||||||
|
## Create np.array with dimensions defined by shape
|
||||||
|
# Fills polygon defined by vertices with ones, all other values zero
|
||||||
|
# Only works correctly for convex hull vertices
|
||||||
|
# Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array
|
||||||
|
# \param shape numpy format shape, [x-size, y-size]
|
||||||
|
# \param vertices
|
||||||
|
@classmethod
|
||||||
|
def arrayFromPolygon(cls, shape, vertices):
|
||||||
|
base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros
|
||||||
|
|
||||||
|
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||||
|
|
||||||
|
# Create check array for each edge segment, combine into fill array
|
||||||
|
for k in range(vertices.shape[0]):
|
||||||
|
fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
|
||||||
|
|
||||||
|
# Set all values inside polygon to one
|
||||||
|
base_array[fill] = 1
|
||||||
|
|
||||||
|
return base_array
|
||||||
|
|
||||||
|
## Return indices that mark one side of the line, used by arrayFromPolygon
|
||||||
|
# Uses the line defined by p1 and p2 to check array of
|
||||||
|
# input indices against interpolated value
|
||||||
|
# Returns boolean array, with True inside and False outside of shape
|
||||||
|
# Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array
|
||||||
|
# \param p1 2-tuple with x, y for point 1
|
||||||
|
# \param p2 2-tuple with x, y for point 2
|
||||||
|
# \param base_array boolean array to project the line on
|
||||||
|
@classmethod
|
||||||
|
def _check(cls, p1, p2, base_array):
|
||||||
|
if p1[0] == p2[0] and p1[1] == p2[1]:
|
||||||
|
return
|
||||||
|
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||||
|
|
||||||
|
p1 = p1.astype(float)
|
||||||
|
p2 = p2.astype(float)
|
||||||
|
|
||||||
|
if p2[0] == p1[0]:
|
||||||
|
sign = numpy.sign(p2[1] - p1[1])
|
||||||
|
return idxs[1] * sign
|
||||||
|
|
||||||
|
if p2[1] == p1[1]:
|
||||||
|
sign = numpy.sign(p2[0] - p1[0])
|
||||||
|
return idxs[1] * sign
|
||||||
|
|
||||||
|
# Calculate max column idx for each row idx based on interpolated line between two points
|
||||||
|
|
||||||
|
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
|
||||||
|
sign = numpy.sign(p2[0] - p1[0])
|
||||||
|
return idxs[1] * sign <= max_col_idx * sign
|
||||||
|
|
||||||
12
cura/SliceableObjectDecorator.py
Normal file
12
cura/SliceableObjectDecorator.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
class SliceableObjectDecorator(SceneNodeDecorator):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def isSliceable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __deepcopy__(self, memo):
|
||||||
|
return type(self)()
|
||||||
|
|
@ -3,6 +3,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
## A decorator that stores the amount an object has been moved below the platform.
|
## A decorator that stores the amount an object has been moved below the platform.
|
||||||
class ZOffsetDecorator(SceneNodeDecorator):
|
class ZOffsetDecorator(SceneNodeDecorator):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
self._z_offset = 0
|
self._z_offset = 0
|
||||||
|
|
||||||
def setZOffset(self, offset):
|
def setZOffset(self, offset):
|
||||||
|
|
|
||||||
37
cura_app.py
37
cura_app.py
|
|
@ -1,11 +1,11 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import platform
|
import platform
|
||||||
|
import faulthandler
|
||||||
|
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
|
|
||||||
|
|
@ -18,6 +18,12 @@ if Platform.isLinux(): # Needed for platform.linux_distribution, which is not av
|
||||||
libGL = find_library("GL")
|
libGL = find_library("GL")
|
||||||
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
||||||
|
|
||||||
|
# When frozen, i.e. installer version, don't let PYTHONPATH mess up the search path for DLLs.
|
||||||
|
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||||
|
try:
|
||||||
|
del os.environ["PYTHONPATH"]
|
||||||
|
except KeyError: pass
|
||||||
|
|
||||||
#WORKAROUND: GITHUB-704 GITHUB-708
|
#WORKAROUND: GITHUB-704 GITHUB-708
|
||||||
# It looks like setuptools creates a .pth file in
|
# It looks like setuptools creates a .pth file in
|
||||||
# the default /usr/lib which causes the default site-packages
|
# the default /usr/lib which causes the default site-packages
|
||||||
|
|
@ -35,8 +41,9 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u
|
||||||
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
|
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
|
||||||
|
|
||||||
def exceptHook(hook_type, value, traceback):
|
def exceptHook(hook_type, value, traceback):
|
||||||
import cura.CrashHandler
|
from cura.CrashHandler import CrashHandler
|
||||||
cura.CrashHandler.show(hook_type, value, traceback)
|
_crash_handler = CrashHandler(hook_type, value, traceback)
|
||||||
|
_crash_handler.show()
|
||||||
|
|
||||||
sys.excepthook = exceptHook
|
sys.excepthook = exceptHook
|
||||||
|
|
||||||
|
|
@ -45,18 +52,32 @@ sys.excepthook = exceptHook
|
||||||
# first seems to prevent Sip from going into a state where it
|
# first seems to prevent Sip from going into a state where it
|
||||||
# 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
|
||||||
from UM.Platform import Platform
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
import cura.Settings.CuraContainerRegistry
|
import cura.Settings.CuraContainerRegistry
|
||||||
|
|
||||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
def get_cura_dir_path():
|
||||||
dirpath = os.path.expanduser("~/AppData/Local/cura/")
|
if Platform.isWindows():
|
||||||
|
return os.path.expanduser("~/AppData/Local/cura/")
|
||||||
|
elif Platform.isLinux():
|
||||||
|
return os.path.expanduser("~/.local/share/cura")
|
||||||
|
elif Platform.isOSX():
|
||||||
|
return os.path.expanduser("~/Library/Logs/cura")
|
||||||
|
|
||||||
|
|
||||||
|
if hasattr(sys, "frozen"):
|
||||||
|
dirpath = get_cura_dir_path()
|
||||||
os.makedirs(dirpath, exist_ok = True)
|
os.makedirs(dirpath, exist_ok = True)
|
||||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
||||||
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
||||||
|
|
||||||
|
faulthandler.enable()
|
||||||
|
|
||||||
# Force an instance of CuraContainerRegistry to be created and reused later.
|
# Force an instance of CuraContainerRegistry to be created and reused later.
|
||||||
cura.Settings.CuraContainerRegistry.getInstance()
|
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
# This prestart up check is needed to determine if we should start the application at all.
|
||||||
|
if not cura.CuraApplication.CuraApplication.preStartUp():
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
app = cura.CuraApplication.CuraApplication.getInstance()
|
app = cura.CuraApplication.CuraApplication.getInstance()
|
||||||
app.run()
|
app.run()
|
||||||
|
|
|
||||||
37
docs/How_to_use_the_flame_graph_profiler.md
Normal file
37
docs/How_to_use_the_flame_graph_profiler.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
|
||||||
|
How to Profile Cura and See What It is Doing
|
||||||
|
============================================
|
||||||
|
Cura has a simple flame graph profiler available as a plugin which can be used to see what Cura is doing as it runs and how much time it takes. A flame graph profile shows its output as a timeline and stacks of "blocks" which represent parts of the code and are stacked up to show call depth. These often form little peaks which look like flames. It is a simple yet powerful way to visualise the activity of a program.
|
||||||
|
|
||||||
|
|
||||||
|
Setting up and installing the profiler
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
The profiler plugin is kept outside of the Cura source code here: https://github.com/sedwards2009/cura-big-flame-graph
|
||||||
|
|
||||||
|
To install it do:
|
||||||
|
|
||||||
|
* Use `git clone https://github.com/sedwards2009/cura-big-flame-graph.git` to grab a copy of the code.
|
||||||
|
* Copy the `BigFlameGraph` directory into the `plugins` directory in your local Cura.
|
||||||
|
* Set the `URANIUM_FLAME_PROFILER` environment variable to something before starting Cura. This flags to the profiler code in Cura to activate and insert the needed hooks into the code.
|
||||||
|
|
||||||
|
|
||||||
|
Using the profiler
|
||||||
|
------------------
|
||||||
|
To open the profiler go to the Extensions menu and select "Start BFG" from the "Big Flame Graph" menu. A page will open up in your default browser. This is the profiler UI. Click on "Record" to start recording, go to Cura and perform an action and then back in the profiler click on "Stop". The results should now load in.
|
||||||
|
|
||||||
|
The time scale is at the top of the window. The blocks should be read as meaning the blocks at the bottom call the blocks which are stacked on top of them. Hover the mouse to get more detailed information about a block such as the name of the code involved and its duration. Use the zoom buttons or mouse wheel to zoom in. The display can be panned by dragging with the left mouse button.
|
||||||
|
|
||||||
|
Note: The profiler front-end itself is quite "heavy" (ok, not optimised). It runs much better in Google Chrome or Chromium than Firefox. It is also a good idea to keep recording sessions short for the same reason.
|
||||||
|
|
||||||
|
|
||||||
|
What the Profiler Sees
|
||||||
|
----------------------
|
||||||
|
The profiler doesn't capture every function call in Cura. It hooks into a number of important systems which give a good picture of activity without too much run time overhead. The most important system is Uranium's signal mechanism and PyQt5 slots. Functions which are called via the signal mechanism are recorded and thier names appear in the results. PyQt5 slots appear in the results with the prefix `[SLOT]`.
|
||||||
|
|
||||||
|
Note that not all slots are captured. Only those slots which belong to classes which use the `pyqtSlot` decorator from the `UM.FlameProfiler` module.
|
||||||
|
|
||||||
|
|
||||||
|
Manually adding profiling code to more detail
|
||||||
|
---------------------------------------------
|
||||||
|
It is also possible to manually add decorators to methods to make them appear in the profiler results. The `UM.FlameProfiler` module contains the `profile` decorator which can be applied to methods. There is also a `profileCall` context manager which can be used with Python's `with` statement to measure a block of code. `profileCall` takes one argument, a label to use in the results.
|
||||||
|
|
@ -126,11 +126,7 @@ Section "Install Arduino Drivers"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section "Open STL files with Cura"
|
Section "Open STL files with Cura"
|
||||||
WriteRegStr HKCR .stl "" "Cura STL model file"
|
${registerExtension} "$INSTDIR\Cura.exe" ".stl" "STL_File"
|
||||||
DeleteRegValue HKCR .stl "Content Type"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\shell" "" "open"
|
|
||||||
WriteRegStr HKCR "Cura STL model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section /o "Open OBJ files with Cura"
|
Section /o "Open OBJ files with Cura"
|
||||||
|
|
|
||||||
265
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
265
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
|
|
@ -1,24 +1,31 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Mesh.MeshReader import MeshReader
|
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Math.Matrix import Matrix
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
|
||||||
import UM.Application
|
|
||||||
from UM.Job import Job
|
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|
||||||
from UM.Application import Application
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
from cura.QualityManager import QualityManager
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Math.Matrix import Matrix
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
|
from UM.Application import Application
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from cura.QualityManager import QualityManager
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from cura.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.ZOffsetDecorator import ZOffsetDecorator
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
|
||||||
|
import Savitar
|
||||||
|
import numpy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if not MYPY:
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
|
|
@ -37,98 +44,10 @@ class ThreeMFReader(MeshReader):
|
||||||
self._base_name = ""
|
self._base_name = ""
|
||||||
self._unit = None
|
self._unit = None
|
||||||
|
|
||||||
def _createNodeFromObject(self, object, name = ""):
|
|
||||||
node = SceneNode()
|
|
||||||
node.setName(name)
|
|
||||||
mesh_builder = MeshBuilder()
|
|
||||||
vertex_list = []
|
|
||||||
|
|
||||||
components = object.find(".//3mf:components", self._namespaces)
|
|
||||||
if components:
|
|
||||||
for component in components:
|
|
||||||
id = component.get("objectid")
|
|
||||||
new_object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
|
|
||||||
new_node = self._createNodeFromObject(new_object, self._base_name + "_" + str(id))
|
|
||||||
node.addChild(new_node)
|
|
||||||
transform = component.get("transform")
|
|
||||||
if transform is not None:
|
|
||||||
new_node.setTransformation(self._createMatrixFromTransformationString(transform))
|
|
||||||
|
|
||||||
# for vertex in entry.mesh.vertices.vertex:
|
|
||||||
for vertex in object.findall(".//3mf:vertex", self._namespaces):
|
|
||||||
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
xml_settings = list(object.findall(".//cura:setting", self._namespaces))
|
|
||||||
|
|
||||||
# Add the setting override decorator, so we can add settings to this node.
|
|
||||||
if xml_settings:
|
|
||||||
node.addDecorator(SettingOverrideDecorator())
|
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
# Ensure the correct next container for the SettingOverride decorator is set.
|
|
||||||
if global_container_stack:
|
|
||||||
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
|
||||||
# Ensure that all extruder data is reset
|
|
||||||
if not multi_extrusion:
|
|
||||||
default_stack_id = global_container_stack.getId()
|
|
||||||
else:
|
|
||||||
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
|
||||||
if default_stack:
|
|
||||||
default_stack_id = default_stack.getId()
|
|
||||||
else:
|
|
||||||
default_stack_id = global_container_stack.getId()
|
|
||||||
node.callDecoration("setActiveExtruder", default_stack_id)
|
|
||||||
|
|
||||||
# Get the definition & set it
|
|
||||||
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
|
||||||
node.callDecoration("getStack").getTop().setDefinition(definition)
|
|
||||||
|
|
||||||
setting_container = node.callDecoration("getStack").getTop()
|
|
||||||
for setting in xml_settings:
|
|
||||||
setting_key = setting.get("key")
|
|
||||||
setting_value = setting.text
|
|
||||||
|
|
||||||
# Extruder_nr is a special case.
|
|
||||||
if setting_key == "extruder_nr":
|
|
||||||
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
|
|
||||||
if extruder_stack:
|
|
||||||
node.callDecoration("setActiveExtruder", extruder_stack.getId())
|
|
||||||
else:
|
|
||||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
|
||||||
continue
|
|
||||||
setting_container.setProperty(setting_key,"value", setting_value)
|
|
||||||
|
|
||||||
if len(node.getChildren()) > 0:
|
|
||||||
group_decorator = GroupDecorator()
|
|
||||||
node.addDecorator(group_decorator)
|
|
||||||
|
|
||||||
triangles = object.findall(".//3mf:triangle", self._namespaces)
|
|
||||||
mesh_builder.reserveFaceCount(len(triangles))
|
|
||||||
|
|
||||||
for triangle in triangles:
|
|
||||||
v1 = int(triangle.get("v1"))
|
|
||||||
v2 = int(triangle.get("v2"))
|
|
||||||
v3 = int(triangle.get("v3"))
|
|
||||||
|
|
||||||
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
|
|
||||||
vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
|
|
||||||
vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
|
|
||||||
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
# TODO: We currently do not check for normals and simply recalculate them.
|
|
||||||
mesh_builder.calculateNormals()
|
|
||||||
mesh_builder.setFileName(name)
|
|
||||||
mesh_data = mesh_builder.build()
|
|
||||||
|
|
||||||
if len(mesh_data.getVertices()):
|
|
||||||
node.setMeshData(mesh_data)
|
|
||||||
|
|
||||||
node.setSelectable(True)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _createMatrixFromTransformationString(self, transformation):
|
def _createMatrixFromTransformationString(self, transformation):
|
||||||
|
if transformation == "":
|
||||||
|
return Matrix()
|
||||||
|
|
||||||
splitted_transformation = transformation.split()
|
splitted_transformation = transformation.split()
|
||||||
## Transformation is saved as:
|
## Transformation is saved as:
|
||||||
## M00 M01 M02 0.0
|
## M00 M01 M02 0.0
|
||||||
|
|
@ -155,40 +74,102 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
return temp_mat
|
return temp_mat
|
||||||
|
|
||||||
|
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
|
||||||
|
# \returns Uranium scene node.
|
||||||
|
def _convertSavitarNodeToUMNode(self, savitar_node):
|
||||||
|
um_node = SceneNode()
|
||||||
|
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||||
|
um_node.setTransformation(transformation)
|
||||||
|
mesh_builder = MeshBuilder()
|
||||||
|
|
||||||
|
data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)
|
||||||
|
|
||||||
|
vertices = numpy.resize(data, (int(data.size / 3), 3))
|
||||||
|
mesh_builder.setVertices(vertices)
|
||||||
|
mesh_builder.calculateNormals(fast=True)
|
||||||
|
mesh_data = mesh_builder.build()
|
||||||
|
|
||||||
|
if len(mesh_data.getVertices()):
|
||||||
|
um_node.setMeshData(mesh_data)
|
||||||
|
|
||||||
|
for child in savitar_node.getChildren():
|
||||||
|
child_node = self._convertSavitarNodeToUMNode(child)
|
||||||
|
if child_node:
|
||||||
|
um_node.addChild(child_node)
|
||||||
|
|
||||||
|
if um_node.getMeshData() is None and len(um_node.getChildren()) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
settings = savitar_node.getSettings()
|
||||||
|
|
||||||
|
# Add the setting override decorator, so we can add settings to this node.
|
||||||
|
if settings:
|
||||||
|
um_node.addDecorator(SettingOverrideDecorator())
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
# Ensure the correct next container for the SettingOverride decorator is set.
|
||||||
|
if global_container_stack:
|
||||||
|
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
||||||
|
|
||||||
|
if default_stack:
|
||||||
|
um_node.callDecoration("setActiveExtruder", default_stack.getId())
|
||||||
|
|
||||||
|
# Get the definition & set it
|
||||||
|
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
||||||
|
um_node.callDecoration("getStack").getTop().setDefinition(definition)
|
||||||
|
|
||||||
|
setting_container = um_node.callDecoration("getStack").getTop()
|
||||||
|
|
||||||
|
for key in settings:
|
||||||
|
setting_value = settings[key]
|
||||||
|
|
||||||
|
# Extruder_nr is a special case.
|
||||||
|
if key == "extruder_nr":
|
||||||
|
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
|
||||||
|
if extruder_stack:
|
||||||
|
um_node.callDecoration("setActiveExtruder", extruder_stack.getId())
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||||
|
continue
|
||||||
|
setting_container.setProperty(key, "value", setting_value)
|
||||||
|
|
||||||
|
if len(um_node.getChildren()) > 0:
|
||||||
|
group_decorator = GroupDecorator()
|
||||||
|
um_node.addDecorator(group_decorator)
|
||||||
|
um_node.setSelectable(True)
|
||||||
|
if um_node.getMeshData():
|
||||||
|
# Assuming that all nodes with mesh data are printable objects
|
||||||
|
# affects (auto) slicing
|
||||||
|
sliceable_decorator = SliceableObjectDecorator()
|
||||||
|
um_node.addDecorator(sliceable_decorator)
|
||||||
|
return um_node
|
||||||
|
|
||||||
def read(self, file_name):
|
def read(self, file_name):
|
||||||
result = []
|
result = []
|
||||||
# The base object of 3mf is a zipped archive.
|
# The base object of 3mf is a zipped archive.
|
||||||
|
try:
|
||||||
archive = zipfile.ZipFile(file_name, "r")
|
archive = zipfile.ZipFile(file_name, "r")
|
||||||
self._base_name = os.path.basename(file_name)
|
self._base_name = os.path.basename(file_name)
|
||||||
try:
|
parser = Savitar.ThreeMFParser()
|
||||||
self._root = ET.parse(archive.open("3D/3dmodel.model"))
|
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
|
||||||
self._unit = self._root.getroot().get("unit")
|
self._unit = scene_3mf.getUnit()
|
||||||
|
for node in scene_3mf.getSceneNodes():
|
||||||
build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces)
|
um_node = self._convertSavitarNodeToUMNode(node)
|
||||||
|
if um_node is None:
|
||||||
for build_item in build_items:
|
|
||||||
id = build_item.get("objectid")
|
|
||||||
object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
|
|
||||||
if "type" in object.attrib:
|
|
||||||
if object.attrib["type"] == "support" or object.attrib["type"] == "other":
|
|
||||||
# Ignore support objects, as cura does not support these.
|
|
||||||
# We can't guarantee that they wont be made solid.
|
|
||||||
# We also ignore "other", as I have no idea what to do with them.
|
|
||||||
Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"])
|
|
||||||
continue
|
|
||||||
elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model":
|
|
||||||
pass # Load these as normal
|
|
||||||
else:
|
|
||||||
# We should technically fail at this point because it's an invalid 3MF, but try to continue anyway.
|
|
||||||
Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec",
|
|
||||||
object.attrib["type"])
|
|
||||||
continue
|
continue
|
||||||
|
# compensate for original center position, if object(s) is/are not around its zero position
|
||||||
|
|
||||||
build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id))
|
transform_matrix = Matrix()
|
||||||
transform = build_item.get("transform")
|
mesh_data = um_node.getMeshData()
|
||||||
if transform is not None:
|
if mesh_data is not None:
|
||||||
build_item_node.setTransformation(self._createMatrixFromTransformationString(transform))
|
extents = mesh_data.getExtents()
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
||||||
|
transform_matrix.setByTranslation(center_vector)
|
||||||
|
transform_matrix.multiply(um_node.getLocalTransformation())
|
||||||
|
um_node.setTransformation(transform_matrix)
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
||||||
# First step: flip the y and z axis.
|
# First step: flip the y and z axis.
|
||||||
|
|
@ -208,18 +189,26 @@ class ThreeMFReader(MeshReader):
|
||||||
translation_matrix.setByTranslation(translation_vector)
|
translation_matrix.setByTranslation(translation_vector)
|
||||||
transformation_matrix.multiply(translation_matrix)
|
transformation_matrix.multiply(translation_matrix)
|
||||||
|
|
||||||
# Third step: 3MF also defines a unit, wheras Cura always assumes mm.
|
# Third step: 3MF also defines a unit, whereas Cura always assumes mm.
|
||||||
scale_matrix = Matrix()
|
scale_matrix = Matrix()
|
||||||
scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
|
scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
|
||||||
transformation_matrix.multiply(scale_matrix)
|
transformation_matrix.multiply(scale_matrix)
|
||||||
|
|
||||||
# Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
|
# Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
|
||||||
build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix))
|
um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
|
||||||
|
|
||||||
result.append(build_item_node)
|
# Check if the model is positioned below the build plate and honor that when loading project files.
|
||||||
|
if um_node.getMeshData() is not None:
|
||||||
|
minimum_z_value = um_node.getMeshData().getExtents(um_node.getWorldTransformation()).minimum.y # y is z in transformation coordinates
|
||||||
|
if minimum_z_value < 0:
|
||||||
|
um_node.addDecorator(ZOffsetDecorator())
|
||||||
|
um_node.callDecoration("setZOffset", minimum_z_value)
|
||||||
|
|
||||||
except Exception as e:
|
result.append(um_node)
|
||||||
Logger.log("e", "An exception occurred in 3mf reader: %s", e)
|
|
||||||
|
except Exception:
|
||||||
|
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||||
|
return []
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
||||||
990
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file → Executable file
990
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file → Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +1,18 @@
|
||||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject, pyqtProperty, QCoreApplication
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceDialog(QObject):
|
class WorkspaceDialog(QObject):
|
||||||
showDialogSignal = pyqtSignal()
|
showDialogSignal = pyqtSignal()
|
||||||
|
|
@ -18,20 +24,169 @@ class WorkspaceDialog(QObject):
|
||||||
self._view = None
|
self._view = None
|
||||||
self._qml_url = "WorkspaceDialog.qml"
|
self._qml_url = "WorkspaceDialog.qml"
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._default_strategy = "override"
|
self._default_strategy = None
|
||||||
self._result = {"machine": self._default_strategy,
|
self._result = {"machine": self._default_strategy,
|
||||||
"quality_changes": self._default_strategy,
|
"quality_changes": self._default_strategy,
|
||||||
|
"definition_changes": self._default_strategy,
|
||||||
"material": self._default_strategy}
|
"material": self._default_strategy}
|
||||||
self._visible = False
|
self._visible = False
|
||||||
self.showDialogSignal.connect(self.__show)
|
self.showDialogSignal.connect(self.__show)
|
||||||
|
|
||||||
self._has_quality_changes_conflict = False
|
self._has_quality_changes_conflict = False
|
||||||
|
self._has_definition_changes_conflict = False
|
||||||
self._has_machine_conflict = False
|
self._has_machine_conflict = False
|
||||||
self._has_material_conflict = False
|
self._has_material_conflict = False
|
||||||
|
self._has_visible_settings_field = False
|
||||||
|
self._num_visible_settings = 0
|
||||||
|
self._num_user_settings = 0
|
||||||
|
self._active_mode = ""
|
||||||
|
self._quality_name = ""
|
||||||
|
self._num_settings_overriden_by_quality_changes = 0
|
||||||
|
self._quality_type = ""
|
||||||
|
self._machine_name = ""
|
||||||
|
self._machine_type = ""
|
||||||
|
self._variant_type = ""
|
||||||
|
self._material_labels = []
|
||||||
|
self._extruders = []
|
||||||
|
self._objects_on_plate = False
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
|
definitionChangesConflictChanged = pyqtSignal()
|
||||||
materialConflictChanged = pyqtSignal()
|
materialConflictChanged = pyqtSignal()
|
||||||
|
numVisibleSettingsChanged = pyqtSignal()
|
||||||
|
activeModeChanged = pyqtSignal()
|
||||||
|
qualityNameChanged = pyqtSignal()
|
||||||
|
hasVisibleSettingsFieldChanged = pyqtSignal()
|
||||||
|
numSettingsOverridenByQualityChangesChanged = pyqtSignal()
|
||||||
|
qualityTypeChanged = pyqtSignal()
|
||||||
|
machineNameChanged = pyqtSignal()
|
||||||
|
materialLabelsChanged = pyqtSignal()
|
||||||
|
objectsOnPlateChanged = pyqtSignal()
|
||||||
|
numUserSettingsChanged = pyqtSignal()
|
||||||
|
machineTypeChanged = pyqtSignal()
|
||||||
|
variantTypeChanged = pyqtSignal()
|
||||||
|
extrudersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=variantTypeChanged)
|
||||||
|
def variantType(self):
|
||||||
|
return self._variant_type
|
||||||
|
|
||||||
|
def setVariantType(self, variant_type):
|
||||||
|
if self._variant_type != variant_type:
|
||||||
|
self._variant_type = variant_type
|
||||||
|
self.variantTypeChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=machineTypeChanged)
|
||||||
|
def machineType(self):
|
||||||
|
return self._machine_type
|
||||||
|
|
||||||
|
def setMachineType(self, machine_type):
|
||||||
|
self._machine_type = machine_type
|
||||||
|
self.machineTypeChanged.emit()
|
||||||
|
|
||||||
|
def setNumUserSettings(self, num_user_settings):
|
||||||
|
if self._num_user_settings != num_user_settings:
|
||||||
|
self._num_user_settings = num_user_settings
|
||||||
|
self.numVisibleSettingsChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify=numUserSettingsChanged)
|
||||||
|
def numUserSettings(self):
|
||||||
|
return self._num_user_settings
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=objectsOnPlateChanged)
|
||||||
|
def hasObjectsOnPlate(self):
|
||||||
|
return self._objects_on_plate
|
||||||
|
|
||||||
|
def setHasObjectsOnPlate(self, objects_on_plate):
|
||||||
|
if self._objects_on_plate != objects_on_plate:
|
||||||
|
self._objects_on_plate = objects_on_plate
|
||||||
|
self.objectsOnPlateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = materialLabelsChanged)
|
||||||
|
def materialLabels(self):
|
||||||
|
return self._material_labels
|
||||||
|
|
||||||
|
def setMaterialLabels(self, material_labels):
|
||||||
|
if self._material_labels != material_labels:
|
||||||
|
self._material_labels = material_labels
|
||||||
|
self.materialLabelsChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify=extrudersChanged)
|
||||||
|
def extruders(self):
|
||||||
|
return self._extruders
|
||||||
|
|
||||||
|
def setExtruders(self, extruders):
|
||||||
|
if self._extruders != extruders:
|
||||||
|
self._extruders = extruders
|
||||||
|
self.extrudersChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = machineNameChanged)
|
||||||
|
def machineName(self):
|
||||||
|
return self._machine_name
|
||||||
|
|
||||||
|
def setMachineName(self, machine_name):
|
||||||
|
if self._machine_name != machine_name:
|
||||||
|
self._machine_name = machine_name
|
||||||
|
self.machineNameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=qualityTypeChanged)
|
||||||
|
def qualityType(self):
|
||||||
|
return self._quality_type
|
||||||
|
|
||||||
|
def setQualityType(self, quality_type):
|
||||||
|
if self._quality_type != quality_type:
|
||||||
|
self._quality_type = quality_type
|
||||||
|
self.qualityTypeChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
||||||
|
def numSettingsOverridenByQualityChanges(self):
|
||||||
|
return self._num_settings_overriden_by_quality_changes
|
||||||
|
|
||||||
|
def setNumSettingsOverridenByQualityChanges(self, num_settings_overriden_by_quality_changes):
|
||||||
|
self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes
|
||||||
|
self.numSettingsOverridenByQualityChangesChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=qualityNameChanged)
|
||||||
|
def qualityName(self):
|
||||||
|
return self._quality_name
|
||||||
|
|
||||||
|
def setQualityName(self, quality_name):
|
||||||
|
if self._quality_name != quality_name:
|
||||||
|
self._quality_name = quality_name
|
||||||
|
self.qualityNameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=activeModeChanged)
|
||||||
|
def activeMode(self):
|
||||||
|
return self._active_mode
|
||||||
|
|
||||||
|
def setActiveMode(self, active_mode):
|
||||||
|
if active_mode == 0:
|
||||||
|
self._active_mode = i18n_catalog.i18nc("@title:tab", "Recommended")
|
||||||
|
else:
|
||||||
|
self._active_mode = i18n_catalog.i18nc("@title:tab", "Custom")
|
||||||
|
self.activeModeChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = hasVisibleSettingsFieldChanged)
|
||||||
|
def hasVisibleSettingsField(self):
|
||||||
|
return self._has_visible_settings_field
|
||||||
|
|
||||||
|
def setHasVisibleSettingsField(self, has_visible_settings_field):
|
||||||
|
self._has_visible_settings_field = has_visible_settings_field
|
||||||
|
self.hasVisibleSettingsFieldChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
|
def totalNumberOfSettings(self):
|
||||||
|
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = numVisibleSettingsChanged)
|
||||||
|
def numVisibleSettings(self):
|
||||||
|
return self._num_visible_settings
|
||||||
|
|
||||||
|
def setNumVisibleSettings(self, num_visible_settings):
|
||||||
|
if self._num_visible_settings != num_visible_settings:
|
||||||
|
self._num_visible_settings = num_visible_settings
|
||||||
|
self.numVisibleSettingsChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = machineConflictChanged)
|
@pyqtProperty(bool, notify = machineConflictChanged)
|
||||||
def machineConflict(self):
|
def machineConflict(self):
|
||||||
|
|
@ -41,6 +196,10 @@ class WorkspaceDialog(QObject):
|
||||||
def qualityChangesConflict(self):
|
def qualityChangesConflict(self):
|
||||||
return self._has_quality_changes_conflict
|
return self._has_quality_changes_conflict
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=definitionChangesConflictChanged)
|
||||||
|
def definitionChangesConflict(self):
|
||||||
|
return self._has_definition_changes_conflict
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=materialConflictChanged)
|
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||||
def materialConflict(self):
|
def materialConflict(self):
|
||||||
return self._has_material_conflict
|
return self._has_material_conflict
|
||||||
|
|
@ -50,36 +209,53 @@ class WorkspaceDialog(QObject):
|
||||||
if key in self._result:
|
if key in self._result:
|
||||||
self._result[key] = strategy
|
self._result[key] = strategy
|
||||||
|
|
||||||
|
## Close the backend: otherwise one could end up with "Slicing..."
|
||||||
|
@pyqtSlot()
|
||||||
|
def closeBackend(self):
|
||||||
|
Application.getInstance().getBackend().close()
|
||||||
|
|
||||||
def setMaterialConflict(self, material_conflict):
|
def setMaterialConflict(self, 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):
|
||||||
|
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):
|
||||||
|
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 setDefinitionChangesConflict(self, definition_changes_conflict):
|
||||||
|
if self._has_definition_changes_conflict != definition_changes_conflict:
|
||||||
|
self._has_definition_changes_conflict = definition_changes_conflict
|
||||||
|
self.definitionChangesConflictChanged.emit()
|
||||||
|
|
||||||
def getResult(self):
|
def getResult(self):
|
||||||
if "machine" in self._result and not self._has_machine_conflict:
|
if "machine" in self._result and not self._has_machine_conflict:
|
||||||
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
|
||||||
|
if "definition_changes" in self._result and not self._has_definition_changes_conflict:
|
||||||
|
self._result["definition_changes"] = None
|
||||||
if "material" in self._result and not self._has_material_conflict:
|
if "material" in self._result and not self._has_material_conflict:
|
||||||
self._result["material"] = None
|
self._result["material"] = None
|
||||||
|
|
||||||
|
# If the machine needs to be re-created, the definition_changes should also be re-created.
|
||||||
|
# If the machine strategy is None, it means that there is no name conflict with existing ones. In this case
|
||||||
|
# new definitions changes are created
|
||||||
|
if "machine" in self._result:
|
||||||
|
if self._result["machine"] == "new" or self._result["machine"] is None and self._result["definition_changes"] is None:
|
||||||
|
self._result["definition_changes"] = "new"
|
||||||
|
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
def _createViewFromQML(self):
|
def _createViewFromQML(self):
|
||||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url))
|
path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)
|
||||||
self._component = QQmlComponent(Application.getInstance()._engine, path)
|
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
self._context = QQmlContext(Application.getInstance()._engine.rootContext())
|
|
||||||
self._context.setContextProperty("manager", self)
|
|
||||||
self._view = self._component.create(self._context)
|
|
||||||
if self._view is None:
|
|
||||||
Logger.log("c", "QQmlComponent status %s", self._component.status())
|
|
||||||
Logger.log("c", "QQmlComponent error string %s", self._component.errorString())
|
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
# Emit signal so the right thread actually shows the view.
|
# Emit signal so the right thread actually shows the view.
|
||||||
|
|
@ -88,6 +264,7 @@ class WorkspaceDialog(QObject):
|
||||||
# Reset the result
|
# Reset the result
|
||||||
self._result = {"machine": self._default_strategy,
|
self._result = {"machine": self._default_strategy,
|
||||||
"quality_changes": self._default_strategy,
|
"quality_changes": self._default_strategy,
|
||||||
|
"definition_changes": self._default_strategy,
|
||||||
"material": self._default_strategy}
|
"material": self._default_strategy}
|
||||||
self._visible = True
|
self._visible = True
|
||||||
self.showDialogSignal.emit()
|
self.showDialogSignal.emit()
|
||||||
|
|
@ -95,14 +272,28 @@ class WorkspaceDialog(QObject):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
## Used to notify the dialog so the lock can be released.
|
## Used to notify the dialog so the lock can be released.
|
||||||
def notifyClosed(self):
|
def notifyClosed(self):
|
||||||
if self._result is None:
|
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
||||||
self._result = {}
|
self._visible = False
|
||||||
|
try:
|
||||||
self._lock.release()
|
self._lock.release()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def hide(self):
|
def hide(self):
|
||||||
self._visible = False
|
self._visible = False
|
||||||
self._lock.release()
|
|
||||||
self._view.hide()
|
self._view.hide()
|
||||||
|
try:
|
||||||
|
self._lock.release()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
|
def _onVisibilityChanged(self, visible):
|
||||||
|
if not visible:
|
||||||
|
try:
|
||||||
|
self._lock.release()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def onOkButtonClicked(self):
|
def onOkButtonClicked(self):
|
||||||
|
|
@ -111,9 +302,9 @@ class WorkspaceDialog(QObject):
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def onCancelButtonClicked(self):
|
def onCancelButtonClicked(self):
|
||||||
|
self._result = {}
|
||||||
self._view.hide()
|
self._view.hide()
|
||||||
self.hide()
|
self.hide()
|
||||||
self._result = {}
|
|
||||||
|
|
||||||
## Block thread until the dialog is closed.
|
## Block thread until the dialog is closed.
|
||||||
def waitForClose(self):
|
def waitForClose(self):
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2016 Ultimaker B.V.
|
// Copyright (c) 2016 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the AGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.1
|
import QtQuick 2.1
|
||||||
import QtQuick.Controls 1.1
|
import QtQuick.Controls 1.1
|
||||||
|
|
@ -10,15 +10,16 @@ import UM 1.1 as UM
|
||||||
|
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
||||||
title: catalog.i18nc("@title:window", "Import workspace conflict")
|
id: base
|
||||||
|
title: catalog.i18nc("@title:window", "Open Project")
|
||||||
|
|
||||||
width: 350 * Screen.devicePixelRatio;
|
minimumWidth: 500 * screenScaleFactor
|
||||||
minimumWidth: 350 * Screen.devicePixelRatio;
|
minimumHeight: 450 * screenScaleFactor
|
||||||
maximumWidth: 350 * Screen.devicePixelRatio;
|
width: minimumWidth
|
||||||
|
height: minimumHeight
|
||||||
|
|
||||||
height: 250 * Screen.devicePixelRatio;
|
property int comboboxHeight: 15 * screenScaleFactor
|
||||||
minimumHeight: 250 * Screen.devicePixelRatio;
|
property int spacerHeight: 10 * screenScaleFactor
|
||||||
maximumHeight: 250 * Screen.devicePixelRatio;
|
|
||||||
|
|
||||||
onClosing: manager.notifyClosed()
|
onClosing: manager.notifyClosed()
|
||||||
onVisibleChanged:
|
onVisibleChanged:
|
||||||
|
|
@ -27,17 +28,23 @@ UM.Dialog
|
||||||
{
|
{
|
||||||
machineResolveComboBox.currentIndex = 0
|
machineResolveComboBox.currentIndex = 0
|
||||||
qualityChangesResolveComboBox.currentIndex = 0
|
qualityChangesResolveComboBox.currentIndex = 0
|
||||||
materialConflictComboBox.currentIndex = 0
|
materialResolveComboBox.currentIndex = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
anchors.margins: 20 * screenScaleFactor
|
||||||
|
|
||||||
UM.I18nCatalog
|
UM.I18nCatalog
|
||||||
{
|
{
|
||||||
id: catalog;
|
id: catalog
|
||||||
name: "cura";
|
name: "cura"
|
||||||
|
}
|
||||||
|
SystemPalette
|
||||||
|
{
|
||||||
|
id: palette
|
||||||
}
|
}
|
||||||
|
|
||||||
ListModel
|
ListModel
|
||||||
|
|
@ -48,7 +55,7 @@ UM.Dialog
|
||||||
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||||
Component.onCompleted:
|
Component.onCompleted:
|
||||||
{
|
{
|
||||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Override existing")});
|
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update existing")});
|
||||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,36 +63,55 @@ UM.Dialog
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
spacing: 2 * screenScaleFactor
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: infoLabel
|
id: titleLabel
|
||||||
|
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||||
|
font.pointSize: 18
|
||||||
|
}
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: separator
|
||||||
|
color: palette.text
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: catalog.i18nc("@action:label", "Cura detected a number of conflicts while importing the workspace. How would you like to resolve these?")
|
height: 1
|
||||||
wrapMode: Text.Wrap
|
}
|
||||||
height: 50
|
Item // Spacer
|
||||||
|
{
|
||||||
|
height: spacerHeight
|
||||||
|
width: height
|
||||||
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
height: childrenRect.height
|
||||||
|
width: parent.width
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Printer settings")
|
||||||
|
font.bold: true
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
// spacer
|
||||||
|
height: spacerHeight
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
}
|
}
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
id: machineResolveTooltip
|
id: machineResolveTooltip
|
||||||
width: parent.width
|
width: (parent.width / 3) | 0
|
||||||
height: visible ? 25 : 0
|
height: visible ? comboboxHeight : 0
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
|
||||||
visible: manager.machineConflict
|
visible: manager.machineConflict
|
||||||
Row
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@action:label","Machine")
|
|
||||||
width: 150
|
|
||||||
}
|
|
||||||
|
|
||||||
ComboBox
|
ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: machineResolveComboBox
|
id: machineResolveComboBox
|
||||||
|
width: parent.width
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
|
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
|
||||||
|
|
@ -93,28 +119,72 @@ UM.Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
id: qualityChangesResolveTooltip
|
|
||||||
width: parent.width
|
|
||||||
height: visible ? 25 : 0
|
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
|
||||||
visible: manager.qualityChangesConflict
|
|
||||||
Row
|
Row
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label","Profile")
|
text: catalog.i18nc("@action:label", "Type")
|
||||||
width: 150
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: manager.machineType
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: manager.machineName
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item // Spacer
|
||||||
|
{
|
||||||
|
height: spacerHeight
|
||||||
|
width: height
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
height: childrenRect.height
|
||||||
|
width: parent.width
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Profile settings")
|
||||||
|
font.bold: true
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
// spacer
|
||||||
|
height: spacerHeight
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
id: qualityChangesResolveTooltip
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: manager.qualityChangesConflict
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||||
ComboBox
|
ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: qualityChangesResolveComboBox
|
id: qualityChangesResolveComboBox
|
||||||
|
width: parent.width
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||||
|
|
@ -122,28 +192,87 @@ UM.Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
id: materialResolveTooltip
|
|
||||||
width: parent.width
|
|
||||||
height: visible ? 25 : 0
|
|
||||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material(s) be resolved?")
|
|
||||||
visible: manager.materialConflict
|
|
||||||
Row
|
Row
|
||||||
{
|
{
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@action:label","Material")
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
width: 150
|
width: (parent.width / 3) | 0
|
||||||
}
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: manager.qualityName
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: manager.numUserSettings != 0 ? childrenRect.height : 0
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Not in profile")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
visible: manager.numUserSettings != 0
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: manager.numSettingsOverridenByQualityChanges != 0 ? childrenRect.height : 0
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Derivative from")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||||
|
}
|
||||||
|
Item // Spacer
|
||||||
|
{
|
||||||
|
height: spacerHeight
|
||||||
|
width: height
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
height: childrenRect.height
|
||||||
|
width: parent.width
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Material settings")
|
||||||
|
font.bold: true
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
// spacer
|
||||||
|
height: spacerHeight
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
id: materialResolveTooltip
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: manager.materialConflict
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||||
ComboBox
|
ComboBox
|
||||||
{
|
{
|
||||||
model: resolveStrategiesModel
|
model: resolveStrategiesModel
|
||||||
textRole: "label"
|
textRole: "label"
|
||||||
id: materialResolveComboBox
|
id: materialResolveComboBox
|
||||||
|
width: parent.width
|
||||||
onActivated:
|
onActivated:
|
||||||
{
|
{
|
||||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||||
|
|
@ -151,22 +280,119 @@ UM.Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
Repeater
|
||||||
rightButtons: [
|
|
||||||
Button
|
|
||||||
{
|
{
|
||||||
id: ok_button
|
model: manager.materialLabels
|
||||||
text: catalog.i18nc("@action:button","OK");
|
delegate: Row
|
||||||
onClicked: { manager.onOkButtonClicked() }
|
{
|
||||||
enabled: true
|
width: parent.width
|
||||||
},
|
height: childrenRect.height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: modelData
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item // Spacer
|
||||||
|
{
|
||||||
|
height: spacerHeight
|
||||||
|
width: height
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Setting visibility")
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Mode")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: manager.activeMode
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
visible: manager.hasVisibleSettingsField
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Visible settings:")
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item // Spacer
|
||||||
|
{
|
||||||
|
height: spacerHeight
|
||||||
|
width: height
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
visible: manager.hasObjectsOnPlate
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
width: warningLabel.height
|
||||||
|
height: width
|
||||||
|
|
||||||
|
source: UM.Theme.getIcon("notice")
|
||||||
|
color: palette.text
|
||||||
|
|
||||||
|
}
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: warningLabel
|
||||||
|
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Button
|
Button
|
||||||
{
|
{
|
||||||
id: cancel_button
|
id: cancel_button
|
||||||
text: catalog.i18nc("@action:button","Cancel");
|
text: catalog.i18nc("@action:button","Cancel");
|
||||||
onClicked: { manager.onCancelButtonClicked() }
|
onClicked: { manager.onCancelButtonClicked() }
|
||||||
enabled: true
|
enabled: true
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: ok_button.left
|
||||||
|
anchors.rightMargin: 2 * screenScaleFactor
|
||||||
|
}
|
||||||
|
Button
|
||||||
|
{
|
||||||
|
id: ok_button
|
||||||
|
text: catalog.i18nc("@action:button","Open");
|
||||||
|
onClicked: { manager.closeBackend(); manager.onOkButtonClicked() }
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.right: parent.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reject() {
|
||||||
|
manager.onCancelButtonClicked();
|
||||||
|
base.visible = false;
|
||||||
|
base.rejected();
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,37 +1,48 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Dict
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
try:
|
||||||
from . import ThreeMFReader
|
from . import ThreeMFReader
|
||||||
|
except ImportError:
|
||||||
|
Logger.log("w", "Could not import ThreeMFReader; libSavitar may be missing")
|
||||||
|
|
||||||
from . import ThreeMFWorkspaceReader
|
from . import ThreeMFWorkspaceReader
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Platform import Platform
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
def getMetaData() -> Dict:
|
||||||
|
# Workarround for osx not supporting double file extensions correctly.
|
||||||
|
if Platform.isOSX():
|
||||||
|
workspace_extension = "3mf"
|
||||||
|
else:
|
||||||
|
workspace_extension = "curaproject.3mf"
|
||||||
|
|
||||||
def getMetaData():
|
metaData = {}
|
||||||
return {
|
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||||
"plugin": {
|
metaData["mesh_reader"] = [
|
||||||
"name": catalog.i18nc("@label", "3MF Reader"),
|
|
||||||
"author": "Ultimaker",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
|
|
||||||
"api": 3
|
|
||||||
},
|
|
||||||
"mesh_reader": [
|
|
||||||
{
|
|
||||||
"extension": "3mf",
|
|
||||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workspace_reader":
|
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"extension": "3mf",
|
"extension": "3mf",
|
||||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
metaData["workspace_reader"] = [
|
||||||
|
{
|
||||||
|
"extension": workspace_extension,
|
||||||
|
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return metaData
|
||||||
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
|
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||||
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
|
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
|
||||||
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
|
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
|
||||||
8
plugins/3MFReader/plugin.json
Normal file
8
plugins/3MFReader/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "3MF Reader",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Provides support for reading 3MF files.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Preferences import Preferences
|
from UM.Preferences import Preferences
|
||||||
|
|
@ -5,6 +8,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
|
|
@ -41,11 +45,26 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
self._writeContainerToArchive(container, archive)
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
# Write preferences to archive
|
# Write preferences to archive
|
||||||
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
|
original_preferences = Preferences.getInstance() #Copy only the preferences that we use to the workspace.
|
||||||
|
temp_preferences = Preferences()
|
||||||
|
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded"}:
|
||||||
|
temp_preferences.addPreference(preference, None)
|
||||||
|
temp_preferences.setValue(preference, original_preferences.getValue(preference))
|
||||||
preferences_string = StringIO()
|
preferences_string = StringIO()
|
||||||
Preferences.getInstance().writeToFile(preferences_string)
|
temp_preferences.writeToFile(preferences_string)
|
||||||
|
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
|
||||||
archive.writestr(preferences_file, preferences_string.getvalue())
|
archive.writestr(preferences_file, preferences_string.getvalue())
|
||||||
|
|
||||||
|
# Save Cura version
|
||||||
|
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||||
|
version_config_parser = configparser.ConfigParser()
|
||||||
|
version_config_parser.add_section("versions")
|
||||||
|
version_config_parser.set("versions", "cura_version", Application.getStaticVersion())
|
||||||
|
|
||||||
|
version_file_string = StringIO()
|
||||||
|
version_config_parser.write(version_file_string)
|
||||||
|
archive.writestr(version_file, version_file_string.getvalue())
|
||||||
|
|
||||||
# Close the archive & reset states.
|
# Close the archive & reset states.
|
||||||
archive.close()
|
archive.close()
|
||||||
mesh_writer.setStoreArchive(False)
|
mesh_writer.setStoreArchive(False)
|
||||||
|
|
@ -56,7 +75,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
# \param archive The archive to write to.
|
# \param archive The archive to write to.
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _writeContainerToArchive(container, archive):
|
def _writeContainerToArchive(container, archive):
|
||||||
if type(container) == type(ContainerRegistry.getInstance().getEmptyInstanceContainer()):
|
if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||||
return # Empty file, do nothing.
|
return # Empty file, do nothing.
|
||||||
|
|
||||||
file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix
|
file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix
|
||||||
|
|
@ -75,4 +94,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
# For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
# For some reason we have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||||
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
archive.writestr(file_in_archive, container.serialize())
|
# Do not include the network authentication keys
|
||||||
|
ignore_keys = ["network_authentication_id", "network_authentication_key"]
|
||||||
|
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
|
||||||
|
|
||||||
|
archive.writestr(file_in_archive, serialized_data)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,20 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Settings.SettingRelation import RelationType
|
from UM.Application import Application
|
||||||
|
import UM.Scene.SceneNode
|
||||||
|
|
||||||
|
import Savitar
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
try:
|
try:
|
||||||
|
if not MYPY:
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
|
|
@ -44,7 +51,7 @@ class ThreeMFWriter(MeshWriter):
|
||||||
result += str(matrix._data[2, 2]) + " "
|
result += str(matrix._data[2, 2]) + " "
|
||||||
result += str(matrix._data[0, 3]) + " "
|
result += str(matrix._data[0, 3]) + " "
|
||||||
result += str(matrix._data[1, 3]) + " "
|
result += str(matrix._data[1, 3]) + " "
|
||||||
result += str(matrix._data[2,3]) + " "
|
result += str(matrix._data[2, 3])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Should we store the archive
|
## Should we store the archive
|
||||||
|
|
@ -53,6 +60,48 @@ class ThreeMFWriter(MeshWriter):
|
||||||
def setStoreArchive(self, store_archive):
|
def setStoreArchive(self, store_archive):
|
||||||
self._store_archive = store_archive
|
self._store_archive = store_archive
|
||||||
|
|
||||||
|
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
|
||||||
|
# \returns Uranium Scenen node.
|
||||||
|
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
|
||||||
|
if type(um_node) is not UM.Scene.SceneNode.SceneNode:
|
||||||
|
return None
|
||||||
|
|
||||||
|
savitar_node = Savitar.SceneNode()
|
||||||
|
|
||||||
|
node_matrix = um_node.getLocalTransformation()
|
||||||
|
|
||||||
|
matrix_string = self._convertMatrixToString(node_matrix.preMultiply(transformation))
|
||||||
|
|
||||||
|
savitar_node.setTransformation(matrix_string)
|
||||||
|
mesh_data = um_node.getMeshData()
|
||||||
|
if mesh_data is not None:
|
||||||
|
savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray())
|
||||||
|
indices_array = mesh_data.getIndicesAsByteArray()
|
||||||
|
if indices_array is not None:
|
||||||
|
savitar_node.getMeshData().setFacesFromBytes(indices_array)
|
||||||
|
else:
|
||||||
|
savitar_node.getMeshData().setFacesFromBytes(numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring())
|
||||||
|
|
||||||
|
# Handle per object settings (if any)
|
||||||
|
stack = um_node.callDecoration("getStack")
|
||||||
|
if stack is not None:
|
||||||
|
changed_setting_keys = set(stack.getTop().getAllKeys())
|
||||||
|
|
||||||
|
# Ensure that we save the extruder used for this object in a multi-extrusion setup
|
||||||
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
|
changed_setting_keys.add("extruder_nr")
|
||||||
|
|
||||||
|
# Get values for all changed settings & save them.
|
||||||
|
for key in changed_setting_keys:
|
||||||
|
savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
|
||||||
|
|
||||||
|
for child_node in um_node.getChildren():
|
||||||
|
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
|
||||||
|
if savitar_child_node is not None:
|
||||||
|
savitar_node.addChild(savitar_child_node)
|
||||||
|
|
||||||
|
return savitar_node
|
||||||
|
|
||||||
def getArchive(self):
|
def getArchive(self):
|
||||||
return self._archive
|
return self._archive
|
||||||
|
|
||||||
|
|
@ -77,98 +126,14 @@ class ThreeMFWriter(MeshWriter):
|
||||||
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
|
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
|
||||||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
||||||
|
|
||||||
model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"])
|
savitar_scene = Savitar.Scene()
|
||||||
resources = ET.SubElement(model, "resources")
|
|
||||||
build = ET.SubElement(model, "build")
|
|
||||||
|
|
||||||
added_nodes = []
|
|
||||||
index = 0 # Ensure index always exists (even if there are no nodes to write)
|
|
||||||
# Write all nodes with meshData to the file as objects inside the resource tag
|
|
||||||
for index, n in enumerate(MeshWriter._meshNodes(nodes)):
|
|
||||||
added_nodes.append(n) # Save the nodes that have mesh data
|
|
||||||
object = ET.SubElement(resources, "object", id = str(index+1), type = "model")
|
|
||||||
mesh = ET.SubElement(object, "mesh")
|
|
||||||
|
|
||||||
mesh_data = n.getMeshData()
|
|
||||||
vertices = ET.SubElement(mesh, "vertices")
|
|
||||||
verts = mesh_data.getVertices()
|
|
||||||
|
|
||||||
if verts is None:
|
|
||||||
Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.")
|
|
||||||
continue # No mesh data, nothing to do.
|
|
||||||
if mesh_data.hasIndices():
|
|
||||||
for face in mesh_data.getIndices():
|
|
||||||
v1 = verts[face[0]]
|
|
||||||
v2 = verts[face[1]]
|
|
||||||
v3 = verts[face[2]]
|
|
||||||
xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2]))
|
|
||||||
xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2]))
|
|
||||||
xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2]))
|
|
||||||
|
|
||||||
triangles = ET.SubElement(mesh, "triangles")
|
|
||||||
for face in mesh_data.getIndices():
|
|
||||||
triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2]))
|
|
||||||
else:
|
|
||||||
triangles = ET.SubElement(mesh, "triangles")
|
|
||||||
for idx, vert in enumerate(verts):
|
|
||||||
xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2]))
|
|
||||||
|
|
||||||
# If we have no faces defined, assume that every three subsequent vertices form a face.
|
|
||||||
if idx % 3 == 0:
|
|
||||||
triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2))
|
|
||||||
|
|
||||||
# Handle per object settings
|
|
||||||
stack = n.callDecoration("getStack")
|
|
||||||
if stack is not None:
|
|
||||||
changed_setting_keys = set(stack.getTop().getAllKeys())
|
|
||||||
|
|
||||||
# Ensure that we save the extruder used for this object.
|
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
|
||||||
changed_setting_keys.add("extruder_nr")
|
|
||||||
|
|
||||||
settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"])
|
|
||||||
|
|
||||||
# Get values for all changed settings & save them.
|
|
||||||
for key in changed_setting_keys:
|
|
||||||
setting_xml = ET.SubElement(settings_xml, "setting", key = key)
|
|
||||||
setting_xml.text = str(stack.getProperty(key, "value"))
|
|
||||||
|
|
||||||
# Add one to the index as we haven't incremented the last iteration.
|
|
||||||
index += 1
|
|
||||||
nodes_to_add = set()
|
|
||||||
|
|
||||||
for node in added_nodes:
|
|
||||||
# Check the parents of the nodes with mesh_data and ensure that they are also added.
|
|
||||||
parent_node = node.getParent()
|
|
||||||
while parent_node is not None:
|
|
||||||
if parent_node.callDecoration("isGroup"):
|
|
||||||
nodes_to_add.add(parent_node)
|
|
||||||
parent_node = parent_node.getParent()
|
|
||||||
else:
|
|
||||||
parent_node = None
|
|
||||||
|
|
||||||
# Sort all the nodes by depth (so nodes with the highest depth are done first)
|
|
||||||
sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True)
|
|
||||||
|
|
||||||
# We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene
|
|
||||||
for node in sorted_nodes_to_add:
|
|
||||||
object = ET.SubElement(resources, "object", id=str(index + 1), type="model")
|
|
||||||
components = ET.SubElement(object, "components")
|
|
||||||
for child in node.getChildren():
|
|
||||||
if child in added_nodes:
|
|
||||||
component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation()))
|
|
||||||
index += 1
|
|
||||||
added_nodes.append(node)
|
|
||||||
|
|
||||||
# Create a transformation Matrix to convert from our worldspace into 3MF.
|
|
||||||
# First step: flip the y and z axis.
|
|
||||||
transformation_matrix = Matrix()
|
transformation_matrix = Matrix()
|
||||||
transformation_matrix._data[1, 1] = 0
|
transformation_matrix._data[1, 1] = 0
|
||||||
transformation_matrix._data[1, 2] = -1
|
transformation_matrix._data[1, 2] = -1
|
||||||
transformation_matrix._data[2, 1] = 1
|
transformation_matrix._data[2, 1] = 1
|
||||||
transformation_matrix._data[2, 2] = 0
|
transformation_matrix._data[2, 2] = 0
|
||||||
|
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
||||||
# build volume.
|
# build volume.
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
|
|
@ -179,14 +144,22 @@ class ThreeMFWriter(MeshWriter):
|
||||||
translation_matrix.setByTranslation(translation_vector)
|
translation_matrix.setByTranslation(translation_vector)
|
||||||
transformation_matrix.preMultiply(translation_matrix)
|
transformation_matrix.preMultiply(translation_matrix)
|
||||||
|
|
||||||
# Find out what the final build items are and add them.
|
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
||||||
for node in added_nodes:
|
for node in nodes:
|
||||||
if node.getParent().callDecoration("isGroup") is None:
|
if node == root_node:
|
||||||
node_matrix = node.getLocalTransformation()
|
for root_child in node.getChildren():
|
||||||
|
savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix)
|
||||||
|
if savitar_node:
|
||||||
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
|
else:
|
||||||
|
savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix)
|
||||||
|
if savitar_node:
|
||||||
|
savitar_scene.addSceneNode(savitar_node)
|
||||||
|
|
||||||
ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix)))
|
parser = Savitar.ThreeMFParser()
|
||||||
|
scene_string = parser.sceneToString(savitar_scene)
|
||||||
|
|
||||||
archive.writestr(model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model))
|
archive.writestr(model_file, scene_string)
|
||||||
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
|
archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types))
|
||||||
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,51 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
try:
|
||||||
|
from . import ThreeMFWriter
|
||||||
|
except ImportError:
|
||||||
|
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
|
||||||
|
from . import ThreeMFWorkspaceWriter
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from . import ThreeMFWorkspaceWriter
|
from UM.Platform import Platform
|
||||||
from . import ThreeMFWriter
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("uranium")
|
i18n_catalog = i18nCatalog("uranium")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
return {
|
# Workarround for osx not supporting double file extensions correctly.
|
||||||
"plugin": {
|
if Platform.isOSX():
|
||||||
"name": i18n_catalog.i18nc("@label", "3MF Writer"),
|
workspace_extension = "3mf"
|
||||||
"author": "Ultimaker",
|
else:
|
||||||
"version": "1.0",
|
workspace_extension = "curaproject.3mf"
|
||||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."),
|
|
||||||
"api": 3
|
metaData = {}
|
||||||
},
|
|
||||||
"mesh_writer": {
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
|
metaData["mesh_writer"] = {
|
||||||
"output": [{
|
"output": [{
|
||||||
"extension": "3mf",
|
"extension": "3mf",
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||||
}]
|
}]
|
||||||
},
|
}
|
||||||
"workspace_writer": {
|
metaData["workspace_writer"] = {
|
||||||
"output": [{
|
"output": [{
|
||||||
"extension": "3mf",
|
"extension": workspace_extension,
|
||||||
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||||
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
"mime_type": "application/x-curaproject+xml",
|
||||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return metaData
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
|
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
|
||||||
|
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
|
||||||
8
plugins/3MFWriter/plugin.json
Normal file
8
plugins/3MFWriter/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "3MF Writer",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Provides support for writing 3MF files.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from . import AutoSave
|
from . import AutoSave
|
||||||
|
|
||||||
|
|
@ -7,15 +7,7 @@ from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
return {
|
return {}
|
||||||
"plugin": {
|
|
||||||
"name": catalog.i18nc("@label", "Auto Save"),
|
|
||||||
"author": "Ultimaker",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Automatically saves Preferences, Machines and Profiles after changes."),
|
|
||||||
"api": 3
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return { "extension": AutoSave.AutoSave() }
|
return { "extension": AutoSave.AutoSave() }
|
||||||
|
|
|
||||||
8
plugins/AutoSave/plugin.json
Normal file
8
plugins/AutoSave/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Auto Save",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Extension import Extension
|
from UM.Extension import Extension
|
||||||
|
|
@ -8,9 +8,7 @@ from UM.Application import Application
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
|
|
||||||
from PyQt5.QtQuick import QQuickView
|
from PyQt5.QtCore import pyqtSlot, QObject
|
||||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
|
||||||
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
|
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import collections
|
import collections
|
||||||
|
|
@ -107,9 +105,5 @@ class ChangeLog(Extension, QObject,):
|
||||||
self._changelog_window.hide()
|
self._changelog_window.hide()
|
||||||
|
|
||||||
def createChangelogWindow(self):
|
def createChangelogWindow(self):
|
||||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml"))
|
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
|
||||||
|
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
component = QQmlComponent(Application.getInstance()._engine, path)
|
|
||||||
self._changelog_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
|
||||||
self._changelog_context.setContextProperty("manager", self)
|
|
||||||
self._changelog_window = component.create(self._changelog_context)
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2015 Ultimaker B.V.
|
// Copyright (c) 2015 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the AGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.1
|
import QtQuick 2.1
|
||||||
import QtQuick.Controls 1.3
|
import QtQuick.Controls 1.3
|
||||||
|
|
@ -11,8 +11,8 @@ import UM 1.1 as UM
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
||||||
id: base
|
id: base
|
||||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width * 0.75
|
minimumWidth: (UM.Theme.getSize("modal_window_minimum").width * 0.75) | 0
|
||||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height * 0.75
|
minimumHeight: (UM.Theme.getSize("modal_window_minimum").height * 0.75) | 0
|
||||||
width: minimumWidth
|
width: minimumWidth
|
||||||
height: minimumHeight
|
height: minimumHeight
|
||||||
title: catalog.i18nc("@label", "Changelog")
|
title: catalog.i18nc("@label", "Changelog")
|
||||||
|
|
|
||||||
646
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file → Executable file
646
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file → Executable file
|
|
@ -1,3 +1,487 @@
|
||||||
|
[3.1.0]
|
||||||
|
*Profile added for 0.25 mm print core
|
||||||
|
This new print core gives extra fine line widths which gives prints extra definition and surface quality.
|
||||||
|
|
||||||
|
*Profile added for Breakaway material
|
||||||
|
New material profile for Breakaway material, a new dry post processing support material, which can be used for models with flat surface area overhangs.
|
||||||
|
|
||||||
|
*Layer view
|
||||||
|
The existing Layer View has been updated in order to see a live simulation of all the paths within a layer.
|
||||||
|
|
||||||
|
*Quick camera controls
|
||||||
|
New buttons have been added to the interface that can quickly reposition the camera view of the buildplate.
|
||||||
|
|
||||||
|
*Increased processing speeds and performance
|
||||||
|
A 5-10% speed increase when calculating normals, loading models and slicing.
|
||||||
|
|
||||||
|
*Jogging
|
||||||
|
It allows the printhead to be moved with on-screen controls. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Large model loading
|
||||||
|
A new feature has been added which unloads the layer view when switching to solid mode, speeding Ultimaker Cura back up without losing your G-code/layer view information.
|
||||||
|
|
||||||
|
*Scripts folder
|
||||||
|
A scripts folder is now available in the Ultimaker Cura configuration folder. This folder can be loaded with post processing plugins scripts, which will automatically show in Ultimaker Cura. Contributed by fieldOfView.
|
||||||
|
|
||||||
|
*Optimized workflow for crash reporting
|
||||||
|
Crash reports are automatically generated and allow the user, in case of a crash, to easily send their report with a description to developers.
|
||||||
|
|
||||||
|
*Floating models enabled
|
||||||
|
In previous releases, models were dropped to the build plate when support was disabled. Models now float when the setting is enabled (even if creates an impossible-to-print situation). This can be used to stack separate models on top of each other.
|
||||||
|
|
||||||
|
*Slicing tolerance
|
||||||
|
A new setting that affects the intersect point to influence the dimensional accuracy for diagonal surfaces. The user can select the behaviour: ‘Inclusive’ makes gaps narrower, ‘Exclusive’ makes gaps wider, and ‘Middle’ is the fastest to process. This can be used to create better tolerances for printed screw holes. Contributed by BagelOrb.
|
||||||
|
|
||||||
|
*Optimized zig zag patterns
|
||||||
|
Zig zag patterns now print more consistently. Lines now have a 5 micron tolerance in which they are printed any way, resulting in longer connected lines. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Aligned z-seam inner wall moves
|
||||||
|
Inner wall travel moves are aligned with the z-seam. This reduces the number of travel moves and reduces the chance of more unwanted seams.
|
||||||
|
|
||||||
|
*Relative positioning of infill patterns
|
||||||
|
Infill patterns are now positioned relative to the center of loaded models and an offset can be applied to control the infill more precisely and adjust it to preference or strength. Contributed by smartavionics.
|
||||||
|
|
||||||
|
*Line resolution
|
||||||
|
Enables the user to specify the minimum allowed distance value between two points in G-code to create lower or higher resolution polygons.
|
||||||
|
|
||||||
|
*Custom mode changes
|
||||||
|
If profile settings have been modified in recommended mode under custom mode, a reset icon will appear to notify the user. Click the icon to show the changes that have been made, and revert back to the default profile settings.
|
||||||
|
|
||||||
|
*Bugfixes
|
||||||
|
- Fix for layer numbers being displayed incorrectly when switching between solid and layer mode
|
||||||
|
- Fix for Ultimaker Cura engine crashes on certain models
|
||||||
|
- Fix for Uninstalling previous versions of Cura on Windows platforms
|
||||||
|
- Fix for displaying visible settings
|
||||||
|
- Fix for loading legacy profiles
|
||||||
|
- Fix for importing custom single extrusion profile
|
||||||
|
|
||||||
|
[3.0.4]
|
||||||
|
*Bug fixes
|
||||||
|
- Fixed OpenGL issue that prevents Cura from starting.
|
||||||
|
|
||||||
|
*License agreement on the first startup has been added
|
||||||
|
|
||||||
|
[3.0.3]
|
||||||
|
*Bug fixes
|
||||||
|
- Add missing libraries for the MakePrintable plugin.
|
||||||
|
|
||||||
|
[3.0.0]
|
||||||
|
*Faster start-up
|
||||||
|
Start-up speed has been cut in half compared to the previous version.
|
||||||
|
|
||||||
|
*New color scheme
|
||||||
|
Color scheme has been updated to reflect Ultimaker Cura rebrand.
|
||||||
|
|
||||||
|
*Updated UX design
|
||||||
|
The Ultimaker Cura logo has moved from the bottom to the top of the interface. Print status icons have been updated and repositioned.
|
||||||
|
|
||||||
|
*Redesigned splash screen
|
||||||
|
A new splash screen on Ultimaker Cura startup has been added.
|
||||||
|
|
||||||
|
*Top navigation bar improvements
|
||||||
|
The width of tab functionality changes accordingly to the word space (multilingual).
|
||||||
|
|
||||||
|
*Print quality slider
|
||||||
|
A slider can now be used to control the quality profile in recommended mode.
|
||||||
|
|
||||||
|
*Infill slider
|
||||||
|
Model infill can now be changed using a slider in recommended mode.
|
||||||
|
|
||||||
|
*Changed layer view
|
||||||
|
Layer view icon, panel and slider have moved to top-right of interface.
|
||||||
|
|
||||||
|
*Rasterized build plate
|
||||||
|
The build plate now shows graduations of 10 mm and 1 mm for easy model positioning.
|
||||||
|
|
||||||
|
*Changed row of extruder buttons
|
||||||
|
Extruder tabs have become buttons and icons have been updated.
|
||||||
|
|
||||||
|
*Add an "Export to Cura" button in SOLIDWORKS
|
||||||
|
SOLIDWORKS plugin can now be installed using an automatic installer.
|
||||||
|
|
||||||
|
*Siemens NX macro
|
||||||
|
When a user updates models in Siemens NX and clicks the button, the updated models replace the models opened in Ultimaker Cura.
|
||||||
|
|
||||||
|
*Skin removal width
|
||||||
|
Remove thin strips of skin from a model to prevent print head zigzagging, in turn preventing vibrations.
|
||||||
|
|
||||||
|
*Skin expand distance
|
||||||
|
Cutting away skins on steep overhangs makes prints less sturdy. By expanding skins with the thickness of walls, features will be better supported. In addition, features such as towers on top of infill will be stronger.
|
||||||
|
|
||||||
|
*Extra skin wall count
|
||||||
|
Printing extra skin directly on top of infill can lead to gaps, curling and pillowing. This is reduced by printing a wall around the skin first, and also improves the printing speed.
|
||||||
|
|
||||||
|
*Minimum extrusion for skin
|
||||||
|
Will prevent filling small gaps that are probably filled already, resulting in less strings, better top details and faster prints.
|
||||||
|
|
||||||
|
*PVA retractions
|
||||||
|
PVA (switch) retraction length is increased, minimum travel distance for retraction is decreased and max count is slightly increased, this reduces stringing by a lot at the cost of slightly increased print time.
|
||||||
|
|
||||||
|
*Z seam options
|
||||||
|
Gives the user control over where to place the seam - hide it in convex corners or in easy to remove locations such as concave corners. Don’t let corner angles influence the seam position.
|
||||||
|
|
||||||
|
*Quarter cubic infill
|
||||||
|
Similar to tetrahedral (octet) infill, but half of the lines are shifted half of the period up. This pattern sacrifices some rigidity of octet infill for greater toughness.
|
||||||
|
|
||||||
|
*Cross infill
|
||||||
|
A fractal pattern infill that requires fewer retractions than other infill types. This is useful for flexible materials as it causes less material elongation. The internal structure given by this infill also assists flexible models having more resistance, while retaining ‘soft’ properties in all directions.
|
||||||
|
|
||||||
|
*Layer start negative position
|
||||||
|
Layer start X/Y values can be less than 0 when the machine centre is zero.
|
||||||
|
|
||||||
|
*PostProcessing stretch script
|
||||||
|
This new script performs "post stretch" algorithm to fix the problem of insufficient inner and outer diameters. Thanks to electrocbd for contributing.
|
||||||
|
|
||||||
|
*Ironing speed settings
|
||||||
|
Ironing speed settings have been moved to experimental category.
|
||||||
|
|
||||||
|
*Doodle3D plugin
|
||||||
|
Update Doodle3D plugin to connect with printers. Thanks to mith for contributing.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Customized profiles are not sent when connecting to a printer
|
||||||
|
- Sync z-hop with layer changes, thanks to smartavionics for contributing
|
||||||
|
- Memory leaks on MacOS
|
||||||
|
- Printer name not loaded when project file is opened
|
||||||
|
- Doodle3D Wifi box was selected by default on non-UM3 printers
|
||||||
|
|
||||||
|
[2.7.0]
|
||||||
|
*Top surface skin
|
||||||
|
Specify print settings of the top-most layers separately in order to improve print duration and achieve higher quality top surfaces.
|
||||||
|
|
||||||
|
*Print thin walls
|
||||||
|
An experimental function that enables features to be printed up to two times smaller than the nozzle size.
|
||||||
|
|
||||||
|
*Ironing (a.k.a. Neosanding)
|
||||||
|
An experimental function that enables the heated nozzle to travel over printed top layers without extrusion to create a smooth finish. Made after an idea by Neotko.
|
||||||
|
|
||||||
|
*Gradual support infill
|
||||||
|
Supports will print faster and with less material while improving overhang quality.
|
||||||
|
|
||||||
|
*Support infill layer thickness
|
||||||
|
Users are able to configure “Support infill layer thickness” for thicker support layers.
|
||||||
|
|
||||||
|
*Relative Z seam
|
||||||
|
A function that positions the Z seam relative to the bounding box of the model, so that the seam stays at the same location no matter what the position of the object is.
|
||||||
|
|
||||||
|
*Prime tower purge
|
||||||
|
In order to prevent under extrusion when printing a prime tower, and to prevent a prime tower failing half way through a job, a feature has been added to wipe off oozed/purged material in the middle of a hollow prime tower before starting to print the next layer of it. The amount of material to purge can be specified in the “Prime Tower Purge Volume” setting.
|
||||||
|
|
||||||
|
*First layer line width
|
||||||
|
A multiplier setting for the line width of the first layer of a print. Multiplying line width gives fewer lines but with greater width, which improves build plate adhesion.
|
||||||
|
|
||||||
|
*Pause standby and resume temperature
|
||||||
|
Turn off the nozzle when printing with extended pauses to prevent burned filament and nozzle clogging. At the end of a pause, the nozzle will reach printing temperature before resuming a print.
|
||||||
|
|
||||||
|
*Extruder per feature
|
||||||
|
Assign specific print features (walls, infill, skin, etc.) to a specific nozzle. A possible application of this would be to print an outer shell of an object with a fine nozzle at a greater level of detail while using a larger second nozzle to print infill faster.
|
||||||
|
|
||||||
|
*Dark theme
|
||||||
|
A dark theme for Cura. Select this theme to reduce eyestrain when working in dark environments. Activate it by selecting “Preferences > Themes > Dark".
|
||||||
|
|
||||||
|
*Top navigation bar redesign
|
||||||
|
The top bar user interface been improved so that “Prepare” and “Print” have moved from the right side of the interface to the left side.
|
||||||
|
|
||||||
|
*New keyboard shortcuts
|
||||||
|
Models can now be manipulated on the build plate using hotkeys Q, A, Z, W, and tab keys. Q selects “move”, A selects “scale”, Z selects “rotate”, and W selects “mirror”. Use the tab key to navigate between settings.
|
||||||
|
|
||||||
|
*Plugin browser
|
||||||
|
Easily download and install plugins using an integrated plugin browser. Go to “Extensions > Plugin Browser > Browse plugins” to select it.
|
||||||
|
|
||||||
|
*Import SolidWorks files as STL
|
||||||
|
A new plugin that enables SolidWorks compatible .SLDPRT files to be imported directly into Cura, where they are automatically converted to .STL format. This plugin can be found in the plugin browser.
|
||||||
|
|
||||||
|
*Zoom towards mouse cursor position
|
||||||
|
Cura preferences now include an option to zoom towards the cursor position on screen.
|
||||||
|
|
||||||
|
*Increased scroll speed in setting lists
|
||||||
|
The scroll speed in the setting lists is now three times faster than previous versions.
|
||||||
|
|
||||||
|
*Extra tooltips
|
||||||
|
Extra tooltips have been added to clarify the machine settings.
|
||||||
|
|
||||||
|
*Polish now supported
|
||||||
|
Polish language support added. This can be selected in the preferences menu.
|
||||||
|
|
||||||
|
*Chinese now supported
|
||||||
|
Chinese language support added. This can be selected in the preferences menu.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Cura project Mac extensions
|
||||||
|
- Crashes when adding printers
|
||||||
|
- Jerk fixes
|
||||||
|
- Z-hop over-extrusion
|
||||||
|
- Material diameter in machine settings
|
||||||
|
|
||||||
|
*3rd party printers
|
||||||
|
- Peopoly Moai
|
||||||
|
- DiscoEasy200
|
||||||
|
- Cartesio
|
||||||
|
- EasyArt Ares
|
||||||
|
- 3Dator
|
||||||
|
- Rigid3D
|
||||||
|
- Type A Series 1
|
||||||
|
- HelloBEEPrusa
|
||||||
|
|
||||||
|
[2.6.2]
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Fixed an issue with Cura crashing on older versions of MacOS.
|
||||||
|
|
||||||
|
[2.6.1]
|
||||||
|
*New profiles
|
||||||
|
The Polypropylene material is added and supported with the Ultimaker 3. Support for CPE+ and PC with 0.8mm nozzles is added as well.
|
||||||
|
|
||||||
|
[2.6.0]
|
||||||
|
*Cura versions
|
||||||
|
Cura 2.6 has local version folders, which means the new version won’t overwrite the existing configuration and profiles from older versions, but can create a new folder instead. You can now safely check out new beta versions and, if necessary, start up an older version without the danger of losing your profiles.
|
||||||
|
|
||||||
|
*Better support adhesion
|
||||||
|
We’ve added extra support settings to allow the creation of improved support profiles with better PVA/PLA adhesion. The Support Interface settings, such as speed and density, are now split up into Support Roof and Support Floor settings.
|
||||||
|
|
||||||
|
*Multi-extrusion support for custom FDM printers
|
||||||
|
Custom third-party printers and Ultimaker modifications now have multi-extrusion support. Thanks to Aldo Hoeben for this feature.
|
||||||
|
|
||||||
|
*Model auto-arrange
|
||||||
|
We’ve improved placing multiple models or multiplying the same ones, making it easier to arrange your build plate. If there’s not enough build plate space or the model is placed beyond the build plate, you can rectify this by selecting ‘Arrange all models’ in the context menu or by pressing Command+R (MacOS) or Ctrl+R (Windows and Linux). Cura 2.6 will then find a better solution for model positioning.
|
||||||
|
|
||||||
|
*Gradual infill
|
||||||
|
You can now find the Gradual Infill button in Recommended mode. This setting makes the infill concentrated near the top of the model – so that we can save time and material for the lower parts of the model. This functionality is especially useful when printing with flexible materials.
|
||||||
|
|
||||||
|
*Support meshes
|
||||||
|
It’s now possible to load an extra model that will be used as a support structure.
|
||||||
|
|
||||||
|
*Mold
|
||||||
|
This is a bit of an experimental improvement. Users can use it to print a mold from a 3D model, which can be cast afterwards with the material that you would like your model to have.
|
||||||
|
|
||||||
|
*Towers for tiny overhangs
|
||||||
|
We’ve added a new support option allowing users to achieve more reliable results by creating towers to support even the smallest overhangs.
|
||||||
|
|
||||||
|
*Cutting meshes
|
||||||
|
Easily transform any model into a dual-extrusion print by applying a pattern for the second extruder. All areas of the original model, which also fall inside the pattern model, will be printed by the extruder selected for the pattern.
|
||||||
|
|
||||||
|
*Extruder per model selection via the context menu or extruder buttons
|
||||||
|
You can now select the necessary extruder in the right-click menu or extruder buttons. This is a quicker and more user-friendly process. The material color for each extruder will also be represented in the extruder icons.
|
||||||
|
|
||||||
|
*Custom toggle
|
||||||
|
We have made the interface a little bit cleaner and more user-friendly for switching from Recommended to Custom mode.
|
||||||
|
|
||||||
|
*Plugin installer
|
||||||
|
It used to be fairly tricky to install new plugins. We have now added a button to select and install new plugins with ease – you will find it in Preferences.
|
||||||
|
|
||||||
|
*Project-based menu
|
||||||
|
It’s a lot simpler to save and open files, and Cura will know if it’s a project, model, or gcode.
|
||||||
|
|
||||||
|
*Theme picker
|
||||||
|
If you have a custom theme, you can now apply it more easily in the preferences screen.
|
||||||
|
|
||||||
|
*Time estimates per feature
|
||||||
|
You can hover over the print time estimate in the lower right corner to see how the printing time is divided over the printing features (walls, infill, etc.). Thanks to 14bitVoid for this feature.
|
||||||
|
|
||||||
|
*Invert the direction of camera zoom
|
||||||
|
We’ve added an option to invert mouse direction for a better user experience.
|
||||||
|
|
||||||
|
*Olsson block upgrade
|
||||||
|
Ultimaker 2 users can now specify if they have the Olsson block installed on their machine. Thanks to Aldo Hoeben for this feature.
|
||||||
|
|
||||||
|
*OctoPrint plugin
|
||||||
|
Cura 2.6 allows users to send prints to OctoPrint. Thanks to Aldo Hoeben for this feature.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Post Processing plugin
|
||||||
|
- Font rendering
|
||||||
|
- Progress bar
|
||||||
|
- Support Bottom Distance issues
|
||||||
|
|
||||||
|
*3rd party printers
|
||||||
|
- MAKEIT
|
||||||
|
- Alya
|
||||||
|
- Peopoly Moai
|
||||||
|
- Rigid3D Zero
|
||||||
|
- 3D maker
|
||||||
|
|
||||||
|
[2.5.0]
|
||||||
|
*Improved speed
|
||||||
|
We’ve made changing printers, profiles, materials, and print cores even faster. 3MF processing is also much faster now. Opening a 3MF file now takes one tenth of the time.
|
||||||
|
|
||||||
|
*Speedup engine – Multithreading
|
||||||
|
Cura can process multiple operations at the same time during slicing. Supported by Windows and Linux operating systems only.
|
||||||
|
|
||||||
|
*Preheat the build plate (with a connected printer)
|
||||||
|
Users can now set the Ultimaker 3 to preheat the build plate, which reduces the downtime, allowing to manually speed up the printing workflow.
|
||||||
|
|
||||||
|
*Better layout for 3D layer view options
|
||||||
|
An improved layer view has been implemented for computers that support OpenGL 4.1. For OpenGL 2.0 to 4.0, we will automatically switch to the old layer view.
|
||||||
|
|
||||||
|
*Disable automatic slicing
|
||||||
|
An option to disable auto-slicing has been added for the better user experience.
|
||||||
|
|
||||||
|
*Auto-scale off by default
|
||||||
|
This change speaks for itself.
|
||||||
|
|
||||||
|
*Print cost calculation
|
||||||
|
The latest version of Cura now contains code to help users calculate the cost of their prints. To do so, users need to enter a cost per spool and an amount of materials per spool. It is also possible to set the cost per material and gain better control of the expenses. Thanks to our community member Aldo Hoeben for adding this feature.
|
||||||
|
|
||||||
|
*G-code reader
|
||||||
|
The g-code reader has been reintroduced, which means users can load g-code from file and display it in layer view. Users can also print saved g-code files with Cura, share and re-use them, as well as preview the printed object via the g-code viewer. Thanks to AlephObjects for this feature.
|
||||||
|
|
||||||
|
*Discard or Keep Changes popup
|
||||||
|
We’ve changed the popup that appears when a user changes a printing profile after setting custom printing settings. It is now more informative and helpful.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
- Window overflow: On some configurations (OS and screen dependant), an overflow on the General (Preferences) panel and the credits list on the About window occurred. This is now fixed.
|
||||||
|
- “Center camera when the item is selected”: This is now set to ‘off’ by default.
|
||||||
|
- Removal of file extension: When users save a file or project (without changing the file type), no file extension is added to the name. It’s only when users change to another file type that the extension is added.
|
||||||
|
- Ultimaker 3 Extended connectivity. Selecting Ultimaker 3 Extended in Cura let you connect and print with Ultimaker 3, without any warning. This now has been fixed.
|
||||||
|
- Different Y / Z colors: Y and Z colors in the tool menu are now similar to the colors on the build plate.
|
||||||
|
- No collision areas: No collision areas used to be generated for some models when "keep models apart" was activated. This is now fixed.
|
||||||
|
- Perimeter gaps: Perimeter gaps are not filled often enough; we’ve now amended this.
|
||||||
|
- File location after restart: The old version of Cura didn’t remember the last opened file location after it’s been restarted. Now it has been fixed.
|
||||||
|
- Project name: The project name changes after the project is opened. This now has been fixed.
|
||||||
|
- Slicing when error value is given (print core 2): When a support is printed with the Extruder 2 (PVA), some support settings will trigger a slice when an error value is given. We’ve now sorted this out.
|
||||||
|
- Support Towers: Support Towers can now be disabled.
|
||||||
|
- Support bottoms: When putting one object on top of another with some space in between, and selecting support with support bottom interface, no support bottom is printed. This has now been resolved.
|
||||||
|
- Summary box size: We’ve enlarged the summary box when saving the project.
|
||||||
|
- Cubic subdivision infill: In the past, the cubic subdivision infill sometimes didn’t produce the infill (WIN) – this has now been addressed.
|
||||||
|
- Spiralize outer contour and fill small gaps: When combining Fill Gaps Between Walls with Spiralize Outer Contour, the model gets a massive infill.
|
||||||
|
- Experimental post-processing plugin: Since the TweakAtZ post-processing plugin is not officially supported, we added the ‘Experimental’ tag.
|
||||||
|
|
||||||
|
*3rd party printers (bug fixes)
|
||||||
|
- Folgertech printer definition has been added.
|
||||||
|
- Hello BEE Prusa printer definition has been added.
|
||||||
|
- Velleman Vertex K8400 printer definitions have been added for both single-extrusion and dual-extrusion versions.
|
||||||
|
- Material profiles for Cartesio printers have been updated.
|
||||||
|
|
||||||
|
[2.4.0]
|
||||||
|
*Project saving & opening
|
||||||
|
You can now save your build plate configuration - with all your active machine’s meshes and settings. When you reopen the project file, you’ll find that the build plate configuration and all settings will be exactly as you last left them when you saved the project.
|
||||||
|
|
||||||
|
*Setting search
|
||||||
|
You can now search the custom settings directly from the side panel, which means you can easily locate the setting you need to tweak. Thanks to community member Aldo Hoeben & LulzBot for this feature.
|
||||||
|
|
||||||
|
*Editing start g-code and end g-code
|
||||||
|
Aldo Hoeben also added this feature, enabling you to alter both start and end code g-code settings for single extrusion machines.
|
||||||
|
|
||||||
|
*Multiply object function
|
||||||
|
By right-clicking on an object, you can multiply it by a variable amount, rather than duplicating multiple times. Thanks again to Aldo Hoeben for this feature.
|
||||||
|
|
||||||
|
*Ultimaker 3 single extrusion prints
|
||||||
|
Dual extrusion printers now allow for single extrusion prints in a larger printable area.
|
||||||
|
|
||||||
|
*Streaming printer monitor view
|
||||||
|
Ultimaker 3’s camera views no longer only show snapshots. They now show a live stream.
|
||||||
|
|
||||||
|
*Explain why slicing is disabled
|
||||||
|
When slicing is blocked by settings with error values, a message now appears, clearly indicating which settings need to be changed.
|
||||||
|
|
||||||
|
*Ultimaker 3 print profiles
|
||||||
|
The initial and final printing temperatures reduce the amount of oozing during PLA-PLA, PLA-PVA and Nylon-PVA prints. This means printing a prime tower is now optional (except for CPE and ABS at the moment). The new Ultimaker 3 printing profiles ensure increased reliability and shorter print time.
|
||||||
|
|
||||||
|
*Initial Layer Printing Temperature
|
||||||
|
Initial and final printing temperature settings have been tuned for higher quality results. For all materials the initial print temperature is 5 degrees above the default value.
|
||||||
|
|
||||||
|
*Printing temperature of the materials
|
||||||
|
The printing temperature of the materials in the material profiles is now the same as the printing temperature for the Fine profile.
|
||||||
|
|
||||||
|
*Improved PLA-PVA layer adhesion
|
||||||
|
The PVA jerk and acceleration have been optimized to improve the layer adhesion between PVA and PLA.
|
||||||
|
|
||||||
|
*Default build plate adhesion type for Nylon
|
||||||
|
The default build plate adhesion type for Nylon prints has been changed from raft to brim.
|
||||||
|
|
||||||
|
*Support Interface Thickness
|
||||||
|
The Support Roof Thickness is now 0.8 mm and PVA support infill has been slightly decreased to lower the printing time.
|
||||||
|
|
||||||
|
*Ultimaker 2+ PC prints
|
||||||
|
In the polycarbonate profiles, the raft settings for the 0.25 mm and 0.4 mm nozzles are tweaked for less warping.
|
||||||
|
|
||||||
|
*Hollow prime tower
|
||||||
|
Print the prime tower hollow to minimize material use while maintaining stability. Wiping the oozed material on the prime tower is now done from the inside, which means the excess material is contained within the prime tower.
|
||||||
|
|
||||||
|
*Precooling and prewarming
|
||||||
|
Printing now starts at a lower temperature, before increasing swiftly to the normal printing temperature. Cooling also starts earlier than the last extrusion (with that print core). This minimizes the material’s heat absorption, which decreases the amount of degradation of the PVA material. This reduces the risk of clogging your nozzles.
|
||||||
|
|
||||||
|
*Remove Mesh Intersection
|
||||||
|
You are now able to turn off resolving of overlapping meshes. Models can now overlap, so you can perform build plate color mixing, by placing meshes over one another and lowering their flow.
|
||||||
|
|
||||||
|
*Alternate Mesh Removal
|
||||||
|
For areas where two models overlap, let each layer of the overlapping volume alternate (depending on which object the overlapping area of that layer belongs to). This improves the bonding between dual color models and allows for more controlled build plate color mixing.
|
||||||
|
|
||||||
|
*Hollow Object
|
||||||
|
Remove the infill from a mesh and treat internal cavities as overhangs, so as to create support in the model’s interior. This experimental setting greatly reduces the amount of material needed on the inside of the print.
|
||||||
|
|
||||||
|
*Fill Gaps Between Walls
|
||||||
|
Fill up small gaps between consecutive walls, making thin pieces in your model dense, rather than hollow. This feature makes the thin pieces stronger.
|
||||||
|
|
||||||
|
*Cubic subdivision infill
|
||||||
|
This experimental new infill pattern is similar to cubic infill, but generates bigger cubes farther inside the mesh. This greatly reduces print times and material use, while maintaining structural integrity. Thanks to community members Martin Boerwinckle and Nicholas Seward for this feature.
|
||||||
|
|
||||||
|
*Concentric 3D infill
|
||||||
|
This new infill pattern is similar to concentric infill, but touches the shell every X layers, creating better support for the top layers.
|
||||||
|
|
||||||
|
* Printing Temperature Initial Layer
|
||||||
|
Nozzle temperature to be used during the first layer.
|
||||||
|
|
||||||
|
*Build Plate Temperature Initial Layer
|
||||||
|
Bed temperature to be used during the first layer.
|
||||||
|
|
||||||
|
*Initial Fan Speed
|
||||||
|
Fan speed to be used during the first layer.
|
||||||
|
|
||||||
|
*Retract at Layer Change
|
||||||
|
Retract each time the printer progresses to the next layer.
|
||||||
|
|
||||||
|
*Outer Wall Wipe Distance
|
||||||
|
Wipe the nozzle after printing the outer wall.
|
||||||
|
|
||||||
|
*Set X-Y coordinate of z-seam
|
||||||
|
Select where to place the Z seam.
|
||||||
|
|
||||||
|
*Start Layers with the Same Part
|
||||||
|
Start each layer with the part closest to a given location.
|
||||||
|
|
||||||
|
*Turn off nozzle after last use
|
||||||
|
Turn off the nozzle after its last use, while other nozzles are still in use.
|
||||||
|
|
||||||
|
*Option for no build plate adhesion
|
||||||
|
Select not to print any build plate adhesion helper parts.
|
||||||
|
|
||||||
|
*Anti-overhang and support meshes
|
||||||
|
Use a mesh to specify a volume within which to classify nothing as overhang for support or specify a volume within which to print support.
|
||||||
|
|
||||||
|
*Delta printer support
|
||||||
|
This release adds support for printers with elliptic buildplates. This feature has not been extensively tested so please let us know if it works or get involved in improving it.
|
||||||
|
|
||||||
|
*AppImage for Linux
|
||||||
|
The Linux distribution is now in AppImage format, which makes Cura easier to install.
|
||||||
|
|
||||||
|
*bugfixes
|
||||||
|
The user is now notified when a new version of Cura is available.
|
||||||
|
When searching in the setting visibility preferences, the category for each setting is always displayed.
|
||||||
|
3MF files are now saved and loaded correctly.
|
||||||
|
Dragging a profile onto Cura now loads it automatically.
|
||||||
|
You can now view which print cores and materials are currently in your Ultimaker 3, via the machine manager.
|
||||||
|
You can now add the heated bed upgrade etc. from the machine manager.
|
||||||
|
Print core and material is now arranged under extruder tabs.
|
||||||
|
Cura now remembers all printers and profiles when you open just after closing it.
|
||||||
|
You can now duplicate the standard profiles.
|
||||||
|
Layer view now doesn’t use as much RAM.
|
||||||
|
It’s now quicker to change the value of the Support Enable setting.
|
||||||
|
Changing a setting updates all dependent settings more quickly.
|
||||||
|
Having errors in your setting values now always blocks slicing.
|
||||||
|
Selecting a model with any active tool no longer causes a reslice.
|
||||||
|
The prime poop now introduces a separate area where you cannot print.
|
||||||
|
Support Extruder setting is now near the support settings.
|
||||||
|
Build Plate Adhesion Extruder setting is now near the build plate adhesion settings.
|
||||||
|
Z hop settings have been moved to the Travel category.
|
||||||
|
Inactive nozzle wiping on the prime tower is re-enabled.
|
||||||
|
There are no more unnecessary retractions in support.
|
||||||
|
Each layer now has less extruder switches than the machine has extruders.
|
||||||
|
Concentric infill doesn’t generate the first infill perimeter next to the walls.
|
||||||
|
Extruder priming now always happens on the first layer.
|
||||||
|
Raising the build plate of the Ultimaker 2 now has the proper speed again.
|
||||||
|
Changing material while the Ultimaker 2 is paused works again.
|
||||||
|
|
||||||
[2.3.1]
|
[2.3.1]
|
||||||
*Layer Height in Profile Selection
|
*Layer Height in Profile Selection
|
||||||
Added the layer height to the profile selection menu.
|
Added the layer height to the profile selection menu.
|
||||||
|
|
@ -212,3 +696,165 @@ Prints the outer walls with a jittering motion to give your object a diffuse fin
|
||||||
|
|
||||||
*Wire Printing
|
*Wire Printing
|
||||||
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
|
The object is printed with a mid-air / net-like structure, following the mesh surface. The build plate will move up and down during diagonal segments. Though not visible in layer view, you can view the result in other software, such as Repetier Host or http://chilipeppr.com/tinyg.
|
||||||
|
|
||||||
|
|
||||||
|
[15.06 Beta]
|
||||||
|
|
||||||
|
Cura 15.06 is a new release built from the ground up on a completely new
|
||||||
|
framework called Uranium. This framework has been designed to make it easier to
|
||||||
|
extend Cura with additional functionality as well as provide a cleaner UI.
|
||||||
|
|
||||||
|
[15.05.95]
|
||||||
|
|
||||||
|
* Fixed: Selection ghost remains visible after deleting an object
|
||||||
|
* Fixed: Window does not show up immediately after starting application on OSX
|
||||||
|
* Fixed: Added display of rotation angle during rotation
|
||||||
|
* Fixed: Object changes position while rotating/scaling
|
||||||
|
* Fixed: Loading improvements in the layer view
|
||||||
|
* Fixed: Added application icons
|
||||||
|
* Fixed: Improved feedback when loading models
|
||||||
|
* Fixed: Eject device on MacOSX now provides proper feedback
|
||||||
|
* Fixed: Make it possible to show retraction settings for UM2
|
||||||
|
* Fixed: Opening the machine preferences page will switch to the first available machine
|
||||||
|
* Fixed: Improved tool handle hit area size
|
||||||
|
* Fixed: Render lines with a thickness based on screen DPI
|
||||||
|
|
||||||
|
[15.05.94]
|
||||||
|
|
||||||
|
* Added Russian translations
|
||||||
|
* Fixed: Infill not displayed in layer view
|
||||||
|
* Fixed: Cannot select/scale/rotate when first activating the tool and then trying to select a model.
|
||||||
|
* Fixed: Improved font rendering on Windows
|
||||||
|
* Fixed: Help > Show Documentation crashes Cura on Windows
|
||||||
|
* Fixed: "There is no disk in the drive" repeating messages on Windows
|
||||||
|
* Fixed: Retraction settings not visible for Ultimaker2
|
||||||
|
* Fixed: Display rotation angle when rotating an object
|
||||||
|
* Fixed: Time/Quality slider values are properly rounded
|
||||||
|
* Fixed: Improved clarity of buttons and text
|
||||||
|
* Fixed: No indication that anything is happening when loading a model
|
||||||
|
* Fixed: Eject device now works on Windows
|
||||||
|
|
||||||
|
[15.05.93]
|
||||||
|
|
||||||
|
* Fixed: No shortcuts for moving up/down layers in layer view.
|
||||||
|
* Fixed: Last view layers could not be scrolled through in layer view.
|
||||||
|
* Fixed: Files provided on command line would not actually show up on the build
|
||||||
|
platform.
|
||||||
|
* Fixed: Render a ghost of the selection in Layer view to make the actual object
|
||||||
|
position clear.
|
||||||
|
* Fixed: Showing a menu would clear the selection.
|
||||||
|
* Fixed: Size and scaling factor display for scale tool.
|
||||||
|
* Fixed: Missing background for additional tool controls.
|
||||||
|
* Fixed: Loading message times out when loading large files.
|
||||||
|
* Fixed: Show recent files in the file menu.
|
||||||
|
* Fixed: Windows installer will now install MSVC 2010 redistributable, to
|
||||||
|
prevent issues with missing DLL's.
|
||||||
|
* Fixed: Collapsed/expanded state of setting categories not stored.
|
||||||
|
|
||||||
|
[15.05.91]
|
||||||
|
|
||||||
|
* There is now a working MacOSX version. Currently it supports OSX 10.7 and
|
||||||
|
higher.
|
||||||
|
* Fixed: Need to deselect before selecting a different object.
|
||||||
|
* Fixed: Object can be moved on Z axis.
|
||||||
|
* Fixed: Error values should be considered invalid values and will not trigger a
|
||||||
|
slice.
|
||||||
|
* Fixed: Text fields used a locale-aware validator while the underlying code did
|
||||||
|
not.
|
||||||
|
* Fixed: Text fields will trigger a slice on text change, not only after focus
|
||||||
|
change/enter press.
|
||||||
|
* Fixed: Rotate Tool snaps to incorrect value.
|
||||||
|
* Fixed: Object Collision would only moved objects to the right.
|
||||||
|
* Fixed: Object Collision would move the selected object when it should not.
|
||||||
|
* Fixed: Camera panning now works correctly instead of doing nothing.
|
||||||
|
* Fixed: Camera would flip around center point at maximum rotation.
|
||||||
|
* Fixed: Build platform grid blocked view from below objects.
|
||||||
|
* Fixed: Viewport on MacOSX with high-DPI screens was only taking 1/4th of the
|
||||||
|
window
|
||||||
|
|
||||||
|
[15.05.90]
|
||||||
|
|
||||||
|
* Fixed: Additional UI elements for tools and views not loading.
|
||||||
|
* Fixed: Double click needed to change setting dialog page.
|
||||||
|
* Fixed: Context menu entries (reload, center object, etc.) not working.
|
||||||
|
* Fixed: "Open With" or passing files from command line not working.
|
||||||
|
* Fixed: "Reload All" would not reload files.
|
||||||
|
|
||||||
|
In addition, a lot of work has gone into getting a usable Mac OSX version.
|
||||||
|
|
||||||
|
New Features
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Plugin based system
|
||||||
|
The Uranium framework provides us with a plugin-based system
|
||||||
|
that provides additional flexibility when extending Cura. Think
|
||||||
|
of new views, tools, file formats, etc. This is probably the
|
||||||
|
biggest new feature.
|
||||||
|
* Improved UI
|
||||||
|
The UI has received a complete overhaul.
|
||||||
|
* Time-Quality Slider
|
||||||
|
The 4 static quick print profiles have been replaced with
|
||||||
|
a slider that should make it easier to find the right spot
|
||||||
|
between print time and print quality.
|
||||||
|
* More Settings
|
||||||
|
The Advanced mode is now configurable and can show many
|
||||||
|
additional settings that were previously not available, while at
|
||||||
|
the same time not overwhelming new users with too many settings.
|
||||||
|
Custom set of visible settings can be created by the user.
|
||||||
|
* Support for high-DPI screens
|
||||||
|
The refreshed UI has been designed with high-DPI screens in
|
||||||
|
mind which should improve the experience of Cura on such
|
||||||
|
devices.
|
||||||
|
* Improved language support
|
||||||
|
(Not yet available for the Beta release.)
|
||||||
|
* Improved support structure generation
|
||||||
|
The new version of the CuraEngine now features improved
|
||||||
|
support generation algorithms and additional options for support
|
||||||
|
structure generation.
|
||||||
|
* Experimental Feature: Wire Printing
|
||||||
|
Wire Printing has been added as an experimental new feature. It
|
||||||
|
will print objects as a structure of lines. It can be enabled by
|
||||||
|
from Advanced Mode -> Fixes -> Wire Printing.
|
||||||
|
* Undo/Redo
|
||||||
|
It is now possible to undo and redo most scene operations, like
|
||||||
|
moving or rotating objects.
|
||||||
|
|
||||||
|
Features from earlier versions not (yet) in this release
|
||||||
|
--------------------------------------------------------
|
||||||
|
|
||||||
|
* The All-at-once/One-at-a-time toggle is not available.
|
||||||
|
We are working on an improved implementation of this mechanism
|
||||||
|
but it will not be available for this release.
|
||||||
|
* No dual extrusion features are available yet.
|
||||||
|
We are working on a completely new workflow for this but this
|
||||||
|
needs additional time.
|
||||||
|
* “Lay Flat” has been removed.
|
||||||
|
The existing implementation was unfortunately not salvageable.
|
||||||
|
We will be looking into an improved implementation for this
|
||||||
|
feature.
|
||||||
|
* "Split Object Into Parts" has been removed.
|
||||||
|
Due to the same reason as Lay Flat.
|
||||||
|
* Support for AMF and DAE file formats has been removed.
|
||||||
|
Both of these will be implemented as plugins in the future.
|
||||||
|
* Support for directly loading a GCode file is not yet available.
|
||||||
|
This will be implemented as a plugin in the future.
|
||||||
|
* Support for PNG, JPG and other image formats has been removed.
|
||||||
|
These can be supported by a plugin with an improved UI.
|
||||||
|
* Support for loading Minecraft levels has been removed.
|
||||||
|
This can be implemented as a plugin.
|
||||||
|
* Windows XP support has been dropped.
|
||||||
|
Microsoft is no longer supporting xp, so they no longer back
|
||||||
|
port certain features that we require.
|
||||||
|
* X-Ray view is missing.
|
||||||
|
Will be implemented as a (you might have guessed it) plugin.
|
||||||
|
* Fixes: Follow Mesh Surface
|
||||||
|
Has been removed from the engine, the same result can be
|
||||||
|
achieved using no infill or top/bottom layers.
|
||||||
|
|
||||||
|
Known Issues
|
||||||
|
------------
|
||||||
|
* Some OBJ files are rendered as black objects due to missing
|
||||||
|
normals.
|
||||||
|
* Disabling plugins does not work correctly yet.
|
||||||
|
* Unicorn occasionally still requires feeding. Do not feed it
|
||||||
|
after midnight.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from . import ChangeLog
|
from . import ChangeLog
|
||||||
|
|
||||||
|
|
@ -7,15 +7,7 @@ from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
return {
|
return {}
|
||||||
"plugin": {
|
|
||||||
"name": catalog.i18nc("@label", "Changelog"),
|
|
||||||
"author": "Ultimaker",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Shows changes since latest checked version."),
|
|
||||||
"api": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return {"extension": ChangeLog.ChangeLog()}
|
return {"extension": ChangeLog.ChangeLog()}
|
||||||
|
|
|
||||||
8
plugins/ChangeLogPlugin/plugin.json
Normal file
8
plugins/ChangeLogPlugin/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Changelog",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Shows changes since latest checked version.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,8 @@ message Polygon {
|
||||||
Type type = 1; // Type of move
|
Type type = 1; // Type of move
|
||||||
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
|
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
|
||||||
float line_width = 3; // The width of the line being laid down
|
float line_width = 3; // The width of the line being laid down
|
||||||
|
float line_thickness = 4; // The thickness of the line being laid down
|
||||||
|
float line_feedrate = 5; // The feedrate of the line being laid down
|
||||||
}
|
}
|
||||||
|
|
||||||
message LayerOptimized {
|
message LayerOptimized {
|
||||||
|
|
@ -82,6 +84,8 @@ message PathSegment {
|
||||||
bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1
|
bytes points = 3; // The points defining the line segments, bytes of float[2/3] array of length N+1
|
||||||
bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path
|
bytes line_type = 4; // Type of line segment as an unsigned char array of length 1 or N, where N is the number of line segments in this path
|
||||||
bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
|
bytes line_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
|
||||||
|
bytes line_thickness = 6; // The thickness of the line segments as bytes of a float array of length 1 or N
|
||||||
|
bytes line_feedrate = 7; // The feedrate of the line segments as bytes of a float array of length 1 or N
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -90,9 +94,21 @@ message GCodeLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
message PrintTimeMaterialEstimates { // The print time for the whole print and material estimates for the extruder
|
message PrintTimeMaterialEstimates { // The print time for each feature and material estimates for the extruder
|
||||||
float time = 1; // Total time estimate
|
// Time estimate in each feature
|
||||||
repeated MaterialEstimates materialEstimates = 2; // materialEstimates data
|
float time_none = 1;
|
||||||
|
float time_inset_0 = 2;
|
||||||
|
float time_inset_x = 3;
|
||||||
|
float time_skin = 4;
|
||||||
|
float time_support = 5;
|
||||||
|
float time_skirt = 6;
|
||||||
|
float time_infill = 7;
|
||||||
|
float time_support_infill = 8;
|
||||||
|
float time_travel = 9;
|
||||||
|
float time_retract = 10;
|
||||||
|
float time_support_interface = 11;
|
||||||
|
|
||||||
|
repeated MaterialEstimates materialEstimates = 12; // materialEstimates data
|
||||||
}
|
}
|
||||||
|
|
||||||
message MaterialEstimates {
|
message MaterialEstimates {
|
||||||
|
|
|
||||||
376
plugins/CuraEngineBackend/CuraEngineBackend.py
Normal file → Executable file
376
plugins/CuraEngineBackend/CuraEngineBackend.py
Normal file → Executable file
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Backend.Backend import Backend, BackendState
|
from UM.Backend.Backend import Backend, BackendState
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
@ -12,13 +12,12 @@ from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
|
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Qt.Duration import DurationFormat
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
import cura.Settings
|
|
||||||
|
|
||||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from . import ProcessSlicedLayersJob
|
from . import ProcessSlicedLayersJob
|
||||||
from . import ProcessGCodeJob
|
|
||||||
from . import StartSliceJob
|
from . import StartSliceJob
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -32,13 +31,14 @@ import Arcus
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
class CuraEngineBackend(Backend):
|
class CuraEngineBackend(QObject, Backend):
|
||||||
## Starts the back-end plug-in.
|
## Starts the back-end plug-in.
|
||||||
#
|
#
|
||||||
# This registers all the signal listeners and prepares for communication
|
# This registers all the signal listeners and prepares for communication
|
||||||
# with the back-end in general.
|
# with the back-end in general.
|
||||||
def __init__(self):
|
# CuraEngineBackend is exposed to qml as well.
|
||||||
super().__init__()
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent = parent)
|
||||||
# Find out where the engine is located, and how it is called.
|
# Find out where the engine is located, and how it is called.
|
||||||
# This depends on how Cura is packaged and which OS we are running on.
|
# This depends on how Cura is packaged and which OS we are running on.
|
||||||
executable_name = "CuraEngine"
|
executable_name = "CuraEngine"
|
||||||
|
|
@ -66,9 +66,6 @@ class CuraEngineBackend(Backend):
|
||||||
default_engine_location = os.path.abspath(default_engine_location)
|
default_engine_location = os.path.abspath(default_engine_location)
|
||||||
Preferences.getInstance().addPreference("backend/location", default_engine_location)
|
Preferences.getInstance().addPreference("backend/location", default_engine_location)
|
||||||
|
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
|
||||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
|
||||||
|
|
||||||
# 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
|
self._layer_view_active = False
|
||||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
@ -76,22 +73,26 @@ class CuraEngineBackend(Backend):
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = []
|
||||||
self._stored_optimized_layer_data = []
|
self._stored_optimized_layer_data = []
|
||||||
|
|
||||||
# Triggers for when to (re)start slicing:
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||||
|
|
||||||
|
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
|
||||||
|
# - auto-slicing is started with a timer
|
||||||
|
# - whenever there is a value change, we start the timer
|
||||||
|
# - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
|
||||||
|
# auto-slicing timer when that error check is finished
|
||||||
|
# If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
|
||||||
|
# and only wait for the error check to be finished to start the auto-slicing timer again.
|
||||||
|
#
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||||
self._onGlobalStackChanged()
|
self._onGlobalStackChanged()
|
||||||
|
|
||||||
self._active_extruder_stack = None
|
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
|
||||||
cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
|
||||||
self._onActiveExtruderChanged()
|
|
||||||
|
|
||||||
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
# A flag indicating if an error check was scheduled
|
||||||
# This timer will group them up, and only slice for the last setting changed signal.
|
# If so, we will stop the auto-slice timer and start upon the error check
|
||||||
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
|
self._is_error_check_scheduled = False
|
||||||
self._change_timer = QTimer()
|
|
||||||
self._change_timer.setInterval(500)
|
|
||||||
self._change_timer.setSingleShot(True)
|
|
||||||
self._change_timer.timeout.connect(self.slice)
|
|
||||||
|
|
||||||
# Listeners for receiving messages from the back-end.
|
# Listeners for receiving messages from the back-end.
|
||||||
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
||||||
|
|
@ -105,12 +106,16 @@ class CuraEngineBackend(Backend):
|
||||||
self._start_slice_job = None
|
self._start_slice_job = None
|
||||||
self._slicing = False # Are we currently slicing?
|
self._slicing = False # Are we currently slicing?
|
||||||
self._restart = False # Back-end is currently restarting?
|
self._restart = False # Back-end is currently restarting?
|
||||||
self._enabled = True # Should we be slicing? Slicing might be paused when, for instance, the user is dragging the mesh around.
|
self._tool_active = False # If a tool is active, some tasks do not have to do anything
|
||||||
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
|
||||||
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
|
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
|
||||||
|
self._need_slicing = False
|
||||||
|
self._engine_is_fresh = True # Is the newly started engine used before or not?
|
||||||
|
|
||||||
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
|
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
|
||||||
self._error_message = None # Pop-up message that shows errors.
|
self._error_message = None # Pop-up message that shows errors.
|
||||||
|
self._last_num_objects = 0 # Count number of objects to see if there is something changed
|
||||||
|
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
|
||||||
|
|
||||||
self.backendQuit.connect(self._onBackendQuit)
|
self.backendQuit.connect(self._onBackendQuit)
|
||||||
self.backendConnected.connect(self._onBackendConnected)
|
self.backendConnected.connect(self._onBackendConnected)
|
||||||
|
|
@ -121,9 +126,22 @@ class CuraEngineBackend(Backend):
|
||||||
|
|
||||||
self._slice_start_time = None
|
self._slice_start_time = None
|
||||||
|
|
||||||
## Called when closing the application.
|
Preferences.getInstance().addPreference("general/auto_slice", True)
|
||||||
|
|
||||||
|
self._use_timer = False
|
||||||
|
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
|
||||||
|
# This timer will group them up, and only slice for the last setting changed signal.
|
||||||
|
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
|
||||||
|
self._change_timer = QTimer()
|
||||||
|
self._change_timer.setSingleShot(True)
|
||||||
|
self._change_timer.setInterval(500)
|
||||||
|
self.determineAutoSlicing()
|
||||||
|
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
||||||
|
|
||||||
|
## Terminate the engine process.
|
||||||
#
|
#
|
||||||
# This function should terminate the engine process.
|
# This function should terminate the engine process.
|
||||||
|
# Called when closing the application.
|
||||||
def close(self):
|
def close(self):
|
||||||
# Terminate CuraEngine if it is still running at this point
|
# Terminate CuraEngine if it is still running at this point
|
||||||
self._terminate()
|
self._terminate()
|
||||||
|
|
@ -147,21 +165,12 @@ class CuraEngineBackend(Backend):
|
||||||
## Emitted when the slicing process is aborted forcefully.
|
## Emitted when the slicing process is aborted forcefully.
|
||||||
slicingCancelled = Signal()
|
slicingCancelled = Signal()
|
||||||
|
|
||||||
## Perform a slice of the scene.
|
@pyqtSlot()
|
||||||
def slice(self):
|
def stopSlicing(self):
|
||||||
self._slice_start_time = time()
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
if not self._enabled or not self._global_container_stack: # We shouldn't be slicing.
|
|
||||||
# try again in a short time
|
|
||||||
self._change_timer.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.printDurationMessage.emit(0, [0])
|
|
||||||
|
|
||||||
self._stored_layer_data = []
|
|
||||||
self._stored_optimized_layer_data = []
|
|
||||||
|
|
||||||
if self._slicing: # We were already slicing. Stop the old job.
|
if self._slicing: # We were already slicing. Stop the old job.
|
||||||
self._terminate()
|
self._terminate()
|
||||||
|
self._createSocket()
|
||||||
|
|
||||||
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
|
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
|
||||||
self._process_layers_job.abort()
|
self._process_layers_job.abort()
|
||||||
|
|
@ -170,6 +179,33 @@ class CuraEngineBackend(Backend):
|
||||||
if self._error_message:
|
if self._error_message:
|
||||||
self._error_message.hide()
|
self._error_message.hide()
|
||||||
|
|
||||||
|
## Manually triggers a reslice
|
||||||
|
@pyqtSlot()
|
||||||
|
def forceSlice(self):
|
||||||
|
if self._use_timer:
|
||||||
|
self._change_timer.start()
|
||||||
|
else:
|
||||||
|
self.slice()
|
||||||
|
|
||||||
|
## Perform a slice of the scene.
|
||||||
|
def slice(self):
|
||||||
|
self._slice_start_time = time()
|
||||||
|
if not self._need_slicing:
|
||||||
|
self.processingProgress.emit(1.0)
|
||||||
|
self.backendStateChange.emit(BackendState.Done)
|
||||||
|
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||||
|
return
|
||||||
|
if Application.getInstance().getPrintInformation():
|
||||||
|
Application.getInstance().getPrintInformation().setToZeroPrintInformation()
|
||||||
|
|
||||||
|
self._stored_layer_data = []
|
||||||
|
self._stored_optimized_layer_data = []
|
||||||
|
|
||||||
|
if self._process is None:
|
||||||
|
self._createSocket()
|
||||||
|
self.stopSlicing()
|
||||||
|
self._engine_is_fresh = False # Yes we're going to use the engine
|
||||||
|
|
||||||
self.processingProgress.emit(0.0)
|
self.processingProgress.emit(0.0)
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
|
||||||
|
|
@ -183,9 +219,9 @@ class CuraEngineBackend(Backend):
|
||||||
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
||||||
|
|
||||||
## Terminate the engine process.
|
## Terminate the engine process.
|
||||||
|
# Start the engine process by calling _createSocket()
|
||||||
def _terminate(self):
|
def _terminate(self):
|
||||||
self._slicing = False
|
self._slicing = False
|
||||||
self._restart = True
|
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = []
|
||||||
self._stored_optimized_layer_data = []
|
self._stored_optimized_layer_data = []
|
||||||
if self._start_slice_job is not None:
|
if self._start_slice_job is not None:
|
||||||
|
|
@ -207,9 +243,6 @@ class CuraEngineBackend(Backend):
|
||||||
|
|
||||||
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
|
except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this.
|
||||||
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
|
Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e))
|
||||||
else:
|
|
||||||
# Process is none, but something did went wrong here. Try and re-create the socket
|
|
||||||
self._createSocket()
|
|
||||||
|
|
||||||
## Event handler to call when the job to initiate the slicing process is
|
## Event handler to call when the job to initiate the slicing process is
|
||||||
# completed.
|
# completed.
|
||||||
|
|
@ -231,9 +264,9 @@ class CuraEngineBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
||||||
if Application.getInstance().getPlatformActivity:
|
if Application.getInstance().platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status",
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
"The selected material is incompatible with the selected machine or configuration."))
|
"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()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
else:
|
else:
|
||||||
|
|
@ -241,7 +274,7 @@ class CuraEngineBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
||||||
if Application.getInstance().getPlatformActivity:
|
if Application.getInstance().platformActivity:
|
||||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||||
error_keys = []
|
error_keys = []
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
|
|
@ -249,29 +282,58 @@ class CuraEngineBackend(Backend):
|
||||||
if not extruders:
|
if not extruders:
|
||||||
error_keys = self._global_container_stack.getErrorKeys()
|
error_keys = self._global_container_stack.getErrorKeys()
|
||||||
error_labels = set()
|
error_labels = set()
|
||||||
definition_container = self._global_container_stack.getBottom()
|
|
||||||
for key in error_keys:
|
for key in error_keys:
|
||||||
error_labels.add(definition_container.findDefinitions(key = key)[0].label)
|
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
||||||
|
definitions = stack.getBottom().findDefinitions(key = key)
|
||||||
|
if definitions:
|
||||||
|
break #Found it! No need to continue search.
|
||||||
|
else: #No stack has a definition for this setting.
|
||||||
|
Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key))
|
||||||
|
continue
|
||||||
|
error_labels.add(definitions[0].label)
|
||||||
|
|
||||||
error_labels = ", ".join(error_labels)
|
error_labels = ", ".join(error_labels)
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}".format(error_labels)))
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels),
|
||||||
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
|
||||||
|
errors = {}
|
||||||
|
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||||
|
stack = node.callDecoration("getStack")
|
||||||
|
if not stack:
|
||||||
|
continue
|
||||||
|
for key in stack.getErrorKeys():
|
||||||
|
definition = self._global_container_stack.getBottom().findDefinitions(key = key)
|
||||||
|
if not definition:
|
||||||
|
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
|
||||||
|
continue
|
||||||
|
definition = definition[0]
|
||||||
|
errors[key] = definition.label
|
||||||
|
error_labels = ", ".join(errors.values())
|
||||||
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
|
||||||
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
|
self._error_message.show()
|
||||||
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
||||||
if Application.getInstance().getPlatformActivity:
|
if Application.getInstance().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"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
|
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
|
||||||
if Application.getInstance().getPlatformActivity:
|
if Application.getInstance().platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."))
|
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
||||||
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.backendStateChange.emit(BackendState.Error)
|
||||||
else:
|
else:
|
||||||
|
|
@ -285,6 +347,33 @@ class CuraEngineBackend(Backend):
|
||||||
|
|
||||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||||
|
|
||||||
|
## Determine enable or disable auto slicing. Return True for enable timer and False otherwise.
|
||||||
|
# It disables when
|
||||||
|
# - preference auto slice is off
|
||||||
|
# - decorator isBlockSlicing is found (used in g-code reader)
|
||||||
|
def determineAutoSlicing(self):
|
||||||
|
enable_timer = True
|
||||||
|
|
||||||
|
if not Preferences.getInstance().getValue("general/auto_slice"):
|
||||||
|
enable_timer = False
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
if node.callDecoration("isBlockSlicing"):
|
||||||
|
enable_timer = False
|
||||||
|
self.backendStateChange.emit(BackendState.Disabled)
|
||||||
|
gcode_list = node.callDecoration("getGCodeList")
|
||||||
|
if gcode_list is not None:
|
||||||
|
self._scene.gcode_list = gcode_list
|
||||||
|
|
||||||
|
if self._use_timer == enable_timer:
|
||||||
|
return self._use_timer
|
||||||
|
if enable_timer:
|
||||||
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
self.enableTimer()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.disableTimer()
|
||||||
|
return False
|
||||||
|
|
||||||
## Listener for when the scene has changed.
|
## Listener for when the scene has changed.
|
||||||
#
|
#
|
||||||
# This should start a slice if the scene is now ready to slice.
|
# This should start a slice if the scene is now ready to slice.
|
||||||
|
|
@ -294,15 +383,33 @@ class CuraEngineBackend(Backend):
|
||||||
if type(source) is not SceneNode:
|
if type(source) is not SceneNode:
|
||||||
return
|
return
|
||||||
|
|
||||||
if source is self._scene.getRoot():
|
root_scene_nodes_changed = False
|
||||||
|
if source == self._scene.getRoot():
|
||||||
|
num_objects = 0
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
# Only count sliceable objects
|
||||||
|
if node.callDecoration("isSliceable"):
|
||||||
|
num_objects += 1
|
||||||
|
if num_objects != self._last_num_objects:
|
||||||
|
self._last_num_objects = num_objects
|
||||||
|
root_scene_nodes_changed = True
|
||||||
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
|
||||||
if source.getMeshData() is None:
|
if source.getMeshData() is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if source.getMeshData().getVertices() is None:
|
if source.getMeshData().getVertices() is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._tool_active:
|
||||||
|
# do it later, each source only has to be done once
|
||||||
|
if source not in self._postponed_scene_change_sources:
|
||||||
|
self._postponed_scene_change_sources.append(source)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.needsSlicing()
|
||||||
|
self.stopSlicing()
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
## Called when an error occurs in the socket connection towards the engine.
|
## Called when an error occurs in the socket connection towards the engine.
|
||||||
|
|
@ -317,16 +424,45 @@ class CuraEngineBackend(Backend):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._terminate()
|
self._terminate()
|
||||||
|
self._createSocket()
|
||||||
|
|
||||||
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
|
if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]:
|
||||||
Logger.log("w", "A socket error caused the connection to be reset")
|
Logger.log("w", "A socket error caused the connection to be reset")
|
||||||
|
|
||||||
|
## Remove old layer data (if any)
|
||||||
|
def _clearLayerData(self):
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
if node.callDecoration("getLayerData"):
|
||||||
|
node.getParent().removeChild(node)
|
||||||
|
break
|
||||||
|
|
||||||
|
## Convenient function: set need_slicing, emit state and clear layer data
|
||||||
|
def needsSlicing(self):
|
||||||
|
self.stopSlicing()
|
||||||
|
self._need_slicing = True
|
||||||
|
self.processingProgress.emit(0.0)
|
||||||
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
if not self._use_timer:
|
||||||
|
# With manually having to slice, we want to clear the old invalid layer data.
|
||||||
|
self._clearLayerData()
|
||||||
|
|
||||||
## A setting has changed, so check if we must reslice.
|
## A setting has changed, so check if we must reslice.
|
||||||
#
|
|
||||||
# \param instance The setting instance that has changed.
|
# \param instance The setting instance that has changed.
|
||||||
# \param property The property of the setting instance that has changed.
|
# \param property The property of the setting instance that has changed.
|
||||||
def _onSettingChanged(self, instance, property):
|
def _onSettingChanged(self, instance, property):
|
||||||
if property == "value": # Only reslice if the value has changed.
|
if property == "value": # Only reslice if the value has changed.
|
||||||
|
self.needsSlicing()
|
||||||
|
self._onChanged()
|
||||||
|
|
||||||
|
elif property == "validationState":
|
||||||
|
if self._use_timer:
|
||||||
|
self._is_error_check_scheduled = True
|
||||||
|
self._change_timer.stop()
|
||||||
|
|
||||||
|
def _onStackErrorCheckFinished(self):
|
||||||
|
self._is_error_check_scheduled = False
|
||||||
|
if not self._slicing and self._need_slicing:
|
||||||
|
self.needsSlicing()
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
## Called when a sliced layer data message is received from the engine.
|
## Called when a sliced layer data message is received from the engine.
|
||||||
|
|
@ -355,7 +491,17 @@ class CuraEngineBackend(Backend):
|
||||||
self.backendStateChange.emit(BackendState.Done)
|
self.backendStateChange.emit(BackendState.Done)
|
||||||
self.processingProgress.emit(1.0)
|
self.processingProgress.emit(1.0)
|
||||||
|
|
||||||
|
for line in self._scene.gcode_list:
|
||||||
|
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
|
||||||
|
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
|
||||||
|
replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
|
||||||
|
replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
|
||||||
|
replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
|
||||||
|
|
||||||
|
self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced
|
||||||
|
|
||||||
self._slicing = False
|
self._slicing = False
|
||||||
|
self._need_slicing = False
|
||||||
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
|
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
|
||||||
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
|
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
|
||||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||||
|
|
@ -376,35 +522,60 @@ class CuraEngineBackend(Backend):
|
||||||
def _onGCodePrefixMessage(self, message):
|
def _onGCodePrefixMessage(self, message):
|
||||||
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
|
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
|
||||||
|
|
||||||
## Called when a print time message is received from the engine.
|
|
||||||
#
|
|
||||||
# \param message The protobuff message containing the print time and
|
|
||||||
# material amount per extruder
|
|
||||||
def _onPrintTimeMaterialEstimates(self, message):
|
|
||||||
material_amounts = []
|
|
||||||
for index in range(message.repeatedMessageCount("materialEstimates")):
|
|
||||||
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
|
||||||
self.printDurationMessage.emit(message.time, material_amounts)
|
|
||||||
|
|
||||||
## Creates a new socket connection.
|
## Creates a new socket connection.
|
||||||
def _createSocket(self):
|
def _createSocket(self):
|
||||||
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
|
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
|
||||||
|
self._engine_is_fresh = True
|
||||||
## Manually triggers a reslice
|
|
||||||
def forceSlice(self):
|
|
||||||
self._change_timer.start()
|
|
||||||
|
|
||||||
## Called when anything has changed to the stuff that needs to be sliced.
|
## Called when anything has changed to the stuff that needs to be sliced.
|
||||||
#
|
#
|
||||||
# This indicates that we should probably re-slice soon.
|
# This indicates that we should probably re-slice soon.
|
||||||
def _onChanged(self, *args, **kwargs):
|
def _onChanged(self, *args, **kwargs):
|
||||||
|
self.needsSlicing()
|
||||||
|
if self._use_timer:
|
||||||
|
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
|
||||||
|
# otherwise business as usual
|
||||||
|
if self._is_error_check_scheduled:
|
||||||
|
self._change_timer.stop()
|
||||||
|
else:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
|
## Called when a print time message is received from the engine.
|
||||||
|
#
|
||||||
|
# \param message The protobuf message containing the print time per feature and
|
||||||
|
# material amount per extruder
|
||||||
|
def _onPrintTimeMaterialEstimates(self, message):
|
||||||
|
material_amounts = []
|
||||||
|
for index in range(message.repeatedMessageCount("materialEstimates")):
|
||||||
|
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
||||||
|
|
||||||
|
times = self._parseMessagePrintTimes(message)
|
||||||
|
self.printDurationMessage.emit(times, material_amounts)
|
||||||
|
|
||||||
|
## Called for parsing message to retrieve estimated time per feature
|
||||||
|
#
|
||||||
|
# \param message The protobuf message containing the print time per feature
|
||||||
|
def _parseMessagePrintTimes(self, message):
|
||||||
|
result = {
|
||||||
|
"inset_0": message.time_inset_0,
|
||||||
|
"inset_x": message.time_inset_x,
|
||||||
|
"skin": message.time_skin,
|
||||||
|
"infill": message.time_infill,
|
||||||
|
"support_infill": message.time_support_infill,
|
||||||
|
"support_interface": message.time_support_interface,
|
||||||
|
"support": message.time_support,
|
||||||
|
"skirt": message.time_skirt,
|
||||||
|
"travel": message.time_travel,
|
||||||
|
"retract": message.time_retract,
|
||||||
|
"none": message.time_none
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
## Called when the back-end connects to the front-end.
|
## Called when the back-end connects to the front-end.
|
||||||
def _onBackendConnected(self):
|
def _onBackendConnected(self):
|
||||||
if self._restart:
|
if self._restart:
|
||||||
self._onChanged()
|
|
||||||
self._restart = False
|
self._restart = False
|
||||||
|
self._onChanged()
|
||||||
|
|
||||||
## Called when the user starts using some tool.
|
## Called when the user starts using some tool.
|
||||||
#
|
#
|
||||||
|
|
@ -413,9 +584,12 @@ class CuraEngineBackend(Backend):
|
||||||
#
|
#
|
||||||
# \param tool The tool that the user is using.
|
# \param tool The tool that the user is using.
|
||||||
def _onToolOperationStarted(self, tool):
|
def _onToolOperationStarted(self, tool):
|
||||||
self._enabled = False # Do not reslice when a tool is doing it's 'thing'
|
self._tool_active = True # Do not react on scene change
|
||||||
self._terminate() # Do not continue slicing once a tool has started
|
self.disableTimer()
|
||||||
|
# Restart engine as soon as possible, we know we want to slice afterwards
|
||||||
|
if not self._engine_is_fresh:
|
||||||
|
self._terminate()
|
||||||
|
self._createSocket()
|
||||||
|
|
||||||
## Called when the user stops using some tool.
|
## Called when the user stops using some tool.
|
||||||
#
|
#
|
||||||
|
|
@ -423,13 +597,18 @@ class CuraEngineBackend(Backend):
|
||||||
#
|
#
|
||||||
# \param tool The tool that the user was using.
|
# \param tool The tool that the user was using.
|
||||||
def _onToolOperationStopped(self, tool):
|
def _onToolOperationStopped(self, tool):
|
||||||
self._enabled = True # Tool stop, start listening for changes again.
|
self._tool_active = False # React on scene change again
|
||||||
|
self.determineAutoSlicing() # Switch timer on if appropriate
|
||||||
|
# Process all the postponed scene changes
|
||||||
|
while self._postponed_scene_change_sources:
|
||||||
|
source = self._postponed_scene_change_sources.pop(0)
|
||||||
|
self._onSceneChanged(source)
|
||||||
|
|
||||||
## Called when the user changes the active view mode.
|
## Called when the user changes the active view mode.
|
||||||
def _onActiveViewChanged(self):
|
def _onActiveViewChanged(self):
|
||||||
if Application.getInstance().getController().getActiveView():
|
if Application.getInstance().getController().getActiveView():
|
||||||
view = Application.getInstance().getController().getActiveView()
|
view = Application.getInstance().getController().getActiveView()
|
||||||
if view.getPluginId() == "LayerView": # 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
|
||||||
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
|
||||||
|
|
@ -449,7 +628,6 @@ class CuraEngineBackend(Backend):
|
||||||
if self._process:
|
if self._process:
|
||||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
||||||
self._process = None
|
self._process = None
|
||||||
self._createSocket()
|
|
||||||
|
|
||||||
## Called when the global container stack changes
|
## Called when the global container stack changes
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self):
|
||||||
|
|
@ -457,9 +635,10 @@ class CuraEngineBackend(Backend):
|
||||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
||||||
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
||||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||||
if extruders:
|
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
||||||
|
extruder.containersChanged.disconnect(self._onChanged)
|
||||||
|
|
||||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
|
@ -467,26 +646,35 @@ class CuraEngineBackend(Backend):
|
||||||
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.
|
||||||
self._global_container_stack.containersChanged.connect(self._onChanged)
|
self._global_container_stack.containersChanged.connect(self._onChanged)
|
||||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||||
if extruders:
|
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
extruder.propertyChanged.connect(self._onSettingChanged)
|
extruder.propertyChanged.connect(self._onSettingChanged)
|
||||||
self._onActiveExtruderChanged()
|
extruder.containersChanged.connect(self._onChanged)
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
|
||||||
if self._global_container_stack:
|
|
||||||
# Connect all extruders of the active machine. This might cause a few connects that have already happend,
|
|
||||||
# but that shouldn't cause issues as only new / unique connections are added.
|
|
||||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
|
||||||
if extruders:
|
|
||||||
for extruder in extruders:
|
|
||||||
extruder.propertyChanged.connect(self._onSettingChanged)
|
|
||||||
if self._active_extruder_stack:
|
|
||||||
self._active_extruder_stack.containersChanged.disconnect(self._onChanged)
|
|
||||||
|
|
||||||
self._active_extruder_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
|
|
||||||
if self._active_extruder_stack:
|
|
||||||
self._active_extruder_stack.containersChanged.connect(self._onChanged)
|
|
||||||
|
|
||||||
def _onProcessLayersFinished(self, job):
|
def _onProcessLayersFinished(self, job):
|
||||||
self._process_layers_job = None
|
self._process_layers_job = None
|
||||||
|
|
||||||
|
## Connect slice function to timer.
|
||||||
|
def enableTimer(self):
|
||||||
|
if not self._use_timer:
|
||||||
|
self._change_timer.timeout.connect(self.slice)
|
||||||
|
self._use_timer = True
|
||||||
|
|
||||||
|
## Disconnect slice function from timer.
|
||||||
|
# This means that slicing will not be triggered automatically
|
||||||
|
def disableTimer(self):
|
||||||
|
if self._use_timer:
|
||||||
|
self._use_timer = False
|
||||||
|
self._change_timer.timeout.disconnect(self.slice)
|
||||||
|
|
||||||
|
def _onPreferencesChanged(self, preference):
|
||||||
|
if preference != "general/auto_slice":
|
||||||
|
return
|
||||||
|
auto_slice = self.determineAutoSlicing()
|
||||||
|
if auto_slice:
|
||||||
|
self._change_timer.start()
|
||||||
|
|
||||||
|
## Tickle the backend so in case of auto slicing, it starts the timer.
|
||||||
|
def tickle(self):
|
||||||
|
if self._use_timer:
|
||||||
|
self._change_timer.start()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
#Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
#Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
|
|
||||||
|
|
@ -8,6 +8,8 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Mesh.MeshData import MeshData
|
from UM.Mesh.MeshData import MeshData
|
||||||
|
from UM.Preferences import Preferences
|
||||||
|
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||||
|
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
@ -15,21 +17,37 @@ from UM.Logger import Logger
|
||||||
|
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura import LayerDataBuilder
|
from cura import LayerDataBuilder
|
||||||
from cura import LayerDataDecorator
|
from cura import LayerDataDecorator
|
||||||
from cura import LayerPolygon
|
from cura import LayerPolygon
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from time import time
|
from time import time
|
||||||
|
from cura.Settings.ExtrudersModel import ExtrudersModel
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Return a 4-tuple with floats 0-1 representing the html color code
|
||||||
|
#
|
||||||
|
# \param color_code html color code, i.e. "#FF0000" -> red
|
||||||
|
def colorCodeToRGBA(color_code):
|
||||||
|
if color_code is None:
|
||||||
|
Logger.log("w", "Unable to convert color code, returning default")
|
||||||
|
return [0, 0, 0, 1]
|
||||||
|
return [
|
||||||
|
int(color_code[1:3], 16) / 255,
|
||||||
|
int(color_code[3:5], 16) / 255,
|
||||||
|
int(color_code[5:7], 16) / 255,
|
||||||
|
1.0]
|
||||||
|
|
||||||
|
|
||||||
class ProcessSlicedLayersJob(Job):
|
class ProcessSlicedLayersJob(Job):
|
||||||
def __init__(self, layers):
|
def __init__(self, layers):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._layers = layers
|
self._layers = layers
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
self._progress = None
|
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
||||||
self._abort_requested = False
|
self._abort_requested = False
|
||||||
|
|
||||||
## Aborts the processing of layers.
|
## Aborts the processing of layers.
|
||||||
|
|
@ -43,13 +61,12 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
start_time = time()
|
start_time = time()
|
||||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
|
||||||
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
self._progress_message.show()
|
||||||
self._progress.show()
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
if self._abort_requested:
|
if self._abort_requested:
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||||
|
|
@ -62,8 +79,8 @@ class ProcessSlicedLayersJob(Job):
|
||||||
node.getParent().removeChild(node)
|
node.getParent().removeChild(node)
|
||||||
break
|
break
|
||||||
if self._abort_requested:
|
if self._abort_requested:
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Force garbage collection.
|
# Force garbage collection.
|
||||||
|
|
@ -78,16 +95,22 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
# Find the minimum layer number
|
# Find the minimum layer number
|
||||||
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
|
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
|
||||||
# instead simply offset all other layers so the lowest layer is always 0.
|
# instead simply offset all other layers so the lowest layer is always 0. It could happens that
|
||||||
|
# the first raft layer has value -8 but there are just 4 raft (negative) layers.
|
||||||
min_layer_number = 0
|
min_layer_number = 0
|
||||||
|
negative_layers = 0
|
||||||
for layer in self._layers:
|
for layer in self._layers:
|
||||||
if layer.id < min_layer_number:
|
if layer.id < min_layer_number:
|
||||||
min_layer_number = layer.id
|
min_layer_number = layer.id
|
||||||
|
if layer.id < 0:
|
||||||
|
negative_layers += 1
|
||||||
|
|
||||||
current_layer = 0
|
current_layer = 0
|
||||||
|
|
||||||
for layer in self._layers:
|
for layer in self._layers:
|
||||||
abs_layer_number = layer.id + abs(min_layer_number)
|
# Negative layers are offset by the minimum layer number, but the positive layers are just
|
||||||
|
# offset by the number of negative layers so there is no layer gap between raft and model
|
||||||
|
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers
|
||||||
|
|
||||||
layer_data.addLayer(abs_layer_number)
|
layer_data.addLayer(abs_layer_number)
|
||||||
this_layer = layer_data.getLayer(abs_layer_number)
|
this_layer = layer_data.getLayer(abs_layer_number)
|
||||||
|
|
@ -111,6 +134,12 @@ class ProcessSlicedLayersJob(Job):
|
||||||
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
|
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
|
||||||
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
|
line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array
|
||||||
|
line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
|
line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array
|
||||||
|
line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
# Create a new 3D-array, copy the 2D points over and insert the right height.
|
# Create a new 3D-array, copy the 2D points over and insert the right height.
|
||||||
# This uses manual array creation + copy rather than numpy.insert since this is
|
# This uses manual array creation + copy rather than numpy.insert since this is
|
||||||
# faster.
|
# faster.
|
||||||
|
|
@ -124,7 +153,7 @@ class ProcessSlicedLayersJob(Job):
|
||||||
new_points[:, 1] = points[:, 2]
|
new_points[:, 1] = points[:, 2]
|
||||||
new_points[:, 2] = -points[:, 1]
|
new_points[:, 2] = -points[:, 1]
|
||||||
|
|
||||||
this_poly = LayerPolygon.LayerPolygon(layer_data, extruder, line_types, new_points, line_widths)
|
this_poly = LayerPolygon.LayerPolygon(extruder, line_types, new_points, line_widths, line_thicknesses, line_feedrates)
|
||||||
this_poly.buildCache()
|
this_poly.buildCache()
|
||||||
|
|
||||||
this_layer.polygons.append(this_poly)
|
this_layer.polygons.append(this_poly)
|
||||||
|
|
@ -137,18 +166,46 @@ class ProcessSlicedLayersJob(Job):
|
||||||
# This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.
|
# This needs some work in LayerData so we can add the new layers instead of recreating the entire mesh.
|
||||||
|
|
||||||
if self._abort_requested:
|
if self._abort_requested:
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
return
|
return
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.setProgress(progress)
|
self._progress_message.setProgress(progress)
|
||||||
|
|
||||||
# We are done processing all the layers we got from the engine, now create a mesh out of the data
|
# We are done processing all the layers we got from the engine, now create a mesh out of the data
|
||||||
layer_mesh = layer_data.build()
|
|
||||||
|
# Find out colors per extruder
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
manager = ExtruderManager.getInstance()
|
||||||
|
extruders = list(manager.getMachineExtruders(global_container_stack.getId()))
|
||||||
|
if extruders:
|
||||||
|
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
||||||
|
for extruder in extruders:
|
||||||
|
position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position
|
||||||
|
try:
|
||||||
|
default_color = ExtrudersModel.defaultColors[position]
|
||||||
|
except IndexError:
|
||||||
|
default_color = "#e0e000"
|
||||||
|
color_code = extruder.material.getMetaDataEntry("color_code", default=default_color)
|
||||||
|
color = colorCodeToRGBA(color_code)
|
||||||
|
material_color_map[position, :] = color
|
||||||
|
else:
|
||||||
|
# Single extruder via global stack.
|
||||||
|
material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
|
||||||
|
color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000")
|
||||||
|
color = colorCodeToRGBA(color_code)
|
||||||
|
material_color_map[0, :] = color
|
||||||
|
|
||||||
|
# We have to scale the colors for compatibility mode
|
||||||
|
if OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode")):
|
||||||
|
line_type_brightness = 0.5 # for compatibility mode
|
||||||
|
else:
|
||||||
|
line_type_brightness = 1.0
|
||||||
|
layer_mesh = layer_data.build(material_color_map, line_type_brightness)
|
||||||
|
|
||||||
if self._abort_requested:
|
if self._abort_requested:
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add LayerDataDecorator to scene node to indicate that the node has layer data
|
# Add LayerDataDecorator to scene node to indicate that the node has layer data
|
||||||
|
|
@ -166,15 +223,15 @@ class ProcessSlicedLayersJob(Job):
|
||||||
if not settings.getProperty("machine_center_is_zero", "value"):
|
if not settings.getProperty("machine_center_is_zero", "value"):
|
||||||
new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2))
|
new_node.setPosition(Vector(-settings.getProperty("machine_width", "value") / 2, 0.0, settings.getProperty("machine_depth", "value") / 2))
|
||||||
|
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.setProgress(100)
|
self._progress_message.setProgress(100)
|
||||||
|
|
||||||
view = Application.getInstance().getController().getActiveView()
|
view = Application.getInstance().getController().getActiveView()
|
||||||
if view.getPluginId() == "LayerView":
|
if view.getPluginId() == "SimulationView":
|
||||||
view.resetLayerData()
|
view.resetLayerData()
|
||||||
|
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
|
|
||||||
# Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
|
# Clear the unparsed layers. This saves us a bunch of memory if the Job does not get destroyed.
|
||||||
self._layers = None
|
self._layers = None
|
||||||
|
|
@ -183,11 +240,12 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
def _onActiveViewChanged(self):
|
def _onActiveViewChanged(self):
|
||||||
if self.isRunning():
|
if self.isRunning():
|
||||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
if Application.getInstance().getController().getActiveView().getPluginId() == "SimulationView":
|
||||||
if not self._progress:
|
if not self._progress_message:
|
||||||
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
|
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0, catalog.i18nc("@info:title", "Information"))
|
||||||
if self._progress.getProgress() != 100:
|
if self._progress_message.getProgress() != 100:
|
||||||
self._progress.show()
|
self._progress_message.show()
|
||||||
else:
|
else:
|
||||||
if self._progress:
|
if self._progress_message:
|
||||||
self._progress.hide()
|
self._progress_message.hide()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from string import Formatter
|
from string import Formatter
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
import time
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
@ -16,8 +17,7 @@ from UM.Settings.Validator import ValidatorState
|
||||||
from UM.Settings.SettingRelation import RelationType
|
from UM.Settings.SettingRelation import RelationType
|
||||||
|
|
||||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
import cura.Settings
|
|
||||||
|
|
||||||
class StartJobResult(IntEnum):
|
class StartJobResult(IntEnum):
|
||||||
Finished = 1
|
Finished = 1
|
||||||
|
|
@ -26,6 +26,7 @@ class StartJobResult(IntEnum):
|
||||||
NothingToSlice = 4
|
NothingToSlice = 4
|
||||||
MaterialIncompatible = 5
|
MaterialIncompatible = 5
|
||||||
BuildPlateError = 6
|
BuildPlateError = 6
|
||||||
|
ObjectSettingError = 7 #When an error occurs in per-object settings.
|
||||||
|
|
||||||
|
|
||||||
## Formatter class that handles token expansion in start/end gcod
|
## Formatter class that handles token expansion in start/end gcod
|
||||||
|
|
@ -84,7 +85,7 @@ class StartSliceJob(Job):
|
||||||
self.setResult(StartJobResult.BuildPlateError)
|
self.setResult(StartJobResult.BuildPlateError)
|
||||||
return
|
return
|
||||||
|
|
||||||
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||||
material = extruder_stack.findContainer({"type": "material"})
|
material = extruder_stack.findContainer({"type": "material"})
|
||||||
if material:
|
if material:
|
||||||
if material.getMetaDataEntry("compatible") == False:
|
if material.getMetaDataEntry("compatible") == False:
|
||||||
|
|
@ -97,7 +98,7 @@ class StartSliceJob(Job):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._checkStackForErrors(node.callDecoration("getStack")):
|
if self._checkStackForErrors(node.callDecoration("getStack")):
|
||||||
self.setResult(StartJobResult.SettingError)
|
self.setResult(StartJobResult.ObjectSettingError)
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._scene.getSceneLock():
|
with self._scene.getSceneLock():
|
||||||
|
|
@ -132,7 +133,7 @@ class StartSliceJob(Job):
|
||||||
temp_list = []
|
temp_list = []
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||||
if not getattr(node, "_outside_buildarea", False):
|
if not getattr(node, "_outside_buildarea", False) or getattr(node, "_non_printing_mesh", False):
|
||||||
temp_list.append(node)
|
temp_list.append(node)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
|
@ -149,7 +150,8 @@ class StartSliceJob(Job):
|
||||||
self._buildGlobalSettingsMessage(stack)
|
self._buildGlobalSettingsMessage(stack)
|
||||||
self._buildGlobalInheritsStackMessage(stack)
|
self._buildGlobalInheritsStackMessage(stack)
|
||||||
|
|
||||||
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
# Build messages for extruder stacks
|
||||||
|
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||||
self._buildExtruderMessage(extruder_stack)
|
self._buildExtruderMessage(extruder_stack)
|
||||||
|
|
||||||
for group in object_groups:
|
for group in object_groups:
|
||||||
|
|
@ -157,28 +159,29 @@ class StartSliceJob(Job):
|
||||||
if group[0].getParent().callDecoration("isGroup"):
|
if group[0].getParent().callDecoration("isGroup"):
|
||||||
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
||||||
for object in group:
|
for object in group:
|
||||||
mesh_data = object.getMeshData().getTransformed(object.getWorldTransformation())
|
mesh_data = object.getMeshData()
|
||||||
|
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||||
|
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||||
|
|
||||||
obj = group_message.addRepeatedMessage("objects")
|
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||||
obj.id = id(object)
|
|
||||||
verts = mesh_data.getVertices()
|
verts = mesh_data.getVertices()
|
||||||
indices = mesh_data.getIndices()
|
verts = verts.dot(rot_scale)
|
||||||
if indices is not None:
|
verts += translate
|
||||||
#TODO: This is a very slow way of doing it! It also locks up the GUI.
|
|
||||||
flat_vert_list = []
|
|
||||||
for face in indices:
|
|
||||||
for vert_index in face:
|
|
||||||
flat_vert_list.append(verts[vert_index])
|
|
||||||
Job.yieldThread()
|
|
||||||
verts = numpy.array(flat_vert_list)
|
|
||||||
else:
|
|
||||||
verts = numpy.array(verts)
|
|
||||||
|
|
||||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||||
verts[:, 1] *= -1
|
verts[:, 1] *= -1
|
||||||
|
|
||||||
obj.vertices = verts
|
obj = group_message.addRepeatedMessage("objects")
|
||||||
|
obj.id = id(object)
|
||||||
|
|
||||||
|
indices = mesh_data.getIndices()
|
||||||
|
if indices is not None:
|
||||||
|
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||||
|
else:
|
||||||
|
flat_verts = numpy.array(verts)
|
||||||
|
|
||||||
|
obj.vertices = flat_verts
|
||||||
|
|
||||||
self._handlePerObjectSettings(object, obj)
|
self._handlePerObjectSettings(object, obj)
|
||||||
|
|
||||||
|
|
@ -193,33 +196,61 @@ class StartSliceJob(Job):
|
||||||
def isCancelled(self):
|
def isCancelled(self):
|
||||||
return self._is_cancelled
|
return self._is_cancelled
|
||||||
|
|
||||||
def _expandGcodeTokens(self, key, value, settings):
|
## Creates a dictionary of tokens to replace in g-code pieces.
|
||||||
|
#
|
||||||
|
# This indicates what should be replaced in the start and end g-codes.
|
||||||
|
# \param stack The stack to get the settings from to replace the tokens
|
||||||
|
# with.
|
||||||
|
# \return A dictionary of replacement tokens to the values they should be
|
||||||
|
# replaced with.
|
||||||
|
def _buildReplacementTokens(self, stack) -> dict:
|
||||||
|
result = {}
|
||||||
|
for key in stack.getAllKeys():
|
||||||
|
result[key] = stack.getProperty(key, "value")
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
|
result["print_bed_temperature"] = result["material_bed_temperature"] #Renamed settings.
|
||||||
|
result["print_temperature"] = result["material_print_temperature"]
|
||||||
|
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||||
|
result["date"] = time.strftime("%d-%m-%Y")
|
||||||
|
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Replace setting tokens in a piece of g-code.
|
||||||
|
# \param value A piece of g-code to replace tokens in.
|
||||||
|
# \param settings A dictionary of tokens to replace and their respective
|
||||||
|
# replacement strings.
|
||||||
|
def _expandGcodeTokens(self, value: str, settings: dict):
|
||||||
try:
|
try:
|
||||||
# any setting can be used as a token
|
# any setting can be used as a token
|
||||||
fmt = GcodeStartEndFormatter()
|
fmt = GcodeStartEndFormatter()
|
||||||
return str(fmt.format(value, **settings)).encode("utf-8")
|
return str(fmt.format(value, **settings))
|
||||||
except:
|
except:
|
||||||
Logger.logException("w", "Unable to do token replacement on start/end gcode")
|
Logger.logException("w", "Unable to do token replacement on start/end gcode")
|
||||||
return str(value).encode("utf-8")
|
return str(value)
|
||||||
|
|
||||||
## Create extruder message from stack
|
## Create extruder message from stack
|
||||||
def _buildExtruderMessage(self, stack):
|
def _buildExtruderMessage(self, stack):
|
||||||
message = self._slice_message.addRepeatedMessage("extruders")
|
message = self._slice_message.addRepeatedMessage("extruders")
|
||||||
message.id = int(stack.getMetaDataEntry("position"))
|
message.id = int(stack.getMetaDataEntry("position"))
|
||||||
|
|
||||||
material_instance_container = stack.findContainer({"type": "material"})
|
settings = self._buildReplacementTokens(stack)
|
||||||
|
|
||||||
for key in stack.getAllKeys():
|
#Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
||||||
|
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
||||||
|
|
||||||
|
#Replace the setting tokens in start and end g-code.
|
||||||
|
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
|
||||||
|
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
|
||||||
|
|
||||||
|
for key, value in settings.items():
|
||||||
# Do not send settings that are not settable_per_extruder.
|
# Do not send settings that are not settable_per_extruder.
|
||||||
if stack.getProperty(key, "settable_per_extruder") == False:
|
if not stack.getProperty(key, "settable_per_extruder"):
|
||||||
continue
|
continue
|
||||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||||
setting.name = key
|
setting.name = key
|
||||||
if key == "material_guid" and material_instance_container:
|
setting.value = str(value).encode("utf-8")
|
||||||
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
|
||||||
setting.value = str(material_instance_container.getMetaDataEntry("GUID", "")).encode("utf-8")
|
|
||||||
else:
|
|
||||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Sends all global settings to the engine.
|
## Sends all global settings to the engine.
|
||||||
|
|
@ -227,35 +258,22 @@ class StartSliceJob(Job):
|
||||||
# The settings are taken from the global stack. This does not include any
|
# The settings are taken from the global stack. This does not include any
|
||||||
# per-extruder settings or per-object settings.
|
# per-extruder settings or per-object settings.
|
||||||
def _buildGlobalSettingsMessage(self, stack):
|
def _buildGlobalSettingsMessage(self, stack):
|
||||||
keys = stack.getAllKeys()
|
settings = self._buildReplacementTokens(stack)
|
||||||
settings = {}
|
|
||||||
for key in keys:
|
|
||||||
# Use resolvement value if available, or take the value
|
|
||||||
resolved_value = stack.getProperty(key, "resolve")
|
|
||||||
if resolved_value is not None:
|
|
||||||
# There is a resolvement value. Check if we need to use it.
|
|
||||||
user_container = stack.findContainer({"type": "user"})
|
|
||||||
quality_changes_container = stack.findContainer({"type": "quality_changes"})
|
|
||||||
if user_container.hasProperty(key,"value") or quality_changes_container.hasProperty(key,"value"):
|
|
||||||
# Normal case
|
|
||||||
settings[key] = stack.getProperty(key, "value")
|
|
||||||
else:
|
|
||||||
settings[key] = resolved_value
|
|
||||||
else:
|
|
||||||
# Normal case
|
|
||||||
settings[key] = stack.getProperty(key, "value")
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
start_gcode = settings["machine_start_gcode"]
|
start_gcode = settings["machine_start_gcode"]
|
||||||
settings["material_bed_temp_prepend"] = "{material_bed_temperature}" not in start_gcode #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
#Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||||
settings["material_print_temp_prepend"] = "{material_print_temperature}" not in start_gcode
|
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
|
||||||
|
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
|
||||||
|
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
|
||||||
|
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
|
||||||
|
|
||||||
|
#Replace the setting tokens in start and end g-code.
|
||||||
|
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], settings)
|
||||||
|
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], settings)
|
||||||
|
|
||||||
for key, value in settings.items(): #Add all submessages for each individual setting.
|
for key, value in settings.items(): #Add all submessages for each individual setting.
|
||||||
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
||||||
setting_message.name = key
|
setting_message.name = key
|
||||||
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
|
|
||||||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
|
||||||
else:
|
|
||||||
setting_message.value = str(value).encode("utf-8")
|
setting_message.value = str(value).encode("utf-8")
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
|
@ -281,16 +299,22 @@ class StartSliceJob(Job):
|
||||||
# \param message object_lists message to put the per object settings in
|
# \param message object_lists message to put the per object settings in
|
||||||
def _handlePerObjectSettings(self, node, message):
|
def _handlePerObjectSettings(self, node, message):
|
||||||
stack = node.callDecoration("getStack")
|
stack = node.callDecoration("getStack")
|
||||||
|
|
||||||
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
||||||
if stack:
|
if not stack:
|
||||||
# Check all settings for relations, so we can also calculate the correct values for dependant settings.
|
return
|
||||||
changed_setting_keys = set(stack.getTop().getAllKeys())
|
|
||||||
for key in stack.getTop().getAllKeys():
|
# Check all settings for relations, so we can also calculate the correct values for dependent settings.
|
||||||
instance = stack.getTop().getInstance(key)
|
top_of_stack = stack.getTop() # Cache for efficiency.
|
||||||
|
changed_setting_keys = set(top_of_stack.getAllKeys())
|
||||||
|
|
||||||
|
# Add all relations to changed settings as well.
|
||||||
|
for key in top_of_stack.getAllKeys():
|
||||||
|
instance = top_of_stack.getInstance(key)
|
||||||
self._addRelations(changed_setting_keys, instance.definition.relations)
|
self._addRelations(changed_setting_keys, instance.definition.relations)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
# Ensure that the engine is aware what the build extruder is
|
# Ensure that the engine is aware what the build extruder is.
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
changed_setting_keys.add("extruder_nr")
|
changed_setting_keys.add("extruder_nr")
|
||||||
|
|
||||||
|
|
@ -298,16 +322,26 @@ class StartSliceJob(Job):
|
||||||
for key in changed_setting_keys:
|
for key in changed_setting_keys:
|
||||||
setting = message.addRepeatedMessage("settings")
|
setting = message.addRepeatedMessage("settings")
|
||||||
setting.name = key
|
setting.name = key
|
||||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||||
|
|
||||||
|
# Check if limited to a specific extruder, but not overridden by per-object settings.
|
||||||
|
if extruder >= 0 and key not in changed_setting_keys:
|
||||||
|
limited_stack = ExtruderManager.getInstance().getActiveExtruderStacks()[extruder]
|
||||||
|
else:
|
||||||
|
limited_stack = stack
|
||||||
|
|
||||||
|
setting.value = str(limited_stack.getProperty(key, "value")).encode("utf-8")
|
||||||
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
## Recursive function to put all settings that require each other for value changes in a list
|
## Recursive function to put all settings that require each other for value changes in a list
|
||||||
# \param relations_set \type{set} Set of keys (strings) of settings that are influenced
|
# \param relations_set \type{set} Set of keys (strings) of settings that are influenced
|
||||||
# \param relations list of relation objects that need to be checked.
|
# \param relations list of relation objects that need to be checked.
|
||||||
def _addRelations(self, relations_set, relations):
|
def _addRelations(self, relations_set, relations):
|
||||||
for relation in filter(lambda r: r.role == "value", relations):
|
for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations):
|
||||||
if relation.type == RelationType.RequiresTarget:
|
if relation.type == RelationType.RequiresTarget:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
relations_set.add(relation.target.key)
|
relations_set.add(relation.target.key)
|
||||||
self._addRelations(relations_set, relation.target.relations)
|
self._addRelations(relations_set, relation.target.relations)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
#Shoopdawoop
|
#Shoopdawoop
|
||||||
from . import CuraEngineBackend
|
from . import CuraEngineBackend
|
||||||
|
|
@ -8,14 +8,7 @@ from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
return {
|
return {}
|
||||||
"plugin": {
|
|
||||||
"name": catalog.i18nc("@label", "CuraEngine Backend"),
|
|
||||||
"author": "Ultimaker",
|
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Provides the link to the CuraEngine slicing backend."),
|
|
||||||
"api": 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def register(app):
|
def register(app):
|
||||||
return { "backend": CuraEngineBackend.CuraEngineBackend() }
|
return { "backend": CuraEngineBackend.CuraEngineBackend() }
|
||||||
|
|
|
||||||
8
plugins/CuraEngineBackend/plugin.json
Normal file
8
plugins/CuraEngineBackend/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "CuraEngine Backend",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||||
|
"api": 4,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Copyright (c) 2016 Ultimaker B.V.
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from UM import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
||||||
from cura.ProfileReader import ProfileReader
|
from cura.ProfileReader import ProfileReader
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the AGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from . import CuraProfileReader
|
from . import CuraProfileReader
|
||||||
|
|
||||||
|
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
return {
|
return {
|
||||||
"plugin": {
|
|
||||||
"name": catalog.i18nc("@label", "Cura Profile Reader"),
|
|
||||||
"author": "Ultimaker",
|
|
||||||
"version": "1.0",
|
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing Cura profiles."),
|
|
||||||
"api": 3
|
|
||||||
},
|
|
||||||
"profile_reader": [
|
"profile_reader": [
|
||||||
{
|
{
|
||||||
"extension": "curaprofile",
|
"extension": "curaprofile",
|
||||||
|
|
|
||||||
8
plugins/CuraProfileReader/plugin.json
Normal file
8
plugins/CuraProfileReader/plugin.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Cura Profile Reader",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Provides support for importing Cura profiles.",
|
||||||
|
"api": 4,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Copyright (c) 2013 David Braam
|
# Copyright (c) 2013 David Braam
|
||||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.SaveFile import SaveFile
|
from UM.SaveFile import SaveFile
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue