mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'master' into python_type_hinting
This commit is contained in:
commit
98a6568313
416 changed files with 160213 additions and 173190 deletions
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -8,6 +8,8 @@ docs/html
|
|||
resources/i18n/en
|
||||
resources/i18n/x-test
|
||||
resources/firmware
|
||||
resources/materials
|
||||
LC_MESSAGES
|
||||
|
||||
# Editors and IDEs.
|
||||
*kdev*
|
||||
|
@ -24,3 +26,10 @@ resources/firmware
|
|||
|
||||
# Debian packaging
|
||||
debian*
|
||||
|
||||
#Externally located plug-ins.
|
||||
plugins/Doodle3D-cura-plugin
|
||||
plugins/GodMode
|
||||
plugins/PostProcessingPlugin
|
||||
plugins/UM3NetworkPrinting
|
||||
plugins/X3GWriter
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
project(cura)
|
||||
project(cura NONE)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
@ -17,48 +17,12 @@ 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 "")
|
||||
include(UraniumTranslationTools)
|
||||
# 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()
|
||||
CREATE_TRANSLATION_TARGETS()
|
||||
endif()
|
||||
|
||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||
|
@ -79,6 +43,8 @@ if(NOT APPLE AND NOT WIN32)
|
|||
DESTINATION lib/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
install(FILES cura.appdata.xml
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/appdata)
|
||||
install(FILES cura.sharedmimeinfo
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/mime/packages/
|
||||
RENAME cura.xml )
|
||||
|
|
|
@ -42,9 +42,11 @@ 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.
|
||||
* [Print Cost Calculator](https://github.com/nallath/PrintCostCalculator): Calculates weight and monetary cost of your print.
|
||||
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code.
|
||||
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
|
||||
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
|
||||
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
|
||||
|
||||
Making profiles for other printers
|
||||
----------------------------------
|
||||
|
|
31
cura.appdata.xml
Normal file
31
cura.appdata.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
|
||||
<component type="desktop">
|
||||
<id>cura.desktop</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>AGPL-3.0 and CC-BY-SA-4.0</project_license>
|
||||
<name>Cura</name>
|
||||
<summary>The world's most advanced 3d printer software</summary>
|
||||
<description>
|
||||
<p>
|
||||
Cura creates a seamless integration between hardware, software and
|
||||
materials for the best 3D printing experience around.
|
||||
Cura supports the 3MF, OBJ and STL file formats and is available on
|
||||
Windows, Mac and Linux.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Novices can start printing right away</li>
|
||||
<li>Experts are able to customize 200 settings to achieve the best results</li>
|
||||
<li>Optimized profiles for Ultimaker materials</li>
|
||||
<li>Supported by a global network of Ultimaker certified service partners</li>
|
||||
<li>Print multiple objects at once with different settings for each object</li>
|
||||
<li>Cura supports STL, 3MF and OBJ file formats</li>
|
||||
<li>Open source and completely free</li>
|
||||
</ul>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://ultimaker.com/en/products/cura-software</url>
|
||||
<translation type="gettext">Cura</translation>
|
||||
</component>
|
|
@ -5,6 +5,7 @@ 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.
|
||||
Comment[de]=Cura wandelt 3D-Modelle in Pfade für einen 3D-Drucker um. Es bereitet Ihren Druck für maximale Genauigkeit, minimale Druckzeit und guter Zuverlässigkeit mit vielen zusätzlichen Funktionen vor, damit Ihr Druck großartig wird.
|
||||
Exec=@CMAKE_INSTALL_FULL_BINDIR@/cura %F
|
||||
TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=@CMAKE_INSTALL_FULL_DATADIR@/cura/resources/images/cura-icon.png
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2016 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.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
|
@ -14,7 +15,7 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
|
|||
from UM.Math.Polygon import Polygon
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
catalog = i18nCatalog("cura")
|
||||
|
@ -22,33 +23,19 @@ catalog = i18nCatalog("cura")
|
|||
import numpy
|
||||
import copy
|
||||
|
||||
import UM.Settings.ContainerRegistry
|
||||
|
||||
|
||||
# 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)
|
||||
PRIME_CLEARANCE = 6.5
|
||||
|
||||
|
||||
## 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)
|
||||
XAxisColor = Color(255, 0, 0, 255)
|
||||
YAxisColor = Color(0, 0, 255, 255)
|
||||
ZAxisColor = Color(0, 255, 0, 255)
|
||||
|
||||
raftThicknessChanged = Signal()
|
||||
|
||||
|
@ -61,12 +48,19 @@ class BuildVolume(SceneNode):
|
|||
|
||||
self._shader = None
|
||||
|
||||
self._origin_mesh = None
|
||||
self._origin_line_length = 20
|
||||
self._origin_line_width = 0.5
|
||||
|
||||
self._grid_mesh = None
|
||||
self._grid_shader = None
|
||||
|
||||
self._disallowed_areas = []
|
||||
self._disallowed_area_mesh = None
|
||||
|
||||
self._error_areas = []
|
||||
self._error_mesh = None
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
self._volume_aabb = None
|
||||
|
||||
|
@ -75,12 +69,66 @@ class BuildVolume(SceneNode):
|
|||
self._platform = Platform(self)
|
||||
|
||||
self._global_container_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
|
||||
self._onGlobalContainerStackChanged()
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
|
||||
self._onStackChanged()
|
||||
|
||||
self._active_extruder_stack = None
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
self._onActiveExtruderStackChanged()
|
||||
self._has_errors = False
|
||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||
|
||||
#Objects loaded at the moment. We are connected to the property changed events of these objects.
|
||||
self._scene_objects = set()
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(100)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
||||
|
||||
self._build_volume_message = 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."))
|
||||
|
||||
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
||||
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
||||
# Therefore this works.
|
||||
Application.getInstance().getMachineManager().activeQualityChanged.connect(self._onStackChanged)
|
||||
# This should also ways work, and it is semantically more correct,
|
||||
# but it does not update the disallowed areas after material change
|
||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
if self._global_container_stack:
|
||||
self._change_timer.start()
|
||||
|
||||
def _onChangeTimerFinished(self):
|
||||
root = Application.getInstance().getController().getScene().getRoot()
|
||||
new_scene_objects = set(node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode)
|
||||
if new_scene_objects != self._scene_objects:
|
||||
for node in new_scene_objects - self._scene_objects: #Nodes that were added to the scene.
|
||||
node.decoratorsChanged.connect(self._onNodeDecoratorChanged)
|
||||
for node in self._scene_objects - new_scene_objects: #Nodes that were removed from the scene.
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
per_mesh_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
||||
if active_extruder_changed is not None:
|
||||
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
||||
node.decoratorsChanged.disconnect(self._onNodeDecoratorChanged)
|
||||
|
||||
self._scene_objects = new_scene_objects
|
||||
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
|
||||
|
||||
## Updates the listeners that listen for changes in per-mesh stacks.
|
||||
#
|
||||
# \param node The node for which the decorators changed.
|
||||
def _onNodeDecoratorChanged(self, node):
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
|
||||
if active_extruder_changed is not None:
|
||||
active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
|
||||
self._updateDisallowedAreasAndRebuild()
|
||||
|
||||
def setWidth(self, width):
|
||||
if width: self._width = width
|
||||
|
@ -106,10 +154,15 @@ class BuildVolume(SceneNode):
|
|||
self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
|
||||
|
||||
renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
|
||||
renderer.queueNode(self, mesh = self._origin_mesh)
|
||||
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)
|
||||
|
||||
if self._error_mesh:
|
||||
renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True,
|
||||
backface_cull=True, sort=-8)
|
||||
|
||||
return True
|
||||
|
||||
## Recalculates the build volume & disallowed areas.
|
||||
|
@ -144,6 +197,37 @@ class BuildVolume(SceneNode):
|
|||
|
||||
self.setMeshData(mb.build())
|
||||
|
||||
mb = MeshBuilder()
|
||||
|
||||
# Indication of the machine origin
|
||||
if self._global_container_stack.getProperty("machine_center_is_zero", "value"):
|
||||
origin = (Vector(min_w, min_h, min_d) + Vector(max_w, min_h, max_d)) / 2
|
||||
else:
|
||||
origin = Vector(min_w, min_h, max_d)
|
||||
|
||||
mb.addCube(
|
||||
width = self._origin_line_length,
|
||||
height = self._origin_line_width,
|
||||
depth = self._origin_line_width,
|
||||
center = origin + Vector(self._origin_line_length / 2, 0, 0),
|
||||
color = self.XAxisColor
|
||||
)
|
||||
mb.addCube(
|
||||
width = self._origin_line_width,
|
||||
height = self._origin_line_length,
|
||||
depth = self._origin_line_width,
|
||||
center = origin + Vector(0, self._origin_line_length / 2, 0),
|
||||
color = self.YAxisColor
|
||||
)
|
||||
mb.addCube(
|
||||
width = self._origin_line_width,
|
||||
height = self._origin_line_width,
|
||||
depth = self._origin_line_length,
|
||||
center = origin - Vector(0, 0, self._origin_line_length / 2),
|
||||
color = self.ZAxisColor
|
||||
)
|
||||
self._origin_mesh = mb.build()
|
||||
|
||||
mb = MeshBuilder()
|
||||
mb.addQuad(
|
||||
Vector(min_w, min_h - 0.2, min_d),
|
||||
|
@ -184,15 +268,29 @@ class BuildVolume(SceneNode):
|
|||
else:
|
||||
self._disallowed_area_mesh = None
|
||||
|
||||
if self._error_areas:
|
||||
mb = MeshBuilder()
|
||||
for error_area in self._error_areas:
|
||||
color = Color(1.0, 0.0, 0.0, 0.5)
|
||||
points = error_area.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
|
||||
self._error_mesh = mb.build()
|
||||
else:
|
||||
self._error_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)
|
||||
bed_adhesion_size = self._getEdgeDisallowedSize()
|
||||
|
||||
# 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!
|
||||
|
@ -207,13 +305,6 @@ class BuildVolume(SceneNode):
|
|||
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.")).show()
|
||||
|
||||
def getRaftThickness(self):
|
||||
return self._raft_thickness
|
||||
|
||||
|
@ -234,23 +325,33 @@ class BuildVolume(SceneNode):
|
|||
self.setPosition(Vector(0, -self._raft_thickness, 0), SceneNode.TransformSpace.World)
|
||||
self.raftThicknessChanged.emit()
|
||||
|
||||
def _onGlobalContainerStackChanged(self):
|
||||
## Update the build volume visualization
|
||||
def _onStackChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
|
||||
self._width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
|
||||
if self._height < machine_height:
|
||||
self._buildVolumeMessage()
|
||||
self._build_volume_message.show()
|
||||
else:
|
||||
self._build_volume_message.hide()
|
||||
else:
|
||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||
self._build_volume_message.hide()
|
||||
self._depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
|
||||
self._updateDisallowedAreas()
|
||||
|
@ -258,13 +359,6 @@ class BuildVolume(SceneNode):
|
|||
|
||||
self.rebuild()
|
||||
|
||||
def _onActiveExtruderStackChanged(self):
|
||||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
self._active_extruder_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
|
||||
def _onSettingPropertyChanged(self, setting_key, property_name):
|
||||
if property_name != "value":
|
||||
return
|
||||
|
@ -272,15 +366,18 @@ class BuildVolume(SceneNode):
|
|||
rebuild_me = False
|
||||
if setting_key == "print_sequence":
|
||||
machine_height = self._global_container_stack.getProperty("machine_height", "value")
|
||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
if Application.getInstance().getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
|
||||
self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
|
||||
if self._height < machine_height:
|
||||
self._buildVolumeMessage()
|
||||
self._build_volume_message.show()
|
||||
else:
|
||||
self._build_volume_message.hide()
|
||||
else:
|
||||
self._height = self._global_container_stack.getProperty("machine_height", "value")
|
||||
self._build_volume_message.hide()
|
||||
rebuild_me = True
|
||||
|
||||
if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings:
|
||||
if setting_key in self._skirt_settings or setting_key in self._prime_settings or setting_key in self._tower_settings or setting_key == "print_sequence" or setting_key in self._ooze_shield_settings or setting_key in self._distance_settings or setting_key in self._extruder_settings:
|
||||
self._updateDisallowedAreas()
|
||||
rebuild_me = True
|
||||
|
||||
|
@ -291,120 +388,355 @@ class BuildVolume(SceneNode):
|
|||
if rebuild_me:
|
||||
self.rebuild()
|
||||
|
||||
def hasErrors(self):
|
||||
return self._has_errors
|
||||
|
||||
## Calls _updateDisallowedAreas and makes sure the changes appear in the
|
||||
# scene.
|
||||
#
|
||||
# This is required for a signal to trigger the update in one go. The
|
||||
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
|
||||
# since there may be other changes before it needs to be rebuilt, which
|
||||
# would hit performance.
|
||||
def _updateDisallowedAreasAndRebuild(self):
|
||||
self._updateDisallowedAreas()
|
||||
self.rebuild()
|
||||
|
||||
def _updateDisallowedAreas(self):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
disallowed_areas = copy.deepcopy(
|
||||
self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
|
||||
areas = []
|
||||
self._error_areas = []
|
||||
|
||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
used_extruders = extruder_manager.getUsedExtruderStacks()
|
||||
disallowed_border_size = self._getEdgeDisallowedSize()
|
||||
|
||||
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
||||
prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
|
||||
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
||||
|
||||
#Check if prime positions intersect with disallowed areas.
|
||||
for extruder in used_extruders:
|
||||
extruder_id = extruder.getId()
|
||||
|
||||
collision = False
|
||||
for prime_polygon in prime_areas[extruder_id]:
|
||||
for disallowed_polygon in prime_disallowed_areas[extruder_id]:
|
||||
if prime_polygon.intersectsPolygon(disallowed_polygon) is not None:
|
||||
collision = True
|
||||
break
|
||||
if collision:
|
||||
break
|
||||
|
||||
#Also check other prime positions (without additional offset).
|
||||
for other_extruder_id in prime_areas:
|
||||
if extruder_id == other_extruder_id: #It is allowed to collide with itself.
|
||||
continue
|
||||
for other_prime_polygon in prime_areas[other_extruder_id]:
|
||||
if prime_polygon.intersectsPolygon(other_prime_polygon):
|
||||
collision = True
|
||||
break
|
||||
if collision:
|
||||
break
|
||||
if collision:
|
||||
break
|
||||
|
||||
|
||||
if not collision:
|
||||
#Prime areas are valid. Add as normal.
|
||||
result_areas[extruder_id].extend(prime_areas[extruder_id])
|
||||
|
||||
nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value")
|
||||
for area in nozzle_disallowed_areas:
|
||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
|
||||
result_areas[extruder_id].append(polygon) #Don't perform the offset on these.
|
||||
|
||||
# Add prime tower location as disallowed area.
|
||||
if self._global_container_stack.getProperty("prime_tower_enable", "value"):
|
||||
prime_tower_collision = False
|
||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||
for extruder_id in prime_tower_areas:
|
||||
for prime_tower_area in prime_tower_areas[extruder_id]:
|
||||
for area in result_areas[extruder_id]:
|
||||
if prime_tower_area.intersectsPolygon(area) is not None:
|
||||
prime_tower_collision = True
|
||||
break
|
||||
if prime_tower_collision: #Already found a collision.
|
||||
break
|
||||
if not prime_tower_collision:
|
||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
else:
|
||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||
|
||||
self._has_errors = len(self._error_areas) > 0
|
||||
|
||||
self._disallowed_areas = []
|
||||
for extruder_id in result_areas:
|
||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||
|
||||
## Computes the disallowed areas for objects that are printed with print
|
||||
# features.
|
||||
#
|
||||
# This means that the brim, travel avoidance and such will be applied to
|
||||
# these features.
|
||||
#
|
||||
# \return A dictionary with for each used extruder ID the disallowed areas
|
||||
# where that extruder may not print.
|
||||
def _computeDisallowedAreasPrinted(self, used_extruders):
|
||||
result = {}
|
||||
for extruder in used_extruders:
|
||||
result[extruder.getId()] = []
|
||||
|
||||
#Currently, the only normally printed object is the prime tower.
|
||||
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable") == True:
|
||||
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
|
||||
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2
|
||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
prime_tower_x = self._global_container_stack.getProperty("prime_tower_position_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
|
||||
|
||||
disallowed_areas.append([
|
||||
prime_tower_area = Polygon([
|
||||
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
|
||||
[prime_tower_x, prime_tower_y - prime_tower_size],
|
||||
[prime_tower_x, prime_tower_y],
|
||||
[prime_tower_x - prime_tower_size, prime_tower_y],
|
||||
])
|
||||
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
|
||||
for extruder in used_extruders:
|
||||
result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
|
||||
|
||||
# Add extruder prime locations as disallowed areas.
|
||||
# Probably needs some rework after coordinate system change.
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
extruders = extruder_manager.getMachineExtruders(self._global_container_stack.getId())
|
||||
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],
|
||||
])
|
||||
return result
|
||||
|
||||
bed_adhesion_size = self._getBedAdhesionSize(self._global_container_stack)
|
||||
## Computes the disallowed areas for the prime locations.
|
||||
#
|
||||
# These are special because they are not subject to things like brim or
|
||||
# travel avoidance. They do get a dilute with the border size though
|
||||
# because they may not intersect with brims and such of other objects.
|
||||
#
|
||||
# \param border_size The size with which to offset the disallowed areas
|
||||
# due to skirt, brim, travel avoid distance, etc.
|
||||
# \param used_extruders The extruder stacks to generate disallowed areas
|
||||
# for.
|
||||
# \return A dictionary with for each used extruder ID the prime areas.
|
||||
def _computeDisallowedAreasPrime(self, border_size, used_extruders):
|
||||
result = {}
|
||||
|
||||
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)))
|
||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
for extruder in used_extruders:
|
||||
prime_x = extruder.getProperty("extruder_prime_pos_x", "value") - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
|
||||
prime_y = machine_depth / 2 - extruder.getProperty("extruder_prime_pos_y", "value")
|
||||
|
||||
areas.append(poly)
|
||||
prime_polygon = Polygon.approximatedCircle(PRIME_CLEARANCE)
|
||||
prime_polygon = prime_polygon.translate(prime_x, prime_y)
|
||||
prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||
result[extruder.getId()] = [prime_polygon]
|
||||
|
||||
# Add the skirt areas around the borders of the build plate.
|
||||
if bed_adhesion_size > 0:
|
||||
return result
|
||||
|
||||
## Computes the disallowed areas that are statically placed in the machine.
|
||||
#
|
||||
# It computes different disallowed areas depending on the offset of the
|
||||
# extruder. The resulting dictionary will therefore have an entry for each
|
||||
# extruder that is used.
|
||||
#
|
||||
# \param border_size The size with which to offset the disallowed areas
|
||||
# due to skirt, brim, travel avoid distance, etc.
|
||||
# \param used_extruders The extruder stacks to generate disallowed areas
|
||||
# for.
|
||||
# \return A dictionary with for each used extruder ID the disallowed areas
|
||||
# where that extruder may not print.
|
||||
def _computeDisallowedAreasStatic(self, border_size, used_extruders):
|
||||
#Convert disallowed areas to polygons and dilate them.
|
||||
machine_disallowed_polygons = []
|
||||
for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
|
||||
polygon = Polygon(numpy.array(area, numpy.float32))
|
||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||
machine_disallowed_polygons.append(polygon)
|
||||
|
||||
result = {}
|
||||
for extruder in used_extruders:
|
||||
extruder_id = extruder.getId()
|
||||
offset_x = extruder.getProperty("machine_nozzle_offset_x", "value")
|
||||
if offset_x is None:
|
||||
offset_x = 0
|
||||
offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||
if offset_y is None:
|
||||
offset_y = 0
|
||||
result[extruder_id] = []
|
||||
|
||||
for polygon in machine_disallowed_polygons:
|
||||
result[extruder_id].append(polygon.translate(offset_x, offset_y)) #Compensate for the nozzle offset of this extruder.
|
||||
|
||||
#Add the border around the edge of the build volume.
|
||||
left_unreachable_border = 0
|
||||
right_unreachable_border = 0
|
||||
top_unreachable_border = 0
|
||||
bottom_unreachable_border = 0
|
||||
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
|
||||
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
|
||||
other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||
left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
|
||||
right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
|
||||
top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
|
||||
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
|
||||
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
||||
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
||||
if border_size - left_unreachable_border > 0:
|
||||
result[extruder_id].append(Polygon(numpy.array([
|
||||
[-half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width, half_machine_depth],
|
||||
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
|
||||
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
|
||||
], numpy.float32)))
|
||||
if border_size + right_unreachable_border > 0:
|
||||
result[extruder_id].append(Polygon(numpy.array([
|
||||
[half_machine_width, half_machine_depth],
|
||||
[half_machine_width, -half_machine_depth],
|
||||
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
|
||||
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
|
||||
], numpy.float32)))
|
||||
if border_size + bottom_unreachable_border > 0:
|
||||
result[extruder_id].append(Polygon(numpy.array([
|
||||
[-half_machine_width, half_machine_depth],
|
||||
[half_machine_width, half_machine_depth],
|
||||
[half_machine_width - border_size - right_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border],
|
||||
[-half_machine_width + border_size - left_unreachable_border, half_machine_depth - border_size - bottom_unreachable_border]
|
||||
], numpy.float32)))
|
||||
if border_size - top_unreachable_border > 0:
|
||||
result[extruder_id].append(Polygon(numpy.array([
|
||||
[half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width, -half_machine_depth],
|
||||
[-half_machine_width + border_size - left_unreachable_border, -half_machine_depth + border_size - top_unreachable_border],
|
||||
[half_machine_width - border_size - right_unreachable_border, -half_machine_depth + border_size - top_unreachable_border]
|
||||
], 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)))
|
||||
return result
|
||||
|
||||
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)))
|
||||
## Private convenience function to get a setting from the adhesion
|
||||
# extruder.
|
||||
#
|
||||
# \param setting_key The key of the setting to get.
|
||||
# \param property The property to get from the setting.
|
||||
# \return The property of the specified setting in the adhesion extruder.
|
||||
def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"):
|
||||
return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property)
|
||||
|
||||
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)))
|
||||
## Private convenience function to get a setting from every extruder.
|
||||
#
|
||||
# For single extrusion machines, this gets the setting from the global
|
||||
# stack.
|
||||
#
|
||||
# \return A sequence of setting values, one for each extruder.
|
||||
def _getSettingFromAllExtruders(self, setting_key, property = "value"):
|
||||
return ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)
|
||||
|
||||
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)))
|
||||
## Private convenience function to get a setting from the support infill
|
||||
# extruder.
|
||||
#
|
||||
# \param setting_key The key of the setting to get.
|
||||
# \param property The property to get from the setting.
|
||||
# \return The property of the specified setting in the support infill
|
||||
# extruder.
|
||||
def _getSettingFromSupportInfillExtruder(self, setting_key, property = "value"):
|
||||
return self._getSettingFromExtruder(setting_key, "support_infill_extruder_nr", property)
|
||||
|
||||
self._disallowed_areas = areas
|
||||
## Helper function to get a setting from an extruder specified in another
|
||||
# setting.
|
||||
#
|
||||
# \param setting_key The key of the setting to get.
|
||||
# \param extruder_setting_key The key of the setting that specifies from
|
||||
# which extruder to get the setting, if there are multiple extruders.
|
||||
# \param property The property to get from the setting.
|
||||
# \return The property of the specified setting in the specified extruder.
|
||||
def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"):
|
||||
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
## Convenience function to calculate the size of the bed adhesion in directions x, y.
|
||||
def _getBedAdhesionSize(self, container_stack):
|
||||
skirt_size = 0.0
|
||||
if not multi_extrusion:
|
||||
return self._global_container_stack.getProperty(setting_key, property)
|
||||
|
||||
extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value")
|
||||
|
||||
if extruder_index == "-1": # If extruder index is -1 use global instead
|
||||
return self._global_container_stack.getProperty(setting_key, property)
|
||||
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
return stack.getProperty(setting_key, property)
|
||||
|
||||
## Convenience function to calculate the disallowed radius around the edge.
|
||||
#
|
||||
# This disallowed radius is to allow for space around the models that is
|
||||
# not part of the collision radius, such as bed adhesion (skirt/brim/raft)
|
||||
# and travel avoid distance.
|
||||
def _getEdgeDisallowedSize(self):
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
container_stack = self._global_container_stack
|
||||
|
||||
# If we are printing one at a time, we need to add the bed adhesion size to the disallowed areas of the objects
|
||||
if container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
return 0.1 # Return a very small value, so we do draw disallowed area's near the edges.
|
||||
|
||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
||||
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"))
|
||||
skirt_distance = self._getSettingFromAdhesionExtruder("skirt_gap")
|
||||
skirt_line_count = self._getSettingFromAdhesionExtruder("skirt_line_count")
|
||||
bed_adhesion_size = skirt_distance + (skirt_line_count * self._getSettingFromAdhesionExtruder("skirt_brim_line_width"))
|
||||
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||
extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
|
||||
del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr.
|
||||
for value in extruder_values:
|
||||
bed_adhesion_size += value
|
||||
elif adhesion_type == "brim":
|
||||
skirt_size = container_stack.getProperty("brim_line_count", "value") * container_stack.getProperty("skirt_brim_line_width", "value")
|
||||
bed_adhesion_size = self._getSettingFromAdhesionExtruder("brim_line_count") * self._getSettingFromAdhesionExtruder("skirt_brim_line_width")
|
||||
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
adhesion_extruder_nr = int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||
extruder_values = ExtruderManager.getInstance().getAllExtruderValues("skirt_brim_line_width")
|
||||
del extruder_values[adhesion_extruder_nr] # Remove the value of the adhesion extruder nr.
|
||||
for value in extruder_values:
|
||||
bed_adhesion_size += value
|
||||
elif adhesion_type == "raft":
|
||||
skirt_size = container_stack.getProperty("raft_margin", "value")
|
||||
bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin")
|
||||
elif adhesion_type == "none":
|
||||
bed_adhesion_size = 0
|
||||
else:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
||||
|
||||
support_expansion = 0
|
||||
if self._getSettingFromSupportInfillExtruder("support_offset") and self._global_container_stack.getProperty("support_enable", "value"):
|
||||
support_expansion += self._getSettingFromSupportInfillExtruder("support_offset")
|
||||
|
||||
farthest_shield_distance = 0
|
||||
if container_stack.getProperty("draft_shield_enabled", "value"):
|
||||
skirt_size += container_stack.getProperty("draft_shield_dist", "value")
|
||||
farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("draft_shield_dist", "value"))
|
||||
if container_stack.getProperty("ooze_shield_enabled", "value"):
|
||||
farthest_shield_distance = max(farthest_shield_distance, container_stack.getProperty("ooze_shield_dist", "value"))
|
||||
|
||||
if container_stack.getProperty("xy_offset", "value"):
|
||||
skirt_size += container_stack.getProperty("xy_offset", "value")
|
||||
move_from_wall_radius = 0 # Moves that start from outer wall.
|
||||
move_from_wall_radius = max(move_from_wall_radius, max(self._getSettingFromAllExtruders("infill_wipe_dist")))
|
||||
avoid_enabled_per_extruder = self._getSettingFromAllExtruders(("travel_avoid_other_parts"))
|
||||
avoid_distance_per_extruder = self._getSettingFromAllExtruders("travel_avoid_distance")
|
||||
for index, avoid_other_parts_enabled in enumerate(avoid_enabled_per_extruder): #For each extruder (or just global).
|
||||
if avoid_other_parts_enabled:
|
||||
move_from_wall_radius = max(move_from_wall_radius, avoid_distance_per_extruder[index]) #Index of the same extruder.
|
||||
|
||||
return skirt_size
|
||||
#Now combine our different pieces of data to get the final border size.
|
||||
#Support expansion is added to the bed adhesion, since the bed adhesion goes around support.
|
||||
#Support expansion is added to farthest shield distance, since the shields go around support.
|
||||
border_size = max(move_from_wall_radius, support_expansion + farthest_shield_distance, support_expansion + bed_adhesion_size)
|
||||
return border_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"]
|
||||
_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"]
|
||||
_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"]
|
||||
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
||||
_extruder_settings = ["support_enable", "support_interface_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_interface_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
|
|
@ -1,9 +1,14 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Application import Application
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Math.Polygon import Polygon
|
||||
from . import ConvexHullNode
|
||||
|
||||
import UM.Settings.ContainerRegistry
|
||||
|
||||
import numpy
|
||||
|
||||
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
|
||||
|
@ -56,6 +61,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
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)))
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
|
||||
## Get the convex hull of the node with the full head size
|
||||
|
@ -109,7 +115,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key, property_name):
|
||||
if key in self._affected_settings and property_name == "value":
|
||||
if property_name != "value": #Not the value that was changed.
|
||||
return
|
||||
|
||||
if key in self._affected_settings:
|
||||
self._onChanged()
|
||||
if key in self._influencing_settings:
|
||||
self._init2DConvexHullCache() #Invalidate the cache.
|
||||
self._onChanged()
|
||||
|
||||
def _init2DConvexHullCache(self):
|
||||
|
@ -138,21 +150,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
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)
|
||||
convex_hull = child_polygon.getConvexHull() #First calculate the normal convex hull around the points.
|
||||
offset_hull = self._offsetHull(convex_hull) #Then apply the offset from the settings.
|
||||
|
||||
# Store the result in the cache
|
||||
self._2d_convex_hull_group_child_polygon = child_polygon
|
||||
self._2d_convex_hull_group_result = rounded_hull
|
||||
self._2d_convex_hull_group_result = offset_hull
|
||||
|
||||
return rounded_hull
|
||||
return offset_hull
|
||||
|
||||
else:
|
||||
rounded_hull = None
|
||||
offset_hull = None
|
||||
mesh = None
|
||||
world_transform = None
|
||||
if self._node.getMeshData():
|
||||
|
@ -190,19 +198,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
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)))
|
||||
offset_hull = self._offsetHull(convex_hull)
|
||||
else:
|
||||
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
||||
|
||||
# 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
|
||||
self._2d_convex_hull_mesh_result = offset_hull
|
||||
|
||||
return rounded_hull
|
||||
return offset_hull
|
||||
|
||||
def _getHeadAndFans(self):
|
||||
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
||||
|
@ -225,40 +231,43 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
# 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)
|
||||
extra_margin = max(0, self._getSettingProperty("raft_margin", "value"))
|
||||
elif adhesion_type == "brim":
|
||||
extra_margin = max(0, self._global_stack.getProperty("brim_width", "value") - head_y_size)
|
||||
extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||
elif adhesion_type == "none":
|
||||
extra_margin = 0
|
||||
elif adhesion_type == "skirt":
|
||||
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)
|
||||
0, self._getSettingProperty("skirt_gap", "value") +
|
||||
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||
else:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||
|
||||
# adjust head_and_fans with extra margin
|
||||
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))
|
||||
|
||||
extra_margin_polygon = Polygon.approximatedCircle(extra_margin)
|
||||
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)))
|
||||
## Offset the convex hull with settings that influence the collision area.
|
||||
#
|
||||
# This also applies a minimum offset of 0.5mm, because of edge cases due
|
||||
# to the rounding we apply.
|
||||
#
|
||||
# \param convex_hull Polygon of the original convex hull.
|
||||
# \return New Polygon instance that is offset with everything that
|
||||
# influences the collision area.
|
||||
def _offsetHull(self, convex_hull):
|
||||
horizontal_expansion = max(0.5, self._getSettingProperty("xy_offset", "value"))
|
||||
expansion_polygon = Polygon(numpy.array([
|
||||
[-horizontal_expansion, -horizontal_expansion],
|
||||
[-horizontal_expansion, horizontal_expansion],
|
||||
[horizontal_expansion, horizontal_expansion],
|
||||
[horizontal_expansion, -horizontal_expansion]
|
||||
], numpy.float32))
|
||||
return convex_hull.getMinkowskiHull(expansion_polygon)
|
||||
|
||||
def _onChanged(self, *args):
|
||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||
|
@ -268,6 +277,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
|
||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
|
@ -275,9 +287,35 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.connect(self._onSettingValueChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
## Returns true if node is a descendent or the same as the root node.
|
||||
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
|
||||
def _getSettingProperty(self, setting_key, property="value"):
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
return per_mesh_stack.getProperty(setting_key, property)
|
||||
|
||||
multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
if not multi_extrusion:
|
||||
return self._global_stack.getProperty(setting_key, property)
|
||||
|
||||
extruder_index = self._global_stack.getProperty(setting_key, "limit_to_extruder")
|
||||
if extruder_index == "-1": #No limit_to_extruder.
|
||||
extruder_stack_id = self._node.callDecoration("getActiveExtruder")
|
||||
if not extruder_stack_id: #Decoration doesn't exist.
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
|
||||
extruder_stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
return extruder_stack.getProperty(setting_key, property)
|
||||
else: #Limit_to_extruder is set. Use that one.
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
return stack.getProperty(setting_key, property)
|
||||
|
||||
## Returns true if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root, node):
|
||||
if node is None:
|
||||
return False
|
||||
|
@ -288,4 +326,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
_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"]
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||
|
||||
## Settings that change the convex hull.
|
||||
#
|
||||
# If these settings change, the convex hull should be recalculated.
|
||||
_influencing_settings = {"xy_offset"}
|
|
@ -23,20 +23,25 @@ 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 UM.Operations.TranslateOperation import TranslateOperation
|
||||
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 cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
from cura.Settings.ProfilesModel import ProfilesModel
|
||||
from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel
|
||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Settings.UserProfilesModel import UserProfilesModel
|
||||
|
||||
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
|
||||
|
@ -56,11 +61,44 @@ from PyQt5.QtGui import QColor, QIcon
|
|||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import sys
|
||||
import os.path
|
||||
import numpy
|
||||
import copy
|
||||
import urllib
|
||||
import os
|
||||
import time
|
||||
|
||||
CONFIG_LOCK_FILENAME = "cura.lock"
|
||||
|
||||
## Contextmanager to create a lock file and remove it afterwards.
|
||||
@contextmanager
|
||||
def lockFile(filename):
|
||||
try:
|
||||
with open(filename, 'w') as lock_file:
|
||||
lock_file.write("Lock file - Cura is currently writing")
|
||||
except:
|
||||
Logger.log("e", "Could not create lock file [%s]" % filename)
|
||||
yield
|
||||
try:
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
except:
|
||||
Logger.log("e", "Could not delete lock file [%s]" % filename)
|
||||
|
||||
|
||||
## Wait for a lock file to disappear
|
||||
# the maximum allowable age is settable; if the file is too old, it will be ignored too
|
||||
def waitFileDisappear(filename, max_age_seconds=10, msg=""):
|
||||
now = time.time()
|
||||
while os.path.exists(filename) and now < os.path.getmtime(filename) + max_age_seconds and now > os.path.getmtime(filename):
|
||||
if msg:
|
||||
Logger.log("d", msg)
|
||||
time.sleep(1)
|
||||
now = time.time()
|
||||
|
||||
|
||||
numpy.seterr(all="ignore")
|
||||
|
||||
|
@ -97,14 +135,24 @@ class CuraApplication(QtApplication):
|
|||
# Need to do this before ContainerRegistry tries to load the machines
|
||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
# this setting can be changed for each group in one-at-a-time mode
|
||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("global_inherits_stack", DefinitionPropertyType.Function, default = "-1")
|
||||
|
||||
# From which stack the setting would inherit if not defined per object (handled in the engine)
|
||||
# AND for settings which are not settable_per_mesh:
|
||||
# which extruder is the only extruder this setting is obtained from
|
||||
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1")
|
||||
|
||||
# For settings which are not settable_per_mesh and not settable_per_extruder:
|
||||
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
|
||||
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None)
|
||||
|
||||
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
||||
|
||||
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
|
||||
SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
|
||||
SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
|
||||
|
||||
## Add the 4 types of profiles to storage.
|
||||
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||
|
@ -128,12 +176,14 @@ class CuraApplication(QtApplication):
|
|||
{
|
||||
("quality", UM.Settings.InstanceContainer.InstanceContainer.Version): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", UM.Settings.ContainerStack.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
|
||||
("preferences", Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences")
|
||||
("preferences", Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences"),
|
||||
("user", UM.Settings.InstanceContainer.InstanceContainer.Version): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
|
||||
}
|
||||
)
|
||||
|
||||
self._machine_action_manager = MachineActionManager.MachineActionManager()
|
||||
self._machine_manager = None # This is initialized on demand.
|
||||
self._setting_inheritance_manager = None
|
||||
|
||||
self._additional_components = {} # Components to add to certain areas in the interface
|
||||
|
||||
|
@ -191,6 +241,8 @@ class CuraApplication(QtApplication):
|
|||
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container._id = "empty_quality"
|
||||
empty_quality_container.setName("Not supported")
|
||||
empty_quality_container.addMetaDataEntry("quality_type", "normal")
|
||||
empty_quality_container.addMetaDataEntry("type", "quality")
|
||||
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
||||
empty_quality_changes_container = copy.deepcopy(empty_container)
|
||||
|
@ -198,6 +250,9 @@ class CuraApplication(QtApplication):
|
|||
empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
|
||||
ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
|
||||
|
||||
# Set the filename to create if cura is writing in the config dir.
|
||||
self._config_lock_filename = os.path.join(Resources.getConfigStoragePath(), CONFIG_LOCK_FILENAME)
|
||||
self.waitConfigLockFile()
|
||||
ContainerRegistry.getInstance().load()
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_mode", "simple")
|
||||
|
@ -219,7 +274,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Preferences.getInstance().setDefault("general/visible_settings", """
|
||||
machine_settings
|
||||
resolution
|
||||
resolution
|
||||
layer_height
|
||||
shell
|
||||
wall_thickness
|
||||
|
@ -244,17 +299,17 @@ class CuraApplication(QtApplication):
|
|||
cool_fan_enabled
|
||||
support
|
||||
support_enable
|
||||
support_extruder_nr
|
||||
support_type
|
||||
support_interface_density
|
||||
platform_adhesion
|
||||
adhesion_type
|
||||
adhesion_extruder_nr
|
||||
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
|
||||
|
@ -280,6 +335,11 @@ class CuraApplication(QtApplication):
|
|||
|
||||
def getContainerRegistry(self):
|
||||
return CuraContainerRegistry.getInstance()
|
||||
## Lock file check: if (another) Cura is writing in the Config dir.
|
||||
# one may not be able to read a valid set of files while writing. Not entirely fool-proof,
|
||||
# but works when you start Cura shortly after shutting down.
|
||||
def waitConfigLockFile(self):
|
||||
waitFileDisappear(self._config_lock_filename, max_age_seconds=10, msg="Waiting for Cura to finish writing in the config dir...")
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
|
@ -308,58 +368,67 @@ class CuraApplication(QtApplication):
|
|||
if not self._started: # Do not do saving during application start
|
||||
return
|
||||
|
||||
for instance in ContainerRegistry.getInstance().findInstanceContainers():
|
||||
if not instance.isDirty():
|
||||
continue
|
||||
self.waitConfigLockFile()
|
||||
|
||||
try:
|
||||
data = instance.serialize()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
||||
continue
|
||||
# When starting Cura, we check for the lockFile which is created and deleted here
|
||||
with lockFile(self._config_lock_filename):
|
||||
|
||||
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" or instance_type == "quality_changes":
|
||||
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)
|
||||
for instance in ContainerRegistry.getInstance().findInstanceContainers():
|
||||
if not instance.isDirty():
|
||||
continue
|
||||
|
||||
if path:
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f:
|
||||
f.write(data)
|
||||
try:
|
||||
data = instance.serialize()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
||||
continue
|
||||
|
||||
for stack in ContainerRegistry.getInstance().findContainerStacks():
|
||||
if not stack.isDirty():
|
||||
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" or instance_type == "quality_changes":
|
||||
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)
|
||||
|
||||
try:
|
||||
data = stack.serialize()
|
||||
except NotImplementedError:
|
||||
continue
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
|
||||
continue
|
||||
if path:
|
||||
instance.setPath(path)
|
||||
with SaveFile(path, "wt") as f:
|
||||
f.write(data)
|
||||
|
||||
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)
|
||||
for stack in ContainerRegistry.getInstance().findContainerStacks():
|
||||
self.saveStack(stack)
|
||||
|
||||
def saveStack(self, stack):
|
||||
if not stack.isDirty():
|
||||
return
|
||||
try:
|
||||
data = stack.serialize()
|
||||
except NotImplementedError:
|
||||
return
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred when serializing container %s", stack.getId())
|
||||
return
|
||||
|
||||
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:
|
||||
stack.setPath(path)
|
||||
with SaveFile(path, "wt") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
@pyqtSlot(str, result = QUrl)
|
||||
|
@ -434,6 +503,8 @@ class CuraApplication(QtApplication):
|
|||
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
|
||||
ExtruderManager.getInstance()
|
||||
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
||||
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
|
||||
self.getSettingInheritanceManager)
|
||||
|
||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||
self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
|
||||
|
@ -457,6 +528,11 @@ class CuraApplication(QtApplication):
|
|||
self._machine_manager = MachineManager.createMachineManager()
|
||||
return self._machine_manager
|
||||
|
||||
def getSettingInheritanceManager(self, *args):
|
||||
if self._setting_inheritance_manager is None:
|
||||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||
return self._setting_inheritance_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.
|
||||
|
@ -493,12 +569,18 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
|
||||
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
|
||||
qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
|
||||
qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
|
||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||
|
||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
|
||||
|
||||
qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
|
||||
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
|
||||
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
|
||||
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
|
||||
|
||||
engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.getInstance())
|
||||
|
||||
|
@ -511,11 +593,19 @@ class CuraApplication(QtApplication):
|
|||
|
||||
def onSelectionChanged(self):
|
||||
if Selection.hasSelection():
|
||||
if not self.getController().getActiveTool():
|
||||
if self.getController().getActiveTool():
|
||||
# If the tool has been disabled by the new selection
|
||||
if not self.getController().getActiveTool().getEnabled():
|
||||
# Default
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
else:
|
||||
if self._previous_active_tool:
|
||||
self.getController().setActiveTool(self._previous_active_tool)
|
||||
if not self.getController().getActiveTool().getEnabled():
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
self._previous_active_tool = None
|
||||
else:
|
||||
# Default
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
if Preferences.getInstance().getValue("view/center_on_select"):
|
||||
self._center_after_select = True
|
||||
|
@ -523,8 +613,6 @@ class CuraApplication(QtApplication):
|
|||
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:
|
||||
|
@ -589,8 +677,6 @@ class CuraApplication(QtApplication):
|
|||
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")
|
||||
|
@ -604,17 +690,17 @@ class CuraApplication(QtApplication):
|
|||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
group_node = None
|
||||
if node.getParent():
|
||||
group_node = node.getParent()
|
||||
op = RemoveSceneNodeOperation(node)
|
||||
op = GroupedOperation()
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
group_node = node.getParent()
|
||||
if group_node:
|
||||
# Note that at this point the node has not yet been deleted
|
||||
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
|
||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
||||
op.addOperation(RemoveSceneNodeOperation(group_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)
|
||||
|
@ -630,10 +716,9 @@ class CuraApplication(QtApplication):
|
|||
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):
|
||||
new_node = copy.deepcopy(current_node)
|
||||
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||
op.push()
|
||||
|
||||
|
@ -707,18 +792,21 @@ class CuraApplication(QtApplication):
|
|||
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)))
|
||||
|
||||
if node.getBoundingBox():
|
||||
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
|
||||
else:
|
||||
center_y = 0
|
||||
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0)))
|
||||
op.push()
|
||||
|
||||
## Reset all transformations on nodes with mesh data.
|
||||
|
||||
## Reset all transformations on nodes with mesh data.
|
||||
@pyqtSlot()
|
||||
def resetAll(self):
|
||||
Logger.log("i", "Resetting all scene transformations")
|
||||
|
@ -734,14 +822,16 @@ class CuraApplication(QtApplication):
|
|||
|
||||
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)))
|
||||
|
||||
if node.getBoundingBox():
|
||||
center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
|
||||
else:
|
||||
center_y = 0
|
||||
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
|
||||
op.push()
|
||||
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
|
@ -806,20 +896,32 @@ class CuraApplication(QtApplication):
|
|||
except Exception as e:
|
||||
Logger.log("d", "mergeSelected: Exception:", e)
|
||||
return
|
||||
multi_material_decorator = MultiMaterialDecorator.MultiMaterialDecorator()
|
||||
group_node.addDecorator(multi_material_decorator)
|
||||
|
||||
# Compute the center of the objects when their origins are aligned.
|
||||
object_centers = [node.getMeshData().getCenterPosition().scale(node.getScale()) for node in group_node.getChildren()]
|
||||
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||
middle_z = sum([v.z for v in object_centers]) / len(object_centers)
|
||||
offset = Vector(middle_x, middle_y, middle_z)
|
||||
meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()]
|
||||
|
||||
# Compute the center of the objects
|
||||
object_centers = []
|
||||
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||
orientation = node.getOrientation().toMatrix()
|
||||
rotated_mesh = mesh.getTransformed(orientation)
|
||||
center = rotated_mesh.getCenterPosition().scale(node.getScale())
|
||||
object_centers.append(center)
|
||||
if object_centers and len(object_centers) > 0:
|
||||
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||
middle_z = sum([v.z for v in object_centers]) / len(object_centers)
|
||||
offset = Vector(middle_x, middle_y, middle_z)
|
||||
else:
|
||||
offset = Vector(0, 0, 0)
|
||||
|
||||
# Move each node to the same position.
|
||||
for center, node in zip(object_centers, group_node.getChildren()):
|
||||
# Align the object and also apply the offset to center it inside the group.
|
||||
node.setPosition(center - offset)
|
||||
for mesh, node in zip(meshes, group_node.getChildren()):
|
||||
orientation = node.getOrientation().toMatrix()
|
||||
rotated_mesh = mesh.getTransformed(orientation)
|
||||
|
||||
# Align the object around its zero position
|
||||
# and also apply the offset to center it inside the group.
|
||||
node.setPosition(-rotated_mesh.getZeroPosition().scale(node.getScale()) - offset)
|
||||
|
||||
# Use the previously found center of the group bounding box as the new location of the group
|
||||
group_node.setPosition(group_node.getBoundingBox().center)
|
||||
|
@ -873,15 +975,16 @@ class CuraApplication(QtApplication):
|
|||
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()
|
||||
nodes = job.getResult()
|
||||
for node in nodes:
|
||||
if node is not 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.
|
||||
self.getController().getScene().sceneChanged.emit(node) #Force scene change.
|
||||
|
||||
def _onJobFinished(self, job):
|
||||
if type(job) is not ReadMeshJob or not job.getResult():
|
||||
|
|
|
@ -14,8 +14,9 @@ class LayerPolygon:
|
|||
SupportInfillType = 7
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
SupportInterfaceType = 10
|
||||
|
||||
__jump_map = numpy.logical_or( numpy.arange(10) == NoneType, numpy.arange(10) >= MoveCombingType )
|
||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType)
|
||||
|
||||
def __init__(self, mesh, extruder, line_types, data, line_widths):
|
||||
self._mesh = mesh
|
||||
|
@ -36,12 +37,12 @@ class LayerPolygon:
|
|||
|
||||
# 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
|
||||
self._color_map = self.__color_map * [1, 1, 1, self._extruder] # The alpha component is used to store the extruder nr
|
||||
self._colors = self._color_map[self._types]
|
||||
|
||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||
# Should be generated in better way, not hardcoded.
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0], dtype=numpy.bool)
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None
|
||||
self._build_cache_needed_points = None
|
||||
|
@ -171,29 +172,17 @@ class LayerPolygon:
|
|||
|
||||
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]
|
||||
[1.0, 1.0, 1.0, 1.0], # NoneType
|
||||
[1.0, 0.0, 0.0, 1.0], # Inset0Type
|
||||
[0.0, 1.0, 0.0, 1.0], # InsetXType
|
||||
[1.0, 1.0, 0.0, 1.0], # SkinType
|
||||
[0.0, 1.0, 1.0, 1.0], # SupportType
|
||||
[0.0, 1.0, 1.0, 1.0], # SkirtType
|
||||
[1.0, 0.75, 0.0, 1.0], # InfillType
|
||||
[0.0, 1.0, 1.0, 1.0], # SupportInfillType
|
||||
[0.0, 0.0, 1.0, 1.0], # MoveCombingType
|
||||
[0.5, 0.5, 1.0, 1.0], # MoveRetractionType
|
||||
[0.25, 0.75, 1.0, 1.0] # SupportInterfaceType
|
||||
])
|
|
@ -1,11 +0,0 @@
|
|||
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()
|
|
@ -15,6 +15,9 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
|
|||
from . import PlatformPhysicsOperation
|
||||
from . import ZOffsetDecorator
|
||||
|
||||
import random # used for list shuffling
|
||||
|
||||
|
||||
class PlatformPhysics:
|
||||
def __init__(self, controller, volume):
|
||||
super().__init__()
|
||||
|
@ -29,6 +32,8 @@ class PlatformPhysics:
|
|||
self._change_timer.setInterval(100)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._onChangeTimerFinished)
|
||||
self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
|
||||
self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
|
||||
|
||||
Preferences.getInstance().addPreference("physics/automatic_push_free", True)
|
||||
Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
|
||||
|
@ -46,7 +51,12 @@ class PlatformPhysics:
|
|||
# same direction.
|
||||
transformed_nodes = []
|
||||
|
||||
for node in BreadthFirstIterator(root):
|
||||
group_nodes = []
|
||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
nodes = list(BreadthFirstIterator(root))
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
|
||||
continue
|
||||
|
||||
|
@ -67,9 +77,12 @@ class PlatformPhysics:
|
|||
if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||
node._outside_buildarea = True
|
||||
|
||||
if node.callDecoration("isGroup"):
|
||||
group_nodes.append(node) # Keep list of affected group_nodes
|
||||
|
||||
# Move it downwards if bottom is above platform
|
||||
move_vector = Vector()
|
||||
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
|
||||
if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
|
||||
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
|
||||
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
|
||||
|
||||
|
@ -89,7 +102,7 @@ class PlatformPhysics:
|
|||
continue
|
||||
|
||||
# Ignore collisions within a group
|
||||
if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
|
||||
if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
|
||||
continue
|
||||
|
||||
# Ignore nodes that do not have the right properties set.
|
||||
|
@ -97,29 +110,41 @@ class PlatformPhysics:
|
|||
continue
|
||||
|
||||
if other_node in transformed_nodes:
|
||||
continue # Other node is already moving, wait for next pass.
|
||||
continue # Other node is already moving, wait for next pass.
|
||||
|
||||
# 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)
|
||||
overlap = (0, 0) # Start loop with no overlap
|
||||
current_overlap_checks = 0
|
||||
# Continue to check the overlap until we no longer find one.
|
||||
while overlap and current_overlap_checks < self._max_overlap_checks:
|
||||
current_overlap_checks += 1
|
||||
head_hull = node.callDecoration("getConvexHullHead")
|
||||
if head_hull: # One at a time intersection.
|
||||
overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
|
||||
if not overlap:
|
||||
other_head_hull = other_node.callDecoration("getConvexHullHead")
|
||||
if other_head_hull:
|
||||
overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
|
||||
if overlap:
|
||||
# Moving ensured that overlap was still there. Try anew!
|
||||
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||
z=move_vector.z + overlap[1] * self._move_factor)
|
||||
else:
|
||||
# Moving ensured that overlap was still there. Try anew!
|
||||
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||
z=move_vector.z + overlap[1] * self._move_factor)
|
||||
else:
|
||||
# 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
|
||||
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.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
|
||||
if overlap: # Moving ensured that overlap was still there. Try anew!
|
||||
move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
|
||||
z=move_vector.z + overlap[1] * self._move_factor)
|
||||
else:
|
||||
# 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():
|
||||
|
@ -129,7 +154,6 @@ class PlatformPhysics:
|
|||
overlap = convex_hull.intersectsPolygon(area)
|
||||
if overlap is None:
|
||||
continue
|
||||
|
||||
node._outside_buildarea = True
|
||||
|
||||
if not Vector.Null.equals(move_vector, epsilon=1e-5):
|
||||
|
@ -137,6 +161,12 @@ class PlatformPhysics:
|
|||
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
|
||||
op.push()
|
||||
|
||||
# Group nodes should override the _outside_buildarea property of their children.
|
||||
for group_node in group_nodes:
|
||||
for child_node in group_node.getAllChildren():
|
||||
child_node._outside_buildarea = group_node._outside_buildarea
|
||||
|
||||
|
||||
def _onToolOperationStarted(self, tool):
|
||||
self._enabled = False
|
||||
|
||||
|
|
|
@ -3,21 +3,22 @@
|
|||
|
||||
from UM.Operations.Operation import Operation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
## 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._old_transformation = node.getLocalTransformation()
|
||||
self._translation = translation
|
||||
self._always_merge = True
|
||||
|
||||
def undo(self):
|
||||
self._node.setPosition(self._old_position)
|
||||
self._node.setTransformation(self._old_transformation)
|
||||
|
||||
def redo(self):
|
||||
self._node.setPosition(self._new_position)
|
||||
self._node.translate(self._translation, SceneNode.TransformSpace.World)
|
||||
|
||||
def mergeWith(self, other):
|
||||
group = GroupedOperation()
|
||||
|
@ -28,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
|
|||
return group
|
||||
|
||||
def __repr__(self):
|
||||
return "PlatformPhysicsOperation(new_position = {0})".format(self._new_position)
|
||||
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
||||
|
|
|
@ -121,6 +121,7 @@ class PrintInformation(QObject):
|
|||
@pyqtSlot(str, result = str)
|
||||
def createJobName(self, base_name):
|
||||
base_name = self._stripAccents(base_name)
|
||||
self._setAbbreviatedMachineName()
|
||||
if Preferences.getInstance().getValue("cura/jobname_prefix"):
|
||||
return self._abbr_machine + "_" + base_name
|
||||
else:
|
||||
|
@ -129,7 +130,12 @@ class PrintInformation(QObject):
|
|||
## 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()
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
self._abbr_machine = ""
|
||||
return
|
||||
|
||||
global_stack_name = global_container_stack.getName()
|
||||
split_name = global_stack_name.split(" ")
|
||||
abbr_machine = ""
|
||||
for word in split_name:
|
||||
|
|
|
@ -2,6 +2,7 @@ from UM.i18n import i18nCatalog
|
|||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
import UM.Settings.ContainerRegistry
|
||||
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
from UM.Logger import Logger
|
||||
|
@ -24,6 +25,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
def __init__(self, device_id, parent = None):
|
||||
super().__init__(device_id = device_id, parent = parent)
|
||||
|
||||
self._container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
self._target_bed_temperature = 0
|
||||
self._bed_temperature = 0
|
||||
self._num_extruders = 1
|
||||
|
@ -44,7 +46,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
self._error_text = ""
|
||||
self._accepts_commands = True
|
||||
|
||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
||||
self._printer_state = ""
|
||||
self._printer_type = "unknown"
|
||||
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
## Signals
|
||||
|
@ -91,10 +96,32 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
|
||||
printerStateChanged = pyqtSignal()
|
||||
|
||||
printerTypeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify=printerTypeChanged)
|
||||
def printerType(self):
|
||||
return self._printer_type
|
||||
|
||||
@pyqtProperty(str, notify=printerStateChanged)
|
||||
def printerState(self):
|
||||
return self._printer_state
|
||||
|
||||
@pyqtProperty(str, notify = jobStateChanged)
|
||||
def jobState(self):
|
||||
return self._job_state
|
||||
|
||||
def _updatePrinterType(self, printer_type):
|
||||
if self._printer_type != printer_type:
|
||||
self._printer_type = printer_type
|
||||
self.printerTypeChanged.emit()
|
||||
|
||||
def _updatePrinterState(self, printer_state):
|
||||
if self._printer_state != printer_state:
|
||||
self._printer_state = printer_state
|
||||
self.printerStateChanged.emit()
|
||||
|
||||
def _updateJobState(self, job_state):
|
||||
if self._job_state != job_state:
|
||||
self._job_state = job_state
|
||||
|
@ -107,6 +134,20 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
def _setJobState(self, job_state):
|
||||
Logger.log("w", "_setJobState is not implemented by this output device")
|
||||
|
||||
@pyqtSlot()
|
||||
def startCamera(self):
|
||||
self._startCamera()
|
||||
|
||||
def _startCamera(self):
|
||||
Logger.log("w", "_startCamera is not implemented by this output device")
|
||||
|
||||
@pyqtSlot()
|
||||
def stopCamera(self):
|
||||
self._stopCamera()
|
||||
|
||||
def _stopCamera(self):
|
||||
Logger.log("w", "_stopCamera is not implemented by this output device")
|
||||
|
||||
@pyqtProperty(str, notify = jobNameChanged)
|
||||
def jobName(self):
|
||||
return self._job_name
|
||||
|
@ -150,8 +191,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
@pyqtSlot(int)
|
||||
def setTargetBedTemperature(self, temperature):
|
||||
self._setTargetBedTemperature(temperature)
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
if self._target_bed_temperature != temperature:
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Time the print has been printing.
|
||||
# Note that timeTotal - timeElapsed should give time remaining.
|
||||
|
@ -212,8 +254,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
# 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()
|
||||
if self._bed_temperature != temperature:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
## Get the target bed temperature if connected printer (if any)
|
||||
@pyqtProperty(int, notify = targetBedTemperatureChanged)
|
||||
|
@ -228,8 +271,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
@pyqtSlot(int, int)
|
||||
def setTargetHotendTemperature(self, index, temperature):
|
||||
self._setTargetHotendTemperature(index, temperature)
|
||||
self._target_hotend_temperatures[index] = temperature
|
||||
self.targetHotendTemperaturesChanged.emit()
|
||||
|
||||
if self._target_hotend_temperatures[index] != temperature:
|
||||
self._target_hotend_temperatures[index] = temperature
|
||||
self.targetHotendTemperaturesChanged.emit()
|
||||
|
||||
## Implementation function of setTargetHotendTemperature.
|
||||
# /param index Index of the hotend to set the temperature of
|
||||
|
@ -251,13 +296,29 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
# /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()
|
||||
if self._hotend_temperatures[index] != temperature:
|
||||
self._hotend_temperatures[index] = temperature
|
||||
self.hotendTemperaturesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||
def materialIds(self):
|
||||
return self._material_ids
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialIdChanged)
|
||||
def materialNames(self):
|
||||
result = []
|
||||
for material_id in self._material_ids:
|
||||
if material_id is None:
|
||||
result.append(i18n_catalog.i18nc("@item:material", "No material loaded"))
|
||||
continue
|
||||
|
||||
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_id)
|
||||
if containers:
|
||||
result.append(containers[0].getName())
|
||||
else:
|
||||
result.append(i18n_catalog.i18nc("@item:material", "Unknown material"))
|
||||
return result
|
||||
|
||||
## Protected setter for the current material id.
|
||||
# /param index Index of the extruder
|
||||
# /param material_id id of the material
|
||||
|
@ -267,7 +328,6 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
self._material_ids[index] = material_id
|
||||
self.materialIdChanged.emit(index, material_id)
|
||||
|
||||
|
||||
@pyqtProperty("QVariantList", notify = hotendIdChanged)
|
||||
def hotendIds(self):
|
||||
return self._hotend_ids
|
||||
|
@ -302,8 +362,9 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
## 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)
|
||||
if self._connection_state != connection_state:
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(str, notify = connectionTextChanged)
|
||||
def connectionText(self):
|
||||
|
@ -351,6 +412,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
if self._head_z != z:
|
||||
self._head_z = z
|
||||
position_changed = True
|
||||
|
||||
if position_changed:
|
||||
self.headPositionChanged.emit()
|
||||
|
||||
|
|
279
cura/QualityManager.py
Normal file
279
cura/QualityManager.py
Normal file
|
@ -0,0 +1,279 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import UM.Application
|
||||
import cura.Settings.ExtruderManager
|
||||
import UM.Settings.ContainerRegistry
|
||||
|
||||
# This collects a lot of quality and quality changes related code which was split between ContainerManager
|
||||
# and the MachineManager and really needs to usable from both.
|
||||
|
||||
class QualityManager:
|
||||
|
||||
## Get the singleton instance for this class.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||
if QualityManager.__instance is None:
|
||||
QualityManager.__instance = cls()
|
||||
return QualityManager.__instance
|
||||
|
||||
__instance = None
|
||||
|
||||
## Find a quality by name for a specific machine definition and materials.
|
||||
#
|
||||
# \param quality_name
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality container \type{ContainerInstance}
|
||||
def findQualityByName(self, quality_name, machine_definition=None, material_containers=None):
|
||||
criteria = {"type": "quality", "name": quality_name}
|
||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||
|
||||
# Fall back to using generic materials and qualities if nothing could be found.
|
||||
if not result and material_containers and len(material_containers) == 1:
|
||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
return result[0] if result else None
|
||||
|
||||
## Find a quality changes container by name.
|
||||
#
|
||||
# \param quality_changes_name \type{str} the name of the quality changes container.
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality changes containers \type{List[ContainerInstance]}
|
||||
def findQualityChangesByName(self, quality_changes_name, machine_definition=None):
|
||||
criteria = {"type": "quality_changes", "name": quality_changes_name}
|
||||
result = self._getFilteredContainersForStack(machine_definition, [], **criteria)
|
||||
|
||||
return result
|
||||
|
||||
## Fetch the list of available quality types for this combination of machine definition and materials.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer}
|
||||
# \param material_containers \type{List[InstanceContainer]}
|
||||
# \return \type{List[str]}
|
||||
def findAllQualityTypesForMachineAndMaterials(self, machine_definition, material_containers):
|
||||
# Determine the common set of quality types which can be
|
||||
# applied to all of the materials for this machine.
|
||||
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
|
||||
common_quality_types = set(quality_type_dict.keys())
|
||||
for material_container in material_containers[1:]:
|
||||
next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
|
||||
common_quality_types.intersection_update(set(next_quality_type_dict.keys()))
|
||||
|
||||
return list(common_quality_types)
|
||||
|
||||
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \param material \type{ContainerInstance} the material.
|
||||
# \return \type{Dict[str, ContainerInstance]} the dict of suitable quality type names mapping to qualities.
|
||||
def __fetchQualityTypeDictForMaterial(self, machine_definition, material):
|
||||
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
|
||||
quality_type_dict = {}
|
||||
for quality in qualities:
|
||||
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
|
||||
return quality_type_dict
|
||||
|
||||
## Find a quality container by quality type.
|
||||
#
|
||||
# \param quality_type \type{str} the name of the quality type to search for.
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality container \type{ContainerInstance}
|
||||
def findQualityByQualityType(self, quality_type, machine_definition=None, material_containers=None, **kwargs):
|
||||
criteria = kwargs
|
||||
criteria["type"] = "quality"
|
||||
if quality_type:
|
||||
criteria["quality_type"] = quality_type
|
||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||
|
||||
# Fall back to using generic materials and qualities if nothing could be found.
|
||||
if not result and material_containers and len(material_containers) == 1:
|
||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
return result[0] if result else None
|
||||
|
||||
## Find all suitable qualities for a combination of machine and material.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \param material_container \type{ContainerInstance} the material.
|
||||
# \return \type{List[ContainerInstance]} the list of suitable qualities.
|
||||
def findAllQualitiesForMachineMaterial(self, machine_definition, material_container):
|
||||
criteria = {"type": "quality" }
|
||||
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
||||
if not result:
|
||||
basic_materials = self._getBasicMaterials(material_container)
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
return result
|
||||
|
||||
## Find all quality changes for a machine.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \return \type{List[InstanceContainer]} the list of quality changes
|
||||
def findAllQualityChangesForMachine(self, machine_definition):
|
||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
definition_id = machine_definition.getId()
|
||||
else:
|
||||
definition_id = "fdmprinter"
|
||||
|
||||
filter_dict = { "type": "quality_changes", "extruder": None, "definition": definition_id }
|
||||
quality_changes_list = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||
return quality_changes_list
|
||||
|
||||
## Find all usable qualities for a machine and extruders.
|
||||
#
|
||||
# Finds all of the qualities for this combination of machine and extruders.
|
||||
# Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
|
||||
# then only one of then is returned (at random).
|
||||
#
|
||||
# \param global_container_stack \type{ContainerStack} the global machine definition
|
||||
# \param extruder_stacks \type{List[ContainerStack]} the list of extruder stacks
|
||||
# \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
|
||||
# return come from the first extruder in the given list of extruders.
|
||||
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack, extruder_stacks):
|
||||
global_machine_definition = global_container_stack.getBottom()
|
||||
|
||||
if extruder_stacks:
|
||||
# Multi-extruder machine detected.
|
||||
materials = [stack.findContainer(type="material") for stack in extruder_stacks]
|
||||
else:
|
||||
# Machine with one extruder.
|
||||
materials = [global_container_stack.findContainer(type="material")]
|
||||
|
||||
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
|
||||
|
||||
# Map the list of quality_types to InstanceContainers
|
||||
qualities = self.findAllQualitiesForMachineMaterial(global_machine_definition, materials[0])
|
||||
quality_type_dict = {}
|
||||
for quality in qualities:
|
||||
quality_type_dict[quality.getMetaDataEntry("quality_type")] = quality
|
||||
|
||||
return [quality_type_dict[quality_type] for quality_type in quality_types]
|
||||
|
||||
## Fetch more basic versions of a material.
|
||||
#
|
||||
# This tries to find a generic or basic version of the given material.
|
||||
# \param material_container \type{InstanceContainer} the material
|
||||
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
|
||||
def _getBasicMaterials(self, material_container):
|
||||
base_material = material_container.getMetaDataEntry("material")
|
||||
material_container_definition = material_container.getDefinition()
|
||||
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
||||
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
||||
else:
|
||||
definition_id = "fdmprinter"
|
||||
|
||||
if base_material:
|
||||
# There is a basic material specified
|
||||
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
containers = [basic_material for basic_material in containers if
|
||||
basic_material.getMetaDataEntry("variant") == material_container.getMetaDataEntry(
|
||||
"variant")]
|
||||
return containers
|
||||
|
||||
return []
|
||||
|
||||
def _getFilteredContainers(self, **kwargs):
|
||||
return self._getFilteredContainersForStack(None, None, **kwargs)
|
||||
|
||||
def _getFilteredContainersForStack(self, machine_definition=None, material_containers=None, **kwargs):
|
||||
# Fill in any default values.
|
||||
if machine_definition is None:
|
||||
machine_definition = UM.Application.getInstance().getGlobalContainerStack().getBottom()
|
||||
quality_definition_id = machine_definition.getMetaDataEntry("quality_definition")
|
||||
if quality_definition_id is not None:
|
||||
machine_definition = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
||||
|
||||
if material_containers is None:
|
||||
active_stacks = cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||
material_containers = [stack.findContainer(type="material") for stack in active_stacks]
|
||||
|
||||
criteria = kwargs
|
||||
filter_by_material = False
|
||||
|
||||
machine_definition = self.getParentMachineDefinition(machine_definition)
|
||||
whole_machine_definition = self.getWholeMachineDefinition(machine_definition)
|
||||
if whole_machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
definition_id = machine_definition.getMetaDataEntry("quality_definition", whole_machine_definition.getId())
|
||||
criteria["definition"] = definition_id
|
||||
|
||||
filter_by_material = whole_machine_definition.getMetaDataEntry("has_materials")
|
||||
else:
|
||||
criteria["definition"] = "fdmprinter"
|
||||
|
||||
# Stick the material IDs in a set
|
||||
if material_containers is None or len(material_containers) == 0:
|
||||
filter_by_material = False
|
||||
else:
|
||||
material_ids = set()
|
||||
for material_instance in material_containers:
|
||||
if material_instance is not None:
|
||||
material_ids.add(material_instance.getId())
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
|
||||
result = []
|
||||
for container in containers:
|
||||
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
|
||||
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and not "global_quality" in kwargs:
|
||||
continue
|
||||
result.append(container)
|
||||
return result
|
||||
|
||||
## Get the parent machine definition of a machine definition.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||
# an extruder definition.
|
||||
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
|
||||
# definition doesn't have a parent then it is simply returned.
|
||||
def getParentMachineDefinition(self, machine_definition):
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
|
||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||
if machine_entry is None:
|
||||
# We have a normal (whole) machine defintion
|
||||
quality_definition = machine_definition.getMetaDataEntry("quality_definition")
|
||||
if quality_definition is not None:
|
||||
parent_machine_definition = container_registry.findDefinitionContainers(id=quality_definition)[0]
|
||||
return self.getParentMachineDefinition(parent_machine_definition)
|
||||
else:
|
||||
return machine_definition
|
||||
else:
|
||||
# This looks like an extruder. Find the rest of the machine.
|
||||
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
||||
parent_machine = self.getParentMachineDefinition(whole_machine)
|
||||
if whole_machine is parent_machine:
|
||||
# This extruder already belongs to a 'parent' machine def.
|
||||
return machine_definition
|
||||
else:
|
||||
# Look up the corresponding extruder definition in the parent machine definition.
|
||||
extruder_position = machine_definition.getMetaDataEntry("position")
|
||||
parent_extruder_id = parent_machine.getMetaDataEntry("machine_extruder_trains")[extruder_position]
|
||||
return container_registry.findDefinitionContainers(id=parent_extruder_id)[0]
|
||||
|
||||
## Get the whole/global machine definition from an extruder definition.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||
# an extruder definition.
|
||||
# \return \type{DefinitionContainer}
|
||||
def getWholeMachineDefinition(self, machine_definition):
|
||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||
if machine_entry is None:
|
||||
# This already is a 'global' machine definition.
|
||||
return machine_definition
|
||||
else:
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
whole_machine = container_registry.findDefinitionContainers(id=machine_entry)[0]
|
||||
return whole_machine
|
|
@ -32,14 +32,27 @@ class SetParentOperation(Operation.Operation):
|
|||
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
|
||||
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())
|
||||
# Special casing for groups that have been removed.
|
||||
# In that case we want to put them back where they belong before checking the depth difference.
|
||||
# If we don't, we always get 0.
|
||||
old_parent = new_parent.callDecoration("getOldParent")
|
||||
if old_parent:
|
||||
new_parent.callDecoration("getNode").setParent(old_parent)
|
||||
|
||||
# Based on the depth difference, we need to do something different.
|
||||
depth_difference = current_parent.getDepth() - new_parent.getDepth()
|
||||
child_transformation = self._node.getLocalTransformation()
|
||||
if depth_difference > 0:
|
||||
parent_transformation = current_parent.getLocalTransformation()
|
||||
# A node in the chain was removed, so we need to squash the parent info into all the nodes, so positions remain the same.
|
||||
self._node.setTransformation(parent_transformation.multiply(child_transformation))
|
||||
else:
|
||||
# A node is inserted into the chain, so use the inverse of the parent to set the transformation of it's children.
|
||||
parent_transformation = new_parent.getLocalTransformation()
|
||||
result = parent_transformation.getInverse().multiply(child_transformation, copy = True)
|
||||
self._node.setTransformation(result)
|
||||
|
||||
self._node.setParent(new_parent)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import os.path
|
||||
import urllib
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal, QUrl, QVariant
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
import UM.PluginRegistry
|
||||
|
@ -14,6 +14,9 @@ import UM.MimeTypeDatabase
|
|||
import UM.Logger
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.QualityManager import QualityManager
|
||||
|
||||
from UM.MimeTypeDatabase import MimeTypeNotFoundError
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
@ -33,6 +36,7 @@ class ContainerManager(QObject):
|
|||
super().__init__(parent)
|
||||
|
||||
self._registry = ContainerRegistry.getInstance()
|
||||
self._machine_manager = Application.getInstance().getMachineManager()
|
||||
self._container_name_filters = {}
|
||||
|
||||
## Create a duplicate of the specified container
|
||||
|
@ -45,7 +49,7 @@ class ContainerManager(QObject):
|
|||
# \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)
|
||||
containers = self._container_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 ""
|
||||
|
@ -53,7 +57,7 @@ class ContainerManager(QObject):
|
|||
container = containers[0]
|
||||
|
||||
new_container = None
|
||||
new_name = self._registry.uniqueName(container.getName())
|
||||
new_name = self._container_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"):
|
||||
|
@ -64,7 +68,7 @@ class ContainerManager(QObject):
|
|||
new_container.setName(new_name)
|
||||
|
||||
if new_container:
|
||||
self._registry.addContainer(new_container)
|
||||
self._container_registry.addContainer(new_container)
|
||||
|
||||
return new_container.getId()
|
||||
|
||||
|
@ -77,24 +81,24 @@ class ContainerManager(QObject):
|
|||
# \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)
|
||||
containers = self._container_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)
|
||||
self._container_registry.removeContainer(container)
|
||||
|
||||
# Ensure we have a unique name for the container
|
||||
new_name = self._registry.uniqueName(new_name)
|
||||
new_name = self._container_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)
|
||||
self._container_registry.addContainer(container)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -105,12 +109,12 @@ class ContainerManager(QObject):
|
|||
# \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)
|
||||
containers = self._container_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())
|
||||
self._container_registry.removeContainer(containers[0].getId())
|
||||
|
||||
return True
|
||||
|
||||
|
@ -125,14 +129,14 @@ class ContainerManager(QObject):
|
|||
# \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)
|
||||
containers = self._container_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)
|
||||
containers = self._container_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
|
||||
|
@ -154,7 +158,7 @@ class ContainerManager(QObject):
|
|||
# \return True if successful, False if not.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def clearContainer(self, container_id):
|
||||
containers = self._registry.findContainers(None, id = container_id)
|
||||
containers = self._container_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
|
||||
|
@ -167,6 +171,19 @@ class ContainerManager(QObject):
|
|||
|
||||
return True
|
||||
|
||||
@pyqtSlot(str, str, result=str)
|
||||
def getContainerMetaDataEntry(self, container_id, entry_name):
|
||||
containers = self._container_registry.findContainers(None, id=container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could not get metadata of container %s because it was not found.", container_id)
|
||||
return ""
|
||||
|
||||
result = containers[0].getMetaDataEntry(entry_name)
|
||||
if result is not None:
|
||||
return str(result)
|
||||
else:
|
||||
return ""
|
||||
|
||||
## Set a metadata entry of the specified container.
|
||||
#
|
||||
# This will set the specified entry of the container's metadata to the specified
|
||||
|
@ -181,7 +198,7 @@ class ContainerManager(QObject):
|
|||
# \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)
|
||||
containers = self._container_registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could not set metadata of container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
@ -215,7 +232,7 @@ class ContainerManager(QObject):
|
|||
## Set the name of the specified container.
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def setContainerName(self, container_id, new_name):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
|
||||
containers = self._container_registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could not set name of container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
@ -240,20 +257,25 @@ class ContainerManager(QObject):
|
|||
@pyqtSlot("QVariantMap", result = "QVariantList")
|
||||
def findInstanceContainers(self, criteria):
|
||||
result = []
|
||||
for entry in self._registry.findInstanceContainers(**criteria):
|
||||
for entry in self._container_registry.findInstanceContainers(**criteria):
|
||||
result.append(entry.getId())
|
||||
|
||||
return result
|
||||
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isContainerUsed(self, container_id):
|
||||
UM.Logger.log("d", "Checking if container %s is currently used in the active stacks", container_id)
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
UM.Logger.log("d", "Checking if container %s is currently used", container_id)
|
||||
containers = self._container_registry.findContainerStacks()
|
||||
for stack in containers:
|
||||
if container_id in [child.getId() for child in stack.getContainers()]:
|
||||
UM.Logger.log("d", "The container is in use by %s", stack.getId())
|
||||
return True
|
||||
return False
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def makeUniqueName(self, original_name):
|
||||
return self._container_registry.uniqueName(original_name)
|
||||
|
||||
## 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
|
||||
|
@ -306,7 +328,7 @@ class ContainerManager(QObject):
|
|||
else:
|
||||
mime_type = self._container_name_filters[file_type]["mime"]
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = container_id)
|
||||
containers = self._container_registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
return { "status": "error", "message": "Container not found"}
|
||||
container = containers[0]
|
||||
|
@ -332,6 +354,9 @@ class ContainerManager(QObject):
|
|||
except NotImplementedError:
|
||||
return { "status": "error", "message": "Unable to serialize container"}
|
||||
|
||||
if contents is None:
|
||||
return {"status": "error", "message": "Serialization returned None. Unable to write to file"}
|
||||
|
||||
with UM.SaveFile(file_url, "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
@ -359,12 +384,12 @@ class ContainerManager(QObject):
|
|||
except MimeTypeNotFoundError:
|
||||
return { "status": "error", "message": "Could not determine mime type of file" }
|
||||
|
||||
container_type = ContainerRegistry.getContainerForMimeType(mime_type)
|
||||
container_type = self._container_registry.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 = ContainerRegistry.getInstance().uniqueName(container_id)
|
||||
container_id = self._container_registry.uniqueName(container_id)
|
||||
|
||||
container = container_type(container_id)
|
||||
|
||||
|
@ -376,7 +401,7 @@ class ContainerManager(QObject):
|
|||
|
||||
container.setName(container_id)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(container)
|
||||
self._container_registry.addContainer(container)
|
||||
|
||||
return { "status": "success", "message": "Successfully imported container {0}".format(container.getName()) }
|
||||
|
||||
|
@ -392,7 +417,7 @@ class ContainerManager(QObject):
|
|||
if not global_stack:
|
||||
return False
|
||||
|
||||
Application.getInstance().getMachineManager().blurSettings.emit()
|
||||
self._machine_manager.blurSettings.emit()
|
||||
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
# Find the quality_changes container for this stack and merge the contents of the top container into it.
|
||||
|
@ -403,18 +428,25 @@ class ContainerManager(QObject):
|
|||
|
||||
self._performMerge(quality_changes, stack.getTop())
|
||||
|
||||
Application.getInstance().getMachineManager().activeQualityChanged.emit()
|
||||
self._machine_manager.activeQualityChanged.emit()
|
||||
|
||||
return True
|
||||
|
||||
## Clear the top-most (user) containers of the active stacks.
|
||||
@pyqtSlot()
|
||||
def clearUserContainers(self):
|
||||
Application.getInstance().getMachineManager().blurSettings.emit()
|
||||
self._machine_manager.blurSettings.emit()
|
||||
|
||||
send_emits_containers = []
|
||||
|
||||
# Go through global and extruder stacks and clear their topmost container (the user settings).
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
stack.getTop().clear()
|
||||
container = stack.getTop()
|
||||
container.clear()
|
||||
send_emits_containers.append(container)
|
||||
|
||||
for container in send_emits_containers:
|
||||
container.sendPostponedEmits()
|
||||
|
||||
## Create quality changes containers from the user containers in the active stacks.
|
||||
#
|
||||
|
@ -423,20 +455,21 @@ class ContainerManager(QObject):
|
|||
# stack and clear the user settings.
|
||||
#
|
||||
# \return \type{bool} True if the operation was successfully, False if not.
|
||||
@pyqtSlot(result = bool)
|
||||
def createQualityChanges(self):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
@pyqtSlot(str, result = bool)
|
||||
def createQualityChanges(self, base_name):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return False
|
||||
|
||||
quality_container = global_stack.findContainer(type = "quality")
|
||||
if not quality_container:
|
||||
active_quality_name = self._machine_manager.activeQualityName
|
||||
if active_quality_name == "":
|
||||
UM.Logger.log("w", "No quality container found in stack %s, cannot create profile", global_stack.getId())
|
||||
return False
|
||||
|
||||
Application.getInstance().getMachineManager().blurSettings.emit()
|
||||
|
||||
unique_name = ContainerRegistry.getInstance().uniqueName(quality_container.getName())
|
||||
self._machine_manager.blurSettings.emit()
|
||||
if base_name is None or base_name == "":
|
||||
base_name = active_quality_name
|
||||
unique_name = self._container_registry.uniqueName(base_name)
|
||||
|
||||
# Go through the active stacks and create quality_changes containers from the user containers.
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
|
@ -447,13 +480,17 @@ class ContainerManager(QObject):
|
|||
UM.Logger.log("w", "No quality or quality changes container found in stack %s, ignoring it", stack.getId())
|
||||
continue
|
||||
|
||||
new_changes = self._createQualityChanges(quality_container, unique_name, stack.getId())
|
||||
extruder_id = None if stack is global_stack else QualityManager.getInstance().getParentMachineDefinition(stack.getBottom()).getId()
|
||||
new_changes = self._createQualityChanges(quality_container, unique_name,
|
||||
UM.Application.getInstance().getGlobalContainerStack().getBottom(),
|
||||
extruder_id)
|
||||
self._performMerge(new_changes, quality_changes_container, clear_settings = False)
|
||||
self._performMerge(new_changes, user_container)
|
||||
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_changes)
|
||||
self._container_registry.addContainer(new_changes)
|
||||
stack.replaceContainer(stack.getContainerIndex(quality_changes_container), new_changes)
|
||||
|
||||
UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
|
||||
self._machine_manager.activeQualityChanged.emit()
|
||||
return True
|
||||
|
||||
## Remove all quality changes containers matching a specified name.
|
||||
|
@ -473,13 +510,31 @@ class ContainerManager(QObject):
|
|||
if not quality_name:
|
||||
return containers_found # Without a name we will never find a container to remove.
|
||||
|
||||
for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
|
||||
# If the container that is being removed is the currently active quality, set another quality as the active quality
|
||||
activate_quality = quality_name == self._machine_manager.activeQualityName
|
||||
activate_quality_type = None
|
||||
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack or not quality_name:
|
||||
return ""
|
||||
machine_definition = global_stack.getBottom()
|
||||
|
||||
for container in QualityManager.getInstance().findQualityChangesByName(quality_name, machine_definition):
|
||||
containers_found = True
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
if activate_quality and not activate_quality_type:
|
||||
activate_quality_type = container.getMetaDataEntry("quality")
|
||||
self._container_registry.removeContainer(container.getId())
|
||||
|
||||
if not containers_found:
|
||||
UM.Logger.log("d", "Unable to remove quality containers, as we did not find any by the name of %s", quality_name)
|
||||
|
||||
elif activate_quality:
|
||||
definition_id = "fdmprinter" if not self._machine_manager.filterQualityByMachine else self._machine_manager.activeDefinitionId
|
||||
containers = self._container_registry.findInstanceContainers(type = "quality", definition = definition_id, quality_type = activate_quality_type)
|
||||
if containers:
|
||||
self._machine_manager.setActiveQuality(containers[0].getId())
|
||||
self._machine_manager.activeQualityChanged.emit()
|
||||
|
||||
return containers_found
|
||||
|
||||
## Rename a set of quality changes containers.
|
||||
|
@ -506,16 +561,22 @@ class ContainerManager(QObject):
|
|||
if not global_stack:
|
||||
return False
|
||||
|
||||
UM.Application.getInstance().getMachineManager().blurSettings.emit()
|
||||
self._machine_manager.blurSettings.emit()
|
||||
|
||||
new_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(new_name)
|
||||
new_name = self._container_registry.uniqueName(new_name)
|
||||
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
|
||||
container_registry = self._container_registry
|
||||
|
||||
containers_to_rename = self._container_registry.findInstanceContainers(type = "quality_changes", name = quality_name)
|
||||
|
||||
for container in containers_to_rename:
|
||||
stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
|
||||
container_registry.renameContainer(container.getId(), new_name, self._createUniqueId(stack_id, new_name))
|
||||
|
||||
UM.Application.getInstance().getMachineManager().activeQualityChanged.emit()
|
||||
if not containers_to_rename:
|
||||
UM.Logger.log("e", "Unable to rename %s, because we could not find the profile", quality_name)
|
||||
|
||||
self._machine_manager.activeQualityChanged.emit()
|
||||
return True
|
||||
|
||||
## Duplicate a specified set of quality or quality_changes containers.
|
||||
|
@ -527,41 +588,120 @@ class ContainerManager(QObject):
|
|||
# \param quality_name The name of the quality to duplicate.
|
||||
#
|
||||
# \return A string containing the name of the duplicated containers, or an empty string if it failed.
|
||||
@pyqtSlot(str, result = str)
|
||||
def duplicateQualityOrQualityChanges(self, quality_name):
|
||||
@pyqtSlot(str, str, result = str)
|
||||
def duplicateQualityOrQualityChanges(self, quality_name, base_name):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack or not quality_name:
|
||||
return ""
|
||||
machine_definition = global_stack.getBottom()
|
||||
|
||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||
material_containers = [stack.findContainer(type="material") for stack in active_stacks]
|
||||
|
||||
result = self._duplicateQualityOrQualityChangesForMachineType(quality_name, base_name,
|
||||
QualityManager.getInstance().getParentMachineDefinition(machine_definition),
|
||||
material_containers)
|
||||
return result[0].getName() if result else ""
|
||||
|
||||
## Duplicate a quality or quality changes profile specific to a machine type
|
||||
#
|
||||
# \param quality_name \type{str} the name of the quality or quality changes container to duplicate.
|
||||
# \param base_name \type{str} the desired name for the new container.
|
||||
# \param machine_definition \type{DefinitionContainer}
|
||||
# \param material_instances \type{List[InstanceContainer]}
|
||||
# \return \type{str} the name of the newly created container.
|
||||
def _duplicateQualityOrQualityChangesForMachineType(self, quality_name, base_name, machine_definition, material_instances):
|
||||
UM.Logger.log("d", "Attempting to duplicate the quality %s", quality_name)
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(name = quality_name)
|
||||
|
||||
if base_name is None:
|
||||
base_name = quality_name
|
||||
# Try to find a Quality with the name.
|
||||
container = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
|
||||
if container:
|
||||
UM.Logger.log("d", "We found a quality to duplicate.")
|
||||
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
|
||||
UM.Logger.log("d", "We found a quality_changes to duplicate.")
|
||||
# Assume it is a quality changes.
|
||||
return self._duplicateQualityChangesForMachineType(quality_name, base_name, machine_definition)
|
||||
|
||||
# Duplicate a quality profile
|
||||
def _duplicateQualityForMachineType(self, quality_container, base_name, machine_definition):
|
||||
if base_name is None:
|
||||
base_name = quality_container.getName()
|
||||
new_name = self._container_registry.uniqueName(base_name)
|
||||
|
||||
new_change_instances = []
|
||||
|
||||
# Handle the global stack first.
|
||||
global_changes = self._createQualityChanges(quality_container, new_name, machine_definition, None)
|
||||
new_change_instances.append(global_changes)
|
||||
self._container_registry.addContainer(global_changes)
|
||||
|
||||
# Handle the extruders if present.
|
||||
extruders = machine_definition.getMetaDataEntry("machine_extruder_trains")
|
||||
if extruders:
|
||||
for extruder_id in extruders:
|
||||
extruder = extruders[extruder_id]
|
||||
new_changes = self._createQualityChanges(quality_container, new_name, machine_definition, extruder)
|
||||
new_change_instances.append(new_changes)
|
||||
self._container_registry.addContainer(new_changes)
|
||||
|
||||
return new_change_instances
|
||||
|
||||
# Duplicate a quality changes container
|
||||
def _duplicateQualityChangesForMachineType(self, quality_changes_name, base_name, machine_definition):
|
||||
new_change_instances = []
|
||||
for container in QualityManager.getInstance().findQualityChangesByName(quality_changes_name,
|
||||
machine_definition):
|
||||
base_id = container.getMetaDataEntry("extruder")
|
||||
if not base_id:
|
||||
base_id = container.getDefinition().getId()
|
||||
new_unique_id = self._createUniqueId(base_id, base_name)
|
||||
new_container = container.duplicate(new_unique_id, base_name)
|
||||
new_change_instances.append(new_container)
|
||||
self._container_registry.addContainer(new_container)
|
||||
|
||||
return new_change_instances
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def duplicateMaterial(self, material_id):
|
||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||
if not containers:
|
||||
UM.Logger.log("d", "Unable to duplicate the quality %s, because it doesn't exist.", quality_name)
|
||||
UM.Logger.log("d", "Unable to duplicate the material with id %s, because it doesn't exist.", material_id)
|
||||
return ""
|
||||
|
||||
new_name = UM.Settings.ContainerRegistry.getInstance().uniqueName(quality_name)
|
||||
# Ensure all settings are saved.
|
||||
UM.Application.getInstance().saveSettings()
|
||||
|
||||
container_type = containers[0].getMetaDataEntry("type")
|
||||
if container_type == "quality":
|
||||
for container in self._getFilteredContainers(name = quality_name, type = "quality"):
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
new_changes = self._createQualityChanges(container, new_name, stack.getId())
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_changes)
|
||||
elif container_type == "quality_changes":
|
||||
for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
|
||||
stack_id = container.getMetaDataEntry("extruder", global_stack.getId())
|
||||
new_container = container.duplicate(self._createUniqueId(stack_id, new_name), new_name)
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
|
||||
else:
|
||||
return ""
|
||||
# Create a new ID & container to hold the data.
|
||||
new_id = self._container_registry.uniqueName(material_id)
|
||||
container_type = type(containers[0]) # Could be either a XMLMaterialProfile or a InstanceContainer
|
||||
duplicated_container = container_type(new_id)
|
||||
|
||||
return new_name
|
||||
# Instead of duplicating we load the data from the basefile again.
|
||||
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
|
||||
# are also correctly created.
|
||||
with open(containers[0].getPath(), encoding="utf-8") as f:
|
||||
duplicated_container.deserialize(f.read())
|
||||
duplicated_container.setDirty(True)
|
||||
self._container_registry.addContainer(duplicated_container)
|
||||
|
||||
## Get the singleton instance for this class.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||
if ContainerManager.__instance is None:
|
||||
ContainerManager.__instance = cls()
|
||||
return ContainerManager.__instance
|
||||
|
||||
__instance = None
|
||||
|
||||
# Factory function, used by QML
|
||||
@staticmethod
|
||||
def createContainerManager(engine, js_engine):
|
||||
return ContainerManager()
|
||||
return ContainerManager.getInstance()
|
||||
|
||||
def _performMerge(self, merge_into, merge):
|
||||
def _performMerge(self, merge_into, merge, clear_settings = True):
|
||||
assert isinstance(merge, type(merge_into))
|
||||
|
||||
if merge == merge_into:
|
||||
|
@ -570,11 +710,12 @@ class ContainerManager(QObject):
|
|||
for key in merge.getAllKeys():
|
||||
merge_into.setProperty(key, "value", merge.getProperty(key, "value"))
|
||||
|
||||
merge.clear()
|
||||
if clear_settings:
|
||||
merge.clear()
|
||||
|
||||
def _updateContainerNameFilters(self):
|
||||
self._container_name_filters = {}
|
||||
for plugin_id, container_type in UM.Settings.ContainerRegistry.getContainerTypes():
|
||||
for plugin_id, container_type in self._container_registry.getContainerTypes():
|
||||
# Ignore default container types since those are not plugins
|
||||
if container_type in (UM.Settings.InstanceContainer, UM.Settings.ContainerStack, UM.Settings.DefinitionContainer):
|
||||
continue
|
||||
|
@ -589,7 +730,7 @@ class ContainerManager(QObject):
|
|||
except KeyError as e:
|
||||
continue
|
||||
|
||||
mime_type = UM.Settings.ContainerRegistry.getMimeTypeForContainer(container_type)
|
||||
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||
|
||||
entry = {
|
||||
"type": serialize_type,
|
||||
|
@ -616,37 +757,13 @@ class ContainerManager(QObject):
|
|||
name_filter = "{0} ({1})".format(mime_type.comment, suffix_list)
|
||||
self._container_name_filters[name_filter] = entry
|
||||
|
||||
## Return a generator that iterates over a set of containers that are filtered by machine and material when needed.
|
||||
## Get containers filtered by machine type and material if required.
|
||||
#
|
||||
# \param kwargs Initial search criteria that the containers need to match.
|
||||
#
|
||||
# \return A generator that iterates over the list of containers matching the search criteria.
|
||||
# \return A list of containers matching the search criteria.
|
||||
def _getFilteredContainers(self, **kwargs):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return False
|
||||
|
||||
criteria = kwargs
|
||||
|
||||
filter_by_material = False
|
||||
|
||||
if global_stack.getMetaDataEntry("has_machine_quality"):
|
||||
criteria["definition"] = global_stack.getBottom().getId()
|
||||
|
||||
filter_by_material = global_stack.getMetaDataEntry("has_materials")
|
||||
|
||||
material_ids = []
|
||||
if filter_by_material:
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
material_ids.append(stack.findContainer(type = "material").getId())
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
for container in containers:
|
||||
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
|
||||
if filter_by_material and container.getMetaDataEntry("material") not in material_ids:
|
||||
continue
|
||||
|
||||
yield container
|
||||
return QualityManager.getInstance()._getFilteredContainers(**kwargs)
|
||||
|
||||
## Creates a unique ID for a container by prefixing the name with the stack ID.
|
||||
#
|
||||
|
@ -668,31 +785,73 @@ class ContainerManager(QObject):
|
|||
#
|
||||
# \param quality_container The quality container to create a changes container for.
|
||||
# \param new_name The name of the new quality_changes container.
|
||||
# \param stack_id The ID of the container stack the new container "belongs to". It is used primarily to ensure a unique ID.
|
||||
# \param machine_definition The machine definition this quality changes container is specific to.
|
||||
# \param extruder_id
|
||||
#
|
||||
# \return A new quality_changes container with the specified container as base.
|
||||
def _createQualityChanges(self, quality_container, new_name, stack_id):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
assert global_stack is not None
|
||||
def _createQualityChanges(self, quality_container, new_name, machine_definition, extruder_id):
|
||||
base_id = machine_definition.getId() if extruder_id is None else extruder_id
|
||||
|
||||
# Create a new quality_changes container for the quality.
|
||||
quality_changes = UM.Settings.InstanceContainer(self._createUniqueId(stack_id, new_name))
|
||||
quality_changes = InstanceContainer(self._createUniqueId(base_id, new_name))
|
||||
quality_changes.setName(new_name)
|
||||
quality_changes.addMetaDataEntry("type", "quality_changes")
|
||||
quality_changes.addMetaDataEntry("quality", quality_container.getMetaDataEntry("quality_type"))
|
||||
quality_changes.addMetaDataEntry("quality_type", quality_container.getMetaDataEntry("quality_type"))
|
||||
|
||||
# If we are creating a container for an extruder, ensure we add that to the container
|
||||
if stack_id != global_stack.getId():
|
||||
quality_changes.addMetaDataEntry("extruder", stack_id)
|
||||
if extruder_id is not None:
|
||||
quality_changes.addMetaDataEntry("extruder", extruder_id)
|
||||
|
||||
# If the machine specifies qualities should be filtered, ensure we match the current criteria.
|
||||
if not global_stack.getMetaDataEntry("has_machine_quality"):
|
||||
quality_changes.setDefinition(UM.Settings.ContainerRegistry.getInstance().findContainers(id = "fdmprinter")[0])
|
||||
if not machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
|
||||
else:
|
||||
quality_changes.setDefinition(global_stack.getBottom())
|
||||
|
||||
if global_stack.getMetaDataEntry("has_materials"):
|
||||
material = quality_container.getMetaDataEntry("material")
|
||||
quality_changes.addMetaDataEntry("material", material)
|
||||
|
||||
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
|
||||
return quality_changes
|
||||
|
||||
|
||||
## Import profiles from a list of file_urls.
|
||||
# Each QUrl item must end with .curaprofile, or it will not be imported.
|
||||
#
|
||||
# \param QVariant<QUrl>, essentially a list with QUrl objects.
|
||||
# \return Dict with keys status, text
|
||||
@pyqtSlot(QVariant, result="QVariantMap")
|
||||
def importProfiles(self, file_urls):
|
||||
status = "ok"
|
||||
results = {"ok": [], "error": []}
|
||||
for file_url in file_urls:
|
||||
if not file_url.isValid():
|
||||
continue
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
continue
|
||||
if not path.endswith(".curaprofile"):
|
||||
continue
|
||||
|
||||
single_result = self._container_registry.importProfile(path)
|
||||
if single_result["status"] == "error":
|
||||
status = "error"
|
||||
results[single_result["status"]].append(single_result["message"])
|
||||
|
||||
return {
|
||||
"status": status,
|
||||
"message": "\n".join(results["ok"] + results["error"])}
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result="QVariantMap")
|
||||
def importProfile(self, file_url):
|
||||
if not file_url.isValid():
|
||||
return
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return
|
||||
return self._container_registry.importProfile(path)
|
||||
|
||||
@pyqtSlot("QVariantList", QUrl, str)
|
||||
def exportProfile(self, instance_id, file_url, file_type):
|
||||
if not file_url.isValid():
|
||||
return
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return
|
||||
self._container_registry.exportProfile(instance_id, path, file_type)
|
||||
|
|
|
@ -31,7 +31,7 @@ class ContainerSettingsModel(ListModel):
|
|||
self._update()
|
||||
|
||||
def _update(self):
|
||||
self.clear()
|
||||
items = []
|
||||
|
||||
if len(self._container_ids) == 0:
|
||||
return
|
||||
|
@ -64,14 +64,15 @@ class ContainerSettingsModel(ListModel):
|
|||
else:
|
||||
values.append("")
|
||||
|
||||
self.appendItem({
|
||||
items.append({
|
||||
"key": key,
|
||||
"values": values,
|
||||
"label": definition.label,
|
||||
"unit": definition.unit,
|
||||
"category": category.label
|
||||
})
|
||||
self.sort(lambda k: (k["category"], k["key"]))
|
||||
items.sort(key = lambda k: (k["category"], k["key"]))
|
||||
self.setItems(items)
|
||||
|
||||
## Set the ids of the containers which have the settings this model should list.
|
||||
# Also makes sure the model updates when the containers have property changes
|
||||
|
|
|
@ -16,6 +16,9 @@ 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 cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -81,11 +84,26 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if result == QMessageBox.No:
|
||||
return
|
||||
found_containers = []
|
||||
extruder_positions = []
|
||||
for instance_id in instance_ids:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id=instance_id)
|
||||
if containers:
|
||||
found_containers.append(containers[0])
|
||||
|
||||
# Determine the position of the extruder of this container
|
||||
extruder_id = containers[0].getMetaDataEntry("extruder", "")
|
||||
if extruder_id == "":
|
||||
# Global stack
|
||||
extruder_positions.append(-1)
|
||||
else:
|
||||
extruder_containers = ContainerRegistry.getInstance().findDefinitionContainers(id=extruder_id)
|
||||
if extruder_containers:
|
||||
extruder_positions.append(int(extruder_containers[0].getMetaDataEntry("position", 0)))
|
||||
else:
|
||||
extruder_positions.append(0)
|
||||
# Ensure the profiles are always exported in order (global, extruder 0, extruder 1, ...)
|
||||
found_containers = [containers for (positions, containers) in sorted(zip(extruder_positions, found_containers))]
|
||||
|
||||
profile_writer = self._findProfileWriter(extension, description)
|
||||
|
||||
try:
|
||||
|
@ -124,49 +142,91 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
# \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):
|
||||
Logger.log("d", "Attempting to import profile %s", 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()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return
|
||||
|
||||
machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
|
||||
machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position"))
|
||||
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
|
||||
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.
|
||||
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))
|
||||
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
|
||||
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name,profile_reader.getPluginId(), str(e))
|
||||
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]
|
||||
new_name = self.uniqueName(name_seed)
|
||||
if type(profile_or_list) is not list:
|
||||
profile = profile_or_list
|
||||
self._configureProfile(profile, name_seed)
|
||||
self._configureProfile(profile, name_seed, new_name)
|
||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
|
||||
else:
|
||||
profile_index = -1
|
||||
global_profile = None
|
||||
|
||||
for profile in profile_or_list:
|
||||
if profile.getId() != "":
|
||||
ContainerRegistry.getInstance().addContainer(profile)
|
||||
if profile_index >= 0:
|
||||
if len(machine_extruders) > profile_index:
|
||||
extruder_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_extruders[profile_index].getBottom())
|
||||
# Ensure the extruder profiles get non-conflicting names
|
||||
# NB: these are not user-facing
|
||||
if "extruder" in profile.getMetaData():
|
||||
profile.setMetaDataEntry("extruder", extruder_id)
|
||||
else:
|
||||
profile.addMetaDataEntry("extruder", extruder_id)
|
||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||
elif profile_index == 0:
|
||||
# Importing a multiextrusion profile into a single extrusion machine; merge 1st extruder profile into global profile
|
||||
profile._id = self.uniqueName("temporary_profile")
|
||||
self.addContainer(profile)
|
||||
ContainerManager.getInstance().mergeContainers(global_profile.getId(), profile.getId())
|
||||
self.removeContainer(profile.getId())
|
||||
break
|
||||
else:
|
||||
# The imported composite profile has a profile for an extruder that this machine does not have. Ignore this extruder-profile
|
||||
break
|
||||
else:
|
||||
self._configureProfile(profile, name_seed)
|
||||
global_profile = profile
|
||||
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
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) }
|
||||
self._configureProfile(profile, profile_id, new_name)
|
||||
|
||||
#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)}
|
||||
profile_index += 1
|
||||
|
||||
def _configureProfile(self, profile, name_seed):
|
||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
||||
|
||||
# If it hasn't returned by now, none of the plugins loaded the profile successfully.
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
||||
|
||||
def _configureProfile(self, profile, id_seed, new_name):
|
||||
profile.setReadOnly(False)
|
||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||
|
||||
new_name = self.createUniqueName("quality_changes", "", name_seed, catalog.i18nc("@label", "Custom profile"))
|
||||
new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile"))
|
||||
profile._id = new_id
|
||||
profile.setName(new_name)
|
||||
profile._id = new_name
|
||||
|
||||
if "type" in profile.getMetaData():
|
||||
profile.setMetaDataEntry("type", "quality_changes")
|
||||
else:
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
|
||||
if self._machineHasOwnQualities():
|
||||
profile.setDefinition(self._activeDefinition())
|
||||
profile.setDefinition(self._activeQualityDefinition())
|
||||
if self._machineHasOwnMaterials():
|
||||
profile.addMetaDataEntry("material", self._activeMaterialId())
|
||||
else:
|
||||
|
@ -187,12 +247,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
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):
|
||||
## Get the definition to use to select quality profiles for the active machine
|
||||
# \return the active quality definition object or None if there is no quality definition
|
||||
def _activeQualityDefinition(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
definition = global_container_stack.getBottom()
|
||||
definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(global_container_stack.getBottom())
|
||||
definition = self.findDefinitionContainers(id=definition_id)[0]
|
||||
|
||||
if definition:
|
||||
return definition
|
||||
return None
|
||||
|
|
|
@ -5,6 +5,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject, QVariant #
|
|||
|
||||
from UM.Application import Application #To get the global container stack to find the current machine.
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers by ID.
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
@ -16,21 +18,23 @@ from typing import Optional
|
|||
#
|
||||
# This keeps a list of extruder stacks for each machine.
|
||||
class ExtruderManager(QObject):
|
||||
## Signal to notify other components when the list of extruders changes.
|
||||
## Signal to notify other components when the list of extruders for a machine definition changes.
|
||||
extrudersChanged = pyqtSignal(QVariant)
|
||||
|
||||
## Signal to notify other components when the global container stack is switched to a definition
|
||||
# that has different extruders than the previous global container stack
|
||||
globalContainerStackDefinitionChanged = pyqtSignal()
|
||||
|
||||
## Notify when the user switches the currently active extruder.
|
||||
activeExtruderChanged = pyqtSignal()
|
||||
|
||||
## Registers listeners and such to listen to changes to the extruders.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Per machine, a dictionary of extruder container stack IDs.
|
||||
self._extruder_trains = {} # type: Dict[str, Dict[str, ContainerStack]]
|
||||
|
||||
self._active_extruder_index = 0 # type: int
|
||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
||||
self._active_extruder_index = 0
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
||||
self._global_container_stack_definition_id = None
|
||||
self._addCurrentMachineExtruders()
|
||||
|
||||
## Gets the unique identifier of the currently active extruder stack.
|
||||
|
@ -49,10 +53,13 @@ class ExtruderManager(QObject):
|
|||
return None
|
||||
|
||||
@pyqtProperty(int, notify = extrudersChanged)
|
||||
def extruderCount(self) -> int:
|
||||
def extruderCount(self):
|
||||
if not Application.getInstance().getGlobalContainerStack():
|
||||
return 0 # No active machine, so no extruders.
|
||||
return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
|
||||
return 0 # No active machine, so no extruders.
|
||||
try:
|
||||
return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()])
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=extrudersChanged)
|
||||
def extruderIds(self):
|
||||
|
@ -142,11 +149,8 @@ class ExtruderManager(QObject):
|
|||
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():
|
||||
shallow_stack = ContainerStack(machine_id + "_shallow")
|
||||
shallow_stack.addContainer(machine_definition)
|
||||
extruder_train.setNextStack(shallow_stack)
|
||||
# regardless of what the next stack is, we have to set it again, because of signal routing.
|
||||
extruder_train.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
changed = True
|
||||
if changed:
|
||||
self.extrudersChanged.emit(machine_id)
|
||||
|
@ -167,7 +171,7 @@ class ExtruderManager(QObject):
|
|||
position, machine_id: str) -> None:
|
||||
# Cache some things.
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
machine_definition_id = machine_definition.getId()
|
||||
machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(machine_definition)
|
||||
|
||||
# Create a container stack for this extruder.
|
||||
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
|
||||
|
@ -187,7 +191,7 @@ class ExtruderManager(QObject):
|
|||
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")
|
||||
preferred_variants = container_registry.findInstanceContainers(id = preferred_variant_id, definition = machine_definition_id, type = "variant")
|
||||
if len(preferred_variants) >= 1:
|
||||
variant = preferred_variants[0]
|
||||
else:
|
||||
|
@ -199,7 +203,8 @@ class ExtruderManager(QObject):
|
|||
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":
|
||||
machine_has_variant_materials = machine_definition.getMetaDataEntry("has_variant_materials", default = False)
|
||||
if machine_has_variant_materials or machine_has_variant_materials == "True":
|
||||
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id, variant = variant.getId())
|
||||
else:
|
||||
materials = container_registry.findInstanceContainers(type = "material", definition = machine_definition_id)
|
||||
|
@ -209,7 +214,7 @@ class ExtruderManager(QObject):
|
|||
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
|
||||
search_criteria["definition"] = machine_definition_id
|
||||
|
||||
if machine_definition.getMetaDataEntry("has_variants") and variant:
|
||||
search_criteria["variant"] = variant.id
|
||||
|
@ -229,7 +234,7 @@ class ExtruderManager(QObject):
|
|||
|
||||
search_criteria = { "type": "quality" }
|
||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = machine_definition.id
|
||||
search_criteria["definition"] = machine_definition_id
|
||||
if machine_definition.getMetaDataEntry("has_materials") and material:
|
||||
search_criteria["material"] = material.id
|
||||
else:
|
||||
|
@ -241,9 +246,9 @@ class ExtruderManager(QObject):
|
|||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if not containers and preferred_quality:
|
||||
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 = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
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 = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
quality = containers[0]
|
||||
|
||||
|
@ -263,14 +268,84 @@ class ExtruderManager(QObject):
|
|||
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():
|
||||
shallow_stack = ContainerStack(machine_id + "_shallow")
|
||||
shallow_stack.addContainer(machine_definition)
|
||||
container_stack.setNextStack(shallow_stack)
|
||||
# regardless of what the next stack is, we have to set it again, because of signal routing.
|
||||
container_stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
|
||||
container_registry.addContainer(container_stack)
|
||||
|
||||
def getAllExtruderValues(self, setting_key):
|
||||
return self.getAllExtruderSettings(setting_key, "value")
|
||||
|
||||
## Gets a property of a setting for all extruders.
|
||||
#
|
||||
# \param setting_key \type{str} The setting to get the property of.
|
||||
# \param property \type{str} The property to get.
|
||||
# \return \type{List} the list of results
|
||||
def getAllExtruderSettings(self, setting_key, property):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
|
||||
return [global_container_stack.getProperty(setting_key, property)]
|
||||
|
||||
result = []
|
||||
for index in self.extruderIds:
|
||||
extruder_stack_id = self.extruderIds[str(index)]
|
||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
result.append(stack.getProperty(setting_key, property))
|
||||
return result
|
||||
|
||||
## Gets the extruder stacks that are actually being used at the moment.
|
||||
#
|
||||
# An extruder stack is being used if it is the extruder to print any mesh
|
||||
# with, or if it is the support infill extruder, the support interface
|
||||
# extruder, or the bed adhesion extruder.
|
||||
#
|
||||
# If there are no extruders, this returns the global stack as a singleton
|
||||
# list.
|
||||
#
|
||||
# \return A list of extruder stacks.
|
||||
def getUsedExtruderStacks(self):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
if global_stack.getProperty("machine_extruder_count", "value") <= 1: #For single extrusion.
|
||||
return [global_stack]
|
||||
|
||||
used_extruder_stack_ids = set()
|
||||
|
||||
#Get the extruders of all meshes in the scene.
|
||||
support_enabled = False
|
||||
support_interface_enabled = False
|
||||
scene_root = Application.getInstance().getController().getScene().getRoot()
|
||||
meshes = [node for node in DepthFirstIterator(scene_root) if type(node) is SceneNode and node.isSelectable()] #Only use the nodes that will be printed.
|
||||
for mesh in meshes:
|
||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||
if not extruder_stack_id: #No per-object settings for this node.
|
||||
extruder_stack_id = self.extruderIds["0"]
|
||||
used_extruder_stack_ids.add(extruder_stack_id)
|
||||
|
||||
#Get whether any of them use support.
|
||||
per_mesh_stack = mesh.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
support_enabled |= per_mesh_stack.getProperty("support_enable", "value")
|
||||
support_interface_enabled |= per_mesh_stack.getProperty("support_interface_enable", "value")
|
||||
else: #Take the setting from the build extruder stack.
|
||||
extruder_stack = container_registry.findContainerStacks(id = extruder_stack_id)[0]
|
||||
support_enabled |= extruder_stack.getProperty("support_enable", "value")
|
||||
support_interface_enabled |= extruder_stack.getProperty("support_enable", "value")
|
||||
|
||||
#The support extruders.
|
||||
if support_enabled:
|
||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_infill_extruder_nr", "value"))])
|
||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_extruder_nr_layer_0", "value"))])
|
||||
if support_interface_enabled:
|
||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("support_interface_extruder_nr", "value"))])
|
||||
|
||||
#The platform adhesion extruder. Not used if using none.
|
||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||
used_extruder_stack_ids.add(self.extruderIds[str(global_stack.getProperty("adhesion_extruder_nr", "value"))])
|
||||
|
||||
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||
|
||||
## Removes the container stack and user profile for the extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to remove the extruders for.
|
||||
|
@ -290,22 +365,32 @@ class ExtruderManager(QObject):
|
|||
return []
|
||||
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
|
||||
|
||||
## Returns a generator that will iterate over the global stack and per-extruder stacks.
|
||||
## Returns a list containing the global stack and active extruder stacks.
|
||||
#
|
||||
# The first generated element is the global container stack. After that any extruder stacks are generated.
|
||||
# The first element is the global container stack, followed by any extruder stacks.
|
||||
# \return \type{List[ContainerStack]}
|
||||
def getActiveGlobalAndExtruderStacks(self):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
return None
|
||||
|
||||
yield global_stack
|
||||
result = [global_stack]
|
||||
result.extend(self.getActiveExtruderStacks())
|
||||
return result
|
||||
|
||||
global_id = global_stack.getId()
|
||||
for name in self._extruder_trains[global_id]:
|
||||
yield self._extruder_trains[global_id][name]
|
||||
## Returns the list of active extruder stacks.
|
||||
#
|
||||
# \return \type{List[ContainerStack]} a list of
|
||||
def getActiveExtruderStacks(self):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
return list(self._extruder_trains[global_stack.getId()].values()) if global_stack else []
|
||||
|
||||
def __globalContainerStackChanged(self) -> None:
|
||||
self._addCurrentMachineExtruders()
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack and global_container_stack.getBottom() and global_container_stack.getBottom().getId() != self._global_container_stack_definition_id:
|
||||
self._global_container_stack_definition_id = global_container_stack.getBottom().getId()
|
||||
self.globalContainerStackDefinitionChanged.emit()
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
## Adds the extruders of the currently active machine.
|
||||
|
@ -330,7 +415,7 @@ class ExtruderManager(QObject):
|
|||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||
value = extruder.getRawProperty(key, "value")
|
||||
|
||||
if not value:
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if isinstance(value, SettingFunction):
|
||||
|
@ -350,7 +435,7 @@ class ExtruderManager(QObject):
|
|||
# \param key The key of the setting to retieve values for.
|
||||
#
|
||||
# \return String representing the extruder values
|
||||
@pyqtSlot(str, result="QList<int>")
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def getInstanceExtruderValues(self, key):
|
||||
return ExtruderManager.getExtruderValues(key)
|
||||
|
||||
|
@ -375,3 +460,29 @@ class ExtruderManager(QObject):
|
|||
value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value")
|
||||
|
||||
return value
|
||||
|
||||
## Get the resolve value or value for a given key
|
||||
#
|
||||
# This is the effective value for a given key, it is used for values in the global stack.
|
||||
# This is exposed to SettingFunction to use in value functions.
|
||||
# \param key The key of the setting to get the value of.
|
||||
#
|
||||
# \return The effective value
|
||||
@staticmethod
|
||||
def getResolveOrValue(key):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
resolved_value = global_stack.getProperty(key, "resolve")
|
||||
if resolved_value is not None:
|
||||
user_container = global_stack.findContainer({"type": "user"})
|
||||
quality_changes_container = global_stack.findContainer({"type": "quality_changes"})
|
||||
if user_container.hasProperty(key, "value") or quality_changes_container.hasProperty(key, "value"):
|
||||
# Normal case
|
||||
value = global_stack.getProperty(key, "value")
|
||||
else:
|
||||
# We have a resolved value and we're using it because of no user and quality_changes value
|
||||
value = resolved_value
|
||||
else:
|
||||
value = global_stack.getRawProperty(key, "value")
|
||||
|
||||
return value
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
|
||||
import UM.Qt.ListModel
|
||||
from UM.Application import Application
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
|
@ -29,6 +30,9 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
# containers.
|
||||
IndexRole = Qt.UserRole + 4
|
||||
|
||||
# The ID of the definition of the extruder.
|
||||
DefinitionRole = Qt.UserRole + 5
|
||||
|
||||
## List of colours to display if there is no material or the material has no known
|
||||
# colour.
|
||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||
|
@ -44,14 +48,16 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.ColorRole, "color")
|
||||
self.addRoleName(self.IndexRole, "index")
|
||||
self.addRoleName(self.DefinitionRole, "definition")
|
||||
|
||||
self._add_global = False
|
||||
self._simple_names = False
|
||||
|
||||
self._active_extruder_stack = None
|
||||
|
||||
#Listen to changes.
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._updateExtruders)
|
||||
manager = ExtruderManager.getInstance()
|
||||
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
|
||||
|
||||
self._updateExtruders()
|
||||
|
||||
|
@ -70,6 +76,21 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
def addGlobal(self):
|
||||
return self._add_global
|
||||
|
||||
## Set the simpleNames property.
|
||||
def setSimpleNames(self, simple_names):
|
||||
if simple_names != self._simple_names:
|
||||
self._simple_names = simple_names
|
||||
self.simpleNamesChanged.emit()
|
||||
self._updateExtruders()
|
||||
|
||||
## Emitted when the simpleNames property changes.
|
||||
simpleNamesChanged = pyqtSignal()
|
||||
|
||||
## Whether or not the model should show all definitions regardless of visibility.
|
||||
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
|
||||
def simpleNames(self):
|
||||
return self._simple_names
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
manager = ExtruderManager.getInstance()
|
||||
active_extruder_stack = manager.getActiveExtruderStack()
|
||||
|
@ -97,10 +118,11 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
changed = False
|
||||
|
||||
if self.rowCount() != 0:
|
||||
self.clear()
|
||||
changed = True
|
||||
|
||||
global_container_stack = UM.Application.Application.getInstance().getGlobalContainerStack()
|
||||
items = []
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if self._add_global:
|
||||
material = global_container_stack.findContainer({ "type": "material" })
|
||||
|
@ -109,16 +131,17 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
"id": global_container_stack.getId(),
|
||||
"name": "Global",
|
||||
"color": color,
|
||||
"index": -1
|
||||
"index": -1,
|
||||
"definition": ""
|
||||
}
|
||||
self.appendItem(item)
|
||||
items.append(item)
|
||||
changed = True
|
||||
|
||||
manager = ExtruderManager.getInstance()
|
||||
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
|
||||
extruder_name = extruder.getName()
|
||||
material = extruder.findContainer({ "type": "material" })
|
||||
if material:
|
||||
if material and not self._simple_names:
|
||||
extruder_name = "%s (%s)" % (material.getName(), extruder_name)
|
||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||
try:
|
||||
|
@ -131,11 +154,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
|||
"id": extruder.getId(),
|
||||
"name": extruder_name,
|
||||
"color": color,
|
||||
"index": position
|
||||
"index": position,
|
||||
"definition": extruder.getBottom().getId()
|
||||
}
|
||||
self.appendItem(item)
|
||||
items.append(item)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
self.sort(lambda item: item["index"])
|
||||
items.sort(key = lambda i: i["index"])
|
||||
self.setItems(items)
|
||||
self.modelChanged.emit()
|
||||
|
|
|
@ -4,23 +4,26 @@ from typing import Union
|
|||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from UM import Util
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingRelation import RelationType
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
|
||||
from cura.QualityManager import QualityManager
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import time
|
||||
import os
|
||||
|
||||
class MachineManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
|
@ -35,7 +38,7 @@ class MachineManager(QObject):
|
|||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self._active_stack_valid = None
|
||||
self._stacks_have_errors = None
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
|
@ -46,8 +49,9 @@ class MachineManager(QObject):
|
|||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||
self.globalValueChanged.connect(self.activeStackChanged)
|
||||
self.globalValueChanged.connect(self.activeStackValueChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeStackChanged)
|
||||
self.activeStackChanged.connect(self.activeStackValueChanged)
|
||||
|
||||
self._empty_variant_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
|
||||
self._empty_material_container = ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
|
||||
|
@ -73,14 +77,19 @@ class MachineManager(QObject):
|
|||
self._auto_materials_changed = {}
|
||||
self._auto_hotends_changed = {}
|
||||
|
||||
globalContainerChanged = pyqtSignal()
|
||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||
"The selected material is incompatible with the selected machine or configuration."))
|
||||
|
||||
globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value)
|
||||
activeMaterialChanged = pyqtSignal()
|
||||
activeVariantChanged = pyqtSignal()
|
||||
activeQualityChanged = pyqtSignal()
|
||||
activeStackChanged = pyqtSignal()
|
||||
activeStackChanged = pyqtSignal() # Emitted whenever the active stack is changed (ie: when changing between extruders, changing a profile, but not when changing a value)
|
||||
|
||||
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
|
||||
activeValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||
activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed.
|
||||
activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed
|
||||
stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed
|
||||
|
||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||
|
||||
|
@ -126,29 +135,13 @@ class MachineManager(QObject):
|
|||
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()
|
||||
definition_id = self.activeQualityDefinitionId
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
|
||||
if containers: # New material ID is known
|
||||
|
@ -159,9 +152,18 @@ class MachineManager(QObject):
|
|||
matching_extruder = extruder
|
||||
break
|
||||
|
||||
if matching_extruder and matching_extruder.findContainer({"type":"material"}).getMetaDataEntry("GUID") != material_id:
|
||||
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()
|
||||
variant_container = matching_extruder.findContainer({"type": "variant"})
|
||||
if self._global_container_stack.getBottom().getMetaDataEntry("has_variants") and variant_container:
|
||||
variant_id = self.getQualityVariantId(self._global_container_stack.getBottom(), variant_container)
|
||||
for container in containers:
|
||||
if container.getMetaDataEntry("variant") == variant_id:
|
||||
self._auto_materials_changed[str(index)] = container.getId()
|
||||
break
|
||||
else:
|
||||
# Just use the first result we found.
|
||||
self._auto_materials_changed[str(index)] = containers[0].getId()
|
||||
self._printer_output_devices[0].materialHotendChangedMessage(self._materialHotendChangedCallback)
|
||||
else:
|
||||
Logger.log("w", "No material definition found for printer definition %s and GUID %s" % (definition_id, material_id))
|
||||
|
@ -191,6 +193,22 @@ class MachineManager(QObject):
|
|||
if old_index is not None:
|
||||
extruder_manager.setActiveExtruderIndex(old_index)
|
||||
|
||||
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 _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.nameChanged.disconnect(self._onMachineNameChanged)
|
||||
|
@ -213,14 +231,38 @@ class MachineManager(QObject):
|
|||
self._global_container_stack.nameChanged.connect(self._onMachineNameChanged)
|
||||
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
|
||||
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
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)
|
||||
if self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
# For multi-extrusion machines, we do not want variant or material profiles in the stack,
|
||||
# because these are extruder specific and may cause wrong values to be used for extruders
|
||||
# that did not specify a value in the extruder.
|
||||
global_variant = self._global_container_stack.findContainer(type = "variant")
|
||||
if global_variant != self._empty_variant_container:
|
||||
self._global_container_stack.replaceContainer(self._global_container_stack.getContainerIndex(global_variant), self._empty_variant_container)
|
||||
|
||||
global_material = self._global_container_stack.findContainer(type = "material")
|
||||
if global_material != self._empty_material_container:
|
||||
self._global_container_stack.replaceContainer(self._global_container_stack.getContainerIndex(global_material), self._empty_material_container)
|
||||
|
||||
else:
|
||||
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)
|
||||
|
||||
|
||||
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
|
||||
def _updateStacksHaveErrors(self):
|
||||
old_stacks_have_errors = self._stacks_have_errors
|
||||
self._stacks_have_errors = self._checkStacksHaveErrors()
|
||||
if old_stacks_have_errors != self._stacks_have_errors:
|
||||
self.stacksValidationChanged.emit()
|
||||
|
||||
def _onActiveExtruderStackChanged(self):
|
||||
self.blurSettings.emit() # Ensure no-one has focus.
|
||||
old_active_container_stack = self._active_container_stack
|
||||
|
||||
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._onPropertyChanged)
|
||||
|
@ -230,8 +272,13 @@ class MachineManager(QObject):
|
|||
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
else:
|
||||
self._active_container_stack = self._global_container_stack
|
||||
self._active_stack_valid = not self._checkStackForErrors(self._active_container_stack)
|
||||
self.activeValidationChanged.emit()
|
||||
|
||||
self._updateStacksHaveErrors()
|
||||
|
||||
if old_active_container_stack != self._active_container_stack:
|
||||
# Many methods and properties related to the active quality actually depend
|
||||
# on _active_container_stack. If it changes, then the properties change.
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
def _onInstanceContainersChanged(self, container):
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
@ -245,35 +292,22 @@ class MachineManager(QObject):
|
|||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if property_name == "value":
|
||||
# If a setting is not settable per extruder, but "has enabled relations" that are settable per extruder
|
||||
# we need to copy the value to global, so that the front-end displays the right settings.
|
||||
if not self._active_container_stack.getProperty(key, "settable_per_extruder"):
|
||||
relations = self._global_container_stack.getBottom()._getDefinition(key).relations
|
||||
for relation in filter(lambda r: r.role == "enabled" and r.type == RelationType.RequiredByTarget, relations):
|
||||
# Target setting is settable per extruder
|
||||
if self._active_container_stack.getProperty(relation.target.key, "settable_per_extruder"):
|
||||
new_value = self._global_container_stack.getProperty(key, "value")
|
||||
stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())]
|
||||
for extruder_stack in stacks:
|
||||
if extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.getTop().setProperty(key, "value", new_value)
|
||||
break
|
||||
# Notify UI items, such as the "changed" star in profile pull down menu.
|
||||
self.activeStackValueChanged.emit()
|
||||
|
||||
if property_name == "validationState":
|
||||
if self._active_stack_valid:
|
||||
if not self._stacks_have_errors:
|
||||
# fast update, we only have to look at the current changed property
|
||||
if self._active_container_stack.getProperty(key, "settable_per_extruder"):
|
||||
changed_validation_state = self._active_container_stack.getProperty(key, property_name)
|
||||
else:
|
||||
changed_validation_state = self._global_container_stack.getProperty(key, property_name)
|
||||
if changed_validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
self._active_stack_valid = False
|
||||
self.activeValidationChanged.emit()
|
||||
self._stacks_have_errors = True
|
||||
self.stacksValidationChanged.emit()
|
||||
else:
|
||||
if not self._checkStackForErrors(self._active_container_stack) and not self._checkStackForErrors(self._global_container_stack):
|
||||
self._active_stack_valid = True
|
||||
self.activeValidationChanged.emit()
|
||||
|
||||
self.activeStackChanged.emit()
|
||||
# Normal check
|
||||
self._updateStacksHaveErrors()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveMachine(self, stack_id: str) -> None:
|
||||
|
@ -328,15 +362,17 @@ class MachineManager(QObject):
|
|||
Logger.log('d', str(ContainerRegistry.getInstance()))
|
||||
return 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
|
||||
def _checkStacksHaveErrors(self):
|
||||
if self._global_container_stack is not None and self._global_container_stack.hasErrors():
|
||||
return True
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
if self._global_container_stack is None:
|
||||
return False
|
||||
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
for stack in stacks:
|
||||
if stack.hasErrors():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
|
||||
|
@ -350,7 +386,7 @@ class MachineManager(QObject):
|
|||
user_settings.clear()
|
||||
|
||||
## Check if the global_container has instances in the user container
|
||||
@pyqtProperty(bool, notify = activeStackChanged)
|
||||
@pyqtProperty(bool, notify = activeStackValueChanged)
|
||||
def hasUserSettings(self):
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
|
@ -358,18 +394,50 @@ class MachineManager(QObject):
|
|||
if self._global_container_stack.getTop().findInstances():
|
||||
return True
|
||||
|
||||
for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()):
|
||||
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
for stack in stacks:
|
||||
if stack.getTop().findInstances():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
## 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) -> bool:
|
||||
return bool(self._active_stack_valid)
|
||||
## Delete a user setting from the global stack and all extruder stacks.
|
||||
# \param key \type{str} the name of the key to delete
|
||||
@pyqtSlot(str)
|
||||
def clearUserSettingAllCurrentStacks(self, key):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
send_emits_containers = []
|
||||
|
||||
top_container = self._global_container_stack.getTop()
|
||||
top_container.removeInstance(key, postpone_emit=True)
|
||||
send_emits_containers.append(top_container)
|
||||
|
||||
linked = not self._global_container_stack.getProperty(key, "settable_per_extruder") or \
|
||||
self._global_container_stack.getProperty(key, "limit_to_extruder") != "-1"
|
||||
|
||||
if not linked:
|
||||
stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
stacks = [stack]
|
||||
else:
|
||||
stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
|
||||
|
||||
for stack in stacks:
|
||||
if stack is not None:
|
||||
container = stack.getTop()
|
||||
container.removeInstance(key, postpone_emit=True)
|
||||
send_emits_containers.append(container)
|
||||
|
||||
for container in send_emits_containers:
|
||||
container.sendPostponedEmits()
|
||||
|
||||
## Check if none of the stacks contain error states
|
||||
# Note that the _stacks_have_errors is cached due to performance issues
|
||||
# Calling _checkStack(s)ForErrors on every change is simply too expensive
|
||||
@pyqtProperty(bool, notify = stacksValidationChanged)
|
||||
def stacksHaveErrors(self):
|
||||
return bool(self._stacks_have_errors)
|
||||
|
||||
@pyqtProperty(str, notify = activeStackChanged)
|
||||
def activeUserProfileId(self) -> str:
|
||||
|
@ -433,19 +501,55 @@ class MachineManager(QObject):
|
|||
|
||||
return result
|
||||
|
||||
## Gets the layer height of the currently active quality profile.
|
||||
#
|
||||
# This is indicated together with the name of the active quality profile.
|
||||
#
|
||||
# \return The layer height of the currently active quality profile. If
|
||||
# there is no quality profile, this returns 0.
|
||||
@pyqtProperty(float, notify=activeQualityChanged)
|
||||
def activeQualityLayerHeight(self):
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
|
||||
quality_changes = self._global_container_stack.findContainer({"type": "quality_changes"})
|
||||
if quality_changes:
|
||||
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = quality_changes.getId())
|
||||
if isinstance(value, SettingFunction):
|
||||
value = value(self._global_container_stack)
|
||||
return value
|
||||
quality = self._global_container_stack.findContainer({"type": "quality"})
|
||||
if quality:
|
||||
value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = quality.getId())
|
||||
if isinstance(value, SettingFunction):
|
||||
value = value(self._global_container_stack)
|
||||
return value
|
||||
|
||||
return 0 #No quality profile.
|
||||
|
||||
## Get the Material ID associated with the currently active material
|
||||
# \returns MaterialID (string) if found, empty string otherwise
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityMaterialId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if quality:
|
||||
return quality.getMetaDataEntry("material")
|
||||
material_id = quality.getMetaDataEntry("material")
|
||||
if material_id:
|
||||
# if the currently active machine inherits its qualities from a different machine
|
||||
# definition, make sure to return a material that is relevant to that machine definition
|
||||
definition_id = self.activeDefinitionId
|
||||
quality_definition_id = self.activeQualityDefinitionId
|
||||
if definition_id != quality_definition_id:
|
||||
material_id = material_id.replace(definition_id, quality_definition_id, 1)
|
||||
|
||||
return material_id
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityName(self):
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality_changes"})
|
||||
if self._active_container_stack and self._global_container_stack:
|
||||
quality = self._global_container_stack.findContainer({"type": "quality_changes"})
|
||||
if quality and quality != self._empty_quality_changes_container:
|
||||
return quality.getName()
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
|
@ -455,6 +559,17 @@ class MachineManager(QObject):
|
|||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityId(self):
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer({"type": "quality_changes"})
|
||||
if quality and quality != self._empty_quality_changes_container:
|
||||
return quality.getId()
|
||||
quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
if quality:
|
||||
return quality.getId()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def globalQualityId(self):
|
||||
if self._global_container_stack:
|
||||
quality = self._global_container_stack.findContainer({"type": "quality_changes"})
|
||||
if quality and quality != self._empty_quality_changes_container:
|
||||
|
@ -466,16 +581,39 @@ class MachineManager(QObject):
|
|||
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityType(self):
|
||||
if self._global_container_stack:
|
||||
quality = self._global_container_stack.findContainer(type = "quality")
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer(type = "quality")
|
||||
if quality:
|
||||
return quality.getMetaDataEntry("quality_type")
|
||||
return ""
|
||||
|
||||
@pyqtProperty(bool, notify = activeQualityChanged)
|
||||
def isActiveQualitySupported(self):
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.findContainer(type = "quality")
|
||||
if quality:
|
||||
return Util.parseBool(quality.getMetaDataEntry("supported", True))
|
||||
return False
|
||||
|
||||
## Get the Quality ID associated with the currently active extruder
|
||||
# Note that this only returns the "quality", not the "quality_changes"
|
||||
# \returns QualityID (string) if found, empty string otherwise
|
||||
# \sa activeQualityId()
|
||||
# \todo Ideally, this method would be named activeQualityId(), and the other one
|
||||
# would be named something like activeQualityOrQualityChanges() for consistency
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityContainerId(self):
|
||||
# We're using the active stack instead of the global stack in case the list of qualities differs per extruder
|
||||
if self._global_container_stack:
|
||||
quality = self._active_container_stack.findContainer(type = "quality")
|
||||
if quality:
|
||||
return quality.getId()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityChangesId(self):
|
||||
if self._global_container_stack:
|
||||
changes = self._global_container_stack.findContainer(type = "quality_changes")
|
||||
if self._active_container_stack:
|
||||
changes = self._active_container_stack.findContainer(type = "quality_changes")
|
||||
if changes:
|
||||
return changes.getId()
|
||||
return ""
|
||||
|
@ -501,140 +639,269 @@ class MachineManager(QObject):
|
|||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.getTop().setProperty(key, "value", new_value)
|
||||
|
||||
## Set the active material by switching out a container
|
||||
# Depending on from/to material+current variant, a quality profile is chosen and set.
|
||||
@pyqtSlot(str)
|
||||
def setActiveMaterial(self, material_id):
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
material_container = containers[0]
|
||||
|
||||
Logger.log("d", "Attempting to change the active material to %s", material_id)
|
||||
|
||||
old_variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
old_material = self._active_container_stack.findContainer({"type": "material"})
|
||||
old_quality = self._active_container_stack.findContainer({"type": "quality"})
|
||||
old_quality_changes = self._active_container_stack.findContainer({"type": "quality_changes"})
|
||||
if not old_material:
|
||||
Logger.log("w", "While trying to set the active material, no material was found to replace it.")
|
||||
return
|
||||
if old_quality_changes.getId() == "empty_quality_changes": #Don't want the empty one.
|
||||
|
||||
if old_quality_changes.getId() == "empty_quality_changes":
|
||||
old_quality_changes = None
|
||||
|
||||
self.blurSettings.emit()
|
||||
old_material.nameChanged.disconnect(self._onMaterialNameChanged)
|
||||
|
||||
material_index = self._active_container_stack.getContainerIndex(old_material)
|
||||
self._active_container_stack.replaceContainer(material_index, containers[0])
|
||||
self._active_container_stack.replaceContainer(material_index, material_container)
|
||||
Logger.log("d", "Active material changed")
|
||||
|
||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||
material_container.nameChanged.connect(self._onMaterialNameChanged)
|
||||
|
||||
if old_quality:
|
||||
if old_quality_changes:
|
||||
new_quality = self._updateQualityChangesContainer(old_quality.getMetaDataEntry("quality_type"), old_quality_changes.getMetaDataEntry("name"))
|
||||
else:
|
||||
new_quality = self._updateQualityContainer(self._global_container_stack.getBottom(), old_variant, containers[0], old_quality.getName())
|
||||
if material_container.getMetaDataEntry("compatible") == False:
|
||||
self._material_incompatible_message.show()
|
||||
else:
|
||||
new_quality = self._updateQualityContainer(self._global_container_stack.getBottom(), old_variant, containers[0])
|
||||
self._material_incompatible_message.hide()
|
||||
|
||||
self.setActiveQuality(new_quality.getId())
|
||||
new_quality_id = old_quality.getId()
|
||||
quality_type = old_quality.getMetaDataEntry("quality_type")
|
||||
if old_quality_changes:
|
||||
quality_type = old_quality_changes.getMetaDataEntry("quality_type")
|
||||
new_quality_id = old_quality_changes.getId()
|
||||
|
||||
# See if the requested quality type is available in the new situation.
|
||||
machine_definition = self._active_container_stack.getBottom()
|
||||
quality_manager = QualityManager.getInstance()
|
||||
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
|
||||
quality_manager.getWholeMachineDefinition(machine_definition),
|
||||
[material_container])
|
||||
if not candidate_quality or candidate_quality.getId() == "empty_quality":
|
||||
# Fall back to a quality
|
||||
new_quality = quality_manager.findQualityByQualityType(None,
|
||||
quality_manager.getWholeMachineDefinition(machine_definition),
|
||||
[material_container])
|
||||
if new_quality:
|
||||
new_quality_id = new_quality.getId()
|
||||
else:
|
||||
if not old_quality_changes:
|
||||
new_quality_id = candidate_quality.getId()
|
||||
|
||||
self.setActiveQuality(new_quality_id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveVariant(self, variant_id):
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
Logger.log("d", "Attempting to change the active variant to %s", variant_id)
|
||||
old_variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
old_material = self._active_container_stack.findContainer({"type": "material"})
|
||||
if old_variant:
|
||||
self.blurSettings.emit()
|
||||
variant_index = self._active_container_stack.getContainerIndex(old_variant)
|
||||
self._active_container_stack.replaceContainer(variant_index, containers[0])
|
||||
Logger.log("d", "Active variant changed")
|
||||
|
||||
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.")
|
||||
|
||||
## set the active quality
|
||||
# \param quality_id The quality_id of either a quality or a quality_changes
|
||||
@pyqtSlot(str)
|
||||
def setActiveQuality(self, quality_id):
|
||||
self.blurSettings.emit()
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
|
||||
if not containers or not self._global_container_stack:
|
||||
return
|
||||
|
||||
quality_container = None
|
||||
quality_changes_container = self._empty_quality_changes_container
|
||||
Logger.log("d", "Attempting to change the active quality to %s", quality_id)
|
||||
|
||||
# Quality profile come in two flavours: type=quality and type=quality_changes
|
||||
# If we found a quality_changes profile then look up its parent quality profile.
|
||||
container_type = containers[0].getMetaDataEntry("type")
|
||||
quality_name = containers[0].getName()
|
||||
quality_type = containers[0].getMetaDataEntry("quality_type")
|
||||
|
||||
# Get quality container and optionally the quality_changes container.
|
||||
if container_type == "quality":
|
||||
quality_container = containers[0]
|
||||
new_quality_settings_list = self.determineQualityAndQualityChangesForQualityType(quality_type)
|
||||
elif container_type == "quality_changes":
|
||||
quality_changes_container = containers[0]
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(
|
||||
quality_type = quality_changes_container.getMetaDataEntry("quality"))
|
||||
if not containers:
|
||||
Logger.log("e", "Could not find quality %s for changes %s, not changing quality", quality_changes_container.getMetaDataEntry("quality"), quality_changes_container.getId())
|
||||
return
|
||||
quality_container = containers[0]
|
||||
new_quality_settings_list = self._determineQualityAndQualityChangesForQualityChanges(quality_name)
|
||||
else:
|
||||
Logger.log("e", "Tried to set quality to a container that is not of the right type")
|
||||
return
|
||||
|
||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||
if not quality_type:
|
||||
quality_type = quality_changes_container.getName()
|
||||
name_changed_connect_stacks = [] # Connect these stacks to the name changed callback
|
||||
for setting_info in new_quality_settings_list:
|
||||
stack = setting_info["stack"]
|
||||
stack_quality = setting_info["quality"]
|
||||
stack_quality_changes = setting_info["quality_changes"]
|
||||
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
extruder_id = stack.getId() if stack != self._global_container_stack else None
|
||||
name_changed_connect_stacks.append(stack_quality)
|
||||
name_changed_connect_stacks.append(stack_quality_changes)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality, postpone_emit = True)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality_changes, postpone_emit = True)
|
||||
|
||||
criteria = { "quality_type": quality_type, "extruder": extruder_id }
|
||||
# Send emits that are postponed in replaceContainer.
|
||||
# Here the stacks are finished replacing and every value can be resolved based on the current state.
|
||||
for setting_info in new_quality_settings_list:
|
||||
setting_info["stack"].sendPostponedEmits()
|
||||
|
||||
material = stack.findContainer(type = "material")
|
||||
if material and material is not self._empty_material_container:
|
||||
criteria["material"] = material.getId()
|
||||
|
||||
if self._global_container_stack.getMetaDataEntry("has_machine_quality"):
|
||||
criteria["definition"] = self._global_container_stack.getBottom().getId()
|
||||
else:
|
||||
criteria["definition"] = "fdmprinter"
|
||||
|
||||
stack_quality = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
if not stack_quality:
|
||||
criteria.pop("extruder")
|
||||
stack_quality = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
if not stack_quality:
|
||||
stack_quality = quality_container
|
||||
else:
|
||||
stack_quality = stack_quality[0]
|
||||
else:
|
||||
stack_quality = stack_quality[0]
|
||||
|
||||
if quality_changes_container != self._empty_quality_changes_container:
|
||||
stack_quality_changes = ContainerRegistry.getInstance().findInstanceContainers(name = quality_changes_container.getName(), extruder = extruder_id)[0]
|
||||
else:
|
||||
stack_quality_changes = self._empty_quality_changes_container
|
||||
|
||||
old_quality = stack.findContainer(type = "quality")
|
||||
old_quality.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
old_changes = stack.findContainer(type = "quality_changes")
|
||||
old_changes.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
|
||||
stack.replaceContainer(stack.getContainerIndex(old_quality), stack_quality)
|
||||
stack.replaceContainer(stack.getContainerIndex(old_changes), stack_quality_changes)
|
||||
|
||||
stack_quality.nameChanged.connect(self._onQualityNameChanged)
|
||||
stack_quality_changes.nameChanged.connect(self._onQualityNameChanged)
|
||||
# Connect to onQualityNameChanged
|
||||
for stack in name_changed_connect_stacks:
|
||||
stack.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)
|
||||
self._askUserToKeepOrClearCurrentSettings()
|
||||
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
## Determine the quality and quality changes settings for the current machine for a quality name.
|
||||
#
|
||||
# \param quality_name \type{str} the name of the quality.
|
||||
# \return \type{List[Dict]} with keys "stack", "quality" and "quality_changes".
|
||||
def determineQualityAndQualityChangesForQualityType(self, quality_type):
|
||||
quality_manager = QualityManager.getInstance()
|
||||
result = []
|
||||
empty_quality_changes = self._empty_quality_changes_container
|
||||
global_container_stack = self._global_container_stack
|
||||
if not global_container_stack:
|
||||
return []
|
||||
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
||||
|
||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
if extruder_stacks:
|
||||
stacks = extruder_stacks
|
||||
else:
|
||||
stacks = [global_container_stack]
|
||||
|
||||
for stack in stacks:
|
||||
material = stack.findContainer(type="material")
|
||||
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
|
||||
result.append({"stack": stack, "quality": quality, "quality_changes": empty_quality_changes})
|
||||
|
||||
if extruder_stacks:
|
||||
# Add an extra entry for the global stack.
|
||||
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = "True")
|
||||
|
||||
if not global_quality:
|
||||
global_quality = self._empty_quality_container
|
||||
|
||||
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": empty_quality_changes})
|
||||
|
||||
return result
|
||||
|
||||
## Determine the quality and quality changes settings for the current machine for a quality changes name.
|
||||
#
|
||||
# \param quality_changes_name \type{str} the name of the quality changes.
|
||||
# \return \type{List[Dict]} with keys "stack", "quality" and "quality_changes".
|
||||
def _determineQualityAndQualityChangesForQualityChanges(self, quality_changes_name):
|
||||
result = []
|
||||
quality_manager = QualityManager.getInstance()
|
||||
|
||||
global_container_stack = self._global_container_stack
|
||||
global_machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
||||
|
||||
quality_changes_profiles = quality_manager.findQualityChangesByName(quality_changes_name,
|
||||
global_machine_definition)
|
||||
|
||||
global_quality_changes = [qcp for qcp in quality_changes_profiles if qcp.getMetaDataEntry("extruder") is None][0]
|
||||
material = global_container_stack.findContainer(type="material")
|
||||
|
||||
# For the global stack, find a quality which matches the quality_type in
|
||||
# the quality changes profile and also satisfies any material constraints.
|
||||
quality_type = global_quality_changes.getMetaDataEntry("quality_type")
|
||||
if global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [], global_quality = True)
|
||||
else:
|
||||
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
|
||||
|
||||
# Find the values for each extruder.
|
||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
|
||||
for stack in extruder_stacks:
|
||||
extruder_definition = quality_manager.getParentMachineDefinition(stack.getBottom())
|
||||
|
||||
quality_changes_list = [qcp for qcp in quality_changes_profiles
|
||||
if qcp.getMetaDataEntry("extruder") == extruder_definition.getId()]
|
||||
if quality_changes_list:
|
||||
quality_changes = quality_changes_list[0]
|
||||
else:
|
||||
quality_changes = global_quality_changes
|
||||
|
||||
material = stack.findContainer(type="material")
|
||||
quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material])
|
||||
|
||||
result.append({"stack": stack, "quality": quality, "quality_changes": quality_changes})
|
||||
|
||||
if extruder_stacks:
|
||||
global_quality = quality_manager.findQualityByQualityType(quality_type, global_machine_definition, [material], global_quality = "True")
|
||||
if not global_quality:
|
||||
global_quality = self._empty_quality_container
|
||||
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes})
|
||||
else:
|
||||
result.append({"stack": global_container_stack, "quality": global_quality, "quality_changes": global_quality_changes})
|
||||
|
||||
return result
|
||||
|
||||
def _replaceQualityOrQualityChangesInStack(self, stack, container, postpone_emit = False):
|
||||
# Disconnect the signal handling from the old container.
|
||||
old_container = stack.findContainer(type=container.getMetaDataEntry("type"))
|
||||
if old_container:
|
||||
old_container.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
else:
|
||||
Logger.log("w", "Could not find old "+ container.getMetaDataEntry("type") + " while changing active " + container.getMetaDataEntry("type") + ".")
|
||||
|
||||
# Swap in the new container into the stack.
|
||||
stack.replaceContainer(stack.getContainerIndex(old_container), container, postpone_emit = postpone_emit)
|
||||
|
||||
# Attach the needed signal handling.
|
||||
container.nameChanged.connect(self._onQualityNameChanged)
|
||||
|
||||
def _askUserToKeepOrClearCurrentSettings(self):
|
||||
# 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_text = catalog.i18nc("@label", "You made changes to the following setting(s):")
|
||||
|
||||
# user changes in global stack
|
||||
details_list = [setting.definition.label for setting in self._global_container_stack.getTop().findInstances(**{})]
|
||||
|
||||
# user changes in extruder stacks
|
||||
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
for stack in stacks:
|
||||
details_list.extend([
|
||||
"%s (%s)" % (setting.definition.label, stack.getName())
|
||||
for setting in stack.getTop().findInstances(**{})])
|
||||
|
||||
# Format to output string
|
||||
details = "\n ".join([details_text, ] + details_list)
|
||||
|
||||
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)
|
||||
|
||||
def _keepUserSettingsDialogCallback(self, button):
|
||||
if button == QMessageBox.Yes:
|
||||
# Yes, keep the settings in the user profile with this profile
|
||||
|
@ -674,6 +941,48 @@ class MachineManager(QObject):
|
|||
|
||||
return ""
|
||||
|
||||
## Get the Definition ID to use to select quality profiles for the currently active machine
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
# \sa getQualityDefinitionId
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeQualityDefinitionId(self):
|
||||
if self._global_container_stack:
|
||||
return self.getQualityDefinitionId(self._global_container_stack.getBottom())
|
||||
return ""
|
||||
|
||||
## Get the Definition ID to use to select quality profiles for machines of the specified definition
|
||||
# This is normally the id of the definition itself, but machines can specify a different definition to inherit qualities from
|
||||
# \param definition (DefinitionContainer) machine definition
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
def getQualityDefinitionId(self, definition):
|
||||
return QualityManager.getInstance().getParentMachineDefinition(definition).getId()
|
||||
|
||||
## Get the Variant ID to use to select quality profiles for the currently active variant
|
||||
# \returns VariantID (string) if found, empty string otherwise
|
||||
# \sa getQualityVariantId
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeQualityVariantId(self):
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.findContainer({"type": "variant"})
|
||||
if variant:
|
||||
return self.getQualityVariantId(self._global_container_stack.getBottom(), variant)
|
||||
return ""
|
||||
|
||||
## Get the Variant ID to use to select quality profiles for variants of the specified definitions
|
||||
# This is normally the id of the variant itself, but machines can specify a different definition
|
||||
# to inherit qualities from, which has consequences for the variant to use as well
|
||||
# \param definition (DefinitionContainer) machine definition
|
||||
# \param variant (DefinitionContainer) variant definition
|
||||
# \returns VariantID (string) if found, empty string otherwise
|
||||
def getQualityVariantId(self, definition, variant):
|
||||
variant_id = variant.getId()
|
||||
definition_id = definition.getId()
|
||||
quality_definition_id = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition_id != quality_definition_id:
|
||||
variant_id = variant_id.replace(definition_id, quality_definition_id, 1)
|
||||
return variant_id
|
||||
|
||||
## Gets how the active definition calls variants
|
||||
# Caveat: per-definition-variant-title is currently not translated (though the fallback is)
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
|
@ -757,14 +1066,13 @@ class MachineManager(QObject):
|
|||
def _updateVariantContainer(self, definition):
|
||||
if not definition.getMetaDataEntry("has_variants"):
|
||||
return self._empty_variant_container
|
||||
|
||||
machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(definition)
|
||||
containers = []
|
||||
preferred_variant = definition.getMetaDataEntry("preferred_variant")
|
||||
if preferred_variant:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id, id = preferred_variant)
|
||||
if not containers:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id)
|
||||
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
@ -778,10 +1086,10 @@ class MachineManager(QObject):
|
|||
search_criteria = { "type": "material" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_materials"):
|
||||
search_criteria["definition"] = definition.id
|
||||
search_criteria["definition"] = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition.getMetaDataEntry("has_variants") and variant_container:
|
||||
search_criteria["variant"] = variant_container.id
|
||||
search_criteria["variant"] = self.getQualityVariantId(definition, variant_container)
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
|
@ -796,15 +1104,15 @@ class MachineManager(QObject):
|
|||
if containers:
|
||||
return containers[0]
|
||||
|
||||
if "name" in search_criteria or "id" in search_criteria:
|
||||
if "variant" 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("variant", None)
|
||||
search_criteria.pop("id", None)
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
Logger.log("w", "Unable to find a material container with provided criteria, returning an empty one instead.")
|
||||
return self._empty_material_container
|
||||
|
||||
def _updateQualityContainer(self, definition, variant_container, material_container = None, preferred_quality_name = None):
|
||||
|
@ -812,7 +1120,7 @@ class MachineManager(QObject):
|
|||
search_criteria = { "type": "quality" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = definition.id
|
||||
search_criteria["definition"] = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition.getMetaDataEntry("has_materials") and material_container:
|
||||
search_criteria["material"] = material_container.id
|
||||
|
@ -831,8 +1139,23 @@ class MachineManager(QObject):
|
|||
return containers[0]
|
||||
|
||||
if "material" in search_criteria:
|
||||
# If a quality for this specific material cannot be found, try finding qualities for a generic version of the material
|
||||
material_search_criteria = { "type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic" }
|
||||
# First check if we can solve our material not found problem by checking if we can find quality containers
|
||||
# that are assigned to the parents of this material profile.
|
||||
try:
|
||||
inherited_files = material_container.getInheritedFiles()
|
||||
except AttributeError: # Material_container does not support inheritance.
|
||||
inherited_files = []
|
||||
|
||||
if inherited_files:
|
||||
for inherited_file in inherited_files:
|
||||
# Extract the ID from the path we used to load the file.
|
||||
search_criteria["material"] = os.path.basename(inherited_file).split(".")[0]
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
# We still weren't able to find a quality for this specific material.
|
||||
# Try to find qualities for a generic version of the material.
|
||||
material_search_criteria = { "type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
if material_container:
|
||||
material_search_criteria["definition"] = material_container.getDefinition().id
|
||||
|
@ -840,13 +1163,12 @@ class MachineManager(QObject):
|
|||
if definition.getMetaDataEntry("has_variants"):
|
||||
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
|
||||
else:
|
||||
material_search_criteria["definition"] = definition.id
|
||||
material_search_criteria["definition"] = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition.getMetaDataEntry("has_variants") and variant_container:
|
||||
material_search_criteria["variant"] = variant_container.id
|
||||
material_search_criteria["variant"] = self.getQualityVariantId(definition, variant_container)
|
||||
else:
|
||||
material_search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
material_containers = container_registry.findInstanceContainers(**material_search_criteria)
|
||||
if material_containers:
|
||||
search_criteria["material"] = material_containers[0].getId()
|
||||
|
@ -864,6 +1186,9 @@ class MachineManager(QObject):
|
|||
if containers:
|
||||
return containers[0]
|
||||
|
||||
# Notify user that we were unable to find a matching quality
|
||||
message = Message(catalog.i18nc("@info:status", "Unable to find a quality profile for this combination. Default settings will be used instead."))
|
||||
message.show()
|
||||
return self._empty_quality_container
|
||||
|
||||
## Finds a quality-changes container to use if any other container
|
||||
|
|
66
cura/Settings/MachineNameValidator.py
Normal file
66
cura/Settings/MachineNameValidator.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, QObject, pyqtSignal, QRegExp
|
||||
from PyQt5.QtGui import QValidator
|
||||
import os #For statvfs.
|
||||
import urllib #To escape machine names for how they're saved to file.
|
||||
|
||||
import UM.Resources
|
||||
import UM.Settings.ContainerRegistry
|
||||
import UM.Settings.InstanceContainer
|
||||
|
||||
## Are machine names valid?
|
||||
#
|
||||
# Performs checks based on the length of the name.
|
||||
class MachineNameValidator(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
#Compute the validation regex for printer names. This is limited by the maximum file name length.
|
||||
try:
|
||||
filename_max_length = os.statvfs(UM.Resources.getDataStoragePath()).f_namemax
|
||||
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||
machine_name_max_length = filename_max_length - len("_current_settings.") - len(UM.Settings.ContainerRegistry.getMimeTypeForContainer(UM.Settings.InstanceContainer).preferredSuffix)
|
||||
# Characters that urllib.parse.quote_plus escapes count for 12! So now
|
||||
# we must devise a regex that allows only 12 normal characters or 1
|
||||
# special character, and that up to [machine_name_max_length / 12] times.
|
||||
maximum_special_characters = int(machine_name_max_length / 12)
|
||||
unescaped = r"[a-zA-Z0-9_\-\.\/]"
|
||||
self.machine_name_regex = r"^((" + unescaped + "){0,12}|.){0," + str(maximum_special_characters) + r"}$"
|
||||
|
||||
validationChanged = pyqtSignal()
|
||||
|
||||
## Check if a specified machine name is allowed.
|
||||
#
|
||||
# \param name The machine name to check.
|
||||
# \param position The current position of the cursor in the text box.
|
||||
# \return ``QValidator.Invalid`` if it's disallowed, or
|
||||
# ``QValidator.Acceptable`` if it's allowed.
|
||||
def validate(self, name, position):
|
||||
#Check for file name length of the current settings container (which is the longest file we're saving with the name).
|
||||
try:
|
||||
filename_max_length = os.statvfs(UM.Resources.getDataStoragePath()).f_namemax
|
||||
except AttributeError: #Doesn't support statvfs. Probably because it's not a Unix system.
|
||||
filename_max_length = 255 #Assume it's Windows on NTFS.
|
||||
escaped_name = urllib.parse.quote_plus(name)
|
||||
current_settings_filename = escaped_name + "_current_settings." + UM.Settings.ContainerRegistry.getMimeTypeForContainer(UM.Settings.InstanceContainer).preferredSuffix
|
||||
if len(current_settings_filename) > filename_max_length:
|
||||
return QValidator.Invalid
|
||||
|
||||
return QValidator.Acceptable #All checks succeeded.
|
||||
|
||||
## Updates the validation state of a machine name text field.
|
||||
@pyqtSlot(str)
|
||||
def updateValidation(self, new_name):
|
||||
is_valid = self.validate(new_name, 0)
|
||||
if is_valid == QValidator.Acceptable:
|
||||
self.validation_regex = "^.*$" #Matches anything.
|
||||
else:
|
||||
self.validation_regex = "a^" #Never matches (unless you manage to get "a" before the start of the string... good luck).
|
||||
self.validationChanged.emit()
|
||||
|
||||
@pyqtProperty("QRegExp", notify=validationChanged)
|
||||
def machineNameRegex(self):
|
||||
return QRegExp(self.machine_name_regex)
|
116
cura/Settings/ProfilesModel.py
Normal file
116
cura/Settings/ProfilesModel.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
|
||||
|
||||
from cura.QualityManager import QualityManager
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
## QML Model for listing the current list of valid quality profiles.
|
||||
#
|
||||
class ProfilesModel(InstanceContainersModel):
|
||||
LayerHeightRole = Qt.UserRole + 1001
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.LayerHeightRole, "layer_height")
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._update)
|
||||
|
||||
Application.getInstance().getMachineManager().activeVariantChanged.connect(self._update)
|
||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._update)
|
||||
Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._update)
|
||||
|
||||
# Factory function, used by QML
|
||||
@staticmethod
|
||||
def createProfilesModel(engine, js_engine):
|
||||
return ProfilesModel.getInstance()
|
||||
|
||||
## Get the singleton instance for this class.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
# Note: Explicit use of class name to prevent issues with inheritance.
|
||||
if ProfilesModel.__instance is None:
|
||||
ProfilesModel.__instance = cls()
|
||||
return ProfilesModel.__instance
|
||||
|
||||
__instance = None
|
||||
|
||||
## Fetch the list of containers to display.
|
||||
#
|
||||
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||
def _fetchInstanceContainers(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None:
|
||||
return []
|
||||
|
||||
# Get the list of extruders and place the selected extruder at the front of the list.
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||
extruder_stacks = extruder_manager.getActiveExtruderStacks()
|
||||
if active_extruder in extruder_stacks:
|
||||
extruder_stacks.remove(active_extruder)
|
||||
extruder_stacks = [active_extruder] + extruder_stacks
|
||||
|
||||
# Fetch the list of useable qualities across all extruders.
|
||||
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||
return QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
||||
extruder_stacks)
|
||||
|
||||
## Re-computes the items in this model, and adds the layer height role.
|
||||
def _recomputeItems(self):
|
||||
#Some globals that we can re-use.
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None:
|
||||
return
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
|
||||
unit = global_container_stack.getBottom().getProperty("layer_height", "unit")
|
||||
if not unit:
|
||||
unit = ""
|
||||
|
||||
for item in super()._recomputeItems():
|
||||
profile = container_registry.findContainers(id = item["id"])
|
||||
if not profile:
|
||||
item["layer_height"] = "" #Can't update a profile that is unknown.
|
||||
yield item
|
||||
continue
|
||||
|
||||
#Easy case: This profile defines its own layer height.
|
||||
profile = profile[0]
|
||||
if profile.hasProperty("layer_height", "value"):
|
||||
item["layer_height"] = str(profile.getProperty("layer_height", "value")) + unit
|
||||
yield item
|
||||
continue
|
||||
|
||||
#Quality-changes profile that has no value for layer height. Get the corresponding quality profile and ask that profile.
|
||||
quality_type = profile.getMetaDataEntry("quality_type", None)
|
||||
if quality_type:
|
||||
quality_results = machine_manager.determineQualityAndQualityChangesForQualityType(quality_type)
|
||||
for quality_result in quality_results:
|
||||
if quality_result["stack"] is global_container_stack:
|
||||
quality = quality_result["quality"]
|
||||
break
|
||||
else: #No global container stack in the results:
|
||||
if quality_results:
|
||||
quality = quality_results[0]["quality"] #Take any of the extruders.
|
||||
else:
|
||||
quality = None
|
||||
if quality and quality.hasProperty("layer_height", "value"):
|
||||
item["layer_height"] = str(quality.getProperty("layer_height", "value")) + unit
|
||||
yield item
|
||||
continue
|
||||
|
||||
#Quality has no value for layer height either. Get the layer height from somewhere lower in the stack.
|
||||
skip_until_container = global_container_stack.findContainer({"type": "material"})
|
||||
if not skip_until_container: #No material in stack.
|
||||
skip_until_container = global_container_stack.findContainer({"type": "variant"})
|
||||
if not skip_until_container: #No variant in stack.
|
||||
skip_until_container = global_container_stack.getBottom()
|
||||
item["layer_height"] = str(global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId())) + unit #Fall through to the currently loaded material.
|
||||
yield item
|
45
cura/Settings/QualityAndUserProfilesModel.py
Normal file
45
cura/Settings/QualityAndUserProfilesModel.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Application import Application
|
||||
|
||||
from cura.QualityManager import QualityManager
|
||||
from cura.Settings.ProfilesModel import ProfilesModel
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
## QML Model for listing the current list of valid quality and quality changes profiles.
|
||||
#
|
||||
class QualityAndUserProfilesModel(ProfilesModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
## Fetch the list of containers to display.
|
||||
#
|
||||
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||
def _fetchInstanceContainers(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return []
|
||||
|
||||
# Fetch the list of quality changes.
|
||||
quality_manager = QualityManager.getInstance()
|
||||
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
||||
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||
|
||||
# Get the list of extruders and place the selected extruder at the front of the list.
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
active_extruder = extruder_manager.getActiveExtruderStack()
|
||||
extruder_stacks = extruder_manager.getActiveExtruderStacks()
|
||||
if active_extruder in extruder_stacks:
|
||||
extruder_stacks.remove(active_extruder)
|
||||
extruder_stacks = [active_extruder] + extruder_stacks
|
||||
|
||||
# Fetch the list of useable qualities across all extruders.
|
||||
# The actual list of quality profiles come from the first extruder in the extruder list.
|
||||
quality_list = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
||||
extruder_stacks)
|
||||
|
||||
# Filter the quality_change by the list of available quality_types
|
||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
||||
|
||||
return quality_list + filtered_quality_changes
|
|
@ -15,20 +15,25 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
LabelRole = Qt.UserRole + 2
|
||||
UnitRole = Qt.UserRole + 3
|
||||
ProfileValueRole = Qt.UserRole + 4
|
||||
UserValueRole = Qt.UserRole + 5
|
||||
CategoryRole = Qt.UserRole + 6
|
||||
ProfileValueSourceRole = Qt.UserRole + 5
|
||||
UserValueRole = Qt.UserRole + 6
|
||||
CategoryRole = Qt.UserRole + 7
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent = parent)
|
||||
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
self._extruder_id = None
|
||||
self._quality = None
|
||||
self._material = None
|
||||
self._extruder_definition_id = None
|
||||
self._quality_id = None
|
||||
self._material_id = None
|
||||
|
||||
self.addRoleName(self.KeyRole, "key")
|
||||
self.addRoleName(self.LabelRole, "label")
|
||||
self.addRoleName(self.UnitRole, "unit")
|
||||
self.addRoleName(self.ProfileValueRole, "profile_value")
|
||||
self.addRoleName(self.ProfileValueSourceRole, "profile_value_source")
|
||||
self.addRoleName(self.UserValueRole, "user_value")
|
||||
self.addRoleName(self.CategoryRole, "category")
|
||||
|
||||
|
@ -43,40 +48,51 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
def extruderId(self):
|
||||
return self._extruder_id
|
||||
|
||||
def setExtruderDefinition(self, extruder_definition):
|
||||
if extruder_definition != self._extruder_definition_id:
|
||||
self._extruder_definition_id = extruder_definition
|
||||
self._update()
|
||||
self.extruderDefinitionChanged.emit()
|
||||
|
||||
extruderDefinitionChanged = pyqtSignal()
|
||||
@pyqtProperty(str, fset = setExtruderDefinition, notify = extruderDefinitionChanged)
|
||||
def extruderDefinition(self):
|
||||
return self._extruder_definition_id
|
||||
|
||||
def setQuality(self, quality):
|
||||
if quality != self._quality:
|
||||
self._quality = quality
|
||||
if quality != self._quality_id:
|
||||
self._quality_id = quality
|
||||
self._update()
|
||||
self.qualityChanged.emit()
|
||||
|
||||
qualityChanged = pyqtSignal()
|
||||
@pyqtProperty(str, fset = setQuality, notify = qualityChanged)
|
||||
def quality(self):
|
||||
return self._quality
|
||||
return self._quality_id
|
||||
|
||||
def setMaterial(self, material):
|
||||
if material != self._material:
|
||||
self._material = material
|
||||
if material != self._material_id:
|
||||
self._material_id = material
|
||||
self._update()
|
||||
self.materialChanged.emit()
|
||||
|
||||
materialChanged = pyqtSignal()
|
||||
@pyqtProperty(str, fset = setMaterial, notify = materialChanged)
|
||||
def material(self):
|
||||
return self._material
|
||||
return self._material_id
|
||||
|
||||
def _update(self):
|
||||
if not self._quality:
|
||||
if not self._quality_id:
|
||||
return
|
||||
|
||||
self.clear()
|
||||
items = []
|
||||
|
||||
settings = collections.OrderedDict()
|
||||
definition_container = UM.Application.getInstance().getGlobalContainerStack().getBottom()
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = self._quality)
|
||||
containers = self._container_registry.findInstanceContainers(id = self._quality_id)
|
||||
if not containers:
|
||||
UM.Logger.log("w", "Could not find a quality container with id %s", self._quality)
|
||||
UM.Logger.log("w", "Could not find a quality container with id %s", self._quality_id)
|
||||
return
|
||||
|
||||
quality_container = None
|
||||
|
@ -89,56 +105,64 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
|
||||
criteria = {
|
||||
"type": "quality",
|
||||
"quality_type": quality_changes_container.getMetaDataEntry("quality"),
|
||||
"quality_type": quality_changes_container.getMetaDataEntry("quality_type"),
|
||||
"definition": quality_changes_container.getDefinition().getId()
|
||||
}
|
||||
|
||||
if self._material:
|
||||
criteria["material"] = self._material
|
||||
|
||||
quality_container = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
quality_container = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not quality_container:
|
||||
UM.Logger.log("w", "Could not find a quality container matching quality changes %s", quality_changes_container.getId())
|
||||
return
|
||||
quality_container = quality_container[0]
|
||||
|
||||
quality_type = quality_container.getMetaDataEntry("quality_type")
|
||||
definition_id = quality_container.getDefinition().getId()
|
||||
definition_id = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(quality_container.getDefinition())
|
||||
|
||||
criteria = { "type": "quality", "quality_type": quality_type, "definition": definition_id }
|
||||
criteria = {"type": "quality", "quality_type": quality_type, "definition": definition_id}
|
||||
|
||||
if self._material:
|
||||
criteria["material"] = self._material
|
||||
if self._material_id and self._material_id != "empty_material":
|
||||
criteria["material"] = self._material_id
|
||||
|
||||
criteria["extruder"] = self._extruder_id
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
if not containers:
|
||||
# Try again, this time without extruder
|
||||
new_criteria = criteria.copy()
|
||||
new_criteria.pop("extruder")
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**new_criteria)
|
||||
containers = self._container_registry.findInstanceContainers(**new_criteria)
|
||||
|
||||
if not containers:
|
||||
if not containers and "material" in criteria:
|
||||
# Try again, this time without material
|
||||
criteria.pop("material")
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
criteria.pop("material", None)
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
|
||||
if not containers:
|
||||
# Try again, this time without material or extruder
|
||||
criteria.pop("extruder") # "material" has already been popped
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
containers = self._container_registry.findInstanceContainers(**criteria)
|
||||
|
||||
if not containers:
|
||||
UM.Logger.log("Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||
UM.Logger.log("w", "Could not find any quality containers matching the search criteria %s" % str(criteria))
|
||||
return
|
||||
|
||||
if quality_changes_container:
|
||||
criteria = {"type": "quality_changes", "quality": quality_type, "extruder": self._extruder_id, "definition": definition_id }
|
||||
changes = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
|
||||
if self._extruder_definition_id != "":
|
||||
extruder_definitions = self._container_registry.findDefinitionContainers(id = self._extruder_definition_id)
|
||||
if extruder_definitions:
|
||||
criteria["extruder"] = Application.getInstance().getMachineManager().getQualityDefinitionId(extruder_definitions[0])
|
||||
criteria["name"] = quality_changes_container.getName()
|
||||
else:
|
||||
criteria["extruder"] = None
|
||||
|
||||
changes = self._container_registry.findInstanceContainers(**criteria)
|
||||
if changes:
|
||||
containers.extend(changes)
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
is_multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
current_category = ""
|
||||
for definition in definition_container.findDefinitions():
|
||||
if definition.type == "category":
|
||||
|
@ -146,27 +170,42 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
|
|||
continue
|
||||
|
||||
profile_value = None
|
||||
profile_value_source = ""
|
||||
for container in containers:
|
||||
new_value = container.getProperty(definition.key, "value")
|
||||
if new_value:
|
||||
if new_value is not None:
|
||||
profile_value_source = container.getMetaDataEntry("type")
|
||||
profile_value = new_value
|
||||
|
||||
user_value = None
|
||||
if not self._extruder_id:
|
||||
user_value = Application.getInstance().getGlobalContainerStack().getTop().getProperty(definition.key, "value")
|
||||
user_value = global_container_stack.getTop().getProperty(definition.key, "value")
|
||||
else:
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_id)
|
||||
extruder_stack = self._container_registry.findContainerStacks(id = self._extruder_id)
|
||||
if extruder_stack:
|
||||
user_value = extruder_stack[0].getTop().getProperty(definition.key, "value")
|
||||
|
||||
if not profile_value and not user_value:
|
||||
if profile_value is None and user_value is None:
|
||||
continue
|
||||
|
||||
self.appendItem({
|
||||
if is_multi_extrusion:
|
||||
settable_per_extruder = global_container_stack.getProperty(definition.key, "settable_per_extruder")
|
||||
# If a setting is not settable per extruder (global) and we're looking at an extruder tab, don't show this value.
|
||||
if self._extruder_id != "" and not settable_per_extruder:
|
||||
continue
|
||||
|
||||
# If a setting is settable per extruder (not global) and we're looking at global tab, don't show this value.
|
||||
if self._extruder_id == "" and settable_per_extruder:
|
||||
continue
|
||||
|
||||
items.append({
|
||||
"key": definition.key,
|
||||
"label": definition.label,
|
||||
"unit": definition.unit,
|
||||
"profile_value": "" if profile_value is None else str(profile_value), # it is for display only
|
||||
"profile_value_source": profile_value_source,
|
||||
"user_value": "" if user_value is None else str(user_value),
|
||||
"category": current_category
|
||||
})
|
||||
|
||||
self.setItems(items)
|
213
cura/Settings/SettingInheritanceManager.py
Normal file
213
cura/Settings/SettingInheritanceManager.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
# 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
|
||||
import UM.Settings
|
||||
from UM.Application import Application
|
||||
import cura.Settings
|
||||
from UM.Logger import Logger
|
||||
|
||||
|
||||
## The settingInheritance manager is responsible for checking each setting in order to see if one of the "deeper"
|
||||
# containers has a setting function and the topmost one with a value has a value. We need to have this check
|
||||
# because some profiles tend to have 'hardcoded' values that break our inheritance. A good example of that are the
|
||||
# speed settings. If all the children of print_speed have a single value override, changing the speed won't
|
||||
# actually do anything, as only the 'leaf' settings are used by the engine.
|
||||
class SettingInheritanceManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._global_container_stack = None
|
||||
self._settings_with_inheritance_warning = []
|
||||
self._active_container_stack = None
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
cura.Settings.ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||
self._onActiveExtruderChanged()
|
||||
|
||||
settingsWithIntheritanceChanged = pyqtSignal()
|
||||
|
||||
## Get the keys of all children settings with an override.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getChildrenKeysWithOverride(self, key):
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
||||
if not definitions:
|
||||
return
|
||||
result = []
|
||||
for key in definitions[0].getAllKeys():
|
||||
if key in self._settings_with_inheritance_warning:
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
@pyqtSlot(str, str, result = "QStringList")
|
||||
def getOverridesForExtruder(self, key, extruder_index):
|
||||
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
if not multi_extrusion:
|
||||
return self._settings_with_inheritance_warning
|
||||
extruder = cura.Settings.ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||
if not extruder:
|
||||
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
|
||||
return []
|
||||
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
||||
if not definitions:
|
||||
return
|
||||
result = []
|
||||
for key in definitions[0].getAllKeys():
|
||||
if self._settingIsOverwritingInheritance(key, extruder):
|
||||
result.append(key)
|
||||
|
||||
return result
|
||||
|
||||
@pyqtSlot(str)
|
||||
def manualRemoveOverride(self, key):
|
||||
if key in self._settings_with_inheritance_warning:
|
||||
self._settings_with_inheritance_warning.remove(key)
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def forceUpdate(self):
|
||||
self._update()
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
new_active_stack = cura.Settings.ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if not new_active_stack:
|
||||
new_active_stack = self._global_container_stack
|
||||
|
||||
if new_active_stack != self._active_container_stack: # Check if changed
|
||||
if self._active_container_stack: # Disconnect signal from old container (if any)
|
||||
self._active_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||
|
||||
self._active_container_stack = new_active_stack
|
||||
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
|
||||
self._update() # Ensure that the settings_with_inheritance_warning list is populated.
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if property_name == "value" and self._global_container_stack:
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key = key)
|
||||
if not definitions:
|
||||
return
|
||||
|
||||
has_overwritten_inheritance = self._settingIsOverwritingInheritance(key)
|
||||
|
||||
settings_with_inheritance_warning_changed = False
|
||||
|
||||
# Check if the setting needs to be in the list.
|
||||
if key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
|
||||
self._settings_with_inheritance_warning.append(key)
|
||||
settings_with_inheritance_warning_changed = True
|
||||
elif key in self._settings_with_inheritance_warning and not has_overwritten_inheritance:
|
||||
self._settings_with_inheritance_warning.remove(key)
|
||||
settings_with_inheritance_warning_changed = True
|
||||
|
||||
# Find the topmost parent (Assumed to be a category)
|
||||
parent = definitions[0].parent
|
||||
while parent.parent is not None:
|
||||
parent = parent.parent
|
||||
|
||||
if parent.key not in self._settings_with_inheritance_warning and has_overwritten_inheritance:
|
||||
# Category was not in the list yet, so needs to be added now.
|
||||
self._settings_with_inheritance_warning.append(parent.key)
|
||||
settings_with_inheritance_warning_changed = True
|
||||
|
||||
elif parent.key in self._settings_with_inheritance_warning and not has_overwritten_inheritance:
|
||||
# Category was in the list and one of it's settings is not overwritten.
|
||||
if not self._recursiveCheck(parent): # Check if any of it's children have overwritten inheritance.
|
||||
self._settings_with_inheritance_warning.remove(parent.key)
|
||||
settings_with_inheritance_warning_changed = True
|
||||
|
||||
# Emit the signal if there was any change to the list.
|
||||
if settings_with_inheritance_warning_changed:
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
def _recursiveCheck(self, definition):
|
||||
for child in definition.children:
|
||||
if child.key in self._settings_with_inheritance_warning:
|
||||
return True
|
||||
if child.children:
|
||||
if self._recursiveCheck(child):
|
||||
return True
|
||||
return False
|
||||
|
||||
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
|
||||
def settingsWithInheritanceWarning(self):
|
||||
return self._settings_with_inheritance_warning
|
||||
|
||||
## Check if a setting has an inheritance function that is overwritten
|
||||
def _settingIsOverwritingInheritance(self, key, stack = None):
|
||||
has_setting_function = False
|
||||
if not stack:
|
||||
stack = self._active_container_stack
|
||||
containers = []
|
||||
|
||||
## Check if the setting has a user state. If not, it is never overwritten.
|
||||
has_user_state = stack.getProperty(key, "state") == UM.Settings.InstanceState.User
|
||||
if not has_user_state:
|
||||
return False
|
||||
|
||||
## If a setting is not enabled, don't label it as overwritten (It's never visible anyway).
|
||||
if not stack.getProperty(key, "enabled"):
|
||||
return False
|
||||
|
||||
## Also check if the top container is not a setting function (this happens if the inheritance is restored).
|
||||
if isinstance(stack.getTop().getProperty(key, "value"), UM.Settings.SettingFunction):
|
||||
return False
|
||||
|
||||
## Mash all containers for all the stacks together.
|
||||
while stack:
|
||||
containers.extend(stack.getContainers())
|
||||
stack = stack.getNextStack()
|
||||
has_non_function_value = False
|
||||
for container in containers:
|
||||
try:
|
||||
value = container.getProperty(key, "value")
|
||||
except AttributeError:
|
||||
continue
|
||||
if value is not None:
|
||||
# If a setting doesn't use any keys, it won't change it's value, so treat it as if it's a fixed value
|
||||
has_setting_function = isinstance(value, UM.Settings.SettingFunction) and len(value.getUsedSettingKeys()) > 0
|
||||
if has_setting_function is False:
|
||||
has_non_function_value = True
|
||||
continue
|
||||
|
||||
if has_setting_function:
|
||||
break # There is a setting function somewhere, stop looking deeper.
|
||||
return has_setting_function and has_non_function_value
|
||||
|
||||
def _update(self):
|
||||
self._settings_with_inheritance_warning = [] # Reset previous data.
|
||||
|
||||
# Check all setting keys that we know of and see if they are overridden.
|
||||
for setting_key in self._global_container_stack.getAllKeys():
|
||||
override = self._settingIsOverwritingInheritance(setting_key)
|
||||
if override:
|
||||
self._settings_with_inheritance_warning.append(setting_key)
|
||||
|
||||
# Check all the categories if any of their children have their inheritance overwritten.
|
||||
for category in self._global_container_stack.getBottom().findDefinitions(type = "category"):
|
||||
if self._recursiveCheck(category):
|
||||
self._settings_with_inheritance_warning.append(category.key)
|
||||
|
||||
# Notify others that things have changed.
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.containersChanged.connect(self._onContainersChanged)
|
||||
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
self._onActiveExtruderChanged()
|
||||
|
||||
def _onContainersChanged(self, container):
|
||||
# TODO: Multiple container changes in sequence now cause quite a few recalculations.
|
||||
# This isn't that big of an issue, but it could be in the future.
|
||||
self._update()
|
||||
|
||||
@staticmethod
|
||||
def createSettingInheritanceManager(engine=None, script_engine=None):
|
||||
return SettingInheritanceManager()
|
|
@ -30,7 +30,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
self._stack.addContainer(self._instance)
|
||||
|
||||
if cura.Settings.ExtruderManager.getInstance().extruderCount > 1:
|
||||
self._extruder_stack = cura.Settings.ExtruderManager.getInstance().activeExtruderStackId
|
||||
self._extruder_stack = cura.Settings.ExtruderManager.getInstance().getExtruderStack(0).getId()
|
||||
else:
|
||||
self._extruder_stack = None
|
||||
|
||||
|
@ -61,6 +61,21 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
|||
def getActiveExtruder(self):
|
||||
return self._extruder_stack
|
||||
|
||||
## Gets the signal that emits if the active extruder changed.
|
||||
#
|
||||
# This can then be accessed via a decorator.
|
||||
def getActiveExtruderChangedSignal(self):
|
||||
return self.activeExtruderChanged
|
||||
|
||||
## Gets the currently active extruders position
|
||||
#
|
||||
# \return An extruder's position, or None if no position info is available.
|
||||
def getActiveExtruderPosition(self):
|
||||
containers = ContainerRegistry.getInstance().findContainers(id = self.getActiveExtruder())
|
||||
if containers:
|
||||
container_stack = containers[0]
|
||||
return container_stack.getMetaDataEntry("position", default=None)
|
||||
|
||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||
if property_name == "value": # Only reslice if the value has changed.
|
||||
Application.getInstance().getBackend().forceSlice()
|
||||
|
|
36
cura/Settings/UserProfilesModel.py
Normal file
36
cura/Settings/UserProfilesModel.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Application import Application
|
||||
|
||||
from cura.QualityManager import QualityManager
|
||||
from cura.Settings.ProfilesModel import ProfilesModel
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
## QML Model for listing the current list of valid quality changes profiles.
|
||||
#
|
||||
class UserProfilesModel(ProfilesModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
## Fetch the list of containers to display.
|
||||
#
|
||||
# See UM.Settings.Models.InstanceContainersModel._fetchInstanceContainers().
|
||||
def _fetchInstanceContainers(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return []
|
||||
|
||||
# Fetch the list of quality changes.
|
||||
quality_manager = QualityManager.getInstance()
|
||||
machine_definition = quality_manager.getParentMachineDefinition(global_container_stack.getBottom())
|
||||
quality_changes_list = quality_manager.findAllQualityChangesForMachine(machine_definition)
|
||||
|
||||
# Fetch the list of qualities
|
||||
quality_list = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
|
||||
ExtruderManager.getInstance().getActiveExtruderStacks())
|
||||
|
||||
# Filter the quality_change by the list of available quality_types
|
||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
||||
|
||||
return filtered_quality_changes
|
BIN
docs/Cura_Data_Model.odg
Normal file
BIN
docs/Cura_Data_Model.odg
Normal file
Binary file not shown.
|
@ -8,15 +8,20 @@ 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
|
||||
import UM.Application
|
||||
from UM.Job import Job
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
from UM.Application import Application
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.QualityManager import QualityManager
|
||||
|
||||
import math
|
||||
import os.path
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
|
||||
|
@ -24,104 +29,225 @@ class ThreeMFReader(MeshReader):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".3mf"]
|
||||
|
||||
self._root = None
|
||||
self._namespaces = {
|
||||
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
|
||||
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
|
||||
}
|
||||
self._base_name = ""
|
||||
self._unit = None
|
||||
|
||||
def _createNodeFromObject(self, object, name = ""):
|
||||
node = SceneNode()
|
||||
node.setName(name)
|
||||
mesh_builder = MeshBuilder()
|
||||
vertex_list = []
|
||||
|
||||
components = object.find(".//3mf:components", self._namespaces)
|
||||
if components:
|
||||
for component in components:
|
||||
id = component.get("objectid")
|
||||
new_object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
|
||||
new_node = self._createNodeFromObject(new_object, self._base_name + "_" + str(id))
|
||||
node.addChild(new_node)
|
||||
transform = component.get("transform")
|
||||
if transform is not None:
|
||||
new_node.setTransformation(self._createMatrixFromTransformationString(transform))
|
||||
|
||||
# for vertex in entry.mesh.vertices.vertex:
|
||||
for vertex in object.findall(".//3mf:vertex", self._namespaces):
|
||||
vertex_list.append([vertex.get("x"), vertex.get("y"), vertex.get("z")])
|
||||
Job.yieldThread()
|
||||
|
||||
xml_settings = list(object.findall(".//cura:setting", self._namespaces))
|
||||
|
||||
# Add the setting override decorator, so we can add settings to this node.
|
||||
if xml_settings:
|
||||
node.addDecorator(SettingOverrideDecorator())
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
# Ensure the correct next container for the SettingOverride decorator is set.
|
||||
if global_container_stack:
|
||||
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
# Ensure that all extruder data is reset
|
||||
if not multi_extrusion:
|
||||
default_stack_id = global_container_stack.getId()
|
||||
else:
|
||||
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
||||
if default_stack:
|
||||
default_stack_id = default_stack.getId()
|
||||
else:
|
||||
default_stack_id = global_container_stack.getId()
|
||||
node.callDecoration("setActiveExtruder", default_stack_id)
|
||||
|
||||
# Get the definition & set it
|
||||
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
|
||||
node.callDecoration("getStack").getTop().setDefinition(definition)
|
||||
|
||||
setting_container = node.callDecoration("getStack").getTop()
|
||||
for setting in xml_settings:
|
||||
setting_key = setting.get("key")
|
||||
setting_value = setting.text
|
||||
|
||||
# Extruder_nr is a special case.
|
||||
if setting_key == "extruder_nr":
|
||||
extruder_stack = ExtruderManager.getInstance().getExtruderStack(int(setting_value))
|
||||
if extruder_stack:
|
||||
node.callDecoration("setActiveExtruder", extruder_stack.getId())
|
||||
else:
|
||||
Logger.log("w", "Unable to find extruder in position %s", setting_value)
|
||||
continue
|
||||
setting_container.setProperty(setting_key,"value", setting_value)
|
||||
|
||||
if len(node.getChildren()) > 0:
|
||||
group_decorator = GroupDecorator()
|
||||
node.addDecorator(group_decorator)
|
||||
|
||||
triangles = object.findall(".//3mf:triangle", self._namespaces)
|
||||
mesh_builder.reserveFaceCount(len(triangles))
|
||||
|
||||
for triangle in triangles:
|
||||
v1 = int(triangle.get("v1"))
|
||||
v2 = int(triangle.get("v2"))
|
||||
v3 = int(triangle.get("v3"))
|
||||
|
||||
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
|
||||
vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
|
||||
vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# TODO: We currently do not check for normals and simply recalculate them.
|
||||
mesh_builder.calculateNormals()
|
||||
mesh_builder.setFileName(name)
|
||||
mesh_data = mesh_builder.build()
|
||||
|
||||
if len(mesh_data.getVertices()):
|
||||
node.setMeshData(mesh_data)
|
||||
|
||||
node.setSelectable(True)
|
||||
return node
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation):
|
||||
splitted_transformation = transformation.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]
|
||||
|
||||
return temp_mat
|
||||
|
||||
def read(self, file_name):
|
||||
result = SceneNode()
|
||||
result = []
|
||||
# The base object of 3mf is a zipped archive.
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
self._base_name = os.path.basename(file_name)
|
||||
try:
|
||||
root = ET.parse(archive.open("3D/3dmodel.model"))
|
||||
self._root = ET.parse(archive.open("3D/3dmodel.model"))
|
||||
self._unit = self._root.getroot().get("unit")
|
||||
|
||||
# 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
|
||||
build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces)
|
||||
|
||||
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()
|
||||
for build_item in build_items:
|
||||
id = build_item.get("objectid")
|
||||
object = self._root.find("./3mf:resources/3mf:object[@id='{0}']".format(id), self._namespaces)
|
||||
if "type" in object.attrib:
|
||||
if object.attrib["type"] == "support" or object.attrib["type"] == "other":
|
||||
# Ignore support objects, as cura does not support these.
|
||||
# We can't guarantee that they wont be made solid.
|
||||
# We also ignore "other", as I have no idea what to do with them.
|
||||
Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"])
|
||||
continue
|
||||
elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model":
|
||||
pass # Load these as normal
|
||||
else:
|
||||
# We should technically fail at this point because it's an invalid 3MF, but try to continue anyway.
|
||||
Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec",
|
||||
object.attrib["type"])
|
||||
continue
|
||||
|
||||
triangles = entry.findall(".//3mf:triangle", self._namespaces)
|
||||
mesh_builder.reserveFaceCount(len(triangles))
|
||||
build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id))
|
||||
transform = build_item.get("transform")
|
||||
if transform is not None:
|
||||
build_item_node.setTransformation(self._createMatrixFromTransformationString(transform))
|
||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
for triangle in triangles:
|
||||
v1 = int(triangle.get("v1"))
|
||||
v2 = int(triangle.get("v2"))
|
||||
v3 = int(triangle.get("v3"))
|
||||
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
||||
# First step: flip the y and z axis.
|
||||
transformation_matrix = Matrix()
|
||||
transformation_matrix._data[1, 1] = 0
|
||||
transformation_matrix._data[1, 2] = 1
|
||||
transformation_matrix._data[2, 1] = -1
|
||||
transformation_matrix._data[2, 2] = 0
|
||||
|
||||
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])
|
||||
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
|
||||
# build volume.
|
||||
if global_container_stack:
|
||||
translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
|
||||
y = -global_container_stack.getProperty("machine_depth", "value") / 2,
|
||||
z = 0)
|
||||
translation_matrix = Matrix()
|
||||
translation_matrix.setByTranslation(translation_vector)
|
||||
transformation_matrix.multiply(translation_matrix)
|
||||
|
||||
Job.yieldThread()
|
||||
# Third step: 3MF also defines a unit, wheras Cura always assumes mm.
|
||||
scale_matrix = Matrix()
|
||||
scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit))
|
||||
transformation_matrix.multiply(scale_matrix)
|
||||
|
||||
# Rotate the model; We use a different coordinate frame.
|
||||
rotation = Matrix()
|
||||
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
|
||||
# Pre multiply the transformation with the loaded transformation, so the data is handled correctly.
|
||||
build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix))
|
||||
|
||||
# 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)
|
||||
result.append(build_item_node)
|
||||
|
||||
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
|
||||
Logger.log("e", "An exception occurred in 3mf reader: %s", e)
|
||||
|
||||
return result
|
||||
|
||||
## Create a scale vector based on a unit string.
|
||||
# The core spec defines the following:
|
||||
# * micron
|
||||
# * millimeter (default)
|
||||
# * centimeter
|
||||
# * inch
|
||||
# * foot
|
||||
# * meter
|
||||
def _getScaleFromUnit(self, unit):
|
||||
if unit is None:
|
||||
unit = "millimeter"
|
||||
if unit == "micron":
|
||||
scale = 0.001
|
||||
elif unit == "millimeter":
|
||||
scale = 1
|
||||
elif unit == "centimeter":
|
||||
scale = 10
|
||||
elif unit == "inch":
|
||||
scale = 25.4
|
||||
elif unit == "foot":
|
||||
scale = 304.8
|
||||
elif unit == "meter":
|
||||
scale = 1000
|
||||
else:
|
||||
Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
|
||||
scale = 1
|
||||
|
||||
return Vector(scale, scale, scale)
|
|
@ -33,7 +33,6 @@ class ChangeLog(Extension, QObject,):
|
|||
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:
|
||||
|
@ -87,6 +86,13 @@ class ChangeLog(Extension, QObject,):
|
|||
else:
|
||||
latest_version_shown = Version(Preferences.getInstance().getValue("general/latest_version_changelog_shown"))
|
||||
|
||||
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||
|
||||
# Do not show the changelog when there is no global container stack
|
||||
# This implies we are running Cura for the first time.
|
||||
if not Application.getInstance().getGlobalContainerStack():
|
||||
return
|
||||
|
||||
if self._version > latest_version_shown:
|
||||
self.showChangelog()
|
||||
|
||||
|
@ -95,7 +101,6 @@ class ChangeLog(Extension, QObject,):
|
|||
self.createChangelogWindow()
|
||||
|
||||
self._changelog_window.show()
|
||||
Preferences.getInstance().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||
|
||||
def hideChangelog(self):
|
||||
if self._changelog_window:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Controls 1.3
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.1
|
||||
|
||||
|
@ -11,33 +11,31 @@ import UM 1.1 as UM
|
|||
UM.Dialog
|
||||
{
|
||||
id: base
|
||||
minimumWidth: 400 * Screen.devicePixelRatio
|
||||
minimumHeight: 300 * Screen.devicePixelRatio
|
||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width * 0.75
|
||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height * 0.75
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
title: catalog.i18nc("@label", "Changelog")
|
||||
|
||||
ScrollView
|
||||
TextArea
|
||||
{
|
||||
width: parent.width
|
||||
height: parent.height - 25
|
||||
Label
|
||||
{
|
||||
text: manager.getChangeLogString()
|
||||
width:base.width - 35
|
||||
wrapMode: Text.Wrap;
|
||||
}
|
||||
anchors.fill: parent
|
||||
text: manager.getChangeLogString()
|
||||
readOnly: true;
|
||||
textFormat: TextEdit.RichText
|
||||
}
|
||||
Button
|
||||
{
|
||||
UM.I18nCatalog
|
||||
|
||||
rightButtons: [
|
||||
Button
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
onClicked: base.hide()
|
||||
}
|
||||
anchors.bottom:parent.bottom
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
onClicked: base.hide()
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,125 @@
|
|||
[2.3.1]
|
||||
*Layer Height in Profile Selection
|
||||
Added the layer height to the profile selection menu.
|
||||
|
||||
*Bug fixes
|
||||
Fixed the option to import g-code from related machines as a profile
|
||||
Fixed a bug where editing material settings has no effect on 3D prints
|
||||
Fixed an issue with automatic profile importing on Cura 2.1 on Mac OSX
|
||||
Fixed an inheritance issue for dual extrusion
|
||||
Fixed an issue with "i" symbol updates
|
||||
Fixed a freeze that can occur while printing via Wi-Fi
|
||||
|
||||
[2.3.0]
|
||||
*Multi Extrusion Support
|
||||
Machines with multiple extruders are now supported. Ultimaker 3 printers and Ultimaker Original printers with dual extrusion upgrade kit are currently supported.
|
||||
|
||||
*Network Printing for Ultimaker 3
|
||||
Sending a print to an Ultimaker 3 remotely via the network is now possible. Requires Wi-Fi or LAN to connect to the printer.
|
||||
|
||||
*Print Monitoring for Ultimaker 3
|
||||
You can monitor your print on an Ultimaker 3 with a live camera feed. Requires Wi-Fi or LAN to connect to the printer.
|
||||
|
||||
*Material and Print Core Synchronization
|
||||
Connecting to an Ultimaker 3 now gives you the option to synchronize the materials in Cura with what is loaded in the printer.
|
||||
|
||||
*Speed improvements
|
||||
The first thing you will notice is the speed. STL loading is now 10 to 20 times faster, layer view is significantly faster and slicing speed is slightly improved.
|
||||
|
||||
*Improved Position Tool
|
||||
Place objects precisely where you want them by manually entering the values for the position.
|
||||
|
||||
*Custom Machine Support
|
||||
It’s now much easier to use Cura with custom machines. You can edit the machine settings when you load a new custom machine.
|
||||
|
||||
*Improved Grouping
|
||||
It's now possible to transform objects that are already grouped.
|
||||
Select an individual item in a group or merged object and edit as usual. Just Ctrl + Click and edit away.
|
||||
|
||||
*Enhanced Profile Management
|
||||
Profile management is improved. You can now easily see and track changes made to your profiles.
|
||||
|
||||
*Improved Setting Visibility
|
||||
Make multiple settings visible at the same time with a checkbox. The Visibility Overview setting indicates why a setting is not shown in the sidebar even if it is enabled.
|
||||
|
||||
*Improved time estimation
|
||||
Time estimations are more accurate. Based on our test time estimations should be within 5% accuracy for Ultimaker printers.
|
||||
|
||||
*Optional G-code Machine Prefix
|
||||
Disable the g-code prefix in Preferences. No more UM2_ on your printer display!
|
||||
|
||||
*Print Weight Estimates
|
||||
Cura now estimates print weight as well as length.
|
||||
|
||||
*Automatic Import Configuration
|
||||
Configurations from older installations of Cura 2.1 are automatically imported into the newest installation.
|
||||
|
||||
*Slicing features
|
||||
*Infill Types
|
||||
Two new infill types are now introduced: Tetrahedral and Cubic. They change along with the Z-axis for more uniform strength in all directions. There are now seven infill types to choose from.
|
||||
|
||||
*Gradual Infill
|
||||
Gradual infill lets users adjust infill density, based on the distance from the top layers. This offers faster printing and reduced material requirements, whilst maintaining surface quality.
|
||||
|
||||
*Set Acceleration and Jerk by Feature
|
||||
You can now set Jerk and Acceleration by feature-type (infill, walls, top/bottom, etc), for more precision.
|
||||
|
||||
*Outer Wall Offset
|
||||
If your outer wall line width is smaller than your nozzle size, move the nozzle a bit inward when printing the outer wall, to improve surface quality.
|
||||
|
||||
*Enhanced Combing
|
||||
The “No Skin” option allows you to comb over infill only to avoid scars on top surfaces.
|
||||
|
||||
*Z Hop
|
||||
Can’t avoid previously printed parts by horizontal moves? The Z Hop Only Over Printed Parts gives you the ability to Z Hop to avoid collisions for better surface quality.
|
||||
|
||||
*Skin and Wall Overlap
|
||||
The Skin Overlap setting allows you to overlap the skin lines with the walls for better adhesion.
|
||||
|
||||
*Adjust Initial Layer Travel Speed
|
||||
Set the travel speed of the initial layer(s) to reduce risk of extruder pulling the print from the bed.
|
||||
|
||||
*Support Interface
|
||||
It is now possible to print a support bottom as well as a support roof. Support bottoms are placed where the support rests on the model. Printing the support interface with PVA leads to improved surface quality.
|
||||
|
||||
*Bug fixes
|
||||
Deleting grouped objects
|
||||
Duplicating groups
|
||||
Bridging
|
||||
Drag and drop (first Windows run)
|
||||
Unretraction speeds
|
||||
Bottom layer in Spiralize mode
|
||||
Overlap Compensation
|
||||
Raft retractions
|
||||
Retractions now occur after each object printed in one-at-a-time mode
|
||||
Rafts are no longer printed outside of build area
|
||||
Spiralize no longer limited to the first printed segment only
|
||||
Line distance is now the actual line distance
|
||||
Enabling raft doesn’t influence at which height the model is sliced any more
|
||||
Brim is now always printed just once
|
||||
Support roofs now only occur just below overhang
|
||||
|
||||
*Minor changes
|
||||
Message display time increased to 30 seconds
|
||||
Notification if you try to save to a locked SD card
|
||||
Engine log now included in the application log
|
||||
Undo and Redo now function with multiple operations
|
||||
The last used folder is now remembered rather than defaulting to home folder
|
||||
Import X3D files
|
||||
Made it possible to add multiple Per Model Settings at once
|
||||
Bed Level and Checkup procedures for UMO+ can be performed without re-adding machine
|
||||
Combing applied in more cases and results in better paths
|
||||
Infill thickness now supports Grid infill also for even multiples of the layer height
|
||||
Support is no longer removed by unprintable thin parts of the model
|
||||
Support generated on each appropriate layer
|
||||
Support no longer goes outside overhang areas
|
||||
Support no longer removes brim around the object
|
||||
Brim is now also generated under the support
|
||||
Draft and Ooze shield get their own brim or raft
|
||||
Settings shared between skirt and brim now also activate when brim is selected
|
||||
Compensate overlapping wall parts now also works for inner walls
|
||||
Bed lowering speed can be adjusted for each layer
|
||||
|
||||
[2.1.3]
|
||||
|
||||
*Material Profiles
|
||||
|
|
|
@ -13,7 +13,7 @@ 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.
|
||||
repeated SettingExtruder limit_to_extruder = 4; // From which stack the setting would inherit if not defined per object
|
||||
}
|
||||
|
||||
message Extruder
|
||||
|
@ -56,6 +56,7 @@ message Polygon {
|
|||
SupportInfillType = 7;
|
||||
MoveCombingType = 8;
|
||||
MoveRetractionType = 9;
|
||||
SupportInterfaceType = 10;
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -220,6 +220,9 @@ class CuraEngineBackend(Backend):
|
|||
#
|
||||
# \param job The start slice job that was just finished.
|
||||
def _onStartSliceCompleted(self, job):
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
|
||||
# Note that cancelled slice jobs can still call this method.
|
||||
if self._start_slice_job is job:
|
||||
self._start_slice_job = None
|
||||
|
@ -227,18 +230,48 @@ class CuraEngineBackend(Backend):
|
|||
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
|
||||
return
|
||||
|
||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
||||
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
|
||||
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 = Message(catalog.i18nc("@info:status",
|
||||
"The selected material is incompatible with the selected machine or configuration."))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
|
||||
if Application.getInstance().getPlatformActivity:
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
error_keys = []
|
||||
for extruder in extruders:
|
||||
error_keys.extend(extruder.getErrorKeys())
|
||||
if not extruders:
|
||||
error_keys = self._global_container_stack.getErrorKeys()
|
||||
error_labels = set()
|
||||
definition_container = self._global_container_stack.getBottom()
|
||||
for key in error_keys:
|
||||
error_labels.add(definition_container.findDefinitions(key = key)[0].label)
|
||||
|
||||
error_labels = ", ".join(error_labels)
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}".format(error_labels)))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
|
||||
if Application.getInstance().getPlatformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
|
||||
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 = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
else:
|
||||
|
@ -286,7 +319,7 @@ class CuraEngineBackend(Backend):
|
|||
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")
|
||||
Logger.log("w", "A socket error caused the connection to be reset")
|
||||
|
||||
## A setting has changed, so check if we must reslice.
|
||||
#
|
||||
|
@ -326,6 +359,7 @@ class CuraEngineBackend(Backend):
|
|||
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.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
|
||||
|
@ -401,6 +435,7 @@ class CuraEngineBackend(Backend):
|
|||
# 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.finished.connect(self._onProcessLayersFinished)
|
||||
self._process_layers_job.start()
|
||||
self._stored_optimized_layer_data = []
|
||||
else:
|
||||
|
@ -453,3 +488,5 @@ class CuraEngineBackend(Backend):
|
|||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
def _onProcessLayersFinished(self, job):
|
||||
self._process_layers_job = None
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import gc
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
@ -64,6 +66,12 @@ class ProcessSlicedLayersJob(Job):
|
|||
self._progress.hide()
|
||||
return
|
||||
|
||||
# Force garbage collection.
|
||||
# For some reason, Python has a tendency to keep the layer data
|
||||
# in memory longer than needed. Forcing the GC to run here makes
|
||||
# sure any old layer data is really cleaned up before adding new.
|
||||
gc.collect()
|
||||
|
||||
mesh = MeshData()
|
||||
layer_data = LayerDataBuilder.LayerDataBuilder()
|
||||
layer_count = len(self._layers)
|
||||
|
|
|
@ -24,6 +24,8 @@ class StartJobResult(IntEnum):
|
|||
Error = 2
|
||||
SettingError = 3
|
||||
NothingToSlice = 4
|
||||
MaterialIncompatible = 5
|
||||
BuildPlateError = 6
|
||||
|
||||
|
||||
## Formatter class that handles token expansion in start/end gcod
|
||||
|
@ -74,10 +76,21 @@ class StartSliceJob(Job):
|
|||
return
|
||||
|
||||
# Don't slice if there is a setting with an error value.
|
||||
if not Application.getInstance().getMachineManager().isActiveStackValid:
|
||||
if Application.getInstance().getMachineManager().stacksHaveErrors:
|
||||
self.setResult(StartJobResult.SettingError)
|
||||
return
|
||||
|
||||
if Application.getInstance().getBuildVolume().hasErrors():
|
||||
self.setResult(StartJobResult.BuildPlateError)
|
||||
return
|
||||
|
||||
for extruder_stack in cura.Settings.ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
material = extruder_stack.findContainer({"type": "material"})
|
||||
if material:
|
||||
if material.getMetaDataEntry("compatible") == False:
|
||||
self.setResult(StartJobResult.MaterialIncompatible)
|
||||
return
|
||||
|
||||
# Don't slice if there is a per object setting with an error value.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if type(node) is not SceneNode or not node.isSelectable():
|
||||
|
@ -148,7 +161,18 @@ class StartSliceJob(Job):
|
|||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
verts = numpy.array(mesh_data.getVertices())
|
||||
verts = mesh_data.getVertices()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
#TODO: This is a very slow way of doing it! It also locks up the GUI.
|
||||
flat_vert_list = []
|
||||
for face in indices:
|
||||
for vert_index in face:
|
||||
flat_vert_list.append(verts[vert_index])
|
||||
Job.yieldThread()
|
||||
verts = numpy.array(flat_vert_list)
|
||||
else:
|
||||
verts = numpy.array(verts)
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
|
@ -186,6 +210,9 @@ class StartSliceJob(Job):
|
|||
material_instance_container = stack.findContainer({"type": "material"})
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
# Do not send settings that are not settable_per_extruder.
|
||||
if stack.getProperty(key, "settable_per_extruder") == False:
|
||||
continue
|
||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||
setting.name = key
|
||||
if key == "material_guid" and material_instance_container:
|
||||
|
@ -206,10 +233,18 @@ class StartSliceJob(Job):
|
|||
# Use resolvement value if available, or take the value
|
||||
resolved_value = stack.getProperty(key, "resolve")
|
||||
if resolved_value is not None:
|
||||
settings[key] = resolved_value
|
||||
# There is a resolvement value. Check if we need to use it.
|
||||
user_container = stack.findContainer({"type": "user"})
|
||||
quality_changes_container = stack.findContainer({"type": "quality_changes"})
|
||||
if user_container.hasProperty(key,"value") or quality_changes_container.hasProperty(key,"value"):
|
||||
# Normal case
|
||||
settings[key] = stack.getProperty(key, "value")
|
||||
else:
|
||||
settings[key] = resolved_value
|
||||
else:
|
||||
# Normal case
|
||||
settings[key] = stack.getProperty(key, "value")
|
||||
Job.yieldThread()
|
||||
|
||||
start_gcode = settings["machine_start_gcode"]
|
||||
settings["material_bed_temp_prepend"] = "{material_bed_temperature}" not in start_gcode #Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||
|
@ -222,22 +257,24 @@ class StartSliceJob(Job):
|
|||
setting_message.value = self._expandGcodeTokens(key, value, settings)
|
||||
else:
|
||||
setting_message.value = str(value).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## 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
|
||||
# This is only set for settings that have the limit_to_extruder
|
||||
# property.
|
||||
#
|
||||
# \param stack The global stack with all settings, from which to read the
|
||||
# global_inherits_stack property.
|
||||
# limit_to_extruder property.
|
||||
def _buildGlobalInheritsStackMessage(self, stack):
|
||||
for key in stack.getAllKeys():
|
||||
extruder = int(round(float(stack.getProperty(key, "global_inherits_stack"))))
|
||||
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
|
||||
if extruder >= 0: #Set to a specific extruder.
|
||||
setting_extruder = self._slice_message.addRepeatedMessage("global_inherits_stack")
|
||||
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
|
||||
setting_extruder.name = key
|
||||
setting_extruder.extruder = extruder
|
||||
Job.yieldThread()
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
import configparser
|
||||
|
||||
import os.path
|
||||
|
||||
from UM import PluginRegistry
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
||||
from cura.ProfileReader import ProfileReader
|
||||
|
@ -26,19 +26,79 @@ class CuraProfileReader(ProfileReader):
|
|||
# not be read or didn't contain a valid profile, \code None \endcode is
|
||||
# returned.
|
||||
def read(self, file_name):
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
results = []
|
||||
for profile_id in archive.namelist():
|
||||
# Create an empty profile.
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
serialized = ""
|
||||
with archive.open(profile_id) as f:
|
||||
serialized = f.read()
|
||||
try:
|
||||
profile.deserialize(serialized.decode("utf-8") )
|
||||
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))
|
||||
continue
|
||||
results.append(profile)
|
||||
return results
|
||||
try:
|
||||
with zipfile.ZipFile(file_name, "r") as archive:
|
||||
results = []
|
||||
for profile_id in archive.namelist():
|
||||
with archive.open(profile_id) as f:
|
||||
serialized = f.read()
|
||||
profile = self._loadProfile(serialized.decode("utf-8"), profile_id)
|
||||
if profile is not None:
|
||||
results.append(profile)
|
||||
return results
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
# It must be an older profile from Cura 2.1.
|
||||
with open(file_name, encoding="utf-8") as fhandle:
|
||||
serialized = fhandle.read()
|
||||
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
|
||||
|
||||
## Convert a profile from an old Cura to this Cura if needed.
|
||||
#
|
||||
# \param serialized \type{str} The profile data to convert in the serialized on-disk format.
|
||||
# \param profile_id \type{str} The name of the profile.
|
||||
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
|
||||
def _upgradeProfile(self, serialized, profile_id):
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
if not "general" in parser:
|
||||
Logger.log("w", "Missing required section 'general'.")
|
||||
return []
|
||||
if not "version" in parser["general"]:
|
||||
Logger.log("w", "Missing required 'version' property")
|
||||
return []
|
||||
|
||||
version = int(parser["general"]["version"])
|
||||
if InstanceContainer.Version != version:
|
||||
name = parser["general"]["name"]
|
||||
return self._upgradeProfileVersion(serialized, name, version)
|
||||
else:
|
||||
return [(serialized, profile_id)]
|
||||
|
||||
## Load a profile from a serialized string.
|
||||
#
|
||||
# \param serialized \type{str} The profile data to read.
|
||||
# \param profile_id \type{str} The name of the profile.
|
||||
# \return \type{InstanceContainer|None}
|
||||
def _loadProfile(self, serialized, profile_id):
|
||||
# Create an empty profile.
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
try:
|
||||
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
|
||||
|
||||
## Upgrade a serialized profile to the current profile format.
|
||||
#
|
||||
# \param serialized \type{str} The profile data to convert.
|
||||
# \param profile_id \type{str} The name of the profile.
|
||||
# \param source_version \type{int} The profile version of 'serialized'.
|
||||
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
|
||||
def _upgradeProfileVersion(self, serialized, profile_id, source_version):
|
||||
converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter={"version_upgrade": {} }, active_only=True)
|
||||
|
||||
source_format = ("profile", source_version)
|
||||
profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins
|
||||
if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version]
|
||||
|
||||
if not profile_convert_funcs:
|
||||
return []
|
||||
|
||||
filenames, outputs = profile_convert_funcs[0](serialized, profile_id)
|
||||
if filenames is None and outputs is None:
|
||||
return []
|
||||
return list(zip(outputs, filenames))
|
||||
|
|
|
@ -70,10 +70,19 @@ class GCodeProfileReader(ProfileReader):
|
|||
|
||||
json_data = json.loads(serialized)
|
||||
|
||||
profile_strings = [json_data["global_quality"]]
|
||||
profile_strings.extend(json_data.get("extruder_quality", []))
|
||||
profiles = []
|
||||
global_profile = readQualityProfileFromString(json_data["global_quality"])
|
||||
|
||||
return [readQualityProfileFromString(profile_string) for profile_string in profile_strings]
|
||||
# This is a fix for profiles created with 2.3.0 For some reason it added the "extruder" property to the
|
||||
# global profile.
|
||||
# The fix is simple and safe, as a global profile should never have the extruder entry.
|
||||
if global_profile.getMetaDataEntry("extruder", None) is not None:
|
||||
global_profile.setMetaDataEntry("extruder", None)
|
||||
profiles.append(global_profile)
|
||||
|
||||
for profile_string in json_data.get("extruder_quality", []):
|
||||
profiles.append(readQualityProfileFromString(profile_string))
|
||||
return profiles
|
||||
|
||||
## Unescape a string which has been escaped for use in a gcode comment.
|
||||
#
|
||||
|
|
|
@ -13,6 +13,7 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
|||
|
||||
import re #For escaping characters in the settings.
|
||||
import json
|
||||
import copy
|
||||
|
||||
## Writes g-code to a file.
|
||||
#
|
||||
|
@ -46,7 +47,17 @@ class GCodeWriter(MeshWriter):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def write(self, stream, node, mode = MeshWriter.OutputMode.TextMode):
|
||||
## Writes the g-code for the entire scene to a stream.
|
||||
#
|
||||
# Note that even though the function accepts a collection of nodes, the
|
||||
# entire scene is always written to the file since it is not possible to
|
||||
# separate the g-code for just specific nodes.
|
||||
#
|
||||
# \param stream The stream to write the g-code to.
|
||||
# \param nodes This is ignored.
|
||||
# \param mode Additional information on how to format the g-code in the
|
||||
# file. This must always be text mode.
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
|
||||
if mode != MeshWriter.OutputMode.TextMode:
|
||||
Logger.log("e", "GCode Writer does not support non-text mode.")
|
||||
return False
|
||||
|
@ -66,8 +77,11 @@ class GCodeWriter(MeshWriter):
|
|||
## 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())
|
||||
if instance_container1.getDefinition():
|
||||
flat_container.setDefinition(instance_container1.getDefinition())
|
||||
else:
|
||||
flat_container.setDefinition(instance_container2.getDefinition())
|
||||
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
|
||||
|
||||
for key in instance_container2.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
|
||||
|
@ -89,23 +103,34 @@ class GCodeWriter(MeshWriter):
|
|||
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||
prefix_length = len(prefix)
|
||||
|
||||
container_with_profile = stack.findContainer({"type": "quality"})
|
||||
container_with_profile = stack.findContainer({"type": "quality_changes"})
|
||||
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)
|
||||
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile)
|
||||
|
||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
|
||||
flat_global_container.addMetaDataEntry("quality_type", stack.findContainer({"type": "quality"}).getMetaDataEntry("quality_type", "normal"))
|
||||
|
||||
serialized = flat_global_container.serialize()
|
||||
data = {"global_quality": serialized}
|
||||
|
||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
extruder_quality = extruder.findContainer({"type": "quality"})
|
||||
for extruder in sorted(ExtruderManager.getInstance().getMachineExtruders(stack.getId()), key = lambda k: k.getMetaDataEntry("position")):
|
||||
extruder_quality = extruder.findContainer({"type": "quality_changes"})
|
||||
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)
|
||||
|
||||
# Ensure that extruder is set. (Can happen if we have empty quality changes).
|
||||
if flat_extruder_quality.getMetaDataEntry("extruder", None) is None:
|
||||
flat_extruder_quality.addMetaDataEntry("extruder", extruder.getBottom().getId())
|
||||
|
||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
|
||||
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.findContainer({"type": "quality"}).getMetaDataEntry("quality_type", "normal"))
|
||||
extruder_serialized = flat_extruder_quality.serialize()
|
||||
data.setdefault("extruder_quality", []).append(extruder_serialized)
|
||||
|
||||
|
|
87
plugins/LayerView/LayerPass.py
Normal file
87
plugins/LayerView/LayerPass.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.ToolHandle import ToolHandle
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
||||
from UM.View.RenderPass import RenderPass
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import os.path
|
||||
|
||||
## RenderPass used to display g-code paths.
|
||||
class LayerPass(RenderPass):
|
||||
def __init__(self, width, height):
|
||||
super().__init__("layerview", width, height)
|
||||
|
||||
self._layer_shader = None
|
||||
self._tool_handle_shader = None
|
||||
self._gl = OpenGL.getInstance().getBindingsObject()
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
self._extruder_manager = ExtruderManager.getInstance()
|
||||
|
||||
self._layer_view = None
|
||||
|
||||
def setLayerView(self, layerview):
|
||||
self._layerview = layerview
|
||||
|
||||
def render(self):
|
||||
if not self._layer_shader:
|
||||
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layers.shader"))
|
||||
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
|
||||
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
|
||||
if not self._tool_handle_shader:
|
||||
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
|
||||
|
||||
self.bind()
|
||||
|
||||
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay)
|
||||
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if isinstance(node, ToolHandle):
|
||||
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
|
||||
|
||||
elif isinstance(node, SceneNode) and node.getMeshData() and node.isVisible():
|
||||
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._layerview._current_layer_num - self._layerview._solid_layers > -1 and not self._layerview._only_show_top_layers:
|
||||
start = 0
|
||||
end = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer, counts in element_counts.items():
|
||||
if layer + self._layerview._solid_layers > self._layerview._current_layer_num:
|
||||
break
|
||||
end += counts
|
||||
|
||||
# This uses glDrawRangeElements internally to only draw a certain range of lines.
|
||||
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
|
||||
batch.addItem(node.getWorldTransformation(), layer_data)
|
||||
batch.render(self._scene.getActiveCamera())
|
||||
|
||||
# Create a new batch that is not range-limited
|
||||
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid)
|
||||
|
||||
if self._layerview._current_layer_mesh:
|
||||
batch.addItem(node.getWorldTransformation(), self._layerview._current_layer_mesh)
|
||||
|
||||
if self._layerview._current_layer_jumps:
|
||||
batch.addItem(node.getWorldTransformation(), self._layerview._current_layer_jumps)
|
||||
|
||||
if len(batch.items) > 0:
|
||||
batch.render(self._scene.getActiveCamera())
|
||||
|
||||
# Render toolhandles on top of the layerview
|
||||
if len(tool_handle_batch.items) > 0:
|
||||
tool_handle_batch.render(self._scene.getActiveCamera())
|
||||
|
||||
self.release()
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.View.View import View
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Resources import Resources
|
||||
|
@ -12,9 +13,11 @@ from UM.Mesh.MeshBuilder import MeshBuilder
|
|||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.Message import Message
|
||||
from UM.Application import Application
|
||||
|
||||
from cura.ConvexHullNode import ConvexHullNode
|
||||
|
||||
|
@ -26,18 +29,16 @@ from . import LayerViewProxy
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from . import LayerPass
|
||||
|
||||
import numpy
|
||||
import os.path
|
||||
|
||||
## 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
|
||||
|
@ -46,17 +47,40 @@ class LayerView(View):
|
|||
self._activity = False
|
||||
self._old_max_layers = 0
|
||||
|
||||
self._busy = False
|
||||
|
||||
self._ghost_shader = None
|
||||
self._layer_pass = None
|
||||
self._composite_pass = None
|
||||
self._old_layer_bindings = None
|
||||
self._layerview_composite_shader = None
|
||||
self._old_composite_shader = None
|
||||
|
||||
self._global_container_stack = None
|
||||
self._proxy = LayerViewProxy.LayerViewProxy()
|
||||
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
|
||||
|
||||
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
|
||||
|
||||
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"))
|
||||
|
||||
def getActivity(self):
|
||||
return self._activity
|
||||
|
||||
def getLayerPass(self):
|
||||
if not self._layer_pass:
|
||||
# Currently the RenderPass constructor requires a size > 0
|
||||
# This should be fixed in RenderPass's constructor.
|
||||
self._layer_pass = LayerPass.LayerPass(1, 1)
|
||||
self._layer_pass.setLayerView(self)
|
||||
self.getRenderer().addRenderPass(self._layer_pass)
|
||||
return self._layer_pass
|
||||
|
||||
def getCurrentLayer(self):
|
||||
return self._current_layer_num
|
||||
|
||||
|
@ -84,9 +108,9 @@ class LayerView(View):
|
|||
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))
|
||||
if not self._ghost_shader:
|
||||
self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
|
||||
self._ghost_shader.setUniformValue("u_color", Color(32, 32, 32, 96))
|
||||
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
|
||||
|
@ -96,30 +120,7 @@ class LayerView(View):
|
|||
|
||||
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)
|
||||
renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
|
||||
|
||||
def setLayer(self, value):
|
||||
if self._current_layer_num != value:
|
||||
|
@ -153,7 +154,7 @@ class LayerView(View):
|
|||
|
||||
# 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.
|
||||
# slider.
|
||||
if new_max_layers > self._current_layer_num:
|
||||
self.maxLayersChanged.emit()
|
||||
self.setLayer(int(self._max_layers))
|
||||
|
@ -166,7 +167,7 @@ class LayerView(View):
|
|||
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.
|
||||
# as this caused some issues.
|
||||
def getProxy(self, engine, script_engine):
|
||||
return self._proxy
|
||||
|
||||
|
@ -184,6 +185,50 @@ class LayerView(View):
|
|||
self.setLayer(self._current_layer_num - 1)
|
||||
return True
|
||||
|
||||
if event.type == Event.ViewActivateEvent:
|
||||
# Make sure the LayerPass is created
|
||||
self.getLayerPass()
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
if not self._layerview_composite_shader:
|
||||
self._layerview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("LayerView"), "layerview_composite.shader"))
|
||||
|
||||
if not self._composite_pass:
|
||||
self._composite_pass = self.getRenderer().getRenderPass("composite")
|
||||
|
||||
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
|
||||
self._composite_pass.getLayerBindings().append("layerview")
|
||||
self._old_composite_shader = self._composite_pass.getCompositeShader()
|
||||
self._composite_pass.setCompositeShader(self._layerview_composite_shader)
|
||||
|
||||
elif event.type == Event.ViewDeactivateEvent:
|
||||
self._wireprint_warning_message.hide()
|
||||
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
||||
self._composite_pass.setLayerBindings(self._old_layer_bindings)
|
||||
self._composite_pass.setCompositeShader(self._old_composite_shader)
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
self._onPropertyChanged("wireframe_enabled", "value")
|
||||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if key == "wireframe_enabled" and property_name == "value":
|
||||
if self._global_container_stack.getProperty("wireframe_enabled", "value"):
|
||||
self._wireprint_warning_message.show()
|
||||
else:
|
||||
self._wireprint_warning_message.hide()
|
||||
|
||||
def _startUpdateTopLayers(self):
|
||||
if self._top_layers_job:
|
||||
self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
|
||||
|
@ -278,3 +323,4 @@ class _CreateTopLayersJob(Job):
|
|||
def cancel(self):
|
||||
self._cancel = True
|
||||
super().cancel()
|
||||
|
||||
|
|
35
plugins/LayerView/layers.shader
Normal file
35
plugins/LayerView/layers.shader
Normal file
|
@ -0,0 +1,35 @@
|
|||
[shaders]
|
||||
vertex =
|
||||
uniform highp mat4 u_modelViewProjectionMatrix;
|
||||
uniform lowp float u_active_extruder;
|
||||
uniform lowp float u_shade_factor;
|
||||
|
||||
attribute highp vec4 a_vertex;
|
||||
attribute lowp vec4 a_color;
|
||||
varying lowp vec4 v_color;
|
||||
void main()
|
||||
{
|
||||
gl_Position = u_modelViewProjectionMatrix * a_vertex;
|
||||
// shade the color depending on the extruder index stored in the alpha component of the color
|
||||
v_color = (a_color.a == u_active_extruder) ? a_color : a_color * u_shade_factor;
|
||||
v_color.a = 1.0;
|
||||
}
|
||||
|
||||
fragment =
|
||||
varying lowp vec4 v_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_FragColor = v_color;
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_active_extruder = 0.0
|
||||
u_shade_factor = 0.60
|
||||
|
||||
[bindings]
|
||||
u_modelViewProjectionMatrix = model_view_projection_matrix
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
a_color = color
|
78
plugins/LayerView/layerview_composite.shader
Normal file
78
plugins/LayerView/layerview_composite.shader
Normal file
|
@ -0,0 +1,78 @@
|
|||
[shaders]
|
||||
vertex =
|
||||
uniform highp mat4 u_modelViewProjectionMatrix;
|
||||
attribute highp vec4 a_vertex;
|
||||
attribute highp vec2 a_uvs;
|
||||
|
||||
varying highp vec2 v_uvs;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = u_modelViewProjectionMatrix * a_vertex;
|
||||
v_uvs = a_uvs;
|
||||
}
|
||||
|
||||
fragment =
|
||||
uniform sampler2D u_layer0;
|
||||
uniform sampler2D u_layer1;
|
||||
uniform sampler2D u_layer2;
|
||||
|
||||
uniform vec2 u_offset[9];
|
||||
|
||||
uniform vec4 u_background_color;
|
||||
uniform float u_outline_strength;
|
||||
uniform vec4 u_outline_color;
|
||||
|
||||
varying vec2 v_uvs;
|
||||
|
||||
float kernel[9];
|
||||
|
||||
const vec3 x_axis = vec3(1.0, 0.0, 0.0);
|
||||
const vec3 y_axis = vec3(0.0, 1.0, 0.0);
|
||||
const vec3 z_axis = vec3(0.0, 0.0, 1.0);
|
||||
|
||||
void main()
|
||||
{
|
||||
kernel[0] = 0.0; kernel[1] = 1.0; kernel[2] = 0.0;
|
||||
kernel[3] = 1.0; kernel[4] = -4.0; kernel[5] = 1.0;
|
||||
kernel[6] = 0.0; kernel[7] = 1.0; kernel[8] = 0.0;
|
||||
|
||||
vec4 result = u_background_color;
|
||||
|
||||
vec4 main_layer = texture2D(u_layer0, v_uvs);
|
||||
vec4 selection_layer = texture2D(u_layer1, v_uvs);
|
||||
vec4 layerview_layer = texture2D(u_layer2, v_uvs);
|
||||
|
||||
result = main_layer * main_layer.a + result * (1.0 - main_layer.a);
|
||||
result = layerview_layer * layerview_layer.a + result * (1.0 - layerview_layer.a);
|
||||
|
||||
vec4 sum = vec4(0.0);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
vec4 color = vec4(texture2D(u_layer1, v_uvs.xy + u_offset[i]).a);
|
||||
sum += color * (kernel[i] / u_outline_strength);
|
||||
}
|
||||
|
||||
if((selection_layer.rgb == x_axis || selection_layer.rgb == y_axis || selection_layer.rgb == z_axis))
|
||||
{
|
||||
gl_FragColor = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
gl_FragColor = mix(result, u_outline_color, abs(sum.a));
|
||||
}
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_layer0 = 0
|
||||
u_layer1 = 1
|
||||
u_layer2 = 2
|
||||
u_background_color = [0.965, 0.965, 0.965, 1.0]
|
||||
u_outline_strength = 1.0
|
||||
u_outline_color = [0.05, 0.66, 0.89, 1.0]
|
||||
|
||||
[bindings]
|
||||
|
||||
[attributes]
|
||||
a_vertex = vertex
|
||||
a_uvs = uv
|
|
@ -70,7 +70,8 @@
|
|||
"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"
|
||||
"ooze_shield_enabled": "ooze_shield",
|
||||
"skin_overlap": "fill_overlap"
|
||||
},
|
||||
|
||||
"defaults": {
|
||||
|
|
|
@ -66,8 +66,17 @@ class LegacyProfileReader(ProfileReader):
|
|||
def read(self, file_name):
|
||||
if file_name.split(".")[-1] != "ini":
|
||||
return None
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return None
|
||||
|
||||
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
if multi_extrusion:
|
||||
Logger.log("e", "Unable to import legacy profile %s. Multi extrusion is not supported", file_name)
|
||||
raise Exception("Unable to import legacy profile. Multi extrusion is not supported")
|
||||
|
||||
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
||||
profile = InstanceContainer("Imported Legacy Profile") #Create an empty profile.
|
||||
profile = InstanceContainer("Imported Legacy Profile") # Create an empty profile.
|
||||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
|
@ -111,7 +120,7 @@ class LegacyProfileReader(ProfileReader):
|
|||
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_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||
current_printer_definition = global_container_stack.getBottom()
|
||||
profile.setDefinition(current_printer_definition)
|
||||
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]
|
||||
|
@ -130,5 +139,6 @@ class LegacyProfileReader(ProfileReader):
|
|||
if len(profile.getAllKeys()) == 0:
|
||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||
profile.setDirty(True)
|
||||
profile.addMetaDataEntry("type", "quality")
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
profile.addMetaDataEntry("quality_type", "normal")
|
||||
return profile
|
|
@ -77,7 +77,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: buildAreaWidthField
|
||||
text: machineWidthProvider.properties.value
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: { machineWidthProvider.setPropertyValue("value", text); manager.forceUpdate() }
|
||||
}
|
||||
Label
|
||||
|
@ -93,7 +93,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: buildAreaDepthField
|
||||
text: machineDepthProvider.properties.value
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: { machineDepthProvider.setPropertyValue("value", text); manager.forceUpdate() }
|
||||
}
|
||||
Label
|
||||
|
@ -109,7 +109,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: buildAreaHeightField
|
||||
text: machineHeightProvider.properties.value
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: { machineHeightProvider.setPropertyValue("value", text); manager.forceUpdate() }
|
||||
}
|
||||
Label
|
||||
|
@ -147,8 +147,16 @@ Cura.MachineAction
|
|||
|
||||
ComboBox
|
||||
{
|
||||
model: ["RepRap (Marlin/Sprinter)", "UltiGCode"]
|
||||
currentIndex: machineGCodeFlavorProvider.properties.value != model[1] ? 0 : 1
|
||||
model: ["RepRap (Marlin/Sprinter)", "UltiGCode", "Repetier"]
|
||||
currentIndex:
|
||||
{
|
||||
var index = model.indexOf(machineGCodeFlavorProvider.properties.value);
|
||||
if(index == -1)
|
||||
{
|
||||
index = 0;
|
||||
}
|
||||
return index
|
||||
}
|
||||
onActivated:
|
||||
{
|
||||
machineGCodeFlavorProvider.setPropertyValue("value", model[index]);
|
||||
|
@ -182,7 +190,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: printheadXMinField
|
||||
text: getHeadPolygonCoord("x", "min")
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: setHeadPolygon()
|
||||
}
|
||||
Label
|
||||
|
@ -198,7 +206,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: printheadYMinField
|
||||
text: getHeadPolygonCoord("y", "min")
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: setHeadPolygon()
|
||||
}
|
||||
Label
|
||||
|
@ -214,7 +222,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: printheadXMaxField
|
||||
text: getHeadPolygonCoord("x", "max")
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: setHeadPolygon()
|
||||
}
|
||||
Label
|
||||
|
@ -230,7 +238,7 @@ Cura.MachineAction
|
|||
{
|
||||
id: printheadYMaxField
|
||||
text: getHeadPolygonCoord("y", "max")
|
||||
validator: RegExpValidator { regExp: /[0-9]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
onEditingFinished: setHeadPolygon()
|
||||
}
|
||||
Label
|
||||
|
|
|
@ -55,21 +55,30 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
|||
|
||||
# Add all instances that are not added, but are in visibility list
|
||||
for item in visible:
|
||||
if not settings.getInstance(item):
|
||||
if not settings.getInstance(item): # Setting was not added already.
|
||||
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 = None
|
||||
# Check from what stack we should copy the raw property of the setting from.
|
||||
if definition.limit_to_extruder != "-1" and self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
|
||||
|
||||
# Check if the found stack_number is in the extruder list of extruders.
|
||||
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
|
||||
stack_nr = -1
|
||||
|
||||
# Use the found stack number to get the right stack to copy the value from.
|
||||
if stack_nr in ExtruderManager.getInstance().extruderIds:
|
||||
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||
|
||||
# Use the raw property to set the value (so the inheritance doesn't break)
|
||||
if stack is not None:
|
||||
new_instance.setProperty("value", stack.getRawProperty(item, "value"))
|
||||
else:
|
||||
stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
new_instance.setProperty("value", stack.getProperty(item, "value"))
|
||||
new_instance.setProperty("value", None)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
settings.addInstance(new_instance)
|
||||
visibility_changed = True
|
||||
|
|
|
@ -31,7 +31,7 @@ Item {
|
|||
spacing: UM.Theme.getSize("default_margin").width
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@label", "Print model with")
|
||||
text: catalog.i18nc("@label Followed by extruder selection drop-down.", "Print model with")
|
||||
anchors.verticalCenter: extruderSelector.verticalCenter
|
||||
|
||||
color: UM.Theme.getColor("setting_control_text")
|
||||
|
@ -44,13 +44,11 @@ Item {
|
|||
|
||||
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
|
||||
id: extrudersModel
|
||||
onModelChanged: extruderSelector.color = extrudersModel.getItem(extruderSelector.currentIndex).color
|
||||
}
|
||||
property string color: extruders_model.getItem(extruderSelector.currentIndex).color
|
||||
visible: extruders_model.rowCount() > 1
|
||||
property string color: extrudersModel.getItem(extruderSelector.currentIndex).color
|
||||
visible: machineExtruderCount.properties.value > 1
|
||||
textRole: "name"
|
||||
width: UM.Theme.getSize("setting_control").width
|
||||
height: UM.Theme.getSize("section").height
|
||||
|
@ -130,19 +128,19 @@ Item {
|
|||
|
||||
onActivated:
|
||||
{
|
||||
UM.ActiveTool.setProperty("SelectedActiveExtruder", extruders_model.getItem(index).id);
|
||||
extruderSelector.color = extruders_model.getItem(index).color;
|
||||
UM.ActiveTool.setProperty("SelectedActiveExtruder", extrudersModel.getItem(index).id);
|
||||
extruderSelector.color = extrudersModel.getItem(index).color;
|
||||
}
|
||||
onModelChanged: updateCurrentIndex();
|
||||
|
||||
function updateCurrentIndex()
|
||||
{
|
||||
for(var i = 0; i < extruders_model.rowCount(); ++i)
|
||||
for(var i = 0; i < extrudersModel.rowCount(); ++i)
|
||||
{
|
||||
if(extruders_model.getItem(i).id == UM.ActiveTool.properties.getValue("SelectedActiveExtruder"))
|
||||
if(extrudersModel.getItem(i).id == UM.ActiveTool.properties.getValue("SelectedActiveExtruder"))
|
||||
{
|
||||
extruderSelector.currentIndex = i;
|
||||
extruderSelector.color = extruders_model.getItem(i).color;
|
||||
extruderSelector.color = extrudersModel.getItem(i).color;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -153,10 +151,11 @@ Item {
|
|||
|
||||
Column
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_lining").height
|
||||
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
|
||||
// It kinda looks ugly otherwise (big panel, no content on it)
|
||||
height: contents.count * UM.Theme.getSize("section").height < 200 ? contents.count * UM.Theme.getSize("section").height : 200
|
||||
property int maximumHeight: 200 * Screen.devicePixelRatio
|
||||
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
|
||||
|
||||
ScrollView
|
||||
{
|
||||
height: parent.height
|
||||
|
@ -165,6 +164,7 @@ Item {
|
|||
ListView
|
||||
{
|
||||
id: contents
|
||||
spacing: UM.Theme.getSize("default_lining").height
|
||||
|
||||
model: UM.SettingDefinitionsModel
|
||||
{
|
||||
|
@ -262,6 +262,14 @@ Item {
|
|||
storeIndex: 0
|
||||
removeUnusedValue: false
|
||||
}
|
||||
|
||||
// If the extruder by which the object needs to be printed is changed, ensure that the
|
||||
// display is also notified of the fact.
|
||||
Connections
|
||||
{
|
||||
target: extruderSelector
|
||||
onActivated: provider.forcePropertiesChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +401,7 @@ Item {
|
|||
}
|
||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||
expanded: [ "*" ]
|
||||
exclude: [ "machine_settings" ]
|
||||
exclude: [ "machine_settings", "command_line_settings" ]
|
||||
}
|
||||
delegate:Loader
|
||||
{
|
||||
|
@ -430,6 +438,16 @@ Item {
|
|||
]
|
||||
}
|
||||
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
id: machineExtruderCount
|
||||
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
key: "machine_extruder_count"
|
||||
watchedProperties: [ "value" ]
|
||||
storeIndex: 0
|
||||
}
|
||||
|
||||
SystemPalette { id: palette; }
|
||||
|
||||
Component
|
||||
|
|
|
@ -7,6 +7,8 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Event import Event
|
||||
|
||||
|
||||
## This tool allows the user to add & change settings per node in the scene.
|
||||
|
@ -20,6 +22,7 @@ class PerObjectSettingsTool(Tool):
|
|||
|
||||
self._advanced_mode = False
|
||||
self._multi_extrusion = False
|
||||
self._single_model_selected = False
|
||||
|
||||
Selection.selectionChanged.connect(self.propertyChanged)
|
||||
|
||||
|
@ -28,8 +31,13 @@ class PerObjectSettingsTool(Tool):
|
|||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
Selection.selectionChanged.connect(self._updateEnabled)
|
||||
|
||||
|
||||
def event(self, event):
|
||||
super().event(event)
|
||||
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
|
||||
self.operationStopped.emit(self)
|
||||
return False
|
||||
|
||||
def getSelectedObjectId(self):
|
||||
|
@ -71,12 +79,37 @@ class PerObjectSettingsTool(Tool):
|
|||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
self._multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
# Ensure that all extruder data is reset
|
||||
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())
|
||||
default_stack_id = global_container_stack.getId()
|
||||
else:
|
||||
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
|
||||
if default_stack:
|
||||
default_stack_id = default_stack.getId()
|
||||
else:
|
||||
default_stack_id = global_container_stack.getId()
|
||||
|
||||
root_node = Application.getInstance().getController().getScene().getRoot()
|
||||
for node in DepthFirstIterator(root_node):
|
||||
new_stack_id = default_stack_id
|
||||
# Get position of old extruder stack for this node
|
||||
old_extruder_pos = node.callDecoration("getActiveExtruderPosition")
|
||||
if old_extruder_pos is not None:
|
||||
# Fetch current (new) extruder stack at position
|
||||
new_stack = ExtruderManager.getInstance().getExtruderStack(old_extruder_pos)
|
||||
if new_stack:
|
||||
new_stack_id = new_stack.getId()
|
||||
node.callDecoration("setActiveExtruder", new_stack_id)
|
||||
|
||||
self._updateEnabled()
|
||||
|
||||
def _updateEnabled(self):
|
||||
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode or self._multi_extrusion)
|
||||
selected_objects = Selection.getAllSelectedObjects()
|
||||
if len(selected_objects)> 1:
|
||||
self._single_model_selected = False
|
||||
elif len(selected_objects) == 1 and selected_objects[0].callDecoration("isGroup"):
|
||||
self._single_model_selected = False # Group is selected, so tool needs to be disabled
|
||||
else:
|
||||
self._single_model_selected = True
|
||||
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, (self._advanced_mode or self._multi_extrusion) and self._single_model_selected)
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
|
||||
from UM.Application import Application
|
||||
|
@ -17,14 +20,24 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
super().__init__(device_id)
|
||||
|
||||
self.setName(device_name)
|
||||
self.setShortDescription(catalog.i18nc("@action:button", "Save to Removable Drive"))
|
||||
self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "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
|
||||
self._stream = None
|
||||
|
||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
||||
## Request the specified nodes to be written to the removable drive.
|
||||
#
|
||||
# \param nodes A collection of scene nodes that should be written to the
|
||||
# removable drive.
|
||||
# \param file_name \type{string} A suggestion for the file name to write
|
||||
# to. If none is provided, a file name will be made from the names of the
|
||||
# meshes.
|
||||
# \param limit_mimetypes Should we limit the available MIME types to the
|
||||
# MIME types available to the currently active machine?
|
||||
def requestWrite(self, nodes, 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()
|
||||
|
@ -49,15 +62,7 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
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()
|
||||
file_name = self._automaticFileName(nodes)
|
||||
|
||||
if extension: # Not empty string.
|
||||
extension = "." + extension
|
||||
|
@ -65,8 +70,9 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
|
||||
try:
|
||||
Logger.log("d", "Writing to %s", file_name)
|
||||
stream = open(file_name, "wt")
|
||||
job = WriteMeshJob(writer, stream, node, MeshWriter.OutputMode.TextMode)
|
||||
# Using buffering greatly reduces the write time for many lines of gcode
|
||||
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
|
||||
job = WriteMeshJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
|
||||
job.setFileName(file_name)
|
||||
job.progress.connect(self._onProgress)
|
||||
job.finished.connect(self._onFinished)
|
||||
|
@ -86,12 +92,33 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
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
|
||||
|
||||
## Generate a file name automatically for the specified nodes to be saved
|
||||
# in.
|
||||
#
|
||||
# The name generated will be the name of one of the nodes. Which node that
|
||||
# is can not be guaranteed.
|
||||
#
|
||||
# \param nodes A collection of nodes for which to generate a file name.
|
||||
def _automaticFileName(self, nodes):
|
||||
for root in nodes:
|
||||
for child in BreadthFirstIterator(root):
|
||||
if child.getMeshData():
|
||||
name = child.getName()
|
||||
if name:
|
||||
return name
|
||||
raise OutputDeviceError.WriteRequestFailedError("Could not find a file name when trying to write to {device}.".format(device = self.getName()))
|
||||
|
||||
def _onProgress(self, job, progress):
|
||||
if hasattr(job, "_message"):
|
||||
job._message.setProgress(progress)
|
||||
self.writeProgress.emit(self, progress)
|
||||
|
||||
def _onFinished(self, job):
|
||||
if self._stream:
|
||||
# Explicitly closing the stream flushes the write-buffer
|
||||
self._stream.close()
|
||||
self._stream = None
|
||||
|
||||
if hasattr(job, "_message"):
|
||||
job._message.hide()
|
||||
job._message = None
|
||||
|
@ -113,4 +140,9 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||
def _onActionTriggered(self, message, action):
|
||||
if action == "eject":
|
||||
if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self):
|
||||
message.hide()
|
||||
message.hide()
|
||||
|
||||
eject_message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(self.getName()))
|
||||
else:
|
||||
eject_message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(self.getName()))
|
||||
eject_message.show()
|
||||
|
|
|
@ -46,11 +46,6 @@ class RemovableDrivePlugin(OutputDevicePlugin):
|
|||
|
||||
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()
|
||||
return result
|
||||
|
||||
def performEjectDevice(self, device):
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
|
@ -18,6 +20,8 @@ import math
|
|||
import urllib.request
|
||||
import urllib.parse
|
||||
import ssl
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -43,9 +47,11 @@ class SliceInfoJob(Job):
|
|||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
|
||||
Logger.log("d", "Sending anonymous slice info to [%s]...", self.url)
|
||||
|
||||
try:
|
||||
f = urllib.request.urlopen(self.url, **kwoptions)
|
||||
Logger.log("i", "Sent anonymous slice info to %s", self.url)
|
||||
Logger.log("i", "Sent anonymous slice info.")
|
||||
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)
|
||||
|
@ -65,7 +71,7 @@ class SliceInfo(Extension):
|
|||
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 = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. 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()
|
||||
|
@ -80,48 +86,27 @@ class SliceInfo(Extension):
|
|||
Logger.log("d", "'info/send_slice_info' is turned off.")
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
# Listing all files placed on the buildplate
|
||||
modelhashes = []
|
||||
for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()):
|
||||
if type(node) is not SceneNode or not node.getMeshData():
|
||||
continue
|
||||
modelhashes.append(node.getMeshData().getHash())
|
||||
|
||||
# Creating md5sums and formatting them as discussed on JIRA
|
||||
modelhash_formatted = ",".join(modelhashes)
|
||||
|
||||
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
|
||||
# Send material per extruder
|
||||
material_used = [str(math.pi * material_radius * material_radius * material_length) for material_length in print_information.materialLengths]
|
||||
material_used = ",".join(material_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"),
|
||||
}
|
||||
containers = { "": global_container_stack.serialize() }
|
||||
for container in global_container_stack.getContainers():
|
||||
container_id = container.getId()
|
||||
try:
|
||||
|
@ -129,12 +114,24 @@ class SliceInfo(Extension):
|
|||
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.
|
||||
containers[container_id] = container_serialized
|
||||
else:
|
||||
Logger.log("i", "No data found in %s to be serialized!", container_id)
|
||||
|
||||
# Bundle the collected data
|
||||
submitted_data = {
|
||||
"processor": platform.processor(),
|
||||
"machine": platform.machine(),
|
||||
"platform": platform.platform(),
|
||||
"settings": json.dumps(containers), # bundle of containers with their serialized contents
|
||||
"version": Application.getInstance().getVersion(),
|
||||
"modelhash": modelhash_formatted,
|
||||
"printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
|
||||
"filament": material_used,
|
||||
"language": Preferences.getInstance().getValue("general/language"),
|
||||
}
|
||||
|
||||
# Convert data to bytes
|
||||
submitted_data = urllib.parse.urlencode(submitted_data)
|
||||
binary_data = submitted_data.encode("utf-8")
|
||||
|
@ -145,4 +142,4 @@ class SliceInfo(Extension):
|
|||
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.
|
||||
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.
|
||||
|
|
|
@ -12,12 +12,13 @@ from UM.Settings.Validator import ValidatorState
|
|||
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
import cura.Settings
|
||||
import cura.Settings.ExtrudersModel
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
||||
|
||||
import math
|
||||
|
||||
## Standard view for mesh models.
|
||||
|
||||
class SolidView(View):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
@ -27,7 +28,7 @@ class SolidView(View):
|
|||
self._enabled_shader = None
|
||||
self._disabled_shader = None
|
||||
|
||||
self._extruders_model = cura.Settings.ExtrudersModel.ExtrudersModel()
|
||||
self._extruders_model = ExtrudersModel()
|
||||
|
||||
def beginRendering(self):
|
||||
scene = self.getController().getScene()
|
||||
|
@ -46,22 +47,34 @@ class SolidView(View):
|
|||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
if multi_extrusion:
|
||||
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
|
||||
support_angle_stack = ExtruderManager.getInstance().getExtruderStack(support_extruder_nr)
|
||||
if not support_angle_stack:
|
||||
support_angle_stack = global_container_stack
|
||||
else:
|
||||
support_angle_stack = global_container_stack
|
||||
|
||||
if Preferences.getInstance().getValue("view/show_overhang"):
|
||||
angle = global_container_stack.getProperty("support_angle", "value")
|
||||
if angle is not None and global_container_stack.getProperty("support_angle", "validationState") == ValidatorState.Valid:
|
||||
angle = support_angle_stack.getProperty("support_angle", "value")
|
||||
# Make sure the overhang angle is valid before passing it to the shader
|
||||
# Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None)
|
||||
if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]:
|
||||
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle)))
|
||||
else:
|
||||
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang.
|
||||
else:
|
||||
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0)))
|
||||
|
||||
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
|
||||
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
if not node.render(renderer):
|
||||
if node.getMeshData() and node.isVisible():
|
||||
|
||||
uniforms = {}
|
||||
shade_factor = 1.0
|
||||
|
||||
if not multi_extrusion:
|
||||
if global_container_stack:
|
||||
material = global_container_stack.findContainer({ "type": "material" })
|
||||
|
@ -76,13 +89,17 @@ class SolidView(View):
|
|||
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
|
||||
|
||||
material_color = self._extruders_model.getItem(extruder_index)["color"]
|
||||
|
||||
if extruder_index != ExtruderManager.getInstance().activeExtruderIndex:
|
||||
# Shade objects that are printed with the non-active extruder 25% darker
|
||||
shade_factor = 0.6
|
||||
try:
|
||||
# Colors are passed as rgb hex strings (eg "#ffffff"), and the shader needs
|
||||
# an rgba list of floats (eg [1.0, 1.0, 1.0, 1.0])
|
||||
uniforms["diffuse_color"] = [
|
||||
int(material_color[1:3], 16) / 255,
|
||||
int(material_color[3:5], 16) / 255,
|
||||
int(material_color[5:7], 16) / 255,
|
||||
shade_factor * int(material_color[1:3], 16) / 255,
|
||||
shade_factor * int(material_color[3:5], 16) / 255,
|
||||
shade_factor * int(material_color[5:7], 16) / 255,
|
||||
1.0
|
||||
]
|
||||
except ValueError:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from .avr_isp import stk500v2, ispBase, intelHex
|
||||
|
@ -19,13 +19,12 @@ from PyQt5.QtCore import QUrl, pyqtSlot, pyqtSignal, pyqtProperty
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class USBPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
def __init__(self, serial_port):
|
||||
super().__init__(serial_port)
|
||||
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
|
||||
self.setShortDescription(catalog.i18nc("@action:button", "Print via USB"))
|
||||
self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB"))
|
||||
self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB"))
|
||||
self.setIconName("print")
|
||||
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
|
||||
|
@ -140,7 +139,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
# \param gcode_list List with gcode (strings).
|
||||
def printGCode(self, gcode_list):
|
||||
if self._progress or self._connection_state != ConnectionState.connected:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Printer is busy or not connected. Unable to start a new job."))
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to start a new job because the printer is busy or not connected."))
|
||||
self._error_message.show()
|
||||
Logger.log("d", "Printer is busy or not connected, aborting print")
|
||||
self.writeError.emit(self)
|
||||
|
@ -426,7 +425,14 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._error_state = error
|
||||
self.onError.emit()
|
||||
|
||||
def requestWrite(self, node, file_name = None, filter_by_machine = False):
|
||||
## Request the current scene to be sent to a USB-connected printer.
|
||||
#
|
||||
# \param nodes A collection of scene nodes to send. This is ignored.
|
||||
# \param file_name \type{string} A suggestion for a file name to write.
|
||||
# This is ignored.
|
||||
# \param filter_by_machine Whether to filter MIME types by machine. This
|
||||
# is ignored.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False):
|
||||
Application.getInstance().showPrintMonitor.emit(True)
|
||||
self.startPrint()
|
||||
|
||||
|
|
|
@ -103,10 +103,12 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
|
||||
self._firmware_view.show()
|
||||
|
||||
@pyqtSlot()
|
||||
def updateAllFirmware(self):
|
||||
@pyqtSlot(str)
|
||||
def updateAllFirmware(self, file_name):
|
||||
if file_name.startswith("file://"):
|
||||
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
|
||||
if not self._usb_output_devices:
|
||||
Message(i18n_catalog.i18nc("@info","Cannot update firmware, there were no connected printers found.")).show()
|
||||
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected.")).show()
|
||||
return
|
||||
|
||||
for printer_connection in self._usb_output_devices:
|
||||
|
@ -114,26 +116,26 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
self.spawnFirmwareInterface("")
|
||||
for printer_connection in self._usb_output_devices:
|
||||
try:
|
||||
self._usb_output_devices[printer_connection].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
|
||||
self._usb_output_devices[printer_connection].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
# Should only happen in dev environments where the resources/firmware folder is absent.
|
||||
self._usb_output_devices[printer_connection].setProgress(100, 100)
|
||||
Logger.log("w", "No firmware found for printer %s called '%s'" %(printer_connection, self._getDefaultFirmwareName()))
|
||||
Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
|
||||
Message(i18n_catalog.i18nc("@info",
|
||||
"Could not find firmware required for the printer at %s.") % printer_connection).show()
|
||||
self._firmware_view.close()
|
||||
|
||||
continue
|
||||
|
||||
@pyqtSlot(str, result = bool)
|
||||
def updateFirmwareBySerial(self, serial_port):
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def updateFirmwareBySerial(self, serial_port, file_name):
|
||||
if serial_port in self._usb_output_devices:
|
||||
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
|
||||
try:
|
||||
self._usb_output_devices[serial_port].updateFirmware(Resources.getPath(CuraApplication.ResourceTypes.Firmware, self._getDefaultFirmwareName()))
|
||||
self._usb_output_devices[serial_port].updateFirmware(file_name)
|
||||
except FileNotFoundError:
|
||||
self._firmware_view.close()
|
||||
Logger.log("e", "Could not find firmware required for this machine called '%s'" %(self._getDefaultFirmwareName()))
|
||||
Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
@ -147,7 +149,8 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
|
||||
return USBPrinterOutputDeviceManager._instance
|
||||
|
||||
def _getDefaultFirmwareName(self):
|
||||
@pyqtSlot(result = str)
|
||||
def getDefaultFirmwareName(self):
|
||||
# Check if there is a valid global container stack
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
|
@ -193,13 +196,13 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
|||
Logger.log("d", "Choosing basic firmware for machine %s.", machine_id)
|
||||
hex_file = machine_without_extras[machine_id] # Return "basic" firmware
|
||||
else:
|
||||
Logger.log("e", "There is no firmware for machine %s.", machine_id)
|
||||
Logger.log("w", "There is no firmware for machine %s.", machine_id)
|
||||
|
||||
if hex_file:
|
||||
return hex_file.format(baudrate=baudrate)
|
||||
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
|
||||
else:
|
||||
Logger.log("e", "Could not find any firmware for machine %s.", machine_id)
|
||||
raise FileNotFoundError()
|
||||
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
|
||||
return ""
|
||||
|
||||
## Helper to identify serial ports (and scan for them)
|
||||
def _addRemovePorts(self, serial_ports):
|
||||
|
|
|
@ -5,6 +5,7 @@ See: http://en.wikipedia.org/wiki/Intel_HEX
|
|||
This is a python 3 conversion of the code created by David Braam for the Cura project.
|
||||
"""
|
||||
import io
|
||||
from UM.Logger import Logger
|
||||
|
||||
def readHex(filename):
|
||||
"""
|
||||
|
@ -41,6 +42,6 @@ def readHex(filename):
|
|||
elif rec_type == 2: #Extended Segment Address Record
|
||||
extra_addr = int(line[9:13], 16) * 16
|
||||
else:
|
||||
print(rec_type, rec_len, addr, check_sum, line)
|
||||
Logger.log("d", "%s, %s, %s, %s, %s", rec_type, rec_len, addr, check_sum, line)
|
||||
f.close()
|
||||
return data
|
||||
|
|
|
@ -8,6 +8,7 @@ The ISP AVR programmer can load firmware into AVR chips. Which are commonly used
|
|||
"""
|
||||
|
||||
from . import chipDB
|
||||
from UM.Logger import Logger
|
||||
|
||||
class IspBase():
|
||||
"""
|
||||
|
@ -22,11 +23,11 @@ class IspBase():
|
|||
raise IspError("Chip with signature: " + str(self.getSignature()) + "not found")
|
||||
self.chipErase()
|
||||
|
||||
print("Flashing %i bytes" % len(flash_data))
|
||||
Logger.log("d", "Flashing %i bytes", len(flash_data))
|
||||
self.writeFlash(flash_data)
|
||||
print("Verifying %i bytes" % len(flash_data))
|
||||
Logger.log("d", "Verifying %i bytes", len(flash_data))
|
||||
self.verifyFlash(flash_data)
|
||||
print("Completed")
|
||||
Logger.log("d", "Completed")
|
||||
|
||||
def getSignature(self):
|
||||
"""
|
||||
|
|
|
@ -3,7 +3,6 @@ STK500v2 protocol implementation for programming AVR chips.
|
|||
The STK500v2 protocol is used by the ArduinoMega2560 and a few other Arduino platforms to load firmware.
|
||||
This is a python 3 conversion of the code created by David Braam for the Cura project.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
@ -11,6 +10,7 @@ import time
|
|||
from serial import Serial
|
||||
from serial import SerialException
|
||||
from serial import SerialTimeoutException
|
||||
from UM.Logger import Logger
|
||||
|
||||
from . import ispBase, intelHex
|
||||
|
||||
|
@ -27,7 +27,7 @@ class Stk500v2(ispBase.IspBase):
|
|||
self.close()
|
||||
try:
|
||||
self.serial = Serial(str(port), speed, timeout=1, writeTimeout=10000)
|
||||
except SerialException as e:
|
||||
except SerialException:
|
||||
raise ispBase.IspError("Failed to open serial port")
|
||||
except:
|
||||
raise ispBase.IspError("Unexpected error while connecting to serial port:" + port + ":" + str(sys.exc_info()[0]))
|
||||
|
@ -84,14 +84,14 @@ class Stk500v2(ispBase.IspBase):
|
|||
#Set load addr to 0, in case we have more then 64k flash we need to enable the address extension
|
||||
page_size = self.chip["pageSize"] * 2
|
||||
flash_size = page_size * self.chip["pageCount"]
|
||||
print("Writing flash")
|
||||
Logger.log("d", "Writing flash")
|
||||
if flash_size > 0xFFFF:
|
||||
self.sendMessage([0x06, 0x80, 0x00, 0x00, 0x00])
|
||||
else:
|
||||
self.sendMessage([0x06, 0x00, 0x00, 0x00, 0x00])
|
||||
load_count = (len(flash_data) + page_size - 1) / page_size
|
||||
for i in range(0, int(load_count)):
|
||||
recv = self.sendMessage([0x13, page_size >> 8, page_size & 0xFF, 0xc1, 0x0a, 0x40, 0x4c, 0x20, 0x00, 0x00] + flash_data[(i * page_size):(i * page_size + page_size)])
|
||||
self.sendMessage([0x13, page_size >> 8, page_size & 0xFF, 0xc1, 0x0a, 0x40, 0x4c, 0x20, 0x00, 0x00] + flash_data[(i * page_size):(i * page_size + page_size)])
|
||||
if self.progress_callback is not None:
|
||||
if self._has_checksum:
|
||||
self.progress_callback(i + 1, load_count)
|
||||
|
@ -151,7 +151,6 @@ class Stk500v2(ispBase.IspBase):
|
|||
raise ispBase.IspError("Timeout")
|
||||
b = struct.unpack(">B", s)[0]
|
||||
checksum ^= b
|
||||
#print(hex(b))
|
||||
if state == "Start":
|
||||
if b == 0x1B:
|
||||
state = "GetSeq"
|
||||
|
@ -183,11 +182,11 @@ class Stk500v2(ispBase.IspBase):
|
|||
def portList():
|
||||
ret = []
|
||||
import _winreg
|
||||
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM")
|
||||
key=_winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE,"HARDWARE\\DEVICEMAP\\SERIALCOMM") #@UndefinedVariable
|
||||
i=0
|
||||
while True:
|
||||
try:
|
||||
values = _winreg.EnumValue(key, i)
|
||||
values = _winreg.EnumValue(key, i) #@UndefinedVariable
|
||||
except:
|
||||
return ret
|
||||
if "USBSER" in values[0]:
|
||||
|
@ -206,7 +205,7 @@ def main():
|
|||
""" Entry point to call the stk500v2 programmer from the commandline. """
|
||||
import threading
|
||||
if sys.argv[1] == "AUTO":
|
||||
print(portList())
|
||||
Logger.log("d", "portList(): ", repr(portList()))
|
||||
for port in portList():
|
||||
threading.Thread(target=runProgrammer, args=(port,sys.argv[2])).start()
|
||||
time.sleep(5)
|
||||
|
|
|
@ -64,7 +64,7 @@ Cura.MachineAction
|
|||
{
|
||||
startBedLevelingButton.visible = false;
|
||||
bedlevelingButton.visible = true;
|
||||
manager.startCheck();
|
||||
manager.startBedLeveling();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ class UMOUpgradeSelection(MachineAction):
|
|||
if variant:
|
||||
if variant.getId() == "empty_variant":
|
||||
variant_index = global_container_stack.getContainerIndex(variant)
|
||||
self._createVariant(global_container_stack, variant_index)
|
||||
variant = self._createVariant(global_container_stack, variant_index)
|
||||
variant.setProperty("machine_heated_bed", "value", heated_bed)
|
||||
self.heatedBedChanged.emit()
|
||||
|
||||
|
@ -41,4 +41,5 @@ class UMOUpgradeSelection(MachineAction):
|
|||
new_variant.addMetaDataEntry("type", "variant")
|
||||
new_variant.setDefinition(global_container_stack.getBottom())
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
|
||||
global_container_stack.replaceContainer(variant_index, new_variant)
|
||||
global_container_stack.replaceContainer(variant_index, new_variant)
|
||||
return new_variant
|
|
@ -1,9 +1,17 @@
|
|||
from cura.MachineAction import MachineAction
|
||||
from UM.i18n import i18nCatalog
|
||||
import cura.Settings.CuraContainerRegistry
|
||||
import UM.Settings.DefinitionContainer
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UpgradeFirmwareMachineAction(MachineAction):
|
||||
def __init__(self):
|
||||
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
|
||||
self._qml_url = "UpgradeFirmwareMachineAction.qml"
|
||||
self._qml_url = "UpgradeFirmwareMachineAction.qml"
|
||||
cura.Settings.CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
# Add this action as a supported action to all machine definitions
|
||||
if isinstance(container, UM.Settings.DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
|
||||
UM.Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
||||
|
|
|
@ -5,6 +5,7 @@ import QtQuick 2.2
|
|||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.1
|
||||
import QtQuick.Dialogs 1.2 // For filedialog
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
@ -44,34 +45,45 @@ Cura.MachineAction
|
|||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: catalog.i18nc("@label", "The firmware shipping with new Ultimakers works, but upgrades have been made to make better prints, and make calibration easier.");
|
||||
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: upgradeText2
|
||||
anchors.top: upgradeText1.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: catalog.i18nc("@label", "Cura requires these new features and thus your firmware will most likely need to be upgraded. You can do so now.");
|
||||
}
|
||||
Row
|
||||
{
|
||||
anchors.top: upgradeText2.bottom
|
||||
anchors.top: upgradeText1.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: childrenRect.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
property var firmwareName: Cura.USBPrinterManager.getDefaultFirmwareName()
|
||||
Button
|
||||
{
|
||||
id: upgradeButton
|
||||
text: catalog.i18nc("@action:button","Upgrade to Marlin Firmware");
|
||||
id: autoUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
|
||||
enabled: parent.firmwareName != ""
|
||||
onClicked:
|
||||
{
|
||||
Cura.USBPrinterManager.updateAllFirmware()
|
||||
Cura.USBPrinterManager.updateAllFirmware(parent.firmwareName)
|
||||
}
|
||||
}
|
||||
Button
|
||||
{
|
||||
id: manualUpgradeButton
|
||||
text: catalog.i18nc("@action:button", "Upload custom Firmware");
|
||||
onClicked:
|
||||
{
|
||||
customFirmwareDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog
|
||||
{
|
||||
id: customFirmwareDialog
|
||||
title: catalog.i18nc("@title:window", "Select custom firmware")
|
||||
nameFilters: "Firmware image files (*.hex)"
|
||||
selectExisting: true
|
||||
onAccepted: Cura.USBPrinterManager.updateAllFirmware(fileUrl)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,13 @@
|
|||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import UM.VersionUpgrade #To indicate that a file is of incorrect format.
|
||||
import UM.VersionUpgradeManager #To schedule more files to be upgraded.
|
||||
import UM.Resources #To get the config storage path.
|
||||
|
||||
import configparser #To read config files.
|
||||
import io #To write config files to strings as if they were files.
|
||||
import os.path #To get the path to write new user profiles to.
|
||||
import urllib #To serialise the user container file name properly.
|
||||
|
||||
## Creates a new machine instance instance by parsing a serialised machine
|
||||
# instance in version 1 of the file format.
|
||||
|
@ -79,39 +83,40 @@ class MachineInstance:
|
|||
variant_materials = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariantForMaterials(self._variant_name, type_name)
|
||||
|
||||
#Convert to quality profile if we have one of the built-in profiles, otherwise convert to a quality-changes profile.
|
||||
if has_machine_qualities:
|
||||
material_name_in_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateMaterialForProfiles(self._active_material_name)
|
||||
variant_name_in_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariantForProfiles(self._variant_name)
|
||||
if self._active_profile_name in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.builtInProfiles(): #This is a built-in profile name. Convert to quality.
|
||||
quality_name = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_profile_name)
|
||||
else:
|
||||
quality_name = "normal" #We have a quality-changes profile. Base it on normal, since we have no information to indicate which one it should be based on.
|
||||
if self._active_material_name == "PLA" and self._type_name == "ultimaker2plus": #UM2+ uses a different naming scheme for PLA profiles.
|
||||
active_quality = material_name_in_quality + "_" + variant_name_in_quality + "_" + quality_name
|
||||
else:
|
||||
printer_name_in_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinterForProfile(self._type_name)
|
||||
active_quality = printer_name_in_quality + "_" + material_name_in_quality + "_" + variant_name_in_quality + "_" + quality_name
|
||||
|
||||
if self._active_profile_name in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.builtInProfiles():
|
||||
active_quality_changes = "empty_quality_changes"
|
||||
else: #No built-in profile. Translate this profile to quality-changes.
|
||||
active_quality_changes = material_name_in_quality + "_" + variant_name_in_quality + "_" + quality_name
|
||||
if self._active_profile_name in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.builtInProfiles():
|
||||
active_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_profile_name)
|
||||
active_quality_changes = "empty_quality_changes"
|
||||
else:
|
||||
if self._active_profile_name in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.builtInProfiles():
|
||||
active_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateProfile(self._active_profile_name)
|
||||
active_quality_changes = "empty_quality_changes"
|
||||
else:
|
||||
active_quality = "normal"
|
||||
active_quality_changes = self._active_profile_name
|
||||
active_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.getQualityFallback(type_name, variant, active_material)
|
||||
active_quality_changes = self._active_profile_name
|
||||
|
||||
if has_machine_qualities: #This machine now has machine-quality profiles.
|
||||
active_material += "_" + variant_materials #That means that the profile was split into multiple.
|
||||
current_settings = "empty" #The profile didn't know the definition ID when it was upgraded, so it will have been invalid. Sorry, your current settings are lost now.
|
||||
else:
|
||||
current_settings = self._name + "_current_settings"
|
||||
active_material += "_" + variant_materials
|
||||
|
||||
#Create a new user profile and schedule it to be upgraded.
|
||||
user_profile = configparser.ConfigParser(interpolation = None)
|
||||
user_profile["general"] = {
|
||||
"version": "2",
|
||||
"name": "Current settings",
|
||||
"definition": type_name
|
||||
}
|
||||
user_profile["metadata"] = {
|
||||
"type": "user",
|
||||
"machine": self._name
|
||||
}
|
||||
user_profile["values"] = {}
|
||||
|
||||
version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager.getInstance()
|
||||
user_storage = os.path.join(UM.Resources.getDataStoragePath(), next(iter(version_upgrade_manager.getStoragePaths("user"))))
|
||||
user_profile_file = os.path.join(user_storage, urllib.parse.quote_plus(self._name) + "_current_settings.inst.cfg")
|
||||
if not os.path.exists(user_storage):
|
||||
os.makedirs(user_storage)
|
||||
with open(user_profile_file, "w", encoding = "utf-8") as file_handle:
|
||||
user_profile.write(file_handle)
|
||||
version_upgrade_manager.upgradeExtraFile(user_storage, urllib.parse.quote_plus(self._name), "user")
|
||||
|
||||
containers = [
|
||||
current_settings,
|
||||
self._name + "_current_settings", #The current profile doesn't know the definition ID when it was upgraded, only the instance ID, so it will be invalid. Sorry, your current settings are lost now.
|
||||
active_quality_changes,
|
||||
active_quality,
|
||||
active_material,
|
||||
|
|
|
@ -5,6 +5,7 @@ import configparser #To read config files.
|
|||
import io #To write config files to strings as if they were files.
|
||||
|
||||
import UM.VersionUpgrade
|
||||
from UM.Logger import Logger
|
||||
|
||||
## Creates a new profile instance by parsing a serialised profile in version 1
|
||||
# of the file format.
|
||||
|
@ -49,7 +50,7 @@ class Profile:
|
|||
self._machine_type_id = parser.get("general", "machine_type", fallback = None)
|
||||
self._machine_variant_name = parser.get("general", "machine_variant", fallback = None)
|
||||
self._machine_instance_name = parser.get("general", "machine_instance", fallback = None)
|
||||
if "material" in parser["general"]:
|
||||
if "material" in parser["general"]: #Note: Material name is unused in this upgrade.
|
||||
self._material_name = parser.get("general", "material")
|
||||
elif self._type == "material":
|
||||
self._material_name = parser.get("general", "name", fallback = None)
|
||||
|
@ -80,7 +81,7 @@ class Profile:
|
|||
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
|
||||
|
||||
if self._name == "Current settings":
|
||||
self._filename += "_current_settings" #This resolves a duplicate ID arising from how Cura 2.1 stores its current settings.
|
||||
return None, None #Can't upgrade these, because the new current profile needs to specify the definition ID and the old file only had the machine instance, not the definition.
|
||||
|
||||
config = configparser.ConfigParser(interpolation = None)
|
||||
|
||||
|
@ -94,10 +95,8 @@ class Profile:
|
|||
config.set("general", "definition", "fdmprinter") #In this case, the machine definition is unknown, and it might now have machine-specific profiles, in which case this will fail.
|
||||
|
||||
config.add_section("metadata")
|
||||
if self._type:
|
||||
config.set("metadata", "type", self._type)
|
||||
else:
|
||||
config.set("metadata", "type", "quality")
|
||||
config.set("metadata", "quality_type", "normal") #This feature doesn't exist in 2.1 yet, so we don't know the actual quality type. For now, always base it on normal.
|
||||
config.set("metadata", "type", "quality_changes")
|
||||
if self._weight:
|
||||
config.set("metadata", "weight", str(self._weight))
|
||||
if self._machine_variant_name:
|
||||
|
@ -107,13 +106,13 @@ class Profile:
|
|||
config.set("metadata", "variant", self._machine_variant_name)
|
||||
|
||||
if self._settings:
|
||||
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._settings)
|
||||
self._settings = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._settings)
|
||||
config.add_section("values")
|
||||
for key, value in self._settings.items():
|
||||
config.set("values", key, str(value))
|
||||
|
||||
if self._changed_settings_defaults:
|
||||
VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._changed_settings_defaults)
|
||||
self._changed_settings_defaults = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettings(self._changed_settings_defaults)
|
||||
config.add_section("defaults")
|
||||
for key, value in self._changed_settings_defaults.items():
|
||||
config.set("defaults", key, str(value))
|
||||
|
@ -126,34 +125,6 @@ class Profile:
|
|||
for item in disabled_settings_defaults[1:]:
|
||||
disabled_defaults_string += "," + str(item)
|
||||
|
||||
#Material metadata may cause the file to split, so do it last to minimise processing time (do more with the copy).
|
||||
filenames = []
|
||||
configs = []
|
||||
if self._material_name and self._type != "material":
|
||||
config.set("metadata", "material", self._material_name)
|
||||
filenames.append(self._filename)
|
||||
configs.append(config)
|
||||
elif self._type != "material" and self._machine_type_id in VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.machinesWithMachineQuality():
|
||||
#Split this profile into multiple profiles, one for each material.
|
||||
_new_materials = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.machinesWithMachineQuality()[self._machine_type_id]["materials"]
|
||||
_new_variants = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.machinesWithMachineQuality()[self._machine_type_id]["variants"]
|
||||
translated_machine = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translatePrinter(self._machine_type_id)
|
||||
for material_id in _new_materials:
|
||||
for variant_id in _new_variants:
|
||||
variant_id_new = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariant(variant_id, translated_machine)
|
||||
filenames.append("{profile}_{material}_{variant}".format(profile = self._filename, material = material_id, variant = variant_id_new))
|
||||
config_copy = configparser.ConfigParser(interpolation = None)
|
||||
config_copy.read_dict(config) #Copy the config to a new ConfigParser instance.
|
||||
variant_id_new_materials = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateVariantForMaterials(variant_id, translated_machine)
|
||||
config_copy.set("metadata", "material", "{material}_{variant}".format(material = material_id, variant = variant_id_new_materials))
|
||||
configs.append(config_copy)
|
||||
else:
|
||||
configs.append(config)
|
||||
filenames.append(self._filename)
|
||||
|
||||
outputs = []
|
||||
for config in configs:
|
||||
output = io.StringIO()
|
||||
config.write(output)
|
||||
outputs.append(output.getvalue())
|
||||
return filenames, outputs
|
||||
output = io.StringIO()
|
||||
config.write(output)
|
||||
return [self._filename], [output.getvalue()]
|
|
@ -65,11 +65,63 @@ _printer_translations_profiles = {
|
|||
}
|
||||
|
||||
## How to translate profile names from the old version to the new.
|
||||
#
|
||||
# This must have an entry for every built-in profile, since it also services
|
||||
# as a set for which profiles were built-in.
|
||||
_profile_translations = {
|
||||
"Low Quality": "low",
|
||||
"Normal Quality": "normal",
|
||||
"High Quality": "high",
|
||||
"Ulti Quality": "high" #This one doesn't have an equivalent. Map it to high.
|
||||
"Ulti Quality": "high", #This one doesn't have an equivalent. Map it to high.
|
||||
"abs_0.25_normal": "um2p_abs_0.25_normal",
|
||||
"abs_0.4_fast": "um2p_abs_0.4_fast",
|
||||
"abs_0.4_high": "um2p_abs_0.4_high",
|
||||
"abs_0.4_normal": "um2p_abs_0.4_normal",
|
||||
"abs_0.6_normal": "um2p_abs_0.6_normal",
|
||||
"abs_0.8_normal": "um2p_abs_0.8_normal",
|
||||
"cpe_0.25_normal": "um2p_cpe_0.25_normal",
|
||||
"cpe_0.4_fast": "um2p_cpe_0.4_fast",
|
||||
"cpe_0.4_high": "um2p_cpe_0.4_high",
|
||||
"cpe_0.4_normal": "um2p_cpe_0.4_normal",
|
||||
"cpe_0.6_normal": "um2p_cpe_0.6_normal",
|
||||
"cpe_0.8_normal": "um2p_cpe_0.8_normal",
|
||||
"cpep_0.4_draft": "um2p_cpep_0.4_draft",
|
||||
"cpep_0.4_normal": "um2p_cpep_0.4_normal",
|
||||
"cpep_0.6_draft": "um2p_cpep_0.6_draft",
|
||||
"cpep_0.6_normal": "um2p_cpep_0.6_normal",
|
||||
"cpep_0.8_draft": "um2p_cpep_0.8_draft",
|
||||
"cpep_0.8_normal": "um2p_cpep_0.8_normal",
|
||||
"nylon_0.25_high": "um2p_nylon_0.25_high",
|
||||
"nylon_0.25_normal": "um2p_nylon_0.25_normal",
|
||||
"nylon_0.4_fast": "um2p_nylon_0.4_fast",
|
||||
"nylon_0.4_normal": "um2p_nylon_0.4_normal",
|
||||
"nylon_0.6_fast": "um2p_nylon_0.6_fast",
|
||||
"nylon_0.6_normal": "um2p_nylon_0.6_normal",
|
||||
"nylon_0.8_draft": "um2p_nylon_0.8_draft",
|
||||
"nylon_0.8_normal": "um2p_nylon_0.8_normal",
|
||||
"pc_0.25_high": "um2p_pc_0.25_high",
|
||||
"pc_0.25_normal": "um2p_pc_0.25_normal",
|
||||
"pc_0.4_fast": "um2p_pc_0.4_fast",
|
||||
"pc_0.4_normal": "um2p_pc_0.4_normal",
|
||||
"pc_0.6_fast": "um2p_pc_0.6_fast",
|
||||
"pc_0.6_normal": "um2p_pc_0.6_normal",
|
||||
"pc_0.8_draft": "um2p_pc_0.8_draft",
|
||||
"pc_0.8_normal": "um2p_pc_0.8_normal",
|
||||
"pla_0.25_normal": "pla_0.25_normal", #Note that the PLA profiles don't get the um2p_ prefix, though they are for UM2+.
|
||||
"pla_0.4_fast": "pla_0.4_fast",
|
||||
"pla_0.4_high": "pla_0.4_high",
|
||||
"pla_0.4_normal": "pla_0.4_normal",
|
||||
"pla_0.6_normal": "pla_0.6_normal",
|
||||
"pla_0.8_normal": "pla_0.8_normal",
|
||||
"tpu_0.25_high": "um2p_tpu_0.25_high",
|
||||
"tpu_0.4_normal": "um2p_tpu_0.4_normal",
|
||||
"tpu_0.6_fast": "um2p_tpu_0.6_fast"
|
||||
}
|
||||
|
||||
## Settings that are no longer in the new version.
|
||||
_removed_settings = {
|
||||
"fill_perimeter_gaps",
|
||||
"support_area_smoothing"
|
||||
}
|
||||
|
||||
## How to translate setting names from the old version to the new.
|
||||
|
@ -78,6 +130,7 @@ _setting_name_translations = {
|
|||
"remove_overlapping_walls_enabled": "travel_compensate_overlapping_walls_enabled",
|
||||
"remove_overlapping_walls_x_enabled": "travel_compensate_overlapping_walls_x_enabled",
|
||||
"retraction_hop": "retraction_hop_enabled",
|
||||
"skin_overlap": "infill_overlap",
|
||||
"skirt_line_width": "skirt_brim_line_width",
|
||||
"skirt_minimal_length": "skirt_brim_minimal_length",
|
||||
"skirt_speed": "skirt_brim_speed",
|
||||
|
@ -91,6 +144,54 @@ _setting_name_translations = {
|
|||
"support_roof_pattern": "support_interface_pattern"
|
||||
}
|
||||
|
||||
## Custom profiles become quality_changes. This dictates which quality to base
|
||||
# the quality_changes profile on.
|
||||
#
|
||||
# Which quality profile to base the quality_changes on depends on the machine,
|
||||
# material and nozzle.
|
||||
#
|
||||
# If a current configuration is missing, fall back to "normal".
|
||||
_quality_fallbacks = {
|
||||
"ultimaker2_plus": {
|
||||
"ultimaker2_plus_0.25": {
|
||||
"generic_abs": "um2p_abs_0.25_normal",
|
||||
"generic_cpe": "um2p_cpe_0.25_normal",
|
||||
#No CPE+.
|
||||
"generic_nylon": "um2p_nylon_0.25_normal",
|
||||
"generic_pc": "um2p_pc_0.25_normal",
|
||||
"generic_pla": "pla_0.25_normal",
|
||||
"generic_tpu": "um2p_tpu_0.25_high"
|
||||
},
|
||||
"ultimaker2_plus_0.4": {
|
||||
"generic_abs": "um2p_abs_0.4_normal",
|
||||
"generic_cpe": "um2p_cpe_0.4_normal",
|
||||
"generic_cpep": "um2p_cpep_0.4_normal",
|
||||
"generic_nylon": "um2p_nylon_0.4_normal",
|
||||
"generic_pc": "um2p_pc_0.4_normal",
|
||||
"generic_pla": "pla_0.4_normal",
|
||||
"generic_tpu": "um2p_tpu_0.4_normal"
|
||||
},
|
||||
"ultimaker2_plus_0.6": {
|
||||
"generic_abs": "um2p_abs_0.6_normal",
|
||||
"generic_cpe": "um2p_cpe_0.6_normal",
|
||||
"generic_cpep": "um2p_cpep_0.6_normal",
|
||||
"generic_nylon": "um2p_nylon_0.6_normal",
|
||||
"generic_pc": "um2p_pc_0.6_normal",
|
||||
"generic_pla": "pla_0.6_normal",
|
||||
"generic_tpu": "um2p_tpu_0.6_fast",
|
||||
},
|
||||
"ultimaker2_plus_0.8": {
|
||||
"generic_abs": "um2p_abs_0.8_normal",
|
||||
"generic_cpe": "um2p_cpe_0.8_normal",
|
||||
"generic_cpep": "um2p_cpep_0.8_normal",
|
||||
"generic_nylon": "um2p_nylon_0.8_normal",
|
||||
"generic_pc": "um2p_pc_0.8_normal",
|
||||
"generic_pla": "pla_0.8_normal",
|
||||
#No TPU.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
## How to translate variants of specific machines from the old version to the
|
||||
# new.
|
||||
_variant_translations = {
|
||||
|
@ -150,6 +251,25 @@ class VersionUpgrade21to22(VersionUpgrade):
|
|||
parser.read_string(serialised)
|
||||
return int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
|
||||
|
||||
## Gets the fallback quality to use for a specific machine-variant-material
|
||||
# combination.
|
||||
#
|
||||
# For custom profiles we fall back onto this quality profile, since we
|
||||
# don't know which quality profile it was based on.
|
||||
#
|
||||
# \param machine The machine ID of the user's configuration in 2.2.
|
||||
# \param variant The variant ID of the user's configuration in 2.2.
|
||||
# \param material The material ID of the user's configuration in 2.2.
|
||||
@staticmethod
|
||||
def getQualityFallback(machine, variant, material):
|
||||
if machine not in _quality_fallbacks:
|
||||
return "normal"
|
||||
if variant not in _quality_fallbacks[machine]:
|
||||
return "normal"
|
||||
if material not in _quality_fallbacks[machine][variant]:
|
||||
return "normal"
|
||||
return _quality_fallbacks[machine][variant][material]
|
||||
|
||||
## Gets the set of built-in profile names in Cura 2.1.
|
||||
#
|
||||
# This is required to test if profiles should be converted to a quality
|
||||
|
@ -271,15 +391,21 @@ class VersionUpgrade21to22(VersionUpgrade):
|
|||
# \return The same dictionary.
|
||||
@staticmethod
|
||||
def translateSettings(settings):
|
||||
new_settings = {}
|
||||
for key, value in settings.items():
|
||||
if key == "fill_perimeter_gaps": #Setting is removed.
|
||||
del settings[key]
|
||||
elif key == "retraction_combing": #Combing was made into an enum instead of a boolean.
|
||||
settings[key] = "off" if (value == "False") else "all"
|
||||
elif key in _setting_name_translations:
|
||||
del settings[key]
|
||||
settings[_setting_name_translations[key]] = value
|
||||
return settings
|
||||
if key in _removed_settings:
|
||||
continue
|
||||
if key == "retraction_combing": #Combing was made into an enum instead of a boolean.
|
||||
new_settings[key] = "off" if (value == "False") else "all"
|
||||
continue
|
||||
if key == "cool_fan_full_layer": #Layer counting was made one-indexed.
|
||||
new_settings[key] = str(int(value) + 1)
|
||||
continue
|
||||
if key in _setting_name_translations:
|
||||
new_settings[_setting_name_translations[key]] = value
|
||||
continue
|
||||
new_settings[key] = value
|
||||
return new_settings
|
||||
|
||||
## Translates a setting name for the change from Cura 2.1 to 2.2.
|
||||
#
|
||||
|
|
|
@ -35,6 +35,10 @@ def getMetaData():
|
|||
"preferences": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"."}
|
||||
},
|
||||
"user": {
|
||||
"get_version": upgrade.getCfgVersion,
|
||||
"location": {"./user"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
916
plugins/X3DReader/X3DReader.py
Normal file
916
plugins/X3DReader/X3DReader.py
Normal file
|
@ -0,0 +1,916 @@
|
|||
# Contributed by Seva Alekseyev <sevaa@nih.gov> with National Institutes of Health, 2016
|
||||
# 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.Job import Job
|
||||
from math import pi, sin, cos, sqrt
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# TODO: preserve the structure of scenes that contain several objects
|
||||
# Use CADPart, for example, to distinguish between separate objects
|
||||
|
||||
DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
|
||||
EPSILON = 0.000001
|
||||
|
||||
class Shape:
|
||||
|
||||
# Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
|
||||
# with vertices stored in rows
|
||||
def __init__(self, verts, faces, index_base, name):
|
||||
self.verts = verts
|
||||
self.faces = faces
|
||||
# Those are here for debugging purposes only
|
||||
self.index_base = index_base
|
||||
self.name = name
|
||||
|
||||
class X3DReader(MeshReader):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._supported_extensions = [".x3d"]
|
||||
self._namespaces = {}
|
||||
|
||||
# Main entry point
|
||||
# Reads the file, returns a SceneNode (possibly with nested ones), or None
|
||||
def read(self, file_name):
|
||||
try:
|
||||
self.defs = {}
|
||||
self.shapes = []
|
||||
|
||||
tree = ET.parse(file_name)
|
||||
xml_root = tree.getroot()
|
||||
|
||||
if xml_root.tag != "X3D":
|
||||
return None
|
||||
|
||||
scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters
|
||||
if xml_root[0].tag == "head":
|
||||
for head_node in xml_root[0]:
|
||||
if head_node.tag == "unit" and head_node.attrib.get("category") == "length":
|
||||
scale *= float(head_node.attrib["conversionFactor"])
|
||||
break
|
||||
xml_scene = xml_root[1]
|
||||
else:
|
||||
xml_scene = xml_root[0]
|
||||
|
||||
if xml_scene.tag != "Scene":
|
||||
return None
|
||||
|
||||
self.transform = Matrix()
|
||||
self.transform.setByScaleFactor(scale)
|
||||
self.index_base = 0
|
||||
|
||||
# Traverse the scene tree, populate the shapes list
|
||||
self.processChildNodes(xml_scene)
|
||||
|
||||
if self.shapes:
|
||||
builder = MeshBuilder()
|
||||
builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes]))
|
||||
builder.setIndices(numpy.concatenate([shape.faces for shape in self.shapes]))
|
||||
builder.calculateNormals()
|
||||
builder.setFileName(file_name)
|
||||
mesh_data = builder.build()
|
||||
|
||||
# Manually try and get the extents of the mesh_data. This should prevent nasty NaN issues from
|
||||
# leaving the reader.
|
||||
mesh_data.getExtents()
|
||||
|
||||
node = SceneNode()
|
||||
node.setMeshData(mesh_data)
|
||||
node.setSelectable(True)
|
||||
node.setName(file_name)
|
||||
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader")
|
||||
return None
|
||||
|
||||
return node
|
||||
|
||||
# ------------------------- XML tree traversal
|
||||
|
||||
def processNode(self, xml_node):
|
||||
xml_node = self.resolveDefUse(xml_node)
|
||||
if xml_node is None:
|
||||
return
|
||||
|
||||
tag = xml_node.tag
|
||||
if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"):
|
||||
self.processChildNodes(xml_node)
|
||||
if tag == "CADPart":
|
||||
self.processTransform(xml_node) # TODO: split the parts
|
||||
elif tag == "LOD":
|
||||
self.processNode(xml_node[0])
|
||||
elif tag == "Transform":
|
||||
self.processTransform(xml_node)
|
||||
elif tag == "Shape":
|
||||
self.processShape(xml_node)
|
||||
|
||||
|
||||
def processShape(self, xml_node):
|
||||
# Find the geometry and the appearance inside the Shape
|
||||
geometry = appearance = None
|
||||
for sub_node in xml_node:
|
||||
if sub_node.tag == "Appearance" and not appearance:
|
||||
appearance = self.resolveDefUse(sub_node)
|
||||
elif sub_node.tag in self.geometry_importers and not geometry:
|
||||
geometry = self.resolveDefUse(sub_node)
|
||||
|
||||
# TODO: appearance is completely ignored. At least apply the material color...
|
||||
if not geometry is None:
|
||||
try:
|
||||
self.verts = self.faces = [] # Safeguard
|
||||
self.geometry_importers[geometry.tag](self, geometry)
|
||||
m = self.transform.getData()
|
||||
verts = m.dot(self.verts)[:3].transpose()
|
||||
|
||||
self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag))
|
||||
self.index_base += len(verts)
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag)
|
||||
|
||||
# Returns the referenced node if the node has USE, the same node otherwise.
|
||||
# May return None is USE points at a nonexistent node
|
||||
# In X3DOM, when both DEF and USE are in the same node, DEF is ignored.
|
||||
# Big caveat: XML element objects may evaluate to boolean False!!!
|
||||
# Don't ever use "if node:", use "if not node is None:" instead
|
||||
def resolveDefUse(self, node):
|
||||
USE = node.attrib.get("USE")
|
||||
if USE:
|
||||
return self.defs.get(USE, None)
|
||||
|
||||
DEF = node.attrib.get("DEF")
|
||||
if DEF:
|
||||
self.defs[DEF] = node
|
||||
return node
|
||||
|
||||
def processChildNodes(self, node):
|
||||
for c in node:
|
||||
self.processNode(c)
|
||||
Job.yieldThread()
|
||||
|
||||
# Since this is a grouping node, will recurse down the tree.
|
||||
# According to the spec, the final transform matrix is:
|
||||
# T * C * R * SR * S * -SR * -C
|
||||
# Where SR corresponds to the rotation matrix to scaleOrientation
|
||||
# C and SR are rather exotic. S, slightly less so.
|
||||
def processTransform(self, node):
|
||||
rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
trans = readVector(node, "translation", (0, 0, 0)) # Vector
|
||||
scale = readVector(node, "scale", (1, 1, 1)) # Vector
|
||||
center = readVector(node, "center", (0, 0, 0)) # Vector
|
||||
scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple
|
||||
|
||||
# Store the previous transform; in Cura, the default matrix multiplication is in place
|
||||
prev = Matrix(self.transform.getData()) # It's deep copy, I've checked
|
||||
|
||||
# The rest of transform manipulation will be applied in place
|
||||
got_center = (center.x != 0 or center.y != 0 or center.z != 0)
|
||||
|
||||
T = self.transform
|
||||
if trans.x != 0 or trans.y != 0 or trans.z !=0:
|
||||
T.translate(trans)
|
||||
if got_center:
|
||||
T.translate(center)
|
||||
if rot[0] != 0:
|
||||
T.rotateByAxis(*rot)
|
||||
if scale.x != 1 or scale.y != 1 or scale.z != 1:
|
||||
got_scale_orient = scale_orient[0] != 0
|
||||
if got_scale_orient:
|
||||
T.rotateByAxis(*scale_orient)
|
||||
# No scale by vector in place operation in UM
|
||||
S = Matrix()
|
||||
S.setByScaleVector(scale)
|
||||
T.multiply(S)
|
||||
if got_scale_orient:
|
||||
T.rotateByAxis(-scale_orient[0], scale_orient[1])
|
||||
if got_center:
|
||||
T.translate(-center)
|
||||
|
||||
self.processChildNodes(node)
|
||||
self.transform = prev
|
||||
|
||||
# ------------------------- Geometry importers
|
||||
# They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest
|
||||
|
||||
# Primitives
|
||||
|
||||
def processGeometryBox(self, node):
|
||||
(dx, dy, dz) = readFloatArray(node, "size", [2, 2, 2])
|
||||
dx /= 2
|
||||
dy /= 2
|
||||
dz /= 2
|
||||
self.reserveFaceAndVertexCount(12, 8)
|
||||
|
||||
# xz plane at +y, ccw
|
||||
self.addVertex(dx, dy, dz)
|
||||
self.addVertex(-dx, dy, dz)
|
||||
self.addVertex(-dx, dy, -dz)
|
||||
self.addVertex(dx, dy, -dz)
|
||||
# xz plane at -y
|
||||
self.addVertex(dx, -dy, dz)
|
||||
self.addVertex(-dx, -dy, dz)
|
||||
self.addVertex(-dx, -dy, -dz)
|
||||
self.addVertex(dx, -dy, -dz)
|
||||
|
||||
self.addQuad(0, 1, 2, 3) # +y
|
||||
self.addQuad(4, 0, 3, 7) # +x
|
||||
self.addQuad(7, 3, 2, 6) # -z
|
||||
self.addQuad(6, 2, 1, 5) # -x
|
||||
self.addQuad(5, 1, 0, 4) # +z
|
||||
self.addQuad(7, 6, 5, 4) # -y
|
||||
|
||||
# The sphere is subdivided into nr rings and ns segments
|
||||
def processGeometrySphere(self, node):
|
||||
r = readFloat(node, "radius", 0.5)
|
||||
subdiv = readIntArray(node, "subdivision", None)
|
||||
if subdiv:
|
||||
if len(subdiv) == 1:
|
||||
nr = ns = subdiv[0]
|
||||
else:
|
||||
(nr, ns) = subdiv
|
||||
else:
|
||||
nr = ns = DEFAULT_SUBDIV
|
||||
|
||||
lau = pi / nr # Unit angle of latitude (rings) for the given tesselation
|
||||
lou = 2 * pi / ns # Unit angle of longitude (segments)
|
||||
|
||||
self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns)
|
||||
|
||||
# +y and -y poles
|
||||
self.addVertex(0, r, 0)
|
||||
self.addVertex(0, -r, 0)
|
||||
|
||||
# The non-polar vertices go from x=0, negative z plane counterclockwise -
|
||||
# to -x, to +z, to +x, back to -z
|
||||
for ring in range(1, nr):
|
||||
for seg in range(ns):
|
||||
self.addVertex(-r*sin(lou * seg) * sin(lau * ring),
|
||||
r*cos(lau * ring),
|
||||
-r*cos(lou * seg) * sin(lau * ring))
|
||||
|
||||
vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap
|
||||
|
||||
# Faces go in order: top cap, sides, bottom cap.
|
||||
# Sides go by ring then by segment.
|
||||
|
||||
# Caps
|
||||
# Top cap face vertices go in order: down right up
|
||||
# (starting from +y pole)
|
||||
# Bottom cap goes: up left down (starting from -y pole)
|
||||
for seg in range(ns):
|
||||
self.addTri(0, seg + 2, (seg + 1) % ns + 2)
|
||||
self.addTri(1, vb + (seg + 1) % ns, vb + seg)
|
||||
|
||||
# Sides
|
||||
# Side face vertices go in order: down right upleft, downright up left
|
||||
for ring in range(nr - 2):
|
||||
tvb = 2 + ring * ns
|
||||
# First vertex index for the top edge of the ring
|
||||
bvb = tvb + ns
|
||||
# First vertex index for the bottom edge of the ring
|
||||
for seg in range(ns):
|
||||
nseg = (seg + 1) % ns
|
||||
self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg)
|
||||
|
||||
def processGeometryCone(self, node):
|
||||
r = readFloat(node, "bottomRadius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
bottom = readBoolean(node, "bottom", True)
|
||||
side = readBoolean(node, "side", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
d = height / 2
|
||||
angle = 2 * pi / n
|
||||
|
||||
self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1)
|
||||
|
||||
# Vertex 0 is the apex, vertices 1..n are the bottom
|
||||
self.addVertex(0, d, 0)
|
||||
for i in range(n):
|
||||
self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i))
|
||||
|
||||
# Side face vertices go: up down right
|
||||
if side:
|
||||
for i in range(n):
|
||||
self.addTri(1 + (i + 1) % n, 0, 1 + i)
|
||||
if bottom:
|
||||
for i in range(2, n):
|
||||
self.addTri(1, i, i+1)
|
||||
|
||||
def processGeometryCylinder(self, node):
|
||||
r = readFloat(node, "radius", 1)
|
||||
height = readFloat(node, "height", 2)
|
||||
bottom = readBoolean(node, "bottom", True)
|
||||
side = readBoolean(node, "side", True)
|
||||
top = readBoolean(node, "top", True)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
nn = n * 2
|
||||
angle = 2 * pi / n
|
||||
hh = height/2
|
||||
|
||||
self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn)
|
||||
|
||||
# The seam is at x=0, z=-r, vertices go ccw -
|
||||
# to pos x, to neg z, to neg x, back to neg z
|
||||
for i in range(n):
|
||||
rs = -r * sin(angle * i)
|
||||
rc = -r * cos(angle * i)
|
||||
self.addVertex(rs, hh, rc)
|
||||
self.addVertex(rs, -hh, rc)
|
||||
|
||||
if side:
|
||||
for i in range(n):
|
||||
ni = (i + 1) % n
|
||||
self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1)
|
||||
|
||||
for i in range(2, nn-3, 2):
|
||||
if top:
|
||||
self.addTri(0, i, i+2)
|
||||
if bottom:
|
||||
self.addTri(1, i+1, i+3)
|
||||
|
||||
# Semi-primitives
|
||||
|
||||
def processGeometryElevationGrid(self, node):
|
||||
dx = readFloat(node, "xSpacing", 1)
|
||||
dz = readFloat(node, "zSpacing", 1)
|
||||
nx = readInt(node, "xDimension", 0)
|
||||
nz = readInt(node, "zDimension", 0)
|
||||
height = readFloatArray(node, "height", False)
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
|
||||
if nx <= 0 or nz <= 0 or len(height) < nx*nz:
|
||||
return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid
|
||||
|
||||
self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz)
|
||||
|
||||
for z in range(nz):
|
||||
for x in range(nx):
|
||||
self.addVertex(x * dx, height[z*nx + x], z * dz)
|
||||
|
||||
for z in range(1, nz):
|
||||
for x in range(1, nx):
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw)
|
||||
self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw)
|
||||
|
||||
def processGeometryExtrusion(self, node):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
begin_cap = readBoolean(node, "beginCap", True)
|
||||
end_cap = readBoolean(node, "endCap", True)
|
||||
cross = readFloatArray(node, "crossSection", (1, 1, 1, -1, -1, -1, -1, 1, 1, 1))
|
||||
cross = [(cross[i], cross[i+1]) for i in range(0, len(cross), 2)]
|
||||
spine = readFloatArray(node, "spine", (0, 0, 0, 0, 1, 0))
|
||||
spine = [(spine[i], spine[i+1], spine[i+2]) for i in range(0, len(spine), 3)]
|
||||
orient = readFloatArray(node, "orientation", None)
|
||||
if orient:
|
||||
# This converts X3D's axis/angle rotation to a 3x3 numpy matrix
|
||||
def toRotationMatrix(rot):
|
||||
(x, y, z) = rot[:3]
|
||||
a = rot[3]
|
||||
s = sin(a)
|
||||
c = cos(a)
|
||||
t = 1-c
|
||||
return numpy.array((
|
||||
(x * x * t + c, x * y * t - z*s, x * z * t + y * s),
|
||||
(x * y * t + z*s, y * y * t + c, y * z * t - x * s),
|
||||
(x * z * t - y * s, y * z * t + x * s, z * z * t + c)))
|
||||
|
||||
orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)]
|
||||
|
||||
scale = readFloatArray(node, "scale", None)
|
||||
if scale:
|
||||
scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1])))
|
||||
if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)]
|
||||
|
||||
|
||||
# Special treatment for the closed spine and cross section.
|
||||
# Let's save some memory by not creating identical but distinct vertices;
|
||||
# later we'll introduce conditional logic to link the last vertex with
|
||||
# the first one where necessary.
|
||||
crossClosed = cross[0] == cross[-1]
|
||||
if crossClosed:
|
||||
cross = cross[:-1]
|
||||
nc = len(cross)
|
||||
cross = [numpy.array((c[0], 0, c[1])) for c in cross]
|
||||
ncf = nc if crossClosed else nc - 1
|
||||
# Face count along the cross; for closed cross, it's the same as the
|
||||
# respective vertex count
|
||||
|
||||
spine_closed = spine[0] == spine[-1]
|
||||
if spine_closed:
|
||||
spine = spine[:-1]
|
||||
ns = len(spine)
|
||||
spine = [Vector(*s) for s in spine]
|
||||
nsf = ns if spine_closed else ns - 1
|
||||
|
||||
# This will be used for fallback, where the current spine point joins
|
||||
# two collinear spine segments. No need to recheck the case of the
|
||||
# closed spine/last-to-first point juncture; if there's an angle there,
|
||||
# it would kick in on the first iteration of the main loop by spine.
|
||||
def findFirstAngleNormal():
|
||||
for i in range(1, ns - 1):
|
||||
spt = spine[i]
|
||||
z = (spine[i + 1] - spt).cross(spine[i - 1] - spt)
|
||||
if z.length() > EPSILON:
|
||||
return z
|
||||
# All the spines are collinear. Fallback to the rotated source
|
||||
# XZ plane.
|
||||
# TODO: handle the situation where the first two spine points match
|
||||
if len(spine) < 2:
|
||||
return Vector(0, 0, 1)
|
||||
v = spine[1] - spine[0]
|
||||
orig_y = Vector(0, 1, 0)
|
||||
orig_z = Vector(0, 0, 1)
|
||||
if v.cross(orig_y).length() > EPSILON:
|
||||
# Spine at angle with global y - rotate the z accordingly
|
||||
a = v.cross(orig_y) # Axis of rotation to get to the Z
|
||||
(x, y, z) = a.normalized().getData()
|
||||
s = a.length()/v.length()
|
||||
c = sqrt(1-s*s)
|
||||
t = 1-c
|
||||
m = numpy.array((
|
||||
(x * x * t + c, x * y * t + z*s, x * z * t - y * s),
|
||||
(x * y * t - z*s, y * y * t + c, y * z * t + x * s),
|
||||
(x * z * t + y * s, y * z * t - x * s, z * z * t + c)))
|
||||
orig_z = Vector(*m.dot(orig_z.getData()))
|
||||
return orig_z
|
||||
|
||||
self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc)
|
||||
|
||||
z = None
|
||||
for i, spt in enumerate(spine):
|
||||
if (i > 0 and i < ns - 1) or spine_closed:
|
||||
snext = spine[(i + 1) % ns]
|
||||
sprev = spine[(i - 1 + ns) % ns]
|
||||
y = snext - sprev
|
||||
vnext = snext - spt
|
||||
vprev = sprev - spt
|
||||
try_z = vnext.cross(vprev)
|
||||
# Might be zero, then all kinds of fallback
|
||||
if try_z.length() > EPSILON:
|
||||
if z is not None and try_z.dot(z) < 0:
|
||||
try_z = -try_z
|
||||
z = try_z
|
||||
elif not z: # No z, and no previous z.
|
||||
# Look ahead, see if there's at least one point where
|
||||
# spines are not collinear.
|
||||
z = findFirstAngleNormal()
|
||||
elif i == 0: # And non-crossed
|
||||
snext = spine[i + 1]
|
||||
y = snext - spt
|
||||
z = findFirstAngleNormal()
|
||||
else: # last point and not crossed
|
||||
sprev = spine[i - 1]
|
||||
y = spt - sprev
|
||||
# If there's more than one point in the spine, z is already set.
|
||||
# One point in the spline is an error anyway.
|
||||
|
||||
z = z.normalized()
|
||||
y = y.normalized()
|
||||
x = y.cross(z) # Already normalized
|
||||
m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z)))
|
||||
|
||||
# Columns are the unit vectors for the xz plane for the cross-section
|
||||
if orient:
|
||||
mrot = orient[i] if len(orient) > 1 else orient[0]
|
||||
if not mrot is None:
|
||||
m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :(
|
||||
|
||||
if scale:
|
||||
mscale = scale[i] if len(scale) > 1 else scale[0]
|
||||
if not mscale is None:
|
||||
m = m.dot(mscale)
|
||||
|
||||
# First the cross-section 2-vector is scaled,
|
||||
# then rotated (which may make it a 3-vector),
|
||||
# then applied to the xz plane unit vectors
|
||||
|
||||
sptv3 = numpy.array(spt.getData()[:3])
|
||||
for cpt in cross:
|
||||
v = sptv3 + m.dot(cpt)
|
||||
self.addVertex(*v)
|
||||
|
||||
if begin_cap:
|
||||
self.addFace([x for x in range(nc - 1, -1, -1)], ccw)
|
||||
|
||||
# Order of edges in the face: forward along cross, forward along spine,
|
||||
# backward along cross, backward along spine, flipped if now ccw.
|
||||
# This order is assumed later in the texture coordinate assignment;
|
||||
# please don't change without syncing.
|
||||
|
||||
for s in range(ns - 1):
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc,
|
||||
(s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw)
|
||||
|
||||
if spine_closed:
|
||||
# The faces between the last and the first spine points
|
||||
b = (ns - 1) * nc
|
||||
for c in range(ncf):
|
||||
self.addQuadFlip(b + c, b + (c + 1) % nc,
|
||||
(c + 1) % nc, c, ccw)
|
||||
|
||||
if end_cap:
|
||||
self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw)
|
||||
|
||||
# Triangle meshes
|
||||
|
||||
# Helper for numerous nodes with a Coordinate subnode holding vertices
|
||||
# That all triangle meshes and IndexedFaceSet
|
||||
# num_faces can be a function, in case the face count is a function of vertex count
|
||||
def startCoordMesh(self, node, num_faces):
|
||||
ccw = readBoolean(node, "ccw", True)
|
||||
self.readVertices(node) # This will allocate and fill the vertex array
|
||||
if hasattr(num_faces, "__call__"):
|
||||
num_faces = num_faces(self.getVertexCount())
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
return ccw
|
||||
|
||||
|
||||
def processGeometryIndexedTriangleSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_faces = len(index) // 3
|
||||
ccw = int(self.startCoordMesh(node, num_faces))
|
||||
|
||||
for i in range(0, num_faces*3, 3):
|
||||
self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2])
|
||||
|
||||
def processGeometryIndexedTriangleStripSet(self, node):
|
||||
strips = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips])))
|
||||
|
||||
for strip in strips:
|
||||
sccw = ccw # Running CCW value, reset for each strip
|
||||
for i in range(len(strip) - 2):
|
||||
self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2])
|
||||
sccw = 1 - sccw
|
||||
|
||||
def processGeometryIndexedTriangleFanSet(self, node):
|
||||
fans = readIndex(node, "index")
|
||||
ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans])))
|
||||
|
||||
for fan in fans:
|
||||
for i in range(1, len(fan) - 1):
|
||||
self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw])
|
||||
|
||||
def processGeometryTriangleSet(self, node):
|
||||
ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3))
|
||||
for i in range(0, self.getVertexCount(), 3):
|
||||
self.addTri(i + 1 - ccw, i + ccw, i+2)
|
||||
|
||||
def processGeometryTriangleStripSet(self, node):
|
||||
strips = readIntArray(node, "stripCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips])))
|
||||
|
||||
vb = 0
|
||||
for n in strips:
|
||||
sccw = ccw
|
||||
for i in range(n-2):
|
||||
self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2)
|
||||
sccw = 1 - sccw
|
||||
vb += n
|
||||
|
||||
def processGeometryTriangleFanSet(self, node):
|
||||
fans = readIntArray(node, "fanCount", [])
|
||||
ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans])))
|
||||
|
||||
vb = 0
|
||||
for n in fans:
|
||||
for i in range(1, n-1):
|
||||
self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw)
|
||||
vb += n
|
||||
|
||||
# Quad geometries from the CAD module, might be relevant for printing
|
||||
|
||||
def processGeometryQuadSet(self, node):
|
||||
ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4))
|
||||
for i in range(0, self.getVertexCount(), 4):
|
||||
self.addQuadFlip(i, i+1, i+2, i+3, ccw)
|
||||
|
||||
def processGeometryIndexedQuadSet(self, node):
|
||||
index = readIntArray(node, "index", [])
|
||||
num_quads = len(index) // 4
|
||||
ccw = self.startCoordMesh(node, num_quads*2)
|
||||
|
||||
for i in range(0, num_quads*4, 4):
|
||||
self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw)
|
||||
|
||||
# 2D polygon geometries
|
||||
# Won't work for now, since Cura expects every mesh to have a nontrivial convex hull
|
||||
# The only way around that is merging meshes.
|
||||
|
||||
def processGeometryDisk2D(self, node):
|
||||
innerRadius = readFloat(node, "innerRadius", 0)
|
||||
outerRadius = readFloat(node, "outerRadius", 1)
|
||||
n = readInt(node, "subdivision", DEFAULT_SUBDIV)
|
||||
|
||||
angle = 2 * pi / n
|
||||
|
||||
self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n)
|
||||
|
||||
for i in range(n):
|
||||
s = sin(angle * i)
|
||||
c = cos(angle * i)
|
||||
self.addVertex(outerRadius*c, outerRadius*s, 0)
|
||||
if innerRadius:
|
||||
self.addVertex(innerRadius*c, innerRadius*s, 0)
|
||||
ni = (i+1) % n
|
||||
self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1)
|
||||
|
||||
if not innerRadius:
|
||||
for i in range(2, n):
|
||||
self.addTri(0, i-1, i)
|
||||
|
||||
def processGeometryRectangle2D(self, node):
|
||||
(x, y) = readFloatArray(node, "size", (2, 2))
|
||||
self.reserveFaceAndVertexCount(2, 4)
|
||||
self.addVertex(-x/2, -y/2, 0)
|
||||
self.addVertex(x/2, -y/2, 0)
|
||||
self.addVertex(x/2, y/2, 0)
|
||||
self.addVertex(-x/2, y/2, 0)
|
||||
self.addQuad(0, 1, 2, 3)
|
||||
|
||||
def processGeometryTriangleSet2D(self, node):
|
||||
verts = readFloatArray(node, "vertices", ())
|
||||
num_faces = len(verts) // 6;
|
||||
verts = [(verts[i], verts[i+1], 0) for i in range(0, 6 * num_faces, 2)]
|
||||
self.reserveFaceAndVertexCount(num_faces, num_faces * 3)
|
||||
for vert in verts:
|
||||
self.addVertex(*vert)
|
||||
|
||||
# The front face is on the +Z side, so CCW is a variable
|
||||
for i in range(0, num_faces*3, 3):
|
||||
a = Vector(*verts[i+2]) - Vector(*verts[i])
|
||||
b = Vector(*verts[i+1]) - Vector(*verts[i])
|
||||
self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x)
|
||||
|
||||
# General purpose polygon mesh
|
||||
|
||||
def processGeometryIndexedFaceSet(self, node):
|
||||
faces = readIndex(node, "coordIndex")
|
||||
ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces]))
|
||||
|
||||
for face in faces:
|
||||
if len(face) == 3:
|
||||
self.addTriFlip(face[0], face[1], face[2], ccw)
|
||||
elif len(face) > 3:
|
||||
self.addFace(face, ccw)
|
||||
|
||||
geometry_importers = {
|
||||
"IndexedFaceSet": processGeometryIndexedFaceSet,
|
||||
"IndexedTriangleSet": processGeometryIndexedTriangleSet,
|
||||
"IndexedTriangleStripSet": processGeometryIndexedTriangleStripSet,
|
||||
"IndexedTriangleFanSet": processGeometryIndexedTriangleFanSet,
|
||||
"TriangleSet": processGeometryTriangleSet,
|
||||
"TriangleStripSet": processGeometryTriangleStripSet,
|
||||
"TriangleFanSet": processGeometryTriangleFanSet,
|
||||
"QuadSet": processGeometryQuadSet,
|
||||
"IndexedQuadSet": processGeometryIndexedQuadSet,
|
||||
"TriangleSet2D": processGeometryTriangleSet2D,
|
||||
"Rectangle2D": processGeometryRectangle2D,
|
||||
"Disk2D": processGeometryDisk2D,
|
||||
"ElevationGrid": processGeometryElevationGrid,
|
||||
"Extrusion": processGeometryExtrusion,
|
||||
"Sphere": processGeometrySphere,
|
||||
"Box": processGeometryBox,
|
||||
"Cylinder": processGeometryCylinder,
|
||||
"Cone": processGeometryCone
|
||||
}
|
||||
|
||||
# Parses the Coordinate.@point field, fills the verts array.
|
||||
def readVertices(self, node):
|
||||
for c in node:
|
||||
if c.tag == "Coordinate":
|
||||
c = self.resolveDefUse(c)
|
||||
if not c is None:
|
||||
pt = c.attrib.get("point")
|
||||
if pt:
|
||||
co = [float(x) for x in pt.split()]
|
||||
num_verts = len(co) // 3
|
||||
self.verts = numpy.empty((4, num_verts), dtype=numpy.float32)
|
||||
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
|
||||
# Group by three
|
||||
for i in range(num_verts):
|
||||
self.verts[:3,i] = co[3*i:3*i+3]
|
||||
|
||||
# Mesh builder helpers
|
||||
|
||||
def reserveFaceAndVertexCount(self, num_faces, num_verts):
|
||||
# Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform
|
||||
self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32)
|
||||
self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32)
|
||||
self.num_verts = 0
|
||||
self.reserveFaceCount(num_faces)
|
||||
|
||||
def reserveFaceCount(self, num_faces):
|
||||
self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32)
|
||||
self.num_faces = 0
|
||||
|
||||
def getVertexCount(self):
|
||||
return self.verts.shape[1]
|
||||
|
||||
def addVertex(self, x, y, z):
|
||||
self.verts[0, self.num_verts] = x
|
||||
self.verts[1, self.num_verts] = y
|
||||
self.verts[2, self.num_verts] = z
|
||||
self.num_verts += 1
|
||||
|
||||
# Indices are 0-based for this shape, but they won't be zero-based in the merged mesh
|
||||
def addTri(self, a, b, c):
|
||||
self.faces[self.num_faces, 0] = self.index_base + a
|
||||
self.faces[self.num_faces, 1] = self.index_base + b
|
||||
self.faces[self.num_faces, 2] = self.index_base + c
|
||||
self.num_faces += 1
|
||||
|
||||
def addTriFlip(self, a, b, c, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
else:
|
||||
self.addTri(b, a, c)
|
||||
|
||||
# Needs to be convex, but not necessaily planar
|
||||
# Assumed ccw, cut along the ac diagonal
|
||||
def addQuad(self, a, b, c, d):
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
|
||||
def addQuadFlip(self, a, b, c, d, ccw):
|
||||
if ccw:
|
||||
self.addTri(a, b, c)
|
||||
self.addTri(c, d, a)
|
||||
else:
|
||||
self.addTri(a, c, b)
|
||||
self.addTri(c, a, d)
|
||||
|
||||
|
||||
# Arbitrary polygon triangulation.
|
||||
# Doesn't assume convexity and doesn't check the "convex" flag in the file.
|
||||
# Works by the "cutting of ears" algorithm:
|
||||
# - Find an outer vertex with the smallest angle and no vertices inside its adjacent triangle
|
||||
# - Remove the triangle at that vertex
|
||||
# - Repeat until done
|
||||
# Vertex coordinates are supposed to be already set
|
||||
def addFace(self, indices, ccw):
|
||||
# Resolve indices to coordinates for faster math
|
||||
face = [Vector(data=self.verts[0:3, i]) for i in indices]
|
||||
|
||||
# Need a normal to the plane so that we can know which vertices form inner angles
|
||||
normal = findOuterNormal(face)
|
||||
|
||||
if not normal: # Couldn't find an outer edge, non-planar polygon maybe?
|
||||
return
|
||||
|
||||
# Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done
|
||||
n = len(face)
|
||||
vi = [i for i in range(n)] # We'll be using this to kick vertices from the face
|
||||
while n > 3:
|
||||
max_cos = EPSILON # We don't want to check anything on Pi angles
|
||||
i_min = 0 # max cos corresponds to min angle
|
||||
for i in range(n):
|
||||
inext = (i + 1) % n
|
||||
iprev = (i + n - 1) % n
|
||||
v = face[vi[i]]
|
||||
next = face[vi[inext]] - v
|
||||
prev = face[vi[iprev]] - v
|
||||
nextXprev = next.cross(prev)
|
||||
if nextXprev.dot(normal) > EPSILON: # If it's an inner angle
|
||||
cos = next.dot(prev) / (next.length() * prev.length())
|
||||
if cos > max_cos:
|
||||
# Check if there are vertices inside the triangle
|
||||
no_points_inside = True
|
||||
for j in range(n):
|
||||
if j != i and j != iprev and j != inext:
|
||||
vx = face[vi[j]] - v
|
||||
if pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
no_points_inside = False
|
||||
break
|
||||
|
||||
if no_points_inside:
|
||||
max_cos = cos
|
||||
i_min = i
|
||||
|
||||
self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw)
|
||||
vi.pop(i_min)
|
||||
n -= 1
|
||||
self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# X3D field parsers
|
||||
# ------------------------------------------------------------
|
||||
def readFloatArray(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return [float(x) for x in s.split()]
|
||||
|
||||
def readIntArray(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return [int(x, 0) for x in s.split()]
|
||||
|
||||
def readFloat(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return float(s)
|
||||
|
||||
def readInt(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return int(s, 0)
|
||||
|
||||
def readBoolean(node, attr, default):
|
||||
s = node.attrib.get(attr)
|
||||
if not s:
|
||||
return default
|
||||
return s.lower() == "true"
|
||||
|
||||
def readVector(node, attr, default):
|
||||
v = readFloatArray(node, attr, default)
|
||||
return Vector(v[0], v[1], v[2])
|
||||
|
||||
def readRotation(node, attr, default):
|
||||
v = readFloatArray(node, attr, default)
|
||||
return (v[3], Vector(v[0], v[1], v[2]))
|
||||
|
||||
# Returns the -1-separated runs
|
||||
def readIndex(node, attr):
|
||||
v = readIntArray(node, attr, [])
|
||||
chunks = []
|
||||
chunk = []
|
||||
for i in range(len(v)):
|
||||
if v[i] == -1:
|
||||
if chunk:
|
||||
chunks.append(chunk)
|
||||
chunk = []
|
||||
else:
|
||||
chunk.append(v[i])
|
||||
if chunk:
|
||||
chunks.append(chunk)
|
||||
return chunks
|
||||
|
||||
# Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple
|
||||
# with a vector along the polygon sequence and a vector backwards
|
||||
def findOuterNormal(face):
|
||||
n = len(face)
|
||||
for i in range(n):
|
||||
for j in range(i+1, n):
|
||||
edge = face[j] - face[i]
|
||||
if edge.length() > EPSILON:
|
||||
edge = edge.normalized()
|
||||
prev_rejection = Vector()
|
||||
is_outer = True
|
||||
for k in range(n):
|
||||
if k != i and k != j:
|
||||
pt = face[k] - face[i]
|
||||
pte = pt.dot(edge)
|
||||
rejection = pt - edge*pte
|
||||
if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one
|
||||
is_outer = False
|
||||
break
|
||||
elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability
|
||||
prev_rejection = rejection
|
||||
|
||||
if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal.
|
||||
return edge.cross(prev_rejection)
|
||||
|
||||
return False
|
||||
|
||||
# Given two *collinear* vectors a and b, returns the coefficient that takes b to a.
|
||||
# No error handling.
|
||||
# For stability, taking the ration between the biggest coordinates would be better...
|
||||
def ratio(a, b):
|
||||
if b.x > EPSILON or b.x < -EPSILON:
|
||||
return a.x / b.x
|
||||
elif b.y > EPSILON or b.y < -EPSILON:
|
||||
return a.y / b.y
|
||||
else:
|
||||
return a.z / b.z
|
||||
|
||||
def pointInsideTriangle(vx, next, prev, nextXprev):
|
||||
vxXprev = vx.cross(prev)
|
||||
r = ratio(vxXprev, nextXprev)
|
||||
if r < 0:
|
||||
return False
|
||||
vxXnext = vx.cross(next);
|
||||
s = -ratio(vxXnext, nextXprev)
|
||||
return s > 0 and (s + r) < 1
|
||||
|
26
plugins/X3DReader/__init__.py
Normal file
26
plugins/X3DReader/__init__.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Seva Alekseyev with National Institutes of Health, 2016
|
||||
|
||||
from . import X3DReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "X3D Reader"),
|
||||
"author": "Seva Alekseyev",
|
||||
"version": "0.5",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading X3D files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "x3d",
|
||||
"description": catalog.i18nc("@item:inlistbox", "X3D File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return { "mesh_reader": X3DReader.X3DReader() }
|
|
@ -16,10 +16,6 @@ fragment =
|
|||
uniform sampler2D u_layer0;
|
||||
uniform sampler2D u_layer1;
|
||||
uniform sampler2D u_layer2;
|
||||
uniform sampler2D u_layer3;
|
||||
|
||||
uniform float u_imageWidth;
|
||||
uniform float u_imageHeight;
|
||||
|
||||
uniform vec2 u_offset[9];
|
||||
|
||||
|
@ -27,6 +23,10 @@ fragment =
|
|||
uniform vec4 u_outline_color;
|
||||
uniform vec4 u_error_color;
|
||||
|
||||
const vec3 x_axis = vec3(1.0, 0.0, 0.0);
|
||||
const vec3 y_axis = vec3(0.0, 1.0, 0.0);
|
||||
const vec3 z_axis = vec3(0.0, 0.0, 1.0);
|
||||
|
||||
varying vec2 v_uvs;
|
||||
|
||||
float kernel[9];
|
||||
|
@ -55,14 +55,21 @@ fragment =
|
|||
sum += color * (kernel[i] / u_outline_strength);
|
||||
}
|
||||
|
||||
gl_FragColor = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a));
|
||||
vec4 layer1 = texture2D(u_layer1, v_uvs);
|
||||
if((layer1.rgb == x_axis || layer1.rgb == y_axis || layer1.rgb == z_axis))
|
||||
{
|
||||
gl_FragColor = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
gl_FragColor = mix(result, vec4(abs(sum.a)) * u_outline_color, abs(sum.a));
|
||||
}
|
||||
}
|
||||
|
||||
[defaults]
|
||||
u_layer0 = 0
|
||||
u_layer1 = 1
|
||||
u_layer2 = 2
|
||||
u_layer3 = 3
|
||||
u_outline_strength = 1.0
|
||||
u_outline_color = [0.05, 0.66, 0.89, 1.0]
|
||||
u_error_color = [1.0, 0.0, 0.0, 1.0]
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import math
|
||||
import copy
|
||||
import io
|
||||
import xml.etree.ElementTree as ET
|
||||
import uuid
|
||||
|
||||
from UM.Resources import Resources
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
import UM.Dictionary
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
@ -18,55 +18,34 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
class XmlMaterialProfile(InstanceContainer):
|
||||
def __init__(self, container_id, *args, **kwargs):
|
||||
super().__init__(container_id, *args, **kwargs)
|
||||
self._inherited_files = []
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def duplicate(self, new_id, new_name = None):
|
||||
base_file = self.getMetaDataEntry("base_file", None)
|
||||
|
||||
if base_file != self.id:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = base_file)
|
||||
if containers:
|
||||
new_basefile = containers[0].duplicate(self.getMetaDataEntry("brand") + "_" + new_id, new_name)
|
||||
base_file = new_basefile.id
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_basefile)
|
||||
|
||||
new_id = self.getMetaDataEntry("brand") + "_" + new_id + "_" + self.getDefinition().getId()
|
||||
variant = self.getMetaDataEntry("variant")
|
||||
if variant:
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant)
|
||||
if variant_containers:
|
||||
new_id += "_" + variant_containers[0].getName().replace(" ", "_")
|
||||
has_base_file = True
|
||||
else:
|
||||
has_base_file = False
|
||||
|
||||
new_id = ContainerRegistry.getInstance().createUniqueName("material", self._id, new_id, "")
|
||||
result = super().duplicate(new_id, new_name)
|
||||
if has_base_file:
|
||||
result.setMetaDataEntry("base_file", base_file)
|
||||
else:
|
||||
result.setMetaDataEntry("base_file", result.id)
|
||||
return result
|
||||
def getInheritedFiles(self):
|
||||
return self._inherited_files
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def setReadOnly(self, read_only):
|
||||
super().setReadOnly(read_only)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is none, this is a basefile.
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container._read_only = read_only
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
|
||||
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container._read_only = read_only # prevent loop instead of calling setReadOnly
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
# set the meta data for all machine / variant combinations
|
||||
def setMetaDataEntry(self, key, value):
|
||||
if self.isReadOnly():
|
||||
return
|
||||
if self.getMetaDataEntry(key, None) == value:
|
||||
# Prevent loop caused by for loop.
|
||||
return
|
||||
|
||||
super().setMetaDataEntry(key, value)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is none, this is a basefile.
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
|
||||
# Update all containers that share GUID and basefile
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container.setMetaData(copy.deepcopy(self._metadata))
|
||||
for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container.setMetaDataEntry(key, value)
|
||||
|
||||
## Overridden from InstanceContainer, similar to setMetaDataEntry.
|
||||
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
|
||||
|
@ -81,7 +60,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
super().setName(new_name)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is none, this is a basefile.
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
|
||||
# Update the basefile as well, this is actually what we're trying to do
|
||||
# Update all containers that share GUID and basefile
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile)
|
||||
|
@ -89,17 +68,20 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
container.setName(new_name)
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def setProperty(self, key, property_name, property_value, container = None):
|
||||
if self.isReadOnly():
|
||||
return
|
||||
|
||||
super().setProperty(key, property_name, property_value)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is none, this is a basefile.
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container._dirty = True
|
||||
# def setProperty(self, key, property_name, property_value, container = None):
|
||||
# if self.isReadOnly():
|
||||
# return
|
||||
#
|
||||
# super().setProperty(key, property_name, property_value)
|
||||
#
|
||||
# basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
|
||||
# for container in UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
# if not container.isReadOnly():
|
||||
# container.setDirty(True)
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
# base file: global settings + supported machines
|
||||
# machine / variant combination: only changes for itself.
|
||||
def serialize(self):
|
||||
registry = ContainerRegistry.getInstance()
|
||||
|
||||
|
@ -108,7 +90,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
|
||||
# we should only serialize the "base" material definition, since that can then take care of
|
||||
# serializing the machine/nozzle specific profiles.
|
||||
raise NotImplementedError("Cannot serialize non-root XML materials")
|
||||
raise NotImplementedError("Ignoring serializing non-root XML materials, the data is contained in the base material")
|
||||
|
||||
builder = ET.TreeBuilder()
|
||||
|
||||
|
@ -150,6 +132,10 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
for key, value in metadata.items():
|
||||
builder.start(key)
|
||||
# Normally value is a string.
|
||||
# Nones get handled well.
|
||||
if isinstance(value, bool):
|
||||
value = str(value) # parseBool in deserialize expects 'True'.
|
||||
builder.data(value)
|
||||
builder.end(key)
|
||||
|
||||
|
@ -177,7 +163,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
machine_container_map = {}
|
||||
machine_nozzle_map = {}
|
||||
|
||||
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"))
|
||||
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self._id)
|
||||
for container in all_containers:
|
||||
definition_id = container.getDefinition().id
|
||||
if definition_id == "fdmprinter":
|
||||
|
@ -201,7 +187,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
try:
|
||||
product = UM.Dictionary.findKey(self.__product_id_map, definition_id)
|
||||
except ValueError:
|
||||
continue
|
||||
# An unknown product id; export it anyway
|
||||
product = definition_id
|
||||
|
||||
builder.start("machine")
|
||||
builder.start("machine_identifier", { "manufacturer": definition.getMetaDataEntry("manufacturer", ""), "product": product})
|
||||
|
@ -220,7 +207,17 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
if not variant_containers:
|
||||
continue
|
||||
|
||||
builder.start("hotend", { "id": variant_containers[0].getName() })
|
||||
builder.start("hotend", {"id": variant_containers[0].getName()})
|
||||
|
||||
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
|
||||
compatible = hotend.getMetaDataEntry("compatible")
|
||||
if compatible is not None:
|
||||
builder.start("setting", {"key": "hardware compatible"})
|
||||
if compatible:
|
||||
builder.data("yes")
|
||||
else:
|
||||
builder.data("no")
|
||||
builder.end("setting")
|
||||
|
||||
for instance in hotend.findInstances():
|
||||
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
|
||||
|
@ -242,10 +239,115 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
_indent(root)
|
||||
stream = io.StringIO()
|
||||
tree = ET.ElementTree(root)
|
||||
tree.write(stream, "unicode", True)
|
||||
tree.write(stream, encoding="unicode", xml_declaration=True)
|
||||
|
||||
return stream.getvalue()
|
||||
|
||||
# Recursively resolve loading inherited files
|
||||
def _resolveInheritance(self, file_name):
|
||||
xml = self._loadFile(file_name)
|
||||
|
||||
inherits = xml.find("./um:inherits", self.__namespaces)
|
||||
if inherits is not None:
|
||||
inherited = self._resolveInheritance(inherits.text)
|
||||
xml = self._mergeXML(inherited, xml)
|
||||
|
||||
return xml
|
||||
|
||||
def _loadFile(self, file_name):
|
||||
path = Resources.getPath(CuraApplication.getInstance().ResourceTypes.MaterialInstanceContainer, file_name + ".xml.fdm_material")
|
||||
|
||||
with open(path, encoding="utf-8") as f:
|
||||
contents = f.read()
|
||||
|
||||
self._inherited_files.append(path)
|
||||
return ET.fromstring(contents)
|
||||
|
||||
# The XML material profile can have specific settings for machines.
|
||||
# Some machines share profiles, so they are only created once.
|
||||
# This function duplicates those elements so that each machine tag only has one identifier.
|
||||
def _expandMachinesXML(self, element):
|
||||
settings_element = element.find("./um:settings", self.__namespaces)
|
||||
machines = settings_element.iterfind("./um:machine", self.__namespaces)
|
||||
machines_to_add = []
|
||||
machines_to_remove = []
|
||||
for machine in machines:
|
||||
identifiers = list(machine.iterfind("./um:machine_identifier", self.__namespaces))
|
||||
has_multiple_identifiers = len(identifiers) > 1
|
||||
if has_multiple_identifiers:
|
||||
# Multiple identifiers found. We need to create a new machine element and copy all it's settings there.
|
||||
for identifier in identifiers:
|
||||
new_machine = copy.deepcopy(machine)
|
||||
# Create list of identifiers that need to be removed from the copied element.
|
||||
other_identifiers = [self._createKey(other_identifier) for other_identifier in identifiers if other_identifier is not identifier]
|
||||
# As we can only remove by exact object reference, we need to look through the identifiers of copied machine.
|
||||
new_machine_identifiers = list(new_machine.iterfind("./um:machine_identifier", self.__namespaces))
|
||||
for new_machine_identifier in new_machine_identifiers:
|
||||
key = self._createKey(new_machine_identifier)
|
||||
# Key was in identifiers to remove, so this element needs to be purged
|
||||
if key in other_identifiers:
|
||||
new_machine.remove(new_machine_identifier)
|
||||
machines_to_add.append(new_machine)
|
||||
machines_to_remove.append(machine)
|
||||
else:
|
||||
pass # Machine only has one identifier. Nothing to do.
|
||||
# Remove & add all required machines.
|
||||
for machine_to_remove in machines_to_remove:
|
||||
settings_element.remove(machine_to_remove)
|
||||
for machine_to_add in machines_to_add:
|
||||
settings_element.append(machine_to_add)
|
||||
return element
|
||||
|
||||
def _mergeXML(self, first, second):
|
||||
result = copy.deepcopy(first)
|
||||
self._combineElement(self._expandMachinesXML(result), self._expandMachinesXML(second))
|
||||
return result
|
||||
|
||||
def _createKey(self, element):
|
||||
key = element.tag.split("}")[-1]
|
||||
if "key" in element.attrib:
|
||||
key += " key:" + element.attrib["key"]
|
||||
if "manufacturer" in element.attrib:
|
||||
key += " manufacturer:" + element.attrib["manufacturer"]
|
||||
if "product" in element.attrib:
|
||||
key += " product:" + element.attrib["product"]
|
||||
if key == "machine":
|
||||
for item in element:
|
||||
if "machine_identifier" in item.tag:
|
||||
key += " " + item.attrib["product"]
|
||||
return key
|
||||
|
||||
# Recursively merges XML elements. Updates either the text or children if another element is found in first.
|
||||
# If it does not exist, copies it from second.
|
||||
def _combineElement(self, first, second):
|
||||
# Create a mapping from tag name to element.
|
||||
|
||||
mapping = {}
|
||||
for element in first:
|
||||
key = self._createKey(element)
|
||||
mapping[key] = element
|
||||
for element in second:
|
||||
key = self._createKey(element)
|
||||
if len(element): # Check if element has children.
|
||||
try:
|
||||
if "setting" in element.tag and not "settings" in element.tag:
|
||||
# Setting can have points in it. In that case, delete all values and override them.
|
||||
for child in list(mapping[key]):
|
||||
mapping[key].remove(child)
|
||||
for child in element:
|
||||
mapping[key].append(child)
|
||||
else:
|
||||
self._combineElement(mapping[key], element) # Multiple elements, handle those.
|
||||
except KeyError:
|
||||
mapping[key] = element
|
||||
first.append(element)
|
||||
else:
|
||||
try:
|
||||
mapping[key].text = element.text
|
||||
except KeyError: # Not in the mapping, so simply add it
|
||||
mapping[key] = element
|
||||
first.append(element)
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def deserialize(self, serialized):
|
||||
data = ET.fromstring(serialized)
|
||||
|
@ -256,6 +358,11 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
# TODO: Add material verfication
|
||||
self.addMetaDataEntry("status", "unknown")
|
||||
|
||||
inherits = data.find("./um:inherits", self.__namespaces)
|
||||
if inherits is not None:
|
||||
inherited = self._resolveInheritance(inherits.text)
|
||||
data = self._mergeXML(inherited, data)
|
||||
|
||||
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
|
||||
for entry in metadata:
|
||||
tag_name = _tag_without_namespace(entry)
|
||||
|
@ -312,6 +419,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
else:
|
||||
Logger.log("d", "Unsupported material setting %s", key)
|
||||
|
||||
self.addMetaDataEntry("compatible", global_compatibility)
|
||||
|
||||
self._dirty = False
|
||||
|
||||
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
|
||||
|
@ -333,8 +442,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
for identifier in identifiers:
|
||||
machine_id = self.__product_id_map.get(identifier.get("product"), None)
|
||||
if machine_id is None:
|
||||
Logger.log("w", "Cannot create material for unknown machine %s", identifier.get("product"))
|
||||
continue
|
||||
# Lets try again with some naive heuristics.
|
||||
machine_id = identifier.get("product").replace(" ", "").lower()
|
||||
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id)
|
||||
if not definitions:
|
||||
|
@ -348,6 +457,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
new_material.setName(self.getName())
|
||||
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_material.setDefinition(definition)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_material.getMetaData()["compatible"] = machine_compatibility
|
||||
|
||||
for key, value in global_setting_values.items():
|
||||
new_material.setProperty(key, "value", value, definition)
|
||||
|
@ -359,6 +470,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
ContainerRegistry.getInstance().addContainer(new_material)
|
||||
|
||||
|
||||
hotends = machine.iterfind("./um:hotend", self.__namespaces)
|
||||
for hotend in hotends:
|
||||
hotend_id = hotend.get("id")
|
||||
|
@ -387,14 +499,13 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
else:
|
||||
Logger.log("d", "Unsupported material setting %s", key)
|
||||
|
||||
if not hotend_compatibility:
|
||||
continue
|
||||
|
||||
new_hotend_material = XmlMaterialProfile(self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_"))
|
||||
new_hotend_material.setName(self.getName())
|
||||
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_hotend_material.setDefinition(definition)
|
||||
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
|
||||
|
||||
for key, value in global_setting_values.items():
|
||||
new_hotend_material.setProperty(key, "value", value, definition)
|
||||
|
@ -408,12 +519,6 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
new_hotend_material._dirty = False
|
||||
ContainerRegistry.getInstance().addContainer(new_hotend_material)
|
||||
|
||||
if not global_compatibility:
|
||||
# Change the type of this container so it is not shown as an option in menus.
|
||||
# This uses InstanceContainer.setMetaDataEntry because otherwise all containers that
|
||||
# share this basefile are also updated.
|
||||
super().setMetaDataEntry("type", "incompatible_material")
|
||||
|
||||
def _addSettingElement(self, builder, instance):
|
||||
try:
|
||||
key = UM.Dictionary.findKey(self.__material_property_setting_map, instance.definition.key)
|
||||
|
|
40
resources/definitions/bfb.def.json
Normal file
40
resources/definitions/bfb.def.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"id": "bfb",
|
||||
"name": "BFB",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "BFB",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"speed_topbottom": { "default_value": 40 },
|
||||
"speed_print": { "default_value": 40 },
|
||||
"machine_extruder_count": { "default_value": 1 },
|
||||
"prime_tower_size": { "default_value": 7.745966692414834 },
|
||||
"machine_name": { "default_value": "BFB_Test" },
|
||||
"machine_heated_bed": { "default_value": false },
|
||||
"machine_nozzle_size": { "default_value": 0.5 },
|
||||
"speed_layer_0": { "default_value": 25 },
|
||||
"machine_width": { "default_value": 275 },
|
||||
"machine_gcode_flavor": { "default_value": "BFB" },
|
||||
"machine_depth": { "default_value": 265 },
|
||||
"speed_infill": { "default_value": 30 },
|
||||
"material_diameter": { "default_value": 1.7 },
|
||||
"machine_center_is_zero": { "default_value": true },
|
||||
"machine_height": { "default_value": 240 },
|
||||
"layer_height": { "default_value": 0.25 },
|
||||
"material_print_temperature": { "default_value": 200 },
|
||||
"retraction_amount": { "default_value": 0.05 },
|
||||
"speed_wall_0": { "default_value": 25 },
|
||||
"speed_travel": { "default_value": 50 },
|
||||
"infill_sparse_density": { "default_value": 10 },
|
||||
"layer_height_0": { "default_value": 0.5 },
|
||||
"speed_wall_x": { "default_value": 20 }
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "BQ Prusa i3 Hephestos" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F1200 ;move Z to position 15.0 mm\nG92 E0 ;zero the extruded length\nG1 E20 F200 ;extrude 20mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F7200 ;set feedrate to 120 mm/s\n; -- end of START GCODE --"
|
||||
},
|
||||
|
@ -36,7 +37,7 @@
|
|||
"default_value": false
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap"
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"layer_height": {
|
||||
"default_value": 0.2
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "BQ Hephestos 2" },
|
||||
"machine_start_gcode": { "default_value": "; -- START GCODE --\nM104 S{material_print_temperature} ; Heat up extruder while leveling\nM800 ; Custom GCODE to fire start print procedure\nM109 S{material_print_temperature} ; Makes sure the temperature is correct before printing\n; -- end of START GCODE --" },
|
||||
"machine_end_gcode": { "default_value": "; -- END GCODE --\nM801 ; Custom GCODE to fire end print procedure\n; -- end of END GCODE --" },
|
||||
"machine_width": { "default_value": 210 },
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "BQ Prusa i3 Hephestos XL" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F1200 ;move Z to position 15.0 mm\nG92 E0 ;zero the extruded length\nG1 E20 F200 ;extrude 20mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F7200 ;set feedrate to 120 mm/s\n; -- end of START GCODE --"
|
||||
},
|
||||
|
@ -36,7 +37,7 @@
|
|||
"default_value": false
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap"
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"layer_height": {
|
||||
"default_value": 0.2
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "BQ Witbox" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 X0 Y0 ;move to the X/Y origin (Home)\nG28 Z0 ;move to the Z origin (Home)\nG1 Z15.0 F1200 ;move Z to position 15.0 mm\nG92 E0 ;zero the extruded length\nG1 E20 F200 ;extrude 20mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F7200 ;set feedrate to 120 mm/s\n; -- end of START GCODE --"
|
||||
},
|
||||
|
@ -36,7 +37,7 @@
|
|||
"default_value": false
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap"
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"layer_height": {
|
||||
"default_value": 0.2
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "BQ Witbox 2" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "; -- START GCODE --\nM800 ; Custom GCODE to fire start print procedure\n; -- end of START GCODE --"
|
||||
},
|
||||
|
@ -36,7 +37,7 @@
|
|||
"default_value": false
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap"
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"material_print_temperature": {
|
||||
"default_value": 210
|
||||
|
|
35
resources/definitions/deltabot.def.json
Normal file
35
resources/definitions/deltabot.def.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"id": "deltabot",
|
||||
"name": "DeltaBot",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Danny Lu",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"speed_travel": { "default_value": 150 },
|
||||
"prime_tower_size": { "default_value": 8.660254037844387 },
|
||||
"infill_sparse_density": { "default_value": 10 },
|
||||
"speed_wall_x": { "default_value": 30 },
|
||||
"speed_wall_0": { "default_value": 30 },
|
||||
"speed_topbottom": { "default_value": 30 },
|
||||
"layer_height": { "default_value": 0.2 },
|
||||
"machine_nozzle_size": { "default_value": 0.5 },
|
||||
"speed_print": { "default_value": 30 },
|
||||
"speed_infill": { "default_value": 30 },
|
||||
"machine_extruder_count": { "default_value": 1 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": true },
|
||||
"machine_height": { "default_value": 150 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_depth": { "default_value": 150 },
|
||||
"machine_width": { "default_value": 150 },
|
||||
"machine_name": { "default_value": "DeltaBot style" }
|
||||
}
|
||||
}
|
|
@ -177,7 +177,8 @@
|
|||
"minimum_value_warning": "machine_nozzle_offset_x",
|
||||
"maximum_value": "machine_width",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
"settable_per_extruder": true,
|
||||
"enabled": false
|
||||
},
|
||||
"extruder_prime_pos_y":
|
||||
{
|
||||
|
@ -189,7 +190,8 @@
|
|||
"minimum_value_warning": "machine_nozzle_offset_y",
|
||||
"maximum_value_warning": "machine_depth",
|
||||
"settable_per_mesh": false,
|
||||
"settable_per_extruder": true
|
||||
"settable_per_extruder": true,
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "German RepRap Neo" },
|
||||
"machine_width": {
|
||||
"default_value": 150
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Innovo INVENTOR" },
|
||||
"machine_width": {
|
||||
"default_value": 340
|
||||
},
|
||||
|
@ -53,7 +54,7 @@
|
|||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G28 ; Home extruder\nM107 ; Turn off fan\nG90 ; Absolute positioning\nM82 ; Extruder in absolute mode\n{IF_BED}M190 S{BED}\n{IF_EXT0}M104 T0 S{TEMP0}\n{IF_EXT0}M109 T0 S{TEMP0}\n{IF_EXT1}M104 T1 S{TEMP1}\n{IF_EXT1}M109 T1 S{TEMP1}\nG32 S3 ; auto level\nG92 E0 ; Reset extruder position"
|
||||
"default_value": "G28 ; Home extruder\nM107 ; Turn off fan\nG90 ; Absolute positioning\nM82 ; Extruder in absolute mode\nM190 S{material_bed_temperature}\nM104 T0 S{material_print_temperature}\nM109 T0 S{material_print_temperature}\nM104 T1 S{material_print_temperature}\nM109 T1 S{material_print_temperature}\nG32 S3 ; auto level\nG92 E0 ; Reset extruder position"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ; turn off extruders\nM140 S0 ; heated bed heater off\nG91 ; relative positioning\nG1 E-2 F5000; retract 2mm\nG28 Z; move bed down\nG90 ; absolute positioning\nM84 ; disable motors"
|
||||
|
|
52
resources/definitions/julia.def.json
Normal file
52
resources/definitions/julia.def.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"id": "julia",
|
||||
"name": "Julia",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Fracktal",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_start_gcode": {
|
||||
"default_value": " ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {infill_sparse_density}\n ;metric values\n M107\n G28\n G29\n G90 ;absolute positioning\n G92 E0; reset extruder distance\n G1 Z5 F300 ;move nozzle up 5mm for safe homing\n G1 X0 Y0 Z0 F5000; move nozzle to home\n M300 S600P200\n M300 S800 P200\n M190 S{material_bed_temperature} ;Uncomment to add your own bed temperature line\n M109 S{material_print_temperature} ;Uncomment to add your own temperature line\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E3 ;extrude 3mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel}\n ;Put printing message on LCD screen\n M117 Printing...\n"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": " M104 S0 ;extruder heater off\n M140 S0 ;heated bed heater off (if you have it)\n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning\n"
|
||||
},
|
||||
"material_bed_temperature": { "default_value": 100 },
|
||||
"layer_height": { "default_value": 0.2 },
|
||||
"support_angle": { "default_value": 30 },
|
||||
"infill_overlap": { "default_value": 30 },
|
||||
"layer_height_0": { "default_value": 0.2 },
|
||||
"speed_print": { "default_value": 80 },
|
||||
"speed_wall_0": { "default_value": 30 },
|
||||
"speed_travel": { "default_value": 150 },
|
||||
"brim_line_count": { "default_value": 15 },
|
||||
"skin_overlap": { "default_value": 30 },
|
||||
"prime_tower_size": { "default_value": 8.660254037844387 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"bottom_thickness": { "default_value": 0.8 },
|
||||
"retraction_amount": { "default_value": 3 },
|
||||
"speed_topbottom": { "default_value": 80 },
|
||||
"material_print_temperature": { "default_value": 230 },
|
||||
"support_pattern": { "default_value": "grid" },
|
||||
"speed_infill": { "default_value": 80 },
|
||||
"infill_sparse_density": { "default_value": 10 },
|
||||
"top_thickness": { "default_value": 0.8 },
|
||||
"machine_extruder_count": { "default_value": 1 },
|
||||
"retraction_combing": { "default_value": "off" },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_height": { "default_value": 260 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_depth": { "default_value": 250 },
|
||||
"machine_width": { "default_value": 210 },
|
||||
"machine_name": { "default_value": "Julia V2" }
|
||||
}
|
||||
}
|
67
resources/definitions/kossel_mini.def.json
Normal file
67
resources/definitions/kossel_mini.def.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"id": "kossel_mini",
|
||||
"version": 2,
|
||||
"name": "Kossel Mini",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Claudio Sampaio (Patola)",
|
||||
"manufacturer": "Other",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"icon": "icon_ultimaker2",
|
||||
"platform": "kossel_platform.stl"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_heated_bed": {
|
||||
"default_value": true
|
||||
},
|
||||
"machine_width": {
|
||||
"default_value": 170
|
||||
},
|
||||
"machine_height": {
|
||||
"default_value": 200
|
||||
},
|
||||
"machine_depth": {
|
||||
"default_value": 170
|
||||
},
|
||||
"machine_center_is_zero": {
|
||||
"default_value": true
|
||||
},
|
||||
"machine_nozzle_size": {
|
||||
"default_value": 0.4
|
||||
},
|
||||
"material_diameter": {
|
||||
"default_value": 1.75
|
||||
},
|
||||
"machine_nozzle_heat_up_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_nozzle_cool_down_speed": {
|
||||
"default_value": 2
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 ;Home all axes (max endstops)\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG28 ;Home all axes (max endstops)\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
},
|
||||
"machine_disallowed_areas": {
|
||||
"default_value": [
|
||||
[[-34, -85], [ -85, -85], [-70, -70]],
|
||||
[[-85, -85], [-85, -34], [-70, -70]],
|
||||
[[34, -85], [ 85, -85], [70, -70]],
|
||||
[[85, -85], [85, -34], [70, -70]],
|
||||
[[-34, 85], [ -85, 85], [-70, 70]],
|
||||
[[-85, 85], [-85, 34], [-70, 70]],
|
||||
[[34, 85], [ 85, 85], [70, 70]],
|
||||
[[85, 85], [85, 34], [70, 70]]
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
38
resources/definitions/kupido.def.json
Normal file
38
resources/definitions/kupido.def.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"id": "kupido",
|
||||
"name": "Kupido",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Kupido",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Kupido" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": " ;Basic settings: Layer height: {layer_height} Walls: {wall_thickness} Fill: {infill_sparse_density}\n ;M190 S{material_bed_temperature} ;Uncomment to add your own bed temperature line\n ;M109 S{material_print_temperature} ;Uncomment to add your own temperature line\n G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X Y to endstops\n G28 Z0 ;move Z to endstops\n G1 Z20.0 F40 ;move the platform down 20mm\n G1 Y0 X170 F{speed_travel}\n G92 E0 ;zero the extruded length\n G1 F200 E10 ;extrude 3mm of feed stock\n G92 E0 ;zero the extruded length again\n G4 P7000\n G1 F{speed_travel}\n ;Put printing message on LCD screen\n M117 Printing...\n"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": " M104 S0 ;extruder heater off\n M140 S0 ;heated bed heater off (if you have it)\n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning\n"
|
||||
},
|
||||
"prime_tower_size": { "default_value": 8.660254037844387 },
|
||||
"retraction_speed": { "default_value": 60 },
|
||||
"material_bed_temperature": { "default_value": 60 },
|
||||
"speed_wall_x": { "default_value": 40 },
|
||||
"skirt_line_count": { "default_value": 2 },
|
||||
"retraction_min_travel": { "default_value": 2 },
|
||||
"speed_wall_0": { "default_value": 30 },
|
||||
"material_print_temperature": { "default_value": 220 },
|
||||
"brim_line_count": { "default_value": 15 },
|
||||
"retraction_amount": { "default_value": 3.6 },
|
||||
"speed_topbottom": { "default_value": 20 },
|
||||
"layer_height": { "default_value": 0.2 },
|
||||
"speed_print": { "default_value": 30 },
|
||||
"speed_infill": { "default_value": 30 }
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Malyan M180" },
|
||||
"machine_width": {
|
||||
"default_value": 230
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "3DMaker Starter" },
|
||||
"machine_width": {
|
||||
"default_value": 210
|
||||
},
|
||||
|
@ -41,7 +42,7 @@
|
|||
"default_value": 55
|
||||
},
|
||||
"machine_gcode_flavor": {
|
||||
"default_value": "RepRap"
|
||||
"default_value": "RepRap (Marlin/Sprinter)"
|
||||
},
|
||||
"machine_disallowed_areas": {
|
||||
"default_value": []
|
||||
|
@ -143,9 +144,9 @@
|
|||
"default_value": 15
|
||||
},
|
||||
"adhesion_type": {
|
||||
"default_value": "Raft"
|
||||
"default_value": "raft"
|
||||
},
|
||||
"skirt_minimal_length": {
|
||||
"skirt_brim_minimal_length": {
|
||||
"default_value": 100
|
||||
},
|
||||
"raft_base_line_spacing": {
|
||||
|
|
31
resources/definitions/makerbotreplicator.def.json
Normal file
31
resources/definitions/makerbotreplicator.def.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"id": "makerbotreplicator",
|
||||
"name": "MakerBotReplicator",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "MakerBot",
|
||||
"category": "Other",
|
||||
"file_formats": "application/x3g",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"prime_tower_size": { "default_value": 10.0 },
|
||||
"infill_sparse_density": { "default_value": 10 },
|
||||
"speed_travel": { "default_value": 150 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"layer_height": { "default_value": 0.15 },
|
||||
"material_print_temperature": { "default_value": 220 },
|
||||
"machine_extruder_count": { "default_value": 1 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_height": { "default_value": 150 },
|
||||
"machine_gcode_flavor": { "default_value": "MakerBot" },
|
||||
"machine_depth": { "default_value": 145 },
|
||||
"machine_width": { "default_value": 225 },
|
||||
"machine_name": { "default_value": "MakerBot Replicator" }
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@
|
|||
"platform": "mankati_fullscale_xt_plus_platform.stl"
|
||||
},
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Mankati Fullscale XT Plus" },
|
||||
"machine_width": { "default_value": 260 },
|
||||
"machine_depth": { "default_value": 260 },
|
||||
"machine_height": { "default_value": 300 },
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
],
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Mendel90" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nG92 E0 ;zero the extruded length\nM107 ;start with the fan off\nG1 X90 Y200 F6000 ;go to the middle of the front\nG1 Z0.05 ;close to the bed\nG1 Z0.3 ;lift Z\n"
|
||||
},
|
||||
|
|
41
resources/definitions/ord.def.json
Normal file
41
resources/definitions/ord.def.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"id": "ord",
|
||||
"name": "RoVa3D",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "ORD Solutions",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"machine_extruder_trains":
|
||||
{
|
||||
"0": "ord_extruder_0",
|
||||
"1": "ord_extruder_1",
|
||||
"2": "ord_extruder_2",
|
||||
"3": "ord_extruder_3",
|
||||
"4": "ord_extruder_4"
|
||||
},
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"material_bed_temperature": { "default_value": 60 },
|
||||
"prime_tower_size": { "default_value": 7.0710678118654755 },
|
||||
"infill_sparse_density": { "default_value": 15 },
|
||||
"speed_travel": { "default_value": 150 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"layer_height": { "default_value": 0.3 },
|
||||
"machine_nozzle_size": { "default_value": 0.35 },
|
||||
"material_print_temperature": { "default_value": 240 },
|
||||
"machine_extruder_count": { "default_value": 5 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_height": { "default_value": 200 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_depth": { "default_value": 250 },
|
||||
"machine_width": { "default_value": 215 },
|
||||
"machine_name": { "default_value": "RoVa3D" }
|
||||
}
|
||||
}
|
51
resources/definitions/printrbot_play.def.json
Normal file
51
resources/definitions/printrbot_play.def.json
Normal file
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"id": "printrbot_play",
|
||||
"version": 2,
|
||||
"name": "Printrbot Play",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Chris Pearson",
|
||||
"manufacturer": "Printrbot",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform": "printrbot_play.stl"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Printrbot Play" },
|
||||
"machine_heated_bed": { "default_value": false },
|
||||
"machine_width": { "default_value": 100 },
|
||||
"machine_depth": { "default_value": 100 },
|
||||
"machine_height": { "default_value": 130 },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"machine_nozzle_size": { "default_value": 0.4 },
|
||||
"layer_height": { "default_value": 0.2 },
|
||||
"layer_height_0": { "default_value": 0.3 },
|
||||
"retraction_amount": { "default_value": 0.7 },
|
||||
"retraction_speed": { "default_value": 45},
|
||||
"adhesion_type": { "default_value": "skirt" },
|
||||
"machine_head_with_fans_polygon": { "default_value": [[-32,999],[37,999],[37,-32],[-32,-32]] },
|
||||
"gantry_height": { "default_value": 55 },
|
||||
"speed_print": { "default_value": 50 },
|
||||
"speed_travel": { "default_value": 55 },
|
||||
"machine_max_feedrate_x": {"default_value": 125},
|
||||
"machine_max_feedrate_y": {"default_value": 125},
|
||||
"machine_max_feedrate_z": { "default_value": 5 },
|
||||
"machine_max_acceleration_x": { "default_value": 2000 },
|
||||
"machine_max_acceleration_y": { "default_value": 2000 },
|
||||
"machine_max_acceleration_z": { "default_value": 30 },
|
||||
"machine_max_acceleration_e": { "default_value": 10000 },
|
||||
"machine_max_jerk_xy": { "default_value": 20 },
|
||||
"machine_max_jerk_z": { "default_value": 0.4 },
|
||||
"machine_max_jerk_e": { "default_value": 5.0 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM106 ;start with the fan on for filament cooling\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG29 ;run auto bed leveling\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E10 ;extrude 10mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
}
|
||||
}
|
||||
}
|
20
resources/definitions/printrbot_play_heated.def.json
Normal file
20
resources/definitions/printrbot_play_heated.def.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"id": "printrbot_play_heated",
|
||||
"version": 2,
|
||||
"name": "Printrbot Play (Heated Bed)",
|
||||
"inherits": "printrbot_play",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Chris Pearson",
|
||||
"manufacturer": "Printrbot",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform": ""
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Printrbot Play (Heated Bed)" },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_depth": { "default_value": 203 }
|
||||
}
|
||||
}
|
|
@ -8,16 +8,19 @@
|
|||
"author": "Calvindog717",
|
||||
"manufacturer": "PrintrBot",
|
||||
"category": "Other",
|
||||
"platform": "printrbot_simple_metal_platform.stl",
|
||||
"platform_offset": [0, -3.45, 0],
|
||||
"file_formats": "text/x-gcode"
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Printrbot Simple" },
|
||||
"machine_heated_bed": { "default_value": false },
|
||||
"machine_width": { "default_value": 150 },
|
||||
"machine_height": { "default_value": 150 },
|
||||
"machine_depth": { "default_value": 140 },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_nozzle_size": { "default_value": 0.3 },
|
||||
"machine_nozzle_size": { "default_value": 0.4 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"machine_nozzle_heat_up_speed": { "default_value": 2 },
|
||||
"machine_nozzle_cool_down_speed": { "default_value": 2 },
|
||||
|
@ -33,10 +36,10 @@
|
|||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG29 ; auto bed-levelling\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..."
|
||||
"default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;home X/Y\nG28 Z0 ;home Z\nG92 E0 ;zero the extruded length\nG29 ;initiate auto bed leveling sequence\nG92 X132.4 Y20 ;correct bed origin (G29 changes it)"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
"default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nM106 S0 ;fan off\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit\nG1 Z+1 E-5 F9000 ;move Z up a bit and retract even more\nG28 X0 Y0 ;home X/Y, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Prusa i3" },
|
||||
"machine_heated_bed": {
|
||||
"default_value": true
|
||||
},
|
||||
|
|
53
resources/definitions/prusa_i3_mk2.def.json
Normal file
53
resources/definitions/prusa_i3_mk2.def.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"id": "prusa_i3_mk2",
|
||||
"version": 2,
|
||||
"name": "Prusa i3 Mk2",
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Apsu",
|
||||
"manufacturer": "Prusa Research",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"icon": "icon_ultimaker2",
|
||||
"platform": "prusai3_platform.stl",
|
||||
"has_materials": true
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Prusa i3 Mk2" },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_width": { "default_value": 250 },
|
||||
"machine_height": { "default_value": 200 },
|
||||
"machine_depth": { "default_value": 210 },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"material_bed_temperature": { "default_value": 55 },
|
||||
"machine_nozzle_size": { "default_value": 0.4 },
|
||||
"layer_height": { "default_value": 0.1 },
|
||||
"layer_height_0": { "default_value": 0.15 },
|
||||
"retraction_amount": { "default_value": 0.8 },
|
||||
"retraction_speed": { "default_value": 35 },
|
||||
"retraction_retract_speed": { "default_value": 35 },
|
||||
"retraction_prime_speed": { "default_value": 35 },
|
||||
"adhesion_type": { "default_value": "skirt" },
|
||||
"machine_nozzle_heat_up_speed": { "default_value": 2 },
|
||||
"machine_nozzle_cool_down_speed": { "default_value": 2 },
|
||||
"machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] },
|
||||
"gantry_height": { "default_value": 28 },
|
||||
"machine_max_feedrate_z": { "default_value": 12 },
|
||||
"machine_max_feedrate_e": { "default_value": 120 },
|
||||
"machine_max_acceleration_z": { "default_value": 500 },
|
||||
"machine_acceleration": { "default_value": 1000 },
|
||||
"machine_max_jerk_xy": { "default_value": 10 },
|
||||
"machine_max_jerk_z": { "default_value": 0.2 },
|
||||
"machine_max_jerk_e": { "default_value": 2.5 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_start_gcode": {
|
||||
"default_value": "G21 ; set units to millimeters\nG90 ; use absolute positioning\nM82 ; absolute extrusion mode\nG28 W ; home all without mesh bed level\nG80 ; mesh bed leveling\nM104 S{material_print_temperature} ; set extruder temp\nM140 S{material_bed_temperature} ; set bed temp\nM190 S{material_bed_temperature} ; wait for bed temp\nM109 S{material_print_temperature} ; wait for extruder temp\nG92 E0.0 ; reset extruder distance position\nG1 Y-3.0 F1000.0 ; go outside print area\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E21.5 F1000.0 ; intro line\nG92 E0.0 ; reset extruder distance position"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": "M104 S0 ; turn off extruder\nM140 S0 ; turn off heatbed\nM107 ; turn off fan\nG1 X0 Y210; home X axis and push Y forward\nM84 ; disable motors"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_name": { "default_value": "Prusa i3 xl" },
|
||||
"machine_heated_bed": {
|
||||
"default_value": true
|
||||
},
|
||||
|
|
41
resources/definitions/punchtec_connect_xl.def.json
Normal file
41
resources/definitions/punchtec_connect_xl.def.json
Normal file
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"id": "punchtec_connect_xl",
|
||||
"name": "Punchtec Connect XL",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Punchtec",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"machine_extruder_trains":
|
||||
{
|
||||
"0": "punchtec_connect_xl_extruder_0",
|
||||
"1": "punchtec_connect_xl_extruder_1"
|
||||
},
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_head_polygon": { "default_value": [[ 0, 0], [ 0, 0], [ 0, 0], [ 0, 0]] },
|
||||
"speed_travel": { "default_value": 150 },
|
||||
"prime_tower_size": { "default_value": 8.660254037844387 },
|
||||
"speed_wall_x": { "default_value": 40 },
|
||||
"speed_wall_0": { "default_value": 40 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"speed_topbottom": { "default_value": 40 },
|
||||
"layer_height": { "default_value": 0.2 },
|
||||
"material_print_temperature": { "default_value": 195 },
|
||||
"speed_print": { "default_value": 40 },
|
||||
"speed_infill": { "default_value": 40 },
|
||||
"machine_extruder_count": { "default_value": 2 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_height": { "default_value": 200 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_depth": { "default_value": 304 },
|
||||
"machine_width": { "default_value": 304 },
|
||||
"machine_name": { "default_value": "Punchtec Connect XL" }
|
||||
}
|
||||
}
|
53
resources/definitions/rigid3d.def.json
Normal file
53
resources/definitions/rigid3d.def.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"id": "rigid3d",
|
||||
"name": "Rigid3D",
|
||||
"version": 2,
|
||||
"inherits": "fdmprinter",
|
||||
"metadata": {
|
||||
"visible": true,
|
||||
"author": "Ultimaker",
|
||||
"manufacturer": "Rigid3D",
|
||||
"category": "Other",
|
||||
"file_formats": "text/x-gcode",
|
||||
"platform_offset": [ 0, 0, 0]
|
||||
},
|
||||
|
||||
"overrides": {
|
||||
"machine_start_gcode": {
|
||||
"default_value": " ; -- START GCODE --\n G21\n G28 ; Home extruder\n G29 ; Autolevel bed\n M107 ; Turn off fan\n G90 ; Absolute positioning\n M82 ; Extruder in absolute mode\n G92 E0 ; Reset extruder position\n ; -- end of START GCODE --\n\n"
|
||||
},
|
||||
"machine_end_gcode": {
|
||||
"default_value": " ; -- END GCODE --\n G1 X0 Y230 ; Get extruder out of way.\n M107 ; Turn off fan\n G91 ; Relative positioning\n G0 Z20 ; Lift extruder up\n T0\n G1 E-1 ; Reduce filament pressure\n M104 T0 S0 ; Turn ectruder heater off\n G90 ; Absolute positioning\n G92 E0 ; Reset extruder position\n M140 S0 ; Disable heated bed\n M84 ; Turn steppers off\n ; -- end of END GCODE --\n"
|
||||
},
|
||||
"machine_head_polygon": { "default_value": [[ 22, 67], [ 22, 51], [ 36, 51], [ 36, 67]] },
|
||||
"skirt_gap": { "default_value": 5.0 },
|
||||
"cool_min_layer_time": { "default_value": 10 },
|
||||
"prime_tower_size": { "default_value": 7.745966692414834 },
|
||||
"speed_wall_x": { "default_value": 40 },
|
||||
"speed_travel": { "default_value": 100 },
|
||||
"bottom_thickness": { "default_value": 0.75 },
|
||||
"material_diameter": { "default_value": 1.75 },
|
||||
"layer_height_0": { "default_value": 0.25 },
|
||||
"support_angle": { "default_value": 45 },
|
||||
"material_bed_temperature": { "default_value": 100 },
|
||||
"top_thickness": { "default_value": 0.75 },
|
||||
"material_print_temperature": { "default_value": 235 },
|
||||
"retraction_speed": { "default_value": 60.0 },
|
||||
"wall_thickness": { "default_value": 0.8 },
|
||||
"retraction_min_travel": { "default_value": 2 },
|
||||
"speed_wall_0": { "default_value": 30 },
|
||||
"retraction_amount": { "default_value": 1 },
|
||||
"speed_topbottom": { "default_value": 30 },
|
||||
"layer_height": { "default_value": 0.25 },
|
||||
"speed_print": { "default_value": 40 },
|
||||
"speed_infill": { "default_value": 40 },
|
||||
"machine_extruder_count": { "default_value": 1 },
|
||||
"machine_heated_bed": { "default_value": true },
|
||||
"machine_center_is_zero": { "default_value": false },
|
||||
"machine_height": { "default_value": 210 },
|
||||
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
|
||||
"machine_depth": { "default_value": 250 },
|
||||
"machine_width": { "default_value": 250 },
|
||||
"machine_name": { "default_value": "Rigid3D" }
|
||||
}
|
||||
}
|
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