mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-11-02 20:52:20 -07:00
Merge branch 'master' into feature_machinesettings
This commit is contained in:
commit
7d37cbd5d2
383 changed files with 295204 additions and 0 deletions
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Compiled and generated things.
|
||||
build
|
||||
*.pyc
|
||||
__pycache__
|
||||
*.mo
|
||||
docs/html
|
||||
*.log
|
||||
resources/i18n/en
|
||||
resources/i18n/x-test
|
||||
resources/firmware
|
||||
|
||||
# Editors and IDEs.
|
||||
*kdev*
|
||||
*.kate-swp
|
||||
*.lprof
|
||||
*~
|
||||
*.qm
|
||||
.idea
|
||||
|
||||
# Eclipse+PyDev
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
# Debian packaging
|
||||
debian*
|
||||
173
CHANGES
Normal file
173
CHANGES
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
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.
|
||||
96
CMakeLists.txt
Normal file
96
CMakeLists.txt
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
|
||||
project(cura)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(URANIUM_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/../uranium/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
|
||||
# Tests
|
||||
# Note that we use exit 0 here to not mark the build as a failure on test failure
|
||||
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)
|
||||
|
||||
|
||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||
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(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||
|
||||
# Macro needed to list all sub-directory of a directory.
|
||||
# There is no function in cmake as far as I know.
|
||||
# Found at: http://stackoverflow.com/a/7788165
|
||||
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 "")
|
||||
# Extract Strings
|
||||
add_custom_target(extract-messages ${URANIUM_SCRIPTS_DIR}/extract-messages ${CMAKE_SOURCE_DIR} cura)
|
||||
|
||||
# Build Translations
|
||||
find_package(Gettext)
|
||||
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()
|
||||
|
||||
find_package(PythonInterp 3.4.0 REQUIRED)
|
||||
|
||||
install(DIRECTORY resources
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||
install(DIRECTORY plugins
|
||||
DESTINATION lib/cura)
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
install(FILES cura_app.py
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
|
||||
RENAME cura)
|
||||
install(DIRECTORY cura
|
||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages
|
||||
FILES_MATCHING PATTERN *.py)
|
||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES cura.sharedmimeinfo
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
|
||||
RENAME cura.xml )
|
||||
else()
|
||||
install(FILES cura_app.py
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
install(DIRECTORY cura
|
||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
||||
FILES_MATCHING PATTERN *.py)
|
||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
||||
endif()
|
||||
|
||||
include(CPackConfig.cmake)
|
||||
16
CPackConfig.cmake
Normal file
16
CPackConfig.cmake
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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)
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
|
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
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/>.
|
||||
63
README.md
Normal file
63
README.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
Cura
|
||||
====
|
||||
|
||||
This is the new, shiny frontend for Cura. [daid/Cura](https://github.com/daid/Cura.git) is the old legacy Cura that everyone knows and loves/hates.
|
||||
|
||||
We re-worked the whole GUI code at Ultimaker, because the old code started to become a unmaintainable.
|
||||
|
||||
|
||||
Logging Issues
|
||||
------------
|
||||
Use [this](https://github.com/Ultimaker/Uranium/wiki/Bug-Reporting-Template) template to report issues. New issues that do not adhere to this template will take us a lot longer to handle and will therefore have a lower pirority.
|
||||
|
||||
For crashes and similar issues, please attach the following information:
|
||||
|
||||
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
||||
* The Cura GUI log file, located at
|
||||
* $User/AppData/Local/cura/cura.log (Windows)
|
||||
* $User/Library/Application Support/cura (OSX)
|
||||
* $USER/.local/share/cura (Ubuntu/Linux)
|
||||
* The Cura Engine log, using Help -> Show Engine Log
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
* [Uranium](https://github.com/Ultimaker/Uranium)
|
||||
Cura is built on top of the Uranium framework.
|
||||
* [CuraEngine](https://github.com/Ultimaker/CuraEngine)
|
||||
This will be needed at runtime to perform the actual slicing.
|
||||
* [PySerial](https://github.com/pyserial/pyserial)
|
||||
Only required for USB printing support.
|
||||
|
||||
Configuring Cura
|
||||
----------------
|
||||
* Link your CuraEngine backend by inserting the following line in home/.config/cura/config.cfg :
|
||||
[backend]
|
||||
location = /[path_to_the..]/CuraEngine/build/CuraEngine
|
||||
|
||||
Build scripts
|
||||
-------------
|
||||
|
||||
Please checkout [cura-build](https://github.com/Ultimaker/cura-build)
|
||||
|
||||
Third party plugins
|
||||
-------------
|
||||
* [Print time calculator](https://github.com/nallath/PrintCostCalculator)
|
||||
* [Post processing plugin](https://github.com/nallath/PostProcessingPlugin)
|
||||
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin) Simple scale tool for imperial to metric.
|
||||
|
||||
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.
|
||||
|
||||
* Change the machine ID to something unique
|
||||
* Change the machine_name to your printer's name
|
||||
* If you have a 3D model of your platform you can put it in resources/meshes and put its name under platform
|
||||
* 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.
|
||||
* 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
|
||||
* 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.
|
||||
34
build.sh
Normal file
34
build.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
set -e
|
||||
set -u
|
||||
|
||||
export OLD_PWD=`pwd`
|
||||
export CMAKE=/c/software/PCL/cmake-3.0.1-win32-x86/bin/cmake.exe
|
||||
export MAKE=mingw32-make.exe
|
||||
export PATH=/c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin:$PATH
|
||||
|
||||
mkdir -p /c/software/protobuf/_build
|
||||
cd /c/software/protobuf/_build
|
||||
$CMAKE ../
|
||||
$MAKE install
|
||||
|
||||
mkdir -p /c/software/libArcus/_build
|
||||
cd /c/software/libArcus/_build
|
||||
$CMAKE ../
|
||||
$MAKE install
|
||||
|
||||
mkdir -p /c/software/PinkUnicornEngine/_build
|
||||
cd /c/software/PinkUnicornEngine/_build
|
||||
$CMAKE ../
|
||||
$MAKE
|
||||
|
||||
cd $OLD_PWD
|
||||
export PYTHONPATH=`pwd`/../libArcus/python:/c/Software/Uranium/
|
||||
/c/python34/python setup.py py2exe
|
||||
|
||||
cp /c/software/PinkUnicornEngine/_build/CuraEngine.exe dist/
|
||||
cp /c/software/libArcus/_install/bin/libArcus.dll dist/
|
||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libgcc_s_dw2-1.dll dist/
|
||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libwinpthread-1.dll dist/
|
||||
cp /c/mingw-w64/i686-4.9.2-posix-dwarf-rt_v3-rev1/mingw32/bin/libstdc++-6.dll dist/
|
||||
|
||||
/c/program\ files\ \(x86\)/NSIS/makensis.exe installer.nsi
|
||||
15
cura.desktop.in
Normal file
15
cura.desktop.in
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[Desktop Entry]
|
||||
Version=1
|
||||
Name=Cura
|
||||
Name[de]=Cura
|
||||
GenericName=3D Printing 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.
|
||||
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
||||
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=@CMAKE_INSTALL_FULL_DATADIR@/cura/resources/images/cura-icon.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png
|
||||
Categories=Graphics;
|
||||
Keywords=3D;Printing;
|
||||
22
cura.sharedmimeinfo
Normal file
22
cura.sharedmimeinfo
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/vnd.ms-3mfdocument">
|
||||
<comment>3D Manufacturing Format Document</comment>
|
||||
<icon name="unknown"/>
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.3mf"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/sla">
|
||||
<comment>Computer-aided design and manufacturing format</comment>
|
||||
<icon name="unknown"/>
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.stl"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/prs.wavefront-obj">
|
||||
<sub-class-of type="text/plain"/>
|
||||
<comment>Wavefront 3D Object file</comment>
|
||||
<icon name="unknown"/>
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.obj"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
380
cura/BuildVolume.py
Normal file
380
cura/BuildVolume.py
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Scene.Platform import Platform
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Color import Color
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Polygon import Polygon
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import numpy
|
||||
import copy
|
||||
|
||||
|
||||
# Setting for clearance around the prime
|
||||
PRIME_CLEARANCE = 10
|
||||
|
||||
|
||||
def approximatedCircleVertices(r):
|
||||
"""
|
||||
Return vertices from an approximated circle.
|
||||
:param r: radius
|
||||
:return: numpy 2-array with the vertices
|
||||
"""
|
||||
|
||||
return numpy.array([
|
||||
[-r, 0],
|
||||
[-r * 0.707, r * 0.707],
|
||||
[0, r],
|
||||
[r * 0.707, r * 0.707],
|
||||
[r, 0],
|
||||
[r * 0.707, -r * 0.707],
|
||||
[0, -r],
|
||||
[-r * 0.707, -r * 0.707]
|
||||
], numpy.float32)
|
||||
|
||||
|
||||
## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
|
||||
class BuildVolume(SceneNode):
|
||||
VolumeOutlineColor = Color(12, 169, 227, 255)
|
||||
|
||||
raftThicknessChanged = Signal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._width = 0
|
||||
self._height = 0
|
||||
self._depth = 0
|
||||
|
||||
self._shader = None
|
||||
|
||||
self._grid_mesh = None
|
||||
self._grid_shader = None
|
||||
|
||||
self._disallowed_areas = []
|
||||
self._disallowed_area_mesh = None
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
self._volume_aabb = None
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
self._adhesion_type = None
|
||||
self._platform = Platform(self)
|
||||
|
||||
self._active_container_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
|
||||
self._onGlobalContainerStackChanged()
|
||||
|
||||
def setWidth(self, width):
|
||||
if width: self._width = width
|
||||
|
||||
def setHeight(self, height):
|
||||
if height: self._height = height
|
||||
|
||||
def setDepth(self, depth):
|
||||
if depth: self._depth = depth
|
||||
|
||||
def getDisallowedAreas(self):
|
||||
return self._disallowed_areas
|
||||
|
||||
def setDisallowedAreas(self, areas):
|
||||
self._disallowed_areas = areas
|
||||
|
||||
def render(self, renderer):
|
||||
if not self.getMeshData():
|
||||
return True
|
||||
|
||||
if not self._shader:
|
||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
|
||||
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
||||
|
||||
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
||||
renderer.queueNode(self, mesh = self._grid_mesh, shader = self._grid_shader, backface_cull = True)
|
||||
if self._disallowed_area_mesh:
|
||||
renderer.queueNode(self, mesh = self._disallowed_area_mesh, shader = self._shader, transparent = True, backface_cull = True, sort = -9)
|
||||
|
||||
return True
|
||||
|
||||
## Recalculates the build volume & disallowed areas.
|
||||
def rebuild(self):
|
||||
if not self._width or not self._height or not self._depth:
|
||||
return
|
||||
|
||||
min_w = -self._width / 2
|
||||
max_w = self._width / 2
|
||||
min_h = 0.0
|
||||
max_h = self._height
|
||||
min_d = -self._depth / 2
|
||||
max_d = self._depth / 2
|
||||
|
||||
mb = MeshBuilder()
|
||||
|
||||
# Outline 'cube' of the build volume
|
||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
|
||||
|
||||
mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
||||
|
||||
mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
|
||||
mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
|
||||
|
||||
self.setMeshData(mb.build())
|
||||
|
||||
mb = MeshBuilder()
|
||||
mb.addQuad(
|
||||
Vector(min_w, min_h - 0.2, min_d),
|
||||
Vector(max_w, min_h - 0.2, min_d),
|
||||
Vector(max_w, min_h - 0.2, max_d),
|
||||
Vector(min_w, min_h - 0.2, max_d)
|
||||
)
|
||||
|
||||
for n in range(0, 6):
|
||||
v = mb.getVertex(n)
|
||||
mb.setVertexUVCoordinates(n, v[0], v[2])
|
||||
self._grid_mesh = mb.build()
|
||||
|
||||
disallowed_area_height = 0.1
|
||||
disallowed_area_size = 0
|
||||
if self._disallowed_areas:
|
||||
mb = MeshBuilder()
|
||||
color = Color(0.0, 0.0, 0.0, 0.15)
|
||||
for polygon in self._disallowed_areas:
|
||||
points = polygon.getPoints()
|
||||
first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
||||
previous_point = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height, self._clamp(points[0][1], min_d, max_d))
|
||||
for point in points:
|
||||
new_point = Vector(self._clamp(point[0], min_w, max_w), disallowed_area_height, self._clamp(point[1], min_d, max_d))
|
||||
mb.addFace(first, previous_point, new_point, color = color)
|
||||
previous_point = new_point
|
||||
|
||||
# Find the largest disallowed area to exclude it from the maximum scale bounds.
|
||||
# This is a very nasty hack. This pretty much only works for UM machines.
|
||||
# This disallowed area_size needs a -lot- of rework at some point in the future: TODO
|
||||
if numpy.min(points[:, 1]) >= 0: # This filters out all areas that have points to the left of the centre. This is done to filter the skirt area.
|
||||
size = abs(numpy.max(points[:, 1]) - numpy.min(points[:, 1]))
|
||||
else:
|
||||
size = 0
|
||||
disallowed_area_size = max(size, disallowed_area_size)
|
||||
|
||||
self._disallowed_area_mesh = mb.build()
|
||||
else:
|
||||
self._disallowed_area_mesh = None
|
||||
|
||||
self._volume_aabb = AxisAlignedBox(
|
||||
minimum = Vector(min_w, min_h - 1.0, min_d),
|
||||
maximum = Vector(max_w, max_h - self._raft_thickness, max_d))
|
||||
|
||||
bed_adhesion_size = 0.0
|
||||
|
||||
container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if container_stack:
|
||||
bed_adhesion_size = self._getBedAdhesionSize(container_stack)
|
||||
|
||||
# As this works better for UM machines, we only add the disallowed_area_size for the z direction.
|
||||
# This is probably wrong in all other cases. TODO!
|
||||
# The +1 and -1 is added as there is always a bit of extra room required to work properly.
|
||||
scale_to_max_bounds = AxisAlignedBox(
|
||||
minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + disallowed_area_size - bed_adhesion_size + 1),
|
||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||
)
|
||||
|
||||
Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||
|
||||
def getBoundingBox(self):
|
||||
return self._volume_aabb
|
||||
|
||||
def _buildVolumeMessage(self):
|
||||
Message(catalog.i18nc(
|
||||
"@info:status",
|
||||
"The build volume height has been reduced due to the value of the"
|
||||
" \"Print Sequence\" setting to prevent the gantry from colliding"
|
||||
" with printed models."), lifetime=10).show()
|
||||
|
||||
def getRaftThickness(self):
|
||||
return self._raft_thickness
|
||||
|
||||
def _updateRaftThickness(self):
|
||||
old_raft_thickness = self._raft_thickness
|
||||
self._adhesion_type = self._active_container_stack.getProperty("adhesion_type", "value")
|
||||
self._raft_thickness = 0.0
|
||||
if self._adhesion_type == "raft":
|
||||
self._raft_thickness = (
|
||||
self._active_container_stack.getProperty("raft_base_thickness", "value") +
|
||||
self._active_container_stack.getProperty("raft_interface_thickness", "value") +
|
||||
self._active_container_stack.getProperty("raft_surface_layers", "value") *
|
||||
self._active_container_stack.getProperty("raft_surface_thickness", "value") +
|
||||
self._active_container_stack.getProperty("raft_airgap", "value"))
|
||||
|
||||
# Rounding errors do not matter, we check if raft_thickness has changed at all
|
||||
if old_raft_thickness != self._raft_thickness:
|
||||
self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
|
||||
self.raftThicknessChanged.emit()
|
||||
|
||||
def _onGlobalContainerStackChanged(self):
|
||||
if self._active_container_stack:
|
||||
self._active_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
|
||||
self._active_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._active_container_stack:
|
||||
self._active_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
|
||||
self._width = self._active_container_stack.getProperty("machine_width", "value")
|
||||
if self._active_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
self._height = self._active_container_stack.getProperty("gantry_height", "value")
|
||||
self._buildVolumeMessage()
|
||||
else:
|
||||
self._height = self._active_container_stack.getProperty("machine_height", "value")
|
||||
self._depth = self._active_container_stack.getProperty("machine_depth", "value")
|
||||
|
||||
self._updateDisallowedAreas()
|
||||
self._updateRaftThickness()
|
||||
|
||||
self.rebuild()
|
||||
|
||||
def _onSettingPropertyChanged(self, setting_key, property_name):
|
||||
if property_name != "value":
|
||||
return
|
||||
|
||||
rebuild_me = False
|
||||
if setting_key == "print_sequence":
|
||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
self._height = self._active_container_stack.getProperty("gantry_height", "value")
|
||||
self._buildVolumeMessage()
|
||||
else:
|
||||
self._height = self._active_container_stack.getProperty("machine_height", "value")
|
||||
rebuild_me = True
|
||||
|
||||
if setting_key in self._skirt_settings or setting_key in self._prime_settings:
|
||||
self._updateDisallowedAreas()
|
||||
rebuild_me = True
|
||||
|
||||
if setting_key in self._raft_settings:
|
||||
self._updateRaftThickness()
|
||||
rebuild_me = True
|
||||
|
||||
if rebuild_me:
|
||||
self.rebuild()
|
||||
|
||||
def _updateDisallowedAreas(self):
|
||||
if not self._active_container_stack:
|
||||
return
|
||||
|
||||
disallowed_areas = copy.deepcopy(
|
||||
self._active_container_stack.getProperty("machine_disallowed_areas", "value"))
|
||||
areas = []
|
||||
|
||||
# Add extruder prime locations as disallowed areas.
|
||||
# Probably needs some rework after coordinate system change.
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
extruders = extruder_manager.getMachineExtruders(self._active_container_stack.getId())
|
||||
machine_width = self._active_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._active_container_stack.getProperty("machine_depth", "value")
|
||||
for single_extruder in extruders:
|
||||
extruder_prime_pos_x = single_extruder.getProperty("extruder_prime_pos_x", "value")
|
||||
extruder_prime_pos_y = single_extruder.getProperty("extruder_prime_pos_y", "value")
|
||||
# TODO: calculate everything in CuraEngine/Firmware/lower left as origin coordinates.
|
||||
# Here we transform the extruder prime pos (lower left as origin) to Cura coordinates
|
||||
# (center as origin, y from back to front)
|
||||
prime_x = extruder_prime_pos_x - machine_width / 2
|
||||
prime_y = machine_depth / 2 - extruder_prime_pos_y
|
||||
disallowed_areas.append([
|
||||
[prime_x - PRIME_CLEARANCE, prime_y - PRIME_CLEARANCE],
|
||||
[prime_x + PRIME_CLEARANCE, prime_y - PRIME_CLEARANCE],
|
||||
[prime_x + PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE],
|
||||
[prime_x - PRIME_CLEARANCE, prime_y + PRIME_CLEARANCE],
|
||||
])
|
||||
|
||||
bed_adhesion_size = self._getBedAdhesionSize(self._active_container_stack)
|
||||
|
||||
if disallowed_areas:
|
||||
# Extend every area already in the disallowed_areas with the skirt size.
|
||||
for area in disallowed_areas:
|
||||
poly = Polygon(numpy.array(area, numpy.float32))
|
||||
poly = poly.getMinkowskiHull(Polygon(approximatedCircleVertices(bed_adhesion_size)))
|
||||
|
||||
areas.append(poly)
|
||||
|
||||
# Add the skirt areas around the borders of the build plate.
|
||||
if bed_adhesion_size > 0:
|
||||
half_machine_width = self._active_container_stack.getProperty("machine_width", "value") / 2
|
||||
half_machine_depth = self._active_container_stack.getProperty("machine_depth", "value") / 2
|
||||
|
||||
areas.append(Polygon(numpy.array([
|
||||
[-half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width, half_machine_depth],
|
||||
[-half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size],
|
||||
[-half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
|
||||
], numpy.float32)))
|
||||
|
||||
areas.append(Polygon(numpy.array([
|
||||
[half_machine_width, half_machine_depth],
|
||||
[half_machine_width, -half_machine_depth],
|
||||
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size],
|
||||
[half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size]
|
||||
], numpy.float32)))
|
||||
|
||||
areas.append(Polygon(numpy.array([
|
||||
[-half_machine_width, half_machine_depth],
|
||||
[half_machine_width, half_machine_depth],
|
||||
[half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size],
|
||||
[-half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size]
|
||||
], numpy.float32)))
|
||||
|
||||
areas.append(Polygon(numpy.array([
|
||||
[half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size],
|
||||
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
|
||||
], numpy.float32)))
|
||||
|
||||
self._disallowed_areas = areas
|
||||
|
||||
## Convenience function to calculate the size of the bed adhesion in directions x, y.
|
||||
def _getBedAdhesionSize(self, container_stack):
|
||||
skirt_size = 0.0
|
||||
|
||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
||||
if adhesion_type == "skirt":
|
||||
skirt_distance = container_stack.getProperty("skirt_gap", "value")
|
||||
skirt_line_count = container_stack.getProperty("skirt_line_count", "value")
|
||||
skirt_size = skirt_distance + (skirt_line_count * container_stack.getProperty("skirt_brim_line_width", "value"))
|
||||
elif adhesion_type == "brim":
|
||||
skirt_size = container_stack.getProperty("brim_line_count", "value") * container_stack.getProperty("skirt_brim_line_width", "value")
|
||||
elif adhesion_type == "raft":
|
||||
skirt_size = container_stack.getProperty("raft_margin", "value")
|
||||
|
||||
if container_stack.getProperty("draft_shield_enabled", "value"):
|
||||
skirt_size += container_stack.getProperty("draft_shield_dist", "value")
|
||||
|
||||
if container_stack.getProperty("xy_offset", "value"):
|
||||
skirt_size += container_stack.getProperty("xy_offset", "value")
|
||||
|
||||
return skirt_size
|
||||
|
||||
def _clamp(self, value, min_value, max_value):
|
||||
return max(min(value, max_value), min_value)
|
||||
|
||||
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "xy_offset"]
|
||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
|
||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
|
||||
27
cura/CameraAnimation.py
Normal file
27
cura/CameraAnimation.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
|
||||
from PyQt5.QtCore import QVariantAnimation, QEasingCurve
|
||||
from PyQt5.QtGui import QVector3D
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
class CameraAnimation(QVariantAnimation):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._camera_tool = None
|
||||
self.setDuration(500)
|
||||
self.setEasingCurve(QEasingCurve.InOutQuad)
|
||||
|
||||
def setCameraTool(self, camera_tool):
|
||||
self._camera_tool = camera_tool
|
||||
|
||||
def setStart(self, start):
|
||||
self.setStartValue(QVector3D(start.x, start.y, start.z))
|
||||
|
||||
def setTarget(self, target):
|
||||
self.setEndValue(QVector3D(target.x, target.y, target.z))
|
||||
|
||||
def updateCurrentValue(self, value):
|
||||
self._camera_tool.setOrigin(Vector(value.x(), value.y(), value.z()))
|
||||
18
cura/CameraImageProvider.py
Normal file
18
cura/CameraImageProvider.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from PyQt5.QtGui import QImage
|
||||
from PyQt5.QtQuick import QQuickImageProvider
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
class CameraImageProvider(QQuickImageProvider):
|
||||
def __init__(self):
|
||||
QQuickImageProvider.__init__(self, QQuickImageProvider.Image)
|
||||
|
||||
## Request a new image.
|
||||
def requestImage(self, id, size):
|
||||
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
||||
try:
|
||||
return output_device.getCameraImage(), QSize(15, 15)
|
||||
except AttributeError:
|
||||
pass
|
||||
return QImage(), QSize(15, 15)
|
||||
291
cura/ConvexHullDecorator.py
Normal file
291
cura/ConvexHullDecorator.py
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Application import Application
|
||||
|
||||
from UM.Math.Polygon import Polygon
|
||||
from . import ConvexHullNode
|
||||
|
||||
import numpy
|
||||
|
||||
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
|
||||
# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
|
||||
class ConvexHullDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._convex_hull_node = None
|
||||
self._init2DConvexHullCache()
|
||||
|
||||
self._global_stack = None
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
# For raft thickness, DRY
|
||||
self._build_volume = Application.getInstance().getBuildVolume()
|
||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
def setNode(self, node):
|
||||
previous_node = self._node
|
||||
# Disconnect from previous node signals
|
||||
if previous_node is not None and node is not previous_node:
|
||||
previous_node.transformationChanged.disconnect(self._onChanged)
|
||||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
|
||||
super().setNode(node)
|
||||
|
||||
self._node.transformationChanged.connect(self._onChanged)
|
||||
self._node.parentChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
## Force that a new (empty) object is created upon copy.
|
||||
def __deepcopy__(self, memo):
|
||||
return ConvexHullDecorator()
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node
|
||||
def getConvexHull(self):
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
hull = self._compute2DConvexHull()
|
||||
|
||||
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"):
|
||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||
return hull
|
||||
|
||||
## Get the convex hull of the node with the full head size
|
||||
def getConvexHullHeadFull(self):
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
return self._compute2DConvexHeadFull()
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
def getConvexHullHead(self):
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
|
||||
## Get convex hull of the node
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# For one at the time this is the area without the head.
|
||||
def getConvexHullBoundary(self):
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
|
||||
|
||||
# Printing one at a time and it's not an object in a group
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
||||
def recomputeConvexHull(self):
|
||||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
self._convex_hull_node = None
|
||||
return
|
||||
|
||||
convex_hull = self.getConvexHull()
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
|
||||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key, property_name):
|
||||
if key in self._affected_settings and property_name == "value":
|
||||
self._onChanged()
|
||||
|
||||
def _init2DConvexHullCache(self):
|
||||
# Cache for the group code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_group_child_polygon = None
|
||||
self._2d_convex_hull_group_result = None
|
||||
|
||||
# Cache for the mesh code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_mesh = None
|
||||
self._2d_convex_hull_mesh_world_transform = None
|
||||
self._2d_convex_hull_mesh_result = None
|
||||
|
||||
def _compute2DConvexHull(self):
|
||||
if self._node.callDecoration("isGroup"):
|
||||
points = numpy.zeros((0, 2), dtype=numpy.int32)
|
||||
for child in self._node.getChildren():
|
||||
child_hull = child.callDecoration("_compute2DConvexHull")
|
||||
if child_hull:
|
||||
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
||||
|
||||
if points.size < 3:
|
||||
return None
|
||||
child_polygon = Polygon(points)
|
||||
|
||||
# Check the cache
|
||||
if child_polygon == self._2d_convex_hull_group_child_polygon:
|
||||
return self._2d_convex_hull_group_result
|
||||
|
||||
# First, calculate the normal convex hull around the points
|
||||
convex_hull = child_polygon.getConvexHull()
|
||||
|
||||
# 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
|
||||
self._2d_convex_hull_group_child_polygon = child_polygon
|
||||
self._2d_convex_hull_group_result = rounded_hull
|
||||
|
||||
return rounded_hull
|
||||
|
||||
else:
|
||||
rounded_hull = None
|
||||
mesh = None
|
||||
world_transform = None
|
||||
if self._node.getMeshData():
|
||||
mesh = self._node.getMeshData()
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
|
||||
# Check the cache
|
||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||
return self._2d_convex_hull_mesh_result
|
||||
|
||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||
# Don't use data below 0.
|
||||
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
|
||||
# Do not throw away vertices: the convex hull may be too small and objects can collide.
|
||||
# vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
|
||||
|
||||
if len(vertex_data) >= 4:
|
||||
# Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
|
||||
# This is done to greatly speed up further convex hull calculations as the convex hull
|
||||
# becomes much less complex when dealing with highly detailed models.
|
||||
vertex_data = numpy.round(vertex_data, 1)
|
||||
|
||||
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
|
||||
|
||||
# Grab the set of unique points.
|
||||
#
|
||||
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
|
||||
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
|
||||
if len(vertex_data) >= 4:
|
||||
# First, calculate the normal convex hull around the points
|
||||
convex_hull = hull.getConvexHull()
|
||||
|
||||
# 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 = 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
|
||||
self._2d_convex_hull_mesh = mesh
|
||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||
self._2d_convex_hull_mesh_result = rounded_hull
|
||||
|
||||
return rounded_hull
|
||||
|
||||
def _getHeadAndFans(self):
|
||||
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
||||
|
||||
def _compute2DConvexHeadFull(self):
|
||||
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
|
||||
|
||||
def _compute2DConvexHeadMin(self):
|
||||
headAndFans = self._getHeadAndFans()
|
||||
mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans)
|
||||
return min_head_hull
|
||||
|
||||
## Compensate given 2D polygon with adhesion margin
|
||||
# \return 2D polygon with added margin
|
||||
def _add2DAdhesionMargin(self, poly):
|
||||
# Compensate for raft/skirt/brim
|
||||
# Add extra margin depending on adhesion type
|
||||
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":
|
||||
extra_margin = max(0, self._global_stack.getProperty("raft_margin", "value") - head_y_size)
|
||||
elif adhesion_type == "brim":
|
||||
extra_margin = max(0, self._global_stack.getProperty("brim_width", "value") - head_y_size)
|
||||
elif adhesion_type == "skirt":
|
||||
extra_margin = max(
|
||||
0, self._global_stack.getProperty("skirt_gap", "value") +
|
||||
self._global_stack.getProperty("skirt_line_count", "value") * self._global_stack.getProperty("skirt_brim_line_width", "value") -
|
||||
head_y_size)
|
||||
# adjust head_and_fans with extra margin
|
||||
if extra_margin > 0:
|
||||
# In Cura 2.2+, there is a function to create this circle-like polygon.
|
||||
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)
|
||||
return poly
|
||||
|
||||
def _roundHull(self, convex_hull):
|
||||
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)))
|
||||
|
||||
def _onChanged(self, *args):
|
||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||
self.recomputeConvexHull()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||
|
||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
## Returns true if node is a descendent or the same as the root node.
|
||||
def __isDescendant(self, root, node):
|
||||
if node is None:
|
||||
return False
|
||||
if root is node:
|
||||
return True
|
||||
return self.__isDescendant(root, node.getParent())
|
||||
|
||||
_affected_settings = [
|
||||
"adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers",
|
||||
"raft_surface_thickness", "raft_airgap", "raft_margin", "print_sequence",
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance"]
|
||||
85
cura/ConvexHullNode.py
Normal file
85
cura/ConvexHullNode.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Resources import Resources
|
||||
from UM.Math.Color import Color
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with.
|
||||
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
class ConvexHullNode(SceneNode):
|
||||
## 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
|
||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||
# to represent the raft as well.
|
||||
def __init__(self, node, hull, thickness, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
|
||||
self._shader = None
|
||||
|
||||
self._original_parent = parent
|
||||
|
||||
# Color of the drawn convex hull
|
||||
self._color = Color(0.4, 0.4, 0.4, 1.0)
|
||||
|
||||
# The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
|
||||
self._mesh_height = 0.1
|
||||
|
||||
self._thickness = thickness
|
||||
|
||||
# The node this mesh is "watching"
|
||||
self._node = node
|
||||
self._convex_hull_head_mesh = None
|
||||
|
||||
self._node.decoratorsChanged.connect(self._onNodeDecoratorsChanged)
|
||||
self._onNodeDecoratorsChanged(self._node)
|
||||
|
||||
self._hull = hull
|
||||
if self._hull:
|
||||
hull_mesh_builder = MeshBuilder()
|
||||
|
||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||
self._hull.getPoints()[::-1], # bottom layer is reversed
|
||||
self._mesh_height-thickness, self._mesh_height, color=self._color):
|
||||
|
||||
hull_mesh = hull_mesh_builder.build()
|
||||
self.setMeshData(hull_mesh)
|
||||
|
||||
def getHull(self):
|
||||
return self._hull
|
||||
|
||||
def getThickness(self):
|
||||
return self._thickness
|
||||
|
||||
def getWatchedNode(self):
|
||||
return self._node
|
||||
|
||||
def render(self, renderer):
|
||||
if not self._shader:
|
||||
self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader"))
|
||||
self._shader.setUniformValue("u_diffuseColor", self._color)
|
||||
self._shader.setUniformValue("u_opacity", 0.6)
|
||||
|
||||
if self.getParent():
|
||||
if self.getMeshData():
|
||||
renderer.queueNode(self, transparent = True, shader = self._shader, backface_cull = True, sort = -8)
|
||||
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)
|
||||
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node):
|
||||
self._color = Color(35, 35, 35, 0.5)
|
||||
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
convex_hull_head_builder.addConvexPolygon(convex_hull_head.getPoints(), self._mesh_height - self._thickness)
|
||||
self._convex_hull_head_mesh = convex_hull_head_builder.build()
|
||||
|
||||
if not node:
|
||||
return
|
||||
|
||||
73
cura/CrashHandler.py
Normal file
73
cura/CrashHandler.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import sys
|
||||
import platform
|
||||
import traceback
|
||||
import webbrowser
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
# 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
|
||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
||||
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
|
||||
fatal_exception_types = [
|
||||
MemoryError,
|
||||
SyntaxError,
|
||||
ImportError,
|
||||
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!")
|
||||
for line in traceback.format_exception(exception_type, value, tb):
|
||||
for part in line.rstrip("\n").split("\n"):
|
||||
Logger.log("c", part)
|
||||
|
||||
if not debug_mode and exception_type not in fatal_exception_types:
|
||||
return
|
||||
|
||||
application = QCoreApplication.instance()
|
||||
if not application:
|
||||
sys.exit(1)
|
||||
|
||||
dialog = QDialog()
|
||||
dialog.setWindowTitle(catalog.i18nc("@title:window", "Oops!"))
|
||||
|
||||
layout = QVBoxLayout(dialog)
|
||||
|
||||
label = QLabel(dialog)
|
||||
layout.addWidget(label)
|
||||
label.setText(catalog.i18nc("@label", "<p>A fatal exception has occurred that we could not recover from!</p><p>Please use the information below to post a bug report at <a href=\"http://github.com/Ultimaker/Cura/issues\">http://github.com/Ultimaker/Cura/issues</a></p>"))
|
||||
|
||||
textarea = QTextEdit(dialog)
|
||||
layout.addWidget(textarea)
|
||||
|
||||
try:
|
||||
from UM.Application import Application
|
||||
version = Application.getInstance().getVersion()
|
||||
except:
|
||||
version = "Unknown"
|
||||
|
||||
trace = "".join(traceback.format_exception(exception_type, value, tb))
|
||||
|
||||
crash_info = "Version: {0}\nPlatform: {1}\nQt: {2}\nPyQt: {3}\n\nException:\n{4}"
|
||||
crash_info = crash_info.format(version, platform.platform(), QT_VERSION_STR, PYQT_VERSION_STR, trace)
|
||||
|
||||
textarea.setText(crash_info)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Close, dialog)
|
||||
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_()
|
||||
sys.exit(1)
|
||||
26
cura/CuraActions.py
Normal file
26
cura/CuraActions.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from PyQt5.QtCore import QObject, pyqtSlot, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from UM.Event import CallFunctionEvent
|
||||
from UM.Application import Application
|
||||
|
||||
|
||||
class CuraActions(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
@pyqtSlot()
|
||||
def openDocumentation(self):
|
||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
||||
Application.getInstance().functionEvent(event)
|
||||
|
||||
@pyqtSlot()
|
||||
def openBugReportPage(self):
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
||||
Application.getInstance().functionEvent(event)
|
||||
|
||||
def _openUrl(self, url):
|
||||
QDesktopServices.openUrl(url)
|
||||
923
cura/CuraApplication.py
Normal file
923
cura/CuraApplication.py
Normal file
|
|
@ -0,0 +1,923 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.ToolHandle import ToolHandle
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Platform import Platform
|
||||
from UM.JobQueue import JobQueue
|
||||
from UM.SaveFile import SaveFile
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
from UM.Settings.Validator import Validator
|
||||
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
from cura.SetParentOperation import SetParentOperation
|
||||
|
||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
from . import PrintInformation
|
||||
from . import CuraActions
|
||||
from . import MultiMaterialDecorator
|
||||
from . import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import CameraImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
import cura.Settings
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
|
||||
from PyQt5.QtGui import QColor, QIcon
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
import platform
|
||||
import sys
|
||||
import os.path
|
||||
import numpy
|
||||
import copy
|
||||
import urllib
|
||||
numpy.seterr(all="ignore")
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType
|
||||
except ImportError:
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
class ResourceTypes:
|
||||
QmlFiles = Resources.UserType + 1
|
||||
Firmware = Resources.UserType + 2
|
||||
QualityInstanceContainer = Resources.UserType + 3
|
||||
MaterialInstanceContainer = Resources.UserType + 4
|
||||
VariantInstanceContainer = Resources.UserType + 5
|
||||
UserInstanceContainer = Resources.UserType + 6
|
||||
MachineStack = Resources.UserType + 7
|
||||
ExtruderStack = Resources.UserType + 8
|
||||
|
||||
Q_ENUMS(ResourceTypes)
|
||||
|
||||
def __init__(self):
|
||||
Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
|
||||
|
||||
self._open_file_queue = [] # Files to open when plug-ins are loaded.
|
||||
|
||||
# Need to do this before ContainerRegistry tries to load the machines
|
||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("global_inherits_stack", DefinitionPropertyType.Function, default = "-1")
|
||||
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
||||
|
||||
SettingFunction.registerOperator("extruderValues", cura.Settings.ExtruderManager.getExtruderValues)
|
||||
SettingFunction.registerOperator("extruderValue", cura.Settings.ExtruderManager.getExtruderValue)
|
||||
|
||||
## Add the 4 types of profiles to storage.
|
||||
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
|
||||
Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
|
||||
Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
|
||||
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
|
||||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
|
||||
|
||||
## Initialise the version upgrade manager with Cura's storage paths.
|
||||
import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
|
||||
self._version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager(
|
||||
{
|
||||
("quality", UM.Settings.InstanceContainer.Version): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", UM.Settings.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
|
||||
("preferences", UM.Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences")
|
||||
}
|
||||
)
|
||||
|
||||
self._machine_action_manager = MachineActionManager.MachineActionManager()
|
||||
self._machine_manager = None # This is initialized on demand.
|
||||
|
||||
self._additional_components = {} # Components to add to certain areas in the interface
|
||||
|
||||
super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
|
||||
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
|
||||
self.setRequiredPlugins([
|
||||
"CuraEngineBackend",
|
||||
"MeshView",
|
||||
"LayerView",
|
||||
"STLReader",
|
||||
"SelectionTool",
|
||||
"CameraTool",
|
||||
"GCodeWriter",
|
||||
"LocalFileOutputDevice"
|
||||
])
|
||||
self._physics = None
|
||||
self._volume = None
|
||||
self._output_devices = {}
|
||||
self._print_information = None
|
||||
self._previous_active_tool = None
|
||||
self._platform_activity = False
|
||||
self._scene_bounding_box = AxisAlignedBox.Null
|
||||
|
||||
self._job_name = None
|
||||
self._center_after_select = False
|
||||
self._camera_animation = None
|
||||
self._cura_actions = None
|
||||
self._started = False
|
||||
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
||||
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||
|
||||
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
|
||||
Resources.addType(self.ResourceTypes.Firmware, "firmware")
|
||||
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading machines..."))
|
||||
|
||||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# We need them to simplify the switching between materials.
|
||||
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
empty_variant_container = copy.deepcopy(empty_container)
|
||||
empty_variant_container._id = "empty_variant"
|
||||
empty_variant_container.addMetaDataEntry("type", "variant")
|
||||
ContainerRegistry.getInstance().addContainer(empty_variant_container)
|
||||
empty_material_container = copy.deepcopy(empty_container)
|
||||
empty_material_container._id = "empty_material"
|
||||
empty_material_container.addMetaDataEntry("type", "material")
|
||||
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container._id = "empty_quality"
|
||||
empty_quality_container.addMetaDataEntry("type", "quality")
|
||||
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
||||
|
||||
ContainerRegistry.getInstance().load()
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_mode", "simple")
|
||||
Preferences.getInstance().addPreference("cura/recent_files", "")
|
||||
Preferences.getInstance().addPreference("cura/categories_expanded", "")
|
||||
Preferences.getInstance().addPreference("cura/jobname_prefix", True)
|
||||
Preferences.getInstance().addPreference("view/center_on_select", True)
|
||||
Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
|
||||
Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
|
||||
|
||||
for key in [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
"dialog_profile_path",
|
||||
"dialog_material_path"]:
|
||||
|
||||
Preferences.getInstance().addPreference("local_file/%s" % key, os.path.expanduser("~/"))
|
||||
|
||||
Preferences.getInstance().setDefault("local_file/last_used_type", "text/x-gcode")
|
||||
|
||||
Preferences.getInstance().setDefault("general/visible_settings", """
|
||||
machine_settings
|
||||
resolution
|
||||
layer_height
|
||||
shell
|
||||
wall_thickness
|
||||
top_bottom_thickness
|
||||
infill
|
||||
infill_sparse_density
|
||||
material
|
||||
material_print_temperature
|
||||
material_bed_temperature
|
||||
material_diameter
|
||||
material_flow
|
||||
retraction_enable
|
||||
speed
|
||||
speed_print
|
||||
speed_travel
|
||||
acceleration_print
|
||||
acceleration_travel
|
||||
jerk_print
|
||||
jerk_travel
|
||||
travel
|
||||
cooling
|
||||
cool_fan_enabled
|
||||
support
|
||||
support_enable
|
||||
support_type
|
||||
support_interface_density
|
||||
platform_adhesion
|
||||
adhesion_type
|
||||
brim_width
|
||||
raft_airgap
|
||||
layer_0_z_overlap
|
||||
raft_surface_layers
|
||||
dual
|
||||
adhesion_extruder_nr
|
||||
support_extruder_nr
|
||||
prime_tower_enable
|
||||
prime_tower_size
|
||||
prime_tower_position_x
|
||||
prime_tower_position_y
|
||||
meshfix
|
||||
blackmagic
|
||||
print_sequence
|
||||
infill_mesh
|
||||
dual
|
||||
experimental
|
||||
""".replace("\n", ";").replace(" ", ""))
|
||||
|
||||
JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
|
||||
|
||||
self.applicationShuttingDown.connect(self.saveSettings)
|
||||
self.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._recent_files = []
|
||||
files = Preferences.getInstance().getValue("cura/recent_files").split(";")
|
||||
for f in files:
|
||||
if not os.path.isfile(f):
|
||||
continue
|
||||
|
||||
self._recent_files.append(QUrl.fromLocalFile(f))
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
|
||||
## A reusable dialogbox
|
||||
#
|
||||
showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
|
||||
def messageBox(self, title, text, informativeText = "", detailedText = "", buttons = QMessageBox.Ok, icon = QMessageBox.NoIcon, callback = None, callback_arguments = []):
|
||||
self._message_box_callback = callback
|
||||
self._message_box_callback_arguments = callback_arguments
|
||||
self.showMessageBox.emit(title, text, informativeText, detailedText, buttons, icon)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def messageBoxClosed(self, button):
|
||||
if self._message_box_callback:
|
||||
self._message_box_callback(button, *self._message_box_callback_arguments)
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
||||
|
||||
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
#
|
||||
# Note that the AutoSave plugin also calls this method.
|
||||
def saveSettings(self):
|
||||
if not self._started: # Do not do saving during application start
|
||||
return
|
||||
|
||||
for instance in ContainerRegistry.getInstance().findInstanceContainers():
|
||||
if not instance.isDirty():
|
||||
continue
|
||||
|
||||
try:
|
||||
data = instance.serialize()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
||||
continue
|
||||
|
||||
mime_type = ContainerRegistry.getMimeTypeForContainer(type(instance))
|
||||
file_name = urllib.parse.quote_plus(instance.getId()) + "." + mime_type.preferredSuffix
|
||||
instance_type = instance.getMetaDataEntry("type")
|
||||
path = None
|
||||
if instance_type == "material":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
|
||||
elif instance_type == "quality":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
|
||||
elif instance_type == "user":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
|
||||
elif instance_type == "variant":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
|
||||
|
||||
if path:
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f:
|
||||
f.write(data)
|
||||
|
||||
for stack in ContainerRegistry.getInstance().findContainerStacks():
|
||||
if not stack.isDirty():
|
||||
continue
|
||||
|
||||
try:
|
||||
data = stack.serialize()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
||||
continue
|
||||
|
||||
mime_type = ContainerRegistry.getMimeTypeForContainer(type(stack))
|
||||
file_name = urllib.parse.quote_plus(stack.getId()) + "." + mime_type.preferredSuffix
|
||||
stack_type = stack.getMetaDataEntry("type", None)
|
||||
path = None
|
||||
if not stack_type or stack_type == "machine":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
|
||||
elif stack_type == "extruder_train":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
|
||||
if path:
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
@pyqtSlot(str, result = QUrl)
|
||||
def getDefaultPath(self, key):
|
||||
default_path = Preferences.getInstance().getValue("local_file/%s" % key)
|
||||
return QUrl.fromLocalFile(default_path)
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def setDefaultPath(self, key, default_path):
|
||||
Preferences.getInstance().setValue("local_file/%s" % key, default_path)
|
||||
|
||||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistery
|
||||
def _loadPlugins(self):
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
|
||||
self._plugin_registry.loadPlugin("ConsoleLogger")
|
||||
self._plugin_registry.loadPlugin("CuraEngineBackend")
|
||||
|
||||
self._plugin_registry.loadPlugins()
|
||||
|
||||
if self.getBackend() == None:
|
||||
raise RuntimeError("Could not load the backend plugin!")
|
||||
|
||||
self._plugins_loaded = True
|
||||
|
||||
def addCommandLineOptions(self, parser):
|
||||
super().addCommandLineOptions(parser)
|
||||
parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
|
||||
parser.add_argument("--debug", dest="debug-mode", action="store_true", default=False, help="Enable detailed crash reports.")
|
||||
|
||||
def run(self):
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
|
||||
|
||||
controller = self.getController()
|
||||
|
||||
controller.setActiveView("SolidView")
|
||||
controller.setCameraTool("CameraTool")
|
||||
controller.setSelectionTool("SelectionTool")
|
||||
|
||||
t = controller.getTool("TranslateTool")
|
||||
if t:
|
||||
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis])
|
||||
|
||||
Selection.selectionChanged.connect(self.onSelectionChanged)
|
||||
|
||||
root = controller.getScene().getRoot()
|
||||
|
||||
# The platform is a child of BuildVolume
|
||||
self._volume = BuildVolume.BuildVolume(root)
|
||||
|
||||
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
|
||||
|
||||
self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
|
||||
|
||||
camera = Camera("3d", root)
|
||||
camera.setPosition(Vector(-80, 250, 700))
|
||||
camera.setPerspective(True)
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
|
||||
self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))
|
||||
|
||||
self._camera_animation = CameraAnimation.CameraAnimation()
|
||||
self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
|
||||
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
|
||||
|
||||
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
|
||||
cura.Settings.ExtruderManager.getInstance()
|
||||
qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
||||
|
||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
|
||||
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
|
||||
self.initializeEngine()
|
||||
|
||||
if self._engine.rootObjects:
|
||||
self.closeSplash()
|
||||
|
||||
for file in self.getCommandLineOption("file", []):
|
||||
self._openFile(file)
|
||||
for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
|
||||
self._openFile(file_name)
|
||||
|
||||
self._started = True
|
||||
|
||||
self.exec_()
|
||||
|
||||
def getMachineManager(self, *args):
|
||||
if self._machine_manager is None:
|
||||
self._machine_manager = cura.Settings.MachineManager.createMachineManager()
|
||||
return self._machine_manager
|
||||
|
||||
## Get the machine action manager
|
||||
# We ignore any *args given to this, as we also register the machine manager as qml singleton.
|
||||
# It wants to give this function an engine and script engine, but we don't care about that.
|
||||
def getMachineActionManager(self, *args):
|
||||
return self._machine_action_manager
|
||||
|
||||
## Handle Qt events
|
||||
def event(self, event):
|
||||
if event.type() == QEvent.FileOpen:
|
||||
if self._plugins_loaded:
|
||||
self._openFile(event.file())
|
||||
else:
|
||||
self._open_file_queue.append(event.file())
|
||||
|
||||
return super().event(event)
|
||||
|
||||
## Get print information (duration / material used)
|
||||
def getPrintInformation(self):
|
||||
return self._print_information
|
||||
|
||||
## Registers objects for the QML engine to use.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
def registerObjects(self, engine):
|
||||
engine.rootContext().setContextProperty("Printer", self)
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
self._print_information = PrintInformation.PrintInformation()
|
||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||
self._cura_actions = CuraActions.CuraActions(self)
|
||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||
|
||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||
|
||||
qmlRegisterType(cura.Settings.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
|
||||
qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||
qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
|
||||
qmlRegisterSingletonType(cura.Settings.ContainerManager, "Cura", 1, 0, "ContainerManager", cura.Settings.ContainerManager.createContainerManager)
|
||||
|
||||
qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
|
||||
|
||||
engine.rootContext().setContextProperty("ExtruderManager", cura.Settings.ExtruderManager.getInstance())
|
||||
|
||||
for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
|
||||
type_name = os.path.splitext(os.path.basename(path))[0]
|
||||
if type_name in ("Cura", "Actions"):
|
||||
continue
|
||||
|
||||
qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
|
||||
|
||||
def onSelectionChanged(self):
|
||||
if Selection.hasSelection():
|
||||
if not self.getController().getActiveTool():
|
||||
if self._previous_active_tool:
|
||||
self.getController().setActiveTool(self._previous_active_tool)
|
||||
self._previous_active_tool = None
|
||||
else:
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
if Preferences.getInstance().getValue("view/center_on_select"):
|
||||
self._center_after_select = True
|
||||
else:
|
||||
if self.getController().getActiveTool():
|
||||
self._previous_active_tool = self.getController().getActiveTool().getPluginId()
|
||||
self.getController().setActiveTool(None)
|
||||
else:
|
||||
self._previous_active_tool = None
|
||||
|
||||
def _onToolOperationStopped(self, event):
|
||||
if self._center_after_select:
|
||||
self._center_after_select = False
|
||||
self._camera_animation.setStart(self.getController().getTool("CameraTool").getOrigin())
|
||||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||
self._camera_animation.start()
|
||||
|
||||
requestAddPrinter = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = activityChanged)
|
||||
def getPlatformActivity(self):
|
||||
return self._platform_activity
|
||||
|
||||
@pyqtProperty(str, notify = sceneBoundingBoxChanged)
|
||||
def getSceneBoundingBoxString(self):
|
||||
return self._i18n_catalog.i18nc("@info", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
|
||||
|
||||
def updatePlatformActivity(self, node = None):
|
||||
count = 0
|
||||
scene_bounding_box = None
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode or not node.getMeshData():
|
||||
continue
|
||||
|
||||
count += 1
|
||||
if not scene_bounding_box:
|
||||
scene_bounding_box = node.getBoundingBox()
|
||||
else:
|
||||
other_bb = node.getBoundingBox()
|
||||
if other_bb is not None:
|
||||
scene_bounding_box = scene_bounding_box + node.getBoundingBox()
|
||||
|
||||
if not scene_bounding_box:
|
||||
scene_bounding_box = AxisAlignedBox.Null
|
||||
|
||||
if repr(self._scene_bounding_box) != repr(scene_bounding_box):
|
||||
self._scene_bounding_box = scene_bounding_box
|
||||
self.sceneBoundingBoxChanged.emit()
|
||||
|
||||
self._platform_activity = True if count > 0 else False
|
||||
self.activityChanged.emit()
|
||||
|
||||
# Remove all selected objects from the scene.
|
||||
@pyqtSlot()
|
||||
def deleteSelection(self):
|
||||
if not self.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()
|
||||
|
||||
pass
|
||||
|
||||
## Remove an object from the scene.
|
||||
# Note that this only removes an object if it is selected.
|
||||
@pyqtSlot("quint64")
|
||||
def deleteObject(self, object_id):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
group_node = None
|
||||
if node.getParent():
|
||||
group_node = node.getParent()
|
||||
op = RemoveSceneNodeOperation(node)
|
||||
|
||||
op.push()
|
||||
if group_node:
|
||||
if len(group_node.getChildren()) == 1 and group_node.callDecoration("isGroup"):
|
||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
||||
op = RemoveSceneNodeOperation(group_node)
|
||||
op.push()
|
||||
|
||||
## Create a number of copies of existing object.
|
||||
@pyqtSlot("quint64", int)
|
||||
def multiplyObject(self, object_id, count):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
current_node = node
|
||||
# Find the topmost group
|
||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||
current_node = current_node.getParent()
|
||||
|
||||
new_node = copy.deepcopy(current_node)
|
||||
|
||||
op = GroupedOperation()
|
||||
for _ in range(count):
|
||||
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||
op.push()
|
||||
|
||||
## Center object on platform.
|
||||
@pyqtSlot("quint64")
|
||||
def centerObject(self, object_id):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if not node:
|
||||
return
|
||||
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
if node:
|
||||
op = SetTransformOperation(node, Vector())
|
||||
op.push()
|
||||
|
||||
## Select all nodes containing mesh data in the scene.
|
||||
@pyqtSlot()
|
||||
def selectAll(self):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
Selection.clear()
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
Selection.add(node)
|
||||
|
||||
## Delete all nodes containing mesh data in the scene.
|
||||
@pyqtSlot()
|
||||
def deleteAll(self):
|
||||
Logger.log("i", "Clearing scene")
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
nodes.append(node)
|
||||
if nodes:
|
||||
op = GroupedOperation()
|
||||
|
||||
for node in nodes:
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
op.push()
|
||||
Selection.clear()
|
||||
|
||||
## Reset all translation on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
def resetAllTranslation(self):
|
||||
Logger.log("i", "Resetting all scene translations")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
nodes.append(node)
|
||||
|
||||
if nodes:
|
||||
op = GroupedOperation()
|
||||
for node in nodes:
|
||||
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||
op.addOperation(SetTransformOperation(node, Vector(0,0,0)))
|
||||
|
||||
op.push()
|
||||
|
||||
## Reset all transformations on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
def resetAll(self):
|
||||
Logger.log("i", "Resetting all scene transformations")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode:
|
||||
continue
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
nodes.append(node)
|
||||
|
||||
if nodes:
|
||||
op = GroupedOperation()
|
||||
|
||||
for node in nodes:
|
||||
# Ensure that the object is above the build platform
|
||||
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||
op.addOperation(SetTransformOperation(node, Vector(0,0,0), Quaternion(), Vector(1, 1, 1)))
|
||||
|
||||
op.push()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode or not node.getMeshData():
|
||||
continue
|
||||
|
||||
nodes.append(node)
|
||||
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
for node in nodes:
|
||||
file_name = node.getMeshData().getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
job.start()
|
||||
else:
|
||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||
|
||||
## Get logging data of the backend engine
|
||||
# \returns \type{string} Logging data
|
||||
@pyqtSlot(result = str)
|
||||
def getEngineLog(self):
|
||||
log = ""
|
||||
|
||||
for entry in self.getBackend().getLog():
|
||||
log += entry.decode()
|
||||
|
||||
return log
|
||||
|
||||
recentFilesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = recentFilesChanged)
|
||||
def recentFiles(self):
|
||||
return self._recent_files
|
||||
|
||||
@pyqtSlot("QStringList")
|
||||
def setExpandedCategories(self, categories):
|
||||
categories = list(set(categories))
|
||||
categories.sort()
|
||||
joined = ";".join(categories)
|
||||
if joined != Preferences.getInstance().getValue("cura/categories_expanded"):
|
||||
Preferences.getInstance().setValue("cura/categories_expanded", joined)
|
||||
self.expandedCategoriesChanged.emit()
|
||||
|
||||
expandedCategoriesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||
def expandedCategories(self):
|
||||
return Preferences.getInstance().getValue("cura/categories_expanded").split(";")
|
||||
|
||||
@pyqtSlot()
|
||||
def mergeSelected(self):
|
||||
self.groupSelected()
|
||||
try:
|
||||
group_node = Selection.getAllSelectedObjects()[0]
|
||||
except Exception as e:
|
||||
Logger.log("d", "mergeSelected: Exception:", e)
|
||||
return
|
||||
multi_material_decorator = MultiMaterialDecorator.MultiMaterialDecorator()
|
||||
group_node.addDecorator(multi_material_decorator)
|
||||
# Reset the position of each node
|
||||
for node in group_node.getChildren():
|
||||
new_position = node.getMeshData().getCenterPosition()
|
||||
new_position = new_position.scale(node.getScale())
|
||||
node.setPosition(new_position)
|
||||
|
||||
# Use the previously found center of the group bounding box as the new location of the group
|
||||
group_node.setPosition(group_node.getBoundingBox().center)
|
||||
|
||||
@pyqtSlot()
|
||||
def groupSelected(self):
|
||||
# Create a group-node
|
||||
group_node = SceneNode()
|
||||
group_decorator = GroupDecorator()
|
||||
group_node.addDecorator(group_decorator)
|
||||
group_node.setParent(self.getController().getScene().getRoot())
|
||||
group_node.setSelectable(True)
|
||||
center = Selection.getSelectionCenter()
|
||||
group_node.setPosition(center)
|
||||
group_node.setCenterPosition(center)
|
||||
|
||||
# Move selected nodes into the group-node
|
||||
Selection.applyOperation(SetParentOperation, group_node)
|
||||
|
||||
# Deselect individual nodes and select the group-node instead
|
||||
for node in group_node.getChildren():
|
||||
Selection.remove(node)
|
||||
Selection.add(group_node)
|
||||
|
||||
@pyqtSlot()
|
||||
def ungroupSelected(self):
|
||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_objects:
|
||||
if node.callDecoration("isGroup"):
|
||||
op = GroupedOperation()
|
||||
|
||||
group_parent = node.getParent()
|
||||
children = node.getChildren().copy()
|
||||
for child in children:
|
||||
# Set the parent of the children to the parent of the group-node
|
||||
op.addOperation(SetParentOperation(child, group_parent))
|
||||
|
||||
# Add all individual nodes to the selection
|
||||
Selection.add(child)
|
||||
|
||||
op.push()
|
||||
# Note: The group removes itself from the scene once all its children have left it,
|
||||
# see GroupDecorator._onChildrenChanged
|
||||
|
||||
def _createSplashScreen(self):
|
||||
return CuraSplashScreen.CuraSplashScreen()
|
||||
|
||||
def _onActiveMachineChanged(self):
|
||||
pass
|
||||
|
||||
fileLoaded = pyqtSignal(str)
|
||||
|
||||
def _onFileLoaded(self, job):
|
||||
node = job.getResult()
|
||||
if node != None:
|
||||
self.fileLoaded.emit(job.getFileName())
|
||||
node.setSelectable(True)
|
||||
node.setName(os.path.basename(job.getFileName()))
|
||||
op = AddSceneNodeOperation(node, self.getController().getScene().getRoot())
|
||||
op.push()
|
||||
|
||||
self.getController().getScene().sceneChanged.emit(node) #Force scene change.
|
||||
|
||||
def _onJobFinished(self, job):
|
||||
if type(job) is not ReadMeshJob or not job.getResult():
|
||||
return
|
||||
|
||||
f = QUrl.fromLocalFile(job.getFileName())
|
||||
if f in self._recent_files:
|
||||
self._recent_files.remove(f)
|
||||
|
||||
self._recent_files.insert(0, f)
|
||||
if len(self._recent_files) > 10:
|
||||
del self._recent_files[10]
|
||||
|
||||
pref = ""
|
||||
for path in self._recent_files:
|
||||
pref += path.toLocalFile() + ";"
|
||||
|
||||
Preferences.getInstance().setValue("cura/recent_files", pref)
|
||||
self.recentFilesChanged.emit()
|
||||
|
||||
def _reloadMeshFinished(self, job):
|
||||
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
|
||||
mesh_data = job.getResult().getMeshData()
|
||||
if mesh_data:
|
||||
job._node.setMeshData(mesh_data)
|
||||
else:
|
||||
Logger.log("w", "Could not find a mesh in reloaded node.")
|
||||
|
||||
def _openFile(self, file):
|
||||
job = ReadMeshJob(os.path.abspath(file))
|
||||
job.finished.connect(self._onFileLoaded)
|
||||
job.start()
|
||||
|
||||
def _addProfileReader(self, profile_reader):
|
||||
# TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
|
||||
pass
|
||||
|
||||
def _addProfileWriter(self, profile_writer):
|
||||
pass
|
||||
|
||||
@pyqtSlot("QSize")
|
||||
def setMinimumWindowSize(self, size):
|
||||
self.getMainWindow().setMinimumSize(size)
|
||||
|
||||
def getBuildVolume(self):
|
||||
return self._volume
|
||||
|
||||
additionalComponentsChanged = pyqtSignal(str, arguments = ["areaId"])
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = additionalComponentsChanged)
|
||||
def additionalComponents(self):
|
||||
return self._additional_components
|
||||
|
||||
## Add a component to a list of components to be reparented to another area in the GUI.
|
||||
# The actual reparenting is done by the area itself.
|
||||
# \param area_id \type{str} Identifying name of the area to which the component should be reparented
|
||||
# \param component \type{QQuickComponent} The component that should be reparented
|
||||
@pyqtSlot(str, "QVariant")
|
||||
def addAdditionalComponent(self, area_id, component):
|
||||
if area_id not in self._additional_components:
|
||||
self._additional_components[area_id] = []
|
||||
self._additional_components[area_id].append(component)
|
||||
|
||||
self.additionalComponentsChanged.emit(area_id)
|
||||
38
cura/CuraSplashScreen.py
Normal file
38
cura/CuraSplashScreen.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QCoreApplication
|
||||
from PyQt5.QtGui import QPixmap, QColor, QFont, QFontMetrics
|
||||
from PyQt5.QtWidgets import QSplashScreen
|
||||
|
||||
from UM.Resources import Resources
|
||||
from UM.Application import Application
|
||||
|
||||
class CuraSplashScreen(QSplashScreen):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._scale = round(QFontMetrics(QCoreApplication.instance().font()).ascent() / 12)
|
||||
|
||||
splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png"))
|
||||
self.setPixmap(splash_image.scaled(splash_image.size() * self._scale))
|
||||
|
||||
def drawContents(self, painter):
|
||||
painter.save()
|
||||
painter.setPen(QColor(0, 0, 0, 255))
|
||||
|
||||
version = Application.getInstance().getVersion().split("-")
|
||||
buildtype = Application.getInstance().getBuildType()
|
||||
if buildtype:
|
||||
version[0] += " (%s)" %(buildtype)
|
||||
|
||||
font = QFont() # Using system-default font here
|
||||
font.setPointSize(20)
|
||||
painter.setFont(font)
|
||||
painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])
|
||||
if len(version) > 1:
|
||||
font.setPointSize(12)
|
||||
painter.setFont(font)
|
||||
painter.drawText(0, 0, 330 * self._scale, 255 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[1])
|
||||
|
||||
painter.restore()
|
||||
super().drawContents(painter)
|
||||
5
cura/CuraVersion.py.in
Normal file
5
cura/CuraVersion.py.in
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
118
cura/Layer.py
Normal file
118
cura/Layer.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
from .LayerPolygon import LayerPolygon
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
||||
import numpy
|
||||
|
||||
class Layer:
|
||||
def __init__(self, layer_id):
|
||||
self._id = layer_id
|
||||
self._height = 0.0
|
||||
self._thickness = 0.0
|
||||
self._polygons = []
|
||||
self._element_count = 0
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@property
|
||||
def thickness(self):
|
||||
return self._thickness
|
||||
|
||||
@property
|
||||
def polygons(self):
|
||||
return self._polygons
|
||||
|
||||
@property
|
||||
def elementCount(self):
|
||||
return self._element_count
|
||||
|
||||
def setHeight(self, height):
|
||||
self._height = height
|
||||
|
||||
def setThickness(self, thickness):
|
||||
self._thickness = thickness
|
||||
|
||||
def lineMeshVertexCount(self):
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshVertexCount()
|
||||
|
||||
return result
|
||||
|
||||
def lineMeshElementCount(self):
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshElementCount()
|
||||
|
||||
return result
|
||||
|
||||
def build(self, vertex_offset, index_offset, vertices, colors, indices):
|
||||
result_vertex_offset = vertex_offset
|
||||
result_index_offset = index_offset
|
||||
self._element_count = 0
|
||||
for polygon in self._polygons:
|
||||
polygon.build(result_vertex_offset, result_index_offset, vertices, colors, indices)
|
||||
result_vertex_offset += polygon.lineMeshVertexCount()
|
||||
result_index_offset += polygon.lineMeshElementCount()
|
||||
self._element_count += polygon.elementCount
|
||||
|
||||
return (result_vertex_offset, result_index_offset)
|
||||
|
||||
def createMesh(self):
|
||||
return self.createMeshOrJumps(True)
|
||||
|
||||
def createJumps(self):
|
||||
return self.createMeshOrJumps(False)
|
||||
|
||||
# Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump
|
||||
__index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 )
|
||||
|
||||
def createMeshOrJumps(self, make_mesh):
|
||||
builder = MeshBuilder()
|
||||
|
||||
line_count = 0
|
||||
if make_mesh:
|
||||
for polygon in self._polygons:
|
||||
line_count += polygon.meshLineCount
|
||||
else:
|
||||
for polygon in self._polygons:
|
||||
line_count += polygon.jumpCount
|
||||
|
||||
|
||||
# Reserve the neccesary space for the data upfront
|
||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||
|
||||
for polygon in self._polygons:
|
||||
# Filter out the types of lines we are not interesed in depending on whether we are drawing the mesh or the jumps.
|
||||
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
|
||||
|
||||
# Create an array with rows [p p+1] and only keep those we whant to draw based on make_mesh
|
||||
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
|
||||
# Line types of the points we want to draw
|
||||
line_types = polygon.types[index_mask]
|
||||
|
||||
# Shift the z-axis according to previous implementation.
|
||||
if make_mesh:
|
||||
points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01
|
||||
else:
|
||||
points[:, 1::3] += 0.01
|
||||
|
||||
# Create an array with normals and tile 2 copies to match size of points variable
|
||||
normals = numpy.tile( polygon.getNormals()[index_mask.ravel()], (1, 2))
|
||||
|
||||
# Scale all normals by the line width of the current line so we can easily offset.
|
||||
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
|
||||
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
|
||||
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)
|
||||
|
||||
builder.addFacesWithColor(f_points, f_indices, f_colors)
|
||||
|
||||
|
||||
return builder.build()
|
||||
25
cura/LayerData.py
Normal file
25
cura/LayerData.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
||||
## Class to holds the layer mesh and information about the layers.
|
||||
# Immutable, use LayerDataBuilder to create one of these.
|
||||
class LayerData(MeshData):
|
||||
def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None,
|
||||
center_position = None, layers=None, element_counts=None):
|
||||
super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs,
|
||||
file_name=file_name, center_position=center_position)
|
||||
self._layers = layers
|
||||
self._element_counts = element_counts
|
||||
|
||||
def getLayer(self, layer):
|
||||
if layer in self._layers:
|
||||
return self._layers[layer]
|
||||
else:
|
||||
return None
|
||||
|
||||
def getLayers(self):
|
||||
return self._layers
|
||||
|
||||
def getElementCounts(self):
|
||||
return self._element_counts
|
||||
75
cura/LayerDataBuilder.py
Normal file
75
cura/LayerDataBuilder.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from .Layer import Layer
|
||||
from .LayerPolygon import LayerPolygon
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from .LayerData import LayerData
|
||||
|
||||
import numpy
|
||||
|
||||
## Builder class for constructing a LayerData object
|
||||
class LayerDataBuilder(MeshBuilder):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._layers = {}
|
||||
self._element_counts = {}
|
||||
|
||||
def addLayer(self, layer):
|
||||
if layer not in self._layers:
|
||||
self._layers[layer] = Layer(layer)
|
||||
|
||||
def addPolygon(self, layer, polygon_type, data, line_width):
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
|
||||
p = LayerPolygon(self, polygon_type, data, line_width)
|
||||
self._layers[layer].polygons.append(p)
|
||||
|
||||
def getLayer(self, layer):
|
||||
if layer in self._layers:
|
||||
return self._layers[layer]
|
||||
|
||||
def getLayers(self):
|
||||
return self._layers
|
||||
|
||||
def getElementCounts(self):
|
||||
return self._element_counts
|
||||
|
||||
def setLayerHeight(self, layer, height):
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
|
||||
self._layers[layer].setHeight(height)
|
||||
|
||||
def setLayerThickness(self, layer, thickness):
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
|
||||
self._layers[layer].setThickness(thickness)
|
||||
|
||||
def build(self):
|
||||
vertex_count = 0
|
||||
index_count = 0
|
||||
for layer, data in self._layers.items():
|
||||
vertex_count += data.lineMeshVertexCount()
|
||||
index_count += data.lineMeshElementCount()
|
||||
|
||||
vertices = numpy.empty((vertex_count, 3), numpy.float32)
|
||||
colors = numpy.empty((vertex_count, 4), numpy.float32)
|
||||
indices = numpy.empty((index_count, 2), numpy.int32)
|
||||
|
||||
vertex_offset = 0
|
||||
index_offset = 0
|
||||
for layer, data in self._layers.items():
|
||||
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, indices)
|
||||
self._element_counts[layer] = data.elementCount
|
||||
|
||||
self.addVertices(vertices)
|
||||
self.addColors(colors)
|
||||
self.addIndices(indices.flatten())
|
||||
|
||||
return LayerData(vertices=self.getVertices(), normals=self.getNormals(), indices=self.getIndices(),
|
||||
colors=self.getColors(), uvs=self.getUVCoordinates(), file_name=self.getFileName(),
|
||||
center_position=self.getCenterPosition(), layers=self._layers,
|
||||
element_counts=self._element_counts)
|
||||
13
cura/LayerDataDecorator.py
Normal file
13
cura/LayerDataDecorator.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
## Simple decorator to indicate a scene node holds layer data.
|
||||
class LayerDataDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._layer_data = None
|
||||
|
||||
def getLayerData(self):
|
||||
return self._layer_data
|
||||
|
||||
def setLayerData(self, layer_data):
|
||||
self._layer_data = layer_data
|
||||
199
cura/LayerPolygon.py
Normal file
199
cura/LayerPolygon.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
from UM.Math.Color import Color
|
||||
|
||||
import numpy
|
||||
|
||||
|
||||
class LayerPolygon:
|
||||
NoneType = 0
|
||||
Inset0Type = 1
|
||||
InsetXType = 2
|
||||
SkinType = 3
|
||||
SupportType = 4
|
||||
SkirtType = 5
|
||||
InfillType = 6
|
||||
SupportInfillType = 7
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
|
||||
__jump_map = numpy.logical_or( numpy.arange(10) == NoneType, numpy.arange(10) >= MoveCombingType )
|
||||
|
||||
def __init__(self, mesh, extruder, line_types, data, line_widths):
|
||||
self._mesh = mesh
|
||||
self._extruder = extruder
|
||||
self._types = line_types
|
||||
self._data = data
|
||||
self._line_widths = line_widths
|
||||
|
||||
self._vertex_begin = 0
|
||||
self._vertex_end = 0
|
||||
self._index_begin = 0
|
||||
self._index_end = 0
|
||||
|
||||
self._jump_mask = self.__jump_map[self._types]
|
||||
self._jump_count = numpy.sum(self._jump_mask)
|
||||
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])
|
||||
|
||||
# Buffering the colors shouldn't be necessary as it is not
|
||||
# re-used and can save alot of memory usage.
|
||||
self._colors = self.__color_map[self._types]
|
||||
self._color_map = self.__color_map
|
||||
|
||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||
# Should be generated in better way, not hardcoded.
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None
|
||||
self._build_cache_needed_points = None
|
||||
|
||||
def buildCache(self):
|
||||
# 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 ))
|
||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||
self._index_begin = 0
|
||||
self._index_end = mesh_line_count
|
||||
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool)
|
||||
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||
numpy.logical_and(self._build_cache_needed_points, self._build_cache_line_mesh_mask, self._build_cache_needed_points )
|
||||
|
||||
self._vertex_begin = 0
|
||||
self._vertex_end = numpy.sum( self._build_cache_needed_points )
|
||||
|
||||
|
||||
def build(self, vertex_offset, index_offset, vertices, colors, indices):
|
||||
if (self._build_cache_line_mesh_mask is None) or (self._build_cache_needed_points is None ):
|
||||
self.buildCache()
|
||||
|
||||
line_mesh_mask = self._build_cache_line_mesh_mask
|
||||
needed_points_list = self._build_cache_needed_points
|
||||
|
||||
# Index to the points we need to represent the line mesh. This is constructed by generating simple
|
||||
# start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1]
|
||||
# Then then the indices for the points we don't need are thrown away based on the pre-calculated list.
|
||||
index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))]
|
||||
|
||||
# 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._vertex_begin += vertex_offset
|
||||
self._vertex_end += vertex_offset
|
||||
|
||||
# Points are picked based on the index list to get the vertices needed.
|
||||
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.
|
||||
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)
|
||||
|
||||
# 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_end += index_offset
|
||||
|
||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
|
||||
# When the line type changes the index needs to be increased by 2.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1))
|
||||
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
||||
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
||||
|
||||
self._build_cache_line_mesh_mask = None
|
||||
self._build_cache_needed_points = None
|
||||
|
||||
def getColors(self):
|
||||
return self._colors
|
||||
|
||||
def mapLineTypeToColor(self, line_types):
|
||||
return self._color_map[line_types]
|
||||
|
||||
def isInfillOrSkinType(self, line_types):
|
||||
return self._isInfillOrSkinTypeMap[line_types]
|
||||
|
||||
def lineMeshVertexCount(self):
|
||||
return (self._vertex_end - self._vertex_begin)
|
||||
|
||||
def lineMeshElementCount(self):
|
||||
return (self._index_end - self._index_begin)
|
||||
|
||||
@property
|
||||
def extruder(self):
|
||||
return self._extruder
|
||||
|
||||
@property
|
||||
def types(self):
|
||||
return self._types
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@property
|
||||
def elementCount(self):
|
||||
return (self._index_end - self._index_begin) * 2 # The range of vertices multiplied by 2 since each vertex is used twice
|
||||
|
||||
@property
|
||||
def lineWidths(self):
|
||||
return self._line_widths
|
||||
|
||||
@property
|
||||
def jumpMask(self):
|
||||
return self._jump_mask
|
||||
|
||||
@property
|
||||
def meshLineCount(self):
|
||||
return self._mesh_line_count
|
||||
|
||||
@property
|
||||
def jumpCount(self):
|
||||
return self._jump_count
|
||||
|
||||
# Calculate normals for the entire polygon using numpy.
|
||||
def getNormals(self):
|
||||
normals = numpy.copy(self._data)
|
||||
normals[:, 1] = 0.0 # We are only interested in 2D normals
|
||||
|
||||
# Calculate the edges between points.
|
||||
# The call to numpy.roll shifts the entire array by one so that
|
||||
# we end up subtracting each next point from the current, wrapping
|
||||
# around. This gives us the edges from the next point to the current
|
||||
# point.
|
||||
normals = numpy.diff(normals, 1, 0)
|
||||
|
||||
# Calculate the length of each edge using standard Pythagoras
|
||||
lengths = numpy.sqrt(normals[:, 0] ** 2 + normals[:, 2] ** 2)
|
||||
# The normal of a 2D vector is equal to its x and y coordinates swapped
|
||||
# and then x inverted. This code does that.
|
||||
normals[:, [0, 2]] = normals[:, [2, 0]]
|
||||
normals[:, 0] *= -1
|
||||
|
||||
# Normalize the normals.
|
||||
normals[:, 0] /= lengths
|
||||
normals[:, 2] /= lengths
|
||||
|
||||
return normals
|
||||
|
||||
__color_mapping = {
|
||||
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.
|
||||
__color_map = numpy.array([
|
||||
[1.0, 1.0, 1.0, 1.0],
|
||||
[1.0, 0.0, 0.0, 1.0],
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
[1.0, 1.0, 0.0, 1.0],
|
||||
[0.0, 1.0, 1.0, 1.0],
|
||||
[0.0, 1.0, 1.0, 1.0],
|
||||
[1.0, 0.74, 0.0, 1.0],
|
||||
[0.0, 1.0, 1.0, 1.0],
|
||||
[0.0, 0.0, 1.0, 1.0],
|
||||
[0.5, 0.5, 1.0, 1.0]
|
||||
])
|
||||
79
cura/MachineAction.py
Normal file
79
cura/MachineAction.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
|
||||
from UM.PluginObject import PluginObject
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class MachineAction(QObject, PluginObject):
|
||||
def __init__(self, key, label = ""):
|
||||
super().__init__()
|
||||
self._key = key
|
||||
self._label = label
|
||||
self._qml_url = ""
|
||||
|
||||
self._component = None
|
||||
self._context = None
|
||||
self._view = None
|
||||
self._finished = False
|
||||
|
||||
labelChanged = pyqtSignal()
|
||||
onFinished = pyqtSignal()
|
||||
|
||||
def getKey(self):
|
||||
return self._key
|
||||
|
||||
@pyqtProperty(str, notify = labelChanged)
|
||||
def label(self):
|
||||
return self._label
|
||||
|
||||
def setLabel(self, label):
|
||||
if self._label != label:
|
||||
self._label = label
|
||||
self.labelChanged.emit()
|
||||
|
||||
## Reset the action to it's default state.
|
||||
# This should not be re-implemented by child classes, instead re-implement _reset.
|
||||
# /sa _reset
|
||||
@pyqtSlot()
|
||||
def reset(self):
|
||||
self._component = None
|
||||
self._finished = False
|
||||
self._reset()
|
||||
|
||||
## Protected implementation of reset.
|
||||
# /sa reset()
|
||||
def _reset(self):
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def setFinished(self):
|
||||
self._finished = True
|
||||
self._reset()
|
||||
self.onFinished.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = onFinished)
|
||||
def finished(self):
|
||||
return self._finished
|
||||
|
||||
def _createViewFromQML(self):
|
||||
path = QUrl.fromLocalFile(
|
||||
os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url))
|
||||
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)
|
||||
def displayItem(self):
|
||||
if not self._component:
|
||||
self._createViewFromQML()
|
||||
|
||||
return self._view
|
||||
145
cura/MachineActionManager.py
Normal file
145
cura/MachineActionManager.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
## Raised when trying to add an unknown machine action as a required action
|
||||
class UnknownMachineActionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
## Raised when trying to add a machine action that does not have an unique key.
|
||||
class NotUniqueMachineActionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MachineActionManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._machine_actions = {} # Dict of all known machine actions
|
||||
self._required_actions = {} # Dict of all required actions by definition ID
|
||||
self._supported_actions = {} # Dict of all supported actions by definition ID
|
||||
self._first_start_actions = {} # Dict of all actions that need to be done when first added by definition ID
|
||||
|
||||
# Add machine_action as plugin type
|
||||
PluginRegistry.addType("machine_action", self.addMachineAction)
|
||||
|
||||
# Ensure that all containers that were registered before creation of this registry are also handled.
|
||||
# This should not have any effect, but it makes it safer if we ever refactor the order of things.
|
||||
for container in ContainerRegistry.getInstance().findDefinitionContainers():
|
||||
self._onContainerAdded(container)
|
||||
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
## Ensure that the actions are added to this manager
|
||||
if isinstance(container, DefinitionContainer):
|
||||
supported_actions = container.getMetaDataEntry("supported_actions", [])
|
||||
for action in supported_actions:
|
||||
self.addSupportedAction(container.getId(), action)
|
||||
|
||||
required_actions = container.getMetaDataEntry("required_actions", [])
|
||||
for action in required_actions:
|
||||
self.addRequiredAction(container.getId(), action)
|
||||
|
||||
first_start_actions = container.getMetaDataEntry("first_start_actions", [])
|
||||
for action in first_start_actions:
|
||||
self.addFirstStartAction(container.getId(), action)
|
||||
|
||||
## Add a required action to a machine
|
||||
# Raises an exception when the action is not recognised.
|
||||
def addRequiredAction(self, definition_id, action_key):
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._required_actions:
|
||||
if self._machine_actions[action_key] not in self._required_actions[definition_id]:
|
||||
self._required_actions[definition_id].append(self._machine_actions[action_key])
|
||||
else:
|
||||
self._required_actions[definition_id] = [self._machine_actions[action_key]]
|
||||
else:
|
||||
raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
|
||||
|
||||
## Add a supported action to a machine.
|
||||
def addSupportedAction(self, definition_id, action_key):
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._supported_actions:
|
||||
if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
|
||||
self._supported_actions[definition_id].append(self._machine_actions[action_key])
|
||||
else:
|
||||
self._supported_actions[definition_id] = [self._machine_actions[action_key]]
|
||||
else:
|
||||
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
|
||||
|
||||
## Add an action to the first start list of a machine.
|
||||
def addFirstStartAction(self, definition_id, action_key, index = None):
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._first_start_actions:
|
||||
if index is not None:
|
||||
self._first_start_actions[definition_id].insert(index, self._machine_actions[action_key])
|
||||
else:
|
||||
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
|
||||
else:
|
||||
self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
|
||||
else:
|
||||
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
|
||||
|
||||
## Add a (unique) MachineAction
|
||||
# if the Key of the action is not unique, an exception is raised.
|
||||
def addMachineAction(self, action):
|
||||
if action.getKey() not in self._machine_actions:
|
||||
self._machine_actions[action.getKey()] = action
|
||||
else:
|
||||
raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey())
|
||||
|
||||
## Get all actions supported by given machine
|
||||
# \param definition_id The ID of the definition you want the supported actions of
|
||||
# \returns set of supported actions.
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getSupportedActions(self, definition_id):
|
||||
if definition_id in self._supported_actions:
|
||||
return list(self._supported_actions[definition_id])
|
||||
else:
|
||||
return set()
|
||||
|
||||
## Get all actions required by given machine
|
||||
# \param definition_id The ID of the definition you want the required actions of
|
||||
# \returns set of required actions.
|
||||
def getRequiredActions(self, definition_id):
|
||||
if definition_id in self._required_actions:
|
||||
return self._required_actions[definition_id]
|
||||
else:
|
||||
return set()
|
||||
|
||||
## Get all actions that need to be performed upon first start of a given machine.
|
||||
# Note that contrary to required / supported actions a list is returned (as it could be required to run the same
|
||||
# action multiple times).
|
||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||
# \returns List of actions.
|
||||
@pyqtSlot(str, result="QVariantList")
|
||||
def getFirstStartActions(self, definition_id):
|
||||
if definition_id in self._first_start_actions:
|
||||
return self._first_start_actions[definition_id]
|
||||
else:
|
||||
return []
|
||||
|
||||
## Remove Machine action from manager
|
||||
# \param action to remove
|
||||
def removeMachineAction(self, action):
|
||||
try:
|
||||
del self._machine_actions[action.getKey()]
|
||||
except KeyError:
|
||||
Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey())
|
||||
|
||||
## Get MachineAction by key
|
||||
# \param key String of key to select
|
||||
# \return Machine action if found, None otherwise
|
||||
def getMachineAction(self, key):
|
||||
if key in self._machine_actions:
|
||||
return self._machine_actions[key]
|
||||
else:
|
||||
return None
|
||||
11
cura/MultiMaterialDecorator.py
Normal file
11
cura/MultiMaterialDecorator.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
class MultiMaterialDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def isMultiMaterial(self):
|
||||
return True
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return MultiMaterialDecorator()
|
||||
111
cura/OneAtATimeIterator.py
Normal file
111
cura/OneAtATimeIterator.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Scene.Iterator import Iterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from functools import cmp_to_key
|
||||
from UM.Application import Application
|
||||
|
||||
## Iterator that returns a list of nodes in the order that they need to be printed
|
||||
# If there is no solution an empty list is returned.
|
||||
# Take note that the list of nodes can have children (that may or may not contain mesh data)
|
||||
class OneAtATimeIterator(Iterator.Iterator):
|
||||
def __init__(self, scene_node):
|
||||
super().__init__(scene_node) # Call super to make multiple inheritence work.
|
||||
self._hit_map = [[]]
|
||||
self._original_node_list = []
|
||||
|
||||
def _fillStack(self):
|
||||
node_list = []
|
||||
for node in self._scene_node.getChildren():
|
||||
if not type(node) is SceneNode:
|
||||
continue
|
||||
|
||||
if node.callDecoration("getConvexHull"):
|
||||
node_list.append(node)
|
||||
|
||||
if len(node_list) < 2:
|
||||
self._node_stack = node_list[:]
|
||||
return
|
||||
|
||||
# Copy the list
|
||||
self._original_node_list = node_list[:]
|
||||
|
||||
## Initialise the hit map (pre-compute all hits between all objects)
|
||||
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
|
||||
|
||||
# Check if we have to files that block eachother. If this is the case, there is no solution!
|
||||
for a in range(0,len(node_list)):
|
||||
for b in range(0,len(node_list)):
|
||||
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
|
||||
return
|
||||
|
||||
# Sort the original list so that items that block the most other objects are at the beginning.
|
||||
# This does not decrease the worst case running time, but should improve it in most cases.
|
||||
sorted(node_list, key = cmp_to_key(self._calculateScore))
|
||||
|
||||
todo_node_list = [_ObjectOrder([], node_list)]
|
||||
while len(todo_node_list) > 0:
|
||||
current = todo_node_list.pop()
|
||||
for node in current.todo:
|
||||
# Check if the object can be placed with what we have and still allows for a solution in the future
|
||||
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
|
||||
# We found a possible result. Create new todo & order list.
|
||||
new_todo_list = current.todo[:]
|
||||
new_todo_list.remove(node)
|
||||
new_order = current.order[:] + [node]
|
||||
if len(new_todo_list) == 0:
|
||||
# We have no more nodes to check, so quit looking.
|
||||
todo_node_list = None
|
||||
self._node_stack = new_order
|
||||
|
||||
return
|
||||
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
|
||||
self._node_stack = [] #No result found!
|
||||
|
||||
|
||||
# Check if first object can be printed before the provided list (using the hit map)
|
||||
def _checkHitMultiple(self, node, other_nodes):
|
||||
node_index = self._original_node_list.index(node)
|
||||
for other_node in other_nodes:
|
||||
other_node_index = self._original_node_list.index(other_node)
|
||||
if self._hit_map[node_index][other_node_index]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _checkBlockMultiple(self, node, other_nodes):
|
||||
node_index = self._original_node_list.index(node)
|
||||
for other_node in other_nodes:
|
||||
other_node_index = self._original_node_list.index(other_node)
|
||||
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
|
||||
return True
|
||||
return False
|
||||
|
||||
## Calculate score simply sums the number of other objects it 'blocks'
|
||||
def _calculateScore(self, a, b):
|
||||
score_a = sum(self._hit_map[self._original_node_list.index(a)])
|
||||
score_b = sum(self._hit_map[self._original_node_list.index(b)])
|
||||
return score_a - score_b
|
||||
|
||||
# Checks if A can be printed before B
|
||||
def _checkHit(self, a, b):
|
||||
if a == b:
|
||||
return False
|
||||
|
||||
overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull"))
|
||||
if overlap:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
## Internal object used to keep track of a possible order in which to print objects.
|
||||
class _ObjectOrder():
|
||||
def __init__(self, order, todo):
|
||||
"""
|
||||
:param order: List of indexes in which to print objects, ordered by printing order.
|
||||
:param todo: List of indexes which are not yet inserted into the order list.
|
||||
"""
|
||||
self.order = order
|
||||
self.todo = todo
|
||||
|
||||
147
cura/PlatformPhysics.py
Normal file
147
cura/PlatformPhysics.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
from cura.ConvexHullDecorator import ConvexHullDecorator
|
||||
|
||||
from . import PlatformPhysicsOperation
|
||||
from . import ZOffsetDecorator
|
||||
|
||||
class PlatformPhysics:
|
||||
def __init__(self, controller, volume):
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
|
||||
self._controller.toolOperationStarted.connect(self._onToolOperationStarted)
|
||||
self._controller.toolOperationStopped.connect(self._onToolOperationStopped)
|
||||
self._build_volume = volume
|
||||
self._enabled = True
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(100)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
||||
|
||||
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
self._change_timer.start()
|
||||
|
||||
def _onChangeTimerFinished(self):
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
root = self._controller.getScene().getRoot()
|
||||
|
||||
for node in BreadthFirstIterator(root):
|
||||
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
|
||||
continue
|
||||
|
||||
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_vector = Vector()
|
||||
if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
|
||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
|
||||
|
||||
# If there is no convex hull for the node, start calculating it and continue.
|
||||
if not node.getDecorator(ConvexHullDecorator):
|
||||
node.addDecorator(ConvexHullDecorator())
|
||||
|
||||
if Preferences.getInstance().getValue("physics/automatic_push_free"):
|
||||
# Check for collisions between convex hulls
|
||||
for other_node in BreadthFirstIterator(root):
|
||||
# Ignore root, ourselves and anything that is not a normal SceneNode.
|
||||
if other_node is root or type(other_node) is not SceneNode or other_node is node:
|
||||
continue
|
||||
|
||||
# Ignore collisions of a group with it's own children
|
||||
if other_node in node.getAllChildren() or node in other_node.getAllChildren():
|
||||
continue
|
||||
|
||||
# Ignore collisions within a group
|
||||
if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
|
||||
continue
|
||||
|
||||
# Ignore nodes that do not have the right properties set.
|
||||
if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
|
||||
continue
|
||||
|
||||
# Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
|
||||
head_hull = node.callDecoration("getConvexHullHead")
|
||||
if head_hull:
|
||||
overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHullHead"))
|
||||
if not overlap:
|
||||
other_head_hull = other_node.callDecoration("getConvexHullHead")
|
||||
if other_head_hull:
|
||||
overlap = node.callDecoration("getConvexHullHead").intersectsPolygon(other_head_hull)
|
||||
else:
|
||||
own_convex_hull = node.callDecoration("getConvexHull")
|
||||
other_convex_hull = other_node.callDecoration("getConvexHull")
|
||||
if own_convex_hull and other_convex_hull:
|
||||
overlap = own_convex_hull.intersectsPolygon(other_convex_hull)
|
||||
else:
|
||||
# This can happen in some cases if the object is not yet done with being loaded.
|
||||
# Simply waiting for the next tick seems to resolve this correctly.
|
||||
overlap = None
|
||||
|
||||
if overlap is None:
|
||||
continue
|
||||
move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1)
|
||||
convex_hull = node.callDecoration("getConvexHull")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
return
|
||||
# Check for collisions between disallowed areas and the object
|
||||
for area in self._build_volume.getDisallowedAreas():
|
||||
overlap = convex_hull.intersectsPolygon(area)
|
||||
if overlap is None:
|
||||
continue
|
||||
|
||||
node._outside_buildarea = True
|
||||
|
||||
if not Vector.Null.equals(move_vector, epsilon=1e-5):
|
||||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||
op.push()
|
||||
|
||||
def _onToolOperationStarted(self, tool):
|
||||
self._enabled = False
|
||||
|
||||
def _onToolOperationStopped(self, tool):
|
||||
if tool.getPluginId() == "TranslateTool":
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
if node.getBoundingBox().bottom < 0:
|
||||
if not node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
|
||||
node.addDecorator(ZOffsetDecorator.ZOffsetDecorator())
|
||||
|
||||
node.callDecoration("setZOffset", node.getBoundingBox().bottom)
|
||||
else:
|
||||
if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
|
||||
node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
|
||||
|
||||
self._enabled = True
|
||||
self._onChangeTimerFinished()
|
||||
31
cura/PlatformPhysicsOperation.py
Normal file
31
cura/PlatformPhysicsOperation.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Operations.Operation import Operation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
|
||||
## A specialised operation designed specifically to modify the previous operation.
|
||||
class PlatformPhysicsOperation(Operation):
|
||||
def __init__(self, node, translation):
|
||||
super().__init__()
|
||||
self._node = node
|
||||
self._old_position = node.getPosition()
|
||||
self._new_position = node.getPosition() + translation
|
||||
self._always_merge = True
|
||||
|
||||
def undo(self):
|
||||
self._node.setPosition(self._old_position)
|
||||
|
||||
def redo(self):
|
||||
self._node.setPosition(self._new_position)
|
||||
|
||||
def mergeWith(self, other):
|
||||
group = GroupedOperation()
|
||||
|
||||
group.addOperation(self)
|
||||
group.addOperation(other)
|
||||
|
||||
return group
|
||||
|
||||
def __repr__(self):
|
||||
return "PlatformPhysicsOperation(new_position = {0})".format(self._new_position)
|
||||
147
cura/PrintInformation.py
Normal file
147
cura/PrintInformation.py
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
import cura.Settings.ExtruderManager
|
||||
|
||||
import math
|
||||
import os.path
|
||||
import unicodedata
|
||||
|
||||
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
||||
#
|
||||
# This class contains all the logic relating to calculation and slicing for the
|
||||
# time/quality slider concept. It is a rather tricky combination of event handling
|
||||
# and state management. The logic behind this is as follows:
|
||||
#
|
||||
# - A scene change or setting change event happens.
|
||||
# We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
|
||||
# - 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.
|
||||
# - 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 the high quality 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 job name is requested by the JobSpecs qml file.
|
||||
class PrintInformation(QObject):
|
||||
class SlicePass:
|
||||
CurrentSettings = 1
|
||||
LowQualitySettings = 2
|
||||
HighQualitySettings = 3
|
||||
|
||||
class SliceReason:
|
||||
SceneChanged = 1
|
||||
SettingChanged = 2
|
||||
ActiveMachineChanged = 3
|
||||
Other = 4
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._current_print_time = Duration(None, self)
|
||||
|
||||
self._material_lengths = []
|
||||
self._material_weights = []
|
||||
|
||||
self._backend = Application.getInstance().getBackend()
|
||||
if self._backend:
|
||||
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
||||
|
||||
self._job_name = ""
|
||||
self._abbr_machine = ""
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
|
||||
Application.getInstance().fileLoaded.connect(self.setJobName)
|
||||
|
||||
currentPrintTimeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
|
||||
def currentPrintTime(self):
|
||||
return self._current_print_time
|
||||
|
||||
materialLengthsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialLengthsChanged)
|
||||
def materialLengths(self):
|
||||
return self._material_lengths
|
||||
|
||||
materialWeightsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialWeightsChanged)
|
||||
def materialWeights(self):
|
||||
return self._material_weights
|
||||
|
||||
def _onPrintDurationMessage(self, total_time, material_amounts):
|
||||
self._current_print_time.setDuration(total_time)
|
||||
self.currentPrintTimeChanged.emit()
|
||||
|
||||
# Material amount is sent as an amount of mm^3, so calculate length from that
|
||||
r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
||||
self._material_lengths = []
|
||||
self._material_weights = []
|
||||
extruder_stacks = list(cura.Settings.ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
|
||||
for index, amount in enumerate(material_amounts):
|
||||
## 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.
|
||||
if extruder_stacks: # Multi extrusion machine
|
||||
extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
|
||||
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
|
||||
else: # Machine with no extruder stacks
|
||||
density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
|
||||
|
||||
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.materialWeightsChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setJobName(self, name):
|
||||
# Ensure that we don't use entire path but only filename
|
||||
name = os.path.basename(name)
|
||||
|
||||
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
||||
# extension. This cuts the extension off if necessary.
|
||||
name = os.path.splitext(name)[0]
|
||||
if self._job_name != name:
|
||||
self._job_name = name
|
||||
self.jobNameChanged.emit()
|
||||
|
||||
jobNameChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = jobNameChanged)
|
||||
def jobName(self):
|
||||
return self._job_name
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def createJobName(self, base_name):
|
||||
base_name = self._stripAccents(base_name)
|
||||
if Preferences.getInstance().getValue("cura/jobname_prefix"):
|
||||
return self._abbr_machine + "_" + base_name
|
||||
else:
|
||||
return base_name
|
||||
|
||||
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
||||
# Called each time the global stack is switched
|
||||
def _setAbbreviatedMachineName(self):
|
||||
global_stack_name = Application.getInstance().getGlobalContainerStack().getName()
|
||||
split_name = global_stack_name.split(" ")
|
||||
abbr_machine = ""
|
||||
for word in split_name:
|
||||
if word.lower() == "ultimaker":
|
||||
abbr_machine += "UM"
|
||||
elif word.isdigit():
|
||||
abbr_machine += word
|
||||
else:
|
||||
abbr_machine += self._stripAccents(word.strip("()[]{}#").upper())[0]
|
||||
|
||||
self._abbr_machine = abbr_machine
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, str):
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
|
||||
466
cura/PrinterOutputDevice.py
Normal file
466
cura/PrinterOutputDevice.py
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Signal import signalemitter
|
||||
|
||||
## Printer output device adds extra interface options on top of output device.
|
||||
#
|
||||
# The assumption is made the printer is a FDM printer.
|
||||
#
|
||||
# Note that a number of settings are marked as "final". This is because decorators
|
||||
# are not inherited by children. To fix this we use the private counter part of those
|
||||
# functions to actually have the implementation.
|
||||
#
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
def __init__(self, device_id, parent = None):
|
||||
super().__init__(device_id = device_id, parent = parent)
|
||||
|
||||
self._target_bed_temperature = 0
|
||||
self._bed_temperature = 0
|
||||
self._num_extruders = 1
|
||||
self._hotend_temperatures = [0] * self._num_extruders
|
||||
self._target_hotend_temperatures = [0] * self._num_extruders
|
||||
self._material_ids = [""] * self._num_extruders
|
||||
self._hotend_ids = [""] * self._num_extruders
|
||||
self._progress = 0
|
||||
self._head_x = 0
|
||||
self._head_y = 0
|
||||
self._head_z = 0
|
||||
self._connection_state = ConnectionState.closed
|
||||
self._connection_text = ""
|
||||
self._time_elapsed = 0
|
||||
self._time_total = 0
|
||||
self._job_state = ""
|
||||
self._job_name = ""
|
||||
self._error_text = ""
|
||||
self._accepts_commands = True
|
||||
|
||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
## Signals
|
||||
|
||||
# Signal to be emitted when bed temp is changed
|
||||
bedTemperatureChanged = pyqtSignal()
|
||||
|
||||
# Signal to be emitted when target bed temp is changed
|
||||
targetBedTemperatureChanged = pyqtSignal()
|
||||
|
||||
# Signal when the progress is changed (usually when this output device is printing / sending lots of data)
|
||||
progressChanged = pyqtSignal()
|
||||
|
||||
# Signal to be emitted when hotend temp is changed
|
||||
hotendTemperaturesChanged = pyqtSignal()
|
||||
|
||||
# Signal to be emitted when target hotend temp is changed
|
||||
targetHotendTemperaturesChanged = pyqtSignal()
|
||||
|
||||
# Signal to be emitted when head position is changed (x,y,z)
|
||||
headPositionChanged = pyqtSignal()
|
||||
|
||||
# Signal to be emitted when either of the material ids is changed
|
||||
materialIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
|
||||
|
||||
# Signal to be emitted when either of the hotend ids is changed
|
||||
hotendIdChanged = pyqtSignal(int, str, arguments = ["index", "id"])
|
||||
|
||||
# Signal that is emitted every time connection state is changed.
|
||||
# it also sends it's own device_id (for convenience sake)
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
|
||||
connectionTextChanged = pyqtSignal()
|
||||
|
||||
timeElapsedChanged = pyqtSignal()
|
||||
|
||||
timeTotalChanged = pyqtSignal()
|
||||
|
||||
jobStateChanged = pyqtSignal()
|
||||
|
||||
jobNameChanged = pyqtSignal()
|
||||
|
||||
errorTextChanged = pyqtSignal()
|
||||
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = jobStateChanged)
|
||||
def jobState(self):
|
||||
return self._job_state
|
||||
|
||||
def _updateJobState(self, job_state):
|
||||
if self._job_state != job_state:
|
||||
self._job_state = job_state
|
||||
self.jobStateChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setJobState(self, job_state):
|
||||
self._setJobState(job_state)
|
||||
|
||||
def _setJobState(self, job_state):
|
||||
Logger.log("w", "_setJobState is not implemented by this output device")
|
||||
|
||||
@pyqtProperty(str, notify = jobNameChanged)
|
||||
def jobName(self):
|
||||
return self._job_name
|
||||
|
||||
def setJobName(self, name):
|
||||
if self._job_name != name:
|
||||
self._job_name = name
|
||||
self.jobNameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = errorTextChanged)
|
||||
def errorText(self):
|
||||
return self._error_text
|
||||
|
||||
## Set the error-text that is shown in the print monitor in case of an error
|
||||
def setErrorText(self, error_text):
|
||||
if self._error_text != error_text:
|
||||
self._error_text = error_text
|
||||
self.errorTextChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||
def acceptsCommands(self):
|
||||
return self._accepts_commands
|
||||
|
||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||
def setAcceptsCommands(self, accepts_commands):
|
||||
if self._accepts_commands != accepts_commands:
|
||||
self._accepts_commands = accepts_commands
|
||||
self.acceptsCommandsChanged.emit()
|
||||
|
||||
## Get the bed temperature of the bed (if any)
|
||||
# This function is "final" (do not re-implement)
|
||||
# /sa _getBedTemperature implementation function
|
||||
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||
def bedTemperature(self):
|
||||
return self._bed_temperature
|
||||
|
||||
## Set the (target) bed temperature
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param temperature new target temperature of the bed (in deg C)
|
||||
# /sa _setTargetBedTemperature implementation function
|
||||
@pyqtSlot(int)
|
||||
def setTargetBedTemperature(self, temperature):
|
||||
self._setTargetBedTemperature(temperature)
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Time the print has been printing.
|
||||
# Note that timeTotal - timeElapsed should give time remaining.
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def timeElapsed(self):
|
||||
return self._time_elapsed
|
||||
|
||||
## Total time of the print
|
||||
# Note that timeTotal - timeElapsed should give time remaining.
|
||||
@pyqtProperty(float, notify=timeTotalChanged)
|
||||
def timeTotal(self):
|
||||
return self._time_total
|
||||
|
||||
@pyqtSlot(float)
|
||||
def setTimeTotal(self, new_total):
|
||||
if self._time_total != new_total:
|
||||
self._time_total = new_total
|
||||
self.timeTotalChanged.emit()
|
||||
|
||||
@pyqtSlot(float)
|
||||
def setTimeElapsed(self, time_elapsed):
|
||||
if self._time_elapsed != time_elapsed:
|
||||
self._time_elapsed = time_elapsed
|
||||
self.timeElapsedChanged.emit()
|
||||
|
||||
## Home the head of the connected printer
|
||||
# This function is "final" (do not re-implement)
|
||||
# /sa _homeHead implementation function
|
||||
@pyqtSlot()
|
||||
def homeHead(self):
|
||||
self._homeHead()
|
||||
|
||||
## Home the head of the connected printer
|
||||
# This is an implementation function and should be overriden by children.
|
||||
def _homeHead(self):
|
||||
Logger.log("w", "_homeHead is not implemented by this output device")
|
||||
|
||||
## Home the bed of the connected printer
|
||||
# This function is "final" (do not re-implement)
|
||||
# /sa _homeBed implementation function
|
||||
@pyqtSlot()
|
||||
def homeBed(self):
|
||||
self._homeBed()
|
||||
|
||||
## Home the bed of the connected printer
|
||||
# This is an implementation function and should be overriden by children.
|
||||
# /sa homeBed
|
||||
def _homeBed(self):
|
||||
Logger.log("w", "_homeBed is not implemented by this output device")
|
||||
|
||||
## Protected setter for the bed temperature of the connected printer (if any).
|
||||
# /parameter temperature Temperature bed needs to go to (in deg celsius)
|
||||
# /sa setTargetBedTemperature
|
||||
def _setTargetBedTemperature(self, temperature):
|
||||
Logger.log("w", "_setTargetBedTemperature is not implemented by this output device")
|
||||
|
||||
## Protected setter for the current bed temperature.
|
||||
# This simply sets the bed temperature, but ensures that a signal is emitted.
|
||||
# /param temperature temperature of the bed.
|
||||
def _setBedTemperature(self, temperature):
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
## Get the target bed temperature if connected printer (if any)
|
||||
@pyqtProperty(int, notify = targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self):
|
||||
return self._target_bed_temperature
|
||||
|
||||
## Set the (target) hotend temperature
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param index the index of the hotend that needs to change temperature
|
||||
# /param temperature The temperature it needs to change to (in deg celsius).
|
||||
# /sa _setTargetHotendTemperature implementation function
|
||||
@pyqtSlot(int, int)
|
||||
def setTargetHotendTemperature(self, index, temperature):
|
||||
self._setTargetHotendTemperature(index, temperature)
|
||||
self._target_hotend_temperatures[index] = temperature
|
||||
self.targetHotendTemperaturesChanged.emit()
|
||||
|
||||
## Implementation function of setTargetHotendTemperature.
|
||||
# /param index Index of the hotend to set the temperature of
|
||||
# /param temperature Temperature to set the hotend to (in deg C)
|
||||
# /sa setTargetHotendTemperature
|
||||
def _setTargetHotendTemperature(self, index, temperature):
|
||||
Logger.log("w", "_setTargetHotendTemperature is not implemented by this output device")
|
||||
|
||||
@pyqtProperty("QVariantList", notify = targetHotendTemperaturesChanged)
|
||||
def targetHotendTemperatures(self):
|
||||
return self._target_hotend_temperatures
|
||||
|
||||
@pyqtProperty("QVariantList", notify = hotendTemperaturesChanged)
|
||||
def hotendTemperatures(self):
|
||||
return self._hotend_temperatures
|
||||
|
||||
## Protected setter for the current hotend temperature.
|
||||
# This simply sets the hotend temperature, but ensures that a signal is emitted.
|
||||
# /param index Index of the hotend
|
||||
# /param temperature temperature of the hotend (in deg C)
|
||||
def _setHotendTemperature(self, index, temperature):
|
||||
self._hotend_temperatures[index] = temperature
|
||||
self.hotendTemperaturesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||
def materialIds(self):
|
||||
return self._material_ids
|
||||
|
||||
## Protected setter for the current material id.
|
||||
# /param index Index of the extruder
|
||||
# /param material_id id of the material
|
||||
def _setMaterialId(self, index, material_id):
|
||||
if material_id and material_id != "" and material_id != self._material_ids[index]:
|
||||
Logger.log("d", "Setting material id of hotend %d to %s" % (index, material_id))
|
||||
self._material_ids[index] = material_id
|
||||
self.materialIdChanged.emit(index, material_id)
|
||||
|
||||
|
||||
@pyqtProperty("QVariantList", notify = hotendIdChanged)
|
||||
def hotendIds(self):
|
||||
return self._hotend_ids
|
||||
|
||||
## Protected setter for the current hotend id.
|
||||
# /param index Index of the extruder
|
||||
# /param hotend_id id of the hotend
|
||||
def _setHotendId(self, index, hotend_id):
|
||||
if hotend_id and hotend_id != "" and hotend_id != self._hotend_ids[index]:
|
||||
Logger.log("d", "Setting hotend id of hotend %d to %s" % (index, hotend_id))
|
||||
self._hotend_ids[index] = hotend_id
|
||||
self.hotendIdChanged.emit(index, hotend_id)
|
||||
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self):
|
||||
raise NotImplementedError("connect needs to be implemented")
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self):
|
||||
raise NotImplementedError("close needs to be implemented")
|
||||
|
||||
@pyqtProperty(bool, notify = connectionStateChanged)
|
||||
def connectionState(self):
|
||||
return self._connection_state
|
||||
|
||||
## Set the connection state of this output device.
|
||||
# /param connection_state ConnectionState enum.
|
||||
def setConnectionState(self, connection_state):
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(str, notify = connectionTextChanged)
|
||||
def connectionText(self):
|
||||
return self._connection_text
|
||||
|
||||
## Set a text that is shown on top of the print monitor tab
|
||||
def setConnectionText(self, connection_text):
|
||||
if self._connection_text != connection_text:
|
||||
self._connection_text = connection_text
|
||||
self.connectionTextChanged.emit()
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
## Get the x position of the head.
|
||||
# This function is "final" (do not re-implement)
|
||||
@pyqtProperty(float, notify = headPositionChanged)
|
||||
def headX(self):
|
||||
return self._head_x
|
||||
|
||||
## Get the y position of the head.
|
||||
# This function is "final" (do not re-implement)
|
||||
@pyqtProperty(float, notify = headPositionChanged)
|
||||
def headY(self):
|
||||
return self._head_y
|
||||
|
||||
## Get the z position of the head.
|
||||
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
|
||||
# This function is "final" (do not re-implement)
|
||||
@pyqtProperty(float, notify = headPositionChanged)
|
||||
def headZ(self):
|
||||
return self._head_z
|
||||
|
||||
## Update the saved position of the head
|
||||
# This function should be called when a new position for the head is recieved.
|
||||
def _updateHeadPosition(self, x, y ,z):
|
||||
position_changed = False
|
||||
if self._head_x != x:
|
||||
self._head_x = x
|
||||
position_changed = True
|
||||
if self._head_y != y:
|
||||
self._head_y = y
|
||||
position_changed = True
|
||||
if self._head_z != z:
|
||||
self._head_z = z
|
||||
position_changed = True
|
||||
if position_changed:
|
||||
self.headPositionChanged.emit()
|
||||
|
||||
## Set the position of the head.
|
||||
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param x new x location of the head.
|
||||
# /param y new y location of the head.
|
||||
# /param z new z location of the head.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadPosition implementation function
|
||||
@pyqtSlot("long", "long", "long")
|
||||
@pyqtSlot("long", "long", "long", "long")
|
||||
def setHeadPosition(self, x, y, z, speed = 3000):
|
||||
self._setHeadPosition(x, y , z, speed)
|
||||
|
||||
## Set the X position of the head.
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param x x position head needs to move to.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadx implementation function
|
||||
@pyqtSlot("long")
|
||||
@pyqtSlot("long", "long")
|
||||
def setHeadX(self, x, speed = 3000):
|
||||
self._setHeadX(x, speed)
|
||||
|
||||
## Set the Y position of the head.
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param y y position head needs to move to.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadY implementation function
|
||||
@pyqtSlot("long")
|
||||
@pyqtSlot("long", "long")
|
||||
def setHeadY(self, y, speed = 3000):
|
||||
self._setHeadY(y, speed)
|
||||
|
||||
## Set the Z position of the head.
|
||||
# In some machines it's actually the bed that moves. For convenience sake we simply see it all as head movements.
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param z z position head needs to move to.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadZ implementation function
|
||||
@pyqtSlot("long")
|
||||
@pyqtSlot("long", "long")
|
||||
def setHeadZ(self, z, speed = 3000):
|
||||
self._setHeadY(z, speed)
|
||||
|
||||
## 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
|
||||
# setHeadPosition
|
||||
# This function is "final" (do not re-implement)
|
||||
# /param x distance in x to move
|
||||
# /param y distance in y to move
|
||||
# /param z distance in z to move
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _moveHead implementation function
|
||||
@pyqtSlot("long", "long", "long")
|
||||
@pyqtSlot("long", "long", "long", "long")
|
||||
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
|
||||
self._moveHead(x, y, z, speed)
|
||||
|
||||
## Implementation function of moveHead.
|
||||
# /param x distance in x to move
|
||||
# /param y distance in y to move
|
||||
# /param z distance in z to move
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa moveHead
|
||||
def _moveHead(self, x, y, z, speed):
|
||||
Logger.log("w", "_moveHead is not implemented by this output device")
|
||||
|
||||
## Implementation function of setHeadPosition.
|
||||
# /param x new x location of the head.
|
||||
# /param y new y location of the head.
|
||||
# /param z new z location of the head.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa setHeadPosition
|
||||
def _setHeadPosition(self, x, y, z, speed):
|
||||
Logger.log("w", "_setHeadPosition is not implemented by this output device")
|
||||
|
||||
## Implementation function of setHeadX.
|
||||
# /param x new x location of the head.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa setHeadX
|
||||
def _setHeadX(self, x, speed):
|
||||
Logger.log("w", "_setHeadX is not implemented by this output device")
|
||||
|
||||
## Implementation function of setHeadY.
|
||||
# /param y new y location of the head.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadY
|
||||
def _setHeadY(self, y, speed):
|
||||
Logger.log("w", "_setHeadY is not implemented by this output device")
|
||||
|
||||
## Implementation function of setHeadZ.
|
||||
# /param z new z location of the head.
|
||||
# /param speed Speed by which it needs to move (in mm/minute)
|
||||
# /sa _setHeadZ
|
||||
def _setHeadZ(self, z, speed):
|
||||
Logger.log("w", "_setHeadZ is not implemented by this output device")
|
||||
|
||||
## Get the progress of any currently active process.
|
||||
# This function is "final" (do not re-implement)
|
||||
# /sa _getProgress
|
||||
# /returns float progress of the process. -1 indicates that there is no process.
|
||||
@pyqtProperty(float, notify = progressChanged)
|
||||
def progress(self):
|
||||
return self._progress
|
||||
|
||||
## Set the progress of any currently active process
|
||||
# /param progress Progress of the process.
|
||||
def setProgress(self, progress):
|
||||
if self._progress != progress:
|
||||
self._progress = progress
|
||||
self.progressChanged.emit()
|
||||
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
closed = 0
|
||||
connecting = 1
|
||||
connected = 2
|
||||
busy = 3
|
||||
error = 4
|
||||
17
cura/ProfileReader.py
Normal file
17
cura/ProfileReader.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.PluginObject import PluginObject
|
||||
|
||||
## A type of plug-ins that reads profiles from a file.
|
||||
#
|
||||
# The profile is then stored as instance container of the type user profile.
|
||||
class ProfileReader(PluginObject):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Read profile data from a file and return a filled profile.
|
||||
#
|
||||
# \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles.
|
||||
def read(self, file_name):
|
||||
raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.")
|
||||
25
cura/ProfileWriter.py
Normal file
25
cura/ProfileWriter.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.PluginObject import PluginObject
|
||||
|
||||
## Base class for profile writer plugins.
|
||||
#
|
||||
# This class defines a write() function to write profiles to files with.
|
||||
class ProfileWriter(PluginObject):
|
||||
## Initialises the profile writer.
|
||||
#
|
||||
# This currently doesn't do anything since the writer is basically static.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Writes a profile to the specified file path.
|
||||
#
|
||||
# The profile writer may write its own file format to the specified file.
|
||||
#
|
||||
# \param path \type{string} The file to output to.
|
||||
# \param profile \type{Profile} The profile to write to the file.
|
||||
# \return \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
def write(self, path, node):
|
||||
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")
|
||||
50
cura/SetParentOperation.py
Normal file
50
cura/SetParentOperation.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Operations import Operation
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
## An operation that parents a scene node to another scene node.
|
||||
|
||||
class SetParentOperation(Operation.Operation):
|
||||
## Initialises this SetParentOperation.
|
||||
#
|
||||
# \param node The node which will be reparented.
|
||||
# \param parent_node The node which will be the parent.
|
||||
def __init__(self, node, parent_node):
|
||||
super().__init__()
|
||||
self._node = node
|
||||
self._parent = parent_node
|
||||
self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
|
||||
|
||||
## Undoes the set-parent operation, restoring the old parent.
|
||||
def undo(self):
|
||||
self._set_parent(self._old_parent)
|
||||
|
||||
## Re-applies the set-parent operation.
|
||||
def redo(self):
|
||||
self._set_parent(self._parent)
|
||||
|
||||
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
|
||||
#
|
||||
# \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):
|
||||
if new_parent:
|
||||
self._node.setPosition(self._node.getWorldPosition() - new_parent.getWorldPosition())
|
||||
current_parent = self._node.getParent()
|
||||
if current_parent:
|
||||
self._node.scale(current_parent.getScale() / new_parent.getScale())
|
||||
self._node.rotate(current_parent.getOrientation())
|
||||
else:
|
||||
self._node.scale(Vector(1, 1, 1) / new_parent.getScale())
|
||||
self._node.rotate(new_parent.getOrientation().getInverse())
|
||||
|
||||
self._node.setParent(new_parent)
|
||||
|
||||
## Returns a programmer-readable representation of this operation.
|
||||
#
|
||||
# \return A programmer-readable representation of this operation.
|
||||
def __repr__(self):
|
||||
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)
|
||||
400
cura/Settings/ContainerManager.py
Normal file
400
cura/Settings/ContainerManager.py
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
import urllib
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
import UM.PluginRegistry
|
||||
import UM.Settings
|
||||
import UM.SaveFile
|
||||
import UM.Platform
|
||||
import UM.MimeTypeDatabase
|
||||
import UM.Logger
|
||||
|
||||
from UM.MimeTypeDatabase import MimeTypeNotFoundError
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Manager class that contains common actions to deal with containers in Cura.
|
||||
#
|
||||
# This is primarily intended as a class to be able to perform certain actions
|
||||
# from within QML. We want to be able to trigger things like removing a container
|
||||
# when a certain action happens. This can be done through this class.
|
||||
class ContainerManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
self._container_name_filters = {}
|
||||
|
||||
## Create a duplicate of the specified container
|
||||
#
|
||||
# This will create and add a duplicate of the container corresponding
|
||||
# to the container ID.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to duplicate.
|
||||
#
|
||||
# \return The ID of the new container, or an empty string if duplication failed.
|
||||
@pyqtSlot(str, result = str)
|
||||
def duplicateContainer(self, container_id):
|
||||
containers = self._registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could duplicate container %s because it was not found.", container_id)
|
||||
return ""
|
||||
|
||||
container = containers[0]
|
||||
|
||||
new_container = None
|
||||
new_name = self._registry.uniqueName(container.getName())
|
||||
# Only InstanceContainer has a duplicate method at the moment.
|
||||
# So fall back to serialize/deserialize when no duplicate method exists.
|
||||
if hasattr(container, "duplicate"):
|
||||
new_container = container.duplicate(new_name)
|
||||
else:
|
||||
new_container = container.__class__(new_name)
|
||||
new_container.deserialize(container.serialize())
|
||||
new_container.setName(new_name)
|
||||
|
||||
if new_container:
|
||||
self._registry.addContainer(new_container)
|
||||
|
||||
return new_container.getId()
|
||||
|
||||
## Change the name of a specified container to a new name.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to change the name of.
|
||||
# \param new_id \type{str} The new ID of the container.
|
||||
# \param new_name \type{str} The new name of the specified container.
|
||||
#
|
||||
# \return True if successful, False if not.
|
||||
@pyqtSlot(str, str, str, result = bool)
|
||||
def renameContainer(self, container_id, new_id, new_name):
|
||||
containers = self._registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could rename container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
||||
container = containers[0]
|
||||
# First, remove the container from the registry. This will clean up any files related to the container.
|
||||
self._registry.removeContainer(container)
|
||||
|
||||
# Ensure we have a unique name for the container
|
||||
new_name = self._registry.uniqueName(new_name)
|
||||
|
||||
# Then, update the name and ID of the container
|
||||
container.setName(new_name)
|
||||
container._id = new_id # TODO: Find a nicer way to set a new, unique ID
|
||||
|
||||
# Finally, re-add the container so it will be properly serialized again.
|
||||
self._registry.addContainer(container)
|
||||
|
||||
return True
|
||||
|
||||
## Remove the specified container.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to remove.
|
||||
#
|
||||
# \return True if the container was successfully removed, False if not.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def removeContainer(self, container_id):
|
||||
containers = self._registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could remove container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
||||
self._registry.removeContainer(containers[0].getId())
|
||||
|
||||
return True
|
||||
|
||||
## Merge a container with another.
|
||||
#
|
||||
# This will try to merge one container into the other, by going through the container
|
||||
# and setting the right properties on the other container.
|
||||
#
|
||||
# \param merge_into_id \type{str} The ID of the container to merge into.
|
||||
# \param merge_id \type{str} The ID of the container to merge.
|
||||
#
|
||||
# \return True if successfully merged, False if not.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def mergeContainers(self, merge_into_id, merge_id):
|
||||
containers = self._registry.findContainers(None, id = merge_into_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could merge into container %s because it was not found.", merge_into_id)
|
||||
return False
|
||||
|
||||
merge_into = containers[0]
|
||||
|
||||
containers = self._registry.findContainers(None, id = merge_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could not merge container %s because it was not found", merge_id)
|
||||
return False
|
||||
|
||||
merge = containers[0]
|
||||
|
||||
if type(merge) != type(merge_into):
|
||||
UM.Logger.log("w", "Cannot merge two containers of different types")
|
||||
return False
|
||||
|
||||
for key in merge.getAllKeys():
|
||||
merge_into.setProperty(key, "value", merge.getProperty(key, "value"))
|
||||
|
||||
return True
|
||||
|
||||
## Clear the contents of a container.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to clear.
|
||||
#
|
||||
# \return True if successful, False if not.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def clearContainer(self, container_id):
|
||||
containers = self._registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could clear container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
||||
if containers[0].isReadOnly():
|
||||
UM.Logger.log("w", "Cannot clear read-only container %s", container_id)
|
||||
return False
|
||||
|
||||
containers[0].clear()
|
||||
|
||||
return True
|
||||
|
||||
## Set a metadata entry of the specified container.
|
||||
#
|
||||
# This will set the specified entry of the container's metadata to the specified
|
||||
# value. Note that entries containing dictionaries can have their entries changed
|
||||
# by using "/" as a separator. For example, to change an entry "foo" in a
|
||||
# dictionary entry "bar", you can specify "bar/foo" as entry name.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to change.
|
||||
# \param entry_name \type{str} The name of the metadata entry to change.
|
||||
# \param entry_value The new value of the entry.
|
||||
#
|
||||
# \return True if successful, False if not.
|
||||
@pyqtSlot(str, str, str, result = bool)
|
||||
def setContainerMetaDataEntry(self, container_id, entry_name, entry_value):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could set metadata of container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
||||
container = containers[0]
|
||||
|
||||
if container.isReadOnly():
|
||||
UM.Logger.log("w", "Cannot set metadata of read-only container %s.", container_id)
|
||||
return False
|
||||
|
||||
entries = entry_name.split("/")
|
||||
entry_name = entries.pop()
|
||||
|
||||
if entries:
|
||||
root_name = entries.pop(0)
|
||||
root = container.getMetaDataEntry(root_name)
|
||||
|
||||
item = root
|
||||
for entry in entries:
|
||||
item = item.get(entries.pop(0), { })
|
||||
|
||||
item[entry_name] = entry_value
|
||||
|
||||
entry_name = root_name
|
||||
entry_value = root
|
||||
|
||||
container.setMetaDataEntry(entry_name, entry_value)
|
||||
|
||||
return True
|
||||
|
||||
## Find instance containers matching certain criteria.
|
||||
#
|
||||
# This effectively forwards to ContainerRegistry::findInstanceContainers.
|
||||
#
|
||||
# \param criteria A dict of key - value pairs to search for.
|
||||
#
|
||||
# \return A list of container IDs that match the given criteria.
|
||||
@pyqtSlot("QVariantMap", result = "QVariantList")
|
||||
def findInstanceContainers(self, criteria):
|
||||
result = []
|
||||
for entry in self._registry.findInstanceContainers(**criteria):
|
||||
result.append(entry.getId())
|
||||
|
||||
return result
|
||||
|
||||
## Get a list of string that can be used as name filters for a Qt File Dialog
|
||||
#
|
||||
# This will go through the list of available container types and generate a list of strings
|
||||
# out of that. The strings are formatted as "description (*.extension)" and can be directly
|
||||
# passed to a nameFilters property of a Qt File Dialog.
|
||||
#
|
||||
# \param type_name Which types of containers to list. These types correspond to the "type"
|
||||
# key of the plugin metadata.
|
||||
#
|
||||
# \return A string list with name filters.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getContainerNameFilters(self, type_name):
|
||||
if not self._container_name_filters:
|
||||
self._updateContainerNameFilters()
|
||||
|
||||
filters = []
|
||||
for filter_string, entry in self._container_name_filters.items():
|
||||
if not type_name or entry["type"] == type_name:
|
||||
filters.append(filter_string)
|
||||
|
||||
filters.append("All Files (*)")
|
||||
return filters
|
||||
|
||||
## Export a container to a file
|
||||
#
|
||||
# \param container_id The ID of the container to export
|
||||
# \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)"
|
||||
# \param file_url The URL where to save the file.
|
||||
#
|
||||
# \return A dictionary containing a key "status" with a status code and a key "message" with a message
|
||||
# explaining the status.
|
||||
# The status code can be one of "error", "cancelled", "success"
|
||||
@pyqtSlot(str, str, QUrl, result = "QVariantMap")
|
||||
def exportContainer(self, container_id, file_type, file_url):
|
||||
if not container_id or not file_type or not file_url:
|
||||
return { "status": "error", "message": "Invalid arguments"}
|
||||
|
||||
if isinstance(file_url, QUrl):
|
||||
file_url = file_url.toLocalFile()
|
||||
|
||||
if not file_url:
|
||||
return { "status": "error", "message": "Invalid path"}
|
||||
|
||||
mime_type = None
|
||||
if not file_type in self._container_name_filters:
|
||||
try:
|
||||
mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
|
||||
except MimeTypeNotFoundError:
|
||||
return { "status": "error", "message": "Unknown File Type" }
|
||||
else:
|
||||
mime_type = self._container_name_filters[file_type]["mime"]
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
return { "status": "error", "message": "Container not found"}
|
||||
container = containers[0]
|
||||
|
||||
if UM.Platform.isOSX() and "." in file_url:
|
||||
file_url = file_url[:file_url.rfind(".")]
|
||||
|
||||
for suffix in mime_type.suffixes:
|
||||
if file_url.endswith(suffix):
|
||||
break
|
||||
else:
|
||||
file_url += "." + mime_type.preferredSuffix
|
||||
|
||||
if not UM.Platform.isWindows():
|
||||
if os.path.exists(file_url):
|
||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||
catalog.i18nc("@label", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_url))
|
||||
if result == QMessageBox.No:
|
||||
return { "status": "cancelled", "message": "User cancelled"}
|
||||
|
||||
try:
|
||||
contents = container.serialize()
|
||||
except NotImplementedError:
|
||||
return { "status": "error", "message": "Unable to serialize container"}
|
||||
|
||||
with UM.SaveFile(file_url, "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
return { "status": "success", "message": "Succesfully exported container", "path": file_url}
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_url A URL that points to the file to import.
|
||||
#
|
||||
# \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key
|
||||
# containing a message for the user
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importContainer(self, file_url):
|
||||
if not file_url:
|
||||
return { "status": "error", "message": "Invalid path"}
|
||||
|
||||
if isinstance(file_url, QUrl):
|
||||
file_url = file_url.toLocalFile()
|
||||
|
||||
if not file_url or not os.path.exists(file_url):
|
||||
return { "status": "error", "message": "Invalid path" }
|
||||
|
||||
try:
|
||||
mime_type = UM.MimeTypeDatabase.getMimeTypeForFile(file_url)
|
||||
except MimeTypeNotFoundError:
|
||||
return { "status": "error", "message": "Could not determine mime type of file" }
|
||||
|
||||
container_type = UM.Settings.ContainerRegistry.getContainerForMimeType(mime_type)
|
||||
if not container_type:
|
||||
return { "status": "error", "message": "Could not find a container to handle the specified file."}
|
||||
|
||||
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
|
||||
container_id = UM.Settings.ContainerRegistry.getInstance().uniqueName(container_id)
|
||||
|
||||
container = container_type(container_id)
|
||||
|
||||
try:
|
||||
with open(file_url, "rt") as f:
|
||||
container.deserialize(f.read())
|
||||
except PermissionError:
|
||||
return { "status": "error", "message": "Permission denied when trying to read the file"}
|
||||
|
||||
container.setName(container_id)
|
||||
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(container)
|
||||
|
||||
return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) }
|
||||
|
||||
def _updateContainerNameFilters(self):
|
||||
self._container_name_filters = {}
|
||||
for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
|
||||
# Ignore default container types since those are not plugins
|
||||
if container_type in (UM.Settings.InstanceContainer, UM.Settings.ContainerStack, UM.Settings.DefinitionContainer):
|
||||
continue
|
||||
|
||||
serialize_type = ""
|
||||
try:
|
||||
plugin_metadata = UM.PluginRegistry.getInstance().getMetaData(plugin_id)
|
||||
if plugin_metadata:
|
||||
serialize_type = plugin_metadata["settings_container"]["type"]
|
||||
else:
|
||||
continue
|
||||
except KeyError as e:
|
||||
continue
|
||||
|
||||
mime_type = UM.Settings.ContainerRegistry.getMimeTypeForContainer(container_type)
|
||||
|
||||
entry = {
|
||||
"type": serialize_type,
|
||||
"mime": mime_type,
|
||||
"container": container_type
|
||||
}
|
||||
|
||||
suffix = mime_type.preferredSuffix
|
||||
if UM.Platform.isOSX() and "." in suffix:
|
||||
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
||||
suffix = suffix[suffix.index(".") + 1:]
|
||||
|
||||
suffix_list = "*." + suffix
|
||||
for suffix in mime_type.suffixes:
|
||||
if suffix == mime_type.preferredSuffix:
|
||||
continue
|
||||
|
||||
if UM.Platform.isOSX() and "." in suffix:
|
||||
# OSX's File dialog is stupid and does not allow selecting files with a . in its name
|
||||
suffix = suffix[suffix.index("."):]
|
||||
|
||||
suffix_list += ", *." + suffix
|
||||
|
||||
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
|
||||
self._container_name_filters[name_filter] = entry
|
||||
|
||||
# Factory function, used by QML
|
||||
@staticmethod
|
||||
def createContainerManager(engine, js_engine):
|
||||
return ContainerManager()
|
||||
96
cura/Settings/ContainerSettingsModel.py
Normal file
96
cura/Settings/ContainerSettingsModel.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
class ContainerSettingsModel(ListModel):
|
||||
LabelRole = Qt.UserRole + 1
|
||||
CategoryRole = Qt.UserRole + 2
|
||||
UnitRole = Qt.UserRole + 3
|
||||
ValuesRole = Qt.UserRole + 4
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.LabelRole, "label")
|
||||
self.addRoleName(self.CategoryRole, "category")
|
||||
self.addRoleName(self.UnitRole, "unit")
|
||||
self.addRoleName(self.ValuesRole, "values")
|
||||
|
||||
self._container_ids = []
|
||||
self._containers = []
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if property_name == "value":
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
self.clear()
|
||||
|
||||
if len(self._container_ids) == 0:
|
||||
return
|
||||
|
||||
keys = []
|
||||
for container in self._containers:
|
||||
keys = keys + list(container.getAllKeys())
|
||||
|
||||
keys = list(set(keys)) # remove duplicate keys
|
||||
|
||||
for key in keys:
|
||||
definition = None
|
||||
category = None
|
||||
values = []
|
||||
for container in self._containers:
|
||||
instance = container.getInstance(key)
|
||||
if instance:
|
||||
definition = instance.definition
|
||||
|
||||
# Traverse up to find the category
|
||||
category = definition
|
||||
while category.type != "category":
|
||||
category = category.parent
|
||||
|
||||
value = container.getProperty(key, "value")
|
||||
if type(value) == SettingFunction:
|
||||
values.append("=\u0192")
|
||||
else:
|
||||
values.append(container.getProperty(key, "value"))
|
||||
else:
|
||||
values.append("")
|
||||
|
||||
self.appendItem({
|
||||
"key": key,
|
||||
"values": values,
|
||||
"label": definition.label,
|
||||
"unit": definition.unit,
|
||||
"category": category.label
|
||||
})
|
||||
self.sort(lambda k: (k["category"], k["key"]))
|
||||
|
||||
## 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
|
||||
def setContainers(self, container_ids):
|
||||
for container in self._containers:
|
||||
container.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
||||
self._container_ids = container_ids
|
||||
self._containers = []
|
||||
|
||||
for container_id in self._container_ids:
|
||||
containers = ContainerRegistry.getInstance().findContainers(id = container_id)
|
||||
if containers:
|
||||
containers[0].propertyChanged.connect(self._onPropertyChanged)
|
||||
self._containers.append(containers[0])
|
||||
|
||||
self._update()
|
||||
|
||||
containersChanged = pyqtSignal()
|
||||
@pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
|
||||
def containers(self):
|
||||
return self.container_ids
|
||||
222
cura/Settings/CuraContainerRegistry.py
Normal file
222
cura/Settings/CuraContainerRegistry.py
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.PluginRegistry import PluginRegistry #For getting the possible profile writers to write with.
|
||||
from UM.Util import parseBool
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class CuraContainerRegistry(ContainerRegistry):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
## Create a name that is not empty and unique
|
||||
# \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 new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
||||
new_name = new_name.strip()
|
||||
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
|
||||
if num_check:
|
||||
new_name = num_check.group(1)
|
||||
if new_name == "":
|
||||
new_name = fallback_name
|
||||
|
||||
unique_name = new_name
|
||||
i = 1
|
||||
# In case we are renaming, the current name of the container is also a valid end-result
|
||||
while self._containerExists(container_type, unique_name) and unique_name != current_name:
|
||||
i += 1
|
||||
unique_name = "%s #%d" % (new_name, i)
|
||||
|
||||
return unique_name
|
||||
|
||||
## Check if a container with of a certain type and a certain name or id exists
|
||||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param container_name \type{string} Name to check
|
||||
def _containerExists(self, container_type, container_name):
|
||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||
|
||||
return self.findContainers(container_class, id = container_name, type = container_type, ignore_case = True) or \
|
||||
self.findContainers(container_class, name = container_name, type = container_type)
|
||||
|
||||
## Exports an profile to a file
|
||||
#
|
||||
# \param instance_id \type{str} the ID of the profile to export.
|
||||
# \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>)"
|
||||
def exportProfile(self, instance_id, 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.
|
||||
# fileType has the format "<description> (*.<extension>)"
|
||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||
if split < 0: # Not found. Invalid format.
|
||||
Logger.log("e", "Invalid file format identifier %s", file_type)
|
||||
return
|
||||
description = file_type[:split]
|
||||
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
|
||||
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
|
||||
file_name += "." + extension
|
||||
|
||||
# On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself.
|
||||
if not Platform.isWindows():
|
||||
if os.path.exists(file_name):
|
||||
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))
|
||||
if result == QMessageBox.No:
|
||||
return
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
|
||||
if not containers:
|
||||
return
|
||||
container = containers[0]
|
||||
|
||||
profile_writer = self._findProfileWriter(extension, description)
|
||||
|
||||
try:
|
||||
success = profile_writer.write(file_name, container)
|
||||
except Exception as 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.show()
|
||||
return
|
||||
if not success:
|
||||
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.show()
|
||||
return
|
||||
m = Message(catalog.i18nc("@info:status", "Exported profile to <filename>{0}</filename>", file_name))
|
||||
m.show()
|
||||
|
||||
## Gets the plugin object matching the criteria
|
||||
# \param extension
|
||||
# \param description
|
||||
# \return The plugin object matching the given extension and description.
|
||||
def _findProfileWriter(self, extension, description):
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
||||
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
||||
supported_extension = supported_type.get("extension", None)
|
||||
if supported_extension == extension: # This plugin supports a file type with the same extension.
|
||||
supported_description = supported_type.get("description", None)
|
||||
if supported_description == description: # The description is also identical. Assume it's the same file type.
|
||||
return plugin_registry.getPluginObject(plugin_id)
|
||||
return None
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_name \type{str} the full path and filename of the profile to import
|
||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
||||
# containing a message for the user
|
||||
def importProfile(self, 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")}
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||
try:
|
||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||
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.
|
||||
Logger.log("e", "Failed to import profile from %s: %s", file_name, 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))}
|
||||
if profile_or_list: # Success!
|
||||
name_seed = os.path.splitext(os.path.basename(file_name))[0]
|
||||
if type(profile_or_list) is not list:
|
||||
profile = 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:
|
||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
||||
else:
|
||||
profile_names = ", ".join([profile.getName() for profile in profile_or_list])
|
||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profiles {0}", profile_names) }
|
||||
|
||||
#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.", file_name)}
|
||||
|
||||
def _configureProfile(self, profile, name_seed):
|
||||
profile.setReadOnly(False)
|
||||
|
||||
new_name = self.createUniqueName("quality", "", name_seed, catalog.i18nc("@label", "Custom profile"))
|
||||
profile.setName(new_name)
|
||||
profile._id = new_name
|
||||
|
||||
if self._machineHasOwnQualities():
|
||||
profile.setDefinition(self._activeDefinition())
|
||||
if self._machineHasOwnMaterials():
|
||||
profile.addMetaDataEntry("material", self._activeMaterialId())
|
||||
else:
|
||||
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
|
||||
ContainerRegistry.getInstance().addContainer(profile)
|
||||
|
||||
## Gets a list of profile writer plugins
|
||||
# \return List of tuples of (plugin_id, meta_data).
|
||||
def _getIOPlugins(self, io_type):
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
active_plugin_ids = plugin_registry.getActivePlugins()
|
||||
|
||||
result = []
|
||||
for plugin_id in active_plugin_ids:
|
||||
meta_data = plugin_registry.getMetaData(plugin_id)
|
||||
if io_type in meta_data:
|
||||
result.append( (plugin_id, meta_data) )
|
||||
return result
|
||||
|
||||
## Gets the active definition
|
||||
# \return the active definition object or None if there is no definition
|
||||
def _activeDefinition(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
definition = global_container_stack.getBottom()
|
||||
if definition:
|
||||
return definition
|
||||
return None
|
||||
|
||||
## Returns true if the current machine requires its own materials
|
||||
# \return True if the current machine requires its own materials
|
||||
def _machineHasOwnMaterials(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
return global_container_stack.getMetaDataEntry("has_materials", False)
|
||||
return False
|
||||
|
||||
## Gets the ID of the active material
|
||||
# \return the ID of the active material or the empty string
|
||||
def _activeMaterialId(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
material = global_container_stack.findContainer({"type": "material"})
|
||||
if material:
|
||||
return material.getId()
|
||||
return ""
|
||||
|
||||
## Returns true if the current machien requires its own quality profiles
|
||||
# \return true if the current machien requires its own quality profiles
|
||||
def _machineHasOwnQualities(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
||||
return False
|
||||
336
cura/Settings/ExtruderManager.py
Normal file
336
cura/Settings/ExtruderManager.py
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject, QVariant #For communicating data and events to Qt.
|
||||
|
||||
import UM.Application #To get the global container stack to find the current machine.
|
||||
import UM.Logger
|
||||
import UM.Settings.ContainerRegistry #Finding containers by ID.
|
||||
import UM.Settings.SettingFunction
|
||||
|
||||
|
||||
## Manages all existing extruder stacks.
|
||||
#
|
||||
# This keeps a list of extruder stacks for each machine.
|
||||
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.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
||||
self._active_extruder_index = 0
|
||||
UM.Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
||||
self._addCurrentMachineExtruders()
|
||||
|
||||
## Gets the unique identifier of the currently active extruder stack.
|
||||
#
|
||||
# The currently active extruder stack is the stack that is currently being
|
||||
# edited.
|
||||
#
|
||||
# \return The unique ID of the currently active extruder stack.
|
||||
@pyqtProperty(str, notify = activeExtruderChanged)
|
||||
def activeExtruderStackId(self):
|
||||
if not UM.Application.getInstance().getGlobalContainerStack():
|
||||
return None # No active machine, so no active extruder.
|
||||
try:
|
||||
return self._extruder_trains[UM.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.
|
||||
return None
|
||||
|
||||
@pyqtProperty(int, notify = extrudersChanged)
|
||||
def extruderCount(self):
|
||||
if not UM.Application.getInstance().getGlobalContainerStack():
|
||||
return 0 # No active machine, so no extruders.
|
||||
return len(self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()])
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=extrudersChanged)
|
||||
def extruderIds(self):
|
||||
map = {}
|
||||
for position in self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()]:
|
||||
map[position] = self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getId()][position].getId()
|
||||
return map
|
||||
|
||||
## The instance of the singleton pattern.
|
||||
#
|
||||
# It's None if the extruder manager hasn't been created yet.
|
||||
__instance = None
|
||||
|
||||
## Gets an instance of the extruder manager, or creates one if no instance
|
||||
# exists yet.
|
||||
#
|
||||
# This is an implementation of singleton. If an extruder manager already
|
||||
# exists, it is re-used.
|
||||
#
|
||||
# \return The extruder manager.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
if not cls.__instance:
|
||||
cls.__instance = ExtruderManager()
|
||||
return cls.__instance
|
||||
|
||||
## Changes the active extruder by index.
|
||||
#
|
||||
# \param index The index of the new active extruder.
|
||||
@pyqtSlot(int)
|
||||
def setActiveExtruderIndex(self, index):
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = activeExtruderChanged)
|
||||
def activeExtruderIndex(self):
|
||||
return self._active_extruder_index
|
||||
|
||||
def getActiveExtruderStack(self):
|
||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if global_container_stack.getId() in self._extruder_trains:
|
||||
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 None
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index):
|
||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if global_container_stack.getId() in self._extruder_trains:
|
||||
if str(index) in self._extruder_trains[global_container_stack.getId()]:
|
||||
return self._extruder_trains[global_container_stack.getId()][str(index)]
|
||||
return None
|
||||
|
||||
## Adds all extruders of a specific machine definition to the extruder
|
||||
# manager.
|
||||
#
|
||||
# \param machine_definition The machine definition to add the extruders for.
|
||||
# \param machine_id The machine_id to add the extruders for.
|
||||
def addMachineExtruders(self, machine_definition, machine_id):
|
||||
changed = False
|
||||
machine_definition_id = machine_definition.getId()
|
||||
if machine_id not in self._extruder_trains:
|
||||
self._extruder_trains[machine_id] = { }
|
||||
changed = True
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
if container_registry:
|
||||
# Add the extruder trains that don't exist yet.
|
||||
for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition_id):
|
||||
position = extruder_definition.getMetaDataEntry("position", None)
|
||||
if not position:
|
||||
UM.Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
|
||||
if not container_registry.findContainerStacks(machine = machine_id, position = position): # Doesn't exist yet.
|
||||
self.createExtruderTrain(extruder_definition, machine_definition, position, machine_id)
|
||||
changed = True
|
||||
|
||||
# Gets the extruder trains that we just created as well as any that still existed.
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_id)
|
||||
for extruder_train in extruder_trains:
|
||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||
|
||||
# Make sure the next stack is a stack that contains only the machine definition
|
||||
if not extruder_train.getNextStack():
|
||||
shallowStack = UM.Settings.ContainerStack(machine_id + "_shallow")
|
||||
shallowStack.addContainer(machine_definition)
|
||||
extruder_train.setNextStack(shallowStack)
|
||||
changed = True
|
||||
if changed:
|
||||
self.extrudersChanged.emit(machine_id)
|
||||
|
||||
## Creates a container stack for an extruder train.
|
||||
#
|
||||
# The container stack has an extruder definition at the bottom, which is
|
||||
# linked to a machine definition. Then it has a variant profile, a material
|
||||
# profile, a quality profile and a user profile, in that order.
|
||||
#
|
||||
# The resulting container stack is added to the registry.
|
||||
#
|
||||
# \param extruder_definition The extruder to create the extruder train for.
|
||||
# \param machine_definition The machine that the extruder train belongs to.
|
||||
# \param position The position of this extruder train in the extruder slots of the machine.
|
||||
# \param machine_id The id of the "global" stack this extruder is linked to.
|
||||
def createExtruderTrain(self, extruder_definition, machine_definition, position, machine_id):
|
||||
# Cache some things.
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
machine_definition_id = machine_definition.getId()
|
||||
|
||||
# Create a container stack for this extruder.
|
||||
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
|
||||
container_stack = UM.Settings.ContainerStack(extruder_stack_id)
|
||||
container_stack.setName(extruder_definition.getName()) # Take over the display name to display the stack with.
|
||||
container_stack.addMetaDataEntry("type", "extruder_train")
|
||||
container_stack.addMetaDataEntry("machine", machine_id)
|
||||
container_stack.addMetaDataEntry("position", position)
|
||||
container_stack.addContainer(extruder_definition)
|
||||
|
||||
# Find the variant to use for this extruder.
|
||||
variant = container_registry.findInstanceContainers(id = "empty_variant")[0]
|
||||
if machine_definition.getMetaDataEntry("has_variants"):
|
||||
# First add any variant. Later, overwrite with preference if the preference is valid.
|
||||
variants = container_registry.findInstanceContainers(definition = machine_definition_id, type = "variant")
|
||||
if len(variants) >= 1:
|
||||
variant = variants[0]
|
||||
preferred_variant_id = machine_definition.getMetaDataEntry("preferred_variant")
|
||||
if preferred_variant_id:
|
||||
preferred_variants = container_registry.findInstanceContainers(id = preferred_variant_id, type = "variant")
|
||||
if len(preferred_variants) >= 1:
|
||||
variant = preferred_variants[0]
|
||||
else:
|
||||
UM.Logger.log("w", "The preferred variant \"%s\" of machine %s doesn't exist or is not a variant profile.", preferred_variant_id, machine_id)
|
||||
# And leave it at the default variant.
|
||||
container_stack.addContainer(variant)
|
||||
|
||||
# Find a material to use for this variant.
|
||||
material = container_registry.findInstanceContainers(id = "empty_material")[0]
|
||||
if machine_definition.getMetaDataEntry("has_materials"):
|
||||
# First add any material. Later, overwrite with preference if the preference is valid.
|
||||
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:
|
||||
search_criteria["variant"] = variant.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
preferred_materials = container_registry.findInstanceContainers(**search_criteria)
|
||||
if len(preferred_materials) >= 1:
|
||||
material = preferred_materials[0]
|
||||
else:
|
||||
UM.Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
|
||||
# And leave it at the default material.
|
||||
container_stack.addContainer(material)
|
||||
|
||||
# Find a quality to use for this extruder.
|
||||
quality = container_registry.getEmptyInstanceContainer()
|
||||
|
||||
search_criteria = { "type": "quality" }
|
||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = machine_definition.id
|
||||
if machine_definition.getMetaDataEntry("has_materials") and material:
|
||||
search_criteria["material"] = material.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
preferred_quality = machine_definition.getMetaDataEntry("preferred_quality")
|
||||
if preferred_quality:
|
||||
search_criteria["id"] = preferred_quality
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if not containers and preferred_quality:
|
||||
UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality, machine_id)
|
||||
search_criteria.pop("id", None)
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
quality = containers[0]
|
||||
|
||||
container_stack.addContainer(quality)
|
||||
|
||||
user_profile = container_registry.findInstanceContainers(type = "user", extruder = extruder_stack_id)
|
||||
if user_profile: # There was already a user profile, loaded from settings.
|
||||
user_profile = user_profile[0]
|
||||
else:
|
||||
user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings") # Add an empty user profile.
|
||||
user_profile.addMetaDataEntry("type", "user")
|
||||
user_profile.addMetaDataEntry("extruder", extruder_stack_id)
|
||||
user_profile.setDefinition(machine_definition)
|
||||
container_registry.addContainer(user_profile)
|
||||
container_stack.addContainer(user_profile)
|
||||
|
||||
# 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.
|
||||
#
|
||||
# \param machine_id The machine to remove the extruders for.
|
||||
def removeMachineExtruders(self, machine_id):
|
||||
for extruder in self.getMachineExtruders(machine_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
|
||||
for container in containers:
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
||||
|
||||
## Returns extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to get the extruders of.
|
||||
def getMachineExtruders(self, machine_id):
|
||||
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
|
||||
for name in self._extruder_trains[machine_id]:
|
||||
yield self._extruder_trains[machine_id][name]
|
||||
|
||||
def __globalContainerStackChanged(self):
|
||||
self._addCurrentMachineExtruders()
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
## Adds the extruders of the currently active machine.
|
||||
def _addCurrentMachineExtruders(self):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if global_stack and global_stack.getBottom():
|
||||
self.addMachineExtruders(global_stack.getBottom(), global_stack.getId())
|
||||
|
||||
## Get all extruder values for a certain setting.
|
||||
#
|
||||
# This is exposed to SettingFunction so it can be used in value functions.
|
||||
#
|
||||
# \param key The key of the setting to retieve 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 getExtruderValues(key):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
result = []
|
||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||
value = extruder.getRawProperty(key, "value")
|
||||
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if isinstance(value, UM.Settings.SettingFunction):
|
||||
value = value(extruder)
|
||||
|
||||
result.append(value)
|
||||
|
||||
if not result:
|
||||
result.append(global_stack.getProperty(key, "value"))
|
||||
|
||||
return result
|
||||
|
||||
## Get the value for a setting from a specific extruder.
|
||||
#
|
||||
# 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 getExtruderValue(extruder_index, key):
|
||||
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||
|
||||
if extruder:
|
||||
value = extruder.getRawProperty(key, "value")
|
||||
if isinstance(value, UM.Settings.SettingFunction):
|
||||
value = value(extruder)
|
||||
else: #Just a value from global.
|
||||
value = UM.Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
||||
|
||||
return value
|
||||
141
cura/Settings/ExtrudersModel.py
Normal file
141
cura/Settings/ExtrudersModel.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
|
||||
import UM.Qt.ListModel
|
||||
|
||||
from . import ExtruderManager
|
||||
|
||||
## Model that holds extruders.
|
||||
#
|
||||
# This model is designed for use by any list of extruders, but specifically
|
||||
# intended for drop-down lists of the current machine's extruders in place of
|
||||
# settings.
|
||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
# The ID of the container stack for the extruder.
|
||||
IdRole = Qt.UserRole + 1
|
||||
|
||||
## Human-readable name of the extruder.
|
||||
NameRole = Qt.UserRole + 2
|
||||
|
||||
## Colour of the material loaded in the extruder.
|
||||
ColorRole = Qt.UserRole + 3
|
||||
|
||||
## Index of the extruder, which is also the value of the setting itself.
|
||||
#
|
||||
# An index of 0 indicates the first extruder, an index of 1 the second
|
||||
# one, and so on. This is the value that will be saved in instance
|
||||
# containers.
|
||||
IndexRole = Qt.UserRole + 4
|
||||
|
||||
## List of colours to display if there is no material or the material has no known
|
||||
# colour.
|
||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||
|
||||
## Initialises the extruders model, defining the roles and listening for
|
||||
# changes in the data.
|
||||
#
|
||||
# \param parent Parent QtObject of this list.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.ColorRole, "color")
|
||||
self.addRoleName(self.IndexRole, "index")
|
||||
|
||||
self._add_global = False
|
||||
|
||||
self._active_extruder_stack = None
|
||||
|
||||
#Listen to changes.
|
||||
manager = ExtruderManager.getInstance()
|
||||
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
|
||||
|
||||
self._updateExtruders()
|
||||
|
||||
manager.activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||
self._onActiveExtruderChanged()
|
||||
|
||||
def setAddGlobal(self, add):
|
||||
if add != self._add_global:
|
||||
self._add_global = add
|
||||
self._updateExtruders()
|
||||
self.addGlobalChanged.emit()
|
||||
|
||||
addGlobalChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
|
||||
def addGlobal(self):
|
||||
return self._add_global
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
manager = ExtruderManager.getInstance()
|
||||
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:
|
||||
# Update the model when the material container is changed
|
||||
active_extruder_stack.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||
self._active_extruder_stack = active_extruder_stack
|
||||
|
||||
|
||||
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
|
||||
if container.getMetaDataEntry("type") == "material":
|
||||
self._updateExtruders()
|
||||
|
||||
modelChanged = pyqtSignal()
|
||||
|
||||
## Update the list of extruders.
|
||||
#
|
||||
# This should be called whenever the list of extruders changes.
|
||||
def _updateExtruders(self):
|
||||
changed = False
|
||||
|
||||
if self.rowCount() != 0:
|
||||
self.clear()
|
||||
changed = True
|
||||
|
||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
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()
|
||||
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
|
||||
extruder_name = extruder.getName()
|
||||
material = extruder.findContainer({ "type": "material" })
|
||||
if material:
|
||||
extruder_name = "%s (%s)" % (material.getName(), extruder_name)
|
||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||
try:
|
||||
position = int(position)
|
||||
except ValueError: #Not a proper int.
|
||||
position = -1
|
||||
default_color = self.defaultColors[position] if position >= 0 and position < len(self.defaultColors) else self.defaultColors[0]
|
||||
color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color
|
||||
item = { #Construct an item with only the relevant information.
|
||||
"id": extruder.getId(),
|
||||
"name": extruder_name,
|
||||
"color": color,
|
||||
"index": position
|
||||
}
|
||||
self.appendItem(item)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.sort(lambda item: item["index"])
|
||||
self.modelChanged.emit()
|
||||
897
cura/Settings/MachineManager.py
Normal file
897
cura/Settings/MachineManager.py
Normal file
|
|
@ -0,0 +1,897 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Logger import Logger
|
||||
|
||||
import UM.Settings
|
||||
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from . import ExtruderManager
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import time
|
||||
|
||||
class MachineManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._active_container_stack = None
|
||||
self._global_container_stack = None
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._active_stack_valid = None
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
self._onActiveExtruderStackChanged()
|
||||
|
||||
## When the global container is changed, active material probably needs to be updated.
|
||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||
self.globalValueChanged.connect(self.activeStackChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged)
|
||||
|
||||
self._empty_variant_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
|
||||
self._empty_material_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
|
||||
self._empty_quality_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_machine", "")
|
||||
|
||||
self._global_event_keys = set()
|
||||
|
||||
active_machine_id = Preferences.getInstance().getValue("cura/active_machine")
|
||||
|
||||
self._printer_output_devices = []
|
||||
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
|
||||
if active_machine_id != "":
|
||||
# An active machine was saved, so restore it.
|
||||
self.setActiveMachine(active_machine_id)
|
||||
if self._global_container_stack and self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
# Make sure _active_container_stack is properly initiated
|
||||
ExtruderManager.getInstance().setActiveExtruderIndex(0)
|
||||
|
||||
self._auto_materials_changed = {}
|
||||
self._auto_hotends_changed = {}
|
||||
|
||||
globalContainerChanged = pyqtSignal()
|
||||
activeMaterialChanged = pyqtSignal()
|
||||
activeVariantChanged = pyqtSignal()
|
||||
activeQualityChanged = pyqtSignal()
|
||||
activeStackChanged = pyqtSignal()
|
||||
|
||||
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
|
||||
activeValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||
|
||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||
|
||||
outputDevicesChanged = pyqtSignal()
|
||||
|
||||
def _onOutputDevicesChanged(self):
|
||||
for printer_output_device in self._printer_output_devices:
|
||||
printer_output_device.hotendIdChanged.disconnect(self._onHotendIdChanged)
|
||||
printer_output_device.materialIdChanged.disconnect(self._onMaterialIdChanged)
|
||||
|
||||
self._printer_output_devices.clear()
|
||||
|
||||
for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
||||
if isinstance(printer_output_device, PrinterOutputDevice):
|
||||
self._printer_output_devices.append(printer_output_device)
|
||||
printer_output_device.hotendIdChanged.connect(self._onHotendIdChanged)
|
||||
printer_output_device.materialIdChanged.connect(self._onMaterialIdChanged)
|
||||
|
||||
self.outputDevicesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
||||
def printerOutputDevices(self):
|
||||
return self._printer_output_devices
|
||||
|
||||
def _onHotendIdChanged(self, index, hotend_id):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type="variant", definition=self._global_container_stack.getBottom().getId(), name=hotend_id)
|
||||
if containers: # New material ID is known
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
|
||||
matching_extruder = None
|
||||
for extruder in extruders:
|
||||
if str(index) == extruder.getMetaDataEntry("position"):
|
||||
matching_extruder = extruder
|
||||
break
|
||||
if matching_extruder and matching_extruder.findContainer({"type": "variant"}).getName() != hotend_id:
|
||||
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
|
||||
self._auto_hotends_changed[str(index)] = containers[0].getId()
|
||||
Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"),
|
||||
catalog.i18nc("@label",
|
||||
"Do you want to change the materials and hotends to match the material in your printer?"),
|
||||
catalog.i18nc("@label",
|
||||
"The materials and / or hotends on your printer were changed. For best results always slice for the materials . hotends that are inserted in your printer."),
|
||||
buttons=QMessageBox.Yes + QMessageBox.No,
|
||||
icon=QMessageBox.Question,
|
||||
callback=self._materialHotendChangedCallback)
|
||||
|
||||
|
||||
else:
|
||||
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
|
||||
|
||||
def _autoUpdateHotends(self):
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
for position in self._auto_hotends_changed:
|
||||
hotend_id = self._auto_hotends_changed[position]
|
||||
old_index = extruder_manager.activeExtruderIndex
|
||||
|
||||
if old_index != int(position):
|
||||
extruder_manager.setActiveExtruderIndex(int(position))
|
||||
else:
|
||||
old_index = None
|
||||
Logger.log("d", "Setting hotend variant of hotend %s to %s" % (position, hotend_id))
|
||||
self.setActiveVariant(hotend_id)
|
||||
|
||||
if old_index is not None:
|
||||
extruder_manager.setActiveExtruderIndex(old_index)
|
||||
|
||||
def _onMaterialIdChanged(self, index, material_id):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
definition_id = "fdmprinter"
|
||||
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
|
||||
definition_id = self._global_container_stack.getBottom().getId()
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
|
||||
if containers: # New material ID is known
|
||||
extruders = list(extruder_manager.getMachineExtruders(self.activeMachineId))
|
||||
matching_extruder = None
|
||||
for extruder in extruders:
|
||||
if str(index) == extruder.getMetaDataEntry("position"):
|
||||
matching_extruder = extruder
|
||||
break
|
||||
|
||||
if matching_extruder and matching_extruder.findContainer({"type":"material"}).getMetaDataEntry("GUID") != material_id:
|
||||
# Save the material that needs to be changed. Multiple changes will be handled by the callback.
|
||||
self._auto_materials_changed[str(index)] = containers[0].getId()
|
||||
Application.getInstance().messageBox(catalog.i18nc("@window:title", "Changes on the Printer"), catalog.i18nc("@label", "Do you want to change the materials and hotends to match the material in your printer?"),
|
||||
catalog.i18nc("@label", "The materials and / or hotends on your printer were changed. For best results always slice for the materials and hotends that are inserted in your printer."),
|
||||
buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._materialHotendChangedCallback)
|
||||
|
||||
else:
|
||||
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
|
||||
|
||||
def _materialHotendChangedCallback(self, button):
|
||||
if button == QMessageBox.No:
|
||||
self._auto_materials_changed = {}
|
||||
self._auto_hotends_changed = {}
|
||||
return
|
||||
self._autoUpdateMaterials()
|
||||
self._autoUpdateHotends()
|
||||
|
||||
def _autoUpdateMaterials(self):
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
for position in self._auto_materials_changed:
|
||||
material_id = self._auto_materials_changed[position]
|
||||
old_index = extruder_manager.activeExtruderIndex
|
||||
|
||||
if old_index != int(position):
|
||||
extruder_manager.setActiveExtruderIndex(int(position))
|
||||
else:
|
||||
old_index = None
|
||||
|
||||
Logger.log("d", "Setting material of hotend %s to %s" % (position, material_id))
|
||||
self.setActiveMaterial(material_id)
|
||||
|
||||
if old_index is not None:
|
||||
extruder_manager.setActiveExtruderIndex(old_index)
|
||||
|
||||
def _onGlobalPropertyChanged(self, key, property_name):
|
||||
if property_name == "value":
|
||||
## We can get recursion issues. So we store a list of keys that we are still handling to prevent this.
|
||||
if key in self._global_event_keys:
|
||||
return
|
||||
self._global_event_keys.add(key)
|
||||
self.globalValueChanged.emit()
|
||||
|
||||
if self._active_container_stack and self._active_container_stack != self._global_container_stack:
|
||||
# Make the global current settings mirror the stack values appropriate for this setting
|
||||
if self._active_container_stack.getProperty("extruder_nr", "value") == int(self._active_container_stack.getProperty(key, "global_inherits_stack")):
|
||||
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
self._global_container_stack.getTop().setProperty(key, "value", new_value)
|
||||
|
||||
# Global-only setting values should be set on all extruders and the global stack
|
||||
if not self._global_container_stack.getProperty(key, "settable_per_extruder"):
|
||||
extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
target_stack_position = int(self._active_container_stack.getProperty(key, "global_inherits_stack"))
|
||||
if target_stack_position == -1: # Prevent -1 from selecting wrong stack.
|
||||
target_stack = self._active_container_stack
|
||||
else:
|
||||
target_stack = extruder_stacks[target_stack_position]
|
||||
new_value = target_stack.getProperty(key, "value")
|
||||
target_stack_has_user_value = target_stack.getTop().getInstance(key) != None
|
||||
for extruder_stack in extruder_stacks:
|
||||
if extruder_stack != target_stack:
|
||||
if target_stack_has_user_value:
|
||||
extruder_stack.getTop().setProperty(key, "value", new_value)
|
||||
else:
|
||||
# Remove from the value from the other stacks as well, unless the
|
||||
# top value from the other stacklevels is different than the new value
|
||||
for container in extruder_stack.getContainers():
|
||||
if container.__class__ == UM.Settings.InstanceContainer and container.getInstance(key) != None:
|
||||
if container.getProperty(key, "value") != new_value:
|
||||
# It could be that the setting needs to be removed instead of updated.
|
||||
temp = extruder_stack
|
||||
containers = extruder_stack.getContainers()
|
||||
# Ensure we have the entire 'chain'
|
||||
while temp.getNextStack():
|
||||
temp = temp.getNextStack()
|
||||
containers.extend(temp.getContainers())
|
||||
instance_needs_removal = False
|
||||
|
||||
if len(containers) > 1:
|
||||
for index in range(1, len(containers)):
|
||||
deeper_container = containers[index]
|
||||
if deeper_container.getProperty(key, "value") is None:
|
||||
continue # Deeper container does not have the value, so continue.
|
||||
if deeper_container.getProperty(key, "value") == new_value:
|
||||
# Removal will result in correct value, so do that.
|
||||
# We do this to prevent the reset from showing up unneeded.
|
||||
instance_needs_removal = True
|
||||
break
|
||||
else:
|
||||
# Container has the value, but it's not the same. Stop looking.
|
||||
break
|
||||
if instance_needs_removal:
|
||||
extruder_stack.getTop().removeInstance(key)
|
||||
else:
|
||||
extruder_stack.getTop().setProperty(key, "value", new_value)
|
||||
else:
|
||||
# Check if we really need to remove something.
|
||||
if extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.getTop().removeInstance(key)
|
||||
break
|
||||
if self._global_container_stack.getProperty(key, "value") != new_value:
|
||||
self._global_container_stack.getTop().setProperty(key, "value", new_value)
|
||||
self._global_event_keys.remove(key)
|
||||
|
||||
if property_name == "global_inherits_stack":
|
||||
if self._active_container_stack and self._active_container_stack != self._global_container_stack:
|
||||
# Update the global user value when the "global_inherits_stack" function points to a different stack
|
||||
extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
target_stack_position = int(self._active_container_stack.getProperty(key, "global_inherits_stack"))
|
||||
if target_stack_position == -1: # Prevent -1 from selecting wrong stack.
|
||||
target_stack = self._active_container_stack
|
||||
else:
|
||||
target_stack = extruder_stacks[target_stack_position]
|
||||
|
||||
new_value = target_stack.getProperty(key, "value")
|
||||
if self._global_container_stack.getProperty(key, "value") != new_value:
|
||||
self._global_container_stack.getTop().setProperty(key, "value", new_value)
|
||||
|
||||
if property_name == "validationState":
|
||||
if self._active_stack_valid:
|
||||
changed_validation_state = self._active_container_stack.getProperty(key, property_name)
|
||||
if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
|
||||
self._active_stack_valid = False
|
||||
self.activeValidationChanged.emit()
|
||||
else:
|
||||
has_errors = self._checkStackForErrors(self._active_container_stack)
|
||||
if not has_errors:
|
||||
self._active_stack_valid = True
|
||||
self.activeValidationChanged.emit()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
|
||||
self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
|
||||
|
||||
material = self._global_container_stack.findContainer({"type": "material"})
|
||||
material.nameChanged.disconnect(self._onMaterialNameChanged)
|
||||
|
||||
quality = self._global_container_stack.findContainer({"type": "quality"})
|
||||
quality.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
self._active_container_stack = self._global_container_stack
|
||||
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
if self._global_container_stack:
|
||||
Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId())
|
||||
self._global_container_stack.nameChanged.connect(self._onMachineNameChanged)
|
||||
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
|
||||
self._global_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
|
||||
material = self._global_container_stack.findContainer({"type": "material"})
|
||||
material.nameChanged.connect(self._onMaterialNameChanged)
|
||||
|
||||
quality = self._global_container_stack.findContainer({"type": "quality"})
|
||||
quality.nameChanged.connect(self._onQualityNameChanged)
|
||||
|
||||
def _onActiveExtruderStackChanged(self):
|
||||
self.blurSettings.emit() # Ensure no-one has focus.
|
||||
if self._active_container_stack and self._active_container_stack != self._global_container_stack:
|
||||
self._active_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
|
||||
self._active_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
|
||||
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if self._active_container_stack:
|
||||
self._active_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
|
||||
self._active_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
|
||||
else:
|
||||
self._active_container_stack = self._global_container_stack
|
||||
self._active_stack_valid = not self._checkStackForErrors(self._active_container_stack)
|
||||
self.activeValidationChanged.emit()
|
||||
|
||||
def _onInstanceContainersChanged(self, container):
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
||||
if self._active_container_stack and self._active_container_stack != self._global_container_stack:
|
||||
if int(self._active_container_stack.getProperty("extruder_nr", "value")) == 0:
|
||||
global_container = self._global_container_stack.findContainer({"type": container_type})
|
||||
if global_container and global_container != container:
|
||||
container_index = self._global_container_stack.getContainerIndex(global_container)
|
||||
self._global_container_stack.replaceContainer(container_index, container)
|
||||
|
||||
for key in container.getAllKeys():
|
||||
# Make sure the values in this profile are distributed to other stacks if necessary
|
||||
self._onGlobalPropertyChanged(key, "value")
|
||||
|
||||
if container_type == "material":
|
||||
self.activeMaterialChanged.emit()
|
||||
elif container_type == "variant":
|
||||
self.activeVariantChanged.emit()
|
||||
elif container_type == "quality":
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveMachine(self, stack_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = stack_id)
|
||||
if containers:
|
||||
Application.getInstance().setGlobalContainerStack(containers[0])
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def addMachine(self, name, definition_id):
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
definitions = container_registry.findDefinitionContainers(id = definition_id)
|
||||
if definitions:
|
||||
definition = definitions[0]
|
||||
name = self._createUniqueName("machine", "", name, definition.getName())
|
||||
new_global_stack = UM.Settings.ContainerStack(name)
|
||||
new_global_stack.addMetaDataEntry("type", "machine")
|
||||
container_registry.addContainer(new_global_stack)
|
||||
|
||||
variant_instance_container = self._updateVariantContainer(definition)
|
||||
material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
|
||||
quality_instance_container = self._updateQualityContainer(definition, material_instance_container)
|
||||
|
||||
current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
|
||||
current_settings_instance_container.addMetaDataEntry("machine", name)
|
||||
current_settings_instance_container.addMetaDataEntry("type", "user")
|
||||
current_settings_instance_container.setDefinition(definitions[0])
|
||||
container_registry.addContainer(current_settings_instance_container)
|
||||
|
||||
# If a definition is found, its a list. Should only have one item.
|
||||
new_global_stack.addContainer(definition)
|
||||
if variant_instance_container:
|
||||
new_global_stack.addContainer(variant_instance_container)
|
||||
if material_instance_container:
|
||||
new_global_stack.addContainer(material_instance_container)
|
||||
if quality_instance_container:
|
||||
new_global_stack.addContainer(quality_instance_container)
|
||||
new_global_stack.addContainer(current_settings_instance_container)
|
||||
|
||||
ExtruderManager.getInstance().addMachineExtruders(definition, new_global_stack.getId())
|
||||
|
||||
Application.getInstance().setGlobalContainerStack(new_global_stack)
|
||||
|
||||
|
||||
## Create a name that is not empty and unique
|
||||
# \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 new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def _createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
||||
return UM.Settings.ContainerRegistry.getInstance().createUniqueName(container_type, current_name, new_name, fallback_name)
|
||||
|
||||
## Convenience function to check if a stack has errors.
|
||||
def _checkStackForErrors(self, stack):
|
||||
if stack is None:
|
||||
return False
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
|
||||
return True
|
||||
return False
|
||||
|
||||
## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
|
||||
@pyqtSlot()
|
||||
def clearUserSettings(self):
|
||||
if not self._active_container_stack:
|
||||
return
|
||||
|
||||
self.blurSettings.emit()
|
||||
user_settings = self._active_container_stack.getTop()
|
||||
user_settings.clear()
|
||||
|
||||
## Check if the global_container has instances in the user container
|
||||
@pyqtProperty(bool, notify = activeStackChanged)
|
||||
def hasUserSettings(self):
|
||||
if not self._active_container_stack:
|
||||
return False
|
||||
|
||||
user_settings = self._active_container_stack.getTop().findInstances(**{})
|
||||
return len(user_settings) != 0
|
||||
|
||||
## Check if the global profile does not contain error states
|
||||
# Note that the _active_stack_valid is cached due to performance issues
|
||||
# Calling _checkStackForErrors on every change is simply too expensive
|
||||
@pyqtProperty(bool, notify = activeValidationChanged)
|
||||
def isActiveStackValid(self):
|
||||
return bool(self._active_stack_valid)
|
||||
|
||||
@pyqtProperty(str, notify = activeStackChanged)
|
||||
def activeUserProfileId(self):
|
||||
if self._active_container_stack:
|
||||
return self._active_container_stack.getTop().getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeMachineName(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getName()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeMachineId(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeStackChanged)
|
||||
def activeStackId(self):
|
||||
if self._active_container_stack:
|
||||
return self._active_container_stack.getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeMaterialChanged)
|
||||
def activeMaterialName(self):
|
||||
if self._active_container_stack:
|
||||
material = self._active_container_stack.findContainer({"type":"material"})
|
||||
if material:
|
||||
return material.getName()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeMaterialChanged)
|
||||
def activeMaterialId(self):
|
||||
if self._active_container_stack:
|
||||
material = self._active_container_stack.findContainer({"type": "material"})
|
||||
if material:
|
||||
return material.getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityName(self):
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if quality:
|
||||
return quality.getName()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityId(self):
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if quality:
|
||||
return quality.getId()
|
||||
return ""
|
||||
|
||||
## Check if a container is read_only
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isReadOnly(self, container_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return True
|
||||
return containers[0].isReadOnly()
|
||||
|
||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||
@pyqtSlot(str)
|
||||
def copyValueToExtruders(self, key):
|
||||
if not self._active_container_stack or self._global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
|
||||
return
|
||||
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())]
|
||||
stacks.append(self._global_container_stack)
|
||||
for extruder_stack in stacks:
|
||||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.getTop().setProperty(key, "value", new_value)
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def newQualityContainerFromQualityAndUser(self):
|
||||
new_container_id = self.duplicateContainer(self.activeQualityId)
|
||||
if new_container_id == "":
|
||||
return
|
||||
self.blurSettings.emit()
|
||||
self.updateQualityContainerFromUserContainer(new_container_id)
|
||||
self.setActiveQuality(new_container_id)
|
||||
return new_container_id
|
||||
|
||||
@pyqtSlot(str, result=str)
|
||||
def duplicateContainer(self, container_id):
|
||||
if not self._active_container_stack:
|
||||
return ""
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if containers:
|
||||
new_name = self._createUniqueName("quality", "", containers[0].getName(), catalog.i18nc("@label", "Custom profile"))
|
||||
|
||||
new_container = containers[0].duplicate(new_name, new_name)
|
||||
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
|
||||
|
||||
return new_name
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def renameQualityContainer(self, container_id, new_name):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id, type = "quality")
|
||||
if containers:
|
||||
new_name = self._createUniqueName("quality", containers[0].getName(), new_name,
|
||||
catalog.i18nc("@label", "Custom profile"))
|
||||
|
||||
if containers[0].getName() == new_name:
|
||||
# Nothing to do.
|
||||
return
|
||||
|
||||
# As we also want the id of the container to be changed (so that profile name is the name of the file
|
||||
# on disk. We need to create a new instance and remove it (so the old file of the container is removed)
|
||||
# If we don't do that, we might get duplicates & other weird issues.
|
||||
new_container = UM.Settings.InstanceContainer("")
|
||||
new_container.deserialize(containers[0].serialize())
|
||||
|
||||
# Actually set the name
|
||||
new_container.setName(new_name)
|
||||
new_container._id = new_name # Todo: Fix proper id change function for this.
|
||||
|
||||
# Add the "new" container.
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
|
||||
|
||||
# Ensure that the renamed profile is saved -before- we remove the old profile.
|
||||
Application.getInstance().saveSettings()
|
||||
|
||||
# Actually set & remove new / old quality.
|
||||
self.setActiveQuality(new_name)
|
||||
self.removeQualityContainer(containers[0].getId())
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeQualityContainer(self, container_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
|
||||
# If the container that is being removed is the currently active container, set another machine as the active container
|
||||
activate_new_container = container_id == self.activeQualityId
|
||||
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container_id)
|
||||
|
||||
if activate_new_container:
|
||||
definition_id = "fdmprinter" if not self.filterQualityByMachine else self.activeDefinitionId
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "quality", definition = definition_id)
|
||||
if containers:
|
||||
self.setActiveQuality(containers[0].getId())
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot()
|
||||
def updateQualityContainerFromUserContainer(self, quality_id = None):
|
||||
if not self._active_container_stack:
|
||||
return
|
||||
|
||||
if quality_id:
|
||||
quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id, type = "quality")
|
||||
if quality:
|
||||
quality = quality[0]
|
||||
else:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
|
||||
if not quality:
|
||||
return
|
||||
|
||||
user_settings = self._active_container_stack.getTop()
|
||||
|
||||
for key in user_settings.getAllKeys():
|
||||
quality.setProperty(key, "value", user_settings.getProperty(key, "value"))
|
||||
self.clearUserSettings() # As all users settings are noq a quality, remove them.
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveMaterial(self, material_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
|
||||
old_material = self._active_container_stack.findContainer({"type":"material"})
|
||||
old_quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if old_material:
|
||||
old_material.nameChanged.disconnect(self._onMaterialNameChanged)
|
||||
|
||||
material_index = self._active_container_stack.getContainerIndex(old_material)
|
||||
self._active_container_stack.replaceContainer(material_index, containers[0])
|
||||
|
||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||
|
||||
preferred_quality_name = None
|
||||
if old_quality:
|
||||
preferred_quality_name = old_quality.getName()
|
||||
|
||||
self.setActiveQuality(self._updateQualityContainer(self._global_container_stack.getBottom(), containers[0], preferred_quality_name).id)
|
||||
else:
|
||||
Logger.log("w", "While trying to set the active material, no material was found to replace.")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveVariant(self, variant_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
old_variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
old_material = self._active_container_stack.findContainer({"type": "material"})
|
||||
if old_variant:
|
||||
variant_index = self._active_container_stack.getContainerIndex(old_variant)
|
||||
self._active_container_stack.replaceContainer(variant_index, containers[0])
|
||||
|
||||
preferred_material = None
|
||||
if old_material:
|
||||
preferred_material_name = old_material.getName()
|
||||
self.setActiveMaterial(self._updateMaterialContainer(self._global_container_stack.getBottom(), containers[0], preferred_material_name).id)
|
||||
else:
|
||||
Logger.log("w", "While trying to set the active variant, no variant was found to replace.")
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveQuality(self, quality_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
|
||||
old_quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if old_quality and old_quality != containers[0]:
|
||||
old_quality.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
|
||||
quality_index = self._active_container_stack.getContainerIndex(old_quality)
|
||||
|
||||
self._active_container_stack.replaceContainer(quality_index, containers[0])
|
||||
|
||||
containers[0].nameChanged.connect(self._onQualityNameChanged)
|
||||
|
||||
if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1:
|
||||
# Ask the user if the user profile should be cleared or not (discarding the current settings)
|
||||
# In Simple Mode we assume the user always wants to keep the (limited) current settings
|
||||
details = catalog.i18nc("@label", "You made changes to the following setting(s):")
|
||||
user_settings = self._active_container_stack.getTop().findInstances(**{})
|
||||
for setting in user_settings:
|
||||
details = details + "\n " + setting.definition.label
|
||||
|
||||
Application.getInstance().messageBox(catalog.i18nc("@window:title", "Switched profiles"), catalog.i18nc("@label", "Do you want to transfer your changed settings to this profile?"),
|
||||
catalog.i18nc("@label", "If you transfer your settings they will override settings in the profile."), details,
|
||||
buttons = QMessageBox.Yes + QMessageBox.No, icon = QMessageBox.Question, callback = self._keepUserSettingsDialogCallback)
|
||||
else:
|
||||
Logger.log("w", "While trying to set the active quality, no quality was found to replace.")
|
||||
|
||||
def _keepUserSettingsDialogCallback(self, button):
|
||||
if button == QMessageBox.Yes:
|
||||
# Yes, keep the settings in the user profile with this profile
|
||||
pass
|
||||
elif button == QMessageBox.No:
|
||||
# No, discard the settings in the user profile
|
||||
self.clearUserSettings()
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeVariantName(self):
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
if variant:
|
||||
return variant.getName()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeVariantId(self):
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
if variant:
|
||||
return variant.getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeDefinitionId(self):
|
||||
if self._global_container_stack:
|
||||
definition = self._global_container_stack.getBottom()
|
||||
if definition:
|
||||
return definition.id
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def renameMachine(self, machine_id, new_name):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||
if containers:
|
||||
new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName())
|
||||
containers[0].setName(new_name)
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeMachine(self, machine_id):
|
||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(machine_id)
|
||||
|
||||
if activate_new_machine:
|
||||
stacks = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
if stacks:
|
||||
Application.getInstance().setGlobalContainerStack(stacks[0])
|
||||
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasMaterials(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
||||
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasVariants(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
||||
|
||||
return False
|
||||
|
||||
## Property to indicate if a machine has "specialized" material profiles.
|
||||
# Some machines have their own material profiles that "override" the default catch all profiles.
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterMaterialsByMachine(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
|
||||
|
||||
return False
|
||||
|
||||
## Property to indicate if a machine has "specialized" quality profiles.
|
||||
# Some machines have their own quality profiles that "override" the default catch all profiles.
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterQualityByMachine(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
||||
return False
|
||||
|
||||
## Get the Definition ID of a machine (specified by ID)
|
||||
# \param machine_id string machine id to get the definition ID of
|
||||
# \returns DefinitionID (string) if found, None otherwise
|
||||
@pyqtSlot(str, result = str)
|
||||
def getDefinitionByMachineId(self, machine_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id=machine_id)
|
||||
if containers:
|
||||
return containers[0].getBottom().getId()
|
||||
|
||||
@staticmethod
|
||||
def createMachineManager(engine=None, script_engine=None):
|
||||
return MachineManager()
|
||||
|
||||
def _updateVariantContainer(self, definition):
|
||||
if not definition.getMetaDataEntry("has_variants"):
|
||||
return self._empty_variant_container
|
||||
|
||||
containers = []
|
||||
preferred_variant = definition.getMetaDataEntry("preferred_variant")
|
||||
if preferred_variant:
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
|
||||
|
||||
if not containers:
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
|
||||
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_variant_container
|
||||
|
||||
def _updateMaterialContainer(self, definition, variant_container = None, preferred_material_name = None):
|
||||
if not definition.getMetaDataEntry("has_materials"):
|
||||
return self._empty_material_container
|
||||
|
||||
search_criteria = { "type": "material" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_materials"):
|
||||
search_criteria["definition"] = definition.id
|
||||
|
||||
if definition.getMetaDataEntry("has_variants") and variant_container:
|
||||
search_criteria["variant"] = variant_container.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
if preferred_material_name:
|
||||
search_criteria["name"] = preferred_material_name
|
||||
else:
|
||||
preferred_material = definition.getMetaDataEntry("preferred_material")
|
||||
if preferred_material:
|
||||
search_criteria["id"] = preferred_material
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
if "name" in search_criteria or "id" in search_criteria:
|
||||
# If a material 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 = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_material_container
|
||||
|
||||
def _updateQualityContainer(self, definition, material_container = None, preferred_quality_name = None):
|
||||
search_criteria = { "type": "quality" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = definition.id
|
||||
|
||||
if definition.getMetaDataEntry("has_materials") and material_container:
|
||||
search_criteria["material"] = material_container.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
if preferred_quality_name:
|
||||
search_criteria["name"] = preferred_quality_name
|
||||
else:
|
||||
preferred_quality = definition.getMetaDataEntry("preferred_quality")
|
||||
if preferred_quality:
|
||||
search_criteria["id"] = preferred_quality
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().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 = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_quality_container
|
||||
|
||||
def _onMachineNameChanged(self):
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
def _onMaterialNameChanged(self):
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
def _onQualityNameChanged(self):
|
||||
self.activeQualityChanged.emit()
|
||||
19
cura/Settings/MaterialSettingsVisibilityHandler.py
Normal file
19
cura/Settings/MaterialSettingsVisibilityHandler.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import UM.Settings.Models
|
||||
|
||||
class MaterialSettingsVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler):
|
||||
def __init__(self, parent = None, *args, **kwargs):
|
||||
super().__init__(parent = parent, *args, **kwargs)
|
||||
|
||||
material_settings = set([
|
||||
"material_print_temperature",
|
||||
"material_bed_temperature",
|
||||
"material_standby_temperature",
|
||||
"cool_fan_speed",
|
||||
"retraction_amount",
|
||||
"retraction_speed",
|
||||
])
|
||||
|
||||
self.setVisible(material_settings)
|
||||
96
cura/Settings/SettingOverrideDecorator.py
Normal file
96
cura/Settings/SettingOverrideDecorator.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import copy
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
import UM.Logger
|
||||
|
||||
import cura.Settings
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
## 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
|
||||
# this stack still resolve.
|
||||
@signalemitter
|
||||
class SettingOverrideDecorator(SceneNodeDecorator):
|
||||
## Event indicating that the user selected a different extruder.
|
||||
activeExtruderChanged = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._stack = ContainerStack(stack_id = id(self))
|
||||
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||
self._instance = InstanceContainer(container_id = "SettingOverrideInstanceContainer")
|
||||
self._stack.addContainer(self._instance)
|
||||
|
||||
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)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(self._stack)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
|
||||
self.activeExtruderChanged.connect(self._updateNextStack)
|
||||
self._updateNextStack()
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
## Create a fresh decorator object
|
||||
deep_copy = SettingOverrideDecorator()
|
||||
## Copy the instance
|
||||
deep_copy._instance = copy.deepcopy(self._instance, memo)
|
||||
|
||||
# Properly set the right extruder on the copy
|
||||
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
|
||||
|
||||
## Gets the currently active extruder to print this object with.
|
||||
#
|
||||
# \return An extruder's container stack.
|
||||
def getActiveExtruder(self):
|
||||
return self._extruder_stack
|
||||
|
||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||
if property_name == "value": # Only reslice if the value has changed.
|
||||
Application.getInstance().getBackend().forceSlice()
|
||||
|
||||
## Makes sure that the stack upon which the container stack is placed is
|
||||
# kept up to date.
|
||||
def _updateNextStack(self):
|
||||
if self._extruder_stack:
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
|
||||
if extruder_stack:
|
||||
if self._stack.getNextStack():
|
||||
old_extruder_stack_id = self._stack.getNextStack().getId()
|
||||
else:
|
||||
old_extruder_stack_id = ""
|
||||
|
||||
self._stack.setNextStack(extruder_stack[0])
|
||||
if self._stack.getNextStack().getId() != old_extruder_stack_id: #Only reslice if the extruder changed.
|
||||
Application.getInstance().getBackend().forceSlice()
|
||||
else:
|
||||
UM.Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
|
||||
else:
|
||||
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
|
||||
## Changes the extruder with which to print this node.
|
||||
#
|
||||
# \param extruder_stack_id The new extruder stack to print with.
|
||||
def setActiveExtruder(self, extruder_stack_id):
|
||||
self._extruder_stack = extruder_stack_id
|
||||
self._updateNextStack()
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
def getStack(self):
|
||||
return self._stack
|
||||
12
cura/Settings/__init__.py
Normal file
12
cura/Settings/__init__.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 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
|
||||
17
cura/ZOffsetDecorator.py
Normal file
17
cura/ZOffsetDecorator.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
## A decorator that stores the amount an object has been moved below the platform.
|
||||
class ZOffsetDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
self._z_offset = 0
|
||||
|
||||
def setZOffset(self, offset):
|
||||
self._z_offset = offset
|
||||
|
||||
def getZOffset(self):
|
||||
return self._z_offset
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
copied_decorator = ZOffsetDecorator()
|
||||
copied_decorator.setZOffset(self.getZOffset())
|
||||
return copied_decorator
|
||||
0
cura/__init__.py
Normal file
0
cura/__init__.py
Normal file
62
cura_app.py
Executable file
62
cura_app.py
Executable file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
|
||||
from UM.Platform import Platform
|
||||
|
||||
#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
|
||||
if Platform.isLinux(): # Needed for platform.linux_distribution, which is not available on Windows and OSX
|
||||
# For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
|
||||
if platform.linux_distribution()[0] in ("debian", "Ubuntu", "LinuxMint"): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
|
||||
import ctypes
|
||||
from ctypes.util import find_library
|
||||
libGL = find_library("GL")
|
||||
ctypes.CDLL(libGL, ctypes.RTLD_GLOBAL)
|
||||
|
||||
#WORKAROUND: GITHUB-704 GITHUB-708
|
||||
# It looks like setuptools creates a .pth file in
|
||||
# the default /usr/lib which causes the default site-packages
|
||||
# to be inserted into sys.path before PYTHONPATH.
|
||||
# This can cause issues such as having libsip loaded from
|
||||
# the system instead of the one provided with Cura, which causes
|
||||
# incompatibility issues with libArcus
|
||||
if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is used
|
||||
PYTHONPATH = os.environ["PYTHONPATH"].split(os.pathsep) # Get the value, split it..
|
||||
PYTHONPATH.reverse() # and reverse it, because we always insert at 1
|
||||
for PATH in PYTHONPATH: # Now beginning with the last PATH
|
||||
PATH_real = os.path.realpath(PATH) # Making the the path "real"
|
||||
if PATH_real in sys.path: # This should always work, but keep it to be sure..
|
||||
sys.path.remove(PATH_real)
|
||||
sys.path.insert(1, PATH_real) # Insert it at 1 after os.curdir, which is 0.
|
||||
|
||||
def exceptHook(hook_type, value, traceback):
|
||||
import cura.CrashHandler
|
||||
cura.CrashHandler.show(hook_type, value, traceback)
|
||||
|
||||
sys.excepthook = exceptHook
|
||||
|
||||
# Workaround for a race condition on certain systems where there
|
||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||
# first seems to prevent Sip from going into a state where it
|
||||
# tries to create PyQt objects on a non-main thread.
|
||||
import Arcus #@UnusedImport
|
||||
from UM.Platform import Platform
|
||||
import cura.CuraApplication
|
||||
import cura.Settings.CuraContainerRegistry
|
||||
|
||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||
dirpath = os.path.expanduser("~/AppData/Local/cura/")
|
||||
os.makedirs(dirpath, exist_ok = True)
|
||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
||||
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
||||
|
||||
# Force an instance of CuraContainerRegistry to be created and reused later.
|
||||
cura.Settings.CuraContainerRegistry.getInstance()
|
||||
|
||||
app = cura.CuraApplication.CuraApplication.getInstance()
|
||||
app.run()
|
||||
BIN
icons/cura-128.png
Normal file
BIN
icons/cura-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
icons/cura-32.png
Normal file
BIN
icons/cura-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/cura-48.png
Normal file
BIN
icons/cura-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/cura-64.png
Normal file
BIN
icons/cura-64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
icons/cura.icns
Normal file
BIN
icons/cura.icns
Normal file
Binary file not shown.
BIN
icons/cura.ico
Normal file
BIN
icons/cura.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 361 KiB |
160
installer.nsi
Normal file
160
installer.nsi
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
!ifndef VERSION
|
||||
!define VERSION '15.09.80'
|
||||
!endif
|
||||
|
||||
; The name of the installer
|
||||
Name "Cura ${VERSION}"
|
||||
|
||||
; The file to write
|
||||
OutFile "Cura_${VERSION}.exe"
|
||||
|
||||
; The default installation directory
|
||||
InstallDir $PROGRAMFILES\Cura_${VERSION}
|
||||
|
||||
; Registry key to check for directory (so if you install again, it will
|
||||
; overwrite the old one automatically)
|
||||
InstallDirRegKey HKLM "Software\Cura_${VERSION}" "Install_Dir"
|
||||
|
||||
; Request application privileges for Windows Vista
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; Set the LZMA compressor to reduce size.
|
||||
SetCompressor /SOLID lzma
|
||||
;--------------------------------
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "Library.nsh"
|
||||
|
||||
; !define MUI_ICON "dist/resources/cura.ico"
|
||||
!define MUI_BGCOLOR FFFFFF
|
||||
|
||||
; Directory page defines
|
||||
!define MUI_DIRECTORYPAGE_VERIFYONLEAVE
|
||||
|
||||
; Header
|
||||
; Don't show the component description box
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
|
||||
;Do not leave (Un)Installer page automaticly
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||
!define MUI_UNFINISHPAGE_NOAUTOCLOSE
|
||||
|
||||
;Run Cura after installing
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Start Cura ${VERSION}"
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||
|
||||
;Add an option to show release notes
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\plugins\ChangeLogPlugin\changelog.txt"
|
||||
|
||||
; Pages
|
||||
;!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
; Languages
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Reserve Files
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
ReserveFile '${NSISDIR}\Plugins\InstallOptions.dll'
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; The stuff to install
|
||||
Section "Cura ${VERSION}"
|
||||
|
||||
SectionIn RO
|
||||
|
||||
; Set output path to the installation directory.
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
; Put file there
|
||||
File /r "dist\"
|
||||
|
||||
; Write the installation path into the registry
|
||||
WriteRegStr HKLM "SOFTWARE\Cura_${VERSION}" "Install_Dir" "$INSTDIR"
|
||||
|
||||
; Write the uninstall keys for Windows
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "DisplayName" "Cura ${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoRepair" 1
|
||||
WriteUninstaller "uninstall.exe"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\Cura ${VERSION}"
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Uninstall Cura ${VERSION}.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk" "$INSTDIR\Cura.exe" '' "$INSTDIR\Cura.exe" 0
|
||||
|
||||
SectionEnd
|
||||
|
||||
Function LaunchLink
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
Exec '"$WINDIR\explorer.exe" "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk"'
|
||||
FunctionEnd
|
||||
|
||||
Section "Install Visual Studio 2010 Redistributable"
|
||||
SetOutPath "$INSTDIR"
|
||||
File "vcredist_2010_20110908_x86.exe"
|
||||
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\vcredist_2010_20110908_x86.exe" /q /norestart'
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "Install Arduino Drivers"
|
||||
; Set output path to the driver directory.
|
||||
SetOutPath "$INSTDIR\drivers\"
|
||||
File /r "drivers\"
|
||||
|
||||
${If} ${RunningX64}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst64.exe" /lm'
|
||||
${Else}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst32.exe" /lm'
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Section "Open STL files with Cura"
|
||||
WriteRegStr HKCR .stl "" "Cura STL model 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
|
||||
|
||||
Section /o "Open OBJ files with Cura"
|
||||
WriteRegStr HKCR .obj "" "Cura OBJ model file"
|
||||
DeleteRegValue HKCR .obj "Content Type"
|
||||
WriteRegStr HKCR "Cura OBJ model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell" "" "open"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
|
||||
SectionEnd
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; Uninstaller
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
; Remove registry keys
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}"
|
||||
DeleteRegKey HKLM "SOFTWARE\Cura_${VERSION}"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
; Remove directories used
|
||||
RMDir /r "$SMPROGRAMS\Cura ${VERSION}"
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
SectionEnd
|
||||
127
plugins/3MFReader/ThreeMFReader.py
Normal file
127
plugins/3MFReader/ThreeMFReader.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Job import Job
|
||||
|
||||
import math
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||
class ThreeMFReader(MeshReader):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".3mf"]
|
||||
|
||||
self._namespaces = {
|
||||
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
||||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
||||
}
|
||||
|
||||
def read(self, file_name):
|
||||
result = SceneNode()
|
||||
# The base object of 3mf is a zipped archive.
|
||||
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.
|
||||
objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
|
||||
if len(objects) == 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)
|
||||
return None
|
||||
|
||||
for entry in objects:
|
||||
mesh_builder = MeshBuilder()
|
||||
node = SceneNode()
|
||||
vertex_list = []
|
||||
#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)
|
||||
mesh_builder.reserveFaceCount(len(triangles))
|
||||
|
||||
for triangle in triangles:
|
||||
v1 = int(triangle.get("v1"))
|
||||
v2 = int(triangle.get("v2"))
|
||||
v3 = int(triangle.get("v3"))
|
||||
|
||||
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
|
||||
vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
|
||||
vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# Rotate the model; We use a different coordinate frame.
|
||||
rotation = Matrix()
|
||||
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
|
||||
|
||||
# TODO: We currently do not check for normals and simply recalculate them.
|
||||
mesh_builder.calculateNormals()
|
||||
mesh_builder.setFileName(file_name)
|
||||
node.setMeshData(mesh_builder.build().getTransformed(rotation))
|
||||
node.setSelectable(True)
|
||||
|
||||
transformations = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces)
|
||||
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 result
|
||||
27
plugins/3MFReader/__init__.py
Normal file
27
plugins/3MFReader/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import ThreeMFReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "3MF Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "3mf",
|
||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "mesh_reader": ThreeMFReader.ThreeMFReader() }
|
||||
54
plugins/AutoSave/AutoSave.py
Normal file
54
plugins/AutoSave/AutoSave.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
from UM.Logger import Logger
|
||||
|
||||
class AutoSave(Extension):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
|
||||
|
||||
self._global_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._onTimeout)
|
||||
|
||||
self._saving = False
|
||||
|
||||
def _triggerTimer(self, *args):
|
||||
if not self._saving:
|
||||
self._change_timer.start()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||
self._global_stack.containersChanged.disconnect(self._triggerTimer)
|
||||
|
||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.connect(self._triggerTimer)
|
||||
self._global_stack.containersChanged.connect(self._triggerTimer)
|
||||
|
||||
def _onTimeout(self):
|
||||
self._saving = True # To prevent the save process from triggering another autosave.
|
||||
Logger.log("d", "Autosaving preferences, instances and profiles")
|
||||
|
||||
Application.getInstance().saveSettings()
|
||||
|
||||
Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg"))
|
||||
|
||||
self._saving = False
|
||||
21
plugins/AutoSave/__init__.py
Normal file
21
plugins/AutoSave/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import AutoSave
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
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):
|
||||
return { "extension": AutoSave.AutoSave() }
|
||||
110
plugins/ChangeLogPlugin/ChangeLog.py
Normal file
110
plugins/ChangeLogPlugin/ChangeLog.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Extension import Extension
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Version import Version
|
||||
|
||||
from PyQt5.QtQuick import QQuickView
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtCore import QUrl, pyqtSlot, QObject
|
||||
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class ChangeLog(Extension, QObject,):
|
||||
def __init__(self, parent = None):
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self._changelog_window = None
|
||||
self._changelog_context = None
|
||||
version_string = Application.getInstance().getVersion()
|
||||
if version_string is not "master":
|
||||
self._version = Version(version_string)
|
||||
else:
|
||||
self._version = None
|
||||
|
||||
self._change_logs = None
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
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.showChangelog()
|
||||
|
||||
def getChangeLogs(self):
|
||||
if not self._change_logs:
|
||||
self.loadChangeLogs()
|
||||
return self._change_logs
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getChangeLogString(self):
|
||||
logs = self.getChangeLogs()
|
||||
result = ""
|
||||
for version in logs:
|
||||
result += "<h1>" + str(version) + "</h1><br>"
|
||||
result += ""
|
||||
for change in logs[version]:
|
||||
if str(change) != "":
|
||||
result += "<b>" + str(change) + "</b><br>"
|
||||
for line in logs[version][change]:
|
||||
result += str(line) + "<br>"
|
||||
result += "<br>"
|
||||
|
||||
pass
|
||||
return result
|
||||
|
||||
def loadChangeLogs(self):
|
||||
self._change_logs = collections.OrderedDict()
|
||||
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f:
|
||||
open_version = None
|
||||
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||
for line in f:
|
||||
line = line.replace("\n","")
|
||||
if "[" in line and "]" in line:
|
||||
line = line.replace("[","")
|
||||
line = line.replace("]","")
|
||||
open_version = Version(line)
|
||||
open_header = ""
|
||||
self._change_logs[open_version] = collections.OrderedDict()
|
||||
elif line.startswith("*"):
|
||||
open_header = line.replace("*","")
|
||||
self._change_logs[open_version][open_header] = []
|
||||
elif line != "":
|
||||
if open_header not in self._change_logs[open_version]:
|
||||
self._change_logs[open_version][open_header] = []
|
||||
self._change_logs[open_version][open_header].append(line)
|
||||
|
||||
def _onEngineCreated(self):
|
||||
if not self._version:
|
||||
return #We're on dev branch.
|
||||
|
||||
if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master":
|
||||
latest_version_shown = Version("0.0.0")
|
||||
else:
|
||||
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
|
||||
|
||||
if self._version > latest_version_shown:
|
||||
self.showChangelog()
|
||||
|
||||
def showChangelog(self):
|
||||
if not self._changelog_window:
|
||||
self.createChangelogWindow()
|
||||
|
||||
self._changelog_window.show()
|
||||
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||
|
||||
def hideChangelog(self):
|
||||
if self._changelog_window:
|
||||
self._changelog_window.hide()
|
||||
|
||||
def createChangelogWindow(self):
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml"))
|
||||
|
||||
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)
|
||||
43
plugins/ChangeLogPlugin/ChangeLog.qml
Normal file
43
plugins/ChangeLogPlugin/ChangeLog.qml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Cura is released under the terms of the AGPLv3 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
|
||||
minimumWidth: 400 * Screen.devicePixelRatio
|
||||
minimumHeight: 300 * Screen.devicePixelRatio
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
title: catalog.i18nc("@label", "Changelog")
|
||||
|
||||
ScrollView
|
||||
{
|
||||
width: parent.width
|
||||
height: parent.height - 25
|
||||
Label
|
||||
{
|
||||
text: manager.getChangeLogString()
|
||||
width:base.width - 35
|
||||
wrapMode: Text.Wrap;
|
||||
}
|
||||
}
|
||||
Button
|
||||
{
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
anchors.bottom:parent.bottom
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
onClicked: base.hide()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
92
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file
92
plugins/ChangeLogPlugin/ChangeLog.txt
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
[2.1.3]
|
||||
|
||||
*Material Profiles
|
||||
New material profiles for CPE+, PC, Nylon and TPU for the Ultimaker 2+ family.
|
||||
|
||||
[2.1.2]
|
||||
|
||||
Cura has been completely reengineered from the ground up for an even more seamless integration between hardware, software and materials. Together with its intuitive new user interface, it’s now also ready for any future developments. For the beginner Cura makes 3D printing incredibly easy, and for more advanced users, there are over 200 customizable settings.
|
||||
|
||||
*Select Multiple Objects
|
||||
You now have the freedom to select and manipulate multiple objects at the same time.
|
||||
|
||||
*Grouping
|
||||
You can now group objects together to make it easier to manipulate multiple objects.
|
||||
|
||||
*Undo/Redo
|
||||
You can now undo and redo your actions, like moving an object or scaling.
|
||||
|
||||
*Setting Profiles
|
||||
The new GUI allows custom profiles to load easily and intuitively, directly from Cura.
|
||||
|
||||
*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.
|
||||
|
||||
*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.
|
||||
|
||||
*64-bit Windows Builds
|
||||
An optimized 64-bit Windows Cura version is now available. This allows you to load larger model files.
|
||||
|
||||
*Automatic calculations
|
||||
Cura allows you to set a number of lines/layers instead of millimeters. The engine automatically calculates the right settings.
|
||||
|
||||
*Per-Object Settings
|
||||
Per-object settings allow you to override individual profile settings per object.
|
||||
|
||||
*Engine Features
|
||||
|
||||
*Line Width
|
||||
Line width settings added per feature: Global, Walls, Top/Bottom, Infill, Skirt, Support.
|
||||
|
||||
*Pattern Settings
|
||||
Pattern settings improved per feature: Top/Bottom, Infill, Support.
|
||||
|
||||
*Shell
|
||||
|
||||
*Alternate Skin Rotation
|
||||
Helps to combat the pillowing problem on top layers.
|
||||
|
||||
*Alternate Extra Wall
|
||||
For better infill adhesion.
|
||||
|
||||
*Horizontal Expansion
|
||||
Allows to compensate model x,y-size to get a 1:1 result.
|
||||
|
||||
*Travel
|
||||
|
||||
*Avoid Printed Parts
|
||||
When moving to the next part to print, avoid collisions between the nozzle and other parts which are already printed.
|
||||
|
||||
*Support
|
||||
|
||||
*Stair Step Height
|
||||
Sets the balance between sturdy and hard to remove support. By setting steps of the stair-like bottom of the support resting on the model.
|
||||
|
||||
*ZigZag
|
||||
A new, infill type that’s easily breakable, introduced specially for support.
|
||||
|
||||
*Support Roofs
|
||||
A new sub-feature to reduce scars the support leaves on overhangs.
|
||||
|
||||
*Support Towers
|
||||
Specialized support for tiny overhang areas.
|
||||
|
||||
*Special Modes
|
||||
|
||||
*Surface Mode
|
||||
This mode will print the surface of the mesh instead of the enclosed volume. This used to be called ‘Only follow mesh surface’. In addition to the ‘surface mode’ and ‘normal’, a ‘both’ mode has now been added. This ensures all closed volumes are printed as normal and all loose geometry as single walls.
|
||||
|
||||
*Experimental Features
|
||||
|
||||
*Conical Support
|
||||
An experimental filament, cost-reduction feature, for support.
|
||||
|
||||
*Draft Shield
|
||||
Prints a protective wall at a set distance around the object that prevents air from hitting the print, reducing warping.
|
||||
|
||||
*Fuzzy Skin
|
||||
Prints the outer walls with a jittering motion to give your object a diffuse finish.
|
||||
|
||||
*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.
|
||||
21
plugins/ChangeLogPlugin/__init__.py
Normal file
21
plugins/ChangeLogPlugin/__init__.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import ChangeLog
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
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):
|
||||
return {"extension": ChangeLog.ChangeLog()}
|
||||
123
plugins/CuraEngineBackend/Cura.proto
Normal file
123
plugins/CuraEngineBackend/Cura.proto
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package cura.proto;
|
||||
|
||||
message ObjectList
|
||||
{
|
||||
repeated Object objects = 1;
|
||||
repeated Setting settings = 2; // meshgroup settings (for one-at-a-time printing)
|
||||
}
|
||||
|
||||
message Slice
|
||||
{
|
||||
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
|
||||
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.
|
||||
}
|
||||
|
||||
message Extruder
|
||||
{
|
||||
int32 id = 1;
|
||||
SettingList settings = 2;
|
||||
}
|
||||
|
||||
message Object
|
||||
{
|
||||
int64 id = 1;
|
||||
bytes vertices = 2; //An array of 3 floats.
|
||||
bytes normals = 3; //An array of 3 floats.
|
||||
bytes indices = 4; //An array of ints.
|
||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||
}
|
||||
|
||||
message Progress
|
||||
{
|
||||
float amount = 1;
|
||||
}
|
||||
|
||||
message Layer {
|
||||
int32 id = 1;
|
||||
float height = 2; // Z position
|
||||
float thickness = 3; // height of a single layer
|
||||
|
||||
repeated Polygon polygons = 4; // layer data
|
||||
}
|
||||
|
||||
message Polygon {
|
||||
enum Type {
|
||||
NoneType = 0;
|
||||
Inset0Type = 1;
|
||||
InsetXType = 2;
|
||||
SkinType = 3;
|
||||
SupportType = 4;
|
||||
SkirtType = 5;
|
||||
InfillType = 6;
|
||||
SupportInfillType = 7;
|
||||
MoveCombingType = 8;
|
||||
MoveRetractionType = 9;
|
||||
}
|
||||
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)
|
||||
float line_width = 3; // The width of the line being laid down
|
||||
}
|
||||
|
||||
message LayerOptimized {
|
||||
int32 id = 1;
|
||||
float height = 2; // Z position
|
||||
float thickness = 3; // height of a single layer
|
||||
|
||||
repeated PathSegment path_segment = 4; // layer data
|
||||
}
|
||||
|
||||
|
||||
message PathSegment {
|
||||
int32 extruder = 1; // The extruder used for this path segment
|
||||
enum PointType {
|
||||
Point2D = 0;
|
||||
Point3D = 1;
|
||||
}
|
||||
PointType point_type = 2;
|
||||
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_width = 5; // The widths of the line segments as bytes of a float array of length 1 or N
|
||||
}
|
||||
|
||||
|
||||
message GCodeLayer {
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
|
||||
message PrintTimeMaterialEstimates { // The print time for the whole print and material estimates for the extruder
|
||||
float time = 1; // Total time estimate
|
||||
repeated MaterialEstimates materialEstimates = 2; // materialEstimates data
|
||||
}
|
||||
|
||||
message MaterialEstimates {
|
||||
int64 id = 1;
|
||||
float material_amount = 2; // material used in the extruder
|
||||
}
|
||||
|
||||
message SettingList {
|
||||
repeated Setting settings = 1;
|
||||
}
|
||||
|
||||
message Setting {
|
||||
string name = 1; // Internal key to signify a setting
|
||||
|
||||
bytes value = 2; // The value of the setting
|
||||
}
|
||||
|
||||
message SettingExtruder {
|
||||
string name = 1; //The setting key.
|
||||
|
||||
int32 extruder = 2; //From which extruder stack the setting should inherit.
|
||||
}
|
||||
|
||||
message GCodePrefix {
|
||||
bytes data = 2; //Header string to be prepended before the rest of the g-code sent from the engine.
|
||||
}
|
||||
|
||||
message SlicingFinished {
|
||||
}
|
||||
448
plugins/CuraEngineBackend/CuraEngineBackend.py
Normal file
448
plugins/CuraEngineBackend/CuraEngineBackend.py
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Backend.Backend import Backend, BackendState
|
||||
from UM.Application import Application
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Signal import Signal
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
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.Platform import Platform
|
||||
|
||||
import cura.Settings
|
||||
|
||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from . import ProcessSlicedLayersJob
|
||||
from . import ProcessGCodeJob
|
||||
from . import StartSliceJob
|
||||
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
import Arcus
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class CuraEngineBackend(Backend):
|
||||
## Starts the back-end plug-in.
|
||||
#
|
||||
# This registers all the signal listeners and prepares for communication
|
||||
# with the back-end in general.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# 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.
|
||||
executable_name = "CuraEngine"
|
||||
if Platform.isWindows():
|
||||
executable_name += ".exe"
|
||||
default_engine_location = executable_name
|
||||
if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
|
||||
default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
|
||||
if hasattr(sys, "frozen"):
|
||||
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
|
||||
if Platform.isLinux() and not default_engine_location:
|
||||
if not os.getenv("PATH"):
|
||||
raise OSError("There is something wrong with your Linux installation.")
|
||||
for pathdir in os.getenv("PATH").split(os.pathsep):
|
||||
execpath = os.path.join(pathdir, executable_name)
|
||||
if os.path.exists(execpath):
|
||||
default_engine_location = execpath
|
||||
break
|
||||
|
||||
if not default_engine_location:
|
||||
raise EnvironmentError("Could not find CuraEngine")
|
||||
|
||||
Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
|
||||
|
||||
default_engine_location = os.path.abspath(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.
|
||||
self._layer_view_active = False
|
||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
self._onActiveViewChanged()
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
# Triggers for when to (re)start slicing:
|
||||
self._global_container_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
self._active_extruder_stack = None
|
||||
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.
|
||||
# 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.setInterval(500)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self.slice)
|
||||
|
||||
# Listeners for receiving messages from the back-end.
|
||||
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
|
||||
self._message_handlers["cura.proto.LayerOptimized"] = self._onOptimizedLayerMessage
|
||||
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
|
||||
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
|
||||
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
|
||||
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
||||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||
|
||||
self._start_slice_job = None
|
||||
self._slicing = False # Are we currently slicing?
|
||||
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._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._backend_log_max_lines = 20000 # Maximum number of lines to buffer
|
||||
self._error_message = None # Pop-up message that shows errors.
|
||||
|
||||
self.backendQuit.connect(self._onBackendQuit)
|
||||
self.backendConnected.connect(self._onBackendConnected)
|
||||
|
||||
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
|
||||
Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
|
||||
Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||
|
||||
self._slice_start_time = None
|
||||
|
||||
## Called when closing the application.
|
||||
#
|
||||
# This function should terminate the engine process.
|
||||
def close(self):
|
||||
# Terminate CuraEngine if it is still running at this point
|
||||
self._terminate()
|
||||
super().close()
|
||||
|
||||
## Get the command that is used to call the engine.
|
||||
# This is useful for debugging and used to actually start the engine.
|
||||
# \return list of commands and args / parameters.
|
||||
def getEngineCommand(self):
|
||||
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
|
||||
return [Preferences.getInstance().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
|
||||
|
||||
## Emitted when we get a message containing print duration and material amount.
|
||||
# This also implies the slicing has finished.
|
||||
# \param time The amount of time the print will take.
|
||||
# \param material_amount The amount of material the print will use.
|
||||
printDurationMessage = Signal()
|
||||
|
||||
## Emitted when the slicing process starts.
|
||||
slicingStarted = Signal()
|
||||
|
||||
## Emitted when the slicing process is aborted forcefully.
|
||||
slicingCancelled = Signal()
|
||||
|
||||
## Perform a slice of the scene.
|
||||
def slice(self):
|
||||
self._slice_start_time = time()
|
||||
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.
|
||||
self._terminate()
|
||||
|
||||
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 = None
|
||||
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
self._scene.gcode_list = []
|
||||
self._slicing = True
|
||||
self.slicingStarted.emit()
|
||||
|
||||
slice_message = self._socket.createMessage("cura.proto.Slice")
|
||||
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
|
||||
self._start_slice_job.start()
|
||||
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
|
||||
|
||||
## Terminate the engine process.
|
||||
def _terminate(self):
|
||||
self._slicing = False
|
||||
self._restart = True
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
if self._start_slice_job is not None:
|
||||
self._start_slice_job.cancel()
|
||||
|
||||
self.slicingCancelled.emit()
|
||||
self.processingProgress.emit(0)
|
||||
Logger.log("d", "Attempting to kill the engine process")
|
||||
|
||||
if Application.getInstance().getCommandLineOption("external-backend", False):
|
||||
self._createSocket()
|
||||
return
|
||||
|
||||
if self._process is not None:
|
||||
Logger.log("d", "Killing engine process")
|
||||
try:
|
||||
self._process.terminate()
|
||||
Logger.log("d", "Engine process is killed. Received return code %s", self._process.wait())
|
||||
self._process = None
|
||||
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))
|
||||
|
||||
## Event handler to call when the job to initiate the slicing process is
|
||||
# completed.
|
||||
#
|
||||
# When the start slice job is successfully completed, it will be happily
|
||||
# slicing. This function handles any errors that may occur during the
|
||||
# bootstrapping of a slice job.
|
||||
#
|
||||
# \param job The start slice job that was just finished.
|
||||
def _onStartSliceCompleted(self, job):
|
||||
# Note that cancelled slice jobs can still call this method.
|
||||
if self._start_slice_job is job:
|
||||
self._start_slice_job = None
|
||||
|
||||
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
||||
return
|
||||
|
||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
||||
if Application.getInstance().getPlatformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. Please check your setting values for errors."))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
|
||||
if Application.getInstance().getPlatformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice. No suitable models found."))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
# Preparation completed, send it to the backend.
|
||||
self._socket.sendMessage(job.getSliceMessage())
|
||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||
|
||||
## Listener for when the scene has changed.
|
||||
#
|
||||
# This should start a slice if the scene is now ready to slice.
|
||||
#
|
||||
# \param source The scene node that was changed.
|
||||
def _onSceneChanged(self, source):
|
||||
if type(source) is not SceneNode:
|
||||
return
|
||||
|
||||
if source is self._scene.getRoot():
|
||||
return
|
||||
|
||||
if source.getMeshData() is None:
|
||||
return
|
||||
|
||||
if source.getMeshData().getVertices() is None:
|
||||
return
|
||||
|
||||
self._onChanged()
|
||||
|
||||
## Called when an error occurs in the socket connection towards the engine.
|
||||
#
|
||||
# \param error The exception that occurred.
|
||||
def _onSocketError(self, error):
|
||||
if Application.getInstance().isShuttingDown():
|
||||
return
|
||||
|
||||
super()._onSocketError(error)
|
||||
if error.getErrorCode() == Arcus.ErrorCode.Debug:
|
||||
return
|
||||
|
||||
self._terminate()
|
||||
|
||||
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")
|
||||
|
||||
## A setting has changed, so check if we must reslice.
|
||||
#
|
||||
# \param instance The setting instance that has changed.
|
||||
# \param property The property of the setting instance that has changed.
|
||||
def _onSettingChanged(self, instance, property):
|
||||
if property == "value": # Only reslice if the value has changed.
|
||||
self._onChanged()
|
||||
|
||||
## Called when a sliced layer data message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
def _onLayerMessage(self, message):
|
||||
self._stored_layer_data.append(message)
|
||||
|
||||
## Called when an optimized sliced layer data message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing sliced layer data.
|
||||
def _onOptimizedLayerMessage(self, message):
|
||||
self._stored_optimized_layer_data.append(message)
|
||||
|
||||
## Called when a progress message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the slicing progress.
|
||||
def _onProgressMessage(self, message):
|
||||
self.processingProgress.emit(message.amount)
|
||||
self.backendStateChange.emit(BackendState.Processing)
|
||||
|
||||
## Called when the engine sends a message that slicing is finished.
|
||||
#
|
||||
# \param message The protobuf message signalling that slicing is finished.
|
||||
def _onSlicingFinishedMessage(self, message):
|
||||
self.backendStateChange.emit(BackendState.Done)
|
||||
self.processingProgress.emit(1.0)
|
||||
|
||||
self._slicing = False
|
||||
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()):
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
## Called when a g-code message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing g-code, encoded as UTF-8.
|
||||
def _onGCodeLayerMessage(self, message):
|
||||
self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
|
||||
|
||||
## Called when a g-code prefix message is received from the engine.
|
||||
#
|
||||
# \param message The protobuf message containing the g-code prefix,
|
||||
# encoded as UTF-8.
|
||||
def _onGCodePrefixMessage(self, message):
|
||||
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.
|
||||
def _createSocket(self):
|
||||
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
|
||||
|
||||
## Manually triggers a reslice
|
||||
def forceSlice(self):
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when anything has changed to the stuff that needs to be sliced.
|
||||
#
|
||||
# This indicates that we should probably re-slice soon.
|
||||
def _onChanged(self, *args, **kwargs):
|
||||
self._change_timer.start()
|
||||
|
||||
## Called when the back-end connects to the front-end.
|
||||
def _onBackendConnected(self):
|
||||
if self._restart:
|
||||
self._onChanged()
|
||||
self._restart = False
|
||||
|
||||
## Called when the user starts using some tool.
|
||||
#
|
||||
# When the user starts using a tool, we should pause slicing to prevent
|
||||
# continuously slicing while the user is dragging some tool handle.
|
||||
#
|
||||
# \param tool The tool that the user is using.
|
||||
def _onToolOperationStarted(self, tool):
|
||||
self._terminate() # Do not continue slicing once a tool has started
|
||||
self._enabled = False # Do not reslice when a tool is doing it's 'thing'
|
||||
|
||||
## Called when the user stops using some tool.
|
||||
#
|
||||
# This indicates that we can safely start slicing again.
|
||||
#
|
||||
# \param tool The tool that the user was using.
|
||||
def _onToolOperationStopped(self, tool):
|
||||
self._enabled = True # Tool stop, start listening for changes again.
|
||||
|
||||
## Called when the user changes the active view mode.
|
||||
def _onActiveViewChanged(self):
|
||||
if 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.
|
||||
self._layer_view_active = True
|
||||
# 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 self._stored_optimized_layer_data and not self._slicing:
|
||||
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
else:
|
||||
self._layer_view_active = False
|
||||
|
||||
## Called when the back-end self-terminates.
|
||||
#
|
||||
# We should reset our state and start listening for new connections.
|
||||
def _onBackendQuit(self):
|
||||
if not self._restart and self._process:
|
||||
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
|
||||
self._process = None
|
||||
self._createSocket()
|
||||
|
||||
## Called when the global container stack changes
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
|
||||
self._global_container_stack.containersChanged.disconnect(self._onChanged)
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
if extruders:
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
||||
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
||||
self._global_container_stack.containersChanged.connect(self._onChanged)
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
if extruders:
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.connect(self._onSettingChanged)
|
||||
self._onActiveExtruderChanged()
|
||||
self._onChanged()
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
if self._global_container_stack:
|
||||
# Connect all extruders of the active machine. This might cause a few connects that have already happend,
|
||||
# but that shouldn't cause issues as only new / unique connections are added.
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
if extruders:
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.connect(self._onSettingChanged)
|
||||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.containersChanged.disconnect(self._onChanged)
|
||||
|
||||
self._active_extruder_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
15
plugins/CuraEngineBackend/ProcessGCodeJob.py
Normal file
15
plugins/CuraEngineBackend/ProcessGCodeJob.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Application import Application
|
||||
|
||||
class ProcessGCodeLayerJob(Job):
|
||||
def __init__(self, message):
|
||||
super().__init__()
|
||||
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._message = message
|
||||
|
||||
def run(self):
|
||||
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace"))
|
||||
185
plugins/CuraEngineBackend/ProcessSlicedLayersJob.py
Normal file
185
plugins/CuraEngineBackend/ProcessSlicedLayersJob.py
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Application import Application
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
from cura import LayerDataBuilder
|
||||
from cura import LayerDataDecorator
|
||||
from cura import LayerPolygon
|
||||
|
||||
import numpy
|
||||
from time import time
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class ProcessSlicedLayersJob(Job):
|
||||
def __init__(self, layers):
|
||||
super().__init__()
|
||||
self._layers = layers
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._progress = None
|
||||
self._abort_requested = False
|
||||
|
||||
## Aborts the processing of layers.
|
||||
#
|
||||
# This abort is made on a best-effort basis, meaning that the actual
|
||||
# job thread will check once in a while to see whether an abort is
|
||||
# requested and then stop processing by itself. There is no guarantee
|
||||
# that the abort will stop the job any time soon or even at all.
|
||||
def abort(self):
|
||||
self._abort_requested = True
|
||||
|
||||
def run(self):
|
||||
start_time = time()
|
||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
||||
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
|
||||
self._progress.show()
|
||||
Job.yieldThread()
|
||||
if self._abort_requested:
|
||||
if self._progress:
|
||||
self._progress.hide()
|
||||
return
|
||||
|
||||
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
|
||||
|
||||
new_node = SceneNode()
|
||||
|
||||
## Remove old layer data (if any)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
if self._abort_requested:
|
||||
if self._progress:
|
||||
self._progress.hide()
|
||||
return
|
||||
|
||||
mesh = MeshData()
|
||||
layer_data = LayerDataBuilder.LayerDataBuilder()
|
||||
layer_count = len(self._layers)
|
||||
|
||||
# Find the minimum layer number
|
||||
# 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.
|
||||
min_layer_number = 0
|
||||
for layer in self._layers:
|
||||
if layer.id < min_layer_number:
|
||||
min_layer_number = layer.id
|
||||
|
||||
current_layer = 0
|
||||
|
||||
for layer in self._layers:
|
||||
abs_layer_number = layer.id + abs(min_layer_number)
|
||||
|
||||
layer_data.addLayer(abs_layer_number)
|
||||
this_layer = layer_data.getLayer(abs_layer_number)
|
||||
layer_data.setLayerHeight(abs_layer_number, layer.height)
|
||||
layer_data.setLayerThickness(abs_layer_number, layer.thickness)
|
||||
|
||||
for p in range(layer.repeatedMessageCount("path_segment")):
|
||||
polygon = layer.getRepeatedMessage("path_segment", p)
|
||||
|
||||
extruder = polygon.extruder
|
||||
|
||||
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
|
||||
line_types = line_types.reshape((-1,1))
|
||||
|
||||
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
|
||||
if polygon.point_type == 0: # Point2D
|
||||
points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||
else: # Point3D
|
||||
points = points.reshape((-1,3))
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
# faster.
|
||||
new_points = numpy.empty((len(points), 3), numpy.float32)
|
||||
if polygon.point_type == 0: # Point2D
|
||||
new_points[:, 0] = points[:, 0]
|
||||
new_points[:, 1] = layer.height / 1000 # layer height value is in backend representation
|
||||
new_points[:, 2] = -points[:, 1]
|
||||
else: # Point3D
|
||||
new_points[:, 0] = points[:, 0]
|
||||
new_points[:, 1] = points[:, 2]
|
||||
new_points[:, 2] = -points[:, 1]
|
||||
|
||||
this_poly = LayerPolygon.LayerPolygon(layer_data, extruder, line_types, new_points, line_widths)
|
||||
this_poly.buildCache()
|
||||
|
||||
this_layer.polygons.append(this_poly)
|
||||
|
||||
Job.yieldThread()
|
||||
Job.yieldThread()
|
||||
current_layer += 1
|
||||
progress = (current_layer / layer_count) * 99
|
||||
# TODO: Rebuild the layer data mesh once the layer has been processed.
|
||||
# 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._progress:
|
||||
self._progress.hide()
|
||||
return
|
||||
if self._progress:
|
||||
self._progress.setProgress(progress)
|
||||
|
||||
# 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()
|
||||
|
||||
if self._abort_requested:
|
||||
if self._progress:
|
||||
self._progress.hide()
|
||||
return
|
||||
|
||||
# Add LayerDataDecorator to scene node to indicate that the node has layer data
|
||||
decorator = LayerDataDecorator.LayerDataDecorator()
|
||||
decorator.setLayerData(layer_mesh)
|
||||
new_node.addDecorator(decorator)
|
||||
|
||||
new_node.setMeshData(mesh)
|
||||
# Set build volume as parent, the build volume can move as a result of raft settings.
|
||||
# It makes sense to set the build volume as parent: the print is actually printed on it.
|
||||
new_node_parent = Application.getInstance().getBuildVolume()
|
||||
new_node.setParent(new_node_parent) # Note: After this we can no longer abort!
|
||||
|
||||
settings = Application.getInstance().getGlobalContainerStack()
|
||||
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))
|
||||
|
||||
if self._progress:
|
||||
self._progress.setProgress(100)
|
||||
|
||||
view = Application.getInstance().getController().getActiveView()
|
||||
if view.getPluginId() == "LayerView":
|
||||
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.
|
||||
self._layers = None
|
||||
|
||||
Logger.log("d", "Processing layers took %s seconds", time() - start_time)
|
||||
|
||||
def _onActiveViewChanged(self):
|
||||
if self.isRunning():
|
||||
if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView":
|
||||
if not self._progress:
|
||||
self._progress = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, 0)
|
||||
if self._progress.getProgress() != 100:
|
||||
self._progress.show()
|
||||
else:
|
||||
if self._progress:
|
||||
self._progress.hide()
|
||||
270
plugins/CuraEngineBackend/StartSliceJob.py
Normal file
270
plugins/CuraEngineBackend/StartSliceJob.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
from string import Formatter
|
||||
from enum import IntEnum
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Settings.SettingRelation import RelationType
|
||||
|
||||
from cura.OneAtATimeIterator import OneAtATimeIterator
|
||||
|
||||
import cura.Settings
|
||||
|
||||
class StartJobResult(IntEnum):
|
||||
Finished = 1
|
||||
Error = 2
|
||||
SettingError = 3
|
||||
NothingToSlice = 4
|
||||
|
||||
|
||||
## Formatter class that handles token expansion in start/end gcod
|
||||
class GcodeStartEndFormatter(Formatter):
|
||||
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
|
||||
if isinstance(key, str):
|
||||
try:
|
||||
return kwargs[key]
|
||||
except KeyError:
|
||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
|
||||
return "{" + key + "}"
|
||||
else:
|
||||
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
|
||||
return "{" + str(key) + "}"
|
||||
|
||||
|
||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||
class StartSliceJob(Job):
|
||||
def __init__(self, slice_message):
|
||||
super().__init__()
|
||||
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._slice_message = slice_message
|
||||
self._is_cancelled = False
|
||||
|
||||
def getSliceMessage(self):
|
||||
return self._slice_message
|
||||
|
||||
## Check if a stack has any errors.
|
||||
## returns true if it has errors, false otherwise.
|
||||
def _checkStackForErrors(self, stack):
|
||||
if stack is None:
|
||||
return False
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
Logger.log("w", "Setting %s is not valid, but %s. Aborting slicing.", key, validation_state)
|
||||
return True
|
||||
Job.yieldThread()
|
||||
return False
|
||||
|
||||
## Runs the job that initiates the slicing.
|
||||
def run(self):
|
||||
stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not stack:
|
||||
self.setResult(StartJobResult.Error)
|
||||
return
|
||||
|
||||
# Don't slice if there is a setting with an error value.
|
||||
if not Application.getInstance().getMachineManager().isActiveStackValid:
|
||||
self.setResult(StartJobResult.SettingError)
|
||||
return
|
||||
|
||||
# Don't slice if there is a per object setting with an error value.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if type(node) is not SceneNode or not node.isSelectable():
|
||||
continue
|
||||
|
||||
if self._checkStackForErrors(node.callDecoration("getStack")):
|
||||
self.setResult(StartJobResult.SettingError)
|
||||
return
|
||||
|
||||
with self._scene.getSceneLock():
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData"):
|
||||
node.getParent().removeChild(node)
|
||||
break
|
||||
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
|
||||
# Node can't be printed, so don't bother sending it.
|
||||
if getattr(node, "_outside_buildarea", False):
|
||||
continue
|
||||
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
temp_list = []
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||
if not getattr(node, "_outside_buildarea", False):
|
||||
temp_list.append(node)
|
||||
Job.yieldThread()
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for group in object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
if group[0].getParent().callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData().getTransformed(object.getWorldTransformation())
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
verts = numpy.array(mesh_data.getVertices())
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
|
||||
obj.vertices = verts
|
||||
|
||||
self._handlePerObjectSettings(object, obj)
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
self.setResult(StartJobResult.Finished)
|
||||
|
||||
def cancel(self):
|
||||
super().cancel()
|
||||
self._is_cancelled = True
|
||||
|
||||
def isCancelled(self):
|
||||
return self._is_cancelled
|
||||
|
||||
def _expandGcodeTokens(self, key, value, settings):
|
||||
try:
|
||||
# any setting can be used as a token
|
||||
fmt = GcodeStartEndFormatter()
|
||||
return str(fmt.format(value, **settings)).encode("utf-8")
|
||||
except:
|
||||
Logger.logException("w", "Unable to do token replacement on start/end gcode")
|
||||
return str(value).encode("utf-8")
|
||||
|
||||
## Create extruder message from stack
|
||||
def _buildExtruderMessage(self, stack):
|
||||
message = self._slice_message.addRepeatedMessage("extruders")
|
||||
message.id = int(stack.getMetaDataEntry("position"))
|
||||
|
||||
material_instance_container = stack.findContainer({"type": "material"})
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||
setting.name = key
|
||||
if key == "material_guid" and material_instance_container:
|
||||
# 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()
|
||||
|
||||
## Sends all global settings to the engine.
|
||||
#
|
||||
# The settings are taken from the global stack. This does not include any
|
||||
# per-extruder settings or per-object settings.
|
||||
def _buildGlobalSettingsMessage(self, stack):
|
||||
keys = stack.getAllKeys()
|
||||
settings = {}
|
||||
for key in keys:
|
||||
settings[key] = stack.getProperty(key, "value")
|
||||
|
||||
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
|
||||
settings["material_print_temp_prepend"] = "{material_print_temperature}" not in start_gcode
|
||||
|
||||
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.name = key
|
||||
if key == "machine_start_gcode" or key == "machine_end_gcode": #If it's a g-code message, use special formatting.
|
||||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
||||
else:
|
||||
setting_message.value = str(value).encode("utf-8")
|
||||
|
||||
## Sends for some settings which extruder they should fallback to if not
|
||||
# set.
|
||||
#
|
||||
# This is only set for settings that have the global_inherits_stack
|
||||
# property.
|
||||
#
|
||||
# \param stack The global stack with all settings, from which to read the
|
||||
# global_inherits_stack property.
|
||||
def _buildGlobalInheritsStackMessage(self, stack):
|
||||
for key in stack.getAllKeys():
|
||||
extruder = int(round(float(stack.getProperty(key, "global_inherits_stack"))))
|
||||
if extruder >= 0: #Set to a specific extruder.
|
||||
setting_extruder = self._slice_message.addRepeatedMessage("global_inherits_stack")
|
||||
setting_extruder.name = key
|
||||
setting_extruder.extruder = extruder
|
||||
|
||||
## 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 message object_lists message to put the per object settings in
|
||||
def _handlePerObjectSettings(self, node, message):
|
||||
stack = node.callDecoration("getStack")
|
||||
# Check if the node has a stack attached to it and the stack has any settings in the top container.
|
||||
if stack:
|
||||
# Check all settings for relations, so we can also calculate the correct values for dependant settings.
|
||||
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
|
||||
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
|
||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Recursive function to put all settings that require eachother for value changes in a list
|
||||
# \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.
|
||||
def _addRelations(self, relations_set, relations):
|
||||
for relation in filter(lambda r: r.role == "value", relations):
|
||||
if relation.type == RelationType.RequiresTarget:
|
||||
continue
|
||||
|
||||
relations_set.add(relation.target.key)
|
||||
self._addRelations(relations_set, relation.target.relations)
|
||||
22
plugins/CuraEngineBackend/__init__.py
Normal file
22
plugins/CuraEngineBackend/__init__.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#Shoopdawoop
|
||||
from . import CuraEngineBackend
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
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):
|
||||
return { "backend": CuraEngineBackend.CuraEngineBackend() }
|
||||
|
||||
42
plugins/CuraProfileReader/CuraProfileReader.py
Normal file
42
plugins/CuraProfileReader/CuraProfileReader.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.InstanceContainer import InstanceContainer #The new profile to make.
|
||||
from cura.ProfileReader import ProfileReader
|
||||
|
||||
## A plugin that reads profile data from Cura profile files.
|
||||
#
|
||||
# It reads a profile from a .curaprofile file, and returns it as a profile
|
||||
# instance.
|
||||
class CuraProfileReader(ProfileReader):
|
||||
## Initialises the cura profile reader.
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Reads a cura profile from a file and returns it.
|
||||
#
|
||||
# \param file_name The file to read the cura profile from.
|
||||
# \return The cura profile that was in the file, if any. If the file could
|
||||
# not be read or didn't contain a valid profile, \code None \endcode is
|
||||
# returned.
|
||||
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:
|
||||
with open(file_name) as f: # Open file for reading.
|
||||
serialized = f.read()
|
||||
except IOError as e:
|
||||
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
|
||||
return None
|
||||
|
||||
try:
|
||||
profile.deserialize(serialized)
|
||||
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))
|
||||
return None
|
||||
return profile
|
||||
27
plugins/CuraProfileReader/__init__.py
Normal file
27
plugins/CuraProfileReader/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import CuraProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
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": [
|
||||
{
|
||||
"extension": "curaprofile",
|
||||
"description": catalog.i18nc("@item:inlistbox", "Cura Profile")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "profile_reader": CuraProfileReader.CuraProfileReader() }
|
||||
26
plugins/CuraProfileWriter/CuraProfileWriter.py
Normal file
26
plugins/CuraProfileWriter/CuraProfileWriter.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2013 David Braam
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.SaveFile import SaveFile
|
||||
from cura.ProfileWriter import ProfileWriter
|
||||
|
||||
|
||||
## Writes profiles to Cura's own profile format with config files.
|
||||
class CuraProfileWriter(ProfileWriter):
|
||||
## Writes a profile to the specified file path.
|
||||
#
|
||||
# \param path \type{string} The file to output to.
|
||||
# \param profile \type{Profile} The profile to write to that file.
|
||||
# \return \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
def write(self, path, profile):
|
||||
serialized = profile.serialize()
|
||||
try:
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f: # Open the specified file.
|
||||
f.write(serialized)
|
||||
except Exception as e:
|
||||
Logger.log("e", "Failed to write profile to %s: %s", path, str(e))
|
||||
return False
|
||||
return True
|
||||
27
plugins/CuraProfileWriter/__init__.py
Normal file
27
plugins/CuraProfileWriter/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import CuraProfileWriter
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Cura Profile Writer"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for exporting Cura profiles."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_writer": [
|
||||
{
|
||||
"extension": "curaprofile",
|
||||
"description": catalog.i18nc("@item:inlistbox", "Cura Profile")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "profile_writer": CuraProfileWriter.CuraProfileWriter() }
|
||||
101
plugins/GCodeProfileReader/GCodeProfileReader.py
Normal file
101
plugins/GCodeProfileReader/GCodeProfileReader.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import re #Regular expressions for parsing escape characters in the settings.
|
||||
import json
|
||||
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura.ProfileReader import ProfileReader
|
||||
|
||||
## A class that reads profile data from g-code files.
|
||||
#
|
||||
# It reads the profile data from g-code files and stores it in a new profile.
|
||||
# This class currently does not process the rest of the g-code in any way.
|
||||
class GCodeProfileReader(ProfileReader):
|
||||
## The file format version of the serialized g-code.
|
||||
#
|
||||
# It can only read settings with the same version as the version it was
|
||||
# written with. If the file format is changed in a way that breaks reverse
|
||||
# compatibility, increment this version number!
|
||||
version = 3
|
||||
|
||||
## Dictionary that defines how characters are escaped when embedded in
|
||||
# g-code.
|
||||
#
|
||||
# Note that the keys of this dictionary are regex strings. The values are
|
||||
# not.
|
||||
escape_characters = {
|
||||
re.escape("\\\\"): "\\", #The escape character.
|
||||
re.escape("\\n"): "\n", #Newlines. They break off the comment.
|
||||
re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors.
|
||||
}
|
||||
|
||||
## Initialises the g-code reader as a profile reader.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Reads a g-code file, loading the profile from it.
|
||||
#
|
||||
# \param file_name The name of the file to read the profile from.
|
||||
# \return The profile that was in the specified file, if any. If the
|
||||
# specified file was no g-code or contained no parsable profile, \code
|
||||
# None \endcode is returned.
|
||||
def read(self, file_name):
|
||||
if file_name.split(".")[-1] != "gcode":
|
||||
return None
|
||||
|
||||
prefix = ";SETTING_" + str(GCodeProfileReader.version) + " "
|
||||
prefix_length = len(prefix)
|
||||
|
||||
# Loading all settings from the file.
|
||||
# They are all at the end, but Python has no reverse seek any more since Python3.
|
||||
# TODO: Consider moving settings to the start?
|
||||
serialized = "" # Will be filled with the serialized profile.
|
||||
try:
|
||||
with open(file_name) as f:
|
||||
for line in f:
|
||||
if line.startswith(prefix):
|
||||
# Remove the prefix and the newline from the line and add it to the rest.
|
||||
serialized += line[prefix_length : -1]
|
||||
except IOError as e:
|
||||
Logger.log("e", "Unable to open file %s for reading: %s", file_name, str(e))
|
||||
return None
|
||||
|
||||
serialized = unescapeGcodeComment(serialized)
|
||||
Logger.log("i", "Serialized the following from %s: %s" %(file_name, repr(serialized)))
|
||||
|
||||
json_data = json.loads(serialized)
|
||||
|
||||
profile_strings = [json_data["global_quality"]]
|
||||
profile_strings.extend(json_data.get("extruder_quality", []))
|
||||
|
||||
return [readQualityProfileFromString(profile_string) for profile_string in profile_strings]
|
||||
|
||||
## Unescape a string which has been escaped for use in a gcode comment.
|
||||
#
|
||||
# \param string The string to unescape.
|
||||
# \return \type{str} The unscaped string.
|
||||
def unescapeGcodeComment(string):
|
||||
# Un-escape the serialized profile.
|
||||
pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys()))
|
||||
|
||||
# Perform the replacement with a regular expression.
|
||||
return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string)
|
||||
|
||||
## Read in a profile from a serialized string.
|
||||
#
|
||||
# \param profile_string The profile data in serialized form.
|
||||
# \return \type{Profile} the resulting Profile object or None if it could not be read.
|
||||
def readQualityProfileFromString(profile_string):
|
||||
# Create an empty profile - the id and name will be changed by the ContainerRegistry
|
||||
profile = InstanceContainer("")
|
||||
try:
|
||||
profile.deserialize(profile_string)
|
||||
except Exception as e: # Not a valid g-code file.
|
||||
Logger.log("e", "Unable to serialise the profile: %s", str(e))
|
||||
return None
|
||||
return profile
|
||||
27
plugins/GCodeProfileReader/__init__.py
Normal file
27
plugins/GCodeProfileReader/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import GCodeProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "GCode Profile Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from g-code files."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_reader": [
|
||||
{
|
||||
"extension": "gcode",
|
||||
"description": catalog.i18nc("@item:inlistbox", "G-code File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "profile_reader": GCodeProfileReader.GCodeProfileReader() }
|
||||
126
plugins/GCodeWriter/GCodeWriter.py
Normal file
126
plugins/GCodeWriter/GCodeWriter.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Logger import Logger
|
||||
from UM.Application import Application
|
||||
import UM.Settings.ContainerRegistry
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
import re #For escaping characters in the settings.
|
||||
import json
|
||||
|
||||
## Writes g-code to a file.
|
||||
#
|
||||
# While this poses as a mesh writer, what this really does is take the g-code
|
||||
# in the entire scene and write it to an output device. Since the g-code of a
|
||||
# single mesh isn't separable from the rest what with rafts and travel moves
|
||||
# and all, it doesn't make sense to write just a single mesh.
|
||||
#
|
||||
# So this plug-in takes the g-code that is stored in the root of the scene
|
||||
# node tree, adds a bit of extra information about the profiles and writes
|
||||
# that to the output device.
|
||||
class GCodeWriter(MeshWriter):
|
||||
## The file format version of the serialised g-code.
|
||||
#
|
||||
# It can only read settings with the same version as the version it was
|
||||
# written with. If the file format is changed in a way that breaks reverse
|
||||
# compatibility, increment this version number!
|
||||
version = 3
|
||||
|
||||
## Dictionary that defines how characters are escaped when embedded in
|
||||
# g-code.
|
||||
#
|
||||
# Note that the keys of this dictionary are regex strings. The values are
|
||||
# not.
|
||||
escape_characters = {
|
||||
re.escape("\\"): "\\\\", # The escape character.
|
||||
re.escape("\n"): "\\n", # Newlines. They break off the comment.
|
||||
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def write(self, stream, node, mode = MeshWriter.OutputMode.TextMode):
|
||||
if mode != MeshWriter.OutputMode.TextMode:
|
||||
Logger.log("e", "GCode Writer does not support non-text mode.")
|
||||
return False
|
||||
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
gcode_list = getattr(scene, "gcode_list")
|
||||
if gcode_list:
|
||||
for gcode in gcode_list:
|
||||
stream.write(gcode)
|
||||
# Serialise the current container stack and put it at the end of the file.
|
||||
settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
|
||||
stream.write(settings)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
## Create a new container with container 2 as base and container 1 written over it.
|
||||
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
|
||||
flat_container = InstanceContainer(instance_container2.getName())
|
||||
flat_container.setDefinition(instance_container2.getDefinition())
|
||||
flat_container.setMetaData(instance_container2.getMetaData())
|
||||
|
||||
for key in instance_container2.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
|
||||
|
||||
for key in instance_container1.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
|
||||
return flat_container
|
||||
|
||||
|
||||
## Serialises a container stack to prepare it for writing at the end of the
|
||||
# g-code.
|
||||
#
|
||||
# The settings are serialised, and special characters (including newline)
|
||||
# are escaped.
|
||||
#
|
||||
# \param settings A container stack to serialise.
|
||||
# \return A serialised string of the settings.
|
||||
def _serialiseSettings(self, stack):
|
||||
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||
prefix_length = len(prefix)
|
||||
|
||||
container_with_profile = stack.findContainer({"type": "quality"})
|
||||
if not container_with_profile:
|
||||
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
|
||||
return ""
|
||||
|
||||
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(),container_with_profile)
|
||||
serialized = flat_global_container.serialize()
|
||||
data = {"global_quality": serialized}
|
||||
|
||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
extruder_quality = extruder.findContainer({"type": "quality"})
|
||||
if not extruder_quality:
|
||||
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
|
||||
continue
|
||||
|
||||
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
|
||||
|
||||
extruder_serialized = flat_extruder_quality.serialize()
|
||||
data.setdefault("extruder_quality", []).append(extruder_serialized)
|
||||
|
||||
json_string = json.dumps(data)
|
||||
|
||||
# Escape characters that have a special meaning in g-code comments.
|
||||
pattern = re.compile("|".join(GCodeWriter.escape_characters.keys()))
|
||||
|
||||
# Perform the replacement with a regular expression.
|
||||
escaped_string = pattern.sub(lambda m: GCodeWriter.escape_characters[re.escape(m.group(0))], json_string)
|
||||
|
||||
# Introduce line breaks so that each comment is no longer than 80 characters. Prepend each line with the prefix.
|
||||
result = ""
|
||||
|
||||
# Lines have 80 characters, so the payload of each line is 80 - prefix.
|
||||
for pos in range(0, len(escaped_string), 80 - prefix_length):
|
||||
result += prefix + escaped_string[pos : pos + 80 - prefix_length] + "\n"
|
||||
return result
|
||||
30
plugins/GCodeWriter/__init__.py
Normal file
30
plugins/GCodeWriter/__init__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import GCodeWriter
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "GCode Writer"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Writes GCode to a file."),
|
||||
"api": 3
|
||||
},
|
||||
|
||||
"mesh_writer": {
|
||||
"output": [{
|
||||
"extension": "gcode",
|
||||
"description": catalog.i18nc("@item:inlistbox", "GCode File"),
|
||||
"mime_type": "text/x-gcode",
|
||||
"mode": GCodeWriter.GCodeWriter.OutputMode.TextMode
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "mesh_writer": GCodeWriter.GCodeWriter() }
|
||||
196
plugins/ImageReader/ConfigUI.qml
Normal file
196
plugins/ImageReader/ConfigUI.qml
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Cura is released under the terms of the AGPLv3 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
|
||||
{
|
||||
width: 350 * Screen.devicePixelRatio;
|
||||
minimumWidth: 350 * Screen.devicePixelRatio;
|
||||
maximumWidth: 350 * Screen.devicePixelRatio;
|
||||
|
||||
height: 250 * Screen.devicePixelRatio;
|
||||
minimumHeight: 250 * Screen.devicePixelRatio;
|
||||
maximumHeight: 250 * Screen.devicePixelRatio;
|
||||
|
||||
title: catalog.i18nc("@title:window", "Convert Image...")
|
||||
|
||||
GridLayout
|
||||
{
|
||||
UM.I18nCatalog{id: catalog; name:"cura"}
|
||||
anchors.fill: parent;
|
||||
Layout.fillWidth: true
|
||||
columnSpacing: 16
|
||||
rowSpacing: 4
|
||||
columns: 1
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The maximum distance of each pixel from \"Base.\"")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Height (mm)")
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: peak_height
|
||||
objectName: "Peak_Height"
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: -500; top: 500;}
|
||||
width: 180
|
||||
onTextChanged: { manager.onPeakHeightChanged(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The base height from the build plate in millimeters.")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Base (mm)")
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: base_height
|
||||
objectName: "Base_Height"
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 0; top: 500;}
|
||||
width: 180
|
||||
onTextChanged: { manager.onBaseHeightChanged(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The width in millimeters on the build plate.")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Width (mm)")
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: width
|
||||
objectName: "Width"
|
||||
focus: true
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
||||
width: 180
|
||||
onTextChanged: { manager.onWidthChanged(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The depth in millimeters on the build plate")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Depth (mm)")
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
TextField {
|
||||
id: depth
|
||||
objectName: "Depth"
|
||||
focus: true
|
||||
validator: DoubleValidator {notation: DoubleValidator.StandardNotation; bottom: 1; top: 500;}
|
||||
width: 180
|
||||
onTextChanged: { manager.onDepthChanged(text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","By default, white pixels represent high points on the mesh and black pixels represent low points on the mesh. Change this option to reverse the behavior such that black pixels represent high points on the mesh and white pixels represent low points on the mesh.")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
//Empty label so 2 column layout works.
|
||||
Label {
|
||||
text: ""
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
ComboBox {
|
||||
id: image_color_invert
|
||||
objectName: "Image_Color_Invert"
|
||||
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ]
|
||||
width: 180
|
||||
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.TooltipArea {
|
||||
Layout.fillWidth:true
|
||||
height: childrenRect.height
|
||||
text: catalog.i18nc("@info:tooltip","The amount of smoothing to apply to the image.")
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Smoothing")
|
||||
width: 150
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 180
|
||||
height: 20
|
||||
Layout.fillWidth:true
|
||||
color: "transparent"
|
||||
|
||||
Slider {
|
||||
id: smoothing
|
||||
objectName: "Smoothing"
|
||||
maximumValue: 100.0
|
||||
stepSize: 1.0
|
||||
width: 180
|
||||
onValueChanged: { manager.onSmoothingChanged(value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
Button
|
||||
{
|
||||
id:ok_button
|
||||
text: catalog.i18nc("@action:button","OK");
|
||||
onClicked: { manager.onOkButtonClicked() }
|
||||
enabled: true
|
||||
},
|
||||
Button
|
||||
{
|
||||
id:cancel_button
|
||||
text: catalog.i18nc("@action:button","Cancel");
|
||||
onClicked: { manager.onCancelButtonClicked() }
|
||||
enabled: true
|
||||
}
|
||||
]
|
||||
}
|
||||
213
plugins/ImageReader/ImageReader.py
Normal file
213
plugins/ImageReader/ImageReader.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
|
||||
from PyQt5.QtGui import QImage, qRed, qGreen, qBlue
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from .ImageReaderUI import ImageReaderUI
|
||||
|
||||
|
||||
class ImageReader(MeshReader):
|
||||
def __init__(self):
|
||||
super(ImageReader, self).__init__()
|
||||
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
|
||||
self._ui = ImageReaderUI(self)
|
||||
|
||||
def preRead(self, file_name):
|
||||
img = QImage(file_name)
|
||||
|
||||
if img.isNull():
|
||||
Logger.log("e", "Image is corrupt.")
|
||||
return MeshReader.PreReadResult.failed
|
||||
|
||||
width = img.width()
|
||||
depth = img.height()
|
||||
|
||||
largest = max(width, depth)
|
||||
width = width / largest * self._ui.default_width
|
||||
depth = depth / largest * self._ui.default_depth
|
||||
|
||||
self._ui.setWidthAndDepth(width, depth)
|
||||
self._ui.showConfigUI()
|
||||
self._ui.waitForUIToClose()
|
||||
|
||||
if self._ui.getCancelled():
|
||||
return MeshReader.PreReadResult.cancelled
|
||||
return MeshReader.PreReadResult.accepted
|
||||
|
||||
def read(self, file_name):
|
||||
size = max(self._ui.getWidth(), self._ui.getDepth())
|
||||
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert)
|
||||
|
||||
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert):
|
||||
scene_node = SceneNode()
|
||||
|
||||
mesh = MeshBuilder()
|
||||
|
||||
img = QImage(file_name)
|
||||
|
||||
if img.isNull():
|
||||
Logger.log("e", "Image is corrupt.")
|
||||
return None
|
||||
|
||||
width = max(img.width(), 2)
|
||||
height = max(img.height(), 2)
|
||||
aspect = height / width
|
||||
|
||||
if img.width() < 2 or img.height() < 2:
|
||||
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
|
||||
|
||||
base_height = max(base_height, 0)
|
||||
peak_height = max(peak_height, -base_height)
|
||||
|
||||
xz_size = max(xz_size, 1)
|
||||
scale_vector = Vector(xz_size, peak_height, xz_size)
|
||||
|
||||
if width > height:
|
||||
scale_vector = scale_vector.set(z=scale_vector.z * aspect)
|
||||
elif height > width:
|
||||
scale_vector = scale_vector.set(x=scale_vector.x / aspect)
|
||||
|
||||
if width > max_size or height > max_size:
|
||||
scale_factor = max_size / width
|
||||
if height > width:
|
||||
scale_factor = max_size / height
|
||||
|
||||
width = int(max(round(width * scale_factor), 2))
|
||||
height = int(max(round(height * scale_factor), 2))
|
||||
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
|
||||
|
||||
width_minus_one = width - 1
|
||||
height_minus_one = height - 1
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
texel_width = 1.0 / (width_minus_one) * scale_vector.x
|
||||
texel_height = 1.0 / (height_minus_one) * scale_vector.z
|
||||
|
||||
height_data = numpy.zeros((height, width), dtype=numpy.float32)
|
||||
|
||||
for x in range(0, width):
|
||||
for y in range(0, height):
|
||||
qrgb = img.pixel(x, y)
|
||||
avg = float(qRed(qrgb) + qGreen(qrgb) + qBlue(qrgb)) / (3 * 255)
|
||||
height_data[y, x] = avg
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
if image_color_invert:
|
||||
height_data = 1 - height_data
|
||||
|
||||
for _ in range(0, blur_iterations):
|
||||
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge")
|
||||
|
||||
height_data += copy[1:-1, 2:]
|
||||
height_data += copy[1:-1, :-2]
|
||||
height_data += copy[2:, 1:-1]
|
||||
height_data += copy[:-2, 1:-1]
|
||||
|
||||
height_data += copy[2:, 2:]
|
||||
height_data += copy[:-2, 2:]
|
||||
height_data += copy[2:, :-2]
|
||||
height_data += copy[:-2, :-2]
|
||||
|
||||
height_data /= 9
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
height_data *= scale_vector.y
|
||||
height_data += base_height
|
||||
|
||||
heightmap_face_count = 2 * height_minus_one * width_minus_one
|
||||
total_face_count = heightmap_face_count + (width_minus_one * 2) * (height_minus_one * 2) + 2
|
||||
|
||||
mesh.reserveFaceCount(total_face_count)
|
||||
|
||||
# initialize to texel space vertex offsets.
|
||||
# 6 is for 6 vertices for each texel quad.
|
||||
heightmap_vertices = numpy.zeros((width_minus_one * height_minus_one, 6, 3), dtype = numpy.float32)
|
||||
heightmap_vertices = heightmap_vertices + numpy.array([[
|
||||
[0, base_height, 0],
|
||||
[0, base_height, texel_height],
|
||||
[texel_width, base_height, texel_height],
|
||||
[texel_width, base_height, texel_height],
|
||||
[texel_width, base_height, 0],
|
||||
[0, base_height, 0]
|
||||
]], dtype = numpy.float32)
|
||||
|
||||
offsetsz, offsetsx = numpy.mgrid[0: height_minus_one, 0: width - 1]
|
||||
offsetsx = numpy.array(offsetsx, numpy.float32).reshape(-1, 1) * texel_width
|
||||
offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height
|
||||
|
||||
# offsets for each texel quad
|
||||
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1)
|
||||
heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)
|
||||
|
||||
# apply height data to y values
|
||||
heightmap_vertices[:, 0, 1] = heightmap_vertices[:, 5, 1] = height_data[:-1, :-1].reshape(-1)
|
||||
heightmap_vertices[:, 1, 1] = height_data[1:, :-1].reshape(-1)
|
||||
heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
|
||||
heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)
|
||||
|
||||
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3)
|
||||
|
||||
mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
|
||||
mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices
|
||||
|
||||
mesh._vertex_count = heightmap_vertices.size // 3
|
||||
mesh._face_count = heightmap_indices.size // 3
|
||||
|
||||
geo_width = width_minus_one * texel_width
|
||||
geo_height = height_minus_one * texel_height
|
||||
|
||||
# bottom
|
||||
mesh.addFaceByPoints(0, 0, 0, 0, 0, geo_height, geo_width, 0, geo_height)
|
||||
mesh.addFaceByPoints(geo_width, 0, geo_height, geo_width, 0, 0, 0, 0, 0)
|
||||
|
||||
# north and south walls
|
||||
for n in range(0, width_minus_one):
|
||||
x = n * texel_width
|
||||
nx = (n + 1) * texel_width
|
||||
|
||||
hn0 = height_data[0, n]
|
||||
hn1 = height_data[0, n + 1]
|
||||
|
||||
hs0 = height_data[height_minus_one, n]
|
||||
hs1 = height_data[height_minus_one, n + 1]
|
||||
|
||||
mesh.addFaceByPoints(x, 0, 0, nx, 0, 0, nx, hn1, 0)
|
||||
mesh.addFaceByPoints(nx, hn1, 0, x, hn0, 0, x, 0, 0)
|
||||
|
||||
mesh.addFaceByPoints(x, 0, geo_height, nx, 0, geo_height, nx, hs1, geo_height)
|
||||
mesh.addFaceByPoints(nx, hs1, geo_height, x, hs0, geo_height, x, 0, geo_height)
|
||||
|
||||
# west and east walls
|
||||
for n in range(0, height_minus_one):
|
||||
y = n * texel_height
|
||||
ny = (n + 1) * texel_height
|
||||
|
||||
hw0 = height_data[n, 0]
|
||||
hw1 = height_data[n + 1, 0]
|
||||
|
||||
he0 = height_data[n, width_minus_one]
|
||||
he1 = height_data[n + 1, width_minus_one]
|
||||
|
||||
mesh.addFaceByPoints(0, 0, y, 0, 0, ny, 0, hw1, ny)
|
||||
mesh.addFaceByPoints(0, hw1, ny, 0, hw0, y, 0, 0, y)
|
||||
|
||||
mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
|
||||
mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)
|
||||
|
||||
mesh.calculateNormals(fast=True)
|
||||
|
||||
scene_node.setMeshData(mesh.build())
|
||||
|
||||
return scene_node
|
||||
155
plugins/ImageReader/ImageReaderUI.py
Normal file
155
plugins/ImageReader/ImageReaderUI.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class ImageReaderUI(QObject):
|
||||
show_config_ui_trigger = pyqtSignal()
|
||||
|
||||
def __init__(self, image_reader):
|
||||
super(ImageReaderUI, self).__init__()
|
||||
self.image_reader = image_reader
|
||||
self._ui_view = None
|
||||
self.show_config_ui_trigger.connect(self._actualShowConfigUI)
|
||||
|
||||
self.default_width = 120
|
||||
self.default_depth = 120
|
||||
|
||||
self._aspect = 1
|
||||
self._width = self.default_width
|
||||
self._depth = self.default_depth
|
||||
|
||||
self.base_height = 1
|
||||
self.peak_height = 10
|
||||
self.smoothing = 1
|
||||
self.image_color_invert = False;
|
||||
|
||||
self._ui_lock = threading.Lock()
|
||||
self._cancelled = False
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
def setWidthAndDepth(self, width, depth):
|
||||
self._aspect = width / depth
|
||||
self._width = width
|
||||
self._depth = depth
|
||||
|
||||
def getWidth(self):
|
||||
return self._width
|
||||
|
||||
def getDepth(self):
|
||||
return self._depth
|
||||
|
||||
def getCancelled(self):
|
||||
return self._cancelled
|
||||
|
||||
def waitForUIToClose(self):
|
||||
self._ui_lock.acquire()
|
||||
self._ui_lock.release()
|
||||
|
||||
def showConfigUI(self):
|
||||
self._ui_lock.acquire()
|
||||
self._cancelled = False
|
||||
self.show_config_ui_trigger.emit()
|
||||
|
||||
def _actualShowConfigUI(self):
|
||||
self._disable_size_callbacks = True
|
||||
|
||||
if self._ui_view is None:
|
||||
self._createConfigUI()
|
||||
self._ui_view.show()
|
||||
|
||||
self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width))
|
||||
self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth))
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
self._ui_view.findChild(QObject, "Base_Height").setProperty("text", str(self.base_height))
|
||||
self._ui_view.findChild(QObject, "Peak_Height").setProperty("text", str(self.peak_height))
|
||||
self._ui_view.findChild(QObject, "Smoothing").setProperty("value", self.smoothing)
|
||||
|
||||
def _createConfigUI(self):
|
||||
if self._ui_view is None:
|
||||
Logger.log("d", "Creating ImageReader config UI")
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml"))
|
||||
component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
self._ui_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._ui_context.setContextProperty("manager", self)
|
||||
self._ui_view = component.create(self._ui_context)
|
||||
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
|
||||
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot()
|
||||
def onOkButtonClicked(self):
|
||||
self._cancelled = False
|
||||
self._ui_view.close()
|
||||
self._ui_lock.release()
|
||||
|
||||
@pyqtSlot()
|
||||
def onCancelButtonClicked(self):
|
||||
self._cancelled = True
|
||||
self._ui_view.close()
|
||||
self._ui_lock.release()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onWidthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._width = float(value)
|
||||
else:
|
||||
self._width = 0
|
||||
|
||||
self._depth = self._width / self._aspect
|
||||
self._disable_size_callbacks = True
|
||||
self._ui_view.findChild(QObject, "Depth").setProperty("text", str(self._depth))
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onDepthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._depth = float(value)
|
||||
else:
|
||||
self._depth = 0
|
||||
|
||||
self._width = self._depth * self._aspect
|
||||
self._disable_size_callbacks = True
|
||||
self._ui_view.findChild(QObject, "Width").setProperty("text", str(self._width))
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onBaseHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.base_height = float(value)
|
||||
else:
|
||||
self.base_height = 0
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onPeakHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.peak_height = float(value)
|
||||
else:
|
||||
self.peak_height = 0
|
||||
|
||||
@pyqtSlot(float)
|
||||
def onSmoothingChanged(self, value):
|
||||
self.smoothing = int(value)
|
||||
|
||||
@pyqtSlot(int)
|
||||
def onImageColorInvertChanged(self, value):
|
||||
if (value == 1):
|
||||
self.image_color_invert = True
|
||||
else:
|
||||
self.image_color_invert = False
|
||||
43
plugins/ImageReader/__init__.py
Normal file
43
plugins/ImageReader/__init__.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import ImageReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "Image Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Enables ability to generate printable geometry from 2D image files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "jpg",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "JPG Image")
|
||||
},
|
||||
{
|
||||
"extension": "jpeg",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "JPEG Image")
|
||||
},
|
||||
{
|
||||
"extension": "png",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "PNG Image")
|
||||
},
|
||||
{
|
||||
"extension": "bmp",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "BMP Image")
|
||||
},
|
||||
{
|
||||
"extension": "gif",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "GIF Image")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "mesh_reader": ImageReader.ImageReader() }
|
||||
280
plugins/LayerView/LayerView.py
Normal file
280
plugins/LayerView/LayerView.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.View.View import View
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Resources import Resources
|
||||
from UM.Event import Event, KeyEvent
|
||||
from UM.Signal import Signal
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Math.Color import Color
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
from cura.ConvexHullNode import ConvexHullNode
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from . import LayerViewProxy
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import numpy
|
||||
|
||||
## View used to display g-code paths.
|
||||
class LayerView(View):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._shader = None
|
||||
self._selection_shader = None
|
||||
self._num_layers = 0
|
||||
self._layer_percentage = 0 # what percentage of layers need to be shown (Slider gives value between 0 - 100)
|
||||
self._proxy = LayerViewProxy.LayerViewProxy()
|
||||
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
|
||||
self._max_layers = 0
|
||||
self._current_layer_num = 0
|
||||
self._current_layer_mesh = None
|
||||
self._current_layer_jumps = None
|
||||
self._top_layers_job = None
|
||||
self._activity = False
|
||||
self._old_max_layers = 0
|
||||
|
||||
Preferences.getInstance().addPreference("view/top_layer_count", 5)
|
||||
Preferences.getInstance().addPreference("view/only_show_top_layers", False)
|
||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
|
||||
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
|
||||
self._busy = False
|
||||
|
||||
def getActivity(self):
|
||||
return self._activity
|
||||
|
||||
def getCurrentLayer(self):
|
||||
return self._current_layer_num
|
||||
|
||||
def _onSceneChanged(self, node):
|
||||
self.calculateMaxLayers()
|
||||
|
||||
def getMaxLayers(self):
|
||||
return self._max_layers
|
||||
|
||||
busyChanged = Signal()
|
||||
|
||||
def isBusy(self):
|
||||
return self._busy
|
||||
|
||||
def setBusy(self, busy):
|
||||
if busy != self._busy:
|
||||
self._busy = busy
|
||||
self.busyChanged.emit()
|
||||
|
||||
def resetLayerData(self):
|
||||
self._current_layer_mesh = None
|
||||
self._current_layer_jumps = None
|
||||
|
||||
def beginRendering(self):
|
||||
scene = self.getController().getScene()
|
||||
renderer = self.getRenderer()
|
||||
|
||||
if not self._selection_shader:
|
||||
self._selection_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
|
||||
self._selection_shader.setUniformValue("u_color", Color(32, 32, 32, 128))
|
||||
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
|
||||
# However, it is somewhat relevant when the node is selected, so do render it then.
|
||||
if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
|
||||
continue
|
||||
|
||||
if not node.render(renderer):
|
||||
if node.getMeshData() and node.isVisible():
|
||||
if Selection.isSelected(node):
|
||||
renderer.queueNode(node, transparent = True, shader = self._selection_shader)
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
||||
# Render all layers below a certain number as line mesh instead of vertices.
|
||||
if self._current_layer_num - self._solid_layers > -1 and not self._only_show_top_layers:
|
||||
start = 0
|
||||
end = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer, counts in element_counts.items():
|
||||
if layer + self._solid_layers > self._current_layer_num:
|
||||
break
|
||||
end += counts
|
||||
|
||||
# This uses glDrawRangeElements internally to only draw a certain range of lines.
|
||||
renderer.queueNode(node, mesh = layer_data, mode = RenderBatch.RenderMode.Lines, range = (start, end))
|
||||
|
||||
if self._current_layer_mesh:
|
||||
renderer.queueNode(node, mesh = self._current_layer_mesh)
|
||||
|
||||
if self._current_layer_jumps:
|
||||
renderer.queueNode(node, mesh = self._current_layer_jumps)
|
||||
|
||||
def setLayer(self, value):
|
||||
if self._current_layer_num != value:
|
||||
self._current_layer_num = value
|
||||
if self._current_layer_num < 0:
|
||||
self._current_layer_num = 0
|
||||
if self._current_layer_num > self._max_layers:
|
||||
self._current_layer_num = self._max_layers
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def calculateMaxLayers(self):
|
||||
scene = self.getController().getScene()
|
||||
self._activity = True
|
||||
|
||||
self._old_max_layers = self._max_layers
|
||||
## Recalculate num max layers
|
||||
new_max_layers = 0
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
||||
if new_max_layers < len(layer_data.getLayers()):
|
||||
new_max_layers = len(layer_data.getLayers()) - 1
|
||||
|
||||
if new_max_layers > 0 and new_max_layers != self._old_max_layers:
|
||||
self._max_layers = new_max_layers
|
||||
|
||||
# The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
|
||||
# if it's the largest value. If we don't do this, we can have a slider block outside of the
|
||||
# slider.
|
||||
if new_max_layers > self._current_layer_num:
|
||||
self.maxLayersChanged.emit()
|
||||
self.setLayer(int(self._max_layers))
|
||||
else:
|
||||
self.setLayer(int(self._max_layers))
|
||||
self.maxLayersChanged.emit()
|
||||
self._startUpdateTopLayers()
|
||||
|
||||
maxLayersChanged = Signal()
|
||||
currentLayerNumChanged = Signal()
|
||||
|
||||
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
|
||||
# as this caused some issues.
|
||||
def getProxy(self, engine, script_engine):
|
||||
return self._proxy
|
||||
|
||||
def endRendering(self):
|
||||
pass
|
||||
|
||||
def event(self, event):
|
||||
modifiers = QApplication.keyboardModifiers()
|
||||
ctrl_is_active = modifiers == Qt.ControlModifier
|
||||
if event.type == Event.KeyPressEvent and ctrl_is_active:
|
||||
if event.key == KeyEvent.UpKey:
|
||||
self.setLayer(self._current_layer_num + 1)
|
||||
return True
|
||||
if event.key == KeyEvent.DownKey:
|
||||
self.setLayer(self._current_layer_num - 1)
|
||||
return True
|
||||
|
||||
def _startUpdateTopLayers(self):
|
||||
if self._top_layers_job:
|
||||
self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
|
||||
self._top_layers_job.cancel()
|
||||
|
||||
self.setBusy(True)
|
||||
|
||||
self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
|
||||
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
|
||||
self._top_layers_job.start()
|
||||
|
||||
def _updateCurrentLayerMesh(self, job):
|
||||
self.setBusy(False)
|
||||
|
||||
if not job.getResult():
|
||||
return
|
||||
self.resetLayerData() # Reset the layer data only when job is done. Doing it now prevents "blinking" data.
|
||||
self._current_layer_mesh = job.getResult().get("layers")
|
||||
self._current_layer_jumps = job.getResult().get("jumps")
|
||||
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
|
||||
|
||||
self._top_layers_job = None
|
||||
|
||||
def _onPreferencesChanged(self, preference):
|
||||
if preference != "view/top_layer_count" and preference != "view/only_show_top_layers":
|
||||
return
|
||||
|
||||
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
|
||||
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
|
||||
|
||||
class _CreateTopLayersJob(Job):
|
||||
def __init__(self, scene, layer_number, solid_layers):
|
||||
super().__init__()
|
||||
|
||||
self._scene = scene
|
||||
self._layer_number = layer_number
|
||||
self._solid_layers = solid_layers
|
||||
self._cancel = False
|
||||
|
||||
def run(self):
|
||||
layer_data = None
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if layer_data:
|
||||
break
|
||||
|
||||
if self._cancel or not layer_data:
|
||||
return
|
||||
|
||||
layer_mesh = MeshBuilder()
|
||||
for i in range(self._solid_layers):
|
||||
layer_number = self._layer_number - i
|
||||
if layer_number < 0:
|
||||
continue
|
||||
|
||||
try:
|
||||
layer = layer_data.getLayer(layer_number).createMesh()
|
||||
except Exception:
|
||||
Logger.logException("w", "An exception occurred while creating layer mesh.")
|
||||
return
|
||||
|
||||
if not layer or layer.getVertices() is None:
|
||||
continue
|
||||
|
||||
layer_mesh.addIndices(layer_mesh.getVertexCount() + layer.getIndices())
|
||||
layer_mesh.addVertices(layer.getVertices())
|
||||
|
||||
# Scale layer color by a brightness factor based on the current layer number
|
||||
# This will result in a range of 0.5 - 1.0 to multiply colors by.
|
||||
brightness = numpy.ones((1, 4), dtype=numpy.float32) * (2.0 - (i / self._solid_layers)) / 2.0
|
||||
brightness[0, 3] = 1.0
|
||||
layer_mesh.addColors(layer.getColors() * brightness)
|
||||
|
||||
if self._cancel:
|
||||
return
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
if self._cancel:
|
||||
return
|
||||
|
||||
Job.yieldThread()
|
||||
jump_mesh = layer_data.getLayer(self._layer_number).createJumps()
|
||||
if not jump_mesh or jump_mesh.getVertices() is None:
|
||||
jump_mesh = None
|
||||
|
||||
self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
|
||||
|
||||
def cancel(self):
|
||||
self._cancel = True
|
||||
super().cancel()
|
||||
116
plugins/LayerView/LayerView.qml
Normal file
116
plugins/LayerView/LayerView.qml
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
|
||||
import UM 1.0 as UM
|
||||
|
||||
Item
|
||||
{
|
||||
width: UM.Theme.getSize("button").width
|
||||
height: UM.Theme.getSize("slider_layerview_size").height
|
||||
|
||||
Slider
|
||||
{
|
||||
id: slider
|
||||
width: UM.Theme.getSize("slider_layerview_size").width
|
||||
height: UM.Theme.getSize("slider_layerview_size").height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("slider_layerview_margin").width/2
|
||||
orientation: Qt.Vertical
|
||||
minimumValue: 0;
|
||||
maximumValue: UM.LayerView.numLayers;
|
||||
stepSize: 1
|
||||
|
||||
property real pixelsPerStep: ((height - UM.Theme.getSize("slider_handle").height) / (maximumValue - minimumValue)) * stepSize;
|
||||
|
||||
value: UM.LayerView.currentLayer
|
||||
onValueChanged: UM.LayerView.setCurrentLayer(value)
|
||||
|
||||
style: UM.Theme.styles.slider;
|
||||
|
||||
Rectangle
|
||||
{
|
||||
x: parent.width + UM.Theme.getSize("slider_layerview_background").width / 2;
|
||||
y: parent.height - (parent.value * parent.pixelsPerStep) - UM.Theme.getSize("slider_handle").height * 1.25;
|
||||
|
||||
height: UM.Theme.getSize("slider_handle").height + UM.Theme.getSize("default_margin").height
|
||||
width: valueLabel.width + UM.Theme.getSize("default_margin").width
|
||||
Behavior on height { NumberAnimation { duration: 50; } }
|
||||
|
||||
border.width: UM.Theme.getSize("default_lining").width;
|
||||
border.color: UM.Theme.getColor("slider_groove_border");
|
||||
|
||||
visible: UM.LayerView.getLayerActivity && Printer.getPlatformActivity ? true : false
|
||||
|
||||
TextField
|
||||
{
|
||||
id: valueLabel
|
||||
property string maxValue: slider.maximumValue + 1
|
||||
text: slider.value + 1
|
||||
horizontalAlignment: TextInput.AlignRight;
|
||||
onEditingFinished:
|
||||
{
|
||||
// Ensure that the cursor is at the first position. On some systems the text isn't fully visible
|
||||
// Seems to have to do something with different dpi densities that QML doesn't quite handle.
|
||||
// Another option would be to increase the size even further, but that gives pretty ugly results.
|
||||
cursorPosition = 0;
|
||||
if(valueLabel.text != '')
|
||||
{
|
||||
slider.value = valueLabel.text - 1;
|
||||
}
|
||||
}
|
||||
validator: IntValidator { bottom: 1; top: slider.maximumValue + 1; }
|
||||
|
||||
anchors.left: parent.left;
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width / 2;
|
||||
anchors.verticalCenter: parent.verticalCenter;
|
||||
|
||||
width: Math.max(UM.Theme.getSize("line").width * maxValue.length + 2, 20);
|
||||
style: TextFieldStyle
|
||||
{
|
||||
textColor: UM.Theme.getColor("setting_control_text");
|
||||
font: UM.Theme.getFont("default");
|
||||
background: Item { }
|
||||
}
|
||||
}
|
||||
|
||||
BusyIndicator
|
||||
{
|
||||
id: busyIndicator;
|
||||
anchors.left: parent.right;
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width / 2;
|
||||
anchors.verticalCenter: parent.verticalCenter;
|
||||
|
||||
width: UM.Theme.getSize("slider_handle").height;
|
||||
height: width;
|
||||
|
||||
running: UM.LayerView.busy;
|
||||
visible: UM.LayerView.busy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
z: slider.z - 1
|
||||
width: UM.Theme.getSize("slider_layerview_background").width
|
||||
height: slider.height + UM.Theme.getSize("default_margin").height * 2
|
||||
color: UM.Theme.getColor("tool_panel_background");
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
property double manualStepSize: slider.maximumValue / 11
|
||||
anchors.fill: parent
|
||||
onWheel: {
|
||||
slider.value = wheel.angleDelta.y < 0 ? slider.value - sliderMouseArea.manualStepSize : slider.value + sliderMouseArea.manualStepSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
plugins/LayerView/LayerViewProxy.py
Normal file
70
plugins/LayerView/LayerViewProxy.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
from UM.Application import Application
|
||||
|
||||
import LayerView
|
||||
|
||||
class LayerViewProxy(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._current_layer = 0
|
||||
self._controller = Application.getInstance().getController()
|
||||
self._controller.activeViewChanged.connect(self._onActiveViewChanged)
|
||||
self._onActiveViewChanged()
|
||||
|
||||
currentLayerChanged = pyqtSignal()
|
||||
maxLayersChanged = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = activityChanged)
|
||||
def getLayerActivity(self):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
return active_view.getActivity()
|
||||
|
||||
@pyqtProperty(int, notify = maxLayersChanged)
|
||||
def numLayers(self):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
return active_view.getMaxLayers()
|
||||
#return 100
|
||||
|
||||
@pyqtProperty(int, notify = currentLayerChanged)
|
||||
def currentLayer(self):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
return active_view.getCurrentLayer()
|
||||
|
||||
busyChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, notify = busyChanged)
|
||||
def busy(self):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
return active_view.isBusy()
|
||||
|
||||
return False
|
||||
|
||||
@pyqtSlot(int)
|
||||
def setCurrentLayer(self, layer_num):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
active_view.setLayer(layer_num)
|
||||
|
||||
def _layerActivityChanged(self):
|
||||
self.activityChanged.emit()
|
||||
|
||||
def _onLayerChanged(self):
|
||||
self.currentLayerChanged.emit()
|
||||
self._layerActivityChanged()
|
||||
|
||||
def _onMaxLayersChanged(self):
|
||||
self.maxLayersChanged.emit()
|
||||
|
||||
def _onBusyChanged(self):
|
||||
self.busyChanged.emit()
|
||||
|
||||
def _onActiveViewChanged(self):
|
||||
active_view = self._controller.getActiveView()
|
||||
if type(active_view) == LayerView.LayerView.LayerView:
|
||||
active_view.currentLayerNumChanged.connect(self._onLayerChanged)
|
||||
active_view.maxLayersChanged.connect(self._onMaxLayersChanged)
|
||||
active_view.busyChanged.connect(self._onBusyChanged)
|
||||
32
plugins/LayerView/__init__.py
Normal file
32
plugins/LayerView/__init__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import LayerView, LayerViewProxy
|
||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Layer View"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides the Layer view."),
|
||||
"api": 3
|
||||
},
|
||||
"view": {
|
||||
"name": catalog.i18nc("@item:inlistbox", "Layers"),
|
||||
"view_panel": "LayerView.qml",
|
||||
"weight": 2
|
||||
}
|
||||
}
|
||||
|
||||
def createLayerViewProxy(engine, script_engine):
|
||||
return LayerViewProxy.LayerViewProxy()
|
||||
|
||||
def register(app):
|
||||
layer_view = LayerView.LayerView()
|
||||
qmlRegisterSingletonType(LayerViewProxy.LayerViewProxy, "UM", 1, 0, "LayerView", layer_view.getProxy)
|
||||
return { "view": LayerView.LayerView() }
|
||||
157
plugins/LegacyProfileReader/DictionaryOfDoom.json
Normal file
157
plugins/LegacyProfileReader/DictionaryOfDoom.json
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
{
|
||||
"source_version": "15.04",
|
||||
"target_version": 1,
|
||||
|
||||
"translation": {
|
||||
"machine_nozzle_size": "nozzle_size",
|
||||
"line_width": "wall_thickness if (spiralize == \"True\" or simple_mode == \"True\") else (nozzle_size if (float(wall_thickness) < 0.01) else (wall_thickness if (float(wall_thickness) < float(nozzle_size)) else (nozzle_size if ((int(float(wall_thickness) / (float(nozzle_size) - 0.0001))) == 0) else ((float(wall_thickness) / ((int(float(wall_thickness) / (float(nozzle_size) - 0.0001))) + 1)) if ((float(wall_thickness) / (int(float(wall_thickness) / (float(nozzle_size) - 0.0001)))) > float(nozzle_size) * 1.5) else ((float(wall_thickness) / (int(float(wall_thickness) / (float(nozzle_size) - 0.0001)))))))))",
|
||||
"layer_height": "layer_height",
|
||||
"layer_height_0": "bottom_thickness",
|
||||
"wall_thickness": "wall_thickness",
|
||||
"top_thickness": "0 if (solid_top == \"False\") else solid_layer_thickness",
|
||||
"bottom_thickness": "0 if (solid_bottom == \"False\") else solid_layer_thickness",
|
||||
"infill_sparse_density": "fill_density",
|
||||
"infill_overlap": "fill_overlap",
|
||||
"infill_before_walls": "False if (perimeter_before_infill == \"True\") else True",
|
||||
"material_print_temperature": "print_temperature",
|
||||
"material_bed_temperature": "print_bed_temperature",
|
||||
"material_diameter": "filament_diameter",
|
||||
"material_flow": "filament_flow",
|
||||
"retraction_enable": "retraction_enable",
|
||||
"retraction_amount": "retraction_amount",
|
||||
"retraction_speed": "retraction_speed",
|
||||
"retraction_min_travel": "retraction_min_travel",
|
||||
"retraction_hop": "retraction_hop",
|
||||
"speed_print": "print_speed",
|
||||
"speed_infill": "infill_speed if (float(infill_speed) != 0) else print_speed",
|
||||
"speed_wall_0": "inset0_speed if (float(inset0_speed) != 0) else print_speed",
|
||||
"speed_wall_x": "insetx_speed if (float(insetx_speed) != 0) else print_speed",
|
||||
"speed_topbottom": "solidarea_speed if (float(solidarea_speed) != 0) else print_speed",
|
||||
"speed_travel": "travel_speed if (float(travel_speed) != 0) else travel_speed",
|
||||
"speed_layer_0": "bottom_layer_speed",
|
||||
"retraction_combing": "True if (retraction_combing == \"All\" or retraction_combing == \"No Skin\") else False",
|
||||
"cool_fan_enabled": "fan_enabled",
|
||||
"cool_fan_speed_min": "fan_speed",
|
||||
"cool_fan_speed_max": "fan_speed_max",
|
||||
"cool_fan_full_at_height": "fan_full_height",
|
||||
"cool_min_layer_time": "cool_min_layer_time",
|
||||
"cool_min_speed": "cool_min_feedrate",
|
||||
"cool_lift_head": "cool_head_lift",
|
||||
"support_enable": "False if (support == \"None\") else True",
|
||||
"support_type": "\"buildplate\" if (support == \"Touching buildplate\") else \"everywhere\"",
|
||||
"support_angle": "support_angle",
|
||||
"support_xy_distance": "support_xy_distance",
|
||||
"support_z_distance": "support_z_distance",
|
||||
"support_pattern": "support_type.lower()",
|
||||
"support_infill_rate": "support_fill_rate",
|
||||
"adhesion_type": "\"skirt\" if (platform_adhesion == \"None\") else platform_adhesion.lower()",
|
||||
"skirt_line_count": "skirt_line_count",
|
||||
"skirt_gap": "skirt_gap",
|
||||
"skirt_brim_minimal_length": "skirt_minimal_length",
|
||||
"brim_line_count": "brim_line_count",
|
||||
"raft_margin": "raft_margin",
|
||||
"raft_airgap": "float(raft_airgap_all) + float(raft_airgap)",
|
||||
"layer_0_z_overlap": "raft_airgap",
|
||||
"raft_surface_layers": "raft_surface_layers",
|
||||
"raft_surface_thickness": "raft_surface_thickness",
|
||||
"raft_surface_line_width": "raft_surface_linewidth",
|
||||
"raft_surface_line_spacing": "raft_line_spacing",
|
||||
"raft_interface_thickness": "raft_interface_thickness",
|
||||
"raft_interface_line_width": "raft_interface_linewidth",
|
||||
"raft_interface_line_spacing": "raft_line_spacing",
|
||||
"raft_base_thickness": "raft_base_thickness",
|
||||
"raft_base_line_width": "raft_base_linewidth",
|
||||
"raft_base_line_spacing": "raft_line_spacing",
|
||||
"meshfix_union_all": "fix_horrible_union_all_type_a",
|
||||
"meshfix_union_all_remove_holes": "fix_horrible_union_all_type_b",
|
||||
"meshfix_extensive_stitching": "fix_horrible_extensive_stitching",
|
||||
"meshfix_keep_open_polygons": "fix_horrible_use_open_bits",
|
||||
"magic_mesh_surface_mode": "simple_mode",
|
||||
"magic_spiralize": "spiralize",
|
||||
"prime_tower_enable": "wipe_tower",
|
||||
"prime_tower_size": "math.sqrt(float(wipe_tower_volume) / float(layer_height))",
|
||||
"ooze_shield_enabled": "ooze_shield"
|
||||
},
|
||||
|
||||
"defaults": {
|
||||
"bottom_layer_speed": "20",
|
||||
"bottom_thickness": "0.3",
|
||||
"brim_line_count": "20",
|
||||
"cool_head_lift": "False",
|
||||
"cool_min_feedrate": "10",
|
||||
"cool_min_layer_time": "5",
|
||||
"fan_enabled": "True",
|
||||
"fan_full_height": "0.5",
|
||||
"fan_speed": "100",
|
||||
"fan_speed_max": "100",
|
||||
"filament_diameter": "2.85",
|
||||
"filament_diameter2": "0",
|
||||
"filament_diameter3": "0",
|
||||
"filament_diameter4": "0",
|
||||
"filament_diameter5": "0",
|
||||
"filament_flow": "100.0",
|
||||
"fill_density": "20",
|
||||
"fill_overlap": "15",
|
||||
"fix_horrible_extensive_stitching": "False",
|
||||
"fix_horrible_union_all_type_a": "True",
|
||||
"fix_horrible_union_all_type_b": "False",
|
||||
"fix_horrible_use_open_bits": "False",
|
||||
"infill_speed": "0.0",
|
||||
"inset0_speed": "0.0",
|
||||
"insetx_speed": "0.0",
|
||||
"layer_height": "0.1",
|
||||
"layer0_width_factor": "100",
|
||||
"nozzle_size": "0.4",
|
||||
"object_sink": "0.0",
|
||||
"ooze_shield": "False",
|
||||
"overlap_dual": "0.15",
|
||||
"perimeter_before_infill": "False",
|
||||
"platform_adhesion": "None",
|
||||
"print_bed_temperature": "70",
|
||||
"print_speed": "50",
|
||||
"print_temperature": "210",
|
||||
"print_temperature2": "0",
|
||||
"print_temperature3": "0",
|
||||
"print_temperature4": "0",
|
||||
"print_temperature5": "0",
|
||||
"raft_airgap": "0.22",
|
||||
"raft_airgap_all": "0.0",
|
||||
"raft_base_linewidth": "1.0",
|
||||
"raft_base_thickness": "0.3",
|
||||
"raft_interface_linewidth": "0.4",
|
||||
"raft_interface_thickness": "0.27",
|
||||
"raft_line_spacing": "3.0",
|
||||
"raft_margin": "5.0",
|
||||
"raft_surface_layers": "2",
|
||||
"raft_surface_linewidth": "0.4",
|
||||
"raft_surface_thickness": "0.27",
|
||||
"retraction_amount": "4.5",
|
||||
"retraction_combing": "All",
|
||||
"retraction_dual_amount": "16.5",
|
||||
"retraction_enable": "True",
|
||||
"retraction_hop": "0.0",
|
||||
"retraction_min_travel": "1.5",
|
||||
"retraction_minimal_extrusion": "0.02",
|
||||
"retraction_speed": "40.0",
|
||||
"simple_mode": "False",
|
||||
"skirt_gap": "3.0",
|
||||
"skirt_line_count": "1",
|
||||
"skirt_minimal_length": "150.0",
|
||||
"solid_bottom": "True",
|
||||
"solid_layer_thickness": "0.6",
|
||||
"solid_top": "True",
|
||||
"solidarea_speed": "0.0",
|
||||
"spiralize": "False",
|
||||
"support": "None",
|
||||
"support_angle": "60",
|
||||
"support_dual_extrusion": "Both",
|
||||
"support_fill_rate": "15",
|
||||
"support_type": "Lines",
|
||||
"support_xy_distance": "0.7",
|
||||
"support_z_distance": "0.15",
|
||||
"travel_speed": "150.0",
|
||||
"wall_thickness": "0.8",
|
||||
"wipe_tower": "False",
|
||||
"wipe_tower_volume": "15"
|
||||
}
|
||||
}
|
||||
130
plugins/LegacyProfileReader/LegacyProfileReader.py
Normal file
130
plugins/LegacyProfileReader/LegacyProfileReader.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import configparser #For reading the legacy profile INI files.
|
||||
import json #For reading the Dictionary of Doom.
|
||||
import math #For mathematical operations included in the Dictionary of Doom.
|
||||
import os.path #For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
|
||||
|
||||
from UM.Application import Application #To get the machine manager to create the new profile in.
|
||||
from UM.Logger import Logger #Logging errors.
|
||||
from UM.PluginRegistry import PluginRegistry #For getting the path to this plugin's directory.
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer #For getting the current machine's defaults.
|
||||
from UM.Settings.InstanceContainer import InstanceContainer #The new profile to make.
|
||||
from cura.ProfileReader import ProfileReader #The plug-in type to implement.
|
||||
|
||||
## A plugin that reads profile data from legacy Cura versions.
|
||||
#
|
||||
# It reads a profile from an .ini file, and performs some translations on it.
|
||||
# Not all translations are correct, mind you, but it is a best effort.
|
||||
class LegacyProfileReader(ProfileReader):
|
||||
## Initialises the legacy profile reader.
|
||||
#
|
||||
# This does nothing since the only other function is basically stateless.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Prepares the default values of all legacy settings.
|
||||
#
|
||||
# These are loaded from the Dictionary of Doom.
|
||||
#
|
||||
# \param json The JSON file to load the default setting values from. This
|
||||
# should not be a URL but a pre-loaded JSON handle.
|
||||
# \return A dictionary of the default values of the legacy Cura version.
|
||||
def prepareDefaults(self, json):
|
||||
defaults = {}
|
||||
for key in json["defaults"]: #We have to copy over all defaults from the JSON handle to a normal dict.
|
||||
defaults[key] = json["defaults"][key]
|
||||
return defaults
|
||||
|
||||
## Prepares the local variables that can be used in evaluation of computing
|
||||
# new setting values from the old ones.
|
||||
#
|
||||
# This fills a dictionary with all settings from the legacy Cura version
|
||||
# and their values, so that they can be used in evaluating the new setting
|
||||
# values as Python code.
|
||||
#
|
||||
# \param config_parser The ConfigParser that finds the settings in the
|
||||
# legacy profile.
|
||||
# \param config_section The section in the profile where the settings
|
||||
# should be found.
|
||||
# \param defaults The default values for all settings in the legacy Cura.
|
||||
# \return A set of local variables, one for each setting in the legacy
|
||||
# profile.
|
||||
def prepareLocals(self, config_parser, config_section, defaults):
|
||||
copied_locals = defaults.copy() #Don't edit the original!
|
||||
for option in config_parser.options(config_section):
|
||||
copied_locals[option] = config_parser.get(config_section, option)
|
||||
return copied_locals
|
||||
|
||||
## Reads a legacy Cura profile from a file and returns it.
|
||||
#
|
||||
# \param file_name The file to read the legacy Cura profile from.
|
||||
# \return The legacy Cura profile that was in the file, if any. If the
|
||||
# file could not be read or didn't contain a valid profile, \code None
|
||||
# \endcode is returned.
|
||||
def read(self, file_name):
|
||||
if file_name.split(".")[-1] != "ini":
|
||||
return None
|
||||
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
||||
profile = InstanceContainer("Imported Legacy Profile") #Create an empty profile.
|
||||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
with open(file_name) as f:
|
||||
parser.readfp(f) #Parse the INI file.
|
||||
except Exception as e:
|
||||
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
|
||||
return None
|
||||
|
||||
#Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile".
|
||||
#Since importing multiple machine profiles is out of scope, just import the first section we find.
|
||||
section = ""
|
||||
for found_section in parser.sections():
|
||||
if found_section.startswith("profile"):
|
||||
section = found_section
|
||||
break
|
||||
if not section: #No section starting with "profile" was found. Probably not a proper INI file.
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", -1, "utf-8") as f:
|
||||
dict_of_doom = json.load(f) #Parse the Dictionary of Doom.
|
||||
except IOError as e:
|
||||
Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e))
|
||||
return None
|
||||
except Exception as e:
|
||||
Logger.log("e", "Could not parse DictionaryOfDoom.json: %s", str(e))
|
||||
return None
|
||||
|
||||
defaults = self.prepareDefaults(dict_of_doom)
|
||||
legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile.
|
||||
|
||||
#Check the target version in the Dictionary of Doom with this application version.
|
||||
if "target_version" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?")
|
||||
return None
|
||||
if InstanceContainer.Version != dict_of_doom["target_version"]:
|
||||
Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version))
|
||||
return None
|
||||
|
||||
if "translation" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
|
||||
return None
|
||||
current_printer = Application.getInstance().getGlobalContainerStack().findContainer({ }, DefinitionContainer)
|
||||
for new_setting in dict_of_doom["translation"]: #Evaluate all new settings that would get a value from the translations.
|
||||
old_setting_expression = dict_of_doom["translation"][new_setting]
|
||||
compiled = compile(old_setting_expression, new_setting, "eval")
|
||||
try:
|
||||
new_value = eval(compiled, {"math": math}, legacy_settings) #Pass the legacy settings as local variables to allow access to in the evaluation.
|
||||
value_using_defaults = eval(compiled, {"math": math}, defaults) #Evaluate again using only the default values to try to see if they are default.
|
||||
except Exception: #Probably some setting name that was missing or something else that went wrong in the ini file.
|
||||
Logger.log("w", "Setting " + new_setting + " could not be set because the evaluation failed. Something is probably missing from the imported legacy profile.")
|
||||
continue
|
||||
if new_value != value_using_defaults and current_printer.findDefinitions(key = new_setting).default_value != new_value: #Not equal to the default in the new Cura OR the default in the legacy Cura.
|
||||
profile.setSettingValue(new_setting, new_value) #Store the setting in the profile!
|
||||
|
||||
if len(profile.getChangedSettings()) == 0:
|
||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||
profile.setDirty(True)
|
||||
return profile
|
||||
27
plugins/LegacyProfileReader/__init__.py
Normal file
27
plugins/LegacyProfileReader/__init__.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import LegacyProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Legacy Cura Profile Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from legacy Cura versions."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_reader": [
|
||||
{
|
||||
"extension": "ini",
|
||||
"description": catalog.i18nc("@item:inlistbox", "Cura 15.04 profiles")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "profile_reader": LegacyProfileReader.LegacyProfileReader() }
|
||||
62
plugins/PerObjectSettingsTool/PerObjectCategory.qml
Normal file
62
plugins/PerObjectSettingsTool/PerObjectCategory.qml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
import ".."
|
||||
|
||||
Button {
|
||||
id: base;
|
||||
|
||||
style: ButtonStyle {
|
||||
background: Item { }
|
||||
label: Row
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_lining").width
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: label.height / 2
|
||||
width: height
|
||||
source: control.checked ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_right");
|
||||
color: control.hovered ? palette.highlight : palette.buttonText
|
||||
}
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: label.height
|
||||
width: height
|
||||
source: control.iconSource
|
||||
color: control.hovered ? palette.highlight : palette.buttonText
|
||||
}
|
||||
Label
|
||||
{
|
||||
id: label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: control.text
|
||||
color: control.hovered ? palette.highlight : palette.buttonText
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
SystemPalette { id: palette }
|
||||
}
|
||||
}
|
||||
|
||||
signal showTooltip(string text);
|
||||
signal hideTooltip();
|
||||
signal contextMenuRequested()
|
||||
|
||||
text: definition.label
|
||||
iconSource: UM.Theme.getIcon(definition.icon)
|
||||
|
||||
checkable: true
|
||||
checked: definition.expanded
|
||||
|
||||
onClicked: definition.expanded ? settingDefinitionsModel.collapse(definition.key) : settingDefinitionsModel.expandAll(definition.key)
|
||||
}
|
||||
34
plugins/PerObjectSettingsTool/PerObjectItem.qml
Normal file
34
plugins/PerObjectSettingsTool/PerObjectItem.qml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
x: model.depth * UM.Theme.getSize("default_margin").width;
|
||||
text: model.description;
|
||||
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: check
|
||||
|
||||
text: definition.label
|
||||
checked: addedSettingsModel.getVisible(model.key)
|
||||
|
||||
onClicked:
|
||||
{
|
||||
addedSettingsModel.setVisible(model.key, checked);
|
||||
UM.ActiveTool.forceUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Logger import Logger
|
||||
import UM.Settings.Models
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherits-stack setting values from different extruders.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
## The per object setting visibility handler ensures that only setting
|
||||
# definitions that have a matching instance Container are returned as visible.
|
||||
class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler):
|
||||
def __init__(self, parent = None, *args, **kwargs):
|
||||
super().__init__(parent = parent, *args, **kwargs)
|
||||
|
||||
self._selected_object_id = None
|
||||
self._node = None
|
||||
self._stack = None
|
||||
|
||||
def setSelectedObjectId(self, id):
|
||||
if id != self._selected_object_id:
|
||||
self._selected_object_id = id
|
||||
|
||||
self._node = Application.getInstance().getController().getScene().findObject(self._selected_object_id)
|
||||
if self._node:
|
||||
self._stack = self._node.callDecoration("getStack")
|
||||
|
||||
self.visibilityChanged.emit()
|
||||
|
||||
@pyqtProperty("quint64", fset = setSelectedObjectId)
|
||||
def selectedObjectId(self):
|
||||
return self._selected_object_id
|
||||
|
||||
def setVisible(self, visible):
|
||||
if not self._node:
|
||||
return
|
||||
|
||||
if not self._stack:
|
||||
self._node.addDecorator(SettingOverrideDecorator())
|
||||
self._stack = self._node.callDecoration("getStack")
|
||||
|
||||
settings = self._stack.getTop()
|
||||
all_instances = settings.findInstances()
|
||||
visibility_changed = False # Flag to check if at the end the signal needs to be emitted
|
||||
|
||||
# Remove all instances that are not in visibility list
|
||||
for instance in all_instances:
|
||||
if instance.definition.key not in visible:
|
||||
settings.removeInstance(instance.definition.key)
|
||||
visibility_changed = True
|
||||
|
||||
# Add all instances that are not added, but are in visibility list
|
||||
for item in visible:
|
||||
if not settings.getInstance(item):
|
||||
definition = self._stack.getSettingDefinition(item)
|
||||
if definition:
|
||||
new_instance = SettingInstance(definition, settings)
|
||||
stack_nr = -1
|
||||
if definition.global_inherits_stack and self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
#Obtain the value from the correct container stack. Only once, upon adding the setting.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty(item, "global_inherits_stack"))))) #Stack to get the setting from. Round it and remove the fractional part.
|
||||
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value"): #Property not defined, but we have an extruder number.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty("extruder_nr", "value")))))
|
||||
if stack_nr in ExtruderManager.getInstance().extruderIds: #We have either a global_inherits_stack or an extruder_nr.
|
||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||
else:
|
||||
stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
new_instance.setProperty("value", stack.getProperty(item, "value"))
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
settings.addInstance(new_instance)
|
||||
visibility_changed = True
|
||||
else:
|
||||
Logger.log("w", "Unable to add instance (%s) to per-object visibility because we couldn't find the matching definition", item)
|
||||
|
||||
if visibility_changed:
|
||||
self.visibilityChanged.emit()
|
||||
|
||||
def getVisible(self):
|
||||
visible_settings = set()
|
||||
if not self._node:
|
||||
return visible_settings
|
||||
|
||||
if not self._stack:
|
||||
return visible_settings
|
||||
|
||||
settings = self._stack.getTop()
|
||||
if not settings:
|
||||
return visible_settings
|
||||
|
||||
visible_settings = set(map(lambda i: i.definition.key, settings.findInstances()))
|
||||
return visible_settings
|
||||
|
||||
469
plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml
Normal file
469
plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml
Normal file
|
|
@ -0,0 +1,469 @@
|
|||
// Copyright (c) 2016 Ultimaker B.V.
|
||||
// Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.2
|
||||
import QtQuick.Controls.Styles 1.2
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.0 as Cura
|
||||
import ".."
|
||||
|
||||
Item {
|
||||
id: base;
|
||||
|
||||
UM.I18nCatalog { id: catalog; name: "cura"; }
|
||||
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
|
||||
Column
|
||||
{
|
||||
id: items
|
||||
anchors.top: parent.top;
|
||||
anchors.left: parent.left;
|
||||
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
Row
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Print model with")
|
||||
anchors.verticalCenter: extruderSelector.verticalCenter
|
||||
|
||||
color: UM.Theme.getColor("setting_control_text")
|
||||
font: UM.Theme.getFont("default")
|
||||
visible: extruderSelector.visible
|
||||
}
|
||||
ComboBox
|
||||
{
|
||||
id: extruderSelector
|
||||
|
||||
model: Cura.ExtrudersModel
|
||||
{
|
||||
id: extruders_model
|
||||
onRowsInserted: extruderSelector.visible = extruders_model.rowCount() > 1
|
||||
onModelReset: extruderSelector.visible = extruders_model.rowCount() > 1
|
||||
onModelChanged: extruderSelector.color = extruders_model.getItem(extruderSelector.currentIndex).color
|
||||
}
|
||||
property string color: extruders_model.getItem(extruderSelector.currentIndex).color
|
||||
visible: extruders_model.rowCount() > 1
|
||||
textRole: "name"
|
||||
width: UM.Theme.getSize("setting_control").width
|
||||
height: UM.Theme.getSize("section").height
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: wheel.accepted = true;
|
||||
}
|
||||
|
||||
style: ComboBoxStyle
|
||||
{
|
||||
background: Rectangle
|
||||
{
|
||||
color:
|
||||
{
|
||||
if(extruderSelector.hovered || base.activeFocus)
|
||||
{
|
||||
return UM.Theme.getColor("setting_control_highlight");
|
||||
}
|
||||
else
|
||||
{
|
||||
return UM.Theme.getColor("setting_control");
|
||||
}
|
||||
}
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("setting_control_border")
|
||||
}
|
||||
label: Item
|
||||
{
|
||||
Rectangle
|
||||
{
|
||||
id: swatch
|
||||
height: UM.Theme.getSize("setting_control").height / 2
|
||||
width: height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("default_lining").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
color: extruderSelector.color
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : UM.Theme.getColor("setting_control_border")
|
||||
}
|
||||
Label
|
||||
{
|
||||
anchors.left: swatch.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_lining").width
|
||||
anchors.right: downArrow.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_lining").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: extruderSelector.currentText
|
||||
font: UM.Theme.getFont("default")
|
||||
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text")
|
||||
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: downArrow
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
source: UM.Theme.getIcon("arrow_bottom")
|
||||
width: UM.Theme.getSize("standard_arrow").width
|
||||
height: UM.Theme.getSize("standard_arrow").height
|
||||
sourceSize.width: width + 5
|
||||
sourceSize.height: width + 5
|
||||
|
||||
color: UM.Theme.getColor("setting_control_text")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onActivated:
|
||||
{
|
||||
UM.ActiveTool.setProperty("SelectedActiveExtruder", extruders_model.getItem(index).id);
|
||||
extruderSelector.color = extruders_model.getItem(index).color;
|
||||
}
|
||||
onModelChanged: updateCurrentIndex();
|
||||
|
||||
function updateCurrentIndex()
|
||||
{
|
||||
for(var i = 0; i < extruders_model.rowCount(); ++i)
|
||||
{
|
||||
if(extruders_model.getItem(i).id == UM.ActiveTool.properties.getValue("SelectedActiveExtruder"))
|
||||
{
|
||||
extruderSelector.currentIndex = i;
|
||||
extruderSelector.color = extruders_model.getItem(i).color;
|
||||
return;
|
||||
}
|
||||
}
|
||||
extruderSelector.currentIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_lining").height
|
||||
|
||||
Repeater
|
||||
{
|
||||
id: contents
|
||||
height: childrenRect.height;
|
||||
|
||||
model: UM.SettingDefinitionsModel
|
||||
{
|
||||
id: addedSettingsModel;
|
||||
containerId: Cura.MachineManager.activeDefinitionId
|
||||
expanded: [ "*" ]
|
||||
|
||||
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
|
||||
{
|
||||
selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId")
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Row
|
||||
{
|
||||
Loader
|
||||
{
|
||||
id: settingLoader
|
||||
width: UM.Theme.getSize("setting").width
|
||||
height: UM.Theme.getSize("section").height
|
||||
|
||||
property var definition: model
|
||||
property var settingDefinitionsModel: addedSettingsModel
|
||||
property var propertyProvider: provider
|
||||
|
||||
//Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989
|
||||
//In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes,
|
||||
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
|
||||
asynchronous: model.type != "enum" && model.type != "extruder"
|
||||
|
||||
onLoaded: {
|
||||
settingLoader.item.showRevertButton = false
|
||||
settingLoader.item.showInheritButton = false
|
||||
settingLoader.item.showLinkedSettingIcon = false
|
||||
settingLoader.item.doDepthIndentation = false
|
||||
settingLoader.item.doQualityUserSettingEmphasis = false
|
||||
}
|
||||
|
||||
sourceComponent:
|
||||
{
|
||||
switch(model.type)
|
||||
{
|
||||
case "int":
|
||||
return settingTextField
|
||||
case "float":
|
||||
return settingTextField
|
||||
case "enum":
|
||||
return settingComboBox
|
||||
case "extruder":
|
||||
return settingExtruder
|
||||
case "bool":
|
||||
return settingCheckBox
|
||||
case "str":
|
||||
return settingTextField
|
||||
case "category":
|
||||
return settingCategory
|
||||
default:
|
||||
return settingUnknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
width: UM.Theme.getSize("setting").height / 2;
|
||||
height: UM.Theme.getSize("setting").height;
|
||||
|
||||
onClicked: addedSettingsModel.setVisible(model.key, false);
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Item
|
||||
{
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: parent.height / 2
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
|
||||
source: UM.Theme.getIcon("minus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
id: provider
|
||||
|
||||
containerStackId: UM.ActiveTool.properties.getValue("ContainerID")
|
||||
key: model.key
|
||||
watchedProperties: [ "value", "enabled", "validationState" ]
|
||||
storeIndex: 0
|
||||
removeUnusedValue: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: customise_settings_button;
|
||||
height: UM.Theme.getSize("setting").height;
|
||||
visible: parseInt(UM.Preferences.getValue("cura/active_mode")) == 1
|
||||
|
||||
text: catalog.i18nc("@action:button", "Select settings");
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Rectangle
|
||||
{
|
||||
width: control.width;
|
||||
height: control.height;
|
||||
border.width: UM.Theme.getSize("default_lining").width;
|
||||
border.color: control.pressed ? UM.Theme.getColor("action_button_active_border") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
|
||||
color: control.pressed ? UM.Theme.getColor("action_button_active") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
|
||||
}
|
||||
label: Label
|
||||
{
|
||||
text: control.text;
|
||||
color: UM.Theme.getColor("setting_control_text");
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: settingPickDialog.visible = true;
|
||||
|
||||
Connections
|
||||
{
|
||||
target: UM.Preferences;
|
||||
|
||||
onPreferenceChanged:
|
||||
{
|
||||
customise_settings_button.visible = parseInt(UM.Preferences.getValue("cura/active_mode"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UM.Dialog {
|
||||
id: settingPickDialog
|
||||
|
||||
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
|
||||
width: screenScaleFactor * 360;
|
||||
|
||||
property string labelFilter: ""
|
||||
|
||||
onVisibilityChanged:
|
||||
{
|
||||
// force updating the model to sync it with addedSettingsModel
|
||||
if(visible)
|
||||
{
|
||||
listview.model.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: filter
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: toggleShowAll.left
|
||||
rightMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
placeholderText: catalog.i18nc("@label:textbox", "Filter...");
|
||||
|
||||
onTextChanged:
|
||||
{
|
||||
if(text != "")
|
||||
{
|
||||
listview.model.filter = {"settable_per_mesh": true, "label": "*" + text}
|
||||
}
|
||||
else
|
||||
{
|
||||
listview.model.filter = {"settable_per_mesh": true}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox
|
||||
{
|
||||
id: toggleShowAll
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
text: catalog.i18nc("@label:checkbox", "Show all")
|
||||
checked: listview.model.showAll
|
||||
onClicked:
|
||||
{
|
||||
listview.model.showAll = checked;
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView
|
||||
{
|
||||
id: scrollView
|
||||
|
||||
anchors
|
||||
{
|
||||
top: filter.bottom;
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
bottom: parent.bottom;
|
||||
}
|
||||
ListView
|
||||
{
|
||||
id:listview
|
||||
model: UM.SettingDefinitionsModel
|
||||
{
|
||||
id: definitionsModel;
|
||||
containerId: Cura.MachineManager.activeDefinitionId
|
||||
filter:
|
||||
{
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||
expanded: [ "*" ]
|
||||
exclude: [ "machine_settings" ]
|
||||
}
|
||||
delegate:Loader
|
||||
{
|
||||
id: loader
|
||||
|
||||
width: parent.width
|
||||
height: model.type != undefined ? UM.Theme.getSize("section").height : 0;
|
||||
|
||||
property var definition: model
|
||||
property var settingDefinitionsModel: definitionsModel
|
||||
|
||||
asynchronous: true
|
||||
source:
|
||||
{
|
||||
switch(model.type)
|
||||
{
|
||||
case "category":
|
||||
return "PerObjectCategory.qml"
|
||||
default:
|
||||
return "PerObjectItem.qml"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
Button {
|
||||
text: catalog.i18nc("@action:button", "Close");
|
||||
onClicked: {
|
||||
settingPickDialog.visible = false;
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
SystemPalette { id: palette; }
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingTextField;
|
||||
|
||||
Cura.SettingTextField { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingComboBox;
|
||||
|
||||
Cura.SettingComboBox { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingExtruder;
|
||||
|
||||
Cura.SettingExtruder { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingCheckBox;
|
||||
|
||||
Cura.SettingCheckBox { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingCategory;
|
||||
|
||||
Cura.SettingCategory { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingUnknown;
|
||||
|
||||
Cura.SettingUnknown { }
|
||||
}
|
||||
}
|
||||
82
plugins/PerObjectSettingsTool/PerObjectSettingsTool.py
Normal file
82
plugins/PerObjectSettingsTool/PerObjectSettingsTool.py
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Tool import Tool
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## This tool allows the user to add & change settings per node in the scene.
|
||||
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
class PerObjectSettingsTool(Tool):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._model = None
|
||||
|
||||
self.setExposedProperties("SelectedObjectId", "ContainerID", "SelectedActiveExtruder")
|
||||
|
||||
self._advanced_mode = False
|
||||
self._multi_extrusion = False
|
||||
|
||||
Selection.selectionChanged.connect(self.propertyChanged)
|
||||
|
||||
Preferences.getInstance().preferenceChanged.connect(self._onPreferenceChanged)
|
||||
self._onPreferenceChanged("cura/active_mode")
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
def event(self, event):
|
||||
return False
|
||||
|
||||
def getSelectedObjectId(self):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
selected_object_id = id(selected_object)
|
||||
return selected_object_id
|
||||
|
||||
def getContainerID(self):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
try:
|
||||
return selected_object.callDecoration("getStack").getId()
|
||||
except AttributeError:
|
||||
return ""
|
||||
|
||||
## Gets the active extruder of the currently selected object.
|
||||
#
|
||||
# \return The active extruder of the currently selected object.
|
||||
def getSelectedActiveExtruder(self):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
return selected_object.callDecoration("getActiveExtruder")
|
||||
|
||||
## Changes the active extruder of the currently selected object.
|
||||
#
|
||||
# \param extruder_stack_id The ID of the extruder to print the currently
|
||||
# selected object with.
|
||||
def setSelectedActiveExtruder(self, extruder_stack_id):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
selected_object.callDecoration("setActiveExtruder", extruder_stack_id)
|
||||
|
||||
def _onPreferenceChanged(self, preference):
|
||||
if preference == "cura/active_mode":
|
||||
self._advanced_mode = Preferences.getInstance().getValue(preference) == 1
|
||||
self._updateEnabled()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
if not self._multi_extrusion:
|
||||
# Ensure that all extruder data is reset
|
||||
root_node = Application.getInstance().getController().getScene().getRoot()
|
||||
for node in DepthFirstIterator(root_node):
|
||||
node.callDecoration("setActiveExtruder", global_container_stack.getId())
|
||||
self._updateEnabled()
|
||||
|
||||
def _updateEnabled(self):
|
||||
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode or self._multi_extrusion)
|
||||
32
plugins/PerObjectSettingsTool/__init__.py
Normal file
32
plugins/PerObjectSettingsTool/__init__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import PerObjectSettingsTool
|
||||
from . import PerObjectSettingVisibilityHandler
|
||||
from PyQt5.QtQml import qmlRegisterType
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "Per Model Settings Tool"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides the Per Model Settings."),
|
||||
"api": 3
|
||||
},
|
||||
"tool": {
|
||||
"name": i18n_catalog.i18nc("@label", "Per Model Settings"),
|
||||
"description": i18n_catalog.i18nc("@info:tooltip", "Configure Per Model Settings"),
|
||||
"icon": "setting_per_object",
|
||||
"tool_panel": "PerObjectSettingsPanel.qml",
|
||||
"weight": 3
|
||||
},
|
||||
}
|
||||
|
||||
def register(app):
|
||||
qmlRegisterType(PerObjectSettingVisibilityHandler.PerObjectSettingVisibilityHandler, "Cura", 1, 0,
|
||||
"PerObjectSettingVisibilityHandler")
|
||||
return { "tool": PerObjectSettingsTool.PerObjectSettingsTool() }
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2013 David Braam
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import RemovableDrivePlugin
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
## Support for removable devices on Linux.
|
||||
#
|
||||
# TODO: This code uses the most basic interfaces for handling this.
|
||||
# We should instead use UDisks2 to handle mount/unmount and hotplugging events.
|
||||
#
|
||||
class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
for volume in glob.glob("/media/*"):
|
||||
if os.path.ismount(volume):
|
||||
drives[volume] = os.path.basename(volume)
|
||||
elif volume == "/media/"+os.getenv("USER"):
|
||||
for volume in glob.glob("/media/"+os.getenv("USER")+"/*"):
|
||||
if os.path.ismount(volume):
|
||||
drives[volume] = os.path.basename(volume)
|
||||
|
||||
for volume in glob.glob("/run/media/" + os.getenv("USER") + "/*"):
|
||||
if os.path.ismount(volume):
|
||||
drives[volume] = os.path.basename(volume)
|
||||
|
||||
return drives
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
p = subprocess.Popen(["umount", device.getId()], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
output = p.communicate()
|
||||
Logger.log("d", "umount returned: %s.", repr(output))
|
||||
|
||||
return_code = p.wait()
|
||||
if return_code != 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2013 David Braam
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import RemovableDrivePlugin
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
import plistlib
|
||||
|
||||
## Support for removable devices on Mac OSX
|
||||
class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE)
|
||||
plist = plistlib.loads(p.communicate()[0])
|
||||
p.wait()
|
||||
|
||||
for entry in plist:
|
||||
if "_items" in entry:
|
||||
for item in entry["_items"]:
|
||||
for dev in item["_items"]:
|
||||
if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0:
|
||||
for vol in dev["volumes"]:
|
||||
if "mount_point" in vol:
|
||||
volume = vol["mount_point"]
|
||||
drives[volume] = os.path.basename(volume)
|
||||
|
||||
p = subprocess.Popen(["system_profiler", "SPCardReaderDataType", "-xml"], stdout=subprocess.PIPE)
|
||||
plist = plistlib.loads(p.communicate()[0])
|
||||
p.wait()
|
||||
|
||||
for entry in plist:
|
||||
if "_items" in entry:
|
||||
for item in entry["_items"]:
|
||||
for dev in item["_items"]:
|
||||
if "removable_media" in dev and dev["removable_media"] == "yes" and "volumes" in dev and len(dev["volumes"]) > 0:
|
||||
for vol in dev["volumes"]:
|
||||
if "mount_point" in vol:
|
||||
volume = vol["mount_point"]
|
||||
drives[volume] = os.path.basename(volume)
|
||||
|
||||
return drives
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
p = subprocess.Popen(["diskutil", "eject", device.getId()], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
|
||||
output = p.communicate()
|
||||
Logger.log("d", "umount returned: %s.", repr(output))
|
||||
|
||||
return_code = p.wait()
|
||||
if return_code != 0:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
116
plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py
Normal file
116
plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import os.path
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Mesh.WriteMeshJob import WriteMeshJob
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from UM.OutputDevice import OutputDeviceError
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class RemovableDriveOutputDevice(OutputDevice):
|
||||
def __init__(self, device_id, device_name):
|
||||
super().__init__(device_id)
|
||||
|
||||
self.setName(device_name)
|
||||
self.setShortDescription(catalog.i18nc("@action:button", "Save to Removable Drive"))
|
||||
self.setDescription(catalog.i18nc("@item:inlistbox", "Save to Removable Drive {0}").format(device_name))
|
||||
self.setIconName("save_sd")
|
||||
self.setPriority(1)
|
||||
|
||||
self._writing = False
|
||||
|
||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
||||
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
|
||||
if self._writing:
|
||||
raise OutputDeviceError.DeviceBusyError()
|
||||
|
||||
# Formats supported by this application (File types that we can actually write)
|
||||
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
|
||||
if filter_by_machine:
|
||||
container = Application.getInstance().getGlobalContainerStack().findContainer({"file_formats": "*"})
|
||||
|
||||
# Create a list from supported file formats string
|
||||
machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
|
||||
|
||||
# Take the intersection between file_formats and machine_file_formats.
|
||||
file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
|
||||
|
||||
if len(file_formats) == 0:
|
||||
Logger.log("e", "There are no file formats available to write with!")
|
||||
raise OutputDeviceError.WriteRequestFailedError()
|
||||
|
||||
# Just take the first file format available.
|
||||
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
|
||||
extension = file_formats[0]["extension"]
|
||||
|
||||
if file_name is None:
|
||||
for n in BreadthFirstIterator(node):
|
||||
if n.getMeshData():
|
||||
file_name = n.getName()
|
||||
if file_name:
|
||||
break
|
||||
|
||||
if not file_name:
|
||||
Logger.log("e", "Could not determine a proper file name when trying to write to %s, aborting", self.getName())
|
||||
raise OutputDeviceError.WriteRequestFailedError()
|
||||
|
||||
if extension: # Not empty string.
|
||||
extension = "." + extension
|
||||
file_name = os.path.join(self.getId(), os.path.splitext(file_name)[0] + extension)
|
||||
|
||||
try:
|
||||
Logger.log("d", "Writing to %s", file_name)
|
||||
stream = open(file_name, "wt")
|
||||
job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode)
|
||||
job.setFileName(file_name)
|
||||
job.progress.connect(self._onProgress)
|
||||
job.finished.connect(self._onFinished)
|
||||
|
||||
message = Message(catalog.i18nc("@info:progress", "Saving to Removable Drive <filename>{0}</filename>").format(self.getName()), 0, False, -1)
|
||||
message.show()
|
||||
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
job._message = message
|
||||
self._writing = True
|
||||
job.start()
|
||||
except PermissionError as e:
|
||||
Logger.log("e", "Permission denied when trying to write to %s: %s", file_name, str(e))
|
||||
raise OutputDeviceError.PermissionDeniedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
|
||||
except OSError as e:
|
||||
Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e))
|
||||
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "Could not save to <filename>{0}</filename>: <message>{1}</message>").format(file_name, str(e))) from e
|
||||
|
||||
def _onProgress(self, job, progress):
|
||||
if hasattr(job, "_message"):
|
||||
job._message.setProgress(progress)
|
||||
self.writeProgress.emit(self, progress)
|
||||
|
||||
def _onFinished(self, job):
|
||||
if hasattr(job, "_message"):
|
||||
job._message.hide()
|
||||
job._message = None
|
||||
|
||||
self._writing = False
|
||||
self.writeFinished.emit(self)
|
||||
if job.getResult():
|
||||
message = Message(catalog.i18nc("@info:status", "Saved to Removable Drive {0} as {1}").format(self.getName(), os.path.basename(job.getFileName())))
|
||||
message.addAction("eject", catalog.i18nc("@action:button", "Eject"), "eject", catalog.i18nc("@action", "Eject removable device {0}").format(self.getName()))
|
||||
message.actionTriggered.connect(self._onActionTriggered)
|
||||
message.show()
|
||||
self.writeSuccess.emit(self)
|
||||
else:
|
||||
message = Message(catalog.i18nc("@info:status", "Could not save to removable drive {0}: {1}").format(self.getName(), str(job.getError())))
|
||||
message.show()
|
||||
self.writeError.emit(self)
|
||||
job.getStream().close()
|
||||
|
||||
def _onActionTriggered(self, message, action):
|
||||
if action == "eject":
|
||||
Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self)
|
||||
|
||||
80
plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
Normal file
80
plugins/RemovableDriveOutputDevice/RemovableDrivePlugin.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from UM.Message import Message
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from UM.Logger import Logger
|
||||
|
||||
from . import RemovableDriveOutputDevice
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class RemovableDrivePlugin(OutputDevicePlugin):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._update_thread = threading.Thread(target = self._updateThread)
|
||||
self._update_thread.setDaemon(True)
|
||||
|
||||
self._check_updates = True
|
||||
|
||||
self._drives = {}
|
||||
|
||||
def start(self):
|
||||
self._update_thread.start()
|
||||
|
||||
def stop(self):
|
||||
self._check_updates = False
|
||||
self._update_thread.join()
|
||||
|
||||
self._addRemoveDrives({})
|
||||
|
||||
def checkRemovableDrives(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def ejectDevice(self, device):
|
||||
try:
|
||||
Logger.log("i", "Attempting to eject the device")
|
||||
result = self.performEjectDevice(device)
|
||||
except Exception as e:
|
||||
Logger.log("e", "Ejection failed due to: %s" % str(e))
|
||||
result = False
|
||||
|
||||
if result:
|
||||
Logger.log("i", "Succesfully ejected the device")
|
||||
message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(device.getName()))
|
||||
message.show()
|
||||
else:
|
||||
message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Maybe it is still in use?").format(device.getName()))
|
||||
message.show()
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _updateThread(self):
|
||||
while self._check_updates:
|
||||
result = self.checkRemovableDrives()
|
||||
self._addRemoveDrives(result)
|
||||
time.sleep(5)
|
||||
|
||||
def _addRemoveDrives(self, drives):
|
||||
# First, find and add all new or changed keys
|
||||
for key, value in drives.items():
|
||||
if key not in self._drives:
|
||||
self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value))
|
||||
continue
|
||||
|
||||
if self._drives[key] != value:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
self.getOutputDeviceManager().addOutputDevice(RemovableDriveOutputDevice.RemovableDriveOutputDevice(key, value))
|
||||
|
||||
# Then check for keys that have been removed
|
||||
for key in self._drives.keys():
|
||||
if key not in drives:
|
||||
self.getOutputDeviceManager().removeOutputDevice(key)
|
||||
|
||||
self._drives = drives
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2013 David Braam
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from . import RemovableDrivePlugin
|
||||
|
||||
import string
|
||||
import ctypes
|
||||
from ctypes import wintypes # Using ctypes.wintypes in the code below does not seem to work
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
# WinAPI Constants that we need
|
||||
# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values.
|
||||
DRIVE_REMOVABLE = 2 # [CodeStyle: Windows Enum value]
|
||||
|
||||
GENERIC_READ = 2147483648 # [CodeStyle: Windows Enum value]
|
||||
GENERIC_WRITE = 1073741824 # [CodeStyle: Windows Enum value]
|
||||
|
||||
FILE_SHARE_READ = 1 # [CodeStyle: Windows Enum value]
|
||||
FILE_SHARE_WRITE = 2 # [CodeStyle: Windows Enum value]
|
||||
|
||||
IOCTL_STORAGE_EJECT_MEDIA = 2967560 # [CodeStyle: Windows Enum value]
|
||||
|
||||
OPEN_EXISTING = 3 # [CodeStyle: Windows Enum value]
|
||||
|
||||
# Setup the DeviceIoControl function arguments and return type.
|
||||
# See ctypes documentation for details on how to call C functions from python, and why this is important.
|
||||
ctypes.windll.kernel32.DeviceIoControl.argtypes = [
|
||||
wintypes.HANDLE, # _In_ HANDLE hDevice
|
||||
wintypes.DWORD, # _In_ DWORD dwIoControlCode
|
||||
wintypes.LPVOID, # _In_opt_ LPVOID lpInBuffer
|
||||
wintypes.DWORD, # _In_ DWORD nInBufferSize
|
||||
wintypes.LPVOID, # _Out_opt_ LPVOID lpOutBuffer
|
||||
wintypes.DWORD, # _In_ DWORD nOutBufferSize
|
||||
ctypes.POINTER(wintypes.DWORD), # _Out_opt_ LPDWORD lpBytesReturned
|
||||
wintypes.LPVOID # _Inout_opt_ LPOVERLAPPED lpOverlapped
|
||||
]
|
||||
ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL
|
||||
|
||||
|
||||
## Removable drive support for windows
|
||||
class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
||||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
|
||||
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
|
||||
# Check possible drive letters, from A to Z
|
||||
# Note: using ascii_uppercase because we do not want this to change with locale!
|
||||
for letter in string.ascii_uppercase:
|
||||
drive = "{0}:/".format(letter)
|
||||
|
||||
# Do we really want to skip A and B?
|
||||
# GetDriveTypeA explicitly wants a byte array of type ascii. It will accept a string, but this wont work
|
||||
if bitmask & 1 and ctypes.windll.kernel32.GetDriveTypeA(drive.encode("ascii")) == DRIVE_REMOVABLE:
|
||||
volume_name = ""
|
||||
name_buffer = ctypes.create_unicode_buffer(1024)
|
||||
filesystem_buffer = ctypes.create_unicode_buffer(1024)
|
||||
error = ctypes.windll.kernel32.GetVolumeInformationW(ctypes.c_wchar_p(drive), name_buffer, ctypes.sizeof(name_buffer), None, None, None, filesystem_buffer, ctypes.sizeof(filesystem_buffer))
|
||||
|
||||
if error != 0:
|
||||
volume_name = name_buffer.value
|
||||
|
||||
if not volume_name:
|
||||
volume_name = catalog.i18nc("@item:intext", "Removable Drive")
|
||||
|
||||
# Certain readers will report themselves as a volume even when there is no card inserted, but will show an
|
||||
# "No volume in drive" warning when trying to call GetDiskFreeSpace. However, they will not report a valid
|
||||
# filesystem, so we can filter on that. In addition, this excludes other things with filesystems Windows
|
||||
# does not support.
|
||||
if filesystem_buffer.value == "":
|
||||
continue
|
||||
|
||||
# Check for the free space. Some card readers show up as a drive with 0 space free when there is no card inserted.
|
||||
free_bytes = ctypes.c_longlong(0)
|
||||
if ctypes.windll.kernel32.GetDiskFreeSpaceExA(drive.encode("ascii"), ctypes.byref(free_bytes), None, None) == 0:
|
||||
continue
|
||||
|
||||
if free_bytes.value < 1:
|
||||
continue
|
||||
|
||||
drives[drive] = "{0} ({1}:)".format(volume_name, letter)
|
||||
bitmask >>= 1
|
||||
|
||||
return drives
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
# Magic WinAPI stuff
|
||||
# First, open a handle to the Device
|
||||
handle = ctypes.windll.kernel32.CreateFileA("\\\\.\\{0}".format(device.getId()[:-1]).encode("ascii"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, 0, None )
|
||||
|
||||
if handle == -1:
|
||||
# ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
|
||||
# So we use this to raise the error to our caller.
|
||||
raise ctypes.WinError()
|
||||
|
||||
# The DeviceIoControl requires a bytes_returned pointer to be a valid pointer.
|
||||
# So create a ctypes DWORD to reference. (Without this pointer the DeviceIoControl function will crash with an access violation after doing its job.
|
||||
bytes_returned = wintypes.DWORD(0)
|
||||
|
||||
error = None
|
||||
|
||||
# Then, try and tell it to eject
|
||||
return_code = ctypes.windll.kernel32.DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, None, 0, None, 0, ctypes.pointer(bytes_returned), None)
|
||||
# DeviceIoControl with IOCTL_STORAGE_EJECT_MEDIA return 0 on error.
|
||||
if return_code == 0:
|
||||
# ctypes.WinError sets up an GetLastError API call for windows as an Python OSError exception.
|
||||
# So we use this to raise the error to our caller.
|
||||
error = ctypes.WinError()
|
||||
# Do not raise an error here yet, so we can properly close the handle.
|
||||
|
||||
# Finally, close the handle
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
|
||||
# If an error happened in the DeviceIoControl, raise it now.
|
||||
if error:
|
||||
raise error
|
||||
|
||||
# Return success
|
||||
return True
|
||||
32
plugins/RemovableDriveOutputDevice/__init__.py
Normal file
32
plugins/RemovableDriveOutputDevice/__init__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import platform
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Removable Drive Output Device Plugin"),
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides removable drive hotplugging and writing support."),
|
||||
"version": "1.0",
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
if platform.system() == "Windows":
|
||||
from . import WindowsRemovableDrivePlugin
|
||||
return { "output_device": WindowsRemovableDrivePlugin.WindowsRemovableDrivePlugin() }
|
||||
elif platform.system() == "Darwin":
|
||||
from . import OSXRemovableDrivePlugin
|
||||
return { "output_device": OSXRemovableDrivePlugin.OSXRemovableDrivePlugin() }
|
||||
elif platform.system() == "Linux":
|
||||
from . import LinuxRemovableDrivePlugin
|
||||
return { "output_device": LinuxRemovableDrivePlugin.LinuxRemovableDrivePlugin() }
|
||||
else:
|
||||
Logger.log("e", "Unsupported system %s, no removable device hotplugging support available.", platform.system())
|
||||
return { }
|
||||
148
plugins/SliceInfoPlugin/SliceInfo.py
Normal file
148
plugins/SliceInfoPlugin/SliceInfo.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Platform import Platform
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
from UM.Job import Job
|
||||
|
||||
import platform
|
||||
import math
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import ssl
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class SliceInfoJob(Job):
|
||||
data = None
|
||||
url = None
|
||||
|
||||
def __init__(self, url, data):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.data = data
|
||||
|
||||
def run(self):
|
||||
if not self.url or not self.data:
|
||||
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
||||
return
|
||||
|
||||
# Submit data
|
||||
kwoptions = {"data" : self.data,
|
||||
"timeout" : 5
|
||||
}
|
||||
|
||||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
|
||||
try:
|
||||
f = urllib.request.urlopen(self.url, **kwoptions)
|
||||
Logger.log("i", "Sent anonymous slice info to %s", self.url)
|
||||
f.close()
|
||||
except urllib.error.HTTPError as http_exception:
|
||||
Logger.log("e", "An HTTP error occurred while trying to send slice information: %s" % http_exception)
|
||||
except Exception as e: # We don't want any exception to cause problems
|
||||
Logger.log("e", "An exception occurred while trying to send slice information: %s" % e)
|
||||
|
||||
## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
|
||||
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
|
||||
# no model files are being sent (Just a SHA256 hash of the model).
|
||||
class SliceInfo(Extension):
|
||||
info_url = "https://stats.youmagine.com/curastats/slice"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
|
||||
Preferences.getInstance().addPreference("info/send_slice_info", True)
|
||||
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
|
||||
|
||||
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
|
||||
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura automatically sends slice info. You can disable this in preferences"), lifetime = 0, dismissable = False)
|
||||
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
|
||||
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
|
||||
self.send_slice_info_message.show()
|
||||
|
||||
def messageActionTriggered(self, message_id, action_id):
|
||||
self.send_slice_info_message.hide()
|
||||
Preferences.getInstance().setValue("info/asked_send_slice_info", True)
|
||||
|
||||
def _onWriteStarted(self, output_device):
|
||||
try:
|
||||
if not Preferences.getInstance().getValue("info/send_slice_info"):
|
||||
Logger.log("d", "'info/send_slice_info' is turned off.")
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
# Get total material used (in mm^3)
|
||||
print_information = Application.getInstance().getPrintInformation()
|
||||
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
|
||||
|
||||
# TODO: Send material per extruder instead of mashing it on a pile
|
||||
material_used = math.pi * material_radius * material_radius * sum(print_information.materialLengths) #Volume of all materials used
|
||||
|
||||
# Get model information (bounding boxes, hashes and transformation matrix)
|
||||
models_info = []
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||
if not getattr(node, "_outside_buildarea", False):
|
||||
model_info = {}
|
||||
model_info["hash"] = node.getMeshData().getHash()
|
||||
model_info["bounding_box"] = {}
|
||||
model_info["bounding_box"]["minimum"] = {}
|
||||
model_info["bounding_box"]["minimum"]["x"] = node.getBoundingBox().minimum.x
|
||||
model_info["bounding_box"]["minimum"]["y"] = node.getBoundingBox().minimum.y
|
||||
model_info["bounding_box"]["minimum"]["z"] = node.getBoundingBox().minimum.z
|
||||
|
||||
model_info["bounding_box"]["maximum"] = {}
|
||||
model_info["bounding_box"]["maximum"]["x"] = node.getBoundingBox().maximum.x
|
||||
model_info["bounding_box"]["maximum"]["y"] = node.getBoundingBox().maximum.y
|
||||
model_info["bounding_box"]["maximum"]["z"] = node.getBoundingBox().maximum.z
|
||||
model_info["transformation"] = str(node.getWorldTransformation().getData())
|
||||
|
||||
models_info.append(model_info)
|
||||
|
||||
# Bundle the collected data
|
||||
submitted_data = {
|
||||
"processor": platform.processor(),
|
||||
"machine": platform.machine(),
|
||||
"platform": platform.platform(),
|
||||
"settings": global_container_stack.serialize(), # global_container with references on used containers
|
||||
"version": Application.getInstance().getVersion(),
|
||||
"modelhash": "None",
|
||||
"printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
|
||||
"filament": material_used,
|
||||
"language": Preferences.getInstance().getValue("general/language"),
|
||||
}
|
||||
for container in global_container_stack.getContainers():
|
||||
container_id = container.getId()
|
||||
try:
|
||||
container_serialized = container.serialize()
|
||||
except NotImplementedError:
|
||||
Logger.log("w", "Container %s could not be serialized!", container_id)
|
||||
continue
|
||||
|
||||
if container_serialized:
|
||||
submitted_data["settings_%s" %(container_id)] = container_serialized # This can be anything, eg. INI, JSON, etc.
|
||||
else:
|
||||
Logger.log("i", "No data found in %s to be serialized!", container_id)
|
||||
|
||||
# Convert data to bytes
|
||||
submitted_data = urllib.parse.urlencode(submitted_data)
|
||||
binary_data = submitted_data.encode("utf-8")
|
||||
|
||||
# Sending slice info non-blocking
|
||||
reportJob = SliceInfoJob(self.info_url, binary_data)
|
||||
reportJob.start()
|
||||
except Exception as e:
|
||||
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
|
||||
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
|
||||
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.
|
||||
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