Merge branch 'um-master' into gh-issue-980

Conflicts:
	resources/definitions/fdmprinter.def.json
This commit is contained in:
nickthetait 2016-10-13 08:01:00 -06:00
commit bf331f39af
220 changed files with 111349 additions and 168598 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ docs/html
resources/i18n/en
resources/i18n/x-test
resources/firmware
LC_MESSAGES
# Editors and IDEs.
*kdev*

View file

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

@ -46,6 +46,7 @@ Third party plugins
* [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

@ -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")
@ -26,26 +27,7 @@ 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 = 1.5
## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
@ -69,8 +51,8 @@ class BuildVolume(SceneNode):
self._disallowed_areas = []
self._disallowed_area_mesh = None
self._prime_tower_area = None
self._prime_tower_area_mesh = None
self._error_areas = []
self._error_mesh = None
self.setCalculateBoundingBox(False)
self._volume_aabb = None
@ -80,10 +62,47 @@ 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._has_errors = False
Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
# Number of objects loaded at the moment.
self._number_of_objects = 0
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_number_of_objects = len([node for node in BreadthFirstIterator(root) if node.getMeshData() and type(node) is SceneNode])
if new_number_of_objects != self._number_of_objects:
recalculate = False
if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time":
recalculate = (new_number_of_objects < 2 and self._number_of_objects > 1) or (new_number_of_objects > 1 and self._number_of_objects < 2)
self._number_of_objects = new_number_of_objects
if recalculate:
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
def setWidth(self, width):
if width: self._width = width
@ -113,8 +132,8 @@ class BuildVolume(SceneNode):
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._prime_tower_area_mesh:
renderer.queueNode(self, mesh = self._prime_tower_area_mesh, shader = self._shader, transparent=True,
if self._error_mesh:
renderer.queueNode(self, mesh=self._error_mesh, shader=self._shader, transparent=True,
backface_cull=True, sort=-8)
return True
@ -191,29 +210,29 @@ class BuildVolume(SceneNode):
else:
self._disallowed_area_mesh = None
if self._prime_tower_area:
if self._error_areas:
mb = MeshBuilder()
color = Color(1.0, 0.0, 0.0, 0.5)
points = self._prime_tower_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._prime_tower_area_mesh = mb.build()
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._prime_tower_area_mesh = None
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 = self._getBedAdhesionSize()
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!
@ -228,13 +247,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
@ -255,7 +267,8 @@ 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())
@ -272,12 +285,15 @@ class BuildVolume(SceneNode):
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 self._number_of_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()
@ -292,15 +308,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 self._number_of_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 or setting_key == "print_sequence":
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:
self._updateDisallowedAreas()
rebuild_me = True
@ -317,115 +336,177 @@ class BuildVolume(SceneNode):
def _updateDisallowedAreas(self):
if not self._global_container_stack:
return
self._has_errors = False # Reset.
self._error_areas = []
disallowed_areas = copy.deepcopy(
self._global_container_stack.getProperty("machine_disallowed_areas", "value"))
areas = []
machine_width = self._global_container_stack.getProperty("machine_width", "value")
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
self._prime_tower_area = None
prime_tower_area = None
# Add prime tower location as disallowed area.
if self._global_container_stack.getProperty("prime_tower_enable", "value") == True:
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
prime_tower_y = - self._global_container_stack.getProperty("prime_tower_position_y", "value") + machine_depth / 2
self._prime_tower_area = Polygon([
prime_tower_area = Polygon([
[prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y - prime_tower_size],
[prime_tower_x, prime_tower_y],
[prime_tower_x - prime_tower_size, prime_tower_y],
])
disallowed_polygons = []
# 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],
])
bed_adhesion_size = self._getBedAdhesionSize()
# Check if prime positions intersect with disallowed areas
prime_collision = False
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)))
# Minkowski with zero, to ensure that the polygon is correct & watertight.
poly = poly.getMinkowskiHull(Polygon.approximatedCircle(0))
disallowed_polygons.append(poly)
extruder_manager = ExtruderManager.getInstance()
extruders = extruder_manager.getMachineExtruders(self._global_container_stack.getId())
prime_polygons = []
# Each extruder has it's own prime location
for extruder in extruders:
prime_x = extruder.getProperty("extruder_prime_pos_x", "value") - machine_width / 2
prime_y = machine_depth / 2 - extruder.getProperty("extruder_prime_pos_y", "value")
prime_polygon = Polygon([
[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],
])
prime_polygon = prime_polygon.getMinkowskiHull(Polygon.approximatedCircle(0))
collision = False
# Check if prime polygon is intersecting with any of the other disallowed areas.
# Note that we check the prime area without bed adhesion.
for poly in disallowed_polygons:
if prime_polygon.intersectsPolygon(poly) is not None:
collision = True
break
# Also collide with other prime positions
for poly in prime_polygons:
if prime_polygon.intersectsPolygon(poly) is not None:
collision = True
break
if not collision:
# Prime area is valid. Add as normal.
# Once it's added like this, it will recieve a bed adhesion offset, just like the others.
prime_polygons.append(prime_polygon)
else:
self._error_areas.append(prime_polygon)
prime_collision = collision or prime_collision
disallowed_polygons.extend(prime_polygons)
disallowed_border_size = self._getEdgeDisallowedSize()
# Extend every area already in the disallowed_areas with the skirt size.
if disallowed_areas:
for poly in disallowed_polygons:
poly = poly.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size))
areas.append(poly)
if self._prime_tower_area:
self._prime_tower_area = self._prime_tower_area.getMinkowskiHull(Polygon(approximatedCircleVertices(bed_adhesion_size)))
# Add the skirt areas around the borders of the build plate.
if bed_adhesion_size > 0:
if disallowed_border_size > 0:
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
areas.append(Polygon(numpy.array([
[-half_machine_width, -half_machine_depth],
[-half_machine_width, half_machine_depth],
[-half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size],
[-half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
[-half_machine_width + disallowed_border_size, half_machine_depth - disallowed_border_size],
[-half_machine_width + disallowed_border_size, -half_machine_depth + disallowed_border_size]
], numpy.float32)))
areas.append(Polygon(numpy.array([
[half_machine_width, half_machine_depth],
[half_machine_width, -half_machine_depth],
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size],
[half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size]
[half_machine_width - disallowed_border_size, -half_machine_depth + disallowed_border_size],
[half_machine_width - disallowed_border_size, half_machine_depth - disallowed_border_size]
], numpy.float32)))
areas.append(Polygon(numpy.array([
[-half_machine_width, half_machine_depth],
[half_machine_width, half_machine_depth],
[half_machine_width - bed_adhesion_size, half_machine_depth - bed_adhesion_size],
[-half_machine_width + bed_adhesion_size, half_machine_depth - bed_adhesion_size]
[half_machine_width - disallowed_border_size, half_machine_depth - disallowed_border_size],
[-half_machine_width + disallowed_border_size, half_machine_depth - disallowed_border_size]
], numpy.float32)))
areas.append(Polygon(numpy.array([
[half_machine_width, -half_machine_depth],
[-half_machine_width, -half_machine_depth],
[-half_machine_width + bed_adhesion_size, -half_machine_depth + bed_adhesion_size],
[half_machine_width - bed_adhesion_size, -half_machine_depth + bed_adhesion_size]
[-half_machine_width + disallowed_border_size, -half_machine_depth + disallowed_border_size],
[half_machine_width - disallowed_border_size, -half_machine_depth + disallowed_border_size]
], numpy.float32)))
# Check if the prime tower area intersects with any of the other areas.
# If this is the case, keep the polygon seperate, so it can be drawn in red.
# If this is the case, add it to the error area's so it can be drawn in red.
# If not, add it back to disallowed area's, so it's rendered as normal.
collision = False
if self._prime_tower_area:
prime_tower_collision = False
if prime_tower_area:
# Using Minkowski of 0 fixes the prime tower area so it's rendered correctly
prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
for area in areas:
if self._prime_tower_area.intersectsPolygon(area) is not None:
collision = True
if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True
break
if not collision:
areas.append(self._prime_tower_area)
self._prime_tower_area = None
self._has_errors = collision
if not prime_tower_collision:
areas.append(prime_tower_area)
else:
self._error_areas.append(prime_tower_area)
# The buildplate has errors if either prime tower or prime has a colission.
self._has_errors = prime_tower_collision or prime_collision
self._disallowed_areas = areas
## 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"):
## Private convenience function to get a setting from the adhesion
# extruder.
#
# \param setting_key The key of the setting to get.
# \param property The property to get from the setting.
# \return The property of the specified setting in the adhesion extruder.
def _getSettingFromAdhesionExtruder(self, setting_key, property = "value"):
return self._getSettingFromExtruder(setting_key, "adhesion_extruder_nr", property)
## Private convenience function to get a setting from 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)
## Helper function to get a setting from an extruder specified in another
# setting.
#
# \param setting_key The key of the setting to get.
# \param extruder_setting_key The key of the setting that specifies from
# which extruder to get the setting, if there are multiple extruders.
# \param property The property to get from the setting.
# \return The property of the specified setting in the specified extruder.
def _getSettingFromExtruder(self, setting_key, extruder_setting_key, property = "value"):
multi_extrusion = self._global_container_stack.getProperty("machine_extruder_count", "value") > 1
if not multi_extrusion:
return self._global_container_stack.getProperty(setting_key, property)
extruder_index = self._global_container_stack.getProperty(setting_key, "limit_to_extruder")
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)
@ -433,12 +514,15 @@ class BuildVolume(SceneNode):
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
return stack.getProperty(setting_key, property)
## Convenience function to calculate the size of the bed adhesion in directions x, y.
def _getBedAdhesionSize(self):
## 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
skirt_size = 0.0
# 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":
@ -446,26 +530,56 @@ class BuildVolume(SceneNode):
adhesion_type = container_stack.getProperty("adhesion_type", "value")
if adhesion_type == "skirt":
skirt_distance = self._getSettingProperty("skirt_gap", "value")
skirt_line_count = self._getSettingProperty("skirt_line_count", "value")
skirt_size = skirt_distance + (skirt_line_count * self._getSettingProperty("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 = self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("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 = self._getSettingProperty("raft_margin", "value")
bed_adhesion_size = self._getSettingFromAdhesionExtruder("raft_margin")
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"):
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.
if self._getSettingFromAdhesionExtruder("infill_wipe_dist"):
move_from_wall_radius = max(move_from_wall_radius, self._getSettingFromAdhesionExtruder("infill_wipe_dist"))
if self._getSettingFromAdhesionExtruder("travel_avoid_distance"):
move_from_wall_radius = max(move_from_wall_radius, self._getSettingFromAdhesionExtruder("travel_avoid_distance"))
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"]

View file

@ -1,3 +1,6 @@
# 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
@ -112,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):
@ -141,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():
@ -193,19 +198,15 @@ 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)
# 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))
@ -228,7 +229,6 @@ 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
if adhesion_type == "raft":
extra_margin = max(0, self._getSettingProperty("raft_margin", "value"))
@ -237,27 +237,33 @@ class ConvexHullDecorator(SceneNodeDecorator):
elif adhesion_type == "skirt":
extra_margin = max(
0, self._getSettingProperty("skirt_gap", "value") +
self._getSettingPropertyy("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "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()
@ -285,20 +291,27 @@ class ConvexHullDecorator(SceneNodeDecorator):
## 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"):
multi_extrusion = self._global_stack.getProperty("machine_extruder_count", "value") > 1
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": # If extruder index is -1 use global instead
return self._global_stack.getProperty(setting_key, property)
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)
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 descendent or the same as the root node.
## Returns true if node is a descendant or the same as the root node.
def __isDescendant(self, root, node):
if node is None:
return False
@ -310,3 +323,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
"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", "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

@ -49,11 +49,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")
@ -86,14 +119,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)
# 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", cura.Settings.ExtruderManager.getExtruderValues)
SettingFunction.registerOperator("extruderValue", cura.Settings.ExtruderManager.getExtruderValue)
SettingFunction.registerOperator("resolveOrValue", cura.Settings.ExtruderManager.getResolveOrValue)
## Add the 4 types of profiles to storage.
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
@ -112,16 +155,18 @@ class CuraApplication(QtApplication):
## Initialise the version upgrade manager with Cura's storage paths.
import UM.VersionUpgradeManager #Needs to be here to prevent circular dependencies.
self._version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager(
UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
{
("quality", UM.Settings.InstanceContainer.Version): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
("machine_stack", UM.Settings.ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
("preferences", UM.Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences")
("preferences", UM.Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences"),
("user", UM.Settings.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
@ -189,6 +234,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")
@ -269,6 +317,12 @@ class CuraApplication(QtApplication):
self._recent_files.append(QUrl.fromLocalFile(f))
## 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())
@ -296,38 +350,43 @@ 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:
instance.setPath(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():
self.saveStack(stack)
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)
if path:
instance.setPath(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():
@ -426,7 +485,7 @@ class CuraApplication(QtApplication):
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
cura.Settings.ExtruderManager.getInstance()
qmlRegisterSingletonType(cura.Settings.MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
qmlRegisterSingletonType(cura.Settings.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"))
self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
@ -449,6 +508,11 @@ class CuraApplication(QtApplication):
self._machine_manager = cura.Settings.MachineManager.createMachineManager()
return self._machine_manager
def getSettingInheritanceManager(self, *args):
if self._setting_inheritance_manager is None:
self._setting_inheritance_manager = cura.Settings.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.
@ -485,6 +549,9 @@ class CuraApplication(QtApplication):
qmlRegisterType(cura.Settings.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(cura.Settings.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
qmlRegisterType(cura.Settings.ProfilesModel, "Cura", 1, 0, "ProfilesModel")
qmlRegisterType(cura.Settings.QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
qmlRegisterType(cura.Settings.UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
qmlRegisterType(cura.Settings.MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
qmlRegisterType(cura.Settings.QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
@ -579,8 +646,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")
@ -594,17 +659,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)
@ -800,11 +865,14 @@ class CuraApplication(QtApplication):
return
# 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)
object_centers = [node.getMeshData().getCenterPosition().scale(node.getScale()) for node in group_node.getChildren() if node.getMeshData()]
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()):

View file

@ -37,8 +37,8 @@ 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.
@ -172,31 +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.75, 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),
SupportInterfaceType: Color(0.25, 0.75, 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.75, 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],
[0.25, 0.75, 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

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

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

@ -47,6 +47,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._accepts_commands = True
self._printer_state = ""
self._printer_type = "unknown"
def requestWrite(self, node, file_name = None, filter_by_machine = False):
raise NotImplementedError("requestWrite needs to be implemented")
@ -97,6 +98,12 @@ class PrinterOutputDevice(QObject, OutputDevice):
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
@ -105,6 +112,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
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

270
cura/QualityManager.py Normal file
View file

@ -0,0 +1,270 @@
# 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 containers \type{List[ContainerInstance]}
def findQualityByName(self, quality_name, machine_definition=None, material_containers=None):
criteria = {"type": "quality", "name": quality_name}
return self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
## 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):
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")
if material_container.getDefinition().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:
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

@ -15,6 +15,7 @@ import UM.MimeTypeDatabase
import UM.Logger
import cura.Settings
from cura.QualityManager import QualityManager
from UM.MimeTypeDatabase import MimeTypeNotFoundError
@ -432,9 +433,16 @@ class ContainerManager(QObject):
def clearUserContainers(self):
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 cura.Settings.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.
#
@ -468,7 +476,11 @@ 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)
self._container_registry.addContainer(new_changes)
@ -498,7 +510,12 @@ class ContainerManager(QObject):
activate_quality = quality_name == self._machine_manager.activeQualityName
activate_quality_type = None
for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
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
if activate_quality and not activate_quality_type:
activate_quality_type = container.getMetaDataEntry("quality")
@ -545,10 +562,16 @@ class ContainerManager(QObject):
new_name = self._container_registry.uniqueName(new_name)
container_registry = self._container_registry
for container in self._getFilteredContainers(name = quality_name, type = "quality_changes"):
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))
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
@ -566,33 +589,73 @@ class ContainerManager(QObject):
global_stack = UM.Application.getInstance().getGlobalContainerStack()
if not global_stack or not quality_name:
return ""
machine_definition = global_stack.getBottom()
active_stacks = cura.Settings.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 = self._container_registry.findInstanceContainers(name = quality_name)
if not containers:
UM.Logger.log("d", "Unable to duplicate the quality %s, because it doesn't exist.", quality_name)
return ""
if base_name is None:
base_name = quality_name
# Try to find a Quality with the name.
containers = QualityManager.getInstance().findQualityByName(quality_name, machine_definition, material_instances)
if containers:
container = containers[0]
return self._duplicateQualityForMachineType(container, base_name, machine_definition)
# 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)
container_type = containers[0].getMetaDataEntry("type")
if container_type == "quality":
for container in self._getFilteredContainers(name = quality_name, type = "quality"):
for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
new_changes = self._createQualityChanges(container, new_name, stack.getId())
self._container_registry.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)
self._container_registry.addContainer(new_container)
else:
UM.Logger.log("w", "Unable to duplicate profile. It has the wrong type.")
return ""
new_change_instances = []
return new_name
# 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):
new_unique_id = self._createUniqueId(container.getId(), 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):
@ -617,12 +680,22 @@ class ContainerManager(QObject):
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:
@ -631,7 +704,8 @@ 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 = {}
@ -677,39 +751,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 = UM.Application.getInstance().getGlobalContainerStack()
if not global_stack:
return False
criteria = kwargs
filter_by_material = False
if global_stack.getMetaDataEntry("has_machine_quality"):
definition = global_stack.getBottom()
definition_id = definition.getMetaDataEntry("quality_definition", definition.getId())
criteria["definition"] = definition_id
filter_by_material = global_stack.getMetaDataEntry("has_materials")
material_ids = []
if filter_by_material:
for stack in cura.Settings.ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
material_ids.append(stack.findContainer(type = "material").getId())
containers = self._container_registry.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.
#
@ -731,34 +779,26 @@ 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 = UM.Settings.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"):
if not machine_definition.getMetaDataEntry("has_machine_quality"):
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
else:
definition = global_stack.getBottom()
definition_id = definition.getMetaDataEntry("quality_definition", definition.getId())
definition = self._container_registry.findContainers(id=definition_id)[0]
quality_changes.setDefinition(definition)
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

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:
@ -128,14 +146,25 @@ class CuraContainerRegistry(ContainerRegistry):
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()
container_registry = ContainerRegistry.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", file_name, str(e), profile_reader.getPluginId())
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]
@ -144,31 +173,60 @@ class CuraContainerRegistry(ContainerRegistry):
self._configureProfile(profile, name_seed)
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
else:
profile_index = -1
global_profile = None
new_name = self.uniqueName(name_seed)
for profile in profile_or_list:
profile.setDirty(True) # Ensure the profiles are correctly saved
if profile.getId() != "":
container_registry.addContainer(profile)
if profile_index >= 0:
if len(machine_extruders) > profile_index:
extruder_id = machine_extruders[profile_index].getBottom().getId()
# 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.", 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:
@ -189,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

@ -13,9 +13,13 @@ import UM.Settings.SettingFunction
#
# 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()
@ -25,6 +29,7 @@ class ExtruderManager(QObject):
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
self._active_extruder_index = 0
UM.Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
self._global_container_stack_definition_id = None
self._addCurrentMachineExtruders()
## Gets the unique identifier of the currently active extruder stack.
@ -136,11 +141,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 = UM.Settings.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(UM.Application.getInstance().getGlobalContainerStack())
changed = True
if changed:
self.extrudersChanged.emit(machine_id)
@ -180,7 +182,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:
@ -192,7 +194,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)
@ -202,7 +205,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
@ -256,14 +259,24 @@ 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 = UM.Settings.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(UM.Application.getInstance().getGlobalContainerStack())
container_registry.addContainer(container_stack)
def getAllExtruderValues(self, setting_key):
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
multi_extrusion = global_container_stack.getProperty("machine_extruder_count", "value") > 1
if not multi_extrusion:
return [global_container_stack.getProperty(setting_key, "value")]
result = []
for index in self.extruderIds:
extruder_stack_id = self.extruderIds[str(index)]
stack = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id=extruder_stack_id)[0]
result.append(stack.getProperty(setting_key, "value"))
return result
## Removes the container stack and user profile for the extruders for a specific machine.
#
# \param machine_id The machine to remove the extruders for.
@ -284,22 +297,32 @@ class ExtruderManager(QObject):
for name in self._extruder_trains[machine_id]:
yield self._extruder_trains[machine_id][name]
## 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 = UM.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 = UM.Application.getInstance().getGlobalContainerStack()
return list(self._extruder_trains[global_stack.getId()].values()) if global_stack else []
def __globalContainerStackChanged(self):
self._addCurrentMachineExtruders()
global_container_stack = UM.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.
@ -324,7 +347,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, UM.Settings.SettingFunction):
@ -344,7 +367,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)
@ -369,3 +392,29 @@ class ExtruderManager(QObject):
value = UM.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 = UM.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

@ -29,6 +29,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,6 +47,7 @@ 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
@ -51,8 +55,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
self._active_extruder_stack = None
#Listen to changes.
UM.Application.getInstance().globalContainerStackChanged.connect(self._updateExtruders)
manager = ExtruderManager.getInstance()
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
self._updateExtruders()
@ -126,7 +130,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"id": global_container_stack.getId(),
"name": "Global",
"color": color,
"index": -1
"index": -1,
"definition": ""
}
items.append(item)
changed = True
@ -148,7 +153,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"id": extruder.getId(),
"name": extruder_name,
"color": color,
"index": position
"index": position,
"definition": extruder.getBottom().getId()
}
items.append(item)
changed = True

View file

@ -3,22 +3,22 @@
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.SettingRelation import RelationType
import UM.Settings
from cura.QualityManager import QualityManager
from cura.PrinterOutputDevice import PrinterOutputDevice
from . import ExtruderManager
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
import time
import os
class MachineManager(QObject):
@ -45,8 +45,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 = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
self._empty_material_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
@ -72,14 +73,18 @@ 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
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
@ -130,7 +135,7 @@ class MachineManager(QObject):
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 = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition_id, GUID = material_id)
if containers: # New material ID is known
@ -141,9 +146,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))
@ -219,6 +233,9 @@ class MachineManager(QObject):
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)
@ -228,8 +245,16 @@ class MachineManager(QObject):
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
else:
self._active_container_stack = self._global_container_stack
old_active_stack_valid = self._active_stack_valid
self._active_stack_valid = not self._checkStackForErrors(self._active_container_stack)
self.activeValidationChanged.emit()
if old_active_stack_valid != self._active_stack_valid:
self.activeStackValidationChanged.emit()
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")
@ -243,19 +268,8 @@ 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:
@ -265,13 +279,11 @@ class MachineManager(QObject):
changed_validation_state = self._global_container_stack.getProperty(key, property_name)
if changed_validation_state in (UM.Settings.ValidatorState.Exception, UM.Settings.ValidatorState.MaximumError, UM.Settings.ValidatorState.MinimumError):
self._active_stack_valid = False
self.activeValidationChanged.emit()
self.activeStackValidationChanged.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()
self.activeStackValidationChanged.emit()
@pyqtSlot(str)
def setActiveMachine(self, stack_id):
@ -347,7 +359,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
@ -355,7 +367,8 @@ 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
@ -368,15 +381,33 @@ class MachineManager(QObject):
if not self._global_container_stack:
return
self._global_container_stack.getTop().removeInstance(key)
send_emits_containers = []
for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()):
stack.getTop().removeInstance(key)
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:
container = stack.getTop()
container.removeInstance(key, postpone_emit=True)
send_emits_containers.append(container)
for container in send_emits_containers:
container.sendPostponedEmits()
## 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)
@pyqtProperty(bool, notify = activeStackValidationChanged)
def isActiveStackValid(self):
return bool(self._active_stack_valid)
@ -463,8 +494,8 @@ class MachineManager(QObject):
@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"})
@ -474,27 +505,50 @@ class MachineManager(QObject):
@pyqtProperty(str, notify=activeQualityChanged)
def activeQualityId(self):
if self._global_container_stack:
quality = self._global_container_stack.findContainer({"type": "quality_changes"})
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._global_container_stack.findContainer({"type": "quality"})
quality = self._active_container_stack.findContainer({"type": "quality"})
if quality:
return quality.getId()
return ""
@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 ""
## 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 ""
@ -527,43 +581,58 @@ class MachineManager(QObject):
containers = UM.Settings.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" or #Don't want the empty one.
old_quality_changes.getMetaDataEntry("material") != material_id): # The quality change is based off a different material; the quality change is probably a custom quality.
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 containers[0].getMetaDataEntry("compatible") == False:
message = Message(catalog.i18nc("@info:status",
"The selected material is imcompatible with the selected machine or configuration."))
message.show()
if old_quality:
if old_quality_changes:
new_quality = self._updateQualityChangesContainer(
old_quality.getMetaDataEntry("quality_type"),
preferred_quality_changes_name = 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):
@ -577,7 +646,7 @@ class MachineManager(QObject):
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()
@ -586,100 +655,176 @@ class MachineManager(QObject):
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 = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
if not containers or not self._global_container_stack:
return
Logger.log("d", "Attempting to change the active quality to %s", quality_id)
self.blurSettings.emit()
quality_container = None
quality_changes_container = self._empty_quality_changes_container
# 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 = UM.Settings.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.activeQualityDefinitionId
else:
criteria["definition"] = "fdmprinter"
stack_quality = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**criteria)
if not stack_quality:
criteria.pop("extruder")
stack_quality = UM.Settings.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 = UM.Settings.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")
if old_quality:
old_quality.nameChanged.disconnect(self._onQualityNameChanged)
else:
Logger.log("w", "Could not find old quality while changing active quality.")
old_changes = stack.findContainer(type = "quality_changes")
if old_changes:
old_changes.nameChanged.disconnect(self._onQualityNameChanged)
else:
Logger.log("w", "Could not find old quality_changes while changing active quality.")
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
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.
result.append({"stack": global_container_stack, "quality": result[0]["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")
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:
# Duplicate the quality from the 1st extruder into the global stack. If anyone
# then looks in the global stack, they should get a reasonable view.
result.append({"stack": global_container_stack, "quality": result[0]["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
@ -733,18 +878,15 @@ class MachineManager(QObject):
# \param definition (DefinitionContainer) machine definition
# \returns DefinitionID (string) if found, empty string otherwise
def getQualityDefinitionId(self, definition):
definition_id = definition.getMetaDataEntry("quality_definition")
if not definition_id:
definition_id = definition.getId()
return definition_id
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._global_container_stack:
variant = self._global_container_stack.findContainer({"type": "variant"})
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 ""
@ -847,14 +989,13 @@ class MachineManager(QObject):
def _updateVariantContainer(self, definition):
if not definition.getMetaDataEntry("has_variants"):
return self._empty_variant_container
machine_definition_id = UM.Application.getInstance().getMachineManager().getQualityDefinitionId(definition)
containers = []
preferred_variant = definition.getMetaDataEntry("preferred_variant")
if preferred_variant:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id, id = preferred_variant)
if not containers:
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id)
if containers:
return containers[0]

View file

@ -0,0 +1,42 @@
# Copyright (c) 2016 Ultimaker B.V.
# Uranium is released under the terms of the AGPLv3 or higher.
from UM.Application import Application
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
from cura.QualityManager import QualityManager
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.MachineManager import MachineManager
## QML Model for listing the current list of valid quality profiles.
#
class ProfilesModel(InstanceContainersModel):
def __init__(self, parent = None):
super().__init__(parent)
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)
## 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)

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

@ -16,8 +16,9 @@ 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)
@ -25,13 +26,15 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
self._container_registry = UM.Settings.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")
@ -46,30 +49,41 @@ 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
items = []
@ -77,9 +91,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
settings = collections.OrderedDict()
definition_container = UM.Application.getInstance().getGlobalContainerStack().getBottom()
containers = self._container_registry.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
@ -92,13 +106,10 @@ 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 = 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())
@ -110,8 +121,8 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
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
@ -122,9 +133,9 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
new_criteria.pop("extruder")
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")
criteria.pop("material", None)
containers = self._container_registry.findInstanceContainers(**criteria)
if not containers:
@ -133,13 +144,14 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
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, "definition": definition_id, "name": quality_changes_container.getName()}
if self._extruder_id != "":
criteria["extruder"] = self._extruder_id
criteria = {"type": "quality_changes", "quality_type": quality_type, "definition": definition_id, "name": quality_changes_container.getName()}
if self._extruder_definition_id != "":
criteria["extruder"] = self._extruder_definition_id
criteria["name"] = quality_changes_container.getName()
else:
criteria["extruder"] = None
@ -157,9 +169,11 @@ 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 is not None:
profile_value_source = container.getMetaDataEntry("type")
profile_value = new_value
user_value = None
@ -182,11 +196,13 @@ class QualitySettingsModel(UM.Qt.ListModel.ListModel):
# 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
})

View file

@ -0,0 +1,180 @@
# 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
## 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)
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 = new_active_stack
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
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):
has_setting_function = False
stack = self._active_container_stack
containers = []
## Check if the setting has a user state. If not, it is never overwritten.
has_user_state = self._active_container_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 self._active_container_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(self._active_container_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")
if value is not None:
has_setting_function = isinstance(value, UM.Settings.SettingFunction)
if has_setting_function is False:
has_non_function_value = True
continue
except AttributeError:
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 = Application.getInstance().getGlobalContainerStack()
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._onActiveExtruderChanged()
@staticmethod
def createSettingInheritanceManager(engine=None, script_engine=None):
return SettingInheritanceManager()

View file

@ -61,6 +61,15 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def getActiveExtruder(self):
return self._extruder_stack
## 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

View file

@ -11,3 +11,7 @@ from .MachineManager import MachineManager
from .MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from .SettingOverrideDecorator import SettingOverrideDecorator
from .QualitySettingsModel import QualitySettingsModel
from .SettingInheritanceManager import SettingInheritanceManager
from .ProfilesModel import ProfilesModel
from .QualityAndUserProfilesModel import QualityAndUserProfilesModel
from .UserProfilesModel import UserProfilesModel

View file

@ -8,9 +8,11 @@ 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 UM.Math.Quaternion import Quaternion
import math
import zipfile
@ -24,104 +26,141 @@ 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"
}
def _createNodeFromObject(self, object, name = ""):
node = SceneNode()
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)
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()
# If this object has no vertices and just one child, just return the child.
if len(vertex_list) == 0 and len(node.getChildren()) == 1:
return node.getChildren()[0]
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()
# Rotate the model; We use a different coordinate frame.
rotation = Matrix()
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
flip_matrix = Matrix()
flip_matrix._data[1, 1] = 0
flip_matrix._data[1, 2] = 1
flip_matrix._data[2, 1] = 1
flip_matrix._data[2, 2] = 0
# TODO: We currently do not check for normals and simply recalculate them.
mesh_builder.calculateNormals()
mesh_builder.setFileName(name)
mesh_data = mesh_builder.build().getTransformed(flip_matrix)
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]
flip_matrix = Matrix()
flip_matrix._data[1, 1] = 0
flip_matrix._data[1, 2] = 1
flip_matrix._data[2, 1] = 1
flip_matrix._data[2, 2] = 0
temp_mat.multiply(flip_matrix)
return temp_mat
def read(self, file_name):
result = SceneNode()
# The base object of 3mf is a zipped archive.
archive = zipfile.ZipFile(file_name, "r")
try:
root = ET.parse(archive.open("3D/3dmodel.model"))
self._root = ET.parse(archive.open("3D/3dmodel.model"))
# There can be multiple objects, try to load all of them.
objects = root.findall("./3mf:resources/3mf:object", self._namespaces)
if len(objects) == 0:
Logger.log("w", "No objects found in 3MF file %s, either the file is corrupt or you are using an outdated format", file_name)
return None
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)
build_item_node = self._createNodeFromObject(object)
transform = build_item.get("transform")
if transform is not None:
build_item_node.setTransformation(self._createMatrixFromTransformationString(transform))
result.addChild(build_item_node)
triangles = entry.findall(".//3mf:triangle", self._namespaces)
mesh_builder.reserveFaceCount(len(triangles))
for triangle in triangles:
v1 = int(triangle.get("v1"))
v2 = int(triangle.get("v2"))
v3 = int(triangle.get("v3"))
mesh_builder.addFaceByPoints(vertex_list[v1][0], vertex_list[v1][1], vertex_list[v1][2],
vertex_list[v2][0], vertex_list[v2][1], vertex_list[v2][2],
vertex_list[v3][0], vertex_list[v3][1], vertex_list[v3][2])
Job.yieldThread()
# Rotate the model; We use a different coordinate frame.
rotation = Matrix()
rotation.setByRotationAxis(-0.5 * math.pi, Vector(1, 0, 0))
# TODO: We currently do not check for normals and simply recalculate them.
mesh_builder.calculateNormals()
mesh_builder.setFileName(file_name)
node.setMeshData(mesh_builder.build().getTransformed(rotation))
node.setSelectable(True)
transformations = root.findall("./3mf:build/3mf:item[@objectid='{0}']".format(entry.get("id")), self._namespaces)
transformation = transformations[0] if transformations else None
if transformation is not None and transformation.get("transform"):
splitted_transformation = transformation.get("transform").split()
## Transformation is saved as:
## M00 M01 M02 0.0
## M10 M11 M12 0.0
## M20 M21 M22 0.0
## M30 M31 M32 1.0
## We switch the row & cols as that is how everyone else uses matrices!
temp_mat = Matrix()
# Rotation & Scale
temp_mat._data[0,0] = splitted_transformation[0]
temp_mat._data[1,0] = splitted_transformation[1]
temp_mat._data[2,0] = splitted_transformation[2]
temp_mat._data[0,1] = splitted_transformation[3]
temp_mat._data[1,1] = splitted_transformation[4]
temp_mat._data[2,1] = splitted_transformation[5]
temp_mat._data[0,2] = splitted_transformation[6]
temp_mat._data[1,2] = splitted_transformation[7]
temp_mat._data[2,2] = splitted_transformation[8]
# Translation
temp_mat._data[0,3] = splitted_transformation[9]
temp_mat._data[1,3] = splitted_transformation[10]
temp_mat._data[2,3] = splitted_transformation[11]
node.setTransformation(temp_mat)
result.addChild(node)
Job.yieldThread()
# If there is more then one object, group them.
if len(objects) > 1:
group_decorator = GroupDecorator()
result.addDecorator(group_decorator)
elif len(objects) == 1:
result = result.getChildren()[0] # Only one object found, return that.
except Exception as e:
Logger.log("e", "exception occured in 3mf reader: %s", e)
try: # Selftest - There might be more functions that should fail
boundingBox = result.getBoundingBox()
boundingBox.isValid()
try: # Selftest - There might be more functions that should fail
bounding_box = result.getBoundingBox()
bounding_box.isValid()
except:
return None
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
if global_container_stack:
translation = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2, y=0,
z=global_container_stack.getProperty("machine_depth", "value") / 2)
result.translate(translation, SceneNode.TransformSpace.World)
return result

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

@ -7,7 +7,7 @@ The first thing you will notice is the speed. STL loading is now 10 to 20 times
Machines with multiple extruders are now supported. If youve got the Ultimaker Original with the dual extrusion upgrade kit, weve got you covered.
*Custom Machine Support
The new custom machine plug-in allows you to change machine settings with ease. That means its now much easier to use Cura with custom machines.
Its now much easier to use Cura with custom machines. You can edit the machine settings when you load a new custom machine.
*Improved Position Tool
Place objects precisely where you want them by manually entering the values for the position.
@ -20,10 +20,10 @@ Select an individual item in a group or merged object and edit as usual. Just Ct
Profile management is improved. You can now easily see and track changes made to your profiles.
*Improved Setting Visibility
Make multiple settings visible at once. The Visibility Overview setting indicates why a setting is not shown in the sidebar even if it is selected.
Make multiple settings visible at once. 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.
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!
@ -35,14 +35,17 @@ Cura now estimates print weight as well as length.
Configurations from older installations of Cura 2.1 are automatically imported into the newest installation.
*Slicing features
*Infill Improvements
We've introduced two new infill types: Tetrahedral and Cubic. They change along with the Z-axis for more uniform strength in all directions. Also, now you can change the density of the infill based on the distance from the top layers. You print get faster, use less material, and maintain the same object strength.
*Infill Types
We've introduced two new infill types: Tetrahedral and Cubic. They change along with the Z-axis for more uniform strength in all directions. There are now 7 infill types to choose from.
*Gradual Infill
Now you can change the density of the infill based on the distance from the top layers. Your objects print faster, use less material, while top surfaces have the same quality.
*Set Acceleration and Jerk by Feature
You can now set Jerk and Acceleration by feature, (infill, walls, top/bottom, etc) for more precision.
You can now set Jerk and Acceleration by feature-type (infill, walls, top/bottom, etc), for more precision.
*Outer Wall Offset
Apply an offset to where the outer wall is printed for better surface quality when the outer wall line width is smaller than the nozzle size.
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.
@ -56,42 +59,46 @@ The Skin Overlap setting allows you to overlap the skin lines with the walls for
*Control 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 Bottoms
This new feature duplicates the Support Roofs feature in the places where the support rests on the model.
*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 & minor changes
Deleting grouped objects works as intended again.
Duplicating groups works as intended again.
Bridging works as intended again.
*Bug fixes
Deleting grouped objects
Duplicating groups
Bridging
Drag and drop on the first run on Windows
Unretraction speeds
Bottom layer in Spiralize mode
Overlap Compensation
Retractions on Raft
Retractions now occur after each object printed in one-at-a-time mode.
Rafts are no longer printed outside of build area.
Messages are now displayed 30 seconds instead of 10, making it less likely that certain messages are missed.
Drag and drop on the first run on windows works again.
You are now notified if you try to save to a locked SD card.
Combing is applied in more cases and results in better paths.
Infill thickness now supports Grid infill also for even multiples of the layer height.
Unretraction speeds are correct again.
Spiralize mode doesnt spiralize the bottom layer any more.
Spiralize now spiralizes each part in a layer.
Support is no longer removed by unprintable thin parts of the model.
Support is now generated on each layer its supposed to.
Support doesn't go outside overhang areas any more.
Spiralize no longer only spiralizes 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.
Overlap Compensation works as intended again.
A raft now produces a reasonable amount of retractions.
No more string plume on top of one-at-a-time printed objects.
Engine log can be viewed even when slicing is finished.
Support roofs now only occur just below overhang.
Brim is now also generated under the support.
Compensate overlapping wall parts now also works for inner walls.
Bed Level and Checkup procedures for UMO+ can now be done without re-adding machine.
*Minor changes
Messages are now displayed 30 seconds instead of 10, making it less likely that certain messages are missed.
You are now notified if you try to save to a locked SD card.
Engine log is now included in the application log.
Undo and Redo now work correctly with multiple operations.
The last used folder is now remembered (instead of defaulting to home folder)
You can now adjust the speed at which the bed is lowered each layer.
Support roofs now only generate directly below the mesh.
Settings shared between skirt and brim now also activate when brim is selected.
The last used folder is now remembered (instead of 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 now be done without re-adding machine.
Combing is 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 is now generated on each layer its supposed to.
Support doesn't go outside overhang areas any more.
Support doesn't remove brim around the object any more.
Brim is now also generated under the support.
Draft shield 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.
You can now adjust the speed at which the bed is lowered each layer.
[2.1.3]

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 limit_to_extruder = 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

View file

@ -230,7 +230,7 @@ class CuraEngineBackend(Backend):
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
if Application.getInstance().getPlatformActivity:
self._error_message = Message(catalog.i18nc("@info:status",
"The selected material is imcompatible with the selected machine or configuration."))
"The selected material is incompatible with the selected machine or configuration."))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
else:
@ -336,6 +336,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 = []
@ -411,6 +412,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:
@ -463,3 +465,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

@ -160,7 +160,13 @@ 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.
verts = numpy.array([verts[vert_index] for face in indices for vert_index in face])
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]]
@ -198,6 +204,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:

View file

@ -98,6 +98,11 @@ class GCodeWriter(MeshWriter):
return ""
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}
@ -106,9 +111,15 @@ class GCodeWriter(MeshWriter):
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
@ -28,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._ghost_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
@ -47,7 +46,19 @@ class LayerView(View):
self._top_layers_job = None
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)
@ -55,13 +66,21 @@ class LayerView(View):
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
@ -91,7 +110,7 @@ class LayerView(View):
if not self._ghost_shader:
self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._ghost_shader.setUniformValue("u_color", Color(0, 0, 0, 64))
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.
@ -101,35 +120,7 @@ class LayerView(View):
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
renderer.queueNode(node,
shader = self._ghost_shader,
type = RenderBatch.RenderType.Transparent )
for node in DepthFirstIterator(scene.getRoot()):
if type(node) is SceneNode:
if 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._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:
@ -163,7 +154,7 @@ class LayerView(View):
# The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
# if it's the largest value. If we don't do this, we can have a slider block outside of the
# slider.
# slider.
if new_max_layers > self._current_layer_num:
self.maxLayersChanged.emit()
self.setLayer(int(self._max_layers))
@ -176,7 +167,7 @@ class LayerView(View):
currentLayerNumChanged = Signal()
## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created
# as this caused some issues.
# as this caused some issues.
def getProxy(self, engine, script_engine):
return self._proxy
@ -195,15 +186,32 @@ class LayerView(View):
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)
@ -315,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

@ -66,8 +66,14 @@ class LegacyProfileReader(ProfileReader):
def read(self, file_name):
if file_name.split(".")[-1] != "ini":
return None
multi_extrusion = Application.getInstance().getGlobalContainerStack().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:
@ -130,5 +136,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", "normal")
return profile

View file

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

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.limit_to_extruder 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, "limit_to_extruder"))))) #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 limit_to_extruder 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.getRawProperty(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")
@ -151,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
@ -163,6 +164,7 @@ Item {
ListView
{
id: contents
spacing: UM.Theme.getSize("default_lining").height
model: UM.SettingDefinitionsModel
{
@ -260,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()
}
}
}
}

View file

@ -8,6 +8,7 @@ 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.
@ -31,6 +32,9 @@ class PerObjectSettingsTool(Tool):
self._onGlobalContainerChanged()
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):
@ -80,11 +84,20 @@ class PerObjectSettingsTool(Tool):
default_stack = ExtruderManager.getInstance().getExtruderStack(0)
if default_stack:
default_stack_id = default_stack.getId()
else: default_stack_id = global_container_stack.getId()
else:
default_stack_id = global_container_stack.getId()
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
node.callDecoration("setActiveExtruder", default_stack_id)
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()

View file

@ -23,6 +23,7 @@ class RemovableDriveOutputDevice(OutputDevice):
self.setPriority(1)
self._writing = False
self._stream = None
def requestWrite(self, node, file_name = None, filter_by_machine = False):
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
@ -65,8 +66,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)
job = WriteMeshJob(writer, self._stream, node, MeshWriter.OutputMode.TextMode)
job.setFileName(file_name)
job.progress.connect(self._onProgress)
job.finished.connect(self._onFinished)
@ -92,6 +94,11 @@ class RemovableDriveOutputDevice(OutputDevice):
self.writeProgress.emit(self, progress)
def _onFinished(self, job):
if self._stream:
# Explicitly closing the stream flushes the write-buffer
self._stream.close()
self._stream = None
if hasattr(job, "_message"):
job._message.hide()
job._message = None
@ -113,4 +120,9 @@ class RemovableDriveOutputDevice(OutputDevice):
def _onActionTriggered(self, message, action):
if action == "eject":
if Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("RemovableDriveOutputDevice").ejectDevice(self):
message.hide()
message.hide()
eject_message = Message(catalog.i18nc("@info:status", "Ejected {0}. You can now safely remove the drive.").format(self.getName()))
else:
eject_message = Message(catalog.i18nc("@info:status", "Failed to eject {0}. Another program may be using the drive.").format(self.getName()))
eject_message.show()

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}. Another program may be using the drive.").format(device.getName()))
message.show()
return result
def performEjectDevice(self, device):

View file

@ -21,6 +21,7 @@ import urllib.request
import urllib.parse
import ssl
import hashlib
import json
catalog = i18nCatalog("cura")
@ -61,7 +62,7 @@ class SliceInfoJob(Job):
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
# no model files are being sent (Just a SHA256 hash of the model).
class SliceInfo(Extension):
info_url = "http://stats.youmagine.com/curastats/slice"
info_url = "https://stats.youmagine.com/curastats/slice"
def __init__(self):
super().__init__()
@ -101,21 +102,11 @@ class SliceInfo(Extension):
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)
# 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": modelhash_formatted,
"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:
@ -123,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")
@ -139,4 +142,4 @@ class SliceInfo(Extension):
except Exception as e:
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.

View file

@ -71,8 +71,9 @@ class SolidView(View):
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" })
@ -87,13 +88,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

@ -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.
@ -84,16 +88,35 @@ class MachineInstance:
active_quality_changes = "empty_quality_changes"
else:
active_quality = VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.getQualityFallback(type_name, variant, active_material)
if has_machine_qualities: #Then the profile will have split into multiple.
active_quality_changes = self._active_profile_name + "_" + active_material + "_" + variant
else:
active_quality_changes = self._active_profile_name
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.
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") as file_handle:
user_profile.write(file_handle)
version_upgrade_manager.upgradeExtraFile(user_storage, urllib.parse.quote_plus(self._name), "user")
containers = [
"", #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.
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

@ -49,7 +49,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)
@ -94,7 +94,7 @@ 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")
config.set("metadata", "quality", "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", "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))
@ -105,13 +105,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))
@ -124,34 +124,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

@ -398,6 +398,9 @@ class VersionUpgrade21to22(VersionUpgrade):
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

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];
@ -62,7 +58,6 @@ fragment =
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

@ -189,7 +189,8 @@ class XmlMaterialProfile(UM.Settings.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})

View file

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

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

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

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

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

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

@ -42,7 +42,7 @@
"default_value": 55
},
"machine_gcode_flavor": {
"default_value": "RepRap"
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_disallowed_areas": {
"default_value": []
@ -144,7 +144,7 @@
"default_value": 15
},
"adhesion_type": {
"default_value": "Raft"
"default_value": "raft"
},
"skirt_brim_minimal_length": {
"default_value": 100

View file

@ -88,16 +88,13 @@
"default_value": 0
},
"skirt_line_count": {
"default_value": 3,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 3
},
"skirt_gap": {
"default_value": 4,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 4
},
"skirt_brim_minimal_length": {
"default_value": 200,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 200
}
}
}

View file

@ -91,16 +91,13 @@
"default_value": 0
},
"skirt_line_count": {
"default_value": 3,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 3
},
"skirt_gap": {
"default_value": 4,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 4
},
"skirt_brim_minimal_length": {
"default_value": 200,
"enabled": "adhesion_type == \"Skirt\""
"default_value": 200
}
}
}

View file

@ -20,6 +20,16 @@
},
"material_standby_temperature": {
"minimum_value": "0"
},
"extruder_prime_pos_y":
{
"minimum_value": "0",
"maximum_value": "machine_depth"
},
"extruder_prime_pos_x":
{
"minimum_value": "0",
"maximum_value": "machine_width"
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Gerät"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Gerätespezifische Einstellungen"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Extruder"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "Die für das Drucken verwendete Extruder-Einheit. Diese wird für die Mehrfach-Extrusion benutzt."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "X-Versatz Düse"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "Die X-Koordinate des Düsenversatzes."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Y-Versatz Düse"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "Die Y-Koordinate des Düsenversatzes."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "G-Code Extruder-Start"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Starten Sie den G-Code jedes Mal, wenn Sie den Extruder einschalten."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Absolute Startposition des Extruders"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Bevorzugen Sie eine absolute Startposition des Extruders anstelle einer relativen Position zur zuletzt bekannten Kopfposition."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "X-Position Extruder-Start"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "Die X-Koordinate der Startposition beim Einschalten des Extruders."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Y-Position Extruder-Start"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "Die Y-Koordinate der Startposition beim Einschalten des Extruders."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "G-Code Extruder-Ende"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Beenden Sie den G-Code jedes Mal, wenn Sie den Extruder ausschalten."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Absolute Extruder-Endposition"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Bevorzugen Sie eine absolute Endposition des Extruders anstelle einer relativen Position zur zuletzt bekannten Kopfposition."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Extruder-Endposition X"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "Die X-Koordinate der Endposition beim Ausschalten des Extruders."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Extruder-Endposition Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "Die Y-Koordinate der Endposition beim Ausschalten des Extruders."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Z-Position Extruder-Einzug"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Die Z-Koordinate der Position, an der die Düse am Druckbeginn einzieht."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Druckplattenhaftung"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Haftung"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "X-Position Extruder-Einzug"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Die X-Koordinate der Position, an der die Düse am Druckbeginn einzieht."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Y-Position Extruder-Einzug"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Die Y-Koordinate der Position, an der die Düse am Druckbeginn einzieht."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -953,7 +953,7 @@ msgid "Cancel"
msgstr "Cancel"
#: /home/ruben/Projects/Cura/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml:34
msgctxt "@label"
msgctxt "@label Followed by extruder selection drop-down."
msgid "Print model with"
msgstr "Print model with"
@ -1852,12 +1852,12 @@ msgstr ""
"Click to make these settings visible."
#: /home/ruben/Projects/Cura/resources/qml/Settings/SettingItem.qml:59
msgctxt "@label"
msgctxt "@label Header for list of settings."
msgid "Affects"
msgstr "Affects"
#: /home/ruben/Projects/Cura/resources/qml/Settings/SettingItem.qml:64
msgctxt "@label"
msgctxt "@label Header for list of settings."
msgid "Affected By"
msgstr "Affected By"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Máquina"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Ajustes específicos de la máquina"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Extrusor"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "El tren extrusor que se utiliza para imprimir. Se emplea en la extrusión múltiple."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Desplazamiento de la tobera sobre el eje X"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "Coordenada X del desplazamiento de la tobera."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Desplazamiento de la tobera sobre el eje Y"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "Coordenada Y del desplazamiento de la tobera."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "Gcode inicial del extrusor"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Gcode inicial que se ejecuta cada vez que se enciende el extrusor."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Posición de inicio absoluta del extrusor"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "El extrusor se coloca en la posición de inicio absoluta según la última ubicación conocida del cabezal."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "Posición de inicio del extrusor sobre el eje X"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "Coordenada X de la posición de inicio cuando se enciende el extrusor."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Posición de inicio del extrusor sobre el eje Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "Coordenada Y de la posición de inicio cuando se enciende el extrusor."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "Gcode final del extrusor"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Gcode final que se ejecuta cada vez que se apaga el extrusor."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Posición final absoluta del extrusor"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "La posición final del extrusor se considera absoluta, en lugar de relativa a la última ubicación conocida del cabezal."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Posición de fin del extrusor sobre el eje X"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "Coordenada X de la posición de fin cuando se apaga el extrusor."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Posición de fin del extrusor sobre el eje Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "Coordenada Y de la posición de fin cuando se apaga el extrusor."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Posición de preparación del extrusor sobre el eje Z"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Coordenada Z de la posición en la que la tobera queda preparada al inicio de la impresión."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Adherencia de la placa de impresión"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Adherencia"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "Posición de preparación del extrusor sobre el eje X"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Coordenada X de la posición en la que la tobera se coloca al inicio de la impresión."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Posición de preparación del extrusor sobre el eje Y"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Coordenada X de la posición en la que la tobera se coloca al inicio de la impresión."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 14:48+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr ""
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr ""
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr ""
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr ""
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr ""

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Laite"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Laitekohtaiset asetukset"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Suulake"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "Tulostukseen käytettävä suulakeryhmä. Tätä käytetään monipursotuksessa."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Suuttimen X-siirtymä"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "Suuttimen siirtymän X-koordinaatti."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Suuttimen Y-siirtymä"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "Suuttimen siirtymän Y-koordinaatti."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "Suulakkeen aloitus-GCode"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Aloitus-GCode, joka suoritetaan suulakkeen käynnistyksen yhteydessä."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Suulakkeen aloitussijainti absoluuttinen"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Tekee suulakkeen aloitussijainnista absoluuttisen eikä suhteellisen viimeksi tunnettuun pään sijaintiin nähden."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "Suulakkeen aloitussijainti X"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "Aloitussijainnin X-koordinaatti suulaketta käynnistettäessä."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Suulakkeen aloitussijainti Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "Aloitussijainnin Y-koordinaatti suulaketta käynnistettäessä."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "Suulakkeen lopetus-GCode"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Lopetus-GCode, joka suoritetaan, kun suulake poistetaan käytöstä."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Suulakkeen lopetussijainti absoluuttinen"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Tekee suulakkeen lopetussijainnista absoluuttisen eikä suhteellisen viimeksi tunnettuun pään sijaintiin nähden."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Suulakkeen lopetussijainti X"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "Lopetussijainnin X-koordinaatti, kun suulake poistetaan käytöstä."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Suulakkeen lopetussijainti Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "Lopetussijainnin Y-koordinaatti, kun suulake poistetaan käytöstä."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Suulakkeen esitäytön Z-sijainti"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Z-koordinaatti sijainnille, jossa suutin esitäytetään tulostusta aloitettaessa."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Alustan tarttuvuus"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Tarttuvuus"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "Suulakkeen esitäytön X-sijainti"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "X-koordinaatti sijainnille, jossa suutin esitäytetään tulostusta aloitettaessa."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Suulakkeen esitäytön Y-sijainti"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Y-koordinaatti sijainnille, jossa suutin esitäytetään tulostusta aloitettaessa."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Machine"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Paramètres spécifiques de la machine"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Extrudeuse"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "Le train d'extrudeuse utilisé pour l'impression. Cela est utilisé en multi-extrusion."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Buse Décalage X"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "Les coordonnées X du décalage de la buse."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Buse Décalage Y"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "Les coordonnées Y du décalage de la buse."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "Extrudeuse G-Code de démarrage"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "G-Code de démarrage à exécuter à chaque mise en marche de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Extrudeuse Position de départ absolue"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Rendre la position de départ de l'extrudeuse absolue plutôt que relative à la dernière position connue de la tête."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "Extrudeuse Position de départ X"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "Les coordonnées X de la position de départ lors de la mise en marche de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Extrudeuse Position de départ Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "Les coordonnées Y de la position de départ lors de la mise en marche de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "Extrudeuse G-Code de fin"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "G-Code de fin à exécuter à chaque arrêt de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Extrudeuse Position de fin absolue"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Rendre la position de fin de l'extrudeuse absolue plutôt que relative à la dernière position connue de la tête."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Extrudeuse Position de fin X"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "Les coordonnées X de la position de fin lors de l'arrêt de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Extrudeuse Position de fin Y"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "Les coordonnées Y de la position de fin lors de l'arrêt de l'extrudeuse."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Extrudeuse Position d'amorçage Z"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Les coordonnées Z de la position à laquelle la buse s'amorce au début de l'impression."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Adhérence du plateau"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Adhérence"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "Extrudeuse Position d'amorçage X"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Les coordonnées X de la position à laquelle la buse s'amorce au début de l'impression."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Extrudeuse Position d'amorçage Y"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Les coordonnées Y de la position à laquelle la buse s'amorce au début de l'impression."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Macchina"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Impostazioni macchina specifiche"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Estrusore"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "Treno estrusore utilizzato per la stampa. Utilizzato nellestrusione multipla."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Offset X ugello"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "La coordinata y delloffset dellugello."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Offset Y ugello"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "La coordinata y delloffset dellugello."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "Codice G avvio estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Codice G di avvio da eseguire ogniqualvolta si accende lestrusore."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Assoluto posizione avvio estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Rende la posizione di partenza estrusore assoluta anziché relativa rispetto allultima posizione nota della testina."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "X posizione avvio estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "La coordinata x della posizione di partenza allaccensione dellestrusore."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Y posizione avvio estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "La coordinata y della posizione di partenza allaccensione dellestrusore."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "Codice G fine estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Codice G di fine da eseguire ogniqualvolta si spegne lestrusore."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Assoluto posizione fine estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Rende la posizione di fine estrusore assoluta anziché relativa rispetto allultima posizione nota della testina."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Posizione X fine estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "La coordinata x della posizione di fine allo spegnimento dellestrusore."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Posizione Y fine estrusore"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "La coordinata y della posizione di fine allo spegnimento dellestrusore."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Posizione Z innesco estrusore"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Indica la coordinata Z della posizione in cui lugello si innesca allavvio della stampa."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Adesione piano di stampa"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Adesione"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "Posizione X innesco estrusore"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "La coordinata X della posizione in cui lugello si innesca allavvio della stampa."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Posizione Y innesco estrusore"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "La coordinata Y della posizione in cui lugello si innesca allavvio della stampa."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Machine"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Instellingen van de machine"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Extruder"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "De extruder train die voor het printen wordt gebruikt. Deze wordt gebruikt in meervoudige doorvoer."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Offset X-nozzle"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "De X-coördinaat van de offset van de nozzle."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Offset Y-nozzle"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "De Y-coördinaat van de offset van de nozzle."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "G-code van extruderstart"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Start-g-code die wordt uitgevoerd wanneer de extruder wordt ingeschakeld."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Absolute startpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Maak van de startpositie van de extruder de absolute startpositie, in plaats van de relatieve startpositie ten opzichte van de laatst bekende locatie van de printkop."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "X-startpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "De X-coördinaat van de startpositie wanneer de extruder wordt ingeschakeld."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Y-startpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "De Y-coördinaat van de startpositie wanneer de extruder wordt ingeschakeld."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "G-code van extrudereinde"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Eind-g-code die wordt uitgevoerd wanneer de extruder wordt uitgeschakeld."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Absolute eindpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Maak van de eindpositie van de extruder de absolute eindpositie, in plaats van de relatieve eindpositie ten opzichte van de laatst bekende locatie van de printkop."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "X-eindpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "De X-coördinaat van de eindpositie wanneer de extruder wordt uitgeschakeld."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Y-eindpositie extruder"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "De Y-coördinaat van de eindpositie wanneer de extruder wordt uitgeschakeld."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Z-positie voor primen extruder"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "De Z-coördinaat van de positie waar filament in de nozzle wordt teruggeduwd aan het begin van het printen."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Hechting aan platform"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Hechting"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "X-positie voor primen extruder"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "De X-coördinaat van de positie waar filament in de nozzle wordt teruggeduwd aan het begin van het printen."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Y-positie voor primen extruder"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "De Y-coördinaat van de positie waar filament in de nozzle wordt teruggeduwd aan het begin van het printen."

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

2335
resources/i18n/tr/cura.po Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,182 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: Uranium json setting files\n"
"Report-Msgid-Bugs-To: http://github.com/ultimaker/uranium\n"
"POT-Creation-Date: 2016-09-20 17:35+0000\n"
"PO-Revision-Date: 2016-09-29 13:02+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: fdmextruder.def.json
msgctxt "machine_settings label"
msgid "Machine"
msgstr "Makine"
#: fdmextruder.def.json
msgctxt "machine_settings description"
msgid "Machine specific settings"
msgstr "Makine özel ayarları"
#: fdmextruder.def.json
msgctxt "extruder_nr label"
msgid "Extruder"
msgstr "Ekstruder"
#: fdmextruder.def.json
msgctxt "extruder_nr description"
msgid "The extruder train used for printing. This is used in multi-extrusion."
msgstr "Yazdırma için kullanılan ekstruder dişli çark. Çoklu ekstrüzyon işlemi için kullanılır."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x label"
msgid "Nozzle X Offset"
msgstr "Nozül NX Ofseti"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_x description"
msgid "The x-coordinate of the offset of the nozzle."
msgstr "Nozül ofsetinin x koordinatı."
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y label"
msgid "Nozzle Y Offset"
msgstr "Nozül Y Ofseti"
#: fdmextruder.def.json
msgctxt "machine_nozzle_offset_y description"
msgid "The y-coordinate of the offset of the nozzle."
msgstr "Nozül ofsetinin y koordinatı."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code label"
msgid "Extruder Start G-Code"
msgstr "Ekstruder G-Code'u başlatma"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_code description"
msgid "Start g-code to execute whenever turning the extruder on."
msgstr "Ekstruderi her açtığınızda g-code'u başlatın."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs label"
msgid "Extruder Start Position Absolute"
msgstr "Ekstruderin Mutlak Başlangıç Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_abs description"
msgid ""
"Make the extruder starting position absolute rather than relative to the "
"last-known location of the head."
msgstr "Ekstruder başlama konumunu, yazıcı başlığının son konumuna göre ayarlamak yerine mutlak olarak ayarlayın."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x label"
msgid "Extruder Start Position X"
msgstr "Ekstruder X Başlangıç Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_x description"
msgid "The x-coordinate of the starting position when turning the extruder on."
msgstr "Ekstruder açılırken başlangıç konumunun x koordinatı."
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y label"
msgid "Extruder Start Position Y"
msgstr "Ekstruder Y Başlangıç Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_start_pos_y description"
msgid "The y-coordinate of the starting position when turning the extruder on."
msgstr "Ekstruder açılırken başlangıç konumunun Y koordinatı."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code label"
msgid "Extruder End G-Code"
msgstr "Ekstruder G-Code'u Sonlandırma"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_code description"
msgid "End g-code to execute whenever turning the extruder off."
msgstr "Ekstruderi her kapattığınızda g-code'u sonlandırın."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs label"
msgid "Extruder End Position Absolute"
msgstr "Ekstruderin Mutlak Bitiş Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_abs description"
msgid ""
"Make the extruder ending position absolute rather than relative to the last-"
"known location of the head."
msgstr "Ekstruder bitiş konumunu, yazıcı başlığının son konumuna göre ayarlamak yerine mutlak olarak ayarlayın."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x label"
msgid "Extruder End Position X"
msgstr "Ekstruderin X Bitiş Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_x description"
msgid "The x-coordinate of the ending position when turning the extruder off."
msgstr "Ekstruder kapatılırken bitiş konumunun x koordinatı."
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y label"
msgid "Extruder End Position Y"
msgstr "Ekstruderin Y Bitiş Konumu"
#: fdmextruder.def.json
msgctxt "machine_extruder_end_pos_y description"
msgid "The y-coordinate of the ending position when turning the extruder off."
msgstr "Ekstruder kapatılırken bitiş konumunun Y koordinatı."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z label"
msgid "Extruder Prime Z Position"
msgstr "Ekstruder İlk Z konumu"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_z description"
msgid ""
"The Z coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Nozül yazdırma işlemini başlatmaya hazırlandığında konumun Z koordinatı."
#: fdmextruder.def.json
msgctxt "platform_adhesion label"
msgid "Build Plate Adhesion"
msgstr "Yapı Levhası Yapıştırması"
#: fdmextruder.def.json
msgctxt "platform_adhesion description"
msgid "Adhesion"
msgstr "Yapıştırma"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x label"
msgid "Extruder Prime X Position"
msgstr "Extruder İlk X konumu"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_x description"
msgid ""
"The X coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Nozül yazdırma işlemini başlatmaya hazırlandığında konumun X koordinatı."
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y label"
msgid "Extruder Prime Y Position"
msgstr "Extruder İlk Y konumu"
#: fdmextruder.def.json
msgctxt "extruder_prime_pos_y description"
msgid ""
"The Y coordinate of the position where the nozzle primes at the start of "
"printing."
msgstr "Nozül yazdırma işlemini başlatmaya hazırlandığında konumun Y koordinatı."

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ Generic Nylon profile. Serves as an example file, data in this file is not corre
<material>Nylon</material>
<color>Generic</color>
</name>
<GUID>35ebb8df-66d2-41cd-9662-b7d96b9c2cbd</GUID>
<GUID>28fb4162-db74-49e1-9008-d05f1e8bef5c</GUID>
<version>1</version>
<color_code>#3DF266</color_code>
</metadata>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Generic Nylon profile. Serves as an example file, data in this file is not correct.
-->
<fdmmaterial xmlns="http://www.ultimaker.com/material">
<metadata>
<name>
<brand>Ultimaker</brand>
<material>Nylon</material>
<color>Black</color>
</name>
<GUID>c64c2dbe-5691-4363-a7d9-66b2dc12837f</GUID>
<version>1</version>
<color_code>#000000</color_code>
</metadata>
<properties>
<density>1.14</density>
<diameter>2.85</diameter>
</properties>
<settings>
<setting key="print temperature">250</setting>
<setting key="heated bed temperature">60</setting>
<setting key="standby temperature">175</setting>
</settings>
</fdmmaterial>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Generic Nylon profile. Serves as an example file, data in this file is not correct.
-->
<fdmmaterial xmlns="http://www.ultimaker.com/material">
<metadata>
<name>
<brand>Ultimaker</brand>
<material>Nylon</material>
<color>Transparent</color>
</name>
<GUID>e256615d-a04e-4f53-b311-114b90560af9</GUID>
<version>1</version>
<color_code>#FFFFFF</color_code>
</metadata>
<properties>
<density>1.14</density>
<diameter>2.85</diameter>
</properties>
<settings>
<setting key="print temperature">250</setting>
<setting key="heated bed temperature">60</setting>
<setting key="standby temperature">175</setting>
</settings>
</fdmmaterial>

Some files were not shown because too many files have changed in this diff Show more