mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-11-02 20:52:20 -07:00
Merge branch 'master' into master
This commit is contained in:
commit
d2e2f13bd7
1060 changed files with 262463 additions and 181480 deletions
45
.gitignore
vendored
45
.gitignore
vendored
|
|
@ -5,9 +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
|
||||||
|
CuraEngine.exe
|
||||||
|
LC_MESSAGES
|
||||||
|
.cache
|
||||||
|
*.qmlc
|
||||||
|
|
||||||
|
#MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
# Editors and IDEs.
|
# Editors and IDEs.
|
||||||
*kdev*
|
*kdev*
|
||||||
|
|
@ -16,11 +25,41 @@ resources/firmware
|
||||||
*~
|
*~
|
||||||
*.qm
|
*.qm
|
||||||
.idea
|
.idea
|
||||||
|
cura.desktop
|
||||||
|
|
||||||
# Eclipse+PyDev
|
# Eclipse+PyDev
|
||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
.settings
|
.settings
|
||||||
|
|
||||||
# Debian packaging
|
#Externally located plug-ins.
|
||||||
debian*
|
plugins/CuraSolidWorksPlugin
|
||||||
|
plugins/Doodle3D-cura-plugin
|
||||||
|
plugins/GodMode
|
||||||
|
plugins/PostProcessingPlugin
|
||||||
|
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,84 +1,69 @@
|
||||||
|
project(cura NONE)
|
||||||
project(cura)
|
|
||||||
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)
|
||||||
|
|
||||||
# Macro needed to list all sub-directory of a directory.
|
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||||
# There is no function in cmake as far as I know.
|
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||||
# Found at: http://stackoverflow.com/a/7788165
|
endif()
|
||||||
MACRO(SUBDIRLIST result curdir)
|
|
||||||
FILE(GLOB children RELATIVE ${curdir} ${curdir}/*)
|
|
||||||
SET(dirlist "")
|
|
||||||
FOREACH(child ${children})
|
|
||||||
IF(IS_DIRECTORY ${curdir}/${child})
|
|
||||||
STRING(REPLACE "/" "" child ${child})
|
|
||||||
LIST(APPEND dirlist ${child})
|
|
||||||
ENDIF()
|
|
||||||
ENDFOREACH()
|
|
||||||
SET(${result} ${dirlist})
|
|
||||||
ENDMACRO()
|
|
||||||
|
|
||||||
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${URANIUM_DIR}/cmake)
|
||||||
|
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)
|
||||||
|
|
||||||
# Build Translations
|
# Build Translations
|
||||||
find_package(Gettext)
|
CREATE_TRANSLATION_TARGETS()
|
||||||
if(GETTEXT_FOUND)
|
|
||||||
# translations target will convert .po files into .mo and .qm as needed.
|
|
||||||
# The files are checked for a _qt suffix and if it is found, converted to
|
|
||||||
# qm, otherwise they are converted to .po.
|
|
||||||
add_custom_target(translations ALL)
|
|
||||||
# copy-translations can be used to copy the built translation files from the
|
|
||||||
# build directory to the source resources directory. This is mostly a convenience
|
|
||||||
# during development, normally you want to simply use the install target to install
|
|
||||||
# the files along side the rest of the application.
|
|
||||||
|
|
||||||
SUBDIRLIST(languages ${CMAKE_SOURCE_DIR}/resources/i18n/)
|
|
||||||
foreach(lang ${languages})
|
|
||||||
file(GLOB po_files ${CMAKE_SOURCE_DIR}/resources/i18n/${lang}/*.po)
|
|
||||||
foreach(po_file ${po_files})
|
|
||||||
string(REGEX REPLACE ".*/(.*).po" "${CMAKE_BINARY_DIR}/resources/i18n/${lang}/LC_MESSAGES/\\1.mo" mo_file ${po_file})
|
|
||||||
add_custom_command(TARGET translations POST_BUILD COMMAND mkdir ARGS -p ${CMAKE_BINARY_DIR}/resources/i18n/${lang}/LC_MESSAGES/ COMMAND ${GETTEXT_MSGFMT_EXECUTABLE} ARGS ${po_file} -o ${mo_file} -f)
|
|
||||||
endforeach()
|
|
||||||
endforeach()
|
|
||||||
install(DIRECTORY ${CMAKE_BINARY_DIR}/resources
|
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
|
||||||
endif()
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(PythonInterp 3.4.0 REQUIRED)
|
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)
|
||||||
install(DIRECTORY cura
|
if(EXISTS /etc/debian_version)
|
||||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages
|
install(DIRECTORY cura
|
||||||
|
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
|
||||||
|
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 )
|
||||||
|
|
@ -87,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.'])
|
||||||
|
}
|
||||||
|
}
|
||||||
816
LICENSE
816
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.
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
You may make, run and propagate covered works that you do not
|
Library as you received it specifies that a certain numbered version
|
||||||
convey, without conditions so long as your license otherwise remains
|
of the GNU Lesser General Public License "or any later version"
|
||||||
in force. You may convey covered works to others for the sole purpose
|
applies to it, you have the option of following the terms and
|
||||||
of having them make modifications exclusively for you, or provide you
|
conditions either of that published version or of any later version
|
||||||
with facilities for running those works, provided that you comply with
|
published by the Free Software Foundation. If the Library as you
|
||||||
the terms of this License in conveying all material for which you do
|
received it does not specify a version number of the GNU Lesser
|
||||||
not control copyright. Those thus making or running the covered works
|
General Public License, you may choose any version of the GNU Lesser
|
||||||
for you must do so exclusively on your behalf, under your direction
|
General Public License ever published by the Free Software Foundation.
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
If the Library as you received it specifies that a proxy can decide
|
||||||
|
whether future versions of the GNU Lesser General Public License shall
|
||||||
Conveying under any other circumstances is permitted solely under
|
apply, that proxy's public statement of acceptance of any version is
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
permanent authorization for you to choose that version for the
|
||||||
makes it unnecessary.
|
Library.
|
||||||
|
|
||||||
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
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
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/>.
|
|
||||||
56
README.md
56
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,13 +49,16 @@ Please checkout [cura-build](https://github.com/Ultimaker/cura-build)
|
||||||
|
|
||||||
Third party plugins
|
Third party plugins
|
||||||
-------------
|
-------------
|
||||||
* [Print time calculator](https://github.com/nallath/PrintCostCalculator)
|
* [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)
|
* [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.
|
||||||
|
* [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
|
||||||
|
|
@ -56,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()
|
||||||
31
cura.appdata.xml
Normal file
31
cura.appdata.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
|
||||||
|
<component type="desktop">
|
||||||
|
<id>cura.desktop</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>AGPL-3.0 and CC-BY-SA-4.0</project_license>
|
||||||
|
<name>Cura</name>
|
||||||
|
<summary>The world's most advanced 3d printer software</summary>
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Cura creates a seamless integration between hardware, software and
|
||||||
|
materials for the best 3D printing experience around.
|
||||||
|
Cura supports the 3MF, OBJ and STL file formats and is available on
|
||||||
|
Windows, Mac and Linux.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Novices can start printing right away</li>
|
||||||
|
<li>Experts are able to customize 200 settings to achieve the best results</li>
|
||||||
|
<li>Optimized profiles for Ultimaker materials</li>
|
||||||
|
<li>Supported by a global network of Ultimaker certified service partners</li>
|
||||||
|
<li>Print multiple objects at once with different settings for each object</li>
|
||||||
|
<li>Cura supports STL, 3MF and OBJ file formats</li>
|
||||||
|
<li>Open source and completely free</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
<url type="homepage">https://ultimaker.com/en/products/cura-software</url>
|
||||||
|
<translation type="gettext">Cura</translation>
|
||||||
|
</component>
|
||||||
|
|
@ -1,15 +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.
|
||||||
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
|
||||||
986
cura/BuildVolume.py
Normal file → Executable file
986
cura/BuildVolume.py
Normal file → Executable file
File diff suppressed because it is too large
Load diff
|
|
@ -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,7 +1,12 @@
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
from UM.Application import Application
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
from UM.Math.Polygon import Polygon
|
from UM.Math.Polygon import Polygon
|
||||||
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from . import ConvexHullNode
|
from . import ConvexHullNode
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
@ -51,11 +56,18 @@ 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)
|
||||||
return hull
|
return hull
|
||||||
|
|
||||||
## Get the convex hull of the node with the full head size
|
## Get the convex hull of the node with the full head size
|
||||||
|
|
@ -73,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
|
||||||
|
|
@ -87,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
|
||||||
|
|
@ -109,7 +120,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
self._convex_hull_node = hull_node
|
self._convex_hull_node = hull_node
|
||||||
|
|
||||||
def _onSettingValueChanged(self, key, property_name):
|
def _onSettingValueChanged(self, key, property_name):
|
||||||
if key in self._affected_settings and property_name == "value":
|
if property_name != "value": #Not the value that was changed.
|
||||||
|
return
|
||||||
|
|
||||||
|
if key in self._affected_settings:
|
||||||
|
self._onChanged()
|
||||||
|
if key in self._influencing_settings:
|
||||||
|
self._init2DConvexHullCache() #Invalidate the cache.
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
def _init2DConvexHullCache(self):
|
def _init2DConvexHullCache(self):
|
||||||
|
|
@ -138,21 +155,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
if child_polygon == self._2d_convex_hull_group_child_polygon:
|
if child_polygon == self._2d_convex_hull_group_child_polygon:
|
||||||
return self._2d_convex_hull_group_result
|
return self._2d_convex_hull_group_result
|
||||||
|
|
||||||
# First, calculate the normal convex hull around the points
|
convex_hull = child_polygon.getConvexHull() #First calculate the normal convex hull around the points.
|
||||||
convex_hull = child_polygon.getConvexHull()
|
offset_hull = self._offsetHull(convex_hull) #Then apply the offset from the settings.
|
||||||
|
|
||||||
# Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
|
|
||||||
# This is done because of rounding errors.
|
|
||||||
rounded_hull = self._roundHull(convex_hull)
|
|
||||||
|
|
||||||
# Store the result in the cache
|
# Store the result in the cache
|
||||||
self._2d_convex_hull_group_child_polygon = child_polygon
|
self._2d_convex_hull_group_child_polygon = child_polygon
|
||||||
self._2d_convex_hull_group_result = rounded_hull
|
self._2d_convex_hull_group_result = offset_hull
|
||||||
|
|
||||||
return rounded_hull
|
return offset_hull
|
||||||
|
|
||||||
else:
|
else:
|
||||||
rounded_hull = None
|
offset_hull = None
|
||||||
mesh = None
|
mesh = None
|
||||||
world_transform = None
|
world_transform = None
|
||||||
if self._node.getMeshData():
|
if self._node.getMeshData():
|
||||||
|
|
@ -189,20 +202,18 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
hull = Polygon(vertex_data)
|
hull = Polygon(vertex_data)
|
||||||
|
|
||||||
if len(vertex_data) >= 4:
|
if len(vertex_data) >= 3:
|
||||||
# First, calculate the normal convex hull around the points
|
|
||||||
convex_hull = hull.getConvexHull()
|
convex_hull = hull.getConvexHull()
|
||||||
|
offset_hull = self._offsetHull(convex_hull)
|
||||||
# Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
|
else:
|
||||||
# This is done because of rounding errors.
|
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
||||||
rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32)))
|
|
||||||
|
|
||||||
# Store the result in the cache
|
# Store the result in the cache
|
||||||
self._2d_convex_hull_mesh = mesh
|
self._2d_convex_hull_mesh = mesh
|
||||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||||
self._2d_convex_hull_mesh_result = rounded_hull
|
self._2d_convex_hull_mesh_result = offset_hull
|
||||||
|
|
||||||
return rounded_hull
|
return offset_hull
|
||||||
|
|
||||||
def _getHeadAndFans(self):
|
def _getHeadAndFans(self):
|
||||||
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
||||||
|
|
@ -225,40 +236,51 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
# Compensate for raft/skirt/brim
|
# Compensate for raft/skirt/brim
|
||||||
# Add extra margin depending on adhesion type
|
# Add extra margin depending on adhesion type
|
||||||
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
||||||
extra_margin = 0
|
|
||||||
machine_head_coords = numpy.array(
|
|
||||||
self._global_stack.getProperty("machine_head_with_fans_polygon", "value"),
|
|
||||||
numpy.float32)
|
|
||||||
head_y_size = abs(machine_head_coords).min() # safe margin to take off in all directions
|
|
||||||
|
|
||||||
if adhesion_type == "raft":
|
if adhesion_type == "raft":
|
||||||
extra_margin = max(0, self._global_stack.getProperty("raft_margin", "value") - head_y_size)
|
extra_margin = max(0, self._getSettingProperty("raft_margin", "value"))
|
||||||
elif adhesion_type == "brim":
|
elif adhesion_type == "brim":
|
||||||
extra_margin = max(0, self._global_stack.getProperty("brim_width", "value") - head_y_size)
|
extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||||
|
elif adhesion_type == "none":
|
||||||
|
extra_margin = 0
|
||||||
elif adhesion_type == "skirt":
|
elif adhesion_type == "skirt":
|
||||||
extra_margin = max(
|
extra_margin = max(
|
||||||
0, self._global_stack.getProperty("skirt_gap", "value") +
|
0, self._getSettingProperty("skirt_gap", "value") +
|
||||||
self._global_stack.getProperty("skirt_line_count", "value") * self._global_stack.getProperty("skirt_brim_line_width", "value") -
|
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||||
head_y_size)
|
else:
|
||||||
|
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||||
|
|
||||||
# adjust head_and_fans with extra margin
|
# adjust head_and_fans with extra margin
|
||||||
if extra_margin > 0:
|
if extra_margin > 0:
|
||||||
# In Cura 2.2+, there is a function to create this circle-like polygon.
|
extra_margin_polygon = Polygon.approximatedCircle(extra_margin)
|
||||||
extra_margin_polygon = Polygon(numpy.array([
|
|
||||||
[-extra_margin, 0],
|
|
||||||
[-extra_margin * 0.707, extra_margin * 0.707],
|
|
||||||
[0, extra_margin],
|
|
||||||
[extra_margin * 0.707, extra_margin * 0.707],
|
|
||||||
[extra_margin, 0],
|
|
||||||
[extra_margin * 0.707, -extra_margin * 0.707],
|
|
||||||
[0, -extra_margin],
|
|
||||||
[-extra_margin * 0.707, -extra_margin * 0.707]
|
|
||||||
], numpy.float32))
|
|
||||||
|
|
||||||
poly = poly.getMinkowskiHull(extra_margin_polygon)
|
poly = poly.getMinkowskiHull(extra_margin_polygon)
|
||||||
return poly
|
return poly
|
||||||
|
|
||||||
def _roundHull(self, convex_hull):
|
## Offset the convex hull with settings that influence the collision area.
|
||||||
return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32)))
|
#
|
||||||
|
# \param convex_hull Polygon of the original convex hull.
|
||||||
|
# \return New Polygon instance that is offset with everything that
|
||||||
|
# influences the collision area.
|
||||||
|
def _offsetHull(self, convex_hull):
|
||||||
|
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([
|
||||||
|
[-hull_offset, -hull_offset],
|
||||||
|
[-hull_offset, hull_offset],
|
||||||
|
[hull_offset, hull_offset],
|
||||||
|
[hull_offset, -hull_offset]
|
||||||
|
], numpy.float32))
|
||||||
|
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()
|
||||||
|
|
@ -268,6 +290,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||||
|
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||||
|
for extruder in extruders:
|
||||||
|
extruder.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||||
|
|
||||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
|
@ -275,9 +300,32 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||||
self._global_stack.containersChanged.connect(self._onChanged)
|
self._global_stack.containersChanged.connect(self._onChanged)
|
||||||
|
|
||||||
|
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||||
|
for extruder in extruders:
|
||||||
|
extruder.propertyChanged.connect(self._onSettingValueChanged)
|
||||||
|
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
## Returns true if node is a descendent or the same as the root node.
|
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
||||||
|
def _getSettingProperty(self, setting_key, prop = "value"):
|
||||||
|
per_mesh_stack = self._node.callDecoration("getStack")
|
||||||
|
if per_mesh_stack:
|
||||||
|
return per_mesh_stack.getProperty(setting_key, prop)
|
||||||
|
|
||||||
|
extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder")
|
||||||
|
if extruder_index == "-1":
|
||||||
|
# No limit_to_extruder
|
||||||
|
extruder_stack_id = self._node.callDecoration("getActiveExtruder")
|
||||||
|
if not extruder_stack_id:
|
||||||
|
# Decoration doesn't exist
|
||||||
|
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
|
||||||
|
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||||
|
return extruder_stack.getProperty(setting_key, prop)
|
||||||
|
else:
|
||||||
|
# Limit_to_extruder is set. The global stack handles this then
|
||||||
|
return self._global_stack.getProperty(setting_key, prop)
|
||||||
|
|
||||||
|
## Returns true if node is a descendant or the same as the root node.
|
||||||
def __isDescendant(self, root, node):
|
def __isDescendant(self, root, node):
|
||||||
if node is None:
|
if node is None:
|
||||||
return False
|
return False
|
||||||
|
|
@ -286,6 +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"]
|
|
||||||
|
## Settings that change the convex hull.
|
||||||
|
#
|
||||||
|
# If these settings change, the convex hull should be recalculated.
|
||||||
|
_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
|
|
||||||
if QCoreApplication.instance():
|
|
||||||
debug_mode = QCoreApplication.instance().getCommandLineOption("debug-mode", False)
|
|
||||||
|
|
||||||
Logger.log("c", "An uncaught exception has occurred!")
|
class CrashHandler:
|
||||||
for line in traceback.format_exception(exception_type, value, tb):
|
crash_url = "https://stats.ultimaker.com/api/cura"
|
||||||
for part in line.rstrip("\n").split("\n"):
|
|
||||||
Logger.log("c", part)
|
|
||||||
|
|
||||||
if not debug_mode and exception_type not in fatal_exception_types:
|
def __init__(self, exception_type, value, tb):
|
||||||
return
|
self.exception_type = exception_type
|
||||||
|
self.value = value
|
||||||
|
self.traceback = tb
|
||||||
|
self.dialog = QDialog()
|
||||||
|
|
||||||
application = QCoreApplication.instance()
|
# While we create the GUI, the information will be stored for sending afterwards
|
||||||
if not application:
|
self.data = dict()
|
||||||
sys.exit(1)
|
self.data["time_stamp"] = time.time()
|
||||||
|
|
||||||
dialog = QDialog()
|
Logger.log("c", "An uncaught exception has occurred!")
|
||||||
dialog.setWindowTitle(catalog.i18nc("@title:window", "Oops!"))
|
for line in traceback.format_exception(exception_type, value, tb):
|
||||||
|
for part in line.rstrip("\n").split("\n"):
|
||||||
|
Logger.log("c", part)
|
||||||
|
|
||||||
layout = QVBoxLayout(dialog)
|
if not CuraDebugMode and exception_type not in fatal_exception_types:
|
||||||
|
return
|
||||||
|
|
||||||
label = QLabel(dialog)
|
application = QCoreApplication.instance()
|
||||||
layout.addWidget(label)
|
if not application:
|
||||||
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>"))
|
sys.exit(1)
|
||||||
|
|
||||||
textarea = QTextEdit(dialog)
|
self._createDialog()
|
||||||
layout.addWidget(textarea)
|
|
||||||
|
|
||||||
try:
|
## Creates a modal dialog.
|
||||||
from UM.Application import Application
|
def _createDialog(self):
|
||||||
version = Application.getInstance().getVersion()
|
self.dialog.setMinimumWidth(640)
|
||||||
except:
|
self.dialog.setMinimumHeight(640)
|
||||||
version = "Unknown"
|
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report"))
|
||||||
|
|
||||||
trace = "".join(traceback.format_exception(exception_type, value, tb))
|
layout = QVBoxLayout(self.dialog)
|
||||||
|
|
||||||
crash_info = "Version: {0}\nPlatform: {1}\nQt: {2}\nPyQt: {3}\n\nException:\n{4}"
|
layout.addWidget(self._messageWidget())
|
||||||
crash_info = crash_info.format(version, platform.platform(), QT_VERSION_STR, PYQT_VERSION_STR, trace)
|
layout.addWidget(self._informationWidget())
|
||||||
|
layout.addWidget(self._exceptionInfoWidget())
|
||||||
|
layout.addWidget(self._logInfoWidget())
|
||||||
|
layout.addWidget(self._userDescriptionWidget())
|
||||||
|
layout.addWidget(self._buttonsWidget())
|
||||||
|
|
||||||
textarea.setText(crash_info)
|
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>
|
||||||
|
"""))
|
||||||
|
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.Close, dialog)
|
return label
|
||||||
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 _informationWidget(self):
|
||||||
sys.exit(1)
|
group = QGroupBox()
|
||||||
|
group.setTitle(catalog.i18nc("@title:groupbox", "System information"))
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
label = QLabel()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from UM.Application import Application
|
||||||
|
self.cura_version = Application.getInstance().getVersion()
|
||||||
|
except:
|
||||||
|
self.cura_version = catalog.i18nc("@label unknown version of Cura", "Unknown")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
group.setLayout(layout)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def _getOpenGLInfo(self):
|
||||||
|
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)
|
||||||
|
|
|
||||||
968
cura/CuraApplication.py
Normal file → Executable file
968
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
|
||||||
|
|
||||||
font = QFont() # Using system-default font here
|
# draw version text
|
||||||
font.setPointSize(20)
|
font = QFont() # Using system-default font here
|
||||||
|
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
|
||||||
|
|
@ -80,8 +78,7 @@ class Layer:
|
||||||
else:
|
else:
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -94,7 +91,7 @@ class Layer:
|
||||||
# Line types of the points we want to draw
|
# Line types of the points we want to draw
|
||||||
line_types = polygon.types[index_mask]
|
line_types = polygon.types[index_mask]
|
||||||
|
|
||||||
# Shift the z-axis according to previous implementation.
|
# Shift the z-axis according to previous implementation.
|
||||||
if make_mesh:
|
if make_mesh:
|
||||||
points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01
|
points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01
|
||||||
else:
|
else:
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -14,41 +17,54 @@ class LayerPolygon:
|
||||||
SupportInfillType = 7
|
SupportInfillType = 7
|
||||||
MoveCombingType = 8
|
MoveCombingType = 8
|
||||||
MoveRetractionType = 9
|
MoveRetractionType = 9
|
||||||
|
SupportInterfaceType = 10
|
||||||
|
__number_of_types = 11
|
||||||
|
|
||||||
|
__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)
|
||||||
|
|
||||||
__jump_map = numpy.logical_or( numpy.arange(10) == NoneType, numpy.arange(10) >= MoveCombingType )
|
## LayerPolygon, used in ProcessSlicedLayersJob
|
||||||
|
# \param extruder
|
||||||
def __init__(self, mesh, extruder, line_types, data, line_widths):
|
# \param line_types array with line_types
|
||||||
self._mesh = mesh
|
# \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
|
||||||
self._index_begin = 0
|
self._index_begin = 0
|
||||||
self._index_end = 0
|
self._index_end = 0
|
||||||
|
|
||||||
self._jump_mask = self.__jump_map[self._types]
|
self._jump_mask = self.__jump_map[self._types]
|
||||||
self._jump_count = numpy.sum(self._jump_mask)
|
self._jump_count = numpy.sum(self._jump_mask)
|
||||||
self._mesh_line_count = len(self._types)-self._jump_count
|
self._mesh_line_count = len(self._types) - self._jump_count
|
||||||
self._vertex_count = self._mesh_line_count + numpy.sum( self._types[1:] == self._types[:-1])
|
self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
|
||||||
|
|
||||||
# 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._colors = self.__color_map[self._types]
|
self._color_map = LayerPolygon.getColorMap()
|
||||||
self._color_map = self.__color_map
|
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
|
||||||
# Should be generated in better way, not hardcoded.
|
# Should be generated in better way, not hardcoded.
|
||||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool)
|
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
|
||||||
|
|
||||||
self._build_cache_line_mesh_mask = None
|
self._build_cache_line_mesh_mask = None
|
||||||
self._build_cache_needed_points = None
|
self._build_cache_needed_points = None
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -58,13 +74,24 @@ class LayerPolygon:
|
||||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||||
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points )
|
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points )
|
||||||
|
|
||||||
self._vertex_begin = 0
|
self._vertex_begin = 0
|
||||||
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
||||||
|
|
||||||
|
|
||||||
def build(self, vertex_offset, index_offset, vertices, colors, indices):
|
## Set all the arrays provided by the function caller, representing the LayerPolygon
|
||||||
if (self._build_cache_line_mesh_mask is None) or (self._build_cache_needed_points is None ):
|
# The arrays are either by vertex or by indices.
|
||||||
|
#
|
||||||
|
# \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
|
||||||
|
|
@ -81,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
|
||||||
|
|
@ -133,6 +172,14 @@ class LayerPolygon:
|
||||||
@property
|
@property
|
||||||
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):
|
||||||
|
|
@ -171,29 +218,25 @@ class LayerPolygon:
|
||||||
|
|
||||||
return normals
|
return normals
|
||||||
|
|
||||||
__color_mapping = {
|
__color_map = None # type: numpy.ndarray[Any]
|
||||||
NoneType: Color(1.0, 1.0, 1.0, 1.0),
|
|
||||||
Inset0Type: Color(1.0, 0.0, 0.0, 1.0),
|
|
||||||
InsetXType: Color(0.0, 1.0, 0.0, 1.0),
|
|
||||||
SkinType: Color(1.0, 1.0, 0.0, 1.0),
|
|
||||||
SupportType: Color(0.0, 1.0, 1.0, 1.0),
|
|
||||||
SkirtType: Color(0.0, 1.0, 1.0, 1.0),
|
|
||||||
InfillType: Color(1.0, 0.74, 0.0, 1.0),
|
|
||||||
SupportInfillType: Color(0.0, 1.0, 1.0, 1.0),
|
|
||||||
MoveCombingType: Color(0.0, 0.0, 1.0, 1.0),
|
|
||||||
MoveRetractionType: Color(0.5, 0.5, 1.0, 1.0),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Should be generated in better way, not hardcoded.
|
## Gets the instance of the VersionUpgradeManager, or creates one.
|
||||||
__color_map = numpy.array([
|
@classmethod
|
||||||
[1.0, 1.0, 1.0, 1.0],
|
def getColorMap(cls):
|
||||||
[1.0, 0.0, 0.0, 1.0],
|
if cls.__color_map is None:
|
||||||
[0.0, 1.0, 0.0, 1.0],
|
theme = Application.getInstance().getTheme()
|
||||||
[1.0, 1.0, 0.0, 1.0],
|
cls.__color_map = numpy.array([
|
||||||
[0.0, 1.0, 1.0, 1.0],
|
theme.getColor("layerview_none").getRgbF(), # NoneType
|
||||||
[0.0, 1.0, 1.0, 1.0],
|
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
|
||||||
[1.0, 0.74, 0.0, 1.0],
|
theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
|
||||||
[0.0, 1.0, 1.0, 1.0],
|
theme.getColor("layerview_skin").getRgbF(), # SkinType
|
||||||
[0.0, 0.0, 1.0, 1.0],
|
theme.getColor("layerview_support").getRgbF(), # SupportType
|
||||||
[0.5, 0.5, 1.0, 1.0]
|
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
|
||||||
|
|
|
||||||
122
cura/PlatformPhysics.py
Normal file → Executable file
122
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
|
||||||
|
|
||||||
|
|
@ -15,6 +15,9 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
|
||||||
from . import PlatformPhysicsOperation
|
from . import PlatformPhysicsOperation
|
||||||
from . import ZOffsetDecorator
|
from . import ZOffsetDecorator
|
||||||
|
|
||||||
|
import random # used for list shuffling
|
||||||
|
|
||||||
|
|
||||||
class PlatformPhysics:
|
class PlatformPhysics:
|
||||||
def __init__(self, controller, volume):
|
def __init__(self, controller, volume):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -29,42 +32,43 @@ class PlatformPhysics:
|
||||||
self._change_timer.setInterval(100)
|
self._change_timer.setInterval(100)
|
||||||
self._change_timer.setSingleShot(True)
|
self._change_timer.setSingleShot(True)
|
||||||
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
||||||
|
self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
|
||||||
|
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
|
||||||
|
|
||||||
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
|
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
|
||||||
|
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
root = self._controller.getScene().getRoot()
|
root = self._controller.getScene().getRoot()
|
||||||
|
|
||||||
for node in BreadthFirstIterator(root):
|
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
|
||||||
|
# same direction.
|
||||||
|
transformed_nodes = []
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
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)
|
||||||
|
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:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Move it downwards if bottom is above platform
|
# Move it downwards if bottom is above platform
|
||||||
move_vector = Vector()
|
move_vector = Vector()
|
||||||
if not (node.getParent() and node.getParent().callDecoration("isGroup")): #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)
|
||||||
|
|
||||||
|
|
@ -84,54 +88,66 @@ class PlatformPhysics:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ignore collisions within a group
|
# Ignore collisions within a group
|
||||||
if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
|
if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Ignore nodes that do not have the right properties set.
|
# Ignore nodes that do not have the right properties set.
|
||||||
if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
|
if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
|
if other_node in transformed_nodes:
|
||||||
head_hull = node.callDecoration("getConvexHullHead")
|
continue # Other node is already moving, wait for next pass.
|
||||||
if head_hull:
|
|
||||||
overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHullHead"))
|
overlap = (0, 0) # Start loop with no overlap
|
||||||
if not overlap:
|
current_overlap_checks = 0
|
||||||
other_head_hull = other_node.callDecoration("getConvexHullHead")
|
# Continue to check the overlap until we no longer find one.
|
||||||
if other_head_hull:
|
while overlap and current_overlap_checks < self._max_overlap_checks:
|
||||||
overlap = node.callDecoration("getConvexHullHead").intersectsPolygon(other_head_hull)
|
current_overlap_checks += 1
|
||||||
else:
|
head_hull = node.callDecoration("getConvexHullHead")
|
||||||
own_convex_hull = node.callDecoration("getConvexHull")
|
if head_hull: # One at a time intersection.
|
||||||
other_convex_hull = other_node.callDecoration("getConvexHull")
|
overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
|
||||||
if own_convex_hull and other_convex_hull:
|
if not overlap:
|
||||||
overlap = own_convex_hull.intersectsPolygon(other_convex_hull)
|
other_head_hull = other_node.callDecoration("getConvexHullHead")
|
||||||
|
if other_head_hull:
|
||||||
|
overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
|
||||||
|
if overlap:
|
||||||
|
# Moving ensured that overlap was still there. Try anew!
|
||||||
|
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||||
|
z=move_vector.z + overlap[1] * self._move_factor)
|
||||||
|
else:
|
||||||
|
# Moving ensured that overlap was still there. Try anew!
|
||||||
|
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||||
|
z=move_vector.z + overlap[1] * self._move_factor)
|
||||||
else:
|
else:
|
||||||
# This can happen in some cases if the object is not yet done with being loaded.
|
own_convex_hull = node.callDecoration("getConvexHull")
|
||||||
# Simply waiting for the next tick seems to resolve this correctly.
|
other_convex_hull = other_node.callDecoration("getConvexHull")
|
||||||
overlap = None
|
if own_convex_hull and other_convex_hull:
|
||||||
|
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
|
||||||
if overlap is None:
|
if overlap: # Moving ensured that overlap was still there. Try anew!
|
||||||
continue
|
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||||
move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1)
|
z=move_vector.z + overlap[1] * self._move_factor)
|
||||||
convex_hull = node.callDecoration("getConvexHull")
|
else:
|
||||||
if convex_hull:
|
# This can happen in some cases if the object is not yet done with being loaded.
|
||||||
if not convex_hull.isValid():
|
# Simply waiting for the next tick seems to resolve this correctly.
|
||||||
return
|
overlap = None
|
||||||
# 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)
|
||||||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
|
# After moving, we have to evaluate the boundary checks for nodes
|
||||||
|
build_volume = Application.getInstance().getBuildVolume()
|
||||||
|
build_volume.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
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:
|
||||||
|
|
@ -144,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,31 +1,32 @@
|
||||||
# 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
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
## A specialised operation designed specifically to modify the previous operation.
|
## A specialised operation designed specifically to modify the previous operation.
|
||||||
class PlatformPhysicsOperation(Operation):
|
class PlatformPhysicsOperation(Operation):
|
||||||
def __init__(self, node, translation):
|
def __init__(self, node, translation):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._node = node
|
self._node = node
|
||||||
self._old_position = node.getPosition()
|
self._old_transformation = node.getLocalTransformation()
|
||||||
self._new_position = node.getPosition() + translation
|
self._translation = translation
|
||||||
self._always_merge = True
|
self._always_merge = True
|
||||||
|
|
||||||
def undo(self):
|
def undo(self):
|
||||||
self._node.setPosition(self._old_position)
|
self._node.setTransformation(self._old_transformation)
|
||||||
|
|
||||||
def redo(self):
|
def redo(self):
|
||||||
self._node.setPosition(self._new_position)
|
self._node.translate(self._translation, SceneNode.TransformSpace.World)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "PlatformPhysicsOperation(new_position = {0})".format(self._new_position)
|
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
||||||
|
|
|
||||||
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,40 +133,124 @@ 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
|
self._job_name = name
|
||||||
name = os.path.basename(name)
|
self.jobNameChanged.emit()
|
||||||
|
|
||||||
# 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.jobNameChanged.emit()
|
|
||||||
|
|
||||||
jobNameChanged = pyqtSignal()
|
jobNameChanged = pyqtSignal()
|
||||||
|
|
||||||
|
|
@ -118,18 +258,64 @@ 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 = ""
|
||||||
if Preferences.getInstance().getValue("cura/jobname_prefix"):
|
self.jobNameChanged.emit()
|
||||||
return self._abbr_machine + "_" + base_name
|
return
|
||||||
|
|
||||||
|
base_name = self._stripAccents(self._base_name)
|
||||||
|
self._setAbbreviatedMachineName()
|
||||||
|
if self._pre_sliced:
|
||||||
|
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:
|
||||||
|
self._job_name = self._abbr_machine + "_" + base_name
|
||||||
else:
|
else:
|
||||||
return base_name
|
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
|
||||||
def _setAbbreviatedMachineName(self):
|
def _setAbbreviatedMachineName(self):
|
||||||
global_stack_name = Application.getInstance().getGlobalContainerStack().getName()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_container_stack:
|
||||||
|
self._abbr_machine = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
global_stack_name = global_container_stack.getName()
|
||||||
split_name = global_stack_name.split(" ")
|
split_name = global_stack_name.split(" ")
|
||||||
abbr_machine = ""
|
abbr_machine = ""
|
||||||
for word in split_name:
|
for word in split_name:
|
||||||
|
|
@ -138,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,12 +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
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
|
|
@ -24,6 +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 = 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
|
||||||
|
|
@ -43,8 +48,29 @@ 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)
|
||||||
|
|
||||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
self._printer_state = ""
|
||||||
|
self._printer_type = "unknown"
|
||||||
|
|
||||||
|
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):
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
## Signals
|
## Signals
|
||||||
|
|
@ -91,10 +117,86 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
acceptsCommandsChanged = pyqtSignal()
|
acceptsCommandsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
printerStateChanged = 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)
|
||||||
|
def printerType(self):
|
||||||
|
return self._printer_type
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=printerStateChanged)
|
||||||
|
def printerState(self):
|
||||||
|
return self._printer_state
|
||||||
|
|
||||||
@pyqtProperty(str, notify = jobStateChanged)
|
@pyqtProperty(str, notify = jobStateChanged)
|
||||||
def jobState(self):
|
def jobState(self):
|
||||||
return self._job_state
|
return self._job_state
|
||||||
|
|
||||||
|
def _updatePrinterType(self, printer_type):
|
||||||
|
if self._printer_type != printer_type:
|
||||||
|
self._printer_type = printer_type
|
||||||
|
self.printerTypeChanged.emit()
|
||||||
|
|
||||||
|
def _updatePrinterState(self, printer_state):
|
||||||
|
if self._printer_state != printer_state:
|
||||||
|
self._printer_state = printer_state
|
||||||
|
self.printerStateChanged.emit()
|
||||||
|
|
||||||
def _updateJobState(self, job_state):
|
def _updateJobState(self, job_state):
|
||||||
if self._job_state != job_state:
|
if self._job_state != job_state:
|
||||||
self._job_state = job_state
|
self._job_state = job_state
|
||||||
|
|
@ -107,6 +209,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
def _setJobState(self, job_state):
|
def _setJobState(self, job_state):
|
||||||
Logger.log("w", "_setJobState is not implemented by this output device")
|
Logger.log("w", "_setJobState is not implemented by this output device")
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def startCamera(self):
|
||||||
|
self._camera_active = True
|
||||||
|
self._startCamera()
|
||||||
|
|
||||||
|
def _startCamera(self):
|
||||||
|
Logger.log("w", "_startCamera is not implemented by this output device")
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def stopCamera(self):
|
||||||
|
self._camera_active = False
|
||||||
|
self._stopCamera()
|
||||||
|
|
||||||
|
def _stopCamera(self):
|
||||||
|
Logger.log("w", "_stopCamera is not implemented by this output device")
|
||||||
|
|
||||||
@pyqtProperty(str, notify = jobNameChanged)
|
@pyqtProperty(str, notify = jobNameChanged)
|
||||||
def jobName(self):
|
def jobName(self):
|
||||||
return self._job_name
|
return self._job_name
|
||||||
|
|
@ -116,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
|
||||||
|
|
@ -150,8 +279,33 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def setTargetBedTemperature(self, temperature):
|
def setTargetBedTemperature(self, temperature):
|
||||||
self._setTargetBedTemperature(temperature)
|
self._setTargetBedTemperature(temperature)
|
||||||
self._target_bed_temperature = temperature
|
if self._target_bed_temperature != temperature:
|
||||||
self.targetBedTemperatureChanged.emit()
|
self._target_bed_temperature = temperature
|
||||||
|
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.
|
||||||
|
|
@ -208,12 +362,29 @@ 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.
|
||||||
def _setBedTemperature(self, temperature):
|
def _setBedTemperature(self, temperature):
|
||||||
self._bed_temperature = temperature
|
if self._bed_temperature != temperature:
|
||||||
self.bedTemperatureChanged.emit()
|
self._bed_temperature = temperature
|
||||||
|
self.bedTemperatureChanged.emit()
|
||||||
|
|
||||||
## Get the target bed temperature if connected printer (if any)
|
## Get the target bed temperature if connected printer (if any)
|
||||||
@pyqtProperty(int, notify = targetBedTemperatureChanged)
|
@pyqtProperty(int, notify = targetBedTemperatureChanged)
|
||||||
|
|
@ -228,8 +399,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
@pyqtSlot(int, int)
|
@pyqtSlot(int, int)
|
||||||
def setTargetHotendTemperature(self, index, temperature):
|
def setTargetHotendTemperature(self, index, temperature):
|
||||||
self._setTargetHotendTemperature(index, temperature)
|
self._setTargetHotendTemperature(index, temperature)
|
||||||
self._target_hotend_temperatures[index] = temperature
|
|
||||||
self.targetHotendTemperaturesChanged.emit()
|
if self._target_hotend_temperatures[index] != temperature:
|
||||||
|
self._target_hotend_temperatures[index] = temperature
|
||||||
|
self.targetHotendTemperaturesChanged.emit()
|
||||||
|
|
||||||
## Implementation function of setTargetHotendTemperature.
|
## Implementation function of setTargetHotendTemperature.
|
||||||
# /param index Index of the hotend to set the temperature of
|
# /param index Index of the hotend to set the temperature of
|
||||||
|
|
@ -251,13 +424,51 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
# /param index Index of the hotend
|
# /param index Index of the hotend
|
||||||
# /param temperature temperature of the hotend (in deg C)
|
# /param temperature temperature of the hotend (in deg C)
|
||||||
def _setHotendTemperature(self, index, temperature):
|
def _setHotendTemperature(self, index, temperature):
|
||||||
self._hotend_temperatures[index] = temperature
|
if self._hotend_temperatures[index] != temperature:
|
||||||
self.hotendTemperaturesChanged.emit()
|
self._hotend_temperatures[index] = temperature
|
||||||
|
self.hotendTemperaturesChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||||
def materialIds(self):
|
def materialIds(self):
|
||||||
return self._material_ids
|
return self._material_ids
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||||
|
def materialNames(self):
|
||||||
|
result = []
|
||||||
|
for material_id in self._material_ids:
|
||||||
|
if material_id is None:
|
||||||
|
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
|
||||||
|
if containers:
|
||||||
|
result.append(containers[0].getName())
|
||||||
|
else:
|
||||||
|
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
|
||||||
|
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
|
||||||
|
|
@ -267,7 +478,6 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
self._material_ids[index] = material_id
|
self._material_ids[index] = material_id
|
||||||
self.materialIdChanged.emit(index, material_id)
|
self.materialIdChanged.emit(index, material_id)
|
||||||
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = hotendIdChanged)
|
@pyqtProperty("QVariantList", notify = hotendIdChanged)
|
||||||
def hotendIds(self):
|
def hotendIds(self):
|
||||||
return self._hotend_ids
|
return self._hotend_ids
|
||||||
|
|
@ -276,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
|
||||||
|
|
@ -302,8 +516,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
## Set the connection state of this output device.
|
## Set the connection state of this output device.
|
||||||
# /param connection_state ConnectionState enum.
|
# /param connection_state ConnectionState enum.
|
||||||
def setConnectionState(self, connection_state):
|
def setConnectionState(self, connection_state):
|
||||||
self._connection_state = connection_state
|
if self._connection_state != connection_state:
|
||||||
self.connectionStateChanged.emit(self._id)
|
self._connection_state = connection_state
|
||||||
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionTextChanged)
|
@pyqtProperty(str, notify = connectionTextChanged)
|
||||||
def connectionText(self):
|
def connectionText(self):
|
||||||
|
|
@ -351,6 +566,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
if self._head_z != z:
|
if self._head_z != z:
|
||||||
self._head_z = z
|
self._head_z = z
|
||||||
position_changed = True
|
position_changed = True
|
||||||
|
|
||||||
if position_changed:
|
if position_changed:
|
||||||
self.headPositionChanged.emit()
|
self.headPositionChanged.emit()
|
||||||
|
|
||||||
|
|
@ -396,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
|
||||||
|
|
||||||
|
|
@ -18,8 +18,8 @@ class ProfileWriter(PluginObject):
|
||||||
# The profile writer may write its own file format to the specified file.
|
# The profile writer may write its own file format to the specified file.
|
||||||
#
|
#
|
||||||
# \param path \type{string} The file to output to.
|
# \param path \type{string} The file to output to.
|
||||||
# \param profile \type{Profile} The profile to write to the file.
|
# \param profiles \type{Profile} or \type{List} The profile(s) to write to the file.
|
||||||
# \return \code True \endcode if the writing was successful, or \code
|
# \return \code True \endcode if the writing was successful, or \code
|
||||||
# False \endcode if it wasn't.
|
# False \endcode if it wasn't.
|
||||||
def write(self, path, node):
|
def write(self, path, profiles):
|
||||||
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")
|
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")
|
||||||
|
|
|
||||||
325
cura/QualityManager.py
Normal file
325
cura/QualityManager.py
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
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:
|
||||||
|
|
||||||
|
## Get the singleton instance for this class.
|
||||||
|
@classmethod
|
||||||
|
def getInstance(cls) -> "QualityManager":
|
||||||
|
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||||
|
if not QualityManager.__instance:
|
||||||
|
QualityManager.__instance = cls()
|
||||||
|
return QualityManager.__instance
|
||||||
|
|
||||||
|
__instance = None # type: "QualityManager"
|
||||||
|
|
||||||
|
## Find a quality by name for a specific machine definition and materials.
|
||||||
|
#
|
||||||
|
# \param quality_name
|
||||||
|
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
|
||||||
|
# specified then the currently selected machine definition is used.
|
||||||
|
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||||
|
# the current set of selected materials is used.
|
||||||
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
|
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[InstanceContainer]:
|
||||||
|
criteria = {"type": "quality", "name": quality_name}
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||||
|
|
||||||
|
# Fall back to using generic materials and qualities if nothing could be found.
|
||||||
|
if not result and material_containers and len(material_containers) == 1:
|
||||||
|
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
## Find a quality changes container by name.
|
||||||
|
#
|
||||||
|
# \param quality_changes_name \type{str} the name of the quality changes container.
|
||||||
|
# \param machine_definition (Optional) \type{DefinitionContainer} If nothing is
|
||||||
|
# specified then the currently selected machine definition is used..
|
||||||
|
# \return the matching quality changes containers \type{List[InstanceContainer]}
|
||||||
|
def findQualityChangesByName(self, quality_changes_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None):
|
||||||
|
if not machine_definition:
|
||||||
|
global_stack = Application.getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
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
|
||||||
|
|
||||||
|
## Fetch the list of available quality types for this combination of machine definition and materials.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer}
|
||||||
|
# \param material_containers \type{List[InstanceContainer]}
|
||||||
|
# \return \type{List[str]}
|
||||||
|
def findAllQualityTypesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[str]:
|
||||||
|
# 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])
|
||||||
|
common_quality_types = set(quality_type_dict.keys())
|
||||||
|
for material_container in material_containers[1:]:
|
||||||
|
next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
|
||||||
|
common_quality_types.intersection_update(set(next_quality_type_dict.keys()))
|
||||||
|
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
|
# \param material \type{InstanceContainer} the material.
|
||||||
|
# \return \type{Dict[str, InstanceContainer]} the dict of suitable quality type names mapping to qualities.
|
||||||
|
def __fetchQualityTypeDictForMaterial(self, machine_definition: "DefinitionContainerInterface", material: InstanceContainer) -> Dict[str, InstanceContainer]:
|
||||||
|
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
|
||||||
|
quality_type_dict = {}
|
||||||
|
for quality in qualities:
|
||||||
|
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
|
||||||
|
return quality_type_dict
|
||||||
|
|
||||||
|
## Find a quality container by quality type.
|
||||||
|
#
|
||||||
|
# \param quality_type \type{str} the name of the quality type to search for.
|
||||||
|
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
|
||||||
|
# specified then the currently selected machine definition is used.
|
||||||
|
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||||
|
# the current set of selected materials is used.
|
||||||
|
# \return the matching quality container \type{InstanceContainer}
|
||||||
|
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> InstanceContainer:
|
||||||
|
criteria = kwargs
|
||||||
|
criteria["type"] = "quality"
|
||||||
|
if quality_type:
|
||||||
|
criteria["quality_type"] = quality_type
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||||
|
# Fall back to using generic materials and qualities if nothing could be found.
|
||||||
|
if not result and material_containers and len(material_containers) == 1:
|
||||||
|
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||||
|
if basic_materials:
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
## Find all suitable qualities for a combination of machine and material.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
|
# \param material_container \type{InstanceContainer} the material.
|
||||||
|
# \return \type{List[InstanceContainer]} the list of suitable qualities.
|
||||||
|
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
|
||||||
|
criteria = {"type": "quality"}
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
||||||
|
if not result:
|
||||||
|
basic_materials = self._getBasicMaterials(material_container)
|
||||||
|
if basic_materials:
|
||||||
|
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Find all quality changes for a machine.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||||
|
# \return \type{List[InstanceContainer]} the list of quality changes
|
||||||
|
def findAllQualityChangesForMachine(self, machine_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
|
||||||
|
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
|
definition_id = machine_definition.getId()
|
||||||
|
else:
|
||||||
|
definition_id = "fdmprinter"
|
||||||
|
|
||||||
|
filter_dict = { "type": "quality_changes", "definition": definition_id }
|
||||||
|
quality_changes_list = ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# then only one of then is returned (at random).
|
||||||
|
#
|
||||||
|
# \param global_container_stack \type{GlobalStack} the global machine definition
|
||||||
|
# \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 come from the first extruder in the given list of extruders.
|
||||||
|
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack: "GlobalStack", extruder_stacks: List["ExtruderStack"]) -> List[InstanceContainer]:
|
||||||
|
global_machine_definition = global_container_stack.getBottom()
|
||||||
|
|
||||||
|
machine_manager = Application.getInstance().getMachineManager()
|
||||||
|
active_stack_id = machine_manager.activeStackId
|
||||||
|
|
||||||
|
materials = []
|
||||||
|
|
||||||
|
for stack in extruder_stacks:
|
||||||
|
if stack.getId() == active_stack_id and machine_manager.newMaterial:
|
||||||
|
materials.append(machine_manager.newMaterial)
|
||||||
|
else:
|
||||||
|
materials.append(stack.material)
|
||||||
|
|
||||||
|
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
|
||||||
|
|
||||||
|
# Map the list of quality_types to InstanceContainers
|
||||||
|
qualities = self.findAllQualitiesForMachineMaterial(global_machine_definition, materials[0])
|
||||||
|
quality_type_dict = {}
|
||||||
|
for quality in qualities:
|
||||||
|
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
|
||||||
|
|
||||||
|
return [quality_type_dict[quality_type] for quality_type in quality_types]
|
||||||
|
|
||||||
|
## Fetch more basic versions of a material.
|
||||||
|
#
|
||||||
|
# This tries to find a generic or basic version of the given 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.
|
||||||
|
def _getBasicMaterials(self, material_container: InstanceContainer):
|
||||||
|
base_material = material_container.getMetaDataEntry("material")
|
||||||
|
material_container_definition = material_container.getDefinition()
|
||||||
|
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
|
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
||||||
|
else:
|
||||||
|
definition_id = "fdmprinter"
|
||||||
|
if base_material:
|
||||||
|
# There is a basic material specified
|
||||||
|
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
|
containers = [basic_material for basic_material in containers if
|
||||||
|
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
|
||||||
|
"variant")]
|
||||||
|
return containers
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _getFilteredContainers(self, **kwargs):
|
||||||
|
return self._getFilteredContainersForStack(None, None, **kwargs)
|
||||||
|
|
||||||
|
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
|
||||||
|
# Fill in any default values.
|
||||||
|
if machine_definition is None:
|
||||||
|
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
|
if quality_definition_id is not None:
|
||||||
|
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
||||||
|
|
||||||
|
# for convenience
|
||||||
|
if material_containers is None:
|
||||||
|
material_containers = []
|
||||||
|
|
||||||
|
if not material_containers:
|
||||||
|
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||||
|
if active_stacks:
|
||||||
|
material_containers = [stack.material for stack in active_stacks]
|
||||||
|
|
||||||
|
criteria = kwargs
|
||||||
|
filter_by_material = False
|
||||||
|
|
||||||
|
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)
|
||||||
|
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||||
|
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
||||||
|
criteria["definition"] = definition_id
|
||||||
|
|
||||||
|
filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
|
||||||
|
# only fall back to "fdmprinter" when there is no container for this machine
|
||||||
|
elif not found_containers_with_machine_definition:
|
||||||
|
criteria["definition"] = "fdmprinter"
|
||||||
|
|
||||||
|
# Stick the material IDs in a set
|
||||||
|
material_ids = set()
|
||||||
|
|
||||||
|
for material_instance in material_containers:
|
||||||
|
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())
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for container in containers:
|
||||||
|
# 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 "global_quality" not in kwargs:
|
||||||
|
continue
|
||||||
|
result.append(container)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Get the parent machine definition of a machine definition.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||||
|
# an extruder definition.
|
||||||
|
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
|
||||||
|
# definition doesn't have a parent then it is simply returned.
|
||||||
|
def getParentMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||||
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||||
|
if machine_entry is None:
|
||||||
|
# We have a normal (whole) machine defintion
|
||||||
|
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||||
|
if quality_definition is not None:
|
||||||
|
parent_machine_definition = container_registry.findDefinitionContainers(id=quality_definition)[0]
|
||||||
|
return self.getParentMachineDefinition(parent_machine_definition)
|
||||||
|
else:
|
||||||
|
return machine_definition
|
||||||
|
else:
|
||||||
|
# This looks like an extruder. Find the rest of the machine.
|
||||||
|
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
||||||
|
parent_machine = self.getParentMachineDefinition(whole_machine)
|
||||||
|
if whole_machine is parent_machine:
|
||||||
|
# This extruder already belongs to a 'parent' machine def.
|
||||||
|
return machine_definition
|
||||||
|
else:
|
||||||
|
# Look up the corresponding extruder definition in the parent machine definition.
|
||||||
|
extruder_position = machine_definition.getMetaDataEntry("position")
|
||||||
|
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
|
||||||
|
return container_registry.findDefinitionContainers(id=parent_extruder_id)[0]
|
||||||
|
|
||||||
|
## Get the whole/global machine definition from an extruder definition.
|
||||||
|
#
|
||||||
|
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||||
|
# an extruder definition.
|
||||||
|
# \return \type{DefinitionContainerInterface}
|
||||||
|
def getWholeMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||||
|
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||||
|
if machine_entry is None:
|
||||||
|
# This already is a 'global' machine definition.
|
||||||
|
return machine_definition
|
||||||
|
else:
|
||||||
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
||||||
|
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
|
||||||
|
|
@ -32,14 +32,27 @@ class SetParentOperation(Operation.Operation):
|
||||||
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
|
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
|
||||||
def _set_parent(self, new_parent):
|
def _set_parent(self, new_parent):
|
||||||
if new_parent:
|
if new_parent:
|
||||||
self._node.setPosition(self._node.getWorldPosition() - new_parent.getWorldPosition())
|
|
||||||
current_parent = self._node.getParent()
|
current_parent = self._node.getParent()
|
||||||
if current_parent:
|
if current_parent:
|
||||||
self._node.scale(current_parent.getScale() / new_parent.getScale())
|
# Special casing for groups that have been removed.
|
||||||
self._node.rotate(current_parent.getOrientation())
|
# In that case we want to put them back where they belong before checking the depth difference.
|
||||||
else:
|
# If we don't, we always get 0.
|
||||||
self._node.scale(Vector(1, 1, 1) / new_parent.getScale())
|
old_parent = new_parent.callDecoration("getOldParent")
|
||||||
self._node.rotate(new_parent.getOrientation().getInverse())
|
if old_parent:
|
||||||
|
new_parent.callDecoration("getNode").setParent(old_parent)
|
||||||
|
|
||||||
|
# Based on the depth difference, we need to do something different.
|
||||||
|
depth_difference = current_parent.getDepth() - new_parent.getDepth()
|
||||||
|
child_transformation = self._node.getLocalTransformation()
|
||||||
|
if depth_difference > 0:
|
||||||
|
parent_transformation = current_parent.getLocalTransformation()
|
||||||
|
# A node in the chain was removed, so we need to squash the parent info into all the nodes, so positions remain the same.
|
||||||
|
self._node.setTransformation(parent_transformation.multiply(child_transformation))
|
||||||
|
else:
|
||||||
|
# A node is inserted into the chain, so use the inverse of the parent to set the transformation of it's children.
|
||||||
|
parent_transformation = new_parent.getLocalTransformation()
|
||||||
|
result = parent_transformation.getInverse().multiply(child_transformation, copy = True)
|
||||||
|
self._node.setTransformation(result)
|
||||||
|
|
||||||
self._node.setParent(new_parent)
|
self._node.setParent(new_parent)
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
|
@ -31,7 +31,7 @@ class ContainerSettingsModel(ListModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
self.clear()
|
items = []
|
||||||
|
|
||||||
if len(self._container_ids) == 0:
|
if len(self._container_ids) == 0:
|
||||||
return
|
return
|
||||||
|
|
@ -64,14 +64,15 @@ class ContainerSettingsModel(ListModel):
|
||||||
else:
|
else:
|
||||||
values.append("")
|
values.append("")
|
||||||
|
|
||||||
self.appendItem({
|
items.append({
|
||||||
"key": key,
|
"key": key,
|
||||||
"values": values,
|
"values": values,
|
||||||
"label": definition.label,
|
"label": definition.label,
|
||||||
"unit": definition.unit,
|
"unit": definition.unit,
|
||||||
"category": category.label
|
"category": category.label
|
||||||
})
|
})
|
||||||
self.sort(lambda k: (k["category"], k["key"]))
|
items.sort(key = lambda k: (k["category"], k["key"]))
|
||||||
|
self.setItems(items)
|
||||||
|
|
||||||
## Set the ids of the containers which have the settings this model should list.
|
## Set the ids of the containers which have the settings this model should list.
|
||||||
# Also makes sure the model updates when the containers have property changes
|
# Also makes sure the model updates when the containers have property changes
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -13,8 +18,16 @@ from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
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 . import ExtruderStack
|
||||||
|
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")
|
||||||
|
|
@ -23,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
|
||||||
|
|
@ -58,12 +94,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
## Exports an profile to a file
|
## Exports an profile to a file
|
||||||
#
|
#
|
||||||
# \param instance_id \type{str} the ID of the profile to export.
|
# \param instance_ids \type{list} the IDs of the profiles to export.
|
||||||
# \param file_name \type{str} the full path and filename to export to.
|
# \param file_name \type{str} the full path and filename to export to.
|
||||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||||
def exportProfile(self, instance_id, file_name, file_type):
|
def exportProfile(self, instance_ids, file_name, file_type):
|
||||||
Logger.log('d', 'exportProfile instance_id: '+str(instance_id))
|
|
||||||
|
|
||||||
# Parse the fileType to deduce what plugin can save the file format.
|
# Parse the fileType to deduce what plugin can save the file format.
|
||||||
# fileType has the format "<description> (*.<extension>)"
|
# fileType has the format "<description> (*.<extension>)"
|
||||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||||
|
|
@ -79,30 +113,50 @@ 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 = []
|
||||||
|
extruder_positions = []
|
||||||
|
for instance_id in instance_ids:
|
||||||
|
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
|
||||||
|
if containers:
|
||||||
|
found_containers.append(containers[0])
|
||||||
|
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
|
# Determine the position of the extruder of this container
|
||||||
if not containers:
|
extruder_id = containers[0].getMetaDataEntry("extruder", "")
|
||||||
return
|
if extruder_id == "":
|
||||||
container = containers[0]
|
# Global stack
|
||||||
|
extruder_positions.append(-1)
|
||||||
|
else:
|
||||||
|
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id)
|
||||||
|
if extruder_containers:
|
||||||
|
extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0)))
|
||||||
|
else:
|
||||||
|
extruder_positions.append(0)
|
||||||
|
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
|
||||||
|
found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]
|
||||||
|
|
||||||
profile_writer = self._findProfileWriter(extension, description)
|
profile_writer = self._findProfileWriter(extension, description)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success = profile_writer.write(file_name, container)
|
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
|
||||||
|
|
@ -126,52 +180,156 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
||||||
# containing a message for the user
|
# containing a message for the user
|
||||||
def importProfile(self, file_name):
|
def importProfile(self, 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]
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_container_stack:
|
||||||
|
return
|
||||||
|
|
||||||
|
machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
|
||||||
|
machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position"))
|
||||||
|
|
||||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||||
|
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||||
|
continue
|
||||||
|
|
||||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||||
try:
|
try:
|
||||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||||
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", file_name, 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)
|
||||||
|
|
||||||
|
# Ensure it is always a list of profiles
|
||||||
if type(profile_or_list) is not list:
|
if type(profile_or_list) is not list:
|
||||||
profile = profile_or_list
|
profile_or_list = [profile_or_list]
|
||||||
self._configureProfile(profile, name_seed)
|
|
||||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
|
|
||||||
else:
|
|
||||||
for profile in profile_or_list:
|
|
||||||
self._configureProfile(profile, name_seed)
|
|
||||||
|
|
||||||
if len(profile_or_list) == 1:
|
if len(profile_or_list) == 1:
|
||||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
# If there is only 1 stack file it means we're loading a legacy (pre-3.1) .curaprofile.
|
||||||
else:
|
# In that case we find the per-extruder settings and put those in a new quality_changes container
|
||||||
profile_names = ", ".join([profile.getName() for profile in profile_or_list])
|
# so that it is compatible with the new stack setup.
|
||||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profiles {0}", profile_names) }
|
profile = profile_or_list[0]
|
||||||
|
extruder_stack_quality_changes_container = ContainerManager.getInstance().duplicateContainerInstance(profile)
|
||||||
|
extruder_stack_quality_changes_container.addMetaDataEntry("extruder", "fdmextruder")
|
||||||
|
|
||||||
#If it hasn't returned by now, none of the plugins loaded the profile successfully.
|
for quality_changes_setting_key in extruder_stack_quality_changes_container.getAllKeys():
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type.", file_name)}
|
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:
|
||||||
|
extruder_stack_quality_changes_container.removeInstance(quality_changes_setting_key, postpone_emit = True)
|
||||||
|
|
||||||
def _configureProfile(self, profile, name_seed):
|
# We add the new container to the profile list so things like extruder positions are taken care of
|
||||||
|
# in the next code segment.
|
||||||
|
profile_or_list.append(extruder_stack_quality_changes_container)
|
||||||
|
|
||||||
|
# Import all profiles
|
||||||
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
|
if profile_index == 0:
|
||||||
|
# This is assumed to be the global profile
|
||||||
|
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
|
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(" ", "_")
|
||||||
|
|
||||||
|
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())}
|
||||||
|
|
||||||
|
# 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)}
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
new_name = self.createUniqueName("quality", "", name_seed, catalog.i18nc("@label", "Custom profile"))
|
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
|
||||||
|
profile._id = new_id
|
||||||
profile.setName(new_name)
|
profile.setName(new_name)
|
||||||
profile._id = new_name
|
|
||||||
|
|
||||||
|
if "type" in profile.getMetaData():
|
||||||
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
|
else:
|
||||||
|
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._activeDefinition())
|
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):
|
||||||
|
|
@ -185,12 +343,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
result.append( (plugin_id, meta_data) )
|
result.append( (plugin_id, meta_data) )
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Gets the active definition
|
## Get the definition to use to select quality profiles for the active machine
|
||||||
# \return the active definition object or None if there is no definition
|
# \return the active quality definition object or None if there is no quality definition
|
||||||
def _activeDefinition(self):
|
def _activeQualityDefinition(self):
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
definition = global_container_stack.getBottom()
|
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
|
||||||
|
definition = self.findDefinitionContainers(id=definition_id)[0]
|
||||||
|
|
||||||
if definition:
|
if definition:
|
||||||
return definition
|
return definition
|
||||||
return None
|
return None
|
||||||
|
|
@ -207,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
|
||||||
621
cura/Settings/ExtruderManager.py
Normal file → Executable file
621
cura/Settings/ExtruderManager.py
Normal file → Executable file
|
|
@ -1,32 +1,54 @@
|
||||||
# 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
|
||||||
import UM.Settings.ContainerRegistry #Finding containers by ID.
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
import UM.Settings.SettingFunction
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
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):
|
||||||
## Signal to notify other components when the list of extruders changes.
|
|
||||||
extrudersChanged = pyqtSignal(QVariant)
|
|
||||||
|
|
||||||
## Notify when the user switches the currently active extruder.
|
|
||||||
activeExtruderChanged = pyqtSignal()
|
|
||||||
|
|
||||||
## Registers listeners and such to listen to changes to the extruders.
|
## Registers listeners and such to listen to changes to the extruders.
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
|
||||||
self._active_extruder_index = 0
|
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
|
||||||
UM.Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
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()
|
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.
|
||||||
|
extrudersChanged = pyqtSignal(QVariant)
|
||||||
|
|
||||||
|
## Signal to notify other components when the global container stack is switched to a definition
|
||||||
|
# that has different extruders than the previous global container stack
|
||||||
|
globalContainerStackDefinitionChanged = pyqtSignal()
|
||||||
|
|
||||||
|
## Notify when the user switches the currently active extruder.
|
||||||
|
activeExtruderChanged = pyqtSignal()
|
||||||
|
|
||||||
## 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
|
||||||
|
|
@ -34,32 +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.
|
||||||
return len(self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()])
|
try:
|
||||||
|
return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
|
||||||
|
except KeyError:
|
||||||
|
return 0
|
||||||
|
|
||||||
@pyqtProperty("QVariantMap", notify=extrudersChanged)
|
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
||||||
|
@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)
|
||||||
|
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
|
||||||
|
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
|
||||||
|
extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
|
||||||
|
if extruder.getId() == extruder_stack_id:
|
||||||
|
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.
|
||||||
#
|
#
|
||||||
|
|
@ -68,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
|
||||||
|
|
@ -77,233 +120,316 @@ 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
|
||||||
|
|
||||||
|
def registerExtruder(self, extruder_train, machine_id):
|
||||||
changed = False
|
changed = False
|
||||||
machine_definition_id = machine_definition.getId()
|
|
||||||
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
|
||||||
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.
|
# do not register if an extruder has already been registered at the position on this machine
|
||||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_id)
|
if any(item.getId() == extruder_train.getId() for item in self._extruder_trains[machine_id].values()):
|
||||||
for extruder_train in extruder_trains:
|
Logger.log("w", "Extruder [%s] has already been registered on machine [%s], not doing anything",
|
||||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
extruder_train.getId(), machine_id)
|
||||||
|
return
|
||||||
|
|
||||||
# Make sure the next stack is a stack that contains only the machine definition
|
if extruder_train:
|
||||||
if not extruder_train.getNextStack():
|
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||||
shallowStack = UM.Settings.ContainerStack(machine_id + "_shallow")
|
changed = True
|
||||||
shallowStack.addContainer(machine_definition)
|
|
||||||
extruder_train.setNextStack(shallowStack)
|
|
||||||
changed = True
|
|
||||||
if changed:
|
if changed:
|
||||||
self.extrudersChanged.emit(machine_id)
|
self.extrudersChanged.emit(machine_id)
|
||||||
|
|
||||||
## Creates a container stack for an extruder train.
|
def getAllExtruderValues(self, setting_key):
|
||||||
|
return self.getAllExtruderSettings(setting_key, "value")
|
||||||
|
|
||||||
|
## Gets a property of a setting for all extruders.
|
||||||
#
|
#
|
||||||
# The container stack has an extruder definition at the bottom, which is
|
# \param setting_key \type{str} The setting to get the property of.
|
||||||
# linked to a machine definition. Then it has a variant profile, a material
|
# \param property \type{str} The property to get.
|
||||||
# profile, a quality profile and a user profile, in that order.
|
# \return \type{List} the list of results
|
||||||
|
def getAllExtruderSettings(self, setting_key: str, prop: str):
|
||||||
|
result = []
|
||||||
|
for index in self.extruderIds:
|
||||||
|
extruder_stack_id = self.extruderIds[str(index)]
|
||||||
|
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||||
|
result.append(extruder_stack.getProperty(setting_key, prop))
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Gets the extruder stacks that are actually being used at the moment.
|
||||||
#
|
#
|
||||||
# The resulting container stack is added to the registry.
|
# An extruder stack is being used if it is the extruder to print any mesh
|
||||||
|
# with, or if it is the support infill extruder, the support interface
|
||||||
|
# extruder, or the bed adhesion extruder.
|
||||||
#
|
#
|
||||||
# \param extruder_definition The extruder to create the extruder train for.
|
# If there are no extruders, this returns the global stack as a singleton
|
||||||
# \param machine_definition The machine that the extruder train belongs to.
|
# list.
|
||||||
# \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.
|
# \return A list of extruder stacks.
|
||||||
def createExtruderTrain(self, extruder_definition, machine_definition, position, machine_id):
|
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
||||||
# Cache some things.
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
machine_definition_id = machine_definition.getId()
|
|
||||||
|
|
||||||
# Create a container stack for this extruder.
|
used_extruder_stack_ids = set()
|
||||||
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.
|
# Get the extruders of all meshes in the scene
|
||||||
variant = container_registry.findInstanceContainers(id = "empty_variant")[0]
|
support_enabled = False
|
||||||
if machine_definition.getMetaDataEntry("has_variants"):
|
support_bottom_enabled = False
|
||||||
# First add any variant. Later, overwrite with preference if the preference is valid.
|
support_roof_enabled = False
|
||||||
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, 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.
|
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||||
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.
|
|
||||||
if machine_definition.getMetaDataEntry("has_variant_materials", default = "False") == "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:
|
# If no extruders are registered in the extruder manager yet, return an empty array
|
||||||
search_criteria["variant"] = variant.id
|
if len(self.extruderIds) == 0:
|
||||||
else:
|
return []
|
||||||
search_criteria["definition"] = "fdmprinter"
|
|
||||||
|
|
||||||
preferred_materials = container_registry.findInstanceContainers(**search_criteria)
|
# Get the extruders of all printable meshes in the scene
|
||||||
if len(preferred_materials) >= 1:
|
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()]
|
||||||
material = preferred_materials[0]
|
for mesh in meshes:
|
||||||
else:
|
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||||
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)
|
if not extruder_stack_id:
|
||||||
# And leave it at the default material.
|
# No per-object settings for this node
|
||||||
container_stack.addContainer(material)
|
extruder_stack_id = self.extruderIds["0"]
|
||||||
|
used_extruder_stack_ids.add(extruder_stack_id)
|
||||||
|
|
||||||
# Find a quality to use for this extruder.
|
# Get whether any of them use support.
|
||||||
quality = container_registry.getEmptyInstanceContainer()
|
stack_to_use = mesh.callDecoration("getStack") # if there is a per-mesh stack, we use it
|
||||||
|
if not stack_to_use:
|
||||||
|
# if there is no per-mesh stack, we use the build extruder for this mesh
|
||||||
|
stack_to_use = container_registry.findContainerStacks(id = extruder_stack_id)[0]
|
||||||
|
|
||||||
search_criteria = { "type": "quality" }
|
support_enabled |= stack_to_use.getProperty("support_enable", "value")
|
||||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
support_bottom_enabled |= stack_to_use.getProperty("support_bottom_enable", "value")
|
||||||
search_criteria["definition"] = machine_definition.id
|
support_roof_enabled |= stack_to_use.getProperty("support_roof_enable", "value")
|
||||||
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")
|
# Check limit to extruders
|
||||||
if preferred_quality:
|
limit_to_extruder_feature_list = ["wall_0_extruder_nr",
|
||||||
search_criteria["id"] = preferred_quality
|
"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)])
|
||||||
|
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
# Check support extruders
|
||||||
if not containers and preferred_quality:
|
if support_enabled:
|
||||||
UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))])
|
||||||
search_criteria.pop("id", None)
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))])
|
||||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
if support_bottom_enabled:
|
||||||
if containers:
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_bottom_extruder_nr", "value"))])
|
||||||
quality = containers[0]
|
if support_roof_enabled:
|
||||||
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_roof_extruder_nr", "value"))])
|
||||||
|
|
||||||
container_stack.addContainer(quality)
|
# The platform adhesion extruder. Not used if using none.
|
||||||
|
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||||
|
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
|
||||||
|
|
||||||
user_profile = container_registry.findInstanceContainers(type = "user", extruder = extruder_stack_id)
|
try:
|
||||||
if user_profile: # There was already a user profile, loaded from settings.
|
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||||
user_profile = user_profile[0]
|
except IndexError: # One or more of the extruders was not found.
|
||||||
else:
|
Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
|
||||||
user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings") # Add an empty user profile.
|
return []
|
||||||
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)
|
|
||||||
|
|
||||||
# Make sure the next stack is a stack that contains only the machine definition
|
|
||||||
if not container_stack.getNextStack():
|
|
||||||
shallowStack = UM.Settings.ContainerStack(machine_id + "_shallow")
|
|
||||||
shallowStack.addContainer(machine_definition)
|
|
||||||
container_stack.setNextStack(shallowStack)
|
|
||||||
|
|
||||||
container_registry.addContainer(container_stack)
|
|
||||||
|
|
||||||
## 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]
|
|
||||||
|
|
||||||
def __globalContainerStackChanged(self):
|
## Returns a list containing the global stack and active extruder stacks.
|
||||||
|
#
|
||||||
|
# The first element is the global container stack, followed by any extruder stacks.
|
||||||
|
# \return \type{List[ContainerStack]}
|
||||||
|
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_stack:
|
||||||
|
return None
|
||||||
|
|
||||||
|
result = [global_stack]
|
||||||
|
result.extend(self.getActiveExtruderStacks())
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Returns the list of active extruder stacks, taking into account the machine extruder count.
|
||||||
|
#
|
||||||
|
# \return \type{List[ContainerStack]} a list of
|
||||||
|
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
|
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:
|
||||||
|
self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
|
||||||
|
self.globalContainerStackDefinitionChanged.emit()
|
||||||
|
|
||||||
|
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||||
self._addCurrentMachineExtruders()
|
self._addCurrentMachineExtruders()
|
||||||
self.activeExtruderChanged.emit()
|
|
||||||
|
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()):
|
||||||
value = extruder.getRawProperty(key, "value")
|
# 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"):
|
||||||
if not value:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if isinstance(value, UM.Settings.SettingFunction):
|
value = extruder.getRawProperty(key, "value")
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if isinstance(value, SettingFunction):
|
||||||
value = value(extruder)
|
value = value(extruder)
|
||||||
|
|
||||||
result.append(value)
|
result.append(value)
|
||||||
|
|
@ -313,6 +439,57 @@ 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.
|
||||||
|
#
|
||||||
|
# This is exposed to qml for display purposes
|
||||||
|
#
|
||||||
|
# \param key The key of the setting to retrieve values for.
|
||||||
|
#
|
||||||
|
# \return String representing the extruder values
|
||||||
|
@pyqtSlot(str, result="QVariant")
|
||||||
|
def getInstanceExtruderValues(self, key):
|
||||||
|
return ExtruderManager.getExtruderValues(key)
|
||||||
|
|
||||||
## Get the value for a setting from a specific extruder.
|
## Get the value for a setting from a specific extruder.
|
||||||
#
|
#
|
||||||
# This is exposed to SettingFunction to use in value functions.
|
# This is exposed to SettingFunction to use in value functions.
|
||||||
|
|
@ -328,9 +505,75 @@ 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.
|
else:
|
||||||
value = UM.Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
# Just a value from global.
|
||||||
|
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
||||||
|
|
||||||
return 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.
|
||||||
|
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
## Get the resolve value or value for a given key
|
||||||
|
#
|
||||||
|
# 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 getResolveOrValue(key):
|
||||||
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
resolved_value = global_stack.getProperty(key, "value")
|
||||||
|
|
||||||
|
return resolved_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.
|
||||||
#
|
#
|
||||||
|
|
@ -29,6 +35,15 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
# containers.
|
# containers.
|
||||||
IndexRole = Qt.UserRole + 4
|
IndexRole = Qt.UserRole + 4
|
||||||
|
|
||||||
|
# The ID of the definition of the extruder.
|
||||||
|
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"]
|
||||||
|
|
@ -44,98 +59,153 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
self.addRoleName(self.NameRole, "name")
|
self.addRoleName(self.NameRole, "name")
|
||||||
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.MaterialRole, "material")
|
||||||
|
self.addRoleName(self.VariantRole, "variant")
|
||||||
|
|
||||||
self._add_global = False
|
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._active_extruder_stack = None
|
self._simple_names = False
|
||||||
|
|
||||||
#Listen to changes.
|
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||||
manager = ExtruderManager.getInstance()
|
self._add_optional_extruder = False
|
||||||
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
|
|
||||||
|
|
||||||
self._updateExtruders()
|
# Listen to changes
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) # When the machine is swapped we must update the active machine extruders
|
||||||
|
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
|
||||||
|
|
||||||
manager.activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
addOptionalExtruderChanged = pyqtSignal()
|
||||||
self._onActiveExtruderChanged()
|
|
||||||
|
|
||||||
def setAddGlobal(self, add):
|
def setAddOptionalExtruder(self, add_optional_extruder):
|
||||||
if add != self._add_global:
|
if add_optional_extruder != self._add_optional_extruder:
|
||||||
self._add_global = add
|
self._add_optional_extruder = add_optional_extruder
|
||||||
|
self.addOptionalExtruderChanged.emit()
|
||||||
self._updateExtruders()
|
self._updateExtruders()
|
||||||
self.addGlobalChanged.emit()
|
|
||||||
|
|
||||||
addGlobalChanged = pyqtSignal()
|
@pyqtProperty(bool, fset = setAddOptionalExtruder, notify = addOptionalExtruderChanged)
|
||||||
|
def addOptionalExtruder(self):
|
||||||
|
return self._add_optional_extruder
|
||||||
|
|
||||||
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
|
## Set the simpleNames property.
|
||||||
def addGlobal(self):
|
def setSimpleNames(self, simple_names):
|
||||||
return self._add_global
|
if simple_names != self._simple_names:
|
||||||
|
self._simple_names = simple_names
|
||||||
|
self.simpleNamesChanged.emit()
|
||||||
|
self._updateExtruders()
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
## Emitted when the simpleNames property changes.
|
||||||
manager = ExtruderManager.getInstance()
|
simpleNamesChanged = pyqtSignal()
|
||||||
active_extruder_stack = manager.getActiveExtruderStack()
|
|
||||||
if self._active_extruder_stack != active_extruder_stack:
|
|
||||||
if self._active_extruder_stack:
|
|
||||||
self._active_extruder_stack.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
|
||||||
|
|
||||||
if active_extruder_stack:
|
## Whether or not the model should show all definitions regardless of visibility.
|
||||||
# Update the model when the material container is changed
|
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
|
||||||
active_extruder_stack.containersChanged.connect(self._onExtruderStackContainersChanged)
|
def simpleNames(self):
|
||||||
self._active_extruder_stack = active_extruder_stack
|
return self._simple_names
|
||||||
|
|
||||||
|
## Links to the stack-changed signal of the new extruders when an extruder
|
||||||
|
# is swapped out or added in the current machine.
|
||||||
|
#
|
||||||
|
# \param machine_id The machine for which the extruders changed. This is
|
||||||
|
# filled by the ExtruderManager.extrudersChanged signal when coming from
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Unlink from old extruders
|
||||||
|
for extruder in self._active_machine_extruders:
|
||||||
|
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||||
|
|
||||||
|
# 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):
|
||||||
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
# Update when there is an empty container or material change
|
||||||
if container.getMetaDataEntry("type") == "material":
|
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
|
||||||
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:
|
||||||
self.clear()
|
extruders_changed = True
|
||||||
changed = True
|
|
||||||
|
|
||||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
items = []
|
||||||
|
|
||||||
|
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" })
|
|
||||||
color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0]
|
|
||||||
item = {
|
|
||||||
"id": global_container_stack.getId(),
|
|
||||||
"name": "Global",
|
|
||||||
"color": color,
|
|
||||||
"index": -1
|
|
||||||
}
|
|
||||||
self.appendItem(item)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
manager = ExtruderManager.getInstance()
|
# get machine extruder count for verification
|
||||||
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
|
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
extruder_name = extruder.getName()
|
|
||||||
material = extruder.findContainer({ "type": "material" })
|
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()):
|
||||||
if material:
|
|
||||||
extruder_name = "%s (%s)" % (material.getName(), extruder_name)
|
|
||||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||||
try:
|
try:
|
||||||
position = int(position)
|
position = int(position)
|
||||||
except ValueError: #Not a proper int.
|
except ValueError:
|
||||||
|
# Not a proper int.
|
||||||
position = -1
|
position = -1
|
||||||
default_color = self.defaultColors[position] if position >= 0 and position < len(self.defaultColors) else self.defaultColors[0]
|
if position >= machine_extruder_count:
|
||||||
color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color
|
continue
|
||||||
item = { #Construct an item with only the relevant information.
|
|
||||||
"id": extruder.getId(),
|
|
||||||
"name": extruder_name,
|
|
||||||
"color": color,
|
|
||||||
"index": position
|
|
||||||
}
|
|
||||||
self.appendItem(item)
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if changed:
|
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
||||||
self.sort(lambda item: item["index"])
|
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
||||||
|
|
||||||
|
# construct an item with only the relevant information
|
||||||
|
item = {
|
||||||
|
"id": extruder.getId(),
|
||||||
|
"name": extruder.getName(),
|
||||||
|
"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,
|
||||||
|
"definition": ""
|
||||||
|
}
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
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)
|
||||||
1423
cura/Settings/MachineManager.py
Normal file → Executable file
1423
cura/Settings/MachineManager.py
Normal file → Executable file
File diff suppressed because it is too large
Load diff
66
cura/Settings/MachineNameValidator.py
Normal file
66
cura/Settings/MachineNameValidator.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QObject, pyqtSignal, QRegExp
|
||||||
|
from PyQt5.QtGui import QValidator
|
||||||
|
import os #For statvfs.
|
||||||
|
import urllib #To escape machine names for how they're saved to file.
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
|
||||||
|
## Are machine names valid?
|
||||||
|
#
|
||||||
|
# Performs checks based on the length of the name.
|
||||||
|
class MachineNameValidator(QObject):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
#Compute the validation regex for printer names. This is limited by the maximum file name length.
|
||||||
|
try:
|
||||||
|
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||||
|
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||||
|
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||||
|
machine_name_max_length = filename_max_length - len("_current_settings.") - len(ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix)
|
||||||
|
# Characters that urllib.parse.quote_plus escapes count for 12! So now
|
||||||
|
# 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.
|
||||||
|
maximum_special_characters = int(machine_name_max_length / 12)
|
||||||
|
unescaped = r"[a-zA-Z0-9_\-\.\/]"
|
||||||
|
self.machine_name_regex = r"^((" + unescaped + "){0,12}|.){0," + str(maximum_special_characters) + r"}$"
|
||||||
|
|
||||||
|
validationChanged = pyqtSignal()
|
||||||
|
|
||||||
|
## Check if a specified machine name is allowed.
|
||||||
|
#
|
||||||
|
# \param name The machine name to check.
|
||||||
|
# \param position The current position of the cursor in the text box.
|
||||||
|
# \return ``QValidator.Invalid`` if it's disallowed, or
|
||||||
|
# ``QValidator.Acceptable`` if it's allowed.
|
||||||
|
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).
|
||||||
|
try:
|
||||||
|
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
|
||||||
|
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||||
|
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||||
|
escaped_name = urllib.parse.quote_plus(name)
|
||||||
|
current_settings_filename = escaped_name + "_current_settings." + ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
||||||
|
if len(current_settings_filename) > filename_max_length:
|
||||||
|
return QValidator.Invalid
|
||||||
|
|
||||||
|
return QValidator.Acceptable #All checks succeeded.
|
||||||
|
|
||||||
|
## Updates the validation state of a machine name text field.
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def updateValidation(self, new_name):
|
||||||
|
is_valid = self.validate(new_name, 0)
|
||||||
|
if is_valid == QValidator.Acceptable:
|
||||||
|
self.validation_regex = "^.*$" #Matches anything.
|
||||||
|
else:
|
||||||
|
self.validation_regex = "a^" #Never matches (unless you manage to get "a" before the start of the string... good luck).
|
||||||
|
self.validationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty("QRegExp", notify=validationChanged)
|
||||||
|
def machineNameRegex(self):
|
||||||
|
return QRegExp(self.machine_name_regex)
|
||||||
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()
|
||||||
218
cura/Settings/ProfilesModel.py
Normal file
218
cura/Settings/ProfilesModel.py
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
|
||||||
|
|
||||||
|
from cura.QualityManager import QualityManager
|
||||||
|
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.
|
||||||
|
#
|
||||||
|
class ProfilesModel(InstanceContainersModel):
|
||||||
|
LayerHeightRole = Qt.UserRole + 1001
|
||||||
|
LayerHeightWithoutUnitRole = Qt.UserRole + 1002
|
||||||
|
AvailableRole = Qt.UserRole + 1003
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
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().getMachineManager().activeVariantChanged.connect(self._update)
|
||||||
|
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
||||||
|
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
||||||
|
|
||||||
|
# Factory function, used by QML
|
||||||
|
@staticmethod
|
||||||
|
def createProfilesModel(engine, js_engine):
|
||||||
|
return ProfilesModel.getInstance()
|
||||||
|
|
||||||
|
## Get the singleton instance for this class.
|
||||||
|
@classmethod
|
||||||
|
def getInstance(cls) -> "ProfilesModel":
|
||||||
|
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||||
|
if not ProfilesModel.__instance:
|
||||||
|
ProfilesModel.__instance = cls()
|
||||||
|
return ProfilesModel.__instance
|
||||||
|
|
||||||
|
__instance = None # type: "ProfilesModel"
|
||||||
|
|
||||||
|
## Fetch the list of containers to display.
|
||||||
|
#
|
||||||
|
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||||
|
def _fetchInstanceContainers(self):
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
global_stack_definition = global_container_stack.definition
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
result = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack, 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.
|
||||||
|
def _recomputeItems(self):
|
||||||
|
|
||||||
|
# Some globals that we can re-use.
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
extruder_stacks = self._getOrderedExtruderStacksList()
|
||||||
|
container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
if not 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():
|
||||||
|
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:
|
||||||
|
self._setItemLayerHeight(item, "", "")
|
||||||
|
item["available"] = False
|
||||||
|
yield item
|
||||||
|
continue
|
||||||
|
|
||||||
|
profile = profile[0]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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_type = profile.getMetaDataEntry("quality_type", None)
|
||||||
|
if quality_type:
|
||||||
|
quality_results = machine_manager.determineQualityAndQualityChangesForQualityType(quality_type)
|
||||||
|
for quality_result in quality_results:
|
||||||
|
if quality_result["stack"] is global_container_stack:
|
||||||
|
quality = quality_result["quality"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# No global container stack in the results:
|
||||||
|
if quality_results:
|
||||||
|
# Take any of the extruders.
|
||||||
|
quality = quality_results[0]["quality"]
|
||||||
|
else:
|
||||||
|
quality = None
|
||||||
|
if quality and quality.hasProperty("layer_height", "value"):
|
||||||
|
self._setItemLayerHeight(item, quality.getProperty("layer_height", "value"), unit)
|
||||||
|
yield item
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
|
||||||
|
skip_until_container = global_container_stack.material
|
||||||
|
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No material in stack.
|
||||||
|
skip_until_container = global_container_stack.variant
|
||||||
|
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): # No variant in stack.
|
||||||
|
skip_until_container = global_container_stack.getBottom()
|
||||||
|
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
|
||||||
|
|
||||||
|
## 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)
|
||||||
44
cura/Settings/QualityAndUserProfilesModel.py
Normal file
44
cura/Settings/QualityAndUserProfilesModel.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM.Application import Application
|
||||||
|
|
||||||
|
from cura.QualityManager import QualityManager
|
||||||
|
from cura.Settings.ProfilesModel import ProfilesModel
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
## QML Model for listing the current list of valid quality and quality changes profiles.
|
||||||
|
#
|
||||||
|
class QualityAndUserProfilesModel(ProfilesModel):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
## Fetch the list of containers to display.
|
||||||
|
#
|
||||||
|
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||||
|
def _fetchInstanceContainers(self):
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_container_stack:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Fetch the list of quality changes.
|
||||||
|
quality_manager = QualityManager.getInstance()
|
||||||
|
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
|
||||||
|
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||||
|
|
||||||
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
|
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||||
|
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
|
||||||
|
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 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
|
||||||
241
cura/Settings/QualitySettingsModel.py
Normal file
241
cura/Settings/QualitySettingsModel.py
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import collections
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
import UM.Qt
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
import os
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
|
|
||||||
|
class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
||||||
|
KeyRole = Qt.UserRole + 1
|
||||||
|
LabelRole = Qt.UserRole + 2
|
||||||
|
UnitRole = Qt.UserRole + 3
|
||||||
|
ProfileValueRole = Qt.UserRole + 4
|
||||||
|
ProfileValueSourceRole = Qt.UserRole + 5
|
||||||
|
UserValueRole = Qt.UserRole + 6
|
||||||
|
CategoryRole = Qt.UserRole + 7
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent = parent)
|
||||||
|
|
||||||
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
|
|
||||||
|
self._extruder_id = None
|
||||||
|
self._extruder_definition_id = None
|
||||||
|
self._quality_id = None
|
||||||
|
self._material_id = None
|
||||||
|
self._i18n_catalog = None
|
||||||
|
|
||||||
|
self.addRoleName(self.KeyRole, "key")
|
||||||
|
self.addRoleName(self.LabelRole, "label")
|
||||||
|
self.addRoleName(self.UnitRole, "unit")
|
||||||
|
self.addRoleName(self.ProfileValueRole, "profile_value")
|
||||||
|
self.addRoleName(self.ProfileValueSourceRole, "profile_value_source")
|
||||||
|
self.addRoleName(self.UserValueRole, "user_value")
|
||||||
|
self.addRoleName(self.CategoryRole, "category")
|
||||||
|
|
||||||
|
def setExtruderId(self, extruder_id):
|
||||||
|
if extruder_id != self._extruder_id:
|
||||||
|
self._extruder_id = extruder_id
|
||||||
|
self._update()
|
||||||
|
self.extruderIdChanged.emit()
|
||||||
|
|
||||||
|
extruderIdChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(str, fset = setExtruderId, notify = extruderIdChanged)
|
||||||
|
def extruderId(self):
|
||||||
|
return self._extruder_id
|
||||||
|
|
||||||
|
def setExtruderDefinition(self, extruder_definition):
|
||||||
|
if extruder_definition != self._extruder_definition_id:
|
||||||
|
self._extruder_definition_id = extruder_definition
|
||||||
|
self._update()
|
||||||
|
self.extruderDefinitionChanged.emit()
|
||||||
|
|
||||||
|
extruderDefinitionChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(str, fset = setExtruderDefinition, notify = extruderDefinitionChanged)
|
||||||
|
def extruderDefinition(self):
|
||||||
|
return self._extruder_definition_id
|
||||||
|
|
||||||
|
def setQuality(self, quality):
|
||||||
|
if quality != self._quality_id:
|
||||||
|
self._quality_id = quality
|
||||||
|
self._update()
|
||||||
|
self.qualityChanged.emit()
|
||||||
|
|
||||||
|
qualityChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(str, fset = setQuality, notify = qualityChanged)
|
||||||
|
def quality(self):
|
||||||
|
return self._quality_id
|
||||||
|
|
||||||
|
def setMaterial(self, material):
|
||||||
|
if material != self._material_id:
|
||||||
|
self._material_id = material
|
||||||
|
self._update()
|
||||||
|
self.materialChanged.emit()
|
||||||
|
|
||||||
|
materialChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(str, fset = setMaterial, notify = materialChanged)
|
||||||
|
def material(self):
|
||||||
|
return self._material_id
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
if not self._quality_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
settings = collections.OrderedDict()
|
||||||
|
definition_container = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||||
|
|
||||||
|
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
||||||
|
if not containers:
|
||||||
|
Logger.log("w", "Could not find a quality container with id %s", self._quality_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
quality_container = None
|
||||||
|
quality_changes_container = None
|
||||||
|
|
||||||
|
if containers[0].getMetaDataEntry("type") == "quality":
|
||||||
|
quality_container = containers[0]
|
||||||
|
else:
|
||||||
|
quality_changes_container = containers[0]
|
||||||
|
|
||||||
|
criteria = {
|
||||||
|
"type": "quality",
|
||||||
|
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
||||||
|
"definition": quality_changes_container.getDefinition().getId()
|
||||||
|
}
|
||||||
|
|
||||||
|
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
if not quality_container:
|
||||||
|
Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||||
|
return
|
||||||
|
quality_container = quality_container[0]
|
||||||
|
|
||||||
|
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||||
|
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}
|
||||||
|
|
||||||
|
if self._material_id and self._material_id != "empty_material":
|
||||||
|
criteria["material"] = self._material_id
|
||||||
|
|
||||||
|
criteria["extruder"] = self._extruder_id
|
||||||
|
|
||||||
|
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
if not containers:
|
||||||
|
# Try again, this time without extruder
|
||||||
|
new_criteria = criteria.copy()
|
||||||
|
new_criteria.pop("extruder")
|
||||||
|
containers = self._container_registry.findInstanceContainers(**new_criteria)
|
||||||
|
|
||||||
|
if not containers and "material" in criteria:
|
||||||
|
# Try again, this time without material
|
||||||
|
criteria.pop("material", None)
|
||||||
|
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
|
||||||
|
if not containers:
|
||||||
|
# Try again, this time without material or extruder
|
||||||
|
criteria.pop("extruder") # "material" has already been popped
|
||||||
|
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
|
||||||
|
if not containers:
|
||||||
|
Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||||
|
return
|
||||||
|
|
||||||
|
if quality_changes_container:
|
||||||
|
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
|
||||||
|
if self._extruder_definition_id != "":
|
||||||
|
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
||||||
|
if extruder_definitions:
|
||||||
|
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
||||||
|
criteria["name"] = quality_changes_container.getName()
|
||||||
|
else:
|
||||||
|
criteria["extruder"] = None
|
||||||
|
|
||||||
|
changes = self._container_registry.findInstanceContainers(**criteria)
|
||||||
|
if changes:
|
||||||
|
containers.extend(changes)
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||||
|
|
||||||
|
current_category = ""
|
||||||
|
for definition in definition_container.findDefinitions():
|
||||||
|
if definition.type == "category":
|
||||||
|
current_category = definition.label
|
||||||
|
if self._i18n_catalog:
|
||||||
|
current_category = self._i18n_catalog.i18nc(definition.key + " label", definition.label)
|
||||||
|
continue
|
||||||
|
|
||||||
|
profile_value = None
|
||||||
|
profile_value_source = ""
|
||||||
|
for container in containers:
|
||||||
|
new_value = container.getProperty(definition.key, "value")
|
||||||
|
|
||||||
|
if new_value is not None:
|
||||||
|
profile_value_source = container.getMetaDataEntry("type")
|
||||||
|
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
|
||||||
|
if not self._extruder_id:
|
||||||
|
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
|
||||||
|
else:
|
||||||
|
extruder_stack = self._container_registry.findContainerStacks(id = self._extruder_id)
|
||||||
|
if extruder_stack:
|
||||||
|
user_value = extruder_stack[0].getTop().getProperty(definition.key, "value")
|
||||||
|
|
||||||
|
if profile_value is None and user_value is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if is_multi_extrusion:
|
||||||
|
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
|
||||||
|
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
|
||||||
|
if self._extruder_id != "" and not settable_per_extruder:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
|
||||||
|
if self._extruder_id == "" and settable_per_extruder:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = definition.label
|
||||||
|
if self._i18n_catalog:
|
||||||
|
label = self._i18n_catalog.i18nc(definition.key + " label", label)
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"key": definition.key,
|
||||||
|
"label": label,
|
||||||
|
"unit": definition.unit,
|
||||||
|
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
|
||||||
|
"profile_value_source": profile_value_source,
|
||||||
|
"user_value": "" if user_value is None else str(user_value),
|
||||||
|
"category": current_category
|
||||||
|
})
|
||||||
|
|
||||||
|
self.setItems(items)
|
||||||
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)
|
||||||
235
cura/Settings/SettingInheritanceManager.py
Normal file
235
cura/Settings/SettingInheritanceManager.py
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Application import Application
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
|
## The settingInheritance manager is responsible for checking each setting in order to see if one of the "deeper"
|
||||||
|
# containers has a setting function and the topmost one with a value has a value. We need to have this check
|
||||||
|
# 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
|
||||||
|
# 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):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
|
self._global_container_stack = None
|
||||||
|
self._settings_with_inheritance_warning = []
|
||||||
|
self._active_container_stack = None
|
||||||
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
|
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||||
|
self._onActiveExtruderChanged()
|
||||||
|
|
||||||
|
settingsWithIntheritanceChanged = pyqtSignal()
|
||||||
|
|
||||||
|
## Get the keys of all children settings with an override.
|
||||||
|
@pyqtSlot(str, result = "QStringList")
|
||||||
|
def getChildrenKeysWithOverride(self, key):
|
||||||
|
definitions = self._global_container_stack.definition.findDefinitions(key=key)
|
||||||
|
if not definitions:
|
||||||
|
Logger.log("w", "Could not find definition for key [%s]", key)
|
||||||
|
return []
|
||||||
|
result = []
|
||||||
|
for key in definitions[0].getAllKeys():
|
||||||
|
if key in self._settings_with_inheritance_warning:
|
||||||
|
result.append(key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@pyqtSlot(str, str, result = "QStringList")
|
||||||
|
def getOverridesForExtruder(self, key, extruder_index):
|
||||||
|
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():
|
||||||
|
if self._settingIsOverwritingInheritance(key, extruder_stack):
|
||||||
|
result.append(key)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def manualRemoveOverride(self, key):
|
||||||
|
if key in self._settings_with_inheritance_warning:
|
||||||
|
self._settings_with_inheritance_warning.remove(key)
|
||||||
|
self.settingsWithIntheritanceChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def forceUpdate(self):
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _onActiveExtruderChanged(self):
|
||||||
|
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||||
|
# if not new_active_stack:
|
||||||
|
# new_active_stack = self._global_container_stack
|
||||||
|
|
||||||
|
if new_active_stack != self._active_container_stack: # Check if changed
|
||||||
|
if self._active_container_stack: # Disconnect signal from old container (if any)
|
||||||
|
self._active_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||||
|
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||||
|
|
||||||
|
self._active_container_stack = new_active_stack
|
||||||
|
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||||
|
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
|
||||||
|
self._update() # Ensure that the settings_with_inheritance_warning list is populated.
|
||||||
|
|
||||||
|
def _onPropertyChanged(self, key, property_name):
|
||||||
|
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
|
||||||
|
definitions = self._global_container_stack.definition.findDefinitions(key = key)
|
||||||
|
if not definitions:
|
||||||
|
return
|
||||||
|
|
||||||
|
has_overwritten_inheritance = self._settingIsOverwritingInheritance(key)
|
||||||
|
|
||||||
|
settings_with_inheritance_warning_changed = False
|
||||||
|
|
||||||
|
# Check if the setting needs to be in the list.
|
||||||
|
if key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
|
||||||
|
self._settings_with_inheritance_warning.append(key)
|
||||||
|
settings_with_inheritance_warning_changed = True
|
||||||
|
elif key in self._settings_with_inheritance_warning and not has_overwritten_inheritance:
|
||||||
|
self._settings_with_inheritance_warning.remove(key)
|
||||||
|
settings_with_inheritance_warning_changed = True
|
||||||
|
|
||||||
|
parent = definitions[0].parent
|
||||||
|
# Find the topmost parent (Assumed to be a category)
|
||||||
|
if parent is not None:
|
||||||
|
while parent.parent is not None:
|
||||||
|
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:
|
||||||
|
# Category was not in the list yet, so needs to be added now.
|
||||||
|
self._settings_with_inheritance_warning.append(parent.key)
|
||||||
|
settings_with_inheritance_warning_changed = True
|
||||||
|
|
||||||
|
elif parent.key in self._settings_with_inheritance_warning and not has_overwritten_inheritance:
|
||||||
|
# Category was in the list and one of it's settings is not overwritten.
|
||||||
|
if not self._recursiveCheck(parent): # Check if any of it's children have overwritten inheritance.
|
||||||
|
self._settings_with_inheritance_warning.remove(parent.key)
|
||||||
|
settings_with_inheritance_warning_changed = True
|
||||||
|
|
||||||
|
# Emit the signal if there was any change to the list.
|
||||||
|
if settings_with_inheritance_warning_changed:
|
||||||
|
self.settingsWithIntheritanceChanged.emit()
|
||||||
|
|
||||||
|
def _recursiveCheck(self, definition):
|
||||||
|
for child in definition.children:
|
||||||
|
if child.key in self._settings_with_inheritance_warning:
|
||||||
|
return True
|
||||||
|
if child.children:
|
||||||
|
if self._recursiveCheck(child):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
|
||||||
|
def settingsWithInheritanceWarning(self):
|
||||||
|
return self._settings_with_inheritance_warning
|
||||||
|
|
||||||
|
## Check if a setting has an inheritance function that is overwritten
|
||||||
|
def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool:
|
||||||
|
has_setting_function = False
|
||||||
|
if not stack:
|
||||||
|
stack = self._active_container_stack
|
||||||
|
containers = []
|
||||||
|
|
||||||
|
## Check if the setting has a user state. If not, it is never overwritten.
|
||||||
|
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||||
|
if not has_user_state:
|
||||||
|
return False
|
||||||
|
|
||||||
|
## If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
||||||
|
if not stack.getProperty(key, "enabled"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
## 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"), SettingFunction):
|
||||||
|
return False
|
||||||
|
|
||||||
|
## Mash all containers for all the stacks together.
|
||||||
|
while stack:
|
||||||
|
containers.extend(stack.getContainers())
|
||||||
|
stack = stack.getNextStack()
|
||||||
|
has_non_function_value = False
|
||||||
|
for container in containers:
|
||||||
|
try:
|
||||||
|
value = container.getProperty(key, "value")
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
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
|
||||||
|
has_setting_function = isinstance(value, SettingFunction)
|
||||||
|
if has_setting_function:
|
||||||
|
for setting_key in value.getUsedSettingKeys():
|
||||||
|
if setting_key in self._active_container_stack.getAllKeys():
|
||||||
|
break # We found an actual setting. So has_setting_function can remain true
|
||||||
|
else:
|
||||||
|
# All of the setting_keys turned out to not be setting keys at all!
|
||||||
|
# This can happen due enum keys also being marked as settings.
|
||||||
|
has_setting_function = False
|
||||||
|
|
||||||
|
if has_setting_function is False:
|
||||||
|
has_non_function_value = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if has_setting_function:
|
||||||
|
break # There is a setting function somewhere, stop looking deeper.
|
||||||
|
return has_setting_function and has_non_function_value
|
||||||
|
|
||||||
|
def _update(self):
|
||||||
|
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.
|
||||||
|
for setting_key in self._global_container_stack.getAllKeys():
|
||||||
|
override = self._settingIsOverwritingInheritance(setting_key)
|
||||||
|
if override:
|
||||||
|
self._settings_with_inheritance_warning.append(setting_key)
|
||||||
|
|
||||||
|
# Check all the categories if any of their children have their inheritance overwritten.
|
||||||
|
for category in self._global_container_stack.definition.findDefinitions(type = "category"):
|
||||||
|
if self._recursiveCheck(category):
|
||||||
|
self._settings_with_inheritance_warning.append(category.key)
|
||||||
|
|
||||||
|
# Notify others that things have changed.
|
||||||
|
self.settingsWithIntheritanceChanged.emit()
|
||||||
|
|
||||||
|
def _onGlobalContainerChanged(self):
|
||||||
|
if self._global_container_stack:
|
||||||
|
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||||
|
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||||
|
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if self._global_container_stack:
|
||||||
|
self._global_container_stack.containersChanged.connect(self._onContainersChanged)
|
||||||
|
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||||
|
self._onActiveExtruderChanged()
|
||||||
|
|
||||||
|
def _onContainersChanged(self, container):
|
||||||
|
# TODO: Multiple container changes in sequence now cause quite a few recalculations.
|
||||||
|
# This isn't that big of an issue, but it could be in the future.
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def createSettingInheritanceManager(engine=None, script_engine=None):
|
||||||
|
return SettingInheritanceManager()
|
||||||
|
|
@ -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().activeExtruderStackId
|
|
||||||
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.
|
||||||
|
|
@ -61,9 +65,28 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def getActiveExtruder(self):
|
def getActiveExtruder(self):
|
||||||
return self._extruder_stack
|
return self._extruder_stack
|
||||||
|
|
||||||
|
## Gets the signal that emits if the active extruder changed.
|
||||||
|
#
|
||||||
|
# This can then be accessed via a decorator.
|
||||||
|
def getActiveExtruderChangedSignal(self):
|
||||||
|
return self.activeExtruderChanged
|
||||||
|
|
||||||
|
## Gets the currently active extruders position
|
||||||
|
#
|
||||||
|
# \return An extruder's position, or None if no position info is available.
|
||||||
|
def getActiveExtruderPosition(self):
|
||||||
|
containers = ContainerRegistry.getInstance().findContainers(id = self.getActiveExtruder())
|
||||||
|
if containers:
|
||||||
|
container_stack = containers[0]
|
||||||
|
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.
|
||||||
|
|
@ -77,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())
|
||||||
|
|
||||||
|
|
@ -90,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)
|
||||||
44
cura/Settings/UserProfilesModel.py
Normal file
44
cura/Settings/UserProfilesModel.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM.Application import Application
|
||||||
|
|
||||||
|
from cura.QualityManager import QualityManager
|
||||||
|
from cura.Settings.ProfilesModel import ProfilesModel
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
|
## QML Model for listing the current list of valid quality changes profiles.
|
||||||
|
#
|
||||||
|
class UserProfilesModel(ProfilesModel):
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
## Fetch the list of containers to display.
|
||||||
|
#
|
||||||
|
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||||
|
def _fetchInstanceContainers(self):
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if not global_container_stack:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Fetch the list of quality changes.
|
||||||
|
quality_manager = QualityManager.getInstance()
|
||||||
|
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.definition)
|
||||||
|
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||||
|
|
||||||
|
extruder_manager = ExtruderManager.getInstance()
|
||||||
|
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||||
|
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
|
||||||
|
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 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
|
||||||
|
|
@ -1,12 +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 .MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
|
||||||
from .SettingOverrideDecorator import SettingOverrideDecorator
|
|
||||||
|
|
|
||||||
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
|
||||||
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||||
|
|
||||||
class MultiMaterialDecorator(SceneNodeDecorator):
|
|
||||||
|
class SliceableObjectDecorator(SceneNodeDecorator):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def isMultiMaterial(self):
|
def isSliceable(self):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo):
|
||||||
return MultiMaterialDecorator()
|
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()
|
||||||
|
|
|
||||||
BIN
docs/Cura_Data_Model.odg
Normal file
BIN
docs/Cura_Data_Model.odg
Normal file
Binary file not shown.
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"
|
||||||
|
|
|
||||||
301
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
301
plugins/3MFReader/ThreeMFReader.py
Normal file → Executable file
|
|
@ -1,22 +1,34 @@
|
||||||
# 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
|
import os.path
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
import zipfile
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
from UM.Math.Quaternion import Quaternion
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||||
from UM.Job import Job
|
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
|
||||||
|
|
||||||
import math
|
MYPY = False
|
||||||
import zipfile
|
|
||||||
|
import Savitar
|
||||||
|
import numpy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import xml.etree.cElementTree as ET
|
if not MYPY:
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||||
|
|
@ -24,104 +36,207 @@ class ThreeMFReader(MeshReader):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._supported_extensions = [".3mf"]
|
self._supported_extensions = [".3mf"]
|
||||||
|
self._root = None
|
||||||
self._namespaces = {
|
self._namespaces = {
|
||||||
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
||||||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
||||||
}
|
}
|
||||||
|
self._base_name = ""
|
||||||
|
self._unit = None
|
||||||
|
|
||||||
def read(self, file_name):
|
def _createMatrixFromTransformationString(self, transformation):
|
||||||
result = SceneNode()
|
if transformation == "":
|
||||||
# The base object of 3mf is a zipped archive.
|
return Matrix()
|
||||||
archive = zipfile.ZipFile(file_name, "r")
|
|
||||||
try:
|
|
||||||
root = ET.parse(archive.open("3D/3dmodel.model"))
|
|
||||||
|
|
||||||
# There can be multiple objects, try to load all of them.
|
splitted_transformation = transformation.split()
|
||||||
objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
|
## Transformation is saved as:
|
||||||
if len(objects) == 0:
|
## M00 M01 M02 0.0
|
||||||
Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
|
## M10 M11 M12 0.0
|
||||||
return None
|
## M20 M21 M22 0.0
|
||||||
|
## M30 M31 M32 1.0
|
||||||
|
## We switch the row & cols as that is how everyone else uses matrices!
|
||||||
|
temp_mat = Matrix()
|
||||||
|
# Rotation & Scale
|
||||||
|
temp_mat._data[0, 0] = splitted_transformation[0]
|
||||||
|
temp_mat._data[1, 0] = splitted_transformation[1]
|
||||||
|
temp_mat._data[2, 0] = splitted_transformation[2]
|
||||||
|
temp_mat._data[0, 1] = splitted_transformation[3]
|
||||||
|
temp_mat._data[1, 1] = splitted_transformation[4]
|
||||||
|
temp_mat._data[2, 1] = splitted_transformation[5]
|
||||||
|
temp_mat._data[0, 2] = splitted_transformation[6]
|
||||||
|
temp_mat._data[1, 2] = splitted_transformation[7]
|
||||||
|
temp_mat._data[2, 2] = splitted_transformation[8]
|
||||||
|
|
||||||
for entry in objects:
|
# Translation
|
||||||
mesh_builder = MeshBuilder()
|
temp_mat._data[0, 3] = splitted_transformation[9]
|
||||||
node = SceneNode()
|
temp_mat._data[1, 3] = splitted_transformation[10]
|
||||||
vertex_list = []
|
temp_mat._data[2, 3] = splitted_transformation[11]
|
||||||
#for vertex in entry.mesh.vertices.vertex:
|
|
||||||
for vertex in entry.findall(".//3mf:vertex", self._namespaces):
|
|
||||||
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
triangles = entry.findall(".//3mf:triangle", self._namespaces)
|
return temp_mat
|
||||||
mesh_builder.reserveFaceCount(len(triangles))
|
|
||||||
|
|
||||||
for triangle in triangles:
|
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
|
||||||
v1 = int(triangle.get("v1"))
|
# \returns Uranium scene node.
|
||||||
v2 = int(triangle.get("v2"))
|
def _convertSavitarNodeToUMNode(self, savitar_node):
|
||||||
v3 = int(triangle.get("v3"))
|
um_node = SceneNode()
|
||||||
|
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||||
|
um_node.setTransformation(transformation)
|
||||||
|
mesh_builder = MeshBuilder()
|
||||||
|
|
||||||
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
|
data = numpy.fromstring(savitar_node.getMeshData().getFlatVerticesAsBytes(), dtype=numpy.float32)
|
||||||
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()
|
vertices = numpy.resize(data, (int(data.size / 3), 3))
|
||||||
|
mesh_builder.setVertices(vertices)
|
||||||
|
mesh_builder.calculateNormals(fast=True)
|
||||||
|
mesh_data = mesh_builder.build()
|
||||||
|
|
||||||
# Rotate the model; We use a different coordinate frame.
|
if len(mesh_data.getVertices()):
|
||||||
rotation = Matrix()
|
um_node.setMeshData(mesh_data)
|
||||||
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
|
|
||||||
|
|
||||||
# TODO: We currently do not check for normals and simply recalculate them.
|
for child in savitar_node.getChildren():
|
||||||
mesh_builder.calculateNormals()
|
child_node = self._convertSavitarNodeToUMNode(child)
|
||||||
mesh_builder.setFileName(file_name)
|
if child_node:
|
||||||
node.setMeshData(mesh_builder.build().getTransformed(rotation))
|
um_node.addChild(child_node)
|
||||||
node.setSelectable(True)
|
|
||||||
|
|
||||||
transformations = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces)
|
if um_node.getMeshData() is None and len(um_node.getChildren()) == 0:
|
||||||
transformation = transformations[0] if transformations else None
|
|
||||||
if transformation is not None and transformation.get("transform"):
|
|
||||||
splitted_transformation = transformation.get("transform").split()
|
|
||||||
## Transformation is saved as:
|
|
||||||
## M00 M01 M02 0.0
|
|
||||||
## M10 M11 M12 0.0
|
|
||||||
## M20 M21 M22 0.0
|
|
||||||
## M30 M31 M32 1.0
|
|
||||||
## We switch the row & cols as that is how everyone else uses matrices!
|
|
||||||
temp_mat = Matrix()
|
|
||||||
# Rotation & Scale
|
|
||||||
temp_mat._data[0,0] = splitted_transformation[0]
|
|
||||||
temp_mat._data[1,0] = splitted_transformation[1]
|
|
||||||
temp_mat._data[2,0] = splitted_transformation[2]
|
|
||||||
temp_mat._data[0,1] = splitted_transformation[3]
|
|
||||||
temp_mat._data[1,1] = splitted_transformation[4]
|
|
||||||
temp_mat._data[2,1] = splitted_transformation[5]
|
|
||||||
temp_mat._data[0,2] = splitted_transformation[6]
|
|
||||||
temp_mat._data[1,2] = splitted_transformation[7]
|
|
||||||
temp_mat._data[2,2] = splitted_transformation[8]
|
|
||||||
|
|
||||||
# Translation
|
|
||||||
temp_mat._data[0,3] = splitted_transformation[9]
|
|
||||||
temp_mat._data[1,3] = splitted_transformation[10]
|
|
||||||
temp_mat._data[2,3] = splitted_transformation[11]
|
|
||||||
|
|
||||||
node.setTransformation(temp_mat)
|
|
||||||
|
|
||||||
result.addChild(node)
|
|
||||||
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
# If there is more then one object, group them.
|
|
||||||
if len(objects) > 1:
|
|
||||||
group_decorator = GroupDecorator()
|
|
||||||
result.addDecorator(group_decorator)
|
|
||||||
elif len(objects) == 1:
|
|
||||||
result = result.getChildren()[0] # Only one object found, return that.
|
|
||||||
except Exception as e:
|
|
||||||
Logger.log("e", "exception occured in 3mf reader: %s", e)
|
|
||||||
|
|
||||||
try: # Selftest - There might be more functions that should fail
|
|
||||||
boundingBox = result.getBoundingBox()
|
|
||||||
boundingBox.isValid()
|
|
||||||
except:
|
|
||||||
return None
|
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):
|
||||||
|
result = []
|
||||||
|
# The base object of 3mf is a zipped archive.
|
||||||
|
try:
|
||||||
|
archive = zipfile.ZipFile(file_name, "r")
|
||||||
|
self._base_name = os.path.basename(file_name)
|
||||||
|
parser = Savitar.ThreeMFParser()
|
||||||
|
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
|
||||||
|
self._unit = scene_3mf.getUnit()
|
||||||
|
for node in scene_3mf.getSceneNodes():
|
||||||
|
um_node = self._convertSavitarNodeToUMNode(node)
|
||||||
|
if um_node is None:
|
||||||
|
continue
|
||||||
|
# compensate for original center position, if object(s) is/are not around its zero position
|
||||||
|
|
||||||
|
transform_matrix = Matrix()
|
||||||
|
mesh_data = um_node.getMeshData()
|
||||||
|
if mesh_data is not None:
|
||||||
|
extents = mesh_data.getExtents()
|
||||||
|
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.
|
||||||
|
# First step: flip the y and z axis.
|
||||||
|
transformation_matrix = Matrix()
|
||||||
|
transformation_matrix._data[1, 1] = 0
|
||||||
|
transformation_matrix._data[1, 2] = 1
|
||||||
|
transformation_matrix._data[2, 1] = -1
|
||||||
|
transformation_matrix._data[2, 2] = 0
|
||||||
|
|
||||||
|
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
||||||
|
# build volume.
|
||||||
|
if global_container_stack:
|
||||||
|
translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2,
|
||||||
|
y=-global_container_stack.getProperty("machine_depth", "value") / 2,
|
||||||
|
z=0)
|
||||||
|
translation_matrix = Matrix()
|
||||||
|
translation_matrix.setByTranslation(translation_vector)
|
||||||
|
transformation_matrix.multiply(translation_matrix)
|
||||||
|
|
||||||
|
# Third step: 3MF also defines a unit, whereas Cura always assumes mm.
|
||||||
|
scale_matrix = Matrix()
|
||||||
|
scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
|
||||||
|
transformation_matrix.multiply(scale_matrix)
|
||||||
|
|
||||||
|
# Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
|
||||||
|
um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
result.append(um_node)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||||
|
return []
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
## Create a scale vector based on a unit string.
|
||||||
|
# The core spec defines the following:
|
||||||
|
# * micron
|
||||||
|
# * millimeter (default)
|
||||||
|
# * centimeter
|
||||||
|
# * inch
|
||||||
|
# * foot
|
||||||
|
# * meter
|
||||||
|
def _getScaleFromUnit(self, unit):
|
||||||
|
if unit is None:
|
||||||
|
unit = "millimeter"
|
||||||
|
if unit == "micron":
|
||||||
|
scale = 0.001
|
||||||
|
elif unit == "millimeter":
|
||||||
|
scale = 1
|
||||||
|
elif unit == "centimeter":
|
||||||
|
scale = 10
|
||||||
|
elif unit == "inch":
|
||||||
|
scale = 25.4
|
||||||
|
elif unit == "foot":
|
||||||
|
scale = 304.8
|
||||||
|
elif unit == "meter":
|
||||||
|
scale = 1000
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
|
||||||
|
scale = 1
|
||||||
|
|
||||||
|
return Vector(scale, scale, scale)
|
||||||
|
|
|
||||||
1120
plugins/3MFReader/ThreeMFWorkspaceReader.py
Executable file
1120
plugins/3MFReader/ThreeMFWorkspaceReader.py
Executable file
File diff suppressed because it is too large
Load diff
325
plugins/3MFReader/WorkspaceDialog.py
Normal file
325
plugins/3MFReader/WorkspaceDialog.py
Normal file
|
|
@ -0,0 +1,325 @@
|
||||||
|
# Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
# 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.Application import Application
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceDialog(QObject):
|
||||||
|
showDialogSignal = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._component = None
|
||||||
|
self._context = None
|
||||||
|
self._view = None
|
||||||
|
self._qml_url = "WorkspaceDialog.qml"
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._default_strategy = None
|
||||||
|
self._result = {"machine": self._default_strategy,
|
||||||
|
"quality_changes": self._default_strategy,
|
||||||
|
"definition_changes": self._default_strategy,
|
||||||
|
"material": self._default_strategy}
|
||||||
|
self._visible = False
|
||||||
|
self.showDialogSignal.connect(self.__show)
|
||||||
|
|
||||||
|
self._has_quality_changes_conflict = False
|
||||||
|
self._has_definition_changes_conflict = False
|
||||||
|
self._has_machine_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()
|
||||||
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
|
definitionChangesConflictChanged = 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)
|
||||||
|
def machineConflict(self):
|
||||||
|
return self._has_machine_conflict
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=qualityChangesConflictChanged)
|
||||||
|
def qualityChangesConflict(self):
|
||||||
|
return self._has_quality_changes_conflict
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=definitionChangesConflictChanged)
|
||||||
|
def definitionChangesConflict(self):
|
||||||
|
return self._has_definition_changes_conflict
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||||
|
def materialConflict(self):
|
||||||
|
return self._has_material_conflict
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def setResolveStrategy(self, key, strategy):
|
||||||
|
if key in self._result:
|
||||||
|
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):
|
||||||
|
if self._has_material_conflict != material_conflict:
|
||||||
|
self._has_material_conflict = material_conflict
|
||||||
|
self.materialConflictChanged.emit()
|
||||||
|
|
||||||
|
def setMachineConflict(self, machine_conflict):
|
||||||
|
if self._has_machine_conflict != machine_conflict:
|
||||||
|
self._has_machine_conflict = machine_conflict
|
||||||
|
self.machineConflictChanged.emit()
|
||||||
|
|
||||||
|
def setQualityChangesConflict(self, quality_changes_conflict):
|
||||||
|
if self._has_quality_changes_conflict != quality_changes_conflict:
|
||||||
|
self._has_quality_changes_conflict = quality_changes_conflict
|
||||||
|
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):
|
||||||
|
if "machine" in self._result and not self._has_machine_conflict:
|
||||||
|
self._result["machine"] = None
|
||||||
|
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
def _createViewFromQML(self):
|
||||||
|
path = os.path.join(PluginRegistry.getInstance().getPluginPath("3MFReader"), self._qml_url)
|
||||||
|
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
# Emit signal so the right thread actually shows the view.
|
||||||
|
if threading.current_thread() != threading.main_thread():
|
||||||
|
self._lock.acquire()
|
||||||
|
# Reset the result
|
||||||
|
self._result = {"machine": self._default_strategy,
|
||||||
|
"quality_changes": self._default_strategy,
|
||||||
|
"definition_changes": self._default_strategy,
|
||||||
|
"material": self._default_strategy}
|
||||||
|
self._visible = True
|
||||||
|
self.showDialogSignal.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
## Used to notify the dialog so the lock can be released.
|
||||||
|
def notifyClosed(self):
|
||||||
|
self._result = {} # The result should be cleared before hide, because after it is released the main thread lock
|
||||||
|
self._visible = False
|
||||||
|
try:
|
||||||
|
self._lock.release()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def hide(self):
|
||||||
|
self._visible = False
|
||||||
|
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()
|
||||||
|
def onOkButtonClicked(self):
|
||||||
|
self._view.hide()
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def onCancelButtonClicked(self):
|
||||||
|
self._result = {}
|
||||||
|
self._view.hide()
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
## Block thread until the dialog is closed.
|
||||||
|
def waitForClose(self):
|
||||||
|
if self._visible:
|
||||||
|
if threading.current_thread() != threading.main_thread():
|
||||||
|
self._lock.acquire()
|
||||||
|
self._lock.release()
|
||||||
|
else:
|
||||||
|
# If this is not run from a separate thread, we need to ensure that the events are still processed.
|
||||||
|
while self._visible:
|
||||||
|
time.sleep(1 / 50)
|
||||||
|
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||||
|
|
||||||
|
def __show(self):
|
||||||
|
if self._view is None:
|
||||||
|
self._createViewFromQML()
|
||||||
|
if self._view:
|
||||||
|
self._view.show()
|
||||||
398
plugins/3MFReader/WorkspaceDialog.qml
Normal file
398
plugins/3MFReader/WorkspaceDialog.qml
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
// Copyright (c) 2016 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.1
|
||||||
|
import QtQuick.Controls 1.1
|
||||||
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtQuick.Window 2.1
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
|
||||||
|
UM.Dialog
|
||||||
|
{
|
||||||
|
id: base
|
||||||
|
title: catalog.i18nc("@title:window", "Open Project")
|
||||||
|
|
||||||
|
minimumWidth: 500 * screenScaleFactor
|
||||||
|
minimumHeight: 450 * screenScaleFactor
|
||||||
|
width: minimumWidth
|
||||||
|
height: minimumHeight
|
||||||
|
|
||||||
|
property int comboboxHeight: 15 * screenScaleFactor
|
||||||
|
property int spacerHeight: 10 * screenScaleFactor
|
||||||
|
|
||||||
|
onClosing: manager.notifyClosed()
|
||||||
|
onVisibleChanged:
|
||||||
|
{
|
||||||
|
if(visible)
|
||||||
|
{
|
||||||
|
machineResolveComboBox.currentIndex = 0
|
||||||
|
qualityChangesResolveComboBox.currentIndex = 0
|
||||||
|
materialResolveComboBox.currentIndex = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 20 * screenScaleFactor
|
||||||
|
|
||||||
|
UM.I18nCatalog
|
||||||
|
{
|
||||||
|
id: catalog
|
||||||
|
name: "cura"
|
||||||
|
}
|
||||||
|
SystemPalette
|
||||||
|
{
|
||||||
|
id: palette
|
||||||
|
}
|
||||||
|
|
||||||
|
ListModel
|
||||||
|
{
|
||||||
|
id: resolveStrategiesModel
|
||||||
|
// Instead of directly adding the list elements, we add them afterwards.
|
||||||
|
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
||||||
|
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||||
|
Component.onCompleted:
|
||||||
|
{
|
||||||
|
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update existing")});
|
||||||
|
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 2 * screenScaleFactor
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: titleLabel
|
||||||
|
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||||
|
font.pointSize: 18
|
||||||
|
}
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: separator
|
||||||
|
color: palette.text
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
}
|
||||||
|
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
|
||||||
|
{
|
||||||
|
id: machineResolveTooltip
|
||||||
|
width: (parent.width / 3) | 0
|
||||||
|
height: visible ? comboboxHeight : 0
|
||||||
|
visible: manager.machineConflict
|
||||||
|
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||||
|
ComboBox
|
||||||
|
{
|
||||||
|
model: resolveStrategiesModel
|
||||||
|
textRole: "label"
|
||||||
|
id: machineResolveComboBox
|
||||||
|
width: parent.width
|
||||||
|
onActivated:
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("machine", resolveStrategiesModel.get(index).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Type")
|
||||||
|
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
|
||||||
|
{
|
||||||
|
model: resolveStrategiesModel
|
||||||
|
textRole: "label"
|
||||||
|
id: qualityChangesResolveComboBox
|
||||||
|
width: parent.width
|
||||||
|
onActivated:
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:label", "Name")
|
||||||
|
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
|
||||||
|
{
|
||||||
|
model: resolveStrategiesModel
|
||||||
|
textRole: "label"
|
||||||
|
id: materialResolveComboBox
|
||||||
|
width: parent.width
|
||||||
|
onActivated:
|
||||||
|
{
|
||||||
|
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater
|
||||||
|
{
|
||||||
|
model: manager.materialLabels
|
||||||
|
delegate: Row
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
id: cancel_button
|
||||||
|
text: catalog.i18nc("@action:button","Cancel");
|
||||||
|
onClicked: { manager.onCancelButtonClicked() }
|
||||||
|
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,27 +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 . import ThreeMFReader
|
from UM.Logger import Logger
|
||||||
|
try:
|
||||||
|
from . import ThreeMFReader
|
||||||
|
except ImportError:
|
||||||
|
Logger.log("w", "Could not import ThreeMFReader; libSavitar may be missing")
|
||||||
|
|
||||||
|
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():
|
def getMetaData() -> Dict:
|
||||||
return {
|
# Workarround for osx not supporting double file extensions correctly.
|
||||||
"plugin": {
|
if Platform.isOSX():
|
||||||
"name": catalog.i18nc("@label", "3MF Reader"),
|
workspace_extension = "3mf"
|
||||||
"author": "Ultimaker",
|
else:
|
||||||
"version": "1.0",
|
workspace_extension = "curaproject.3mf"
|
||||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
|
|
||||||
"api": 3
|
metaData = {}
|
||||||
},
|
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||||
"mesh_reader": [
|
metaData["mesh_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):
|
||||||
return { "mesh_reader": ThreeMFReader.ThreeMFReader() }
|
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||||
|
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
|
||||||
|
"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"
|
||||||
|
}
|
||||||
101
plugins/3MFWriter/ThreeMFWorkspaceWriter.py
Normal file
101
plugins/3MFWriter/ThreeMFWorkspaceWriter.py
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 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.Application import Application
|
||||||
|
from UM.Preferences import Preferences
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
import zipfile
|
||||||
|
from io import StringIO
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
|
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
|
||||||
|
mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter")
|
||||||
|
|
||||||
|
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
||||||
|
mesh_writer.setStoreArchive(True)
|
||||||
|
mesh_writer.write(stream, nodes, mode)
|
||||||
|
|
||||||
|
archive = mesh_writer.getArchive()
|
||||||
|
if archive is None: # This happens if there was no mesh data to write.
|
||||||
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
|
|
||||||
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
|
# Add global container stack data to the archive.
|
||||||
|
self._writeContainerToArchive(global_container_stack, archive)
|
||||||
|
|
||||||
|
# Also write all containers in the stack to the file
|
||||||
|
for container in global_container_stack.getContainers():
|
||||||
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
|
# Check if the machine has extruders and save all that data as well.
|
||||||
|
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
|
||||||
|
self._writeContainerToArchive(extruder_stack, archive)
|
||||||
|
for container in extruder_stack.getContainers():
|
||||||
|
self._writeContainerToArchive(container, archive)
|
||||||
|
|
||||||
|
# Write preferences to archive
|
||||||
|
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()
|
||||||
|
temp_preferences.writeToFile(preferences_string)
|
||||||
|
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
|
||||||
|
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.
|
||||||
|
archive.close()
|
||||||
|
mesh_writer.setStoreArchive(False)
|
||||||
|
return True
|
||||||
|
|
||||||
|
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||||
|
# \param container That follows the \type{ContainerInterface} to archive.
|
||||||
|
# \param archive The archive to write to.
|
||||||
|
@staticmethod
|
||||||
|
def _writeContainerToArchive(container, archive):
|
||||||
|
if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||||
|
return # Empty file, do nothing.
|
||||||
|
|
||||||
|
file_suffix = ContainerRegistry.getMimeTypeForContainer(type(container)).preferredSuffix
|
||||||
|
|
||||||
|
# Some containers have a base file, which should then be the file to use.
|
||||||
|
if "base_file" in container.getMetaData():
|
||||||
|
base_file = container.getMetaDataEntry("base_file")
|
||||||
|
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
|
||||||
|
|
||||||
|
file_name = "Cura/%s.%s" % (container.getId(), file_suffix)
|
||||||
|
|
||||||
|
if file_name in archive.namelist():
|
||||||
|
return # File was already saved, no need to do it again. Uranium guarantees unique ID's, so this should hold.
|
||||||
|
|
||||||
|
file_in_archive = zipfile.ZipInfo(file_name)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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)
|
||||||
174
plugins/3MFWriter/ThreeMFWriter.py
Normal file
174
plugins/3MFWriter/ThreeMFWriter.py
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
|
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Math.Matrix import Matrix
|
||||||
|
from UM.Application import Application
|
||||||
|
import UM.Scene.SceneNode
|
||||||
|
|
||||||
|
import Savitar
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
try:
|
||||||
|
if not MYPY:
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
except ImportError:
|
||||||
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
import UM.Application
|
||||||
|
|
||||||
|
|
||||||
|
class ThreeMFWriter(MeshWriter):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._namespaces = {
|
||||||
|
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
||||||
|
"content-types": "http://schemas.openxmlformats.org/package/2006/content-types",
|
||||||
|
"relationships": "http://schemas.openxmlformats.org/package/2006/relationships",
|
||||||
|
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
||||||
|
}
|
||||||
|
|
||||||
|
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||||
|
self._archive = None
|
||||||
|
self._store_archive = False
|
||||||
|
|
||||||
|
def _convertMatrixToString(self, matrix):
|
||||||
|
result = ""
|
||||||
|
result += str(matrix._data[0, 0]) + " "
|
||||||
|
result += str(matrix._data[1, 0]) + " "
|
||||||
|
result += str(matrix._data[2, 0]) + " "
|
||||||
|
result += str(matrix._data[0, 1]) + " "
|
||||||
|
result += str(matrix._data[1, 1]) + " "
|
||||||
|
result += str(matrix._data[2, 1]) + " "
|
||||||
|
result += str(matrix._data[0, 2]) + " "
|
||||||
|
result += str(matrix._data[1, 2]) + " "
|
||||||
|
result += str(matrix._data[2, 2]) + " "
|
||||||
|
result += str(matrix._data[0, 3]) + " "
|
||||||
|
result += str(matrix._data[1, 3]) + " "
|
||||||
|
result += str(matrix._data[2, 3])
|
||||||
|
return result
|
||||||
|
|
||||||
|
## Should we store the archive
|
||||||
|
# Note that if this is true, the archive will not be closed.
|
||||||
|
# The object that set this parameter is then responsible for closing it correctly!
|
||||||
|
def setStoreArchive(self, 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):
|
||||||
|
return self._archive
|
||||||
|
|
||||||
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
|
||||||
|
self._archive = None # Reset archive
|
||||||
|
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||||
|
try:
|
||||||
|
model_file = zipfile.ZipInfo("3D/3dmodel.model")
|
||||||
|
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
||||||
|
model_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
|
||||||
|
# Create content types file
|
||||||
|
content_types_file = zipfile.ZipInfo("[Content_Types].xml")
|
||||||
|
content_types_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
content_types = ET.Element("Types", xmlns = self._namespaces["content-types"])
|
||||||
|
rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml")
|
||||||
|
model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml")
|
||||||
|
|
||||||
|
# Create _rels/.rels file
|
||||||
|
relations_file = zipfile.ZipInfo("_rels/.rels")
|
||||||
|
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
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")
|
||||||
|
|
||||||
|
savitar_scene = Savitar.Scene()
|
||||||
|
transformation_matrix = Matrix()
|
||||||
|
transformation_matrix._data[1, 1] = 0
|
||||||
|
transformation_matrix._data[1, 2] = -1
|
||||||
|
transformation_matrix._data[2, 1] = 1
|
||||||
|
transformation_matrix._data[2, 2] = 0
|
||||||
|
|
||||||
|
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
|
||||||
|
# build volume.
|
||||||
|
if global_container_stack:
|
||||||
|
translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2,
|
||||||
|
y=global_container_stack.getProperty("machine_depth", "value") / 2,
|
||||||
|
z=0)
|
||||||
|
translation_matrix = Matrix()
|
||||||
|
translation_matrix.setByTranslation(translation_vector)
|
||||||
|
transformation_matrix.preMultiply(translation_matrix)
|
||||||
|
|
||||||
|
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
|
||||||
|
for node in nodes:
|
||||||
|
if node == root_node:
|
||||||
|
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)
|
||||||
|
|
||||||
|
parser = Savitar.ThreeMFParser()
|
||||||
|
scene_string = parser.sceneToString(savitar_scene)
|
||||||
|
|
||||||
|
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(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element))
|
||||||
|
except Exception as e:
|
||||||
|
Logger.logException("e", "Error writing zip file")
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
if not self._store_archive:
|
||||||
|
archive.close()
|
||||||
|
else:
|
||||||
|
self._archive = archive
|
||||||
|
|
||||||
|
return True
|
||||||
51
plugins/3MFWriter/__init__.py
Normal file
51
plugins/3MFWriter/__init__.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
|
# 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.Platform import Platform
|
||||||
|
|
||||||
|
i18n_catalog = i18nCatalog("uranium")
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
# Workarround for osx not supporting double file extensions correctly.
|
||||||
|
if Platform.isOSX():
|
||||||
|
workspace_extension = "3mf"
|
||||||
|
else:
|
||||||
|
workspace_extension = "curaproject.3mf"
|
||||||
|
|
||||||
|
metaData = {}
|
||||||
|
|
||||||
|
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||||
|
metaData["mesh_writer"] = {
|
||||||
|
"output": [{
|
||||||
|
"extension": "3mf",
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
|
||||||
|
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
|
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
metaData["workspace_writer"] = {
|
||||||
|
"output": [{
|
||||||
|
"extension": workspace_extension,
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||||
|
"mime_type": "application/x-curaproject+xml",
|
||||||
|
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
return metaData
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
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
|
||||||
|
|
@ -33,7 +31,6 @@ class ChangeLog(Extension, QObject,):
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
Preferences.getInstance().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
||||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||||
#self.showChangelog()
|
|
||||||
|
|
||||||
def getChangeLogs(self):
|
def getChangeLogs(self):
|
||||||
if not self._change_logs:
|
if not self._change_logs:
|
||||||
|
|
@ -87,6 +84,13 @@ class ChangeLog(Extension, QObject,):
|
||||||
else:
|
else:
|
||||||
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
|
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
|
||||||
|
|
||||||
|
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||||
|
|
||||||
|
# Do not show the changelog when there is no global container stack
|
||||||
|
# This implies we are running Cura for the first time.
|
||||||
|
if not Application.getInstance().getGlobalContainerStack():
|
||||||
|
return
|
||||||
|
|
||||||
if self._version > latest_version_shown:
|
if self._version > latest_version_shown:
|
||||||
self.showChangelog()
|
self.showChangelog()
|
||||||
|
|
||||||
|
|
@ -95,16 +99,11 @@ class ChangeLog(Extension, QObject,):
|
||||||
self.createChangelogWindow()
|
self.createChangelogWindow()
|
||||||
|
|
||||||
self._changelog_window.show()
|
self._changelog_window.show()
|
||||||
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
|
||||||
|
|
||||||
def hideChangelog(self):
|
def hideChangelog(self):
|
||||||
if self._changelog_window:
|
if self._changelog_window:
|
||||||
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,8 +1,8 @@
|
||||||
// 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.1
|
import QtQuick.Controls 1.3
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
import QtQuick.Window 2.1
|
import QtQuick.Window 2.1
|
||||||
|
|
||||||
|
|
@ -11,33 +11,31 @@ import UM 1.1 as UM
|
||||||
UM.Dialog
|
UM.Dialog
|
||||||
{
|
{
|
||||||
id: base
|
id: base
|
||||||
minimumWidth: 400 * Screen.devicePixelRatio
|
minimumWidth: (UM.Theme.getSize("modal_window_minimum").width * 0.75) | 0
|
||||||
minimumHeight: 300 * Screen.devicePixelRatio
|
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")
|
||||||
|
|
||||||
ScrollView
|
TextArea
|
||||||
{
|
{
|
||||||
width: parent.width
|
anchors.fill: parent
|
||||||
height: parent.height - 25
|
text: manager.getChangeLogString()
|
||||||
Label
|
readOnly: true;
|
||||||
{
|
textFormat: TextEdit.RichText
|
||||||
text: manager.getChangeLogString()
|
|
||||||
width:base.width - 35
|
|
||||||
wrapMode: Text.Wrap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Button
|
|
||||||
{
|
rightButtons: [
|
||||||
UM.I18nCatalog
|
Button
|
||||||
{
|
{
|
||||||
id: catalog
|
UM.I18nCatalog
|
||||||
name: "cura"
|
{
|
||||||
|
id: catalog
|
||||||
|
name: "cura"
|
||||||
|
}
|
||||||
|
|
||||||
|
text: catalog.i18nc("@action:button", "Close")
|
||||||
|
onClicked: base.hide()
|
||||||
}
|
}
|
||||||
anchors.bottom:parent.bottom
|
]
|
||||||
text: catalog.i18nc("@action:button", "Close")
|
|
||||||
onClicked: base.hide()
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
784
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file → Executable file
784
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file → Executable file
|
|
@ -1,3 +1,621 @@
|
||||||
|
[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.
|
||||||
|
|
||||||
|
*Lock model on platform
|
||||||
|
The move tool has a new option to lock a selected model to the platform.
|
||||||
|
|
||||||
|
*Faster profile switching speed
|
||||||
|
Duplicating and removing a profile could take Ultimaker Cura quite some time, it now happens instantly.
|
||||||
|
|
||||||
|
*Faster printer selection
|
||||||
|
Removing a printer from the library is now instant. No more unresponsive screens.
|
||||||
|
|
||||||
|
*Faster processing speed
|
||||||
|
A 5 - 10 % speed increase when calculating normals, loading models, and slicing.
|
||||||
|
|
||||||
|
*Feedrate visualization
|
||||||
|
Feedrate visualization has been added to the Layer view. Using this gives the user an idea of the print speeds per model part, allowing for better control over prints.
|
||||||
|
|
||||||
|
*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 importing legacy .ini files
|
||||||
|
- Prevent skipping user agreement dialog by pressing escape
|
||||||
|
|
||||||
|
[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
|
||||||
|
A macro can be added to your SOLIDWORKS installation that loads your model into Ultimaker Cura.
|
||||||
|
|
||||||
|
*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]
|
||||||
|
*Layer Height in Profile Selection
|
||||||
|
Added the layer height to the profile selection menu.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
Fixed the option to import g-code from related machines as a profile
|
||||||
|
Fixed a bug where editing material settings has no effect on 3D prints
|
||||||
|
Fixed an issue with automatic profile importing on Cura 2.1 on Mac OSX
|
||||||
|
Fixed an inheritance issue for dual extrusion
|
||||||
|
Fixed an issue with "i" symbol updates
|
||||||
|
Fixed a freeze that can occur while printing via Wi-Fi
|
||||||
|
|
||||||
|
[2.3.0]
|
||||||
|
*Multi Extrusion Support
|
||||||
|
Machines with multiple extruders are now supported. Ultimaker 3 printers and Ultimaker Original printers with dual extrusion upgrade kit are currently supported.
|
||||||
|
|
||||||
|
*Network Printing for Ultimaker 3
|
||||||
|
Sending a print to an Ultimaker 3 remotely via the network is now possible. Requires Wi-Fi or LAN to connect to the printer.
|
||||||
|
|
||||||
|
*Print Monitoring for Ultimaker 3
|
||||||
|
You can monitor your print on an Ultimaker 3 with a live camera feed. Requires Wi-Fi or LAN to connect to the printer.
|
||||||
|
|
||||||
|
*Material and Print Core Synchronization
|
||||||
|
Connecting to an Ultimaker 3 now gives you the option to synchronize the materials in Cura with what is loaded in the printer.
|
||||||
|
|
||||||
|
*Speed improvements
|
||||||
|
The first thing you will notice is the speed. STL loading is now 10 to 20 times faster, layer view is significantly faster and slicing speed is slightly improved.
|
||||||
|
|
||||||
|
*Improved Position Tool
|
||||||
|
Place objects precisely where you want them by manually entering the values for the position.
|
||||||
|
|
||||||
|
*Custom Machine Support
|
||||||
|
It’s now much easier to use Cura with custom machines. You can edit the machine settings when you load a new custom machine.
|
||||||
|
|
||||||
|
*Improved Grouping
|
||||||
|
It's now possible to transform objects that are already grouped.
|
||||||
|
Select an individual item in a group or merged object and edit as usual. Just Ctrl + Click and edit away.
|
||||||
|
|
||||||
|
*Enhanced Profile Management
|
||||||
|
Profile management is improved. You can now easily see and track changes made to your profiles.
|
||||||
|
|
||||||
|
*Improved Setting Visibility
|
||||||
|
Make multiple settings visible at the same time with a checkbox. The Visibility Overview setting indicates why a setting is not shown in the sidebar even if it is enabled.
|
||||||
|
|
||||||
|
*Improved time estimation
|
||||||
|
Time estimations are more accurate. Based on our test time estimations should be within 5% accuracy for Ultimaker printers.
|
||||||
|
|
||||||
|
*Optional G-code Machine Prefix
|
||||||
|
Disable the g-code prefix in Preferences. No more UM2_ on your printer display!
|
||||||
|
|
||||||
|
*Print Weight Estimates
|
||||||
|
Cura now estimates print weight as well as length.
|
||||||
|
|
||||||
|
*Automatic Import Configuration
|
||||||
|
Configurations from older installations of Cura 2.1 are automatically imported into the newest installation.
|
||||||
|
|
||||||
|
*Slicing features
|
||||||
|
*Infill Types
|
||||||
|
Two new infill types are now introduced: Tetrahedral and Cubic. They change along with the Z-axis for more uniform strength in all directions. There are now seven infill types to choose from.
|
||||||
|
|
||||||
|
*Gradual Infill
|
||||||
|
Gradual infill lets users adjust infill density, based on the distance from the top layers. This offers faster printing and reduced material requirements, whilst maintaining surface quality.
|
||||||
|
|
||||||
|
*Set Acceleration and Jerk by Feature
|
||||||
|
You can now set Jerk and Acceleration by feature-type (infill, walls, top/bottom, etc), for more precision.
|
||||||
|
|
||||||
|
*Outer Wall Offset
|
||||||
|
If your outer wall line width is smaller than your nozzle size, move the nozzle a bit inward when printing the outer wall, to improve surface quality.
|
||||||
|
|
||||||
|
*Enhanced Combing
|
||||||
|
The “No Skin” option allows you to comb over infill only to avoid scars on top surfaces.
|
||||||
|
|
||||||
|
*Z Hop
|
||||||
|
Can’t avoid previously printed parts by horizontal moves? The Z Hop Only Over Printed Parts gives you the ability to Z Hop to avoid collisions for better surface quality.
|
||||||
|
|
||||||
|
*Skin and Wall Overlap
|
||||||
|
The Skin Overlap setting allows you to overlap the skin lines with the walls for better adhesion.
|
||||||
|
|
||||||
|
*Adjust Initial Layer Travel Speed
|
||||||
|
Set the travel speed of the initial layer(s) to reduce risk of extruder pulling the print from the bed.
|
||||||
|
|
||||||
|
*Support Interface
|
||||||
|
It is now possible to print a support bottom as well as a support roof. Support bottoms are placed where the support rests on the model. Printing the support interface with PVA leads to improved surface quality.
|
||||||
|
|
||||||
|
*Bug fixes
|
||||||
|
Deleting grouped objects
|
||||||
|
Duplicating groups
|
||||||
|
Bridging
|
||||||
|
Drag and drop (first Windows run)
|
||||||
|
Unretraction speeds
|
||||||
|
Bottom layer in Spiralize mode
|
||||||
|
Overlap Compensation
|
||||||
|
Raft retractions
|
||||||
|
Retractions now occur after each object printed in one-at-a-time mode
|
||||||
|
Rafts are no longer printed outside of build area
|
||||||
|
Spiralize no longer limited to the first printed segment only
|
||||||
|
Line distance is now the actual line distance
|
||||||
|
Enabling raft doesn’t influence at which height the model is sliced any more
|
||||||
|
Brim is now always printed just once
|
||||||
|
Support roofs now only occur just below overhang
|
||||||
|
|
||||||
|
*Minor changes
|
||||||
|
Message display time increased to 30 seconds
|
||||||
|
Notification if you try to save to a locked SD card
|
||||||
|
Engine log now included in the application log
|
||||||
|
Undo and Redo now function with multiple operations
|
||||||
|
The last used folder is now remembered rather than defaulting to home folder
|
||||||
|
Import X3D files
|
||||||
|
Made it possible to add multiple Per Model Settings at once
|
||||||
|
Bed Level and Checkup procedures for UMO+ can be performed without re-adding machine
|
||||||
|
Combing applied in more cases and results in better paths
|
||||||
|
Infill thickness now supports Grid infill also for even multiples of the layer height
|
||||||
|
Support is no longer removed by unprintable thin parts of the model
|
||||||
|
Support generated on each appropriate layer
|
||||||
|
Support no longer goes outside overhang areas
|
||||||
|
Support no longer removes brim around the object
|
||||||
|
Brim is now also generated under the support
|
||||||
|
Draft and Ooze shield get their own brim or raft
|
||||||
|
Settings shared between skirt and brim now also activate when brim is selected
|
||||||
|
Compensate overlapping wall parts now also works for inner walls
|
||||||
|
Bed lowering speed can be adjusted for each layer
|
||||||
|
|
||||||
[2.1.3]
|
[2.1.3]
|
||||||
|
|
||||||
*Material Profiles
|
*Material Profiles
|
||||||
|
|
@ -22,10 +640,10 @@ The new GUI allows custom profiles to load easily and intuitively, directly from
|
||||||
*3MF File Loading Support
|
*3MF File Loading Support
|
||||||
We’re happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
|
We’re happy to report we now support loading 3MF files. This is a new file format similar to AMF, but freely available.
|
||||||
|
|
||||||
*Intuitive Cut-Off Object Bottom
|
*Intuitive Cut-Off Object Bottom
|
||||||
We’ve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
|
We’ve added a feature that allows you to move objects below the build plate. You can either correct a model with a rough bottom, or print only a part of an object. Please note that the implementation differs greatly from the old one when it was just a setting.
|
||||||
|
|
||||||
*64-bit Windows Builds
|
*64-bit Windows Builds
|
||||||
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
|
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
|
||||||
|
|
||||||
*Automatic calculations
|
*Automatic calculations
|
||||||
|
|
@ -90,3 +708,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"
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ message Slice
|
||||||
repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another
|
repeated ObjectList object_lists = 1; // The meshgroups to be printed one after another
|
||||||
SettingList global_settings = 2; // The global settings used for the whole print job
|
SettingList global_settings = 2; // The global settings used for the whole print job
|
||||||
repeated Extruder extruders = 3; // The settings sent to each extruder object
|
repeated Extruder extruders = 3; // The settings sent to each extruder object
|
||||||
repeated SettingExtruder global_inherits_stack = 4; //From which stack the setting would inherit if not defined in a stack.
|
repeated SettingExtruder limit_to_extruder = 4; // From which stack the setting would inherit if not defined per object
|
||||||
}
|
}
|
||||||
|
|
||||||
message Extruder
|
message Extruder
|
||||||
|
|
@ -56,10 +56,13 @@ message Polygon {
|
||||||
SupportInfillType = 7;
|
SupportInfillType = 7;
|
||||||
MoveCombingType = 8;
|
MoveCombingType = 8;
|
||||||
MoveRetractionType = 9;
|
MoveRetractionType = 9;
|
||||||
|
SupportInterfaceType = 10;
|
||||||
}
|
}
|
||||||
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 {
|
||||||
|
|
@ -81,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -89,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 {
|
||||||
|
|
@ -120,4 +137,4 @@ message GCodePrefix {
|
||||||
}
|
}
|
||||||
|
|
||||||
message SlicingFinished {
|
message SlicingFinished {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
438
plugins/CuraEngineBackend/CuraEngineBackend.py
Normal file → Executable file
438
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,13 +126,25 @@ 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()
|
||||||
super().close()
|
|
||||||
|
|
||||||
## Get the command that is used to call the engine.
|
## Get the command that is used to call the engine.
|
||||||
# This is useful for debugging and used to actually start the engine.
|
# This is useful for debugging and used to actually start the engine.
|
||||||
|
|
@ -148,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()
|
||||||
|
|
@ -171,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)
|
||||||
|
|
||||||
|
|
@ -184,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:
|
||||||
|
|
@ -197,7 +232,6 @@ class CuraEngineBackend(Backend):
|
||||||
Logger.log("d", "Attempting to kill the engine process")
|
Logger.log("d", "Attempting to kill the engine process")
|
||||||
|
|
||||||
if Application.getInstance().getCommandLineOption("external-backend", False):
|
if Application.getInstance().getCommandLineOption("external-backend", False):
|
||||||
self._createSocket()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._process is not None:
|
if self._process is not None:
|
||||||
|
|
@ -206,6 +240,7 @@ class CuraEngineBackend(Backend):
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
|
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
|
||||||
self._process = None
|
self._process = None
|
||||||
|
|
||||||
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))
|
||||||
|
|
||||||
|
|
@ -218,6 +253,9 @@ class CuraEngineBackend(Backend):
|
||||||
#
|
#
|
||||||
# \param job The start slice job that was just finished.
|
# \param job The start slice job that was just finished.
|
||||||
def _onStartSliceCompleted(self, job):
|
def _onStartSliceCompleted(self, job):
|
||||||
|
if self._error_message:
|
||||||
|
self._error_message.hide()
|
||||||
|
|
||||||
# Note that cancelled slice jobs can still call this method.
|
# Note that cancelled slice jobs can still call this method.
|
||||||
if self._start_slice_job is job:
|
if self._start_slice_job is job:
|
||||||
self._start_slice_job = None
|
self._start_slice_job = None
|
||||||
|
|
@ -225,28 +263,117 @@ class CuraEngineBackend(Backend):
|
||||||
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
||||||
if Application.getInstance().getPlatformActivity:
|
if Application.getInstance().platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
|
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
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
|
||||||
|
|
||||||
|
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
||||||
|
if Application.getInstance().platformActivity:
|
||||||
|
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||||
|
error_keys = []
|
||||||
|
for extruder in extruders:
|
||||||
|
error_keys.extend(extruder.getErrorKeys())
|
||||||
|
if not extruders:
|
||||||
|
error_keys = self._global_container_stack.getErrorKeys()
|
||||||
|
error_labels = set()
|
||||||
|
for key in error_keys:
|
||||||
|
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)
|
||||||
|
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.backendStateChange.emit(BackendState.Error)
|
||||||
|
else:
|
||||||
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
|
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 Application.getInstance().platformActivity:
|
||||||
|
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.backendStateChange.emit(BackendState.Error)
|
||||||
|
else:
|
||||||
|
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", "Unable to slice. No suitable models found."))
|
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:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.backendStateChange.emit(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Preparation completed, send it to the backend.
|
# Preparation completed, send it to the backend.
|
||||||
self._socket.sendMessage(job.getSliceMessage())
|
self._socket.sendMessage(job.getSliceMessage())
|
||||||
|
|
||||||
|
# Notify the user that it's now up to the backend to do it's job
|
||||||
|
self.backendStateChange.emit(BackendState.Processing)
|
||||||
|
|
||||||
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.
|
||||||
|
|
@ -256,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
|
||||||
return
|
if source == self._scene.getRoot():
|
||||||
|
num_objects = 0
|
||||||
if source.getMeshData() is None:
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
return
|
# Only count sliceable objects
|
||||||
|
if node.callDecoration("isSliceable"):
|
||||||
if source.getMeshData().getVertices() is None:
|
num_objects += 1
|
||||||
|
if num_objects != self._last_num_objects:
|
||||||
|
self._last_num_objects = num_objects
|
||||||
|
root_scene_nodes_changed = True
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
|
||||||
|
if source.getMeshData() is None:
|
||||||
|
return
|
||||||
|
if source.getMeshData().getVertices() is None:
|
||||||
|
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
|
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.
|
||||||
|
|
@ -279,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("e", "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.
|
||||||
|
|
@ -317,10 +491,21 @@ 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)
|
||||||
|
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||||
self._process_layers_job.start()
|
self._process_layers_job.start()
|
||||||
self._stored_optimized_layer_data = []
|
self._stored_optimized_layer_data = []
|
||||||
|
|
||||||
|
|
@ -337,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._change_timer.start()
|
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()
|
||||||
|
|
||||||
|
## 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.
|
||||||
#
|
#
|
||||||
|
|
@ -374,8 +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._terminate() # Do not continue slicing once a tool has started
|
self._tool_active = True # Do not react on scene change
|
||||||
self._enabled = False # Do not reslice when a tool is doing it's 'thing'
|
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.
|
||||||
#
|
#
|
||||||
|
|
@ -383,18 +597,24 @@ 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.
|
||||||
if self._stored_optimized_layer_data and not self._slicing:
|
if self._stored_optimized_layer_data and not self._slicing:
|
||||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||||
|
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
|
||||||
self._process_layers_job.start()
|
self._process_layers_job.start()
|
||||||
self._stored_optimized_layer_data = []
|
self._stored_optimized_layer_data = []
|
||||||
else:
|
else:
|
||||||
|
|
@ -404,10 +624,10 @@ class CuraEngineBackend(Backend):
|
||||||
#
|
#
|
||||||
# We should reset our state and start listening for new connections.
|
# We should reset our state and start listening for new connections.
|
||||||
def _onBackendQuit(self):
|
def _onBackendQuit(self):
|
||||||
if not self._restart and self._process:
|
if not self._restart:
|
||||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
if self._process:
|
||||||
self._process = None
|
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
||||||
self._createSocket()
|
self._process = None
|
||||||
|
|
||||||
## Called when the global container stack changes
|
## Called when the global container stack changes
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self):
|
||||||
|
|
@ -415,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()
|
||||||
|
|
||||||
|
|
@ -425,24 +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)
|
extruder.containersChanged.connect(self._onChanged)
|
||||||
self._onActiveExtruderChanged()
|
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
def _onActiveExtruderChanged(self):
|
def _onProcessLayersFinished(self, job):
|
||||||
if self._global_container_stack:
|
self._process_layers_job = None
|
||||||
# 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()
|
## Connect slice function to timer.
|
||||||
if self._active_extruder_stack:
|
def enableTimer(self):
|
||||||
self._active_extruder_stack.containersChanged.connect(self._onChanged)
|
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,11 +1,15 @@
|
||||||
# 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
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.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
|
||||||
|
|
@ -13,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.
|
||||||
|
|
@ -41,13 +61,14 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
start_time = time()
|
start_time = time()
|
||||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
view = Application.getInstance().getController().getActiveView()
|
||||||
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
if view.getPluginId() == "SimulationView":
|
||||||
self._progress.show()
|
view.resetLayerData()
|
||||||
|
self._progress_message.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)
|
||||||
|
|
@ -60,26 +81,38 @@ 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.
|
||||||
|
# For some reason, Python has a tendency to keep the layer data
|
||||||
|
# in memory longer than needed. Forcing the GC to run here makes
|
||||||
|
# sure any old layer data is really cleaned up before adding new.
|
||||||
|
gc.collect()
|
||||||
|
|
||||||
mesh = MeshData()
|
mesh = MeshData()
|
||||||
layer_data = LayerDataBuilder.LayerDataBuilder()
|
layer_data = LayerDataBuilder.LayerDataBuilder()
|
||||||
layer_count = len(self._layers)
|
layer_count = len(self._layers)
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
@ -102,23 +135,29 @@ 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.
|
||||||
new_points = numpy.empty((len(points), 3), numpy.float32)
|
new_points = numpy.empty((len(points), 3), numpy.float32)
|
||||||
if polygon.point_type == 0: # Point2D
|
if polygon.point_type == 0: # Point2D
|
||||||
new_points[:, 0] = points[:, 0]
|
new_points[:, 0] = points[:, 0]
|
||||||
new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation
|
new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation
|
||||||
new_points[:, 2] = -points[:, 1]
|
new_points[:, 2] = -points[:, 1]
|
||||||
else: # Point3D
|
else: # Point3D
|
||||||
new_points[:, 0] = points[:, 0]
|
new_points[:, 0] = points[:, 0]
|
||||||
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)
|
||||||
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
@ -129,18 +168,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
|
||||||
|
|
@ -158,15 +225,11 @@ 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()
|
if self._progress_message:
|
||||||
if view.getPluginId() == "LayerView":
|
self._progress_message.hide()
|
||||||
view.resetLayerData()
|
|
||||||
|
|
||||||
if self._progress:
|
|
||||||
self._progress.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
|
||||||
|
|
@ -175,11 +238,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,14 +17,16 @@ 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
|
||||||
Error = 2
|
Error = 2
|
||||||
SettingError = 3
|
SettingError = 3
|
||||||
NothingToSlice = 4
|
NothingToSlice = 4
|
||||||
|
MaterialIncompatible = 5
|
||||||
|
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
|
||||||
|
|
@ -74,17 +77,28 @@ class StartSliceJob(Job):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Don't slice if there is a setting with an error value.
|
# Don't slice if there is a setting with an error value.
|
||||||
if not Application.getInstance().getMachineManager().isActiveStackValid:
|
if Application.getInstance().getMachineManager().stacksHaveErrors:
|
||||||
self.setResult(StartJobResult.SettingError)
|
self.setResult(StartJobResult.SettingError)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if Application.getInstance().getBuildVolume().hasErrors():
|
||||||
|
self.setResult(StartJobResult.BuildPlateError)
|
||||||
|
return
|
||||||
|
|
||||||
|
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||||
|
material = extruder_stack.findContainer({"type": "material"})
|
||||||
|
if material:
|
||||||
|
if material.getMetaDataEntry("compatible") == False:
|
||||||
|
self.setResult(StartJobResult.MaterialIncompatible)
|
||||||
|
return
|
||||||
|
|
||||||
# Don't slice if there is a per object setting with an error value.
|
# Don't slice if there is a per object setting with an error value.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if type(node) is not SceneNode or not node.isSelectable():
|
if type(node) is not SceneNode or not node.isSelectable():
|
||||||
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():
|
||||||
|
|
@ -119,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()
|
||||||
|
|
||||||
|
|
@ -136,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:
|
||||||
|
|
@ -144,17 +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 = numpy.array(mesh_data.getVertices())
|
verts = verts.dot(rot_scale)
|
||||||
|
verts += translate
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
|
|
@ -169,30 +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.
|
||||||
|
if not stack.getProperty(key, "settable_per_extruder"):
|
||||||
|
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.
|
||||||
|
|
@ -200,71 +258,90 @@ 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:
|
|
||||||
settings[key] = stack.getProperty(key, "value")
|
|
||||||
|
|
||||||
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 = str(value).encode("utf-8")
|
||||||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
Job.yieldThread()
|
||||||
else:
|
|
||||||
setting_message.value = str(value).encode("utf-8")
|
|
||||||
|
|
||||||
## Sends for some settings which extruder they should fallback to if not
|
## Sends for some settings which extruder they should fallback to if not
|
||||||
# set.
|
# set.
|
||||||
#
|
#
|
||||||
# This is only set for settings that have the global_inherits_stack
|
# This is only set for settings that have the limit_to_extruder
|
||||||
# property.
|
# property.
|
||||||
#
|
#
|
||||||
# \param stack The global stack with all settings, from which to read the
|
# \param stack The global stack with all settings, from which to read the
|
||||||
# global_inherits_stack property.
|
# limit_to_extruder property.
|
||||||
def _buildGlobalInheritsStackMessage(self, stack):
|
def _buildGlobalInheritsStackMessage(self, stack):
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
extruder = int(round(float(stack.getProperty(key, "global_inherits_stack"))))
|
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||||
if extruder >= 0: #Set to a specific extruder.
|
if extruder >= 0: #Set to a specific extruder.
|
||||||
setting_extruder = self._slice_message.addRepeatedMessage("global_inherits_stack")
|
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
|
||||||
setting_extruder.name = key
|
setting_extruder.name = key
|
||||||
setting_extruder.extruder = extruder
|
setting_extruder.extruder = extruder
|
||||||
|
Job.yieldThread()
|
||||||
|
|
||||||
## Check if a node has per object settings and ensure that they are set correctly in the message
|
## Check if a node has per object settings and ensure that they are set correctly in the message
|
||||||
# \param node \type{SceneNode} Node to check.
|
# \param node \type{SceneNode} Node to check.
|
||||||
# \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():
|
|
||||||
instance = stack.getTop().getInstance(key)
|
|
||||||
self._addRelations(changed_setting_keys, instance.definition.relations)
|
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
# Ensure that the engine is aware what the build extruder is
|
# Check all settings for relations, so we can also calculate the correct values for dependent settings.
|
||||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
top_of_stack = stack.getTop() # Cache for efficiency.
|
||||||
changed_setting_keys.add("extruder_nr")
|
changed_setting_keys = set(top_of_stack.getAllKeys())
|
||||||
|
|
||||||
# Get values for all changed settings
|
# Add all relations to changed settings as well.
|
||||||
for key in changed_setting_keys:
|
for key in top_of_stack.getAllKeys():
|
||||||
setting = message.addRepeatedMessage("settings")
|
instance = top_of_stack.getInstance(key)
|
||||||
setting.name = key
|
self._addRelations(changed_setting_keys, instance.definition.relations)
|
||||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
Job.yieldThread()
|
||||||
Job.yieldThread()
|
|
||||||
|
|
||||||
## Recursive function to put all settings that require eachother for value changes in a list
|
# Ensure that the engine is aware what the build extruder is.
|
||||||
|
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
|
changed_setting_keys.add("extruder_nr")
|
||||||
|
|
||||||
|
# Get values for all changed settings
|
||||||
|
for key in changed_setting_keys:
|
||||||
|
setting = message.addRepeatedMessage("settings")
|
||||||
|
setting.name = key
|
||||||
|
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()
|
||||||
|
|
||||||
|
## 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,12 +1,14 @@
|
||||||
# Copyright (c) 2015 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 os.path
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
import zipfile
|
||||||
|
|
||||||
## A plugin that reads profile data from Cura profile files.
|
## A plugin that reads profile data from Cura profile files.
|
||||||
#
|
#
|
||||||
# It reads a profile from a .curaprofile file, and returns it as a profile
|
# It reads a profile from a .curaprofile file, and returns it as a profile
|
||||||
|
|
@ -24,19 +26,79 @@ class CuraProfileReader(ProfileReader):
|
||||||
# not be read or didn't contain a valid profile, \code None \endcode is
|
# not be read or didn't contain a valid profile, \code None \endcode is
|
||||||
# returned.
|
# returned.
|
||||||
def read(self, file_name):
|
def read(self, file_name):
|
||||||
# Create an empty profile.
|
|
||||||
profile = InstanceContainer(os.path.basename(os.path.splitext(file_name)[0]))
|
|
||||||
profile.addMetaDataEntry("type", "quality")
|
|
||||||
try:
|
try:
|
||||||
with open(file_name) as f: # Open file for reading.
|
with zipfile.ZipFile(file_name, "r") as archive:
|
||||||
serialized = f.read()
|
results = []
|
||||||
except IOError as e:
|
for profile_id in archive.namelist():
|
||||||
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
|
with archive.open(profile_id) as f:
|
||||||
return None
|
serialized = f.read()
|
||||||
|
profile = self._loadProfile(serialized.decode("utf-8"), profile_id)
|
||||||
|
if profile is not None:
|
||||||
|
results.append(profile)
|
||||||
|
return results
|
||||||
|
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
# It must be an older profile from Cura 2.1.
|
||||||
|
with open(file_name, encoding="utf-8") as fhandle:
|
||||||
|
serialized = fhandle.read()
|
||||||
|
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
|
||||||
|
|
||||||
|
## Convert a profile from an old Cura to this Cura if needed.
|
||||||
|
#
|
||||||
|
# \param serialized \type{str} The profile data to convert in the serialized on-disk format.
|
||||||
|
# \param profile_id \type{str} The name of the profile.
|
||||||
|
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
|
||||||
|
def _upgradeProfile(self, serialized, profile_id):
|
||||||
|
parser = configparser.ConfigParser(interpolation=None)
|
||||||
|
parser.read_string(serialized)
|
||||||
|
|
||||||
|
if not "general" in parser:
|
||||||
|
Logger.log("w", "Missing required section 'general'.")
|
||||||
|
return []
|
||||||
|
if not "version" in parser["general"]:
|
||||||
|
Logger.log("w", "Missing required 'version' property")
|
||||||
|
return []
|
||||||
|
|
||||||
|
version = int(parser["general"]["version"])
|
||||||
|
if InstanceContainer.Version != version:
|
||||||
|
name = parser["general"]["name"]
|
||||||
|
return self._upgradeProfileVersion(serialized, name, version)
|
||||||
|
else:
|
||||||
|
return [(serialized, profile_id)]
|
||||||
|
|
||||||
|
## Load a profile from a serialized string.
|
||||||
|
#
|
||||||
|
# \param serialized \type{str} The profile data to read.
|
||||||
|
# \param profile_id \type{str} The name of the profile.
|
||||||
|
# \return \type{InstanceContainer|None}
|
||||||
|
def _loadProfile(self, serialized, profile_id):
|
||||||
|
# Create an empty profile.
|
||||||
|
profile = InstanceContainer(profile_id)
|
||||||
|
profile.addMetaDataEntry("type", "quality_changes")
|
||||||
try:
|
try:
|
||||||
profile.deserialize(serialized)
|
profile.deserialize(serialized)
|
||||||
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
|
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
|
||||||
Logger.log("e", "Error while trying to parse profile: %s", str(e))
|
Logger.log("e", "Error while trying to parse profile: %s", str(e))
|
||||||
return None
|
return None
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
|
## Upgrade a serialized profile to the current profile format.
|
||||||
|
#
|
||||||
|
# \param serialized \type{str} The profile data to convert.
|
||||||
|
# \param profile_id \type{str} The name of the profile.
|
||||||
|
# \param source_version \type{int} The profile version of 'serialized'.
|
||||||
|
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
|
||||||
|
def _upgradeProfileVersion(self, serialized, profile_id, source_version):
|
||||||
|
converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter={"version_upgrade": {} }, active_only=True)
|
||||||
|
|
||||||
|
source_format = ("profile", source_version)
|
||||||
|
profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins
|
||||||
|
if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version]
|
||||||
|
|
||||||
|
if not profile_convert_funcs:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filenames, outputs = profile_convert_funcs[0](serialized, profile_id)
|
||||||
|
if filenames is None and outputs is None:
|
||||||
|
return []
|
||||||
|
return list(zip(outputs, filenames))
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
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