Merge branch 'master' into python_type_hinting

This commit is contained in:
Simon Edwards 2016-11-22 11:15:04 +01:00
commit 98a6568313
416 changed files with 160213 additions and 173190 deletions

9
.gitignore vendored
View file

@ -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

View file

@ -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 )

View file

@ -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
View 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>

View file

@ -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

View file

@ -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.

View file

@ -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"}

View file

@ -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,15 +792,18 @@ 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.
@ -734,12 +822,14 @@ 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.
@ -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():

View file

@ -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
])

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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:

View file

@ -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
View 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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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

View 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)

View 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

View 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

View file

@ -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)

View 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()

View file

@ -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()

View 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

Binary file not shown.

View file

@ -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)

View file

@ -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:

View file

@ -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
}
]
}

View file

@ -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
Its 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
Cant 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 doesnt 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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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.

View file

@ -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))

View file

@ -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.
#

View file

@ -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)

View 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()

View file

@ -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:
@ -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()

View 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

View 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

View file

@ -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": {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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
@ -114,3 +141,8 @@ class RemovableDriveOutputDevice(OutputDevice):
if action == "eject":
if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self):
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()

View file

@ -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):

View file

@ -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")

View file

@ -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:

View file

@ -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()

View file

@ -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):

View file

@ -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

View file

@ -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):
"""

View file

@ -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)

View file

@ -64,7 +64,7 @@ Cura.MachineAction
{
startBedLevelingButton.visible = false;
bedlevelingButton.visible = true;
manager.startCheck();
manager.startBedLeveling();
}
}

View file

@ -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()
@ -42,3 +42,4 @@ class UMOUpgradeSelection(MachineAction):
new_variant.setDefinition(global_container_stack.getBottom())
UM.Settings.ContainerRegistry.getInstance().addContainer(new_variant)
global_container_stack.replaceContainer(variant_index, new_variant)
return new_variant

View file

@ -1,5 +1,7 @@
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
import cura.Settings.CuraContainerRegistry
import UM.Settings.DefinitionContainer
catalog = i18nCatalog("cura")
@ -7,3 +9,9 @@ class UpgradeFirmwareMachineAction(MachineAction):
def __init__(self):
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
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())

View file

@ -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)
}
}
}

View file

@ -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,

View file

@ -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()]

View file

@ -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.
#

View file

@ -35,6 +35,10 @@ def getMetaData():
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}

View 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

View 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() }

View file

@ -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]

View file

@ -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)

View 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 }
}
}

View file

@ -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

View file

@ -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 },

View file

@ -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

View file

@ -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

View file

@ -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

View 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" }
}
}

View file

@ -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

View file

@ -14,6 +14,7 @@
},
"overrides": {
"machine_name": { "default_value": "German RepRap Neo" },
"machine_width": {
"default_value": 150
},

View file

@ -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"

View 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" }
}
}

View 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]]
]
}
}
}

View 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 }
}
}

View file

@ -12,6 +12,7 @@
},
"overrides": {
"machine_name": { "default_value": "Malyan M180" },
"machine_width": {
"default_value": 230
},

View file

@ -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": {

View 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" }
}
}

View file

@ -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 },

View file

@ -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"
},

View 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" }
}
}

View 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"
}
}
}

View 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 }
}
}

View file

@ -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"
}
}
}

View file

@ -14,6 +14,7 @@
},
"overrides": {
"machine_name": { "default_value": "Prusa i3" },
"machine_heated_bed": {
"default_value": true
},

View 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"
}
}
}

View file

@ -14,6 +14,7 @@
},
"overrides": {
"machine_name": { "default_value": "Prusa i3 xl" },
"machine_heated_bed": {
"default_value": true
},

View 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" }
}
}

View 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